Skip to content

Controllers

Michael Yali edited this page Jun 27, 2019 · 11 revisions

Description

@nestjsx/crud - core package which provides @Crud() controller decorator for endpoints generation, global configuration, validation, helper decorators.

Table of Contents

Install

npm i @nestjsx/crud class-transformer class-validator

Getting started

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.

API Endpoints

Crud() decorator generates the following API endpoints:

Get many resources

GET /heroes
GET /heroes/:heroId/perks

Result: array of resources | pagination object with data
Status Codes: 200

Get one resource

GET /heroes/:id
GET /heroes/:heroId/perks:id

Request Params: :id - some resource field (slug)
Result: resource object | error object
Status Codes: 200 | 404

Create one resource

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

Create many resources

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

Update one resource

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

Replace one resource

PUT /heroes/:id
PUT /heroes/:heroId/perks/:id

Request Params: :id - some resource field (slug)
Request Body: resource object | resource object with nested (relational) resources (or partial)
Result:: replaced resource object | error object
Status codes: 200 | 400

Delete one resource

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

Swagger

Swagger support is present out of the box.

Options

Crud() decorator accepts the following CrudOptions:

model

{
  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

{
  validation?: ValidationPipeOptions | false;
}

Optional

Accepts ValidationPipe options or false if you want to use your own validation implementation.

params

{
  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

{
  routes?: {
    exclude?: BaseRouteName[],
    only?: BaseRouteName[],
    getManyBase?: {
      interceptors?: [],
      decorators?: [],
    },
    getOneBase?: {
      interceptors?: [],
      decorators?: [],
    },
    createOneBase?: {
      interceptors?: [],
      decorators?: [],
    },
    createManyBase?: {
      interceptors?: [],
      decorators?: [],
    },
    updateOneBase: {
      interceptors?: [],
      decorators?: [],
      allowParamsOverride?: boolean,
    },
    replaceOneBase: {
      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

{
  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

{
  allow: ['name', 'email'];
}

Optional

An Array of fields that are allowed to be received in GET requests. If empty or undefined - allow all.

exclude

{
  exclude: ['accessToken'];
}

Optional

An Array of fields that will be excluded from the GET response (and not queried from the DB).

persist

{
  persist: ['createdAt'];
}

Optional

An Array of fields that will be always persisted in GET response.

filter

{
  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

{
  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

{
  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

{
  limit: 25,
}

Optional

Default limit that will be aplied to the DB query.

maxLimit

{
  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

{
  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.

Global options

In order to reduce some repetition in your CrudOptions in every controller you can specify some options globally:

{
  queryParser?: RequestQueryBuilderOptions;
  routes?: RoutesOptions;
  params?: ParamsOptions;
  query?: {
    limit?: number;
    maxLimit?: number;
    cache?: number | false;
  };
}

queryParser are options for RequestQueryParser that is being used in CrudRequestInterceptor to parse/validate query and path params. Frontend has similar customization ability.

routes are the same as here.

params are the same as here.

query are similar to options described here except the fact that limit, maxLimit and cache can be applied only.

So in order to apply global options you need load them in your main.ts (index.ts) file BEFORE you import AppModule class. That's because TypeScript decorators are executed when we declare our class but not when we create new class instance. So in your main.ts:

import { CrudConfigService } from '@nestjsx/crud';

CrudConfigService.load({
  query: {
    limit: 25,
    cache: 2000,
  },
  params: {
    id: {
      field: 'id',
      type: 'uuid',
      primary: true,
    },
  },
  routes: {
    updateOneBase: {
      allowParamsOverride: true,
    },
    deleteOneBase: {
      returnDeleted: true,
    },
  },
});

import { AppModule } from './app.module';

...

Notice: all those options can be overridden in each CrudController.

Request validation

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.

IntelliSense

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;
  }
}

Routes override

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>;

  replaceOneBase(
    @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:

  1. Attach @Override() decorator without any argument to the newly created method wich name doesn't contain Base ending. So if you want to override getManyBase, you need to create getMany method.

  2. 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('replaceOneBase')
  awesomePUT(
    @ParsedRequest() req: CrudRequest,
    @ParsedBody() dto: Hero,
  ) {
    return this.base.replaceOneBase(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.

Adding routes

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
}

Additional decorators

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',
  ReplaceOne = 'Replace-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;
  }
}
You can’t perform that action at this time.