Pagination et CRUD en NestJS
Qui suis-je ?
Un peu de contexte
Un peu de contexte
De quoi ai-je besoin ?
- Mettre en place facilement des routes GET/POST/PUT/DELETE
- Outil facile à configurer
- Outil qui fonctionne avec mon ORM (typeorm)
La solution
@nestjsx/crud
Les features clés
- Facile à mettre en place
- Facile à overrider
- Stratégie de filtres
- Validation du payload à partir du DTO
- Pagination out of the box
- Swagger out of the box
Comment ça marche ?
L'entité
import { CrudValidationGroups } from '@nestjsx/crud';
const { CREATE, UPDATE } = CrudValidationGroups;
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString()
@Column({ length: 50 })
name: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString()
@Column({ length: 100, select: false })
password: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString()
@Column({ length: 100 })
email: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ each: true })
@Column('simple-array')
roles: string[];
}
Comment ça marche ?
Le controlleur
Le décorateur Crud
@Crud({
model: {
type: User,
},
params: {
id: {
field: 'id',
type: 'uuid',
primary: true,
},
},
routes: {
only: [
'getOneBase',
'getManyBase',
'deleteOneBase',
'createOneBase',
'updateOneBase'
],
getOneBase: {
decorators: [Roles('admin')]
},
getManyBase: {
decorators: [Roles('admin')]
},
deleteOneBase: {
decorators: [Roles('admin')]
}
},
query: {
exclude: ['password']
}
})
Comment ça marche ?
Le controlleur
Les routes surchargées
@Controller('users')
@UseGuards(AuthGuard('jwt'), RolesGuard)
export class UserController implements CrudController<User> {
constructor(
public readonly service: UserService,
) {}
@Override()
createOne(@Body() user: User) {
return this.service.createUser(user);
}
@Roles('admin')
@Override('updateOneBase')
anotherName(@Param('id') id: string, @Body() user: User) {
return this.service.updateUser(id, user);
}
}
Comment ça marche ?
Le service
@Injectable()
export class UserService extends TypeOrmCrudService<User> {
constructor(@InjectRepository(User) private readonly userRepository: Repository<User>) {
super(userRepository);
}
createUser = async (user: User) => {
const plainPassword = user.password;
const encryptedPassword = await hash(plainPassword, SALT_ROUNDS);
const createdUser = await this.userRepository.save({ ...user, password: encryptedPassword });
const { password, ...createdUserDto } = createdUser;
return createdUserDto;
};
updateUser = async (id: string, user: User) => {
let newUser = user;
const plainPassword = user.password;
if (Boolean(plainPassword)) {
const encryptedPassword = await hash(plainPassword, SALT_ROUNDS);
newUser = { ...newUser, password: encryptedPassword };
}
await this.userRepository.update({ id }, newUser);
return await this.userRepository.findOne({ id });
};
}
On rajoute une entité ?
import { CrudValidationGroups } from '@nestjsx/crud';
const { CREATE, UPDATE } = CrudValidationGroups;
@Entity('users')
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString()
@Column({ length: 50 })
name: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString()
@Column({ length: 100, select: false })
password: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString()
@Column({ length: 100 })
email: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ each: true })
@Column('simple-array')
roles: string[];
}
Ce qu'il se passe
Les solutions
L'exclude dans la jointure
@Crud({
model: {
type: Article,
},
params: {
id: {
field: 'id',
type: 'uuid',
primary: true,
},
},
query: {
join: {
user: { eager: true, exclude: ['password'] },
},
alwaysPaginate: true,
},
})
Les solutions
L'entité
// user.entity.ts
import { Exclude } from 'class-transformer';
...
@Entity('users')
export class User {
...
@Exclude()
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString()
@Column({ length: 100, select: false })
password: string;
...
}
// article.controller.ts
import { UseInterceptors, ClassSerializerInterceptor } from '@nestjs/common';
...
@UseInterceptors(ClassSerializerInterceptor)
@Controller('articles')
...
Les limites
- Ignore les hidden columns de typeorm
- Fonctionnement orienté blacklist (/!\ sécu)
- Génère automatiquement toutes les routes sauf si vous précisez le contraire
Quelles alternatives ?
- Les exemples de la doc Nest
- Les query builders de typeorm
- nestjs-typeorm-paginate (soon 2.0 <3)
Quelles alternatives ?
function getCandidates(options: IPaginationOptions): Promise<CustomPagination<Candidate>> {
const queryBuilder = this.candidateRepository
.createQueryBuilder('candidate')
.leftJoinAndSelect('candidate.headhunter', 'headhunter')
.leftJoinAndSelect('candidate.school', 'school');
return customPaginate<Candidate>(queryBuilder, options);
}
interface IPaginationOptions {
limit: number;
page: number;
route?: string;
}
@nestjsx/crud ou pas ?
Questions
NestJs Paris #3
By Léo Anesi
NestJs Paris #3
- 515