I'm working on a DTO(data transfer object) where I put directly validations based on what class validator provides
here is a sample:
import { IsDefined, IsNotEmpty, Min, ValidatePromise } from 'class-validator';
export class UpdateMentorParamDto {
#IsDefined()
#IsNotEmpty()
#Min(1, {
message: 'the Id in the params is supposed to be a valid id number greater than 0',
})
#ValidatePromise()
id: number;
}
This is how I'm using it in the controller
import { UpdateMentorParamDto } from './dtos/update-mentor-param.dto';
import { Controller, Put, Param, UsePipes, ValidationPipe } from '#nestjs/common';
#Controller('/users')
export class UsersController {
#Put('/:id')
#UsePipes(ValidationPipe)
updateMentor(#Param() params: UpdateMentorParamDto) {
// some code
}
}
I saw here that it is possible to do Promise validation with class-validator.
this is how they suggest we do it:
import {ValidatePromise, Min} from "class-validator";
export class Post {
#Min(0)
#ValidatePromise()
userId: Promise<number>;
}
My question is this:
Before I proceed with the update, I need to make sure that the record whose Id is provided in the params actually exists in the database. I assume it is possible to do that with the Promise validation approach suggested on the readme of class-validator.
If that is really possible, how do I do that with the structure that I've shown above?
Related
I have created a Custom ValidatorConstraint in Nestjs from class-validator, just to create my own decorator and apply later to DTO classes for validations.
Imagine this route.
foo/:client
after request it, I just want to check that client contains some pattern
client --> XXX123 ✘
client --> ZZZ123 ✔
I am struggling with it and although I saw some examples, it is still not very clear why it fails.
main.ts
app.useGlobalPipes(new ValidationPipe());
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.module.ts
providers: [..., IsValidClientConstraint],
app.controller.ts
#Get(':client')
getHello(#Param('client') client: ClientDTO): string {
custom.validator.ts
import { registerDecorator, ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
import { Injectable } from '#nestjs/common';
#ValidatorConstraint({ async: false })
#Injectable()
export class IsValidClientConstraint implements ValidatorConstraintInterface {
validate(client: any, args: ValidationArguments) {
console.log(client)
return client.includes('ZZZ');
}
}
export function IsValidClient(validationOptions?: ValidationOptions) {
return function (object: Object, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsValidClientConstraint,
});
};
}
client.dto.ts
export class ClientDTO {
#IsValidClient({ message: 'blabla' })
client: string;
}
However doing a request with -> foo/XXX344
ERROR [ExceptionsHandler] Cannot read properties of undefined
So it is not receiving the value of the client itself
What am I missing there?
I with leave the repo here
https://github.com/ackuser/nestjs-sample-custom-validator
Thanks,
I appreciate any help
You don't have to pass parameter name to #Param decorator when you want to use class-validator to validate params, So change it to #Param() params: ClientDTO instead.
Use custom pipes if you want to validate each parameter one by one. because the DTO method you used turns all parameters (not just :client) into a single data class.
Also in IsValidClientConstraint check if client is defined before using it.
I'm using Prisma to implement a GraphQL interface to expose some data stored in a PostgreSQL database. My code is inspired by the GraphQL Tools (SDL-first) example. This logic is pretty inefficient though and I'd like to improve it.
Here is a minimal piece of code to show the problem and ask for a solution. My real code is of course more complicated.
My GraphQL schema
type Query {
allUsers: [User!]!
}
type User {
name: String!
posts: [Post!]!
}
type Post {
text: String!
author: User!
}
My resolver object, in the Node.JS code
const resolvers = {
Query: {
allUsers: ()=>prisma.users.findMany()
},
User: {
posts: (user)=>prisma.posts.findMany({where:{author:user.id}})
}
};
Problems
This code works but it's inefficient. Imagine you're running the query {allUsers{posts{text}}}:
My code runs N+1 queries against PostgreSQL to fetch the whole result: one to fetch the list of the users, then other N: one for each user. A single query, using a JOIN, should be enough.
My code selects every column from every table it queries, even though I only need user.id and don't need user.name or anything else.
Question
I know that Prisma supports nested searches (include and select options) which could fix both problems. However I don't know how to configure the options object using the GraphQL query.
How can I extract from the GraphQL query the list of fields that are requested? And how can I use these to create to options object to perform an optimal nested-search with Prisma?
This package can help you parse the request info: https://www.npmjs.com/package/graphql-parse-resolve-info
Then you need to transform it to a usable parameter that you can use in your ORM.
Here is an example with NestJS:
import {createParamDecorator, ExecutionContext} from '#nestjs/common';
import {GqlExecutionContext} from '#nestjs/graphql';
import {GraphQLResolveInfo} from 'graphql';
import {parseResolveInfo, ResolveTree} from 'graphql-parse-resolve-info';
export type PrismaSelect = {
select: {
[key: string]: true | PrismaSelect;
};
};
export const Relations = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const info = GqlExecutionContext.create(ctx).getInfo<GraphQLResolveInfo>();
const ast = parseResolveInfo(info);
return astToPrisma(Object.values((ast as ResolveTree).fieldsByTypeName)[0]);
},
);
export const astToPrisma = (ast: {
[str: string]: ResolveTree;
}): PrismaSelect => {
return {
select: Object.fromEntries(
Object.values(ast).map(field => [
field.name,
Object.keys(field.fieldsByTypeName).length === 0
? true
: astToPrisma(Object.values(field.fieldsByTypeName)[0]),
]),
),
};
};
Then you do:
import {Parent, Query, ResolveField, Resolver} from '#nestjs/graphql';
import {PrismaService} from '../services/prisma.service';
import {User} from '../entities/user.entity';
import {Relations} from 'src/decorators/relations.decorator';
import {Prisma} from '#prisma/client';
#Resolver(() => User)
export class UserResolver {
constructor(public prisma: PrismaService) {}
#Query(() => [User])
async usersWithRelationsResolver(
#Relations() relations: {select: Prisma.UserSelect},
): Promise<Partial<User>[]> {
return this.prisma.user.findMany({
...relations,
});
}
Alternatively, if you want to solve the N+1 problem you can use Prisma built-in findUnique method. See https://www.prisma.io/docs/guides/performance-and-optimization/query-optimization-performance#solving-the-n1-problem
I am trying to write a function to handle the Get request, here is my code:
#Get('/find')
async find(#Param() testname: NameDto) {
console.log(testname.name);
}
Here is my dto:
export class NameDto {
#IsString()
#ApiProperty({ required: true })
name: string;
}
I am using the Swagger to test this API :
When I input a signle a , I got the following response:
{
"statusCode": 400,
"message": [
"name must be a string"
],
"error": "Bad Request"
}
here are more input example :
They all return the same response.
Then, I change the find function like this with #Query:
#Get('/find')
async find(#Query() testname: NameDto) {
console.log(testname.name);
}
here is my input :
I can have the 200-ok response.
Here is another example:
I enter 1 as the input, and I still can get the 200 response. The dto is not working as expected.
Am I missing something?
Any help would be appreciate.
you are sending nothing in param
you can read in Nestjs document
try this one:
#Get('/find/:testname')
async find(#Param('testname') testname: NameDto) {
console.log(testname.name);
}
You're using query parameters for the url, so you do need to use #Query(). Query parameters will always come in as strings, because that's how express and fastify work. You can enable transform: true and {transformOptions: { enableImplicitConverion: true } } in the ValidationPipe's options and get numbers to show up as actual numbers.
you can do something like this it's works for me:
//find-one.dto.ts
import { IsNotEmpty, IsUUID } from 'class-validator';
export class FindOneParams {
#IsUUID()
#IsNotEmpty()
uuid: string;
}
//controller.ts
#Get(':uuid')
find(#Param() { uuid }: FindOneParams) {
return this.yourService.find(uuid);
}
//service.ts
find = (uuid: string): Promise<yourType> => ...
When the query result Entity[] is directly returned from the controller, the #Transform defined in the entity can take effect normally, but when returning data such as {datalist: Entity[]}, it is found that the method in #Transform is not executed
[Google Translate ~]
entity
import { Transform } from 'class-transformer';
import {CreateDateColumn, Entity} from 'typeorm';
#Entity({ name: 't_articles' })
export class ArticleEntity {
...
#Transform((v) => {
console.log(123);
return new Date(v).toLocaleString();
})
#CreateDateColumn()
create_time: Date;
...
}
controller
const [datalist, count] = await this.articleRepository.findAndCount({skip, take, where});
return datalist // ===> transformed
return {datalist} // ===> untransformed, and '123' is not printed
If you are using NestJs make sure you have:
app.useGlobalPipes(
new ValidationPipe({
transform: true
})
);
I want to return a different response from a Game controller depending on whether or not a User owns the game or is simply invited to it. Essentially: filter out certain attributes from the response if the user is only invited to the game.
Here's a naive implementation of what I want using two different controllers:
#SerializeOptions({
groups: ['invited'],
})
#Get(':id')
async findOne(#User() user, #Param('id') id: string) {
const retGame = await this.gamesService.findOne(id);
const ability = this.caslAbilityFactory.createForUser(user);
Throw401(ability.can(Action.Read, retGame));
return retGame;
}
#SerializeOptions({
groups: ['owner'],
})
#Get('full/:id')
async findOneFull(#User() user, #Param('id') id: string) {
const retGame = await this.gamesService.findOne(id);
const ability = this.caslAbilityFactory.createForUser(user);
Throw401(ability.can(Action.FullRead, retGame));
// the main difference is ^^^^^^^^, using a different CASL rule for authorization
return retGame;
}
I'm using a different set of CASL rules to allow a "full read" or not, the full read being only allowed for the game owner. That way I can attach a different group through the SerializeOptions decorator, which allows me to conditionally expose an entity attribute:
#Column()
#Expose({ groups: ['owner'] })
inviteKey: string;
But it feels wrong to use different routes and methods to basically do the same thing, I'd like to pass a dynamic condition (user.id === ownerId) instead of a group to the Expose decorator, and I believe the best next thing would be to use an interceptor to filter certain fields from the response. I'm not sure how to proceed from there, would an interceptor be the right approach?
I have stumbled upon the same problem, where it makes sense to have one route but based on user rights give the result with some attributes filtered out from the response. Disclaimer: Since I'm fairly new to nestjs there are some gaps in my knowledge and it might be possible that it is not a proper way to achieve the intended result.
To achieve this you'd have to:
Use your already defined class with exposed/excluded attributes - using class-transformer and groups should add/remove attributes from the class when transforming to json object
Create an interceptor - this will intercept the response before it gets sent to clinet
Use the interceptor for desired routes
So in your case, it could look something like this:
db/models/game.model.ts
import { Exclude, Expose } from 'class-transformer';
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
#Entity()
export class Game {
#Exponse()
#PrimaryGeneratedColumn()
id: number;
#Column()
#Expose({ groups: ['owner'] })
inviteKey: string;
#Column()
#Expose({ groups: ['owner', 'invited']})
name: string;
}
transform.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '#nestjs/common';
import { classToPlain } from 'class-transformer';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Game } from 'db/models/game.model';
export interface Response<T> {
data: T;
}
#Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
// This is how you can get access to requesting user (asuming it is in the request)
const request = context.switchToHttp().getRequest();
const user = request.user;
/* get your group based on the CASL rules. */
const group = 'invited';
// Transform the data (filter out private fields)
return next.handle().pipe(map((data) => {
if (data instanceof Game) {
return classToPlain(game, { groups: [group] });
}
// in case there is Game[]
if (
Array.isArray(data)
&& data[0] instanceof Game
) {
return data.map(game => classToPlain(game, { groups: [group] }));
}
// in case response is something else, don't touch it
return data;
}));
}
}
games.controller.ts
// Use the interceptor here - you no longer need `/games/full/:id` route
#UseInterceptors(TransformInterceptor)
#Get(':id')
async findOne(#User() user, #Param('id') id: string) {
// Return the Game instance here, interceptor will take care of transforming it
return this.gamesService.findOne(id);
}
As a result to your GET /games/:id as user with Action.Read access the output should be
{
"id": 1,
"name": "Best game in the business"
}
As a result to your GET /games/:id as user with Action.FullRead access the output should be
{
"id": 1,
"inviteKey": "super-secret-invite-key",
"name": "Best game in the business"
}
Hopefully this helps.