At the moment, I have a very simple class-validator file with a ValidationPipe in Nest.js as follows:
import {
IsDateString,
IsEmail,
IsOptional,
IsString,
Length,
Max,
} from 'class-validator';
export class UpdateUserDto {
#IsString()
id: string;
#Length(2, 50)
#IsString()
firstName: string;
#IsOptional()
#Length(2, 50)
#IsString()
middleName?: string;
#Length(2, 50)
#IsString()
lastName: string;
#IsEmail()
#Max(255)
email: string;
#Length(8, 50)
password: string;
#IsDateString()
dateOfBirth: string | Date;
}
Lets say in the above "UpdateUserDto," the user passes an "email" field. I want to build a custom validation rule through class-validator such that:
Check if email address is already taken by a user from the DB
If the email address is already in use, check if the current user (using the value of 'id' property) is using it, if so, validation passes, otherwise, if it is already in use by another user, the validation fails.
While checking if the email address is already in use is a pretty simple task, how would you be able to pass the values of other properties within the DTO to a custom decorator #IsEmailUsed
It was pretty simple to solve, I solved it by creating a custom class-validation Decorator as below:
import { PrismaService } from '../../prisma/prisma.service';
import {
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
ValidationArguments,
} from 'class-validator';
import { Injectable } from '#nestjs/common';
#ValidatorConstraint({ name: 'Unique', async: true })
#Injectable()
export class UniqueConstraint implements ValidatorConstraintInterface {
constructor(private readonly prisma: PrismaService) {}
async validate(value: any, args: ValidationArguments): Promise<boolean> {
const [model, property = 'id', exceptField = null] = args.constraints;
if (!value || !model) return false;
const record = await this.prisma[model].findUnique({
where: {
[property]: value,
},
});
if (record === null) return true;
if (!exceptField) return false;
const exceptFieldValue = (args.object as any)[exceptField];
if (!exceptFieldValue) return false;
return record[exceptField] === exceptFieldValue;
}
defaultMessage(args: ValidationArguments) {
return `${args.property} entered is not valid`;
}
}
export function Unique(
model: string,
uniqueField: string,
exceptField: string = null,
validationOptions?: ValidationOptions,
) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [model, uniqueField, exceptField],
validator: UniqueConstraint,
});
};
}
However, to allow DI to that particular Decorator, you need to also add this to your main.ts bootstrap function:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
...
// Line below needs to be added.
useContainer(app.select(AppModule), { fallbackOnErrors: true });
...
}
Also, make sure to import the "Constraint" in the app module:
#Module({
imports: ...,
controllers: [AppController],
providers: [
AppService,
PrismaService,
...,
// Line below added
UniqueConstraint,
],
})
export class AppModule {}
Finally, add it to your DTO as such:
export class UpdateUserDto {
#IsString()
id: string;
#IsEmail()
#Unique('user', 'email', 'id') // Adding this will check in the user table for a user with email entered, if it is already taken, it will check if it is taken by the same current user, and if so, no issues with validation, otherwise, validation fails.
email: string;
}
Luckily for us, the class-validator provides a very handy useContainer function, which allows setting the container to be used by the class-validor library.
So add this code in your main.ts file (app variable is your Nest application instance):
useContainer(app.select(AppModule), { fallbackOnErrors: true });
It allows the class-validator to use the NestJS dependency injection container.
#ValidatorConstraint({ name: 'emailId', async: true })
#Injectable()
export class CustomEmailvalidation implements ValidatorConstraintInterface {
constructor(private readonly prisma: PrismaService) {}
async validate(value: string, args: ValidationArguments): Promise<boolean> {
return this.prisma.user
.findMany({ where: { email: value } })
.then((user) => {
if (user) return false;
return true;
});
}
defaultMessage(args: ValidationArguments) {
return `Email already exist`;
}
}
Don't forget to declare your injectable classes as providers in the appropriate module.
Now you can use your custom validation constraint. Simply decorate the class property with #Validate(CustomEmailValidation) decorator:
export class CreateUserDto {
#Validate(customEmailValidation)
email: string;
name: string;
mobile: number;
}
If the email already exists in the database, you should get an error with the default message "Email already exists". Although using #Validate() is fine enough, you can write your own decorator, which will be much more convenient. Having written Validator Constraint is quick and easy. We need to just write decorator factory with registerDecorator() function.
export function Unique(validationOptions?: ValidationOptions) {
return function (object: any, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
validator: CustomEmailvalidation,
});
};
}
As you can see, you can either write new validator logic or use written before validator constraint (in our case - Unique class).
Now we can go back to our User class and use the #Unique validator instead of the #Validate(CustomEmailValidation) decorator.
export class CreateUserDto {
#Unique()
email: string;
name: string;
mobile: number;
}
I think your first use case (Check if email address is already taken by a user from the DB), can be solved by using custom-validator
For the second one there is no option to get the current user before the validation. Suppose you are getting the current user using the #CurrentUser decorator. Then once the normal dto validation is done, you need to check inside the controller or service if the current user is accessing your resource.
Related
I try to use class-transformer but i can't do it.
I also use type-graphql and #typegoose/typegoose
Here's my code:
My Decorator
import { Transform } from 'class-transformer';
export function Trim() {
console.log('DECORATOR');
return Transform(({ value }: { value: string }) => {
console.log('value: ', value);
return value.trim();
});
}
My InputType
import { IsEmail } from 'class-validator';
import { InputType, Field } from 'type-graphql';
import { User } from '../Entities/User';
import { Trim } from '../../Decorators/Sanitize';
#InputType({ description: 'Input for user creation' })
export class AddUserInput implements Partial<User> {
#Field()
#Trim()
#IsEmail({ domain_specific_validation: true, allow_utf8_local_part: false })
email!: string;
}
My Resolver
import { Arg, Mutation, Resolver } from 'type-graphql';
import { User } from '../Entities/User';
import { AddUserInput } from '../Types/UsersInputs';
#Resolver(() => User)
export class UserResolvers {
#Mutation(() => String, { description: 'Register an admin' })
async createAccount(#Arg('data') data: AddUserInput): Promise<string> {
console.log({ ...data });
return data.email;
}
}
My Entity
import { prop, getModelForClass } from '#typegoose/typegoose';
import { ObjectType, Field } from 'type-graphql';
#ObjectType({ description: 'User model' })
export class User {
#Field({ description: 'The user email' })
#prop({ required: true, unique: true, match: [/\S+#\S+\.\S+/, 'is invalid'] })
email!: string;
}
export const UserModel = getModelForClass(User, {
schemaOptions: { timestamps: true }
});
My Request
POST http://localhost:5000/app HTTP/1.1
Content-Type: application/json
X-REQUEST-TYPE: GraphQL
mutation
{
createAccount (data : {
email: " email#gmail.com ",
})
}
The problem is that the console.log('value: ', value) inside Transform function is never call and my email is not trim.
Also console.log('DECORATOR') is not call when I do the request but just one time when server starting.
Thanks !
Typegoose transpiles classes into mongoose schemas & models, it does not apply any validation / transformation aside from mongoose provided ones, so your class-transformer decorators would only be called when using the class directly and using its functions. (like plainToClass and classToPlain)
In your case, it would be better to either use PropOptions get & set or pre-hooks.
As a note, typegoose provides a guide for using class-transformer with typegoose classes Integration Example: class-transformer, just to show that it can be used like normal classes.
Also note, it is currently recommended against using class-transformer because of mentioned issues inside the documentation.
Let say I have a simple collection events created by TypeGraphql and Typegoose which stores objects like:
{ _id: ObjectId(...), name: 'SomeEvent', category: ObjectId('...') }
and corresponding type:
#ObjectType()
export class Event {
#Field(() => ID)
_id!: Types.ObjectId
#prop({ ref: 'Category' })
#Field(() => Category)
category!: Ref<Category>
#prop()
#Field()
name!: string
}
I have also collection categories which contains right now only _id and name.
Now I want to insert some Event to database. Is it possible to automatically check if categoryId provided in input exist in collection categories and if it does not, throw an error? Right now, event can be added with anything in category field and next when I try to get it by query it throws an error that category cannot be resolved because there is no category with this ID. I know, that I can check it manually during adding event but if I have more fields like that it will be problematic.
With the help of Martin Devillers answer I was able to write a validator to validate referenced documents using class-validator with typegoose.
This is my refdoc.validator.ts:
import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { Injectable } from "#nestjs/common";
import { getModelForClass } from "#typegoose/typegoose";
#ValidatorConstraint({ name: "RefDoc", async: true })
#Injectable()
export class RefDocValidator implements ValidatorConstraintInterface {
async validate(refId: string, args: ValidationArguments) {
const modelClass = args.constraints[0];
return getModelForClass(modelClass).exists({ _id: refId })
}
defaultMessage(): string {
return "Referenced Document not found!";
}
}
Then I can apply it on the DTO or model with the #Validate-Decorator. The Argument I'm passing in is the typegoose model.
#Validate(RefDocValidator, [Costcenter])
costcenterId: string;
Seems to be working for me, I'm open for any improvements..
Edit: Even better with custom decorator, as Martin Devillers suggested:
refdoc.validator.ts
import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator";
import { Injectable } from "#nestjs/common";
import { getModelForClass } from "#typegoose/typegoose";
#ValidatorConstraint({ name: "RefDoc", async: true })
#Injectable()
export class RefDocValidator implements ValidatorConstraintInterface {
async validate(refId: string, args: ValidationArguments) {
const modelClass = args.constraints[0];
return getModelForClass(modelClass).exists({ _id: refId })
}
defaultMessage(): string {
return "Referenced Document not found!";
}
}
export function RefDocExists(modelClass: any, validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
name: 'RefDocExists',
target: object.constructor,
propertyName: propertyName,
constraints: [modelClass],
options: validationOptions,
validator: RefDocValidator,
});
};
}
Then you can use it on the DTO like:
#ApiProperty()
#IsNotEmpty()
//#Validate(RefDocValidator, [Costcenter]) old
#RefDocExists(Costcenter) //new
costcenterId: string;
Out of the box, neither MongoDB nor mongoose nor typegoose offer any automated referential integrity checks.
At the database level, this feature doesn't exist (which is also one of the fundamental differences between a database like MongoDB and SQL Server/Oracle).
However, at the application level, you can achieve this behavior in various ways:
Middleware. Mongoose middleware allows you add generalized behavior to your models. This is useful if you're inserting documents in your EventModel in various places in your codebase and don't want to repeat your validation logic. For example:
EventModel.path('category').validate(async (value, respond) => {
const categoryExists = await CategoryModel.exists({ _id: value })
respond(categoryExists)
})
Mongoose plugins. A plugin like mongoose-id-validator allows you to add the above behavior to any type of document reference in all your schemas.
Manually. Probably the least favorite option, but I'm mentioning it for completeness sake: with mongoose's Model.exists() you can achieve this with a one-liner like: const categoryExists = await CategoryModel.exists({ _id: event.category })
To reiterate: all the above options are stopgaps. It's always possible for someone to go directly in your database and break referential integrity anyway.
I feel like a combination of this thread and this thread is what I need to implement, I'm having trouble drawing them together.
I have a DTO that contains an enum.
Using Postman, I am sending a PurchasableType of FOO and expecting to get an error of some sort. Reading through the above links, it seems like the process is quite involved; which makes me thing I'm completely missing the point.
How can I use the validation pipe(s) to make sure only the values in the purchasable-type.enum.ts are allowed?
Thank you for any suggestions!
// create-order.dto.ts
import { IsEmail, IsNotEmpty, IsEnum } from 'class-validator';
import { PurchasableType } from '../enum/purchaseable-type.enum';
export class CreateOrderDto {
#IsNotEmpty()
readonly userId: string;
#IsNotEmpty()
readonly locationId: string;
#IsNotEmpty()
#IsEnum(PurchasableType)
readonly purchasableType: PurchasableType;
#IsNotEmpty()
#IsEmail()
readonly email: string;
}
// purchasable-type.enum.ts
export enum PurchasableType {
CLINIC = 'CLINIC',
EVENT = 'EVENT',
LESSON = 'LESSON',
RESERVATION = 'RESERVATION',
TEAM = 'TEAM',
}
EDIT
It seems I was also not defining the entity correctly, and that may have been the main issue. I am still curious if my implementation good/bad.
// order.entity.ts
...
import { PurchasableType } from '../enum/purchaseable-type.enum';
#Entity()
export class Order extends BaseEntity {
#PrimaryGeneratedColumn()
id: number;
#Column({
type: 'enum',
enum: PurchasableType,
})
Now when I send a purchasableType of foo I am getting a 500 error. If I send any valid value that is within the enum I am getting a 200/201.
EDIT 2
Sure - here is a bit wider view of what I've got. Everything seems to be working properly, I'd just like to have a better grasp of what was really happening.
// event.controller.ts
#Post('/:id/orders')
async purchaseEventTickets(#Body() createOrderDto: CreateOrderDto):
Promise<Order> {
return await this.eventService.purchaseEventTickets(createOrderDto);
}
// create-order.dto.ts
export class CreateOrderDto {
#IsNotEmpty()
#IsEnum(PurchasableType)
readonly purchasableType: PurchasableType;
}
// event.service.ts
async purchaseEventTickets(createOrderDto: CreateOrderDto): Promise<Order> {
...
return await this.orderRepository.createOrder(createOrderDto);
}
// order.repository.ts
async createOrder(createOrderDto: CreateOrderDto): Promise<Order> {
const { purchasableType } = createOrderDto;
const order = this.create();
order.purchasableType = purchasableType;
try {
await order.save();
} catch (error) {
this.logger.error(`Failed to create the order: ${error.stack}`);
throw new InternalServerErrorException();
}
return order;
}
Using Postman, if I send an invalid value of "Foo" as a PurchasableType I get the expected error.
It took me a while to find a good solution.
#ApiProperty({
description: 'List of enums',
isArray: true,
enum: MyEnum
})
#IsEnum(MyEnum, { each: true })
prop: MyEnum[];
Here is what your create-dto looks like that contains an enum.
// create-order.dto.ts
import { IsEmail, IsNotEmpty, IsEnum } from 'class-validator';
import { PurchasableType } from '../enum/purchaseable-type.enum';
export class CreateOrderDto {
...
#IsNotEmpty()
#IsEnum(PurchasableType)
readonly purchasableType: PurchasableType;
}
Here is what that enum file looks like:
// purchasable-type.enum.ts
export enum PurchasableType {
CLINIC = 'CLINIC',
EVENT = 'EVENT',
LESSON = 'LESSON',
RESERVATION = 'RESERVATION',
TEAM = 'TEAM',
}
From there I can confidently expect the value of the enum to be one of the above values. If some other value comes through, nest will throw a validation error.
Additionally, If you are attempting to use a nested object (or something with multiple attributes or an array) you can do something like this in your DTO:
import { PurchasableType } from '../interface/purchasable-type.interface';
...
#ApiProperty()
#IsArray()
#ArrayMinSize(7)
#ArrayMaxSize(7)
#ValidateNested({ each: true })
#Type(() => PurchasableType)
#IsNotEmpty()
readonly PurchasableType: PurchasableType[];
...
#IsArray()
#IsEnum(enum, { each: true })
prop: enum[]
#IsEnum(myEnum, { each: true })
#Transform((value) => myEnum[value])
tags: myEnum[];
None of the above worked for me, I did it this way:
#Column({ type: 'enum', enum: MyEnum, array: true })
myProperty: MyEnum[];
I am trying to create simple appliaction with Nest.js, GraphQL and MongoDB. I wnated to use TypeORM and TypeGraphql to generate my schema and make a connection with localhost databasebut but i can not run my server with nest start becouse I am getting this error:
UnhandledPromiseRejectionWarning: Error: Cannot determine GraphQL output type for getArticles
I have no idea why i am getting this error. My class ArticleEntity does't has any not primary types, so there should not be any problem. I tried to remove () => ID from #Field() decorator of filed _id of ArticleEntity class but it didn't helped
ArticleResolver
#Resolver(() => ArticleEntity)
export class ArticlesResolver {
constructor(
private readonly articlesService: ArticlesService) {}
#Query(() => String)
async hello(): Promise<string> {
return 'Hello world';
}
#Query(() => [ArticleEntity])
async getArticles(): Promise<ArticleEntity[]> {
return await this.articlesService.findAll();
}
}
ArticleService
#Injectable()
export class ArticlesService {
constructor(
#InjectRepository(ArticleEntity)
private readonly articleRepository: MongoRepository<ArticleEntity>,
) {}
async findAll(): Promise<ArticleEntity[]> {
return await this.articleRepository.find();
}
}
ArticleEntity
#Entity()
export class ArticleEntity {
#Field(() => ID)
#ObjectIdColumn()
_id: string;
#Field()
#Column()
title: string;
#Field()
#Column()
description: string;
}
ArticleDTO
#InputType()
export class CreateArticleDTO {
#Field()
readonly title: string;
#Field()
readonly description: string;
}
If you need anything else comment
ArticleEntity should be decorated with the #ObjectType decorator as shown in the docs https://typegraphql.com/docs/types-and-fields.html.
#Entity()
#ObjectType()
export class ArticleEntity {
...
}
For anyone who gets this error and uses enums, you may be missing a call to registerEnumType.
In my case, I was using the #ObjectType decorator, but I was importing from type-graphql. I imported from #nestjs/graphql instead, and the problem was resolved.
import { ObjectType } from '#nestjs/graphql';
See here for a related discussion on GitHub.
I was using MongoDB and I had my Query return the schema instead of the model class.
Changing #Query((returns) => UserSchema) to #Query((returns) => User) fixed the issue for me.
user.schema.ts
#ObjectType()
#Schema({ versionKey: `version` })
export class User {
#Field()
_id: string
#Prop({ required: true })
#Field()
email: string
#Prop({ required: true })
password: string
}
export const UserSchema = SchemaFactory.createForClass(User)
user.resolver.ts
#Query((returns) => User)
async user(): Promise<UserDocument> {
const newUser = new this.userModel({
id: ``,
email: `test#test.com`,
password: `abcdefg`,
})
return await newUser.save()
}
Output model class should be decorated with #ObjectType() and then all properties of that class will decorated with #Field().
For any one who is using a custom output model class and NOT an entity(sequelize, typeorm, prisma etc). Because I was using database entity first, everything was working fine till I moved to a more customized output model.
One more case would be someone using class A as output and class B is used within A
export class A{
id: number;
name:string;
childProperty: B
. . . . .
}
export class B{
prop1:string;
prop2:string;
}
In that case class B should also be decorated with #ObjectType and fields (prop1 , prop2 ) should be also be decorated with #Field as well.
I want to serialize a controller response by the nestjs serialization technique. I didn't find any approach and my solution is as follows:
User Entity
export type UserRoleType = "admin" | "editor" | "ghost";
#Entity()
export class User {
#PrimaryGeneratedColumn() id: number;
#Column('text')
username: string;
#Column('text')
password: string;
#Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
#Column({ nullable: true })
profileId: number;
}
User Response Classes
import { Exclude } from 'class-transformer';
export class UserResponse {
id: number;
username: string;
#Exclude()
roles: string;
#Exclude()
password: string;
#Exclude()
profileId: number;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}
import { Exclude, Type } from 'class-transformer';
import { User } from 'src/_entities/user.entity';
import { UserResponse } from './user.response';
export class UsersResponse {
#Type(() => UserResponse)
users: User[]
constructor() { }
}
Controller
#Controller('user')
export class UsersController {
constructor(
private readonly userService: UserService
) {
}
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}
It works, but I must explicitly assign the db query result to the response users member.
Is there a better way? Thanks a lot
Here the actual Response and wanted result, for a better explanation.
Result in this Approach
{
"users": [
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
]
}
Result Wanted
{
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
}
I would recommend to directly put the #Exclude decorators on your entity class User instead of duplicating the properties in UserResponse. The following answer assumes you have done so.
Flat Response
If you have a look at the code of the ClassSerializerInterceptor, you can see that it automatically handles arrays:
return isArray
? (response as PlainLiteralObject[]).map(item =>
this.transformToPlain(item, options),
)
: this.transformToPlain(response, options);
However, it will only transform them, if you directly return the array, so return users instead of return {users: users}:
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(): Promise<User> {
return this.userService.findAll()
}
Nested Response
If you need the nested response, then your way is a good solution.
Alternatively, you can call class-transformer's serialize directly instead of using the ClassSerializerInterceptor. It also handles arrays automatically:
import { serialize } from 'class-transformer';
#Get('all')
async findAll(): Promise<UsersResponse> {
const users: User[] = await this.userService.findAll();
return {users: serialize(users)};
}
Wow, what easy, if i know! Perfect, this solves my problem. Also your recommendation for the User Entity with the class-transformer #Exclue() decorator.
And i know that i do not need a custom UsersResponse class in this use case.
This solution was that what i was looking for, but i overjump this quite easy way
Thank you so much for your superfast answer and the problem solution.
Greetings to Berlin from Rostock :)
Here my final approach:
Controller
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(
): Promise<User> {
return await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
}
User Entitiy
import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
import { Profile } from './profile.entity';
import { Photo } from './photo.entity';
import { Album } from './album.entity';
import { Exclude } from 'class-transformer';
export type UserRoleType = "admin" | "editor" | "ghost";
#Entity()
export class User {
#PrimaryGeneratedColumn() id: number;
#Column('text')
username: string;
#Exclude()
#Column('text')
password: string;
#Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
#Exclude()
#Column({ nullable: true })
profileId: number;
#OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
#OneToMany(type => Album, albums => albums.user)
albums: Album[];
#OneToOne(type => Profile, profile => profile.user)
#JoinColumn()
profile: Profile;
}
Response Result
[
{
"id": 1,
"username": "a",
"roles": "admin"
},
{
"id": 2,
"username": "bbbbbb",
"roles": "ghost"
}
]
I have alternative way for your problem.
you can remove #UseInterceptors(ClassSerializerInterceptor) from your Controller. Instead use serialize and deserialize function.
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
#Get('all')
async findAll() {
const users = serialize(await this.userService.findAll());
return {
status: 200,
message: 'ok',
users: deserialize(User, users)
};
}
it's work too for single data
import { Param } from '#nestjs/common';
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
#Get(':id')
async findById(#Param('id') id: number) {
const user = serialize(await this.userService.findById(id));
return {
status: 200,
message: 'ok',
user: deserialize(User, user)
};
}
Your approach is recommended by nestjs but that has a fault. You are excluding some properties from being exposed to the client. What if, you work in a project that has an admin and admin wants to see all the data about the users or products. If you exclude fields in the entities, your admin won't see those fields either. Instead, leave the entities as it is, and write dto's for each controller or for each request handler and in this dto's just list the properties you want to expose.
Then write a custom interceptor and create specific dto for ecah entity. For example in your example, you create a userDto:
import { Expose } from 'class-transformer';
// this is a serizalization dto
export class UserDto {
#Expose()
id: number;
#Expose()
roles: UserRoleType;
#Expose()
albums: Album[];
// Basically you list what you wanna expose here
}
custom interceptor is a little messy:
import {
UseInterceptors,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '#nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';
// Normally user entity goes into the interceptor and nestjs turns it into the JSON. But we we ill turn it to User DTO which will have all the serialization rules.then nest will take dto and turn it to the json and send it back as response
export class SerializerInterceptor implements NestInterceptor {
// dto is the variable. so you can use this class for different entities
constructor(private dto:any){
}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
// you can write some code to run before request is handled
return handler.handle().pipe(
// data is the incoming user entity
map((data: any) => {
return plainToClass(this.dto, data, {
// this takes care of everything. this will expose things that are set in the UserDto
excludeExtraneousValues: true,
});
}),
);
}
}
Now you use this in the controller:
// See we passed UserDto. for different entities, we would just write a new dto for that entity and our custom interceptor would stay reusable
#UseInterceptors(new SerializerInterceptor(UserDto))
#Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}