Skip to content

Commit

Permalink
feat(controller): add function to create crud controller
Browse files Browse the repository at this point in the history
also use common exception filter as the default filter instead of index filter
  • Loading branch information
moesjarraf committed Feb 25, 2023
1 parent 886fd97 commit ac2e073
Show file tree
Hide file tree
Showing 10 changed files with 180 additions and 41 deletions.
6 changes: 0 additions & 6 deletions src/api/base.controller.ts

This file was deleted.

135 changes: 135 additions & 0 deletions src/api/create-crud-controller.function.ts
Original file line number Diff line number Diff line change
@@ -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<DatabaseEntity>) {}

@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;
}
10 changes: 5 additions & 5 deletions src/api/crud.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DatabaseEntity>,
) {
super();
}
) {}

@Get()
async list(@Query() query) {
Expand Down
4 changes: 2 additions & 2 deletions src/app/app.providers.ts
Original file line number Diff line number Diff line change
@@ -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,
},
];
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';

Expand Down
5 changes: 4 additions & 1 deletion src/modules/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export class ConfigService extends NestConfigService {
}

get debug() {
return boolean(this.get<string>('DEBUG')) || true;
return (
boolean(this.get<string>('DEBUG')) ||
(this.node.isEnv('production') ? false : true)
);
}

get frontend() {
Expand Down
26 changes: 26 additions & 0 deletions src/modules/database/functions/create-provider.function.ts
Original file line number Diff line number Diff line change
@@ -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<DatabaseEntity>,
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],
};
}
16 changes: 4 additions & 12 deletions src/modules/exception/common-exception.filter.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
import { ConfigService } from '../config/config.service';
import { LoggerService } from '../logger/logger.service';
import {
ArgumentsHost,
Catch,
ExceptionFilter,
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);
Expand Down
10 changes: 1 addition & 9 deletions src/modules/exception/http-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 1 addition & 5 deletions src/modules/exception/index-exception.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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();
Expand Down

0 comments on commit ac2e073

Please sign in to comment.