Reputation: 141
I've started to play with NestJS, migrating from my old express/mongoose project and immediately crashed into a fence, just following MongoDB/serializations chapters from NestJS docs. I've prepared following schema
/////// schema
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import * as mongoose from 'mongoose';
import { Exclude, Expose } from 'class-transformer';
export type UserDocument = User & mongoose.Document;
@Schema()
export class User {
@Prop()
@Exclude()
_id: String
@Expose()
get id(): String { return this._id ? `${this._id}` : undefined }
@Prop()
name: string
@Prop({ unique: true })
login: string
@Exclude()
@Prop()
password: string
}
export const UserSchema = SchemaFactory.createForClass(User);
registered it in app.module
MongooseModule.forRoot('mongodb://localhost/old_project'),
MongooseModule.forFeature([ { name: User.name, schema: UserSchema } ]),
and tried following calls, expecting no password property revealed in results
/////// controller
@UseInterceptors(ClassSerializerInterceptor)
@Get('default')
async default(): Promise<User> {
let u = new User();
u.name = 'Kos';
u.password = "secret";
u.login = '[email protected]'
return u;
}
// returns
// {"name":"Kos","login":"[email protected]"}
@Get('first_raw')
async firstRaw(): Promise<User> {
return this.userModel.findOne()
}
@Get('first_lean')
async firstLean(): Promise<User> {
return this.userModel.findOne().lean()
}
//both return
// {"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0}
@UseInterceptors(ClassSerializerInterceptor)
@Get('first_raw_stripped')
async firstRawStripped(): Promise<User> {
return this.userModel.findOne()
}
//returns
// {"$__":{"strictMode":true,"selected":{},"getters":{},"_id":"5f8731a36fc003421db08921","wasPopulated":false,"activePaths":{"paths":{"_id":"init","name":"init","login":"init","password":"init","__v":"init"},"states":{"ignore":{},"default":{},"init":{"_id":true,"name":true,"login":true,"password":true,"__v":true},"modify":{},"require":{}},"stateNames":["require","modify","init","default","ignore"]},"pathsToScopes":{},"cachedRequired":{},"$setCalled":[],"emitter":{"_events":{},"_eventsCount":0,"_maxListeners":0},"$options":{"skipId":true,"isNew":false,"willInit":true,"defaults":true}},"isNew":false,"$locals":{},"$op":null,"_doc":{"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0},"$init":true}
@UseInterceptors(ClassSerializerInterceptor)
@Get('first_lean_stripped')
async firstLeanStripped(): Promise<User> {
return this.userModel.findOne().lean()
}
//returns
// {"_id":"5f8731a36fc003421db08921","name":"Kos","login":"kos","password":"secret","__v":0}
Finally I've found that only manual instantiation of User class does somehow what it should do, so I've added constructor to User class
constructor(partial?: Partial<User>) {
if (partial)
Object.assign(this, partial);
}
and then it finally returned what was expected - no password prop in result
@UseInterceptors(ClassSerializerInterceptor)
@Get('first')
async first(): Promise<User> {
return new User(await this.userModel.findOne().lean());
}
//finally returns what's expected
// {"name":"Kos","login":"kos","__v":0,"id":"5f8731a36fc003421db08921"}
Am I missing something? Somehow it seems a bit overwhelming...
UPDATE: it is either question about NestJS mongoose and serialization coupling - why this
@UseInterceptors(ClassSerializerInterceptor)
@Get('first')
async first(): Promise<User> {
return await this.userModel.findOne().lean();
}
doesn't work and this
@UseInterceptors(ClassSerializerInterceptor)
@Get('first')
async first(): Promise<User> {
return new User(await this.userModel.findOne().lean());
}
works (which also means for each result enumerable map with entity creations required)
Upvotes: 9
Views: 10702
Reputation: 1
if you any of npm package like (mongoose-exclude) then it will only exclude single object not nested object and if you implement your own custom interceptor then @Expose() group will not work.
keep all of these issues in concertation if found a Hack
import { Role } from "@type/UserType"
import { Exclude } from "class-transformer"
import { HydratedDocument, ObjectId } from "mongoose"
import { Prop, SchemaFactory, Schema } from "@nestjs/mongoose"
export type UserDocument = HydratedDocument<User>
@Schema({
timestamps: true,
versionKey: false
})
export class User {
toObject(): Partial<User> {
throw new Error("Method not implemented.")
}
@Exclude()
_id: ObjectId
@Prop({
type: String,
required: true
})
name: string
@Prop({
unique: true,
type: String,
required: true
})
email: string
@Exclude()
@Prop({ type: String })
password: string
@Prop({
type: String,
default: Role.USER
})
role: Role
@Prop({
type: String,
select: false
})
token: string
constructor(partial: Partial<User>) {
Object.assign(this, partial)
}
}
export const UserSchema = SchemaFactory.createForClass(User)
import { SignUpDto } from "@dto/UserDto"
import { Model, FilterQuery } from "mongoose"
import { StaticError } from "@type/ErrorType"
import { InjectModel } from "@nestjs/mongoose"
import { User, UserDocument } from "@schema/UserSchema"
import { Injectable, NotFoundException } from "@nestjs/common"
import { IUserRepository } from "@irepository/IUserRepository"
@Injectable()
export class UserRepository implements IUserRepository {
constructor(@InjectModel(User.name) private readonly userModel: Model<UserDocument>) { }
public async signUp(signUpDto: SignUpDto): Promise<User> {
const user: User = await this.userModel.create(signUpDto)
return new User(user.toObject())
}
}
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
All things are working as expected like Expose with group and nested exclude
Upvotes: 0
Reputation: 2102
Mongoose has its own suppression builtin with the toJson method, you can use it when you create the schema for the model.
export const UserSchema = (() =>
const userSchema = SchemaFactory.createForClass(User);
schema.set('toJSON', {
transform: function (_, ret) {
delete ret.password;
},
});
return emailSchema;
})();
Upvotes: 1
Reputation: 1555
As explained by @Ali Sherafat, unfortunately solution didn't worked for me.
The Mongoose library that we use for connecting to MongoDB and fetching entities does not return instances of our User class. Therefore, the ClassSerializerInterceptor won’t work out of the box.
Definitely we would be requiring interceptor for mongoose serialization
. So, I came up with one more similar solution with modifications.
Create interceptor for mongoose serialization as:
import {
CallHandler,
ExecutionContext,
NestInterceptor,
UseInterceptors,
} from '@nestjs/common';
import { plainToClass } from 'class-transformer';
import { map, Observable } from 'rxjs';
interface ClassConstructor {
new ( ...args: any[ ] ): { };
}
export function MongooseClassSerializerInterceptor( dto: any ) {
return UseInterceptors( new SerializeInterceptor( dto ) );
}
export class SerializeInterceptor implements NestInterceptor {
constructor( private dto: any ) { }
intercept( context: ExecutionContext, handler: CallHandler ): Observable< any > {
return handler.handle( ).pipe(
map( ( data: any ) => {
return plainToClass( this.dto, data, {
excludeExtraneousValues: true
} )
} )
)
}
}
Create user dto as, this way you can use it for different role. So, for normal user we can expose required things:
import { Expose } from "class-transformer";
export class UserDto {
@Expose( )
id: number;
@Expose( )
name: string;
@Expose( )
login: string;
}
Now in your controller use @MongooseClassSerializerInterceptor( UserDto )
Using exclude
in schema is not very flexible when want to return response based on some role, e.g in required case admin may have access to more fields than normal user or vice-versa. In that case this is better approach.
Upvotes: 5
Reputation: 3855
After spending several hours finally I found a solution which was described in this post
The Mongoose library that we use for connecting to MongoDB and fetching entities does not return instances of our User class. Therefore, the
ClassSerializerInterceptor
won’t work out of the box.
First: create a interceptor for mongoose serialization:
mongooseClassSerializer.interceptor.ts
import {
ClassSerializerInterceptor,
PlainLiteralObject,
Type,
} from '@nestjs/common';
import { ClassTransformOptions, plainToClass } from 'class-transformer';
import { Document } from 'mongoose';
function MongooseClassSerializerInterceptor(
classToIntercept: Type,
): typeof ClassSerializerInterceptor {
return class Interceptor extends ClassSerializerInterceptor {
private changePlainObjectToClass(document: PlainLiteralObject) {
if (!(document instanceof Document)) {
return document;
}
return plainToClass(classToIntercept, document.toJSON());
}
private prepareResponse(
response: PlainLiteralObject | PlainLiteralObject[],
) {
if (Array.isArray(response)) {
return response.map(this.changePlainObjectToClass);
}
return this.changePlainObjectToClass(response);
}
serialize(
response: PlainLiteralObject | PlainLiteralObject[],
options: ClassTransformOptions,
) {
return super.serialize(this.prepareResponse(response), options);
}
};
}
export default MongooseClassSerializerInterceptor;
update your controller to apply this interceptor:
@UseInterceptors(MongooseClassSerializerInterceptor(User))
and your model(schema) should look like this:
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { Exclude, Transform } from 'class-transformer';
export type UserDocument = User & Document;
@Schema()
export class User {
@Transform(({ value }) => value.toString())
_id: string;
@Prop({ unique: true })
email: string;
@Prop()
name: string;
@Prop()
@Exclude()
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
Upvotes: 10
Reputation: 722
NestJS documentation explicitly states that it needs to be a class – not a plain object – for serialization to work properly. See Warning in red here: https://docs.nestjs.com/techniques/serialization#exclude-properties
This is why when you wrap it in the class constructor it works properly.
The proper way seems to be not to add a constructor to the model, as you did, but inject the schema/model into the service using the @InjectModel
decorator so that the findOne
method returns a class and not a plain object: https://docs.nestjs.com/techniques/serialization#exclude-properties
Once you've registered the schema, you can inject a
Cat
model into theCatsService
using the@InjectModel()
decorator:
Upvotes: 0
Reputation: 41
I noticed that you did not use: [1]: https://www.npmjs.com/package/nestjs-mongoose-exclude.
I realize that it is not too well known and that there is not a lot to download, but you have to give the small package a chance. If you don't want to use this package, you can do the following before returning your object:
// destructuring
const { password, ...newUser } = user;
return newUser;
Upvotes: 1
Reputation: 94
I think that I have the solution
@Schema()
export class User {
@Prop({select: false})
password: string;
@Prop()
username: string;
}
when you do this prop to the decorator the value of the property inside of mongo is ignored in finds.
Upvotes: 0