27

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.

Azamat Abdullaev
  • 726
  • 9
  • 24
Sinandro
  • 2,426
  • 3
  • 21
  • 36

7 Answers7

45

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

Azamat Abdullaev
  • 726
  • 9
  • 24
austinthedeveloper
  • 2,401
  • 19
  • 27
  • Hmmm, looks promising. Gonna try it out soon – Sinandro Oct 24 '20 at 18:44
  • 3
    will it create 2 separate collections? I want a single collection. – Ankit Tanna Dec 06 '20 at 16:41
  • 3
    @AnkitTanna No, only the schema you pass to `MongooseModule.forFeature` in the module will get created. – Sinandro May 08 '21 at 16:51
  • 13
    Is BodyContentInterface supposed to be the same as BodyApi? Its not defined anywhere – Clashsoft Jun 10 '21 at 22:39
  • If we do not pass the subdocument schema into `MongooseModule`, then how should we use and populate subdocument inside the corresponding `service.ts` – AlexanderK1987 Nov 02 '21 at 10:58
  • 2
    @AlexanderK1987, you don't need to pass it to the MongooseModule tho, I was making the same mistake and once I removed them, those collections never came back. I guess you'd have to stick to using those models inside of their own modules and that's a good practice. – uhum Nov 17 '21 at 21:51
  • This solution creates a collection for the sub document on the fly. To create an embedded document without creating the collection, read my answer below – Alex Mar 03 '22 at 17:06
  • @austinthedeveloper what is BodyContentInterface please? class annotated with @Schema()? Why in your example classes annotated with @Scehma() needs to extend Document? At least in nestjs docs, i cant find example where they extends from Document any schema. – Srle Mar 13 '23 at 00:01
  • @austinthedeveloper or maybe it is HydratedDocument? Something like: export type BodyContentInterface = HydratedDocument? – Srle Mar 13 '23 at 00:13
10

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
    }
  }
});
Gaetan LOISEL
  • 263
  • 3
  • 7
Andi Aleksandrov
  • 443
  • 2
  • 6
  • 16
  • This solution work perfectly for me, including prop args like `default`, `get`/`set` and so on. Hope I know this method earlier – Yari Apr 05 '23 at 06:53
6

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;
}
  • 1
    Will it work for unique: true for a property inside an array of sub document? – Ankit Tanna Dec 06 '20 at 16:44
  • 1
    This solution will correctly create a nested object (user.name.firstName) but the type (:string) validation will not work. You will be allowed to write a number or another type into the firstName field. It's not a working solution. – Andi Aleksandrov Nov 12 '21 at 11:03
  • I'm following the same the but my defaults are not being set on the nested object. – Unknown User Jan 27 '23 at 14:16
2

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);
Dezzley
  • 1,463
  • 1
  • 13
  • 19
Francisco Cardoso
  • 1,438
  • 15
  • 20
  • What is the problem? Are you getting any error messages? I just did that structure in my project and it is working fine – Francisco Cardoso Jul 08 '20 at 12:06
  • I don't know, I don't get any error, it just doesn't work. Try to put a default value on a nested property like 'firstName', the default value won't set, showing there's a problem. – Sinandro Jul 08 '20 at 12:39
  • https://discordapp.com/channels/520622812742811698/606125380817911828/738145362635915275 – Samith Bharadwaj Jul 29 '20 at 21:28
2

You can also use this.

    @Schema()
    class User extends Document {
      @Prop({ type:  { firstName: String, lastName: String })
      name: Name;
    }
Dezzley
  • 1,463
  • 1
  • 13
  • 19
1

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 }.

Alex
  • 1,623
  • 1
  • 24
  • 48
  • You should use `markModified()` for the sub-document when anything has changed on that path. – Alex Mar 03 '22 at 17:31
-5

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>;
  • I want to use my typescript model both in frontend and backend and I save this model in a shared folder. With this approach, I can't do this anymore! – Sinandro Jul 07 '20 at 07:49
  • 3
    Totally, It should not. Because the schemas and model are different. You should define interface files as well. The return data should be compatibility with interface. Then share the interface to frontend. Using OpenAPI generator. – Vương Nguyễn Jul 07 '20 at 09:23
  • the question clearly states using Nest decorators. – Ankit Tanna Dec 06 '20 at 16:45