Class-Transformer: access request object or pass in additional metadata? - nestjs

I need to transform some file URL's in the response, the file paths need to contain the client id,
I have a shared class 'AssetReponseImagesDTO' this is used in lots of other classes, I need to transform the value of filename from a local path to append the CDN URL and client id and return a proper URL
I can get the CDN URL from the configService as it's just a string, to get the value of ClientId i have a custom decorator for nest.js - tried to use this decorator to access the same Id within the response but it doesn't work - seems to just return a string of the actual function
I also tried to create a new ClientId property on my top-level response DTO however the lower level Type such as AssetResponseImagesDTO won't have access to the parent's properties (unless I'm mistaken?)
Is there a way to pass in additional information to class-transformer which can then be referenced during the transform process?
export class AssetResponseImagesDTO {
#Expose({ name: 'url' })
#Transform(({ value }) =>
value ? `${configService.getCDNUrl()}${NEED_CLIENT_ID_HERE}}/asset/${value}` : null,
)
filename: string;
#Expose()
caption: string;
#Expose({ name: 'link' })
url: string;
}
Updated for more detail
Top-level response DTO
export class NewVehicleDetailResponseWrapperDTO {
#Type(() => NewVehicleResponseDTO)
vehicle: NewVehicle;
#Type(() => AssetResponseDTO)
assets: Asset[];
constructor(vehicle, assets) {
this.vehicle = vehicle;
this.assets = assets;
}
}
AssetResponseDTO
#Exclude()
export class AssetResponseDTO {
#Expose({ name: 'type' })
#Transform(({ value }) => AssetType[value])
assetType: AssetType;
#Expose({ name: 'name' })
scrapName: string;
#Expose({ name: 'images' })
#Type(() => AssetResponseImagesDTO)
assetImages: AssetImages[];
#Expose({ name: 'snippet' })
#Transform(({ value }) => value?.snippet)
#Type(() => AssetSnippets)
assetSnippet: AssetSnippets;
#Expose({ name: 'content' })
#Transform(({ value }) => value?.article)
#Type(() => AssetContents)
assetContent: AssetContents;
}
ClientId Decorator
export const ClientId = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
// We need to set the clientId either via a query param or from the logged in user.
let clientId;
if (request.user.clientId) {
clientId = request.user.clientId;
}
if (request.headers['x-client-id']) {
clientId = request.headers['x-client-id'];
}
if (!clientId) {
throw new BadRequestException();
}
return clientId;
},
);

Related

Can apollo-upload file be optional? NestJS code-first approach

I have an upload funtion using apollo-upload, and I want to validate if the file is provided in the request or not, but I'm messing around and can't find any solution.
I have a Mutation like below:
async someFn(
#Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, filename }: FileUpload
)
If the file was not provided, the server return INTERNAL_SERVER_ERROR that I didn't want to.
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
I tried validate the file inside the resolvers but it not working, maybe because the FileUpload path is required. I still can't figure out is there any way that I can catch the error when the file is empty?
UPDATE: just wrap input into InputType class and we easy to customize input type and the interface
export class MyInput {
#Field(() => GraphQLUpload, { nullable: true})
file?: FileUpload;
}
-----
export interface FileUpload {
filename: string;
mimetype: string;
encoding: string;
createReadStream: () => Stream;
}
-----
#Args('payload', { nullable: true }), payload: MyInput)

Show additional data in the response from another table that is connected via a many-to-many relation by TypeORM in NestJS

I have four tables users, ingredients, recipes and a connection table ingredients_ingrec_recipes that TypeORM created automatically. This connection table consists of two columns: ingredientsId and recipesId.
In the ingredient.entity.ts file I have defined #ManyToMany relation with { eager:true } between ingredients and recipes. When I make a Get-Request for a specific ingredient (via Postman), I also see the associated recipes in the response automatically, thanks to the magic of { eager: true } and TypeORM+NestJS.
Now I need exactly the same, but for recipes (other side). In the recipe response, the corresponding ingredients should also be displayed. Unfortunately, according to TypeORM, I can only use #ManyToMany with { eager: true } in one of the two entities. In order to achieve the same with recipes, I have to do this somehow via leftJoinAndSelect(). However, I don't know how to do this via this connection table ingredients_ingrec_recipes in my code.
Here is my code:
ingredient.entitiy.ts
#Entity({ name: 'ingredients' })
export class IngredientEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
slug: string;
#Column()
name: string;
#Column({ default: '' })
description: string;
// more fields
#ManyToMany(() => RecipeEntity, { eager: true })
#JoinTable()
ingrec: RecipeEntity[];
#ManyToOne(() => UserEntity, (user) => user.recipes, { eager: true })
author: UserEntity;
}
recipe.entity.ts
#Entity({ name: 'recipes' })
export class RecipeEntity {
#PrimaryGeneratedColumn()
id: number;
#Column()
slug: string;
#Column()
title: string;
#Column({ default: '' })
description: string;
#Column({ default: '' })
body: string;
// more fields
#ManyToOne(() => UserEntity, (user) => user.recipes, { eager: true })
author: UserEntity;
}
recipe.service.ts
async findAll(query: any) {
const queryBuilder = getRepository(RecipeEntity)
.createQueryBuilder('recipes')
.leftJoinAndSelect('recipes.author', 'author');
queryBuilder.orderBy('recipes.createdAt', 'DESC');
if (query.author) {
const author = await this.userRepository.findOne({
username: query.author,
});
queryBuilder.andWhere('recipes.authorId = :id', {
id: author.id,
});
}
const recipes = await queryBuilder.getMany();
return { recipes };
}
I have already used leftJoinAndSelect() with the author, but in a #OneToMany relationship. How can I adjust my findAll() service function so that I can still see in my recipe-response the data from my user (author) table and also the associated data from the ingredients?
ingredientsId
recipesId
3
1
1
2
3
3
3
4
If I add following to the RecipeEntity:
#ManyToMany(() => IngredientEntity, { eager: true })
ingrec: IngredientEntity[];
I get an server error and [Nest] 230 - 05/17/2022, 3:02:11 PM ERROR [ExceptionsHandler] Maximum call stack size exceeded
Without { eager: true } I don't get any errors but I also don't see any data from the ingredients table.

Class-Validator (Node.js) Get another property value within custom validation

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.

How can i post a DTO that contain an array of entities?

I have a couple of questions about NestJS and TypeOrm.
First, how to pass an array of strings to DTO? I tried just to use :string[] type, but the compiler gives an error.
This is my Post entity:
#Entity('posts')
export class Post {
#PrimaryGeneratedColumn()
id: number;
#ManyToOne(() => User, user => user.posts, { cascade: true })
author: number;
#Column({ type: 'timestamp' })
date: Date;
#Column()
text: string;
#Column({ default: 0 })
likes: number;
#OneToMany(() => Photo, photo => photo.post, { cascade: true })
photos: Photo[];
}
And CreatePostDto:
export class CreatePostDto {
authorId: number;
date: Date;
text?: string;
// photos?: string[];
}
And the second question: How can i save to the repository every photo (keeping the connection with post), posts to the posts repo and update user by adding new post binded to him.
I tried something like this, but it won't work obviously.
async create(createPostDto: CreatePostDto) {
const post = this.postsRepository.create(createPostDto);
const user = await this.usersRepository.findOne(createPostDto.authorId);
return this.postsRepository.save({author: user, date: createPostDto.date, text: createPostDto.text});
}
What you missed here is saving photos before bind them with the post, here's an example:
async create(createPostDto: CreatePostDto) {
let photos:Array<Photo> = [] ; array of type photo entity
for(let urlPhoto of createPostDto.photos)
{
let photo = await this.imageRepository.save({url : urlPhoto }); you must save the photos first
photos.push(photo);
}
const user = await this.usersRepository.findOne(createPostDto.authorId);
return this.postsRepository.save({author: user, date: createPostDto.date, text:
createPostDto.text,photos:photos});
}
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number
#Column("simple-array")
names: string[]
}

How to write #Param and #Body in one DTO

I am using nestjs and I make an effort DTO's and I generate update-todo.dto.ts like this.
How can I use #Param and #Body together in one DTO?
#Param('id') id: string,
#Body('status') status: TodoStatus
So how to convert my code?
import { TodoStatus } from '../todo.model';
export class UpdateTodoDto {
id: string;
status: TodoStatus;
}
#Patch('/:id/status')
updateTodoStatus(
#Param('id') id: string,
#Body('status') status: TodoStatus
// convert this line
): Todo {
return this.todosService.updateTodoStatus(id, status);
}
You'd need, four components to work together.
A custom decorator to combine the #Param() and #Body() decorators
A DTO to hold the shape of the #Param() DTO
A DTO to hold the shape of the #Body() DTO
A DTO to combine the body and param DTOs
This repository goes through an example with an optional body based on a query parameter.
1
export const BodyAndParam = createParamDecorator((data: unknwon, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest();
return { body: req.body, params: req.params };
});
2
export class ParamsDTO {
#IsString()
id: string;
}
3
export class BodyDTO {
#IsString()
hello: string;
}
4
export class MixedDTO {
#Type(() => ParamsDTO)
params: ParamsDTO;
#Type(() => BodyDTO);
body: BodyDTO;
}
Use
#Controller()
export class FooController {
#Post()
bar(#BodyAndParam() bodyAndParam: MixedDTO) {
// do stuff here
}
}

Resources