nartc/nest-mean

Base controller implementation

Closed this issue ยท 6 comments

Hello and thanks for the awesome tutorial in YouTube, now I'm asking for your help, I'm traying to implement a "BaseController" like this:

import {
  Body,
  Delete,
  Get,
  HttpException,
  HttpStatus,
  InternalServerErrorException,
  Param,
  Post,
  Put,
  Query,
} from '@nestjs/common';
import { UpdateResult } from 'typeorm';
import { BaseService } from './base.service';
import { ApiException } from '@models/api-exception.model';

export abstract class BaseController<T> {
  protected readonly service: BaseService<T>;

  constructor(service: BaseService<T>) {
    this.service = service;
  }

  @Get()
  root(@Query('filter') filter = {}): Promise<T[]> {
    try {
      return this.service.find(filter);
    } catch(e) {
      throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @Get(':id')
  getById(@Param('id') id: string | number): Promise<T> {
    try {
      return this.service.findById(id);
    } catch(e) {
      throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @Post()
  create(@Body() body: T): Promise<T> {
    try {
      return this.service.create(body);
    } catch(e) {
      throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @Put()
  update(@Body() body: { id: string | number } & T): Promise<UpdateResult> {
    return this.service.update(body.id, body);
  }

  @Delete(':id')
  delete(@Param('id') id: string | number) {
    try {
      return this.service.delete(id);
    } catch(e) {
      throw new InternalServerErrorException(e);
    }
  }
}

But how I could decorate my methods when I extends from this class

nartc commented

Hi @rcanessa89, if you're using Swagger, you CANNOT have a BaseController. The extended controllers will not have the tags and the models correctly.

For non-Swagger project, you can implement a BaseController like you have right now. You don't have to decorate anything in your Extended controllers for those 5 route handlers you already created in your baseController

Thanks! Even knowing that I implemented this solution.

const defaultAuthObj = {
  root: true,
  getById: true,
  create: true,
  update: true,
  delete: true,
};

export function getBaseController<T>(
  TypeClass: { new(): T },
  TypeCreateVM: { new(): any },
  TypeUpdateVM: { new(): any },
  TypeFindVM: { new(): any },
  authObj: IAuthGuards = defaultAuthObj,
) {
  const auth = {
    ...defaultAuthObj,
    ...authObj,
  };

  @ApiUseTags(TypeClass.name)
  abstract class BaseController {
    protected readonly service: BaseService<T>;

    constructor(service: BaseService<T>) {
      this.service = service;
    }

    @Get()
    @ConditionalDecorator(auth.root, UseGuards(AuthGuard('jwt')))
    @ConditionalDecorator(auth.root, ApiBearerAuth())
    @ApiImplicitQuery({
      name: 'filter',
      description: 'TypeORM find query',
      required: false,
      isArray: false,
    })
    @ApiOkResponse({
      type: TypeFindVM,
      isArray: true,
    })
    @ApiBadRequestResponse({ type: ApiException })
    @ApiOperation(getOperationId(TypeClass.name, 'Find'))
    public async root(@Query('filter') filter = {}): Promise<T[] | Partial<T[]>> {
      try {
        const data = await this.service.find(filter);
        const mapped = [];

        data.forEach(async (v: T) => {
          mapped.push(await this.service.map(v));
        });

        return mapped;
      } catch (e) {
        throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
      }
    }

    @Get(':id')
    @ConditionalDecorator(auth.getById, UseGuards(AuthGuard('jwt')))
    @ConditionalDecorator(auth.getById, ApiBearerAuth())
    @ApiImplicitParam({
      name: 'id',
      description: 'The ID of the entity to find',
      required: true,
    })
    @ApiOkResponse({ type: TypeFindVM })
    @ApiBadRequestResponse({ type: ApiException })
    @ApiOperation(getOperationId(TypeClass.name, 'FindById'))
    public async getById(@Param('id') id: string | number): Promise<T | Partial<T>> {
      try {
        return this.service.map(await this.service.findById(id));
      } catch (e) {
        throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
      }
    }

    @Post()
    @ConditionalDecorator(auth.create, UseGuards(AuthGuard('jwt')))
    @ConditionalDecorator(auth.create, ApiBearerAuth())
    @ApiImplicitBody({
      name: TypeCreateVM.name,
      type: TypeCreateVM,
      description: 'Data for entity creation',
      required: true,
      isArray: false,
    })
    @ApiCreatedResponse({ type: TypeClass })
    @ApiBadRequestResponse({ type: ApiException })
    @ApiOperation(getOperationId(TypeClass.name, 'Create'))
    public async create(@Body() body): Promise<T | Partial<T>> {
      try {
        return this.service.map(await this.service.create(body));
      } catch (e) {
        throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
      }
    }

    @Put()
    @ConditionalDecorator(auth.update, UseGuards(AuthGuard('jwt')))
    @ConditionalDecorator(auth.update, ApiBearerAuth())
    @ApiImplicitBody({
      name: TypeUpdateVM.name,
      type: TypeUpdateVM,
      description: 'Data for entity update',
      required: true,
      isArray: false,
    })
    @ApiOkResponse({ type: TypeClass })
    @ApiBadRequestResponse({ type: ApiException })
    @ApiOperation(getOperationId(TypeClass.name, 'Update'))
    public update(@Body() body: { id: string | number } & T): Promise<UpdateResult> {
      return this.service.update(body.id, body);
    }

    @Delete(':id')
    @ConditionalDecorator(auth.delete, UseGuards(AuthGuard('jwt')))
    @ConditionalDecorator(auth.delete, ApiBearerAuth())
    @ApiImplicitParam({
      name: 'id',
      description: 'The ID of the entity to delete',
      required: true,
    })
    @ApiOkResponse({ type: TypeClass })
    @ApiBadRequestResponse({ type: ApiException })
    @ApiOperation(getOperationId(TypeClass.name, 'Delete'))
    public delete(@Param('id') id: string | number) {
      try {
        return this.service.delete(id);
      } catch (e) {
        throw new InternalServerErrorException(e);
      }
    }
  }

  return BaseController;
}

In action with my "TodoController"

const BaseController = getBaseController<Todo>(Todo, TodoCreateVM, TodoUpdateVM, TodoFindVM);

@Controller('todo')
export class TodoController extends BaseController {
  constructor(
    private readonly todoService: TodoService,
  ) {
    super(todoService);
  }
}

It works very well, I guess.

nartc commented

That's a really nice solution. I didn't even think about making a Factory like that. It's called a Factory where you encapsulate a Class within a Function ๐Ÿ‘ Good job! Why won't you make a merge request?

Sure, I will clone this repo a make the changes

nartc commented

@rcanessa89 Hey buddy, I don't have any way to contact you. Based on your BaseController, I've made this https://github.com/nartc/nest-abstract package. I gave you credit as I wouldn't be able to make this without your idea. Thanks man.

And also, I know yours applies to TypeOrm and I made the package to be compatible with both Mongoose and TypeOrm.

It is still in early development stage so please give me feedback if you find any.

Hello and thanks for the awesome tutorial in YouTube, now I'm asking for your help, I'm traying to implement a "BaseController" like this:

import {
  Body,
  Delete,
  Get,
  HttpException,
  HttpStatus,
  InternalServerErrorException,
  Param,
  Post,
  Put,
  Query,
} from '@nestjs/common';
import { UpdateResult } from 'typeorm';
import { BaseService } from './base.service';
import { ApiException } from '@models/api-exception.model';

export abstract class BaseController<T> {
  protected readonly service: BaseService<T>;

  constructor(service: BaseService<T>) {
    this.service = service;
  }

  @Get()
  root(@Query('filter') filter = {}): Promise<T[]> {
    try {
      return this.service.find(filter);
    } catch(e) {
      throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @Get(':id')
  getById(@Param('id') id: string | number): Promise<T> {
    try {
      return this.service.findById(id);
    } catch(e) {
      throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @Post()
  create(@Body() body: T): Promise<T> {
    try {
      return this.service.create(body);
    } catch(e) {
      throw new HttpException(e, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @Put()
  update(@Body() body: { id: string | number } & T): Promise<UpdateResult> {
    return this.service.update(body.id, body);
  }

  @Delete(':id')
  delete(@Param('id') id: string | number) {
    try {
      return this.service.delete(id);
    } catch(e) {
      throw new InternalServerErrorException(e);
    }
  }
}

But how I could decorate my methods when I extends from this class

Hi, I'm sorry for barging in, in your code is the BaseSevice an interface or an abstract class? Also, do I have to inject an Entity into that BaseService, if you pleased, may I look into your BaseService code?