From 300b0be4a5cb3ebaca50185437e8c91a82a9ec0a Mon Sep 17 00:00:00 2001 From: Josue Date: Thu, 14 Mar 2024 23:01:03 -0400 Subject: [PATCH] feat: add mongo error handling and a new field for project --- api/src/database/errorHandler.ts | 47 +++++++++++++++++ api/src/flags/flags.controller.ts | 34 +++++++++---- api/src/flags/flags.module.ts | 3 +- api/src/flags/flags.repository.ts | 61 ++++++++++++++++++++++ api/src/flags/flags.service.ts | 75 ++++++---------------------- api/src/flags/schemas/flag.schema.ts | 3 ++ api/src/main.ts | 6 ++- 7 files changed, 159 insertions(+), 70 deletions(-) create mode 100644 api/src/database/errorHandler.ts create mode 100644 api/src/flags/flags.repository.ts diff --git a/api/src/database/errorHandler.ts b/api/src/database/errorHandler.ts new file mode 100644 index 0000000..06764f7 --- /dev/null +++ b/api/src/database/errorHandler.ts @@ -0,0 +1,47 @@ +import { ArgumentsHost, Catch, HttpStatus, Logger } from '@nestjs/common'; +import { BaseExceptionFilter } from '@nestjs/core'; +import { Response } from 'express'; +import { MongoError } from 'mongodb'; + +@Catch(MongoError) +export class MongoExceptionFilter extends BaseExceptionFilter { + private readonly logger = new Logger(MongoExceptionFilter.name); + + catch(exception: MongoError, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + this.logger.error(exception.message); + + let status; + switch (exception.name) { + case 'DocumentNotFoundError': { + status = HttpStatus.NOT_FOUND; + response.status(status).json({ + statusCode: HttpStatus.NOT_FOUND, + message: 'Not Found', + }); + + break; + } + case 'MongooseError': + case 'CastError': + case 'DisconnectedError': + case 'DivergentArrayError': + case 'MissingSchemaError': + case 'ValidatorError': + case 'ValidationError': + case 'ObjectExpectedError': + case 'ObjectParameterError': + case 'OverwriteModelError': + case 'ParallelSaveError': + case 'StrictModeError': + case 'VersionError': + case 'MongooseError': + default: { + super.catch(exception, host); + + break; + } + } + } +} diff --git a/api/src/flags/flags.controller.ts b/api/src/flags/flags.controller.ts index a076205..96ce712 100644 --- a/api/src/flags/flags.controller.ts +++ b/api/src/flags/flags.controller.ts @@ -1,4 +1,13 @@ -import { Body, Controller, Get, Post, Patch, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Post, + Patch, + UseGuards, + Param, + Delete, +} from '@nestjs/common'; import FlagsService from './flags.service'; import FlagDto from './flag.dts'; import FlagGuard from './flag.guard'; @@ -8,23 +17,30 @@ import EnableGuard from './enable.guard'; class FlagsController { constructor(private readonly flagsService: FlagsService) {} - @Get('getFlags') + @Get('/') async getFlags(): Promise { - return this.flagsService.findAll(); + return this.flagsService.get(); } @UseGuards(FlagGuard) - @Post('saveFlag') + @Post('/') async saveFlag(@Body() flagRequest: FlagDto): Promise { - return this.flagsService.saveFlag(flagRequest); + return this.flagsService.create(flagRequest); } @UseGuards(EnableGuard) - @Patch('setEnabled') - async setEnabled( - @Body() { id, isEnabled }: { id: string; isEnabled: boolean }, + @Patch('/:id') + async toggleEnabled( + @Param('id') id: string, + @Body() { isEnabled }: { isEnabled: boolean }, ): Promise { - return this.flagsService.setEnabled({ id, isEnabled }); + return this.flagsService.toggleEnabled(id, isEnabled); + } + + @UseGuards(EnableGuard) + @Delete('/:id') + delete(@Param('id') id: string): void { + this.flagsService.delete(id); } } diff --git a/api/src/flags/flags.module.ts b/api/src/flags/flags.module.ts index fb3ad4d..1496847 100644 --- a/api/src/flags/flags.module.ts +++ b/api/src/flags/flags.module.ts @@ -3,12 +3,13 @@ import { MongooseModule } from '@nestjs/mongoose'; import FlagsController from './flags.controller'; import FlagsService from './flags.service'; import { Flag, FlagSchema } from './schemas/flag.schema'; +import FlagsRepository from './flags.repository'; @Module({ imports: [ MongooseModule.forFeature([{ name: Flag.name, schema: FlagSchema }]), ], controllers: [FlagsController], - providers: [FlagsService], + providers: [FlagsService, FlagsRepository], }) export default class FlagsModule {} diff --git a/api/src/flags/flags.repository.ts b/api/src/flags/flags.repository.ts new file mode 100644 index 0000000..4e00046 --- /dev/null +++ b/api/src/flags/flags.repository.ts @@ -0,0 +1,61 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Flag, FlagDocument } from './schemas/flag.schema'; +import { Model, Types } from 'mongoose'; +import FlagDto from './flag.dts'; + +@Injectable() +class FlagsRepository { + private readonly logger = new Logger(FlagsRepository.name); + + constructor( + @InjectModel(Flag.name) + private db: Model, + ) {} + + private formatFlags(flags: FlagDocument[]): FlagDto[] { + return flags.map( + ({ _id, name, type, value, environment, project, isEnabled }) => ({ + id: _id as Types.ObjectId, + name, + type, + value, + environment, + project, + isEnabled, + }), + ); + } + + async create(flag: FlagDto): Promise { + const newFlag = new this.db(flag); + const f = await newFlag.save(); + + return this.formatFlags([f])[0]; + } + + async get(): Promise { + const fs = await this.db.find(); + + return this.formatFlags(fs); + } + + async toggleEnabled(id: string, isEnabled: boolean): Promise { + this.db.findById(id); + const f = await this.db + .findByIdAndUpdate( + { _id: new Types.ObjectId(id) }, + { isEnabled }, + { new: true }, + ) + .orFail(); + + return this.formatFlags([f])[0]; + } + + delete(id: string): void { + this.db.deleteOne({ _id: new Types.ObjectId(id) }); + } +} + +export default FlagsRepository; diff --git a/api/src/flags/flags.service.ts b/api/src/flags/flags.service.ts index 3c72b3d..f6ff7ef 100644 --- a/api/src/flags/flags.service.ts +++ b/api/src/flags/flags.service.ts @@ -1,76 +1,33 @@ -import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model, Types } from 'mongoose'; +import { Injectable, Logger } from '@nestjs/common'; import FlagDto from './flag.dts'; -import { Flag, FlagDocument } from './schemas/flag.schema'; +import FlagsRepository from './flags.repository'; @Injectable() class FlagsService { private readonly logger = new Logger(FlagsService.name); - constructor( - @InjectModel(Flag.name) - private FlagModel: Model, - ) {} + constructor(private repository: FlagsRepository) {} - static formatFlags(flags: FlagDocument[]): FlagDto[] { - return flags.map(({ _id, name, type, value, environment, isEnabled }) => ({ - id: _id as Types.ObjectId, - name, - type, - value, - environment, - isEnabled, - })); - } - - async findAll(): Promise { - const flags = await this.FlagModel.find(); - const formattedFlags = FlagsService.formatFlags(flags); - this.logger.log({ formattedFlags }); + async get(): Promise { + const flags = await this.repository.get(); + this.logger.log({ flags }); - return formattedFlags; + return flags; } - async saveFlag(flag: FlagDto): Promise { - try { - const newFlag = new this.FlagModel(flag); - const storedFlag = await newFlag.save(); - const trimmedFlag = FlagsService.formatFlags([storedFlag]); + async create(flag: FlagDto): Promise { + const f = await this.repository.create(flag); + this.logger.log({ flag: f }); - this.logger.log({ trimmedFlag }); - return trimmedFlag[0]; - } catch (error) { - this.logger.error({ error }); - - throw new HttpException( - 'Service Unavailable', - HttpStatus.SERVICE_UNAVAILABLE, - ); - } + return f; } - async setEnabled({ - id, - isEnabled, - }: { - id: string; - isEnabled: boolean; - }): Promise { - try { - return await this.FlagModel.findOneAndUpdate( - { _id: new Types.ObjectId(id) }, - { isEnabled }, - { new: true }, - ); - } catch (error) { - this.logger.error({ error }); + toggleEnabled(id: string, isEnabled: boolean): Promise { + return this.repository.toggleEnabled(id, isEnabled); + } - throw new HttpException( - 'Service Unavailable', - HttpStatus.SERVICE_UNAVAILABLE, - ); - } + delete(id: string): void { + this.repository.delete(id); } } diff --git a/api/src/flags/schemas/flag.schema.ts b/api/src/flags/schemas/flag.schema.ts index 07449ec..531d6a7 100644 --- a/api/src/flags/schemas/flag.schema.ts +++ b/api/src/flags/schemas/flag.schema.ts @@ -19,6 +19,9 @@ export class Flag { @Prop({ type: String }) environment!: FlagEnv; + @Prop({ type: String }) + project!: string; + @Prop({ type: Boolean, default: true }) isEnabled: boolean; } diff --git a/api/src/main.ts b/api/src/main.ts index fadc812..301e1d5 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,7 +1,8 @@ -import { NestFactory } from '@nestjs/core'; +import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import AppModule from './app.module'; +import { MongoExceptionFilter } from './database/errorHandler'; async function bootstrap() { const logger = new Logger('main.ts'); @@ -11,6 +12,9 @@ async function bootstrap() { const port = configService.get('PORT'); await app.listen(port); + const { httpAdapter } = app.get(HttpAdapterHost); + app.useGlobalFilters(new MongoExceptionFilter(httpAdapter)); + logger.log(`App started on port ${port}`); } bootstrap();