diff --git a/src/api/base.controller.ts b/src/api/base.controller.ts deleted file mode 100644 index 99db2eb..0000000 --- a/src/api/base.controller.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { UseFilters, UsePipes, ValidationPipe } from '@nestjs/common'; -import { CommonExceptionFilter } from '../modules/exception/common-exception.filter'; - -@UsePipes(new ValidationPipe({ whitelist: true })) -@UseFilters(CommonExceptionFilter) -export abstract class BaseApiController {} diff --git a/src/api/create-crud-controller.function.ts b/src/api/create-crud-controller.function.ts new file mode 100644 index 0000000..9dad982 --- /dev/null +++ b/src/api/create-crud-controller.function.ts @@ -0,0 +1,135 @@ +import { + ArgumentMetadata, + Body, + Delete, + Get, + HttpCode, + Injectable, + NotFoundException, + Param, + Post, + Query, + Type, + UsePipes, + ValidationPipe, + ValidationPipeOptions, +} from '@nestjs/common'; +import { ApiBody, ApiQuery } from '@nestjs/swagger'; +import { DatabaseEntityService } from '../modules/database/classes/entity-service.class'; +import { DatabaseEntity } from '../modules/database/classes/entity.class'; +import { ApiGetByIdParams } from './get-by-id.api'; +import { ApiSearchQuery } from './search.api'; + +@Injectable() +export class AbstractValidationPipe extends ValidationPipe { + constructor( + options: ValidationPipeOptions, + private readonly targetTypes: { + body?: Type; + query?: Type; + param?: Type; + custom?: Type; + }, + ) { + super(options); + } + + async transform(value: any, metadata: ArgumentMetadata) { + const targetType = this.targetTypes[metadata.type]; + if (!targetType) { + return super.transform(value, metadata); + } + return super.transform(value, { ...metadata, metatype: targetType }); + } +} + +// @todo: support passing generic get by id +// GetByIdParams: Type = ApiGetByIdParams, // unable to make this generic, @ApiParam({ type: GetByIdParams }) requires a name +export function CreateCrudApiController( + AddDto: Type, + UpdateDto: Type, + SearchQuery: Type = ApiSearchQuery, +) { + @UsePipes(new ValidationPipe({ whitelist: true })) + class CrudApiController { + constructor(readonly service: DatabaseEntityService) {} + + @UsePipes( + new AbstractValidationPipe( + { whitelist: true, transform: true }, + { query: SearchQuery }, + ), + ) + @ApiQuery({ type: SearchQuery }) + @Get() + async list(@Query() query) { + return this.service.list({}, query as any); + } + + @UsePipes( + new AbstractValidationPipe( + { whitelist: true, transform: true }, + { param: ApiGetByIdParams }, + ), + ) + @Get(':id') + async get(@Param() params: ApiGetByIdParams) { + const result = await this.service.get(params.id); + if (!result) { + throw new NotFoundException(); + } + return result; + } + + @UsePipes( + new AbstractValidationPipe( + { whitelist: true, transform: true }, + { body: AddDto }, + ), + ) + @ApiBody({ type: AddDto }) + @HttpCode(201) + @Post() + async add(@Body() body) { + return this.service.add(body as any); + } + + @UsePipes( + new AbstractValidationPipe( + { whitelist: true, transform: true }, + { body: UpdateDto, param: ApiGetByIdParams }, + ), + ) + @ApiBody({ type: UpdateDto }) + @Post(':id') + async update(@Body() body, @Param() params: ApiGetByIdParams) { + const result = await this.service.get(params.id); + + if (!result) { + throw new NotFoundException(); + } + + return this.service.update(result, body as any); + } + + @UsePipes( + new AbstractValidationPipe( + { whitelist: true, transform: true }, + { param: ApiGetByIdParams }, + ), + ) + @HttpCode(204) + @Delete(':id') + async delete(@Param() params: ApiGetByIdParams) { + const result = await this.service.get(params.id); + + if (!result) { + throw new NotFoundException(); + } + + await this.service.delete(result); + } + } + + return CrudApiController; +} diff --git a/src/api/crud.controller.ts b/src/api/crud.controller.ts index be45273..e32ee9d 100644 --- a/src/api/crud.controller.ts +++ b/src/api/crud.controller.ts @@ -7,17 +7,17 @@ import { Param, NotFoundException, HttpCode, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { DatabaseEntity } from '../modules/database/classes/entity.class'; import { DatabaseEntityService } from '../modules/database/classes/entity-service.class'; -import { BaseApiController } from './base.controller'; -export abstract class CrudApiController extends BaseApiController { +@UsePipes(new ValidationPipe({ whitelist: true })) +export abstract class CrudApiController { constructor( protected readonly service: DatabaseEntityService, - ) { - super(); - } + ) {} @Get() async list(@Query() query) { diff --git a/src/app/app.providers.ts b/src/app/app.providers.ts index 04508e3..994b034 100644 --- a/src/app/app.providers.ts +++ b/src/app/app.providers.ts @@ -1,9 +1,9 @@ import { APP_FILTER } from '@nestjs/core'; -import { IndexExceptionFilter } from '../modules/exception/index-exception.filter'; +import { CommonExceptionFilter } from '../modules/exception/common-exception.filter'; export const commonAppProviders = [ { provide: APP_FILTER, - useClass: IndexExceptionFilter, + useClass: CommonExceptionFilter, }, ]; diff --git a/src/index.ts b/src/index.ts index 88b53f1..e297f21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export * from './modules/database/classes/repository.class'; export * from './modules/database/classes/soft-delete-repository.class'; export * from './modules/database/classes/entity-service.class'; export * from './modules/database/classes/soft-delete-entity-service.class'; +export * from './modules/database/functions/create-provider.function'; export * from './modules/emitter/emitter.service'; export * from './modules/emitter/emitter.module'; @@ -57,8 +58,8 @@ export * from './modules/ssl/ssl.service'; export * from './modules/ssl/ssl.module'; // api -export * from './api/base.controller'; export * from './api/crud.controller'; +export * from './api/create-crud-controller.function'; export * from './api/search.api'; export * from './api/get-by-id.api'; diff --git a/src/modules/config/config.service.ts b/src/modules/config/config.service.ts index be00fae..438db5b 100644 --- a/src/modules/config/config.service.ts +++ b/src/modules/config/config.service.ts @@ -28,7 +28,10 @@ export class ConfigService extends NestConfigService { } get debug() { - return boolean(this.get('DEBUG')) || true; + return ( + boolean(this.get('DEBUG')) || + (this.node.isEnv('production') ? false : true) + ); } get frontend() { diff --git a/src/modules/database/functions/create-provider.function.ts b/src/modules/database/functions/create-provider.function.ts new file mode 100644 index 0000000..cba5e5e --- /dev/null +++ b/src/modules/database/functions/create-provider.function.ts @@ -0,0 +1,26 @@ +import { Type } from '@nestjs/common'; +import { getModelForClass } from '@typegoose/typegoose'; +import mongoose from 'mongoose'; +import { DB_DEFAULT_CONNECTION } from '../../../constants'; +import { DatabaseEntity } from '../classes/entity.class'; + +export function CreateDatabaseEntityProvider( + token: string, + entity: Type, + collection: string, +) { + return { + provide: token, + useFactory: (connection: mongoose.Connection) => { + return getModelForClass(entity, { + existingConnection: connection, + schemaOptions: { + collection: collection, + read: 'nearest', + versionKey: false, + }, + }); + }, + inject: [DB_DEFAULT_CONNECTION], + }; +} diff --git a/src/modules/exception/common-exception.filter.ts b/src/modules/exception/common-exception.filter.ts index 9f0fd62..17aa732 100644 --- a/src/modules/exception/common-exception.filter.ts +++ b/src/modules/exception/common-exception.filter.ts @@ -1,5 +1,4 @@ import { ConfigService } from '../config/config.service'; -import { LoggerService } from '../logger/logger.service'; import { ArgumentsHost, Catch, @@ -7,28 +6,21 @@ import { HttpException, Injectable, } from '@nestjs/common'; -import { HttpExceptionFilter } from './http-exception.filter'; import { InternalExceptionFilter } from './internal-exception.filter'; +import { IndexExceptionFilter } from './index-exception.filter'; @Injectable() @Catch() export class CommonExceptionFilter implements ExceptionFilter { constructor( public readonly config: ConfigService, - private readonly logger: LoggerService, - public readonly httpException: HttpExceptionFilter, + public readonly indexException: IndexExceptionFilter, public readonly internalException: InternalExceptionFilter, - ) { - this.logger = this.logger.build(CommonExceptionFilter.name); - } + ) {} async catch(exception: Error, host: ArgumentsHost) { if (exception instanceof HttpException) { - return this.httpException.catch(exception, host); - } - - if (this.config.node.isEnv('development')) { - this.logger.error(exception.stack); + return this.indexException.catch(exception, host); } return this.internalException.catch(exception, host); diff --git a/src/modules/exception/http-exception.filter.ts b/src/modules/exception/http-exception.filter.ts index 2b4b9ed..0fe23ae 100644 --- a/src/modules/exception/http-exception.filter.ts +++ b/src/modules/exception/http-exception.filter.ts @@ -6,23 +6,15 @@ import { HttpException, Injectable, } from '@nestjs/common'; -import { InternalExceptionFilter } from './internal-exception.filter'; @Injectable() @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { - constructor( - private readonly logger: LoggerService, - private readonly internalException: InternalExceptionFilter, - ) { + constructor(private readonly logger: LoggerService) { this.logger = this.logger.build(HttpExceptionFilter.name); } async catch(exception: HttpException, host: ArgumentsHost) { - if (!(exception instanceof HttpException)) { - return this.internalException.catch(exception, host); - } - const time = new Date().toISOString(); const ctx = host.switchToHttp(); const response = ctx.getResponse(); diff --git a/src/modules/exception/index-exception.filter.ts b/src/modules/exception/index-exception.filter.ts index 7844f6c..07a341f 100644 --- a/src/modules/exception/index-exception.filter.ts +++ b/src/modules/exception/index-exception.filter.ts @@ -7,7 +7,6 @@ import { Injectable, } from '@nestjs/common'; import { HttpExceptionFilter } from './http-exception.filter'; -import { LoggerService } from '../logger/logger.service'; import { fileExists } from '../../utils/file-exists.util'; @Injectable() @@ -16,10 +15,7 @@ export class IndexExceptionFilter implements ExceptionFilter { constructor( private readonly config: ConfigService, private readonly httpException: HttpExceptionFilter, - private readonly logger: LoggerService, - ) { - this.logger = logger.build(IndexExceptionFilter.name); - } + ) {} async catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp();