NestJS - How to create nested schema with decorators

Viewed 21759

Let's say I want to build the below schema with mongoose:

const userSchema = new Schema({
  name: {
    firstName: String,
    lastName: String
  }
})

How can I do it with NestJS decorators (@Schema() & @Prop())?

I try this method, but no luck:

@Schema()
class Name {
  @Prop()
  firstName: string;

  @Prop()
  lastName: string;
}

@Schema()
class User extends Document {
  @Prop({ type: Name })
  name: Name;
}

I also don't want to use the raw() method.

7 Answers

Here's my method which works great and doesn't involve removing @schema():

// Nested Schema
@Schema()
export class BodyApi extends Document {
  @Prop({ required: true })
  type: string;

  @Prop()
  content: string;
}
export const BodySchema = SchemaFactory.createForClass(BodyApi);

// Parent Schema
@Schema()
export class ChaptersApi extends Document {
  // Array example
  @Prop({ type: [BodySchema], default: [] })
  body: BodyContentInterface[];

  // Single example
  @Prop({ type: BodySchema })
  body: BodyContentInterface;
}
export const ChaptersSchema = SchemaFactory.createForClass(ChaptersApi);

This saves correctly and shows the timestamps when you have that option set on your schema

Changes made:

  1. No @Schema decorator for the sub-document class
  2. Sub-document class needs to extend Document from 'mongoose'

user.schema.ts

import { Document } from 'mongoose';

@Schema()
export class User extends Document {
  @Prop({ type: Name })
  name: Name;
}

export const UserSchema = SchemaFactory.createForClass(User);

name.schema.ts

import { Document } from 'mongoose';

export class Name extends Document {
  @Prop({ default: " " })
  firstName: string;

  @Prop({ default: " " })
  lastName: string;
}

I haven't found this part of NestJS to be flexible enough. A working solution (tested) for me is the following:

@Schema({_id: false}) // _id:false is optional
class Name {
  @Prop() // any options will be evaluated
  firstName: string; // data type will be checked

  @Prop()
  lastName: string;
}

@Schema()
class User {
  @Prop({type: Name}) // {type: Name} can be omitted
  name: Name;
}

Defining your schemas this way will keep everything (class decorators, passed options, data types verification, NestJS functionalities, etc.) working as expected. The only "issue" is that _id properties will be created for each @Schema and you might not want that, like in your case. You can avoid that by adding {_id: false} as an options object to your @Schema(). Keep in mind, that any further nested schemas won't be prevented from creating _id properties, e.g.

This:

@Schema() // will create _id filed
class Father {
  age: number;
  name: string;
}

@Schema({_id: false}) // won't create _id field
class Parents {
  @Prop()
  father: Father;

  @Prop()
  mother: string;
}

@Schema()
class Person {
  @Prop()
  parents: Parents;
}

will produce this:

{
  _id: ObjectId('someIdThatMongoGenerated'),
  parents: {
    father: {
      _id: ObjectId('someIdThatMongoGenerated'),
      age: 40,
      name: Jon Doe
    },
    mother: Jane Doe
  }
}

The other workaround is to use native mongoose for creating your schemas in NestJS, like so:

const UserSchema = new mongoose.Schema({
  name: {
    firstName: {
      type: String, // note uppercase
      required: true // optional
    },
    lastName: {
      type: String,
      required: true
    }
  }
});

You can also use this.

    @Schema()
    class User extends Document {
      @Prop({ type:  { firstName: String, lastName: String })
      name: Name;
    }

Try to remove @schema() decorator from the nested "Name", leaving it only at the root of your document.

Also remember to extend 'mongoose.Document' at the root level.

import { Prop, Schema, SchemaFactory, } from '@nestjs/mongoose';
import { Document  } from 'mongoose';
    
class Name {
  @Prop()
  firstName: string;

  @Prop()
  lastName: string;
}

@Schema()
class User extends Document {
  @Prop({ type: Name })
  name: Name;
}
export const userSchema = SchemaFactory.createForClass(user);

Top-Level Document

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { HydratedDocument } from 'mongoose';
import { FanNotification } from './notification.schema';

export type FanDocument = HydratedDocument<Fan>;

@Schema()
export class Fan {

  @Prop({ type: FanNotification, default: () => ({}) })
  notifications: FanNotification;

}

export const FanSchema = SchemaFactory.createForClass(Fan);

I used default: () => ({}) to create defaults.

Embedded Document

import { Prop, Schema } from '@nestjs/mongoose';
import { AllMethod } from 'common/types';
import { Schema as MongooseSchema } from 'mongoose';

@Schema({ _id: false })
export class FanNotification {
  @Prop({
    type: MongooseSchema.Types.Mixed,
    default: { sms: true, email: true },
  })
  chat: AllMethod;

}

To make sure @Prop()s are recognized, and prevent automatic collection creation, I passed { _id: false }.

Firstly, you should use mongoose schema for this case. It is clear & simple:

export const UserSchema = new mongoose.Schema(
  {
    name: [UserNameSchema],
  },
  {
    timestamps: true,
  },
);

If you don't like this approach, you should follow the offical documentation:

@Prop(raw({
  firstName: { type: String },
  lastName: { type: String }
}))
details: Record<string, any>;
Related