I'm trying to create a DTO that has another DTO as array, but when sending the body, nestjs/swagger not detecting the body content.
My DTOs are:
export class CreatePageDto {
#ApiHideProperty()
createAt: Date;
#ApiHideProperty()
updateAt: Date;
#ApiProperty({
type: CreatePageTranslateDto,
isArray: true,
})
translations: CreatePageTranslateDto[];
}
export class CreatePageTranslateDto {
#ApiProperty()
slug: string;
#ApiProperty()
title: string;
#ApiProperty({
enum: AvailableLanguages,
})
lang: AvailableLanguages;
}
When a post a body like this:
curl --location --request POST 'http://localhost:3000/pages' \
--header 'Content-Type: application/json' \
--data-raw '{
"translations": [
{
"slug": "nombre-de-ejemplo",
"title": "Nombre de ejemplo",
"lang": "es"
}
]
}'
I get an empty body.
The problem was class-validator. I decided to set class validator globally and I didn't do it right. The problem was due to the whitelist property set to true: "if set to true validator will strip validated object of any properties that do not have any decorators".
You have to switch the class position first and then please add this decorator #ApiExtraModels() above the class CreatePageTranslateDto.
So, the code will be like this:
#ApiExtraModels() <-----
export class CreatePageTranslateDto {
#ApiProperty()
slug: string;
#ApiProperty()
title: string;
#ApiProperty({
enum: AvailableLanguages,
})
lang: AvailableLanguages;
}
export class CreatePageDto {
#ApiHideProperty()
createAt: Date;
#ApiHideProperty()
updateAt: Date;
#ApiProperty({
type: CreatePageTranslateDto,
isArray: true,
})
translations: CreatePageTranslateDto[];
}
you have to do this below to validate nested DTO
import { Type } from 'class-transformer';
import { ValidateNested } from 'class-validator';
#ApiProperty({
type: CreatePageTranslateDto,
isArray: true,
})
#ValidateNested({ each: true })
#Type(() => CreatePageTranslateDto)
translations: CreatePageTranslateDto[];
Related
I'm working on a nestjs project with TypeORM. I'm setting up a bidirectional ManyToMany relationship between two entities: Notes and Gcalevents. Every Note could be linked to multiple Gcalevents, and every Gcalevent could be linked to many Notes.
Snippets from my entity definitions look like this:
base.entity.ts:
export class BaseEntity {
#PrimaryGeneratedColumn('uuid')
id: string;
#Column({ type: 'boolean', default: true })
isActive: boolean;
... // some more fields
}
note.entity.ts:
import { Gcalevent } from './gcalevent.entity';
import { BaseEntity } from './base.entity';
#Entity({ name: 'note' })
export class Note extends BaseEntity {
#Column({ type: 'varchar', length: 300 })
title: string;
...
#ManyToMany(() => Gcalevent, (gcalevent) => gcalevent.notes)
#JoinTable()
gcalevents: Gcalevent[]
}
gcalevent.entity.ts:
import { Note } from "./note.entity";
#Entity({ name: 'gcalevent' })
export class Gcalevent {
#PrimaryGeneratedColumn('uuid')
id: string;
#Column({
type: 'varchar',
nullable: false,
})
eventId: string;
...
#ManyToMany(() => Note, (note) => note.gcalevents)
notes: Note[]
}
I believe I'm declaring the ManyToMany relationships correctly, but when I try to generate the TypeORM migration, I get the error TypeError: Class extends value undefined is not a constructor or null, which points to both the note.entity and gcalevent.entity files
I'm guessing this has something to do with the gcalevent.entity and note.entity files importing each other, combined with the note.entity file importing BaseEntity, but not sure how to fix it!
I had the same problem, I've found here
this "workaround" for the issue.
Try using "strings" instead of "types" in the #ManyToMany decorator:
note.entity.ts:
import { Gcalevent } from './gcalevent.entity';
import { BaseEntity } from './base.entity';
#Entity({ name: 'note' })
export class Note extends BaseEntity {
#Column({ type: 'varchar', length: 300 })
title: string;
...
#ManyToMany("Gcalevent","notes")
#JoinTable()
gcalevents: Gcalevent[]
}
gcalevent.entity.ts:
import { Note } from "./note.entity";
#Entity({ name: 'gcalevent' })
export class Gcalevent {
#PrimaryGeneratedColumn('uuid')
id: string;
#Column({
type: 'varchar',
nullable: false,
})
eventId: string;
...
#ManyToMany("Note","gcalevents")
notes: Note[]
}
For example:
{
phone: string
email: string
....
....
otherFields
....
....
}
Either phone or email should be required in request body. I found solution only for validation, here is:
export class MyDto {
#IsString()
#ValidateIf((obj, value) => !obj.email || value)
phone: string;
#IsString()
#ValidateIf((obj, value) => !obj.phone || value)
phone: string;
..... other fields .....
}
However, how to apply swagger documentation for this?
There are oneOf and anyOf in OAS3 docs. Here is suitable solution in terms of swagger:
link
How is it possible to implement both validation and swagger docs at the same time?
The only alternative that I see for your problem is this.
export class EmailDto {
#ApiProperty()
#IsString()
email: string;
}
export class PhoneDto {
#ApiProperty()
#IsString()
phone: string;
}
export class CreateCatDto {
#ApiProperty({
oneOf: [
{
type: 'object',
required: ['phoneNumber'],
properties: {
phoneNumber: { type: 'string' },
},
},
{
type: 'object',
required: ['email'],
properties: { email: { type: 'string' } },
},
],
})
#ValidateNested()
#Type((type) => (type.object.phoneOrEmail.email ? EmailDto : PhoneDto))
phoneOrEmail: PhoneDto | EmailDto;
}
Here you have the documentation for anyOf in NestJs
I didn't use getSchemaPath because my schema wasn't initialised by Swagger so I create manually my schema in #ApiProperty
And for the class-validator I ask to validate the DTO for the right Type with #ValidateNested() and #Type().
The problem is. If they sent you both email and phoneNumber class-validator will only validate email because:
(type.object.phoneOrEmail.email ? EmailDto : PhoneDto)
If there is an email, it will validate the class EmailDto first. So you need to tell if there are Email and PhoneNumber you need to validate a DTO with both.
Go check on Google if there is an anyOf like for class-validaor
I have tried adding toJSON(){return classtoPlain(this)} in my entity still its not working.
Here is my controller
#Controller('users')
#UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
#HttpCode(200)
#Post()
async create(#Body() user: User): Promise<User> {
return await this.usersService.create(user).catch((err) => {
throw new HttpException({ message: err.message }, HttpStatus.BAD_REQUEST);
});
}
Here is my Entity
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Gender } from '../Constants/enum';
import { Exclude, instanceToPlain } from 'class-transformer';
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number;
#Column()
firstName: string;
#Column()
lastName?: string;
#Column({
type: 'enum',
enum: Gender,
})
gender: Gender;
#Column({ unique: true })
email: string;
#Column()
#Exclude({ toPlainOnly: true })
password: string;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
toJSON() {
return instanceToPlain(this);
}
}
Here is my service
import { Injectable } from '#nestjs/common';
import { InjectRepository } from '#nestjs/typeorm';
import { Repository } from 'typeorm';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
#Injectable()
export class UsersService {
constructor(
#InjectRepository(User)
private readonly user_detailsRepository: Repository<User>,
) {}
create(user: User) {
return this.user_detailsRepository.save(user);
}
I have tried many solutions but still nothing works.
For GET its working but for post its showing the password field.If any one can provide any solution it would be great.
You're mixing between Entity definition/validation, which typeORM functions/decorators (in your case) should handle. And DTO definition, which class-transformer/validator should handle
So you should define a createUserDTO.ts file like below, and use it as in the controller/service file:
export class createUserDTO {
#IsNotEmpty()
#IsString()
firstName: string;
#IsNotEmpty()
#IsString()
lastName?: string;
#IsNotEmpty()
#IsEnum(Gender)
gender: Gender;
#IsNotEmpty()
email: string;
#Exclude({ toPlainOnly: true })
password: string;
}
The entity file shouldn't have the password column (just omit the field completely), should look like this:
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number;
#Column()
firstName: string;
#Column()
lastName?: string;
#Column({
type: 'enum',
enum: Gender,
})
gender: Gender;
#Column({ unique: true })
email: string;
}
Then, when you call return this.user_detailsRepository.save(userDto);, it should work fine
Can I use class-validator to validate Entity columns?
This doesn't validate the columns:
import { IsEmail } from 'class-validator';
#Entity()
export class Admin extends BaseEntity {
#Column({ unique: true })
#IsEmail()
email: string;
}
However when I use the class-validator anywhere else in the code other than entities it validates properly and doesn't allow for bad inputs.
This works:
#InputType()
export class RegisterInput {
#Field()
#IsEmail()
email: string;
}
The Entity should be clean
#Entity()
export class Admin extends BaseEntity {
#Column({ unique: true })
email: string;
}
While you define a DTO for checking the incoming request.
export class AdminDto {
#IsEmail()
email: string;
}
In your controller, you would check the incoming request with your AdminDto.
#Controlller('route')
export class AdminController {
constructor(private yourService: yourserviceClass)
#Post('/api')
async createSomething(#Body('email', ValidationPipe) email: string){ //Your request body //would be checked against your DTO
this.yourService.createMethod(email)
}
Hope this answers your question.
some-dto.ts
export class CreateCatDto {
#ApiProperty()
name: string;
#ApiProperty()
age: number;
#ApiProperty()
breed: string;
}
I don't want response something like this:
#ApiOkResponse(
description: 'Cat object',
type: CreateCatDto
)
but my response must be array of like dto objects.
I want smth like soo
ApiOkResponse(
description: 'The record has been successfully removed.',
schema: {
type: 'array',
properties: {
obj: {
type: CreateCatDto
}
}
}
)
have you tried something like this:
#ApiOkResponse(
description: 'Cat object',
type: CreateCatDto,
isArray: true // <= diff is here
)
Let me know if it helps
I found another solution we can wrap in array like this
#ApiOkResponse(
description: 'Cat object',
type: [CreateCatDto]
)
And if you need to deal with multiple types :
#ApiOkResponse({
description: 'More or less dangerous animals',
schema: {
type: 'array',
items: {
oneOf: [
{ $ref: getSchemaPath(CreateCatDto) },
{ $ref: getSchemaPath(CreateAlligatorDto) }
],
},
},
})