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