3

As the title suggests, I am a little confused about working with pre-aggregated views on collections trough Mongoose. I'am able to create a view via the this.connection.createCollection() method -after injecting the connection option- but how should I start querying this view? Below option works but doesn't feel right.

const schema = new mongoose.Schema({ },{strict:false});
let NM = this.connection.model('view', schema,'newsOverview' ) ;
return NM.find()

Any help would be greatly appreciated! Nomis

Esteday
  • 41
  • 3

1 Answers1

1

I couldn't find a solid answer to this either so I threw this together. I would love a more "official" answer. Maybe like a sweet @View decorator or something?

// feature.module.ts
@Module({
  imports: [
    // views
    MongooseModule.forFeature([{ name: LogHydrated.name, schema: LogHydratedSchema }]),
    // collections
    MongooseModule.forFeature([{ name: Log.name, schema: LogSchema }]),  ],
  providers: [...]
})
export class FeatureModule {
  @InjectConnection() private readonly connection: Connection

  async onModuleInit (): Promise<void> {
    const collections = await this.connection.db.listCollections().toArray()
    if (collections.every(collection => collection.name !== 'logs_hydrated')) {
      await this.connection.db.createCollection('logs_hydrated', {
        viewOn: 'logs',
        pipeline: [/*aggregation pipeline here */]
      })
    }
  }
}

// log.schema.ts
@Schema({ collection: 'logs' })
export class Log extends Mongo implements MongoDoc {
  //...some props
}

export const LogSchema = SchemaFactory.createForClass(Log)
export type LogDocument = Log & Document

// autoCreate: false is what makes this work. The module creates the "view" collection
// on init and the model relies on the view collection always being present
@Schema({ collection: 'logs_hydrated', autoCreate: false })
export class LogHydrated extends Log {
  @Prop({ type: [LogField] })
  fields: LogField[]
}
export const LogHydratedSchema = SchemaFactory.createForClass(LogHydrated)
export type LogHydratedDocument = LogHydrated & Document

// feature.service.ts
export class LogService {
  constructor (
    @InjectModel(Log.name) private readonly logModel: Model<LogDocument>,
    @InjectModel(LogHydrated.name) private readonly logHydratedModel: Model<LogHydratedDocument>
  ) {}

  async findById (id: string): Promise<LogHydrated> {
    try {
      const model = await this.logHydratedModel.findById(id)
      if (model === null) {
        throw new HttpException(`Unable to find log by id: ${String(id)}`, HttpStatus.BAD_REQUEST)
      }
      return model
    } catch (error) {
      // handle error
    }
  }
}


Edit: I discovered a method to do this a bit less hacky. It doesn't actually create a view but it lets you populate virtual properties in which you can achieve the same effect.

// log.schema.ts
// notice the @Type decorator vs @Prop
class Log {
   ...
   
  @Type(() => LogField)
  fields: LogField[]
}
export const LogSchema = SchemaFactory.createForClass(Log)

LogSchema.virtual('fields', {
  ref: 'LogField',
  localField: 'fieldIds',
  foreignField: '_id'
})

// feature.service.ts
// call populate
const model = await this.LogModel.findById(id).populate({
  path: 'fields'
})

this one prevents the need for multiple models