Perform a few custom async validations with class-validator - node.js

I would like to have the following dto:
export class SetEntryPasswordDto {
#ApiProperty()
#Validate(EntryBelongsToUser)
#Validate(EntryIsNotLocked)
#Type(() => Number)
#IsNumber()
id: number;
#ApiProperty()
#IsString()
#IsNotEmpty()
#Validate(PasswordMatchValidator)
#Matches(EValidator.PASSWORD, { message: 'password is not strong enough' })
password: string;
#ApiProperty()
#IsNotEmpty()
#IsString()
confirmPassword: string;
#ApiProperty()
#IsOptional()
#IsString()
passwordHint?: string;
#IsNumber()
userId: number;
}
The problem with it is that I need to do a couple of async validations and I would like to use the class-validator lib for this.
My question is: if I do this like in the code snippet above, can I be sure that the first one to complete is EntryIsNotLocked? If not then how to make those validation execute in order?
Thank you.
Additional information:
Seems like there's a bit of information that is of importance.
The EntryBelongsToUser and EntryIsNotLocked are the ValidatorConstraint classes. For instance, one of them looks as follows:
#ValidatorConstraint({ name: 'EntryIsNotLocked', async: false })
#Injectable()
export class EntryIsNotLocked implements ValidatorConstraintInterface {
constructor(
private readonly entryService: EntryService,
) {}
public async validate(val: any, args: ValidationArguments): Promise<boolean> {
// here goes some validation logic
}
public defaultMessage(args: ValidationArguments): string {
return `Unauthorized to execute this action`;
}
}
The second one looks exactly the same. So the question is can I guarantee the order by setting the async option of the ValidatorConstraint decorator to false for both of them?

No, you can't be sure of the sequential order of async functions. Thet's why you have validateSync method in class-validator package. You can use the validateSync method instead of the regular validate method to perform a simple non async validation.
See this for reference.

Related

NestJs class-validator accepts empty payload

I'm trying to create validator that accepts 2 values as strings (must exist, min/max length etc).
The issue I am facing is that when I POST empty payload the validation passes and TypeORM tries to insert null values and I end up with HTTP status 500.
When I POST with invalid payload the validation works properly.
I want to get proper validation errors as a response when payload is empty (existence of name property, min/max length etc...) ...
I tried adding various class annotations and global settings but no luck...
Global validation enabled:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
skipMissingProperties: false, //Thought this would check for missing properties
transform: true,
}),
);
Entity:
#Entity('r_cat')
export class ResearchCategory {
[...]
#PrimaryGeneratedColumn()
id: number;
#ApiProperty({
description: 'Name of the category',
example: 'Analytics, Integration',
})
#Column('text')
#IsString()
#ApiProperty()
#Length(2, 30)
#IsNotEmpty()
#IsDefined()
name: string;
[...]
My request object (DTO):
export class CreateResearchCategoryRequest extends PartialType(
OmitType(ResearchCategory, ['created_at', 'updated_at', 'deleted_at'] as const),
) {}
Controller:
#Post()
public async create(
#Req() req,
#Body() researchCategory: CreateResearchCategoryRequest,
): Promise<ResearchCategory> {
return await this.service.createNew(researchCategory, req.user);
}
I'm not sure this is the correct way to create a DTO based on the repository class. The usage of the mapper is different from what you've done since Nest examples show is all based on another DTO class, not a repository class.
However, I think the following snippet should work in your situation:
export class CreateResearchCategoryRequest extends OmitType(
ResearchCategory, ['created_at', 'updated_at', 'deleted_at'] as const
) {}

Is it possible to maintain flat input structure with nestjs/graphql Args?

I have a simple query:
#Query(() => ProfileOutput)
async profile(#Args('id') id: string) {
return await this.profileFacade.findProfileById(input.id);
}
The problem is that I want to apply #IsMongoId() from class-validator for the id here. I do not want to create new #InputType here, because I do not want to change API specification. Is there a way to apply validator like #IsMongoId here without the need to change query definition for frontend?
For anyone seeking an answer I found a feature called dedicated argument class. Instead of creating new input type as I thought like this:
#InputType()
export class MongoIdBaseInput {
#IsMongoId()
#Field()
id: string;
}
#Query(() => ProfileOutput)
async profile(#Args('data') input: MongoIdBaseInput) {
return await this.profileFacade.findProfileById(input.id);
}
We can define it almost the same, but annotate input with ArgsType it will maintain flat structure of args for us:
#ArgsType()
export class MongoIdBaseInput {
#IsMongoId()
#Field()
id: string;
}
#Query(() => ProfileOutput)
async profile(#Args() input: MongoIdBaseInput) {
return await this.profileFacade.findProfileById(input.id);
}

Circular dependency between entities

i have three entities TestUser , TestProfile and TestPhoto in which TestUser has a OneToOne relationship with TestProfile and TestProfiles has a OneToOne relationship with TestPhoto and at the las TestPhoto has this ManyToOne relationship with User which might has not been created yet
im using cascade when defining my entites and i wish to have them all get created with a single call in my UserService but facing this Cyclic dependency: "TestPhoto" Error and had no progress since then , i see its not probably what is should do in real life scenarios but apart from that ,any possible hack for it or its just fundamentally not possible?
#Entity()
#Unique(["name"])
export class TestUser {
#PrimaryGeneratedColumn()
id: number;
#Column()
name: string;
#OneToOne(() => TestProfile,{
cascade:true,
nullable:true
})
#JoinColumn()
profile: TestProfile;
#Column({nullable:true})
profileId: number
#OneToMany(() => TestPhoto, photo => photo.user)
photos: TestPhoto[];
}
#Entity()
export class TestProfile {
#PrimaryGeneratedColumn()
id: number;
#Column()
gender: string;
#OneToOne(type=>TestPhoto,{
cascade:true,
nullable:true
})
#JoinColumn()
photo: TestPhoto;
#Column({nullable:true})
photoId: number
}
#Entity()
export class TestPhoto {
#PrimaryGeneratedColumn()
id: number;
#Column()
url: string;
#ManyToOne(() => TestUser, user => user.photos,{
cascade:true,
nullable:true
})
user: TestUser;
#Column({nullable:true})
userId: number;
}
and in my UserService abstracted the calls as followed
const user = new TestUser();
const profile1 = new TestProfile();
const photo1 = new TestPhoto();
photo1.user = user;
profile1.photo = photo1;
user.profile = profile1
await connection.manager.save(user);
Does these entities are living in the same file?
I use import type TS's feature to resolve cyclic dependencies at module resolution level. I'm not sure if that is your case tho.
Before you write code please feel free to understand the concept of circular dependency; Link. It is possible to have circular dependency in your case but might not be in real life scenarios. What you have to do is make your Entity/Modal a forwardRef on both side. Then make services inject-able to others using #Inject(forwardRef(() => YourService)) inside constructor of another service. If you did not get an idea I will post a complete example of how circular dependency works in your case and in real life scenarios.
I solved this issue with typeorm Relation as explaine here tyopeorm entities circular dependencies
I use a common import file when there is a circular dependency problem:
// common.ts
// also it's important to keep order, parent first and then...
export * from './parent.entity';
export * from './child1.entity';
export * from './child2.entity';
// parent.entity.ts ------------------------------------
import { Child1Entity, Child2Entity } from './common.ts'
export class ParentEntity {
#OneToOne(() => Child1Entity, child => child.parent)
public child1: Child1Entity[];
#OneToOne(() => Child2Entity, child => child.parent)
public child2: Child2Entity[];
}
// child1.entity.ts ----------------------
import { ParentEntity } from './common.ts'
export class Child1Entity extends Parent {
#OneToOne(() => ParentEntity, parent => parent.child1)
#JoinColumn()
public parent: ParentEntity;
}
// child2.entity.ts ----------------------
import { ParentEntity } from './common.ts'
export class Child2Entity extends Parent {
#OneToOne(() => ParentEntity, parent => parent.child2)
#JoinColumn()
public parent: ParentEntity;
}
This pattern can help you with various circular dependency problems

How to set default values for DTO in nest js?

I want to set default values for a node in a DTO. So if the value for that node is not passed, a default value will be used. Although this is working, I want the node should be present, the values is optional.
import { IsNotEmpty, IsDefined } from "class-validator";
export class IssueSearch
{
#IsDefined()
search: string;
#IsNotEmpty()
length: number = 10;
#IsNotEmpty()
lastId: string = "0"
}
This doesn't serve my purpose. I want the url to be searched like so
http://base-url/folder?search=value
If the value is not passed it should not throw an error.
But if the param search is not there it should throw an error.
If you want to set a default value go to entity and set in the field for example in mongo db
export class DumpDoc extends Document {
#Prop()
title: string;
#Prop({ default: new Date() }) //set as default
createdAt: string;
}

Nest.js + Mikro-ORM: Collection of entity not initialized when using createQueryBuilder and leftJoin

I'm using Nest.js, and considering migrating from TypeORM to Mikro-ORM. I'm using the nestjs-mikro-orm module. But I'm stuck on something that seems very simple...
I've 3 entities, AuthorEntity, BookEntity and BookMetadata. From my Author module, I try to left join the Book and BookMetadata tables with the createQueryBuilder method. But when running my query, I'm getting an error where Collection<BookEntity> of entity AuthorEntity[3390] not initialized. However columns from the Author table are well retrieved.
My 3 entities:
#Entity()
#Unique({ properties: ['key'] })
export class AuthorEntity {
#PrimaryKey()
id!: number;
#Property({ length: 255 })
key!: string;
#OneToMany('BookEntity', 'author', { orphanRemoval: true })
books? = new Collection<BookEntity>(this);
}
#Entity()
export class BookEntity {
#PrimaryKey()
id!: number;
#ManyToOne(() => AuthorEntity)
author!: AuthorEntity;
#OneToMany('BookMetadataEntity', 'book', { orphanRemoval: true })
bookMetadata? = new Collection<BookMetadataEntity>(this);
}
#Entity()
#Unique({ properties: ['book', 'localeKey'] })
export class BookMetadataEntity {
#PrimaryKey()
id!: number;
#Property({ length: 5 })
localeKey!: string;
#ManyToOne(() => BookEntity)
book!: BookEntity;
}
And the service file where I run my query:
#Injectable()
export class AuthorService {
constructor(
#InjectRepository(AuthorEntity)
private readonly authorRepository: EntityRepository<AuthorEntity>,
) {}
async findOneByKey(props: { key: string; localeKey: string; }): Promise<AuthorEntity> {
const { key, localeKey } = props;
return this.authorRepository
.createQueryBuilder('a')
.select(['a.*', 'b.*', 'c.*'])
.leftJoin('a.books', 'b')
.leftJoin('b.bookMetadata', 'c')
.where('a.key = ?', [key])
.andWhere('c.localeKey = ?', [localeKey])
.getSingleResult();
}
}
Am I missing something? Might be not related, but I also noticed that there is a special autoLoadEntities: true for TypeORM users using Nest.js. Is there something similar for Mikro-ORM? Thanks ;)
Mapping of multiple entities from single query is not yet supported, it is planned for v4. You can subscribe here: https://github.com/mikro-orm/mikro-orm/issues/440
In v3 you need to use 2 queries to load 2 entities, which for your use case is much easier without the QB involved.
return this.authorRepository.findOne({ key }, ['books']);
Or you could use qb.execute() to get the raw results and map them yourself, but you would also have to manually alias all the fields to get around duplicities (Author.name vs Book.name), as doing qb.select(['a.*', 'b.*']) will result in query select a.*, b.* ... and the duplicate columns would not be correctly mapped.
https://mikro-orm.io/docs/query-builder/#mapping-raw-results-to-entities
About the autoLoadEntities thing, never heard of that, will take a look how it works, but in general, the nestjs adapter is not developed by me, so if its something only nest related, it would be better to ask on their GH repo.
Or you could use folder based discovery (entitiesDirs).
here is the new example with 3 entities:
return this.authorRepository.findOne({
key,
books: { bookMetadata: localeKey } },
}, ['books.bookMetadata']);
This will produce 3 queries, one for each db table, but the first one will auto-join books and bookMetadata to be able to filter by them. The condition will be propagated down in the second and third query.
If you omit the populate parameter (['books.bookMetadata']), then only the first query will be fired and you will end up with books not being populated (but the Author will be queried with the joined condition).

Resources