How to push subdocument to Mongoose Model with Typescript?

281 Views Asked by At

in my NestJS app I have currently an Product schema and a payment method schema as subdocument in the product schema.

@Schema()
export class Product {
  @Prop({ required: true })
  name: string;

  @Prop([PaymentDetails])
  payments: PaymentDetails[];

}

@Schema()
export class PaymentDetails {
  @Prop()
  method: String;

  @Prop({ type: Boolean, default: false })
  isDefault: Boolean;
}

Now for validation purpose I created a DTO for the payment method. When I want to add the payment method to my product, then the push()-methods expects an instance of PaymentDetails[]. How to use my DTO now to create/push a new entry?

async addPayment(user: IUser, productId: string, payment: PaymentMethodDto) {
    const filter = { _id: productId };

    let p = await this.productModel.findOne<ProductDocument>(filter).exec();

    p.payments.push(`PaymentDetails[] expected`))

    return p.save();
}
2

There are 2 best solutions below

2
EcksDy On

I've read through your question and was a little confused by parts of it. Lets break it down and see if it's helpful.

Your schema definitions seem to be alright, although I think the more "correct" way is to get a schema for the sub-document class and pass that schema to the prop of the parent document as such:

@Schema()
export class PaymentDetails {
  ...
}
const PaymentDetailsSchema = SchemaFactory.createForClass(PaymentDetails);

export class Product {
  ...

  @Prop([PaymentDetailsSchema])
  /* @Prop({ type: [PaymentDetailsSchema] }) */
  payments: PaymentDetails[];
}

DTOs in nest serve as validators on the API level. Which means that inputs from outside(for example POST/PUT methods) are matching the shape of the relevant DTO. This happens via validation pipes that method payloads go through before they end up in your controller.

In nest convention there are 3 layers:

  • Controller - handles input/output to/from the service, authentication/authorization
  • Service - handles business logic
  • Data Access Layer (DAL) - handles data access/persistence

The control flow is Controller > Service > DAL > Your DB

What you tried to add to the DTO should be handled by the DAL. Your DAL can be either generic, having general CRUD methods for your various data, or it can be specific to your use case. It can be both.

You can use findByIdAndUpdate with a $push operator:

async addPayment(user: IUser, productId: string, payment: PaymentMethodDto) {
    const filter = { _id: productId };

    const productDoc = await this.productModel.findByIdAndUpdate(
      productId,
      {
        $push: { 'payments': payment } },
      },
      {
        new: true, // Will return the updated document, with the new payment added
        useFindAndModify: false, // You might not need this
      },
    );

    return productDoc.toObject();
}
0
Gerrit On

I solved the issue with:

@Prop([PaymentDetailsSchema])
payments: PaymentDetails[];

and

export type ProductDocument = HydratedDocument<
  Product,
  {
    payments: Types.DocumentArray<PaymentDetails>;
  }
>;

Afterwards doc.payments.id() is available.