I create POST endpoint to create a new entity.
I also created schema for mongoose with field userId (to connect this entity to specified user) and DTO which I use on my POST method.
#UseGuards(JwtAuthGuard)
#Post("/")
createAction(#Request() req, #Body() createActionDto: CreateActionDto) {
return this.actionService.createAction(req?.user?.userId, createActionDto);
}
DTO:
import { IsString, IsNumber, IsUrl } from 'class-validator';
export class CreateActionDto {
userId: string;
#IsString()
name: string;
#IsNumber()
timeStart: number;
}
Schema:
import { Prop, Schema, SchemaFactory } from '#nestjs/mongoose';
import { Document } from 'mongoose';
#Schema()
export class Action extends Document {
#Prop()
userId: string;
#Prop()
name: string;
#Prop()
timeStart: number;
}
export const ActionSchema = SchemaFactory.createForClass(Action)
In the req property I have userId. What is the best way to create an entity and attach userId extracted from token?
Should I pass req to the service, and in the service set userId property on DTO like this?:
#Injectable()
export class ActionService {
constructor(
#InjectModel(Action.name) private actionModel: Model<Action>,
) { }
async createAction(req: string, createActionDto: CreateActionDto) {
createActionDto.userId = req.user.userId
// ... save to mongoose createActionDto
}
}
Is it a correct solution or there is another, a better way to deal with it?
Personally I would set the userId in the controller in order to not having to pass it around:
#UseGuards(JwtAuthGuard)
#Post("/")
createAction(#Request() req, #Body() createActionDto: CreateActionDto) {
createActionDto.userId = req?.user?.userId;
return this.actionService.createAction(createActionDto);
}
If you have many different controllers and DTOs that require the userId you could also define an Interceptor and do it there in order to reduce duplication:
#Injectable()
export class SetUserIdInterceptor implements NestInterceptor {
public intercept(_context: ExecutionContext, $next: CallHandler): Observable<any> {
const request: any = _context.switchToHttp().getRequest(); //instead of any you could also define a super-class for all DTOs that require the `userId`-property
request.body?.userId = req?.user?.userId;
return $next;
}
}
You can then use this interceptor on your route as follows:
#UseGuards(JwtAuthGuard)
#Post("/")
#UseInterceptors(SetUserIdInterceptor)
createAction(#Body() createActionDto: CreateActionDto) {
return this.actionService.createAction(createActionDto)
}
Related
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.
My function save of an object (in model.ts file) which is created by typegoose should return Promise<Todo> but it returns Promise<Document<any, {}>>
and i have this error :
Type 'Document<any, {}>' is missing the following properties from type 'Todo': createdAt, updatedAt, content, isDone
How should i correct this ?
model ts
import { getModelForClass } from "#typegoose/typegoose";
import { ObjectId } from "mongodb";
import { Todo } from "../../entities";
import { NewTodoInput } from "./input";
// This generates the mongoose model for us
export const TodoMongooseModel = getModelForClass(Todo);
export default class TodoModel {
async getById(_id: ObjectId): Promise<Todo | null> {
// Use mongoose as usual
return TodoMongooseModel.findById(_id).lean().exec();
}
async create(data: NewTodoInput): Promise<Todo> {
const todo = new TodoMongooseModel(data);
return todo.save();
}
}
input ts
import { Field, InputType, ID } from "type-graphql";
import { MaxLength, MinLength } from "class-validator";
#InputType()
export class NewTodoInput {
#Field()
#MaxLength(300)
#MinLength(1)
content: string | undefined;
}
entities todo ts
import { ObjectType, Field } from "type-graphql";
import { prop } from "#typegoose/typegoose";
import { ObjectId } from "mongodb";
#ObjectType()
export class Todo {
#Field()
readonly _id!: ObjectId;
#prop()
#Field(() => Date)
createdAt!: Date;
#prop()
#Field(() => Date)
updatedAt!: Date;
#prop()
#Field()
content!: string;
#prop({ default: false })
#Field()
isDone!: boolean;
}
Thank you.
with the unofficial mongoose types, it is an known problem that await document.save() does not return the correct typings
-> this is not an problem with typegoose, it is with #types/mongoose
the workaround is to use:
await doc.save();
return doc;
(or maybe in your case directly use the mongoose provided function .create)
PS: this is not a problem in the official types of mongoose (can be currently used with typegoose 8.0.0-beta.x)
I have created a sample application to save data for a user. I wanted to know how to validate it before saving and showing the response in JSON. I have got a reference link but its not using a controller.
Please suggest a solution using a controller. Below is my code
Entity file:
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number;
#Column()
firstName: string;
#Column()
lastName: string;
#Column()
age: number;
}
Controller file:
import {getRepository} from "typeorm"; import {NextFunction, Request, Response} from "express"; import {User} from "../entity/User";
export class UserController {
private userRepository = getRepository(User);
async all(request: Request, response: Response, next: NextFunction) {
return this.userRepository.find();
}
async one(request: Request, response: Response, next: NextFunction) {
return this.userRepository.findOne(request.params.id);
}
async save(request: Request, response: Response, next: NextFunction) {
return this.userRepository.save(request.body);
}
async remove(request: Request, response: Response, next: NextFunction) {
let userToRemove = await this.userRepository.findOne(request.params.id);
await this.userRepository.remove(userToRemove);
}
}
I wanted to validate firstname and lastname as mandatory i.e. no blank value should be accepted and age should take no blank values and numbers only.
Yes you can by using validate function in model class:
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
#Entity()
export class User {
#PrimaryGeneratedColumn()
id: number;
#Column()
firstName: string;
#Column()
lastName: string;
#Column()
age: number;
#BeforeInsert()
#BeforeUpdate()
async validate() {
await validateOrReject(this);
}
}
You can use the class-validator and class-transformer package.
Define the structure of JSON as a typescript class and add decorators from class-validator package.
export class UserStructure {
#IsString()
firstName: string;
#IsString()
lastName: string;
age: number;
}
Use class transformer to convert plain object to UserStructure Class and then pass the class to validate function which is imported from class-validator package for performing validation
import { plainToClass } from 'class-transformer';
import {validate} from 'class-validator';
//Place the following code inside your controller
let userStructureClass = plainToClass(UserStructure , req.body); //convert req.body plain object to UserStructure Class
let errors = await validate(userStructureClass);
if(errors.length > 0) req.json(errors)
class-validator : https://www.npmjs.com/package/class-validator
class-transformer : https://www.npmjs.com/package/class-transformer
NOTE : validate method only works for classes that is why we converted req.body plain object to class using class-transformer and then performed validation.
These packages uses decorators so make sure to place the following in tsconfig.json
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
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
}
}
I want to serialize a controller response by the nestjs serialization technique. I didn't find any approach and my solution is as follows:
User Entity
export type UserRoleType = "admin" | "editor" | "ghost";
#Entity()
export class User {
#PrimaryGeneratedColumn() id: number;
#Column('text')
username: string;
#Column('text')
password: string;
#Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
#Column({ nullable: true })
profileId: number;
}
User Response Classes
import { Exclude } from 'class-transformer';
export class UserResponse {
id: number;
username: string;
#Exclude()
roles: string;
#Exclude()
password: string;
#Exclude()
profileId: number;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}
import { Exclude, Type } from 'class-transformer';
import { User } from 'src/_entities/user.entity';
import { UserResponse } from './user.response';
export class UsersResponse {
#Type(() => UserResponse)
users: User[]
constructor() { }
}
Controller
#Controller('user')
export class UsersController {
constructor(
private readonly userService: UserService
) {
}
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}
It works, but I must explicitly assign the db query result to the response users member.
Is there a better way? Thanks a lot
Here the actual Response and wanted result, for a better explanation.
Result in this Approach
{
"users": [
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
]
}
Result Wanted
{
{
"id": 1,
"username": "a"
},
{
"id": 2,
"username": "bbbbbb"
}
}
I would recommend to directly put the #Exclude decorators on your entity class User instead of duplicating the properties in UserResponse. The following answer assumes you have done so.
Flat Response
If you have a look at the code of the ClassSerializerInterceptor, you can see that it automatically handles arrays:
return isArray
? (response as PlainLiteralObject[]).map(item =>
this.transformToPlain(item, options),
)
: this.transformToPlain(response, options);
However, it will only transform them, if you directly return the array, so return users instead of return {users: users}:
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(): Promise<User> {
return this.userService.findAll()
}
Nested Response
If you need the nested response, then your way is a good solution.
Alternatively, you can call class-transformer's serialize directly instead of using the ClassSerializerInterceptor. It also handles arrays automatically:
import { serialize } from 'class-transformer';
#Get('all')
async findAll(): Promise<UsersResponse> {
const users: User[] = await this.userService.findAll();
return {users: serialize(users)};
}
Wow, what easy, if i know! Perfect, this solves my problem. Also your recommendation for the User Entity with the class-transformer #Exclue() decorator.
And i know that i do not need a custom UsersResponse class in this use case.
This solution was that what i was looking for, but i overjump this quite easy way
Thank you so much for your superfast answer and the problem solution.
Greetings to Berlin from Rostock :)
Here my final approach:
Controller
#UseInterceptors(ClassSerializerInterceptor)
#Get('all')
async findAll(
): Promise<User> {
return await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
}
User Entitiy
import { Entity, Column, PrimaryGeneratedColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
import { Profile } from './profile.entity';
import { Photo } from './photo.entity';
import { Album } from './album.entity';
import { Exclude } from 'class-transformer';
export type UserRoleType = "admin" | "editor" | "ghost";
#Entity()
export class User {
#PrimaryGeneratedColumn() id: number;
#Column('text')
username: string;
#Exclude()
#Column('text')
password: string;
#Column({
type: "enum",
enum: ["admin", "editor", "ghost"],
default: "ghost"
})
roles: UserRoleType;
#Exclude()
#Column({ nullable: true })
profileId: number;
#OneToMany(type => Photo, photo => photo.user)
photos: Photo[];
#OneToMany(type => Album, albums => albums.user)
albums: Album[];
#OneToOne(type => Profile, profile => profile.user)
#JoinColumn()
profile: Profile;
}
Response Result
[
{
"id": 1,
"username": "a",
"roles": "admin"
},
{
"id": 2,
"username": "bbbbbb",
"roles": "ghost"
}
]
I have alternative way for your problem.
you can remove #UseInterceptors(ClassSerializerInterceptor) from your Controller. Instead use serialize and deserialize function.
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
#Get('all')
async findAll() {
const users = serialize(await this.userService.findAll());
return {
status: 200,
message: 'ok',
users: deserialize(User, users)
};
}
it's work too for single data
import { Param } from '#nestjs/common';
import { serialize, deserialize } from 'class-transformer';
import { User } from './users.entity';
#Get(':id')
async findById(#Param('id') id: number) {
const user = serialize(await this.userService.findById(id));
return {
status: 200,
message: 'ok',
user: deserialize(User, user)
};
}
Your approach is recommended by nestjs but that has a fault. You are excluding some properties from being exposed to the client. What if, you work in a project that has an admin and admin wants to see all the data about the users or products. If you exclude fields in the entities, your admin won't see those fields either. Instead, leave the entities as it is, and write dto's for each controller or for each request handler and in this dto's just list the properties you want to expose.
Then write a custom interceptor and create specific dto for ecah entity. For example in your example, you create a userDto:
import { Expose } from 'class-transformer';
// this is a serizalization dto
export class UserDto {
#Expose()
id: number;
#Expose()
roles: UserRoleType;
#Expose()
albums: Album[];
// Basically you list what you wanna expose here
}
custom interceptor is a little messy:
import {
UseInterceptors,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '#nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { plainToClass } from 'class-transformer';
// Normally user entity goes into the interceptor and nestjs turns it into the JSON. But we we ill turn it to User DTO which will have all the serialization rules.then nest will take dto and turn it to the json and send it back as response
export class SerializerInterceptor implements NestInterceptor {
// dto is the variable. so you can use this class for different entities
constructor(private dto:any){
}
intercept(context: ExecutionContext, handler: CallHandler): Observable<any> {
// you can write some code to run before request is handled
return handler.handle().pipe(
// data is the incoming user entity
map((data: any) => {
return plainToClass(this.dto, data, {
// this takes care of everything. this will expose things that are set in the UserDto
excludeExtraneousValues: true,
});
}),
);
}
}
Now you use this in the controller:
// See we passed UserDto. for different entities, we would just write a new dto for that entity and our custom interceptor would stay reusable
#UseInterceptors(new SerializerInterceptor(UserDto))
#Get('all')
async findAll(
): Promise<UsersResponse> {
let users = await this.userService.findAll().catch(e => { throw new NotAcceptableException(e) })
let rsp =new UsersResponse()
rsp.users = users
return rsp
}