In my NestJs app I have two entities: person.entity.ts and property.entity.ts, the two are connected with OneToMany relation. I have created DTOs for both person and property.
The owning side of the relation is defined in Person like this:
#JoinColumn()
#OneToMany(
(type) => PersonProperties,
(personProperties) => personProperties.person,
{ cascade: ['insert', 'update'] },
)
personProperties: PersonProperties[];
My Controller for posting new Person looks like this:
#Post()
async create(#Body() createPersonDto: CreatePersonDto) {
return this.personService.create(createPersonDto);
}
For validation I am using Global ValidationPipes as below:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
transformOptions: {
enableImplicitConversion: true,
},
}),
);
How should I modify my code so I can post (and validate) a Person with their properties with a single Request? Isn't this going to include one DTO inside another?
What you must use are Pipes Read more about it
In your case, a Validation Pipe is required to validate the incoming request against your DTO.
Pipes are used inside a controller, though you can read about creating custom pipes inbuilt ValidationPipe would satisfy your needs for now.
METHOD 1
#Post()
async create(#Body(ValidationPipe) createPersonDto: CreatePersonDto) {
return this.personService.create(createPersonDto);
}
METHOD 2
#Post()
#UsePipes(ValidationPipe)
async create(#Body() createPersonDto: CreatePersonDto) {
return this.personService.create(createPersonDto);
}
Both methods essentially do the same thing. It takes your Body request object and validates it against your defined DTO.
Hope this answers your question.
The solution is to create a composite DTO object as follows:
export class CreatePersonWithPropertiesDto {
#ValidateNested()
#Type(() => CreatePersonDto)
neuron: CreatePersonDto;
#IsOptional()
#ValidateNested({ each: true })
#Type(() => CreatePersonPropertiesDto)
personProperties?: CreatePersonPropertiesDto[];
}
and then adjust the controller to expect the composite DTO:
#Post()
async create(#Body() createPersonWithPropertiesDto: CreatePersonWithPropertiesDto) {
return this.personService.create(createPersonWithPropertiesDto);
}
Related
As mentioned in the docs
The ValidationPipe can automatically transform payloads to be objects typed according to their DTO classes. To enable auto-transformation, set transform to true.
But I always get Bad Request, so the transform doesn't work.
validation dto
export class CreateDistrictDto {
#IsString()
#IsNotEmpty()
name: string;
// associations
#IsNotEmpty()
#IsNumber()
cityId: number;
}
route in controller
#Post()
async createCity(#Body() cityDto: CreateCityDto) {
return await this.cityService.createCity(cityDto);
}
main.ts (I use default #nestjs/common ValidationPipe)
app.useGlobalPipes(new ValidationPipe({
transform: true,
}));
This answer says that transform doesn't work for primitives, which seems to be true.
https://stackoverflow.com/a/67181540/8419307
https://github.com/nestjs/nest/blob/master/packages/common/pipes/validation.pipe.ts#L137
Then why does the docs say otherwise and what is the transform: true option for?
So it appears that body-parser is not transforming the number into a number implicitly. To get around this, either send in a JSON request (application/json) with an explicit number or enable implicit type conversion in the class-transformer options via transformOptions: { enableImplicitConversion: true }, in the ValidationPipe options.
{
provide: APP_PIPE,
useValue: new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
}),
},
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'm using NestJS 7.0.2 and have globally enabled validation pipes via app.useGlobalPipes(new ValidationPipe());.
I'd like to be able to have a unit test that verifies that errors are being thrown if the improperly shaped object is provided, however the test as written still passes. I've seen that one solution is to do this testing in e2e via this post, but I'm wondering if there is anything I'm missing that would allow me to do this in unit testing.
I have a very simple controller with a very simple DTO.
Controller
async myApi(#Body() myInput: myDto): Promise<myDto | any> {
return {};
}
DTO
export class myDto {
#IsNotEmpty()
a: string;
#IsNotEmpty()
b: string | Array<string>
}
Spec file
describe('generate', () => {
it('should require the proper type', async () => {
const result = await controller.generate(<myDto>{});
// TODO: I expect a validation error to occur here so I can test against it.
expect(result).toEqual({})
})
})
It also fails if I do not coerce the type of myDto and just do a ts-ignore on a generic object.
Just test your DTO with ValidationPipe:
it('validate DTO', async() => {
let target: ValidationPipe = new ValidationPipe({ transform: true, whitelist: true });
const metadata: ArgumentMetadata = {
type: 'body',
metatype: myDto,
data: ''
};
await target.transform(<myDto>{}, metadata)
.catch(err => {
expect(err.getResponse().message).toEqual(["your validation error"])
})
});
You can find here complete test examples for ValidationPipe in Nestjs code repository
To test custom ValidationPipe:
let target = new ValidationDob();
const metadata: ArgumentMetadata = {
type: 'query',
metatype: GetAgeDto,
data: '',
};
it('should throw error when dob is invalid', async () => {
try {
await target.transform(<GetAgeDto>{ dob: 'null' }, metadata);
expect(true).toBe(false);
} catch (err) {
expect(err.getResponse().message).toEqual('Invalid dob timestamp');
}
});
I am using Nest.Js with TypeORM and I want to hash my password before persisting into the DB.
I tried using the event decorator #BeforeInsert() however it wasn't working for me but later I found that it was not working because I was taking an DTO as an input.
user.controller.ts
#Post()
async create(#Body() data: CreateUserDto, #Res() res) {
// if user already exist
const isUserExist = await this.service.findByEmail(data.email);
if (isUserExist) {
throw new BadRequestException('Email already exist');
}
// insert user
this.service.create(data);
// send response
return res.status(201).json({
statusCode: 201,
message: 'User added Successfully',
});
}
user.service.ts
create(data: CreateUserDto) {
return this.userRepository.save(data)
}
So, I was basically using an DTO to save the data. That's why it was not working.
But what I want to do is map the DTO to user object. So, This is what I did.
#Post()
async create(#Body() data: CreateUserDto, #Res() res) {
// Create User object
const user = new User();
// Map DTO to User object
for (const [key, value] of Object.entries(data)) {
user[key] = value;
}
// if user already exist
const isUserExist = await this.service.findByEmail(user.email);
if (isUserExist) {
throw new BadRequestException('Email already exist');
}
// insert user
this.service.create(user);
// send response
return res.status(201).json({
statusCode: 201,
message: 'User added Successfully',
});
}
create-user.dto.ts
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { ApiProperty } from '#nestjs/swagger';
export class CreateUserDto {
#IsNotEmpty()
#IsString()
#ApiProperty()
readonly firstName: string;
#IsNotEmpty()
#IsString()
#ApiProperty()
readonly lastName: string;
#IsNotEmpty()
#IsString()
#IsEmail()
#ApiProperty()
readonly email: string;
#IsNotEmpty()
#IsString()
#ApiProperty()
readonly password: string;
}
Is there any better approach for this? Because currently I have to write the code in every method to map it.
I would first move all the logic from my controller into the service. This would allow you to reuse the logic in other places, if there are any (since you prefer to have that service class).
Personally, I would avoid writing smart code because it saves me 2 or 3 lines of code. When someone else than you would have to review/refactor it would be a pain in the back. Just write something that's easy to understand.
Third, I would avoid using magic things like beforeInsert. Yeah, it might look smart but you don't make it clear how the pass is generated.
If your entity has the same fields as your DTO what's then the benefit of having the dto. Personally I would avoid exposing entity's password property. Instead, I would have a changePassword(generator: IUserPassGenerator) method in the entity. As for checking the pass, I would have somethingnlike verifyPass(validator: IPassChecker) method.
Another thing that I would avoid would be setters or public props mainly because it might cause your entity to enter into an invalid state. In your case e.g. someone else might change the password property with a md5 hash. After all, they can even change it with an unhashed string.
We can easily map Plain Object Literal to Class Instances by using 'class-transformer' package
Answer:
async create(#Body() data: CreateUserDto, #Res() res) {
const user = plainToClass(User, data)
}
this is a valid approach.
What you can do is extract this logic from the create method and create some kind of Builder object to create User objects from the DTO and vice-versa and call the builder where you need it.
I have a DTO
export class UpdateUserRoleDTO {
#ApiProperty()
#IsNotEmpty()
readonly userId:number;
#ApiProperty()
#IsNotEmpty()
#IsNumber()
readonly roleId: number;
}
My controller looks like this
#UsePipes(new ValidationPipe())
#Post('/update')
async updateUser(#Body() updateUserDto: UpdateUserDTO): Promise<User> {
return await this.userService.updateUser(updateUserDto);
}
Whenever client sends a request with the following payload
payloadObj = {
userId : 1,
roleId : 1,
xyz : 'assddcds',
someotherkey : 'fsdvs'
}
It's hitting my service file .I want to avoid this make sure only parameter mentioned in DTO should be passed else it should give 400
given your code I'd pass the whitelist option set to true to the ValidationPipe you're instantiating, like so in your controller:
controller.ts
#UsePipes(new ValidationPipe({ whitelist: true }))
#Post('/update')
async updateUser(#Body() updateUserDto: UpdateUserDTO): Promise<User> {
return await this.userService.updateUser(updateUserDto);
}
This should do the work.
Let me know if it helps, otherwise don't hesitate to comment & share your findings ;)