Reputation: 121
I've a simple user model and i want to exclude password from it. Using the official docs and answer here i've tried to make it work but this doesn't seem to work as i get a response something like this.
[
{
"$__": {
"strictMode": true,
"selected": {},
"getters": {},
"_id": {
"_bsontype": "ObjectID",
"id": {
"type": "Buffer",
"data": [
94,
19,
73,
179,
3,
138,
216,
246,
182,
234,
62,
37
]
}
},
"wasPopulated": false,
"activePaths": {
"paths": {
"password": "init",
"email": "init",
"name": "init",
"_id": "init",
"__v": "init"
},
"states": {
"ignore": {},
"default": {},
"init": {
"_id": true,
"name": true,
"email": true,
"password": true,
"__v": true
},
"modify": {},
"require": {}
},
"stateNames": [
"require",
"modify",
"init",
"default",
"ignore"
]
},
"pathsToScopes": {},
"cachedRequired": {},
"session": null,
"$setCalled": [],
"emitter": {
"_events": {},
"_eventsCount": 0,
"_maxListeners": 0
},
"$options": {
"skipId": true,
"isNew": false,
"willInit": true
}
},
"isNew": false,
"_doc": {
"_id": {
"_bsontype": "ObjectID",
"id": {
"type": "Buffer",
"data": [
94,
19,
73,
179,
3,
138,
216,
246,
182,
234,
62,
37
]
}
},
"name": "Kamran",
"email": "[email protected]",
"password": "Pass1234",
"__v": 0
},
"$locals": {},
"$init": true
}
]
Here's my model. I'm using Typegoose
but the same is the case with Mongoose
as well.
export class User extends Typegoose {
@Transform((value) => value.toString(), { toPlainOnly: true })
_id: string;
@prop({ required: true })
public name!: string;
@prop({ required: true })
public email!: string;
@Exclude({ toPlainOnly: true })
@prop({ required: true })
public password!: string;
}
My user service
@Injectable()
export class UserService {
constructor(@InjectModel(User) private readonly user: ReturnModelType<typeof User>) {}
async getUsers() {
return this.user.find().exec();
}
}
and user controller
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UserController {
constructor(private readonly userService: UserService) {}
@Get()
async index() : Promise<User[] | []> {
return this.userService.getUsers();
}
}
I tried to use my custom interceptor as described here but that didn't work so i changed it to below code as given here
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => classToPlain(this.transform(data))));
}
transform(data) {
const transformObject = (obj) => {
const result = obj.toObject();
const classProto = Object.getPrototypeOf(new User());
Object.setPrototypeOf(result, classProto);
return result;
}
return Array.isArray(data) ? data.map(obj => transformObject(obj)) : transformObject(data);
}
}
Now it's working but the code is not generic. Any way to make it generic?
Upvotes: 2
Views: 7959
Reputation: 121
I think i've identified the problem but not sure why this happens yet. So here's the problem if i return the instance of the class then the serialization works but if i just return plain db response then the above mentioned issue occurs. So what i did is i updated the prototype of the response objects in the transform
method of toObject
to my user class. Here's the code.
User Model
@modelOptions({
schemaOptions: {
toObject: {
transform: function(doc, ret, options) {
Object.setPrototypeOf(ret, Object.getPrototypeOf(new User()));
}
},
},
})
export class User {
@Transform((value) => value.toString(), { toPlainOnly: true })
public _id: string;
@prop({ required: true })
public name!: string;
@prop({ required: true })
public email!: string;
@Exclude({ toPlainOnly: true })
@prop({ required: true })
public password!: string;
}
TransformInterceptor
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => classToPlain(this.transform(data))));
}
transform(data) {
return Array.isArray(data) ? data.map(obj => obj.toObject()) : data.toObject();
}
}
And now if you just decorate your controller or method with @UseInterceptors(TransformInterceptor)
it will work perfectly. This is a typegoose
solution but it will work the same way with mongoose
as well.
Upvotes: 3
Reputation: 7146
To avoid any back-pain and headaches with Mongoose,
I would suggest using the plainToClass
to have a full mongoose/class-transform compatibility and avoid having to make custom overrides to overcome this isse.
Example, add this in your service :
async validateUser(email: string, password: string): Promise<UserWithoutPassword | null> {
const user = await this.usersService.findOne({ email });
if (user && await compare(password, user.password))
{
return plainToClass(UserWithoutPassword, user.toObject());
}
return null;
}
This way you can use the @Exclude()
and other decorators
Source : Stackoverflow answer
Upvotes: 1
Reputation: 481
The solution that worked for me: @Transform(({value}) => value.toHexString(), {toPlainOnly: true})
Full code example:
export class User {
@ObjectIdColumn()
@Transform(({value}) => value.toHexString(), {toPlainOnly: true})
_id: ObjectID
@Column({unique: true })
username: string
@Column({ unique: true })
email: string
@Exclude({ toPlainOnly: true })
@Column()
password: string
}
Upvotes: 0
Reputation: 39
import { Exclude, Expose } from "class-transformer";
export class UserSerializer {
@Expose()
email: string;
@Expose()
fullName: string;
@Exclude()
password: string;
@Expose()
username: string;
}
@Post("new")
async createNewAccount(@Body() body: CreateUserDTO) {
return plainToClass(UserSerializer, await (await this.authService.createNewUser(body)).toJSON())
}
Upvotes: 1
Reputation: 360
Here is my implementation, all the Decorators will work without needing the ClassSerializerInterceptor
PersonSchema.methods.toJSON = function () {
return plainToClass(Person, this.toObject());
};
Upvotes: 1