-
-
Notifications
You must be signed in to change notification settings - Fork 553
Controllers
@nestjsx/crud - core package which provides @Crud()
controller decorator for endpoints generation, global configuration, validation, helper decorators.
- Install
- Getting started
- API endpoints
- Options
- Global options
- Request validation
- IntelliSense
- Routes override
- Adding routes
- Additional decorators
npm i @nestjsx/crud class-transformer class-validator
Let's take a look at the example of using @nestjsx/crud
with TypeORM.
Assume we have some TypeORM enitity:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Company {
@PrimaryGeneratedColumn() id: number;
@Column() name: string;
}
Then we need to create a service:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm';
import { Company } from './company.entity';
@Injectable()
export class CompaniesService extends TypeOrmCrudService<Company> {
constructor(@InjectRepository(Company) repo) {
super(repo);
}
}
We've done with the service so let's create a controller:
import { Controller } from '@nestjs/common';
import { Crud, CrudController } from '@nestjsx/crud';
import { Company } from './company.entity';
import { CompaniesService } from './companies.service';
@Crud({
model: {
type: Company,
},
})
@Controller('companies')
export class CompaniesController implements CrudController<Company> {
constructor(public service: CompaniesService) {}
}
All we have to do next is to connect our service and controller in the CompaniesModule
as we usually do:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Company } from './company.entity';
import { CompaniesService } from './companies.service';
import { CompaniesController } from './companies.controller';
@Module({
imports: [TypeOrmModule.forFeature([Company])],
providers: [CompaniesService],
exports: [CompaniesService],
controllers: [CompaniesController],
})
export class CompaniesModule {}
That's it.
Crud()
decorator generates the following API endpoints:
GET /heroes
GET /heroes/:heroId/perks
Result: array of resources | pagination object with data
Status Codes: 200
GET /heroes/:id
GET /heroes/:heroId/perks:id
Request Params: :id
- some resource field (slug)
Result: resource object | error object
Status Codes: 200 | 404
POST /heroes
POST /heroes/:heroId/perks
Request Body: resource object | resource object with nested (relational) resources
Result: created resource object | error object
Status Codes: 201 | 400
POST /heroes/bulk
POST /heroes/:heroId/perks/bulk
Request Body: array of resources objects | array of resources objects with nested (relational) resources
{
"bulk": [{ "name": "Batman" }, { "name": "Batgirl" }]
}
Result: array of created resources | error object
Status codes: 201 | 400
PATCH /heroes/:id
PATCH /heroes/:heroId/perks/:id
Request Params: :id
- some resource field (slug)
Request Body: resource object (or partial) | resource object with nested (relational) resources (or partial)
Result:: updated partial resource object | error object
Status codes: 200 | 400 | 404
DELETE /heroes/:id
DELETE /heroes/:heroId/perks/:id
Request Params: :id
- some resource field (slug)
Result:: empty | resource object | error object
Status codes: 200 | 404
Crud()
decorator accepts the following CrudOptions
:
{
model: {
type: Entity|Model|DTO
},
}
Required
Entity
, Model
or DTO
class must be provided here. Everything else described bellow is optional. It's needed for a built in validation based on NestJS ValidationPipe
.
{
validation?: ValidationPipeOptions | false;
}
Optional
Accepts ValidationPipe
options or false
if you want to use your own validation implementation.
{
params?: {
[key: string]: {
field: string;
type: 'number' | 'string' | 'uuid';
primary?: boolean;
},
},
}
Optional
By default @Crud()
decorator will use id
with the type number
as a primary slug param.
If you have, for instance, a resorce field called slug
or whatever, it's a UUID and you need it to be a primary slug by which your resource should be fetched, you can set up this params options:
{
params: {
slug: {
field: 'slug',
type: 'uuid',
primary: true,
},
},
}
If you have a controller path with that looks kinda similar to this /companies/:companyId/users
you need to add this param option:
{
params: {
...
companyId: {
field: 'companyId',
type: 'number'
},
},
}
{
routes?: {
exclude?: BaseRouteName[],
only?: BaseRouteName[],
getManyBase?: {
interceptors?: [],
decorators?: [],
},
getOneBase?: {
interceptors?: [],
decorators?: [],
},
createOneBase?: {
interceptors?: [],
decorators?: [],
},
createManyBase?: {
interceptors?: [],
decorators?: [],
},
updateOneBase: {
interceptors?: [],
decorators?: [],
allowParamsOverride?: boolean,
},
deleteOneBase?: {
interceptors?: [],
decorators?: [],
returnDeleted?: boolean,
},
}
}
Optional
It's a set of options for each of the generated routes.
interceptors
- an array of your custom interceptors
decorators
- an array of your custom decorators
allowParamsOverride
- whether or not to allow body data be overriten by the URL params on PATH request. Default: false
returnDeleted
- whether or not an entity object should be returned in the response body on DELETE request. Default: false
Also you can specify what routes should be excluded or what routes whould be used only by providing routes names to the exclude
or only
accordingly.
{
query?: {
allow?: string[];
exclude?: string[];
persist?: string[];
filter?: QueryFilter[];
join?: JoinOptions;
sort?: QuerySort[];
limit?: number;
maxLimit?: number;
cache?: number | false;
}
}
Optional
It's a set of query options fro GET request.
{
allow: ['name', 'email'];
}
Optional
An Array of fields that are allowed to be received in GET requests. If empty or undefined - allow all.
{
exclude: ['accessToken'];
}
Optional
An Array of fields that will be excluded from the GET response (and not queried from the DB).
{
persist: ['createdAt'];
}
Optional
An Array of fields that will be always persisted in GET response.
{
filter: [
{
field: 'isActive',
operator: 'ne',
value: false,
},
];
}
Optional
An Array of filter
objects that will be merged (combined) with query filter
if those are passed in GET request. If not - filter
will be added to the DB query as a stand-alone condition.
If multiple items are added, they will be interpreted as AND
type of conditions.
{
join: {
profile: {
persist: ['name'],
eager: true,
},
tasks: {
allow: ['content'],
},
notifications: {
exclude: ['token']
},
company: {},
'company.projects': {
persist: ['status']
},
'users.projects.tasks': {
exclude: ['description'],
},
}
}
Optional
An Object of relations that allowed to be fetched by passing join
query parameter in GET requests.
Each key of join
object must strongly match the name of the corresponding resource relation. If particular relation name is not present in this option, then user will not be able to get this relational objects in GET request.
Each relation option can have (all below are optional):
allow
- an Array of fields that are allowed to be received in GET requests. If empty or undefined - allow all.
exclude
- an Array of fields that will be excluded from the GET response (and not queried from the DB).
persist
- an Array of fields that will be always persisted in GET response.
eager
- type boolean
- whether or not current relation should persist in every GET response.
{
sort: [
{
field: 'id',
order: 'DESC',
},
];
}
Optional
An Array of sort
objects that will be merged (combined) with query sort
if those are passed in GET request. If not - sort
will be added to the DB query as a stand-alone condition.
{
limit: 25,
}
Optional
Default limit
that will be aplied to the DB query.
{
maxLimit: 100,
}
Optional
Max amount of results that can be queried in GET request.
Notice: it's strongly recommended to set up this option. Otherwise DB query will be executed without any LIMIT if no limit
was passed in the query or if the limit
option hasn't been set up in crud options.
{
cache: 2000,
}
Optional
If Caching Results
is implemented on you project, then you can set up default cache
in milliseconds for GET response data.
Cache can be reseted by using cache=0
query parameter in your GET requests.
Query params and path params validation is performed by an interceptor. It parses query and path parameters and then validates them.
Body request validation is done by ValidationPipe
. It uses model.type
from CrudOptions
as a DTO that describes validation rules. We distinguish body validation on create
and update
methods. This was achieved by using validation groups.
Let's take a look at this example:
import { Entity, Column, OneToMany } from 'typeorm';
import { IsOptional, IsString, MaxLength, IsNotEmpty } from 'class-validator';
import { Type } from 'class-transformer';
import { CrudValidationGroups } from '@nestjsx/crud';
import { BaseEntity } from '../base-entity';
import { User } from '../users/user.entity';
import { Project } from '../projects/project.entity';
const { CREATE, UPDATE } = CrudValidationGroups;
@Entity('companies')
export class Company extends BaseEntity {
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ always: true })
@MaxLength(100, { always: true })
@Column({ type: 'varchar', length: 100, nullable: false })
name: string;
@IsOptional({ groups: [UPDATE] })
@IsNotEmpty({ groups: [CREATE] })
@IsString({ groups: [CREATE, UPDATE] })
@MaxLength(100, { groups: [CREATE, UPDATE] })
@Column({ type: 'varchar', length: 100, nullable: false, unique: true })
domain: string;
@IsOptional({ always: true })
@IsString({ always: true })
@Column({ type: 'text', nullable: true, default: null })
description: string;
/**
* Relations
*/
@OneToMany((type) => User, (u) => u.company)
@Type((t) => User)
users: User[];
@OneToMany((type) => Project, (p) => p.company)
projects: Project[];
}
You can import CrudValidationGroups
enum and set up validation rules for each field on firing of POST
, PATCH
requests or both of them.
Please, keep in mind that we compose crud controllers by the logic inside our @Crud()
class decorator. And there are some unpleasant but not very significant side effects of this approach.
First, there is no IntelliSense on composed methods. That's why we need to use CrudController
interface. This will help to make sure that you're injecting proper CrudService
.
Second, even after adding CrudController
interface you still wouldn't see composed methods, accessible from this
keyword, furthermore, you'll get a TS error. In order to solve this, you can do as follows:
...
import { Crud, CrudController } from '@nestjsx/crud';
@Crud(Hero)
@Controller('heroes')
export class HeroesCrud implements CrudController<Hero> {
constructor(public service: HeroesService) {}
get base(): CrudController<Hero> {
return this;
}
}
Here is the list of composed base routes methods by @Crud()
decorator:
{
getManyBase(
@ParsedRequest() req: CrudRequest,
): Promise<GetManyDefaultResponse<T> | T[]>;
getOneBase(
@ParsedRequest() req: CrudRequest,
): Promise<T>;
createOneBase(
@ParsedRequest() req: CrudRequest,
@ParsedBody() dto: T,
): Promise<T>;
createManyBase(
@ParsedRequest() req: CrudRequest,
@ParsedBody() dto: CreateManyDto<T>,
): Promise<T>;
updateOneBase(
@ParsedRequest() req: CrudRequest,
@ParsedBody() dto: T,
): Promise<T>;
deleteOneBase(
@ParsedRequest() req: CrudRequest,
): Promise<void | T>;
}
Since all composed methods have Base
ending in their names, overriding those endpoints could be done in two ways:
-
Attach
@Override()
decorator without any argument to the newly created method wich name doesn't containBase
ending. So if you want to overridegetManyBase
, you need to creategetMany
method. -
Attach
@Override('getManyBase')
decorator with passed base method name as an argument if you want to override base method with a function that has a custom name.
Example:
...
import {
Crud,
CrudController,
Override,
CrudRequest,
ParsedRequest,
ParsedBody
CreateManyDto,
} from '@nestjsx/crud';
@Crud({
model: {
type: Hero,
}
})
@Controller('heroes')
export class HeroesCrud implements CrudController<Hero> {
constructor(public service: HeroesService) {}
get base(): CrudController<Hero> {
return this;
}
@Override()
getMany(
@ParsedRequest() req: CrudRequest,
) {
return this.base.getManyBase(req);
}
@Override('getOneBase')
getOneAndDoStuff(
@ParsedRequest() req: CrudRequest,
) {
return this.base.getOneBase(req);
}
@Override()
createOne(
@ParsedRequest() req: CrudRequest,
@ParsedBody() dto: Hero,
) {
return this.base.createOneBase(req, dto);
}
@Override()
createMany(
@ParsedRequest() req: CrudRequest,
@ParsedBody() dto: CreateManyDto<Hero>
) {
return this.base.createManyBase(req, dto);
}
@Override('updateOneBase')
coolFunction(
@ParsedRequest() req: CrudRequest,
@ParsedBody() dto: Hero,
) {
return this.base.updateOneBase(req, dto);
}
@Override()
async deleteOne(
@ParsedRequest() req: CrudRequest,
) {
return this.base.deleteOneBase(req);
}
}
Notice: new custom route decorators were created to simplify process: @ParsedRequest()
and @ParsedBody()
. But you still can add your param decorators to any of the methods, e.g. @Param()
, @Session()
, etc. Or any of your own cutom route decorators.
Sometimes you might need to add a new route and to use @ParsedRequest()
in it. You need attach CrudRequestInterceptor
in order to do that:
...
import { UseInterceptors } from '@nestjs/common';
import {
ParsedRequest,
CrudRequest,
CrudRequestInterceptor,
} from '@nestjsx/crud';
...
@UseInterceptors(CrudRequestInterceptor)
@Get('/export/list.xlsx')
async exportSome(@ParsedRequest() req: CrudRequest) {
// some awesome feature handling
}
There are two additional decorators that come out of the box: @Feature()
and @Action()
. You can use them with your ACL implementation. @Action()
will be applyed automaticaly on controller compoesd base methods. There is CrudActions
enum that you can import and use:
enum CrudActions {
ReadAll = 'Read-All',
ReadOne = 'Read-One',
CreateOne = 'Create-One',
CreateMany = 'Create-Many',
UpdateOne = 'Update-One',
DeleteOne = 'Delete-One',
}
ACLGuard
dummy example with helper functions getFeature
and getAction
:
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { getFeature, getAction } from '@nestjsx/crud';
@Injectable()
export class ACLGuard implements CanActivate {
canActivate(ctx: ExecutionContext): boolean {
const handler = ctx.getHandler();
const controller = ctx.getClass();
const feature = getFeature(controller);
const action = getAction(handler);
console.log(`${feature}-${action}`); // e.g. 'Heroes-Read-All'
return true;
}
}