mongoosepolymorphismnestjsmongoose-schema

NestJS - Nested polymorphism in sub schema


In NestJs, I am trying to let mongoose create a document based on a schema of which one of its properties can have different types. However, when I am saving the document, all the properties related to a specific type are lost. What am I missing? For simplicity reasons in this snippet I used an array of strings instead of typing out the enum

@Schema()
export class ClassAModel {
  @Prop({ type: ClassBSchema, required: true })
  object!: ClassB1Model | ClassB2Model
}

export const ClassASchema = SchemaFactory.createForClass(ClassAModel)

///
@Schema({ _id: false })
export class ClassB1Model  {
  @Prop({ enum: ['B1'], required: true  })
  type!: 'B1'
  
  @Prop({ required: true  })
  onlyForClassB1Model!: string
}

export const ClassB1Schema = SchemaFactory.createForClass(ClassB1Model)

///
@Schema({ _id: false })
export class ClassB2Model  {
  @Prop({ enum: ['B2'], required: true  })
  type!: 'B2'
  
  @Prop({ required: true })
  onlyForClassB2Model!: string
}

export const ClassB2Schema = SchemaFactory.createForClass(ClassB2Model)

///
@Schema({ _id: false, discriminatorKey: 'type' })
export class ClassBModel {
  @Prop({ enum: ['B1', 'B2'], required: true  })
  type!: 'B1' | 'B2'
}

export const ClassBSchema = SchemaFactory.createForClass(ClassBModel)

ClassBSchema.discriminators = {
  B1: ClassB1Schema,
  B2: ClassB2Schema,
}

When I try to save the document using the model instance, it only saved the props that are defined in ClassBModel (so only type). All other props are not being picked up.

 // some class method
 public async saveDoc(): Promise<void> {
   const payload = {
     object: {
       type: 'B2',
       onlyForClassB2Model: 'random string'
     }
   }
   return this.model.create(payload) // yields { object: { type: 'B2' } }
 }

I know that discriminators for top level documents can be defined as described in the NestJSDocs, using discrimimators in the nestjs module, but this is a different case where the discriminators are inside of the injected model. How can I make mongoose recognise that it needs to save all the properties from the payload?


Solution

  • Using the discriminators object on the subschemas will not link the discriminators to the base schema. You need to invoke the discriminator method on either the Schema.DocumentsArray or Schema. Embedded schemaType. Since Typescript cannot infer what schema type it is on its own using the createForClass method, you will have to explicitly declare that it involves an Embedded schema type (for simple objects and DocumentsArray for arrays). Discriminator method takes in the chosen key as discriminator and its associated schema.

    import { Schema as MongooseSchema } from 'mongoose'
    
    @Schema()
    export class ClassAModel {
      @Prop({ type: ClassBSchema, required: true })
      object!: ClassB1Model | ClassB2Model
    }
    
    export const ClassASchema = SchemaFactory.createForClass(ClassAModel)
    const classBSchema = ClassASchema.path<MongooseSchema.Types.Embedded>('type')
    classBSchema.discriminator('B1', ClassB1Schema)
    classBSchema.discriminator('B2', ClassB2Schema)
    

    Also remove the Prop decorator on the subtype schemas for the discriminator for mongo validation, as mongoose will complain that the subtypes cannot have the same key as the basetype

    ///
    @Schema({ _id: false })
    export class ClassB1Model  {
      type!: 'B1'
      
      @Prop({ required: true  })
      onlyForClassB1Model!: string
    }
    
    export const ClassB1Schema = SchemaFactory.createForClass(ClassB1Model)
    
    ///
    @Schema({ _id: false })
    export class ClassB2Model  {
      type!: 'B2'
      
      @Prop({ required: true })
      onlyForClassB2Model!: string
    }
    

    Finally, you can remove the discriminatos object on the subschema, but you still need to declare the discriminator key in the schema decorator

    @Schema({ _id: false, discriminatorKey: 'type' })
    export class ClassBModel {
      @Prop({ enum: ['B1', 'B2'], required: true  })
      type!: 'B1' | 'B2'
    }
    
    export const ClassBSchema = SchemaFactory.createForClass(ClassBModel)