From d553947cc32bba7f30d04fafe7082315f439e96e Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 15:13:33 +0900 Subject: [PATCH 1/9] feat: update ORM package --- api/package-lock.json | 32 ++++++++++++++++---------------- api/package.json | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 22fc51487..3b6284bea 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -10,13 +10,13 @@ "license": "MIT", "dependencies": { "@openapi-contrib/json-schema-to-openapi-schema": "^2.2.5", - "@prisma/client": "^5.3.1", + "@prisma/client": "^5.5.2", "config": "^3.3.9", "express": "^4.18.2", "express-async-errors": "^3.1.1", "express-openapi-validator": "^5.0.4", "js-yaml": "^4.1.0", - "prisma": "^5.3.1", + "prisma": "^5.5.2", "rimraf": "^5.0.0", "swagger-ui-express": "^5.0.0", "tsc-alias": "^1.8.5", @@ -1894,12 +1894,12 @@ } }, "node_modules/@prisma/client": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz", - "integrity": "sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.5.2.tgz", + "integrity": "sha512-54XkqR8M+fxbzYqe+bIXimYnkkcGqgOh0dn0yWtIk6CQT4IUCAvNFNcQZwk2KqaLU+/1PHTSWrcHtx4XjluR5w==", "hasInstallScript": true, "dependencies": { - "@prisma/engines-version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" + "@prisma/engines-version": "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a" }, "engines": { "node": ">=16.13" @@ -1914,15 +1914,15 @@ } }, "node_modules/@prisma/engines": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz", - "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.5.2.tgz", + "integrity": "sha512-Be5hoNF8k+lkB3uEMiCHbhbfF6aj1GnrTBnn5iYFT7GEr3TsOEp1soviEcBR0tYCgHbxjcIxJMhdbvxALJhAqg==", "hasInstallScript": true }, "node_modules/@prisma/engines-version": { - "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz", - "integrity": "sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w==" + "version": "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a.tgz", + "integrity": "sha512-O+qHFnZvAyOFk1tUco2/VdiqS0ym42a3+6CYLScllmnpbyiTplgyLt2rK/B9BTjYkSHjrgMhkG47S0oqzdIckA==" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -6568,12 +6568,12 @@ } }, "node_modules/prisma": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.3.1.tgz", - "integrity": "sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.5.2.tgz", + "integrity": "sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w==", "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.3.1" + "@prisma/engines": "5.5.2" }, "bin": { "prisma": "build/index.js" diff --git a/api/package.json b/api/package.json index 3b03089d9..8480a39e0 100644 --- a/api/package.json +++ b/api/package.json @@ -50,13 +50,13 @@ }, "dependencies": { "@openapi-contrib/json-schema-to-openapi-schema": "^2.2.5", - "@prisma/client": "^5.3.1", + "@prisma/client": "^5.5.2", "config": "^3.3.9", "express": "^4.18.2", "express-async-errors": "^3.1.1", "express-openapi-validator": "^5.0.4", "js-yaml": "^4.1.0", - "prisma": "^5.3.1", + "prisma": "^5.5.2", "rimraf": "^5.0.0", "swagger-ui-express": "^5.0.0", "tsc-alias": "^1.8.5", From 863d84e4dc1cbeff8ab81da493925e1177051dbd Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 12:45:20 +0900 Subject: [PATCH 2/9] feat: update sample entity --- api/src/core/entities/profile.entity.ts | 4 +--- api/src/core/entities/user.entity.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/api/src/core/entities/profile.entity.ts b/api/src/core/entities/profile.entity.ts index a3b73b1b4..796adcc42 100644 --- a/api/src/core/entities/profile.entity.ts +++ b/api/src/core/entities/profile.entity.ts @@ -1,6 +1,4 @@ -import { UUID } from 'crypto'; - export interface Profile { - id: UUID; + id: string; name: string; } diff --git a/api/src/core/entities/user.entity.ts b/api/src/core/entities/user.entity.ts index a60ef9b34..15c990154 100644 --- a/api/src/core/entities/user.entity.ts +++ b/api/src/core/entities/user.entity.ts @@ -1,7 +1,5 @@ -import { UUID } from 'crypto'; - export interface User { - id: UUID; + id: string; name: string; email: string; } From 964a1fad29bb6e1b07823a60f14bebef0e6a1fe9 Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 12:51:57 +0900 Subject: [PATCH 3/9] feat: generate ORM client --- api/config/default.yaml | 6 +++--- api/src/infrastructure/prisma/prisma.client.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 api/src/infrastructure/prisma/prisma.client.ts diff --git a/api/config/default.yaml b/api/config/default.yaml index 77e74c5e9..5a7b3d250 100644 --- a/api/config/default.yaml +++ b/api/config/default.yaml @@ -9,6 +9,6 @@ logger: format: 'text' database: host: '127.0.0.1' - port: 5432 - user: 'admin' - password: 'Admin123!' \ No newline at end of file + port: 5435 + user: 'mrc-client' + password: 'Client123!' \ No newline at end of file diff --git a/api/src/infrastructure/prisma/prisma.client.ts b/api/src/infrastructure/prisma/prisma.client.ts new file mode 100644 index 000000000..50398ab7b --- /dev/null +++ b/api/src/infrastructure/prisma/prisma.client.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from '@prisma/client'; + +import { DatabaseConfig } from '@src/infrastructure/repositories/types'; + +export function generatePrismaClient(config: DatabaseConfig): PrismaClient { + return new PrismaClient({ + datasources: { + db: { + url: `postgresql://${config.user}:${config.password}@${config.host}:${config.port}/mrc` + } + } + }); +} From 52ca75bf290a7e89fb5a41889eb60a5006d2f322 Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 12:48:35 +0900 Subject: [PATCH 4/9] feat: define repository and tx manager interface --- api/.eslintrc | 4 +++- api/src/ports/profile.repository.ts | 12 ++++++++++++ api/src/ports/transaction.manager.ts | 11 +++++++++++ api/src/ports/user.repository.ts | 17 +++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 api/src/ports/profile.repository.ts create mode 100644 api/src/ports/transaction.manager.ts create mode 100644 api/src/ports/user.repository.ts diff --git a/api/.eslintrc b/api/.eslintrc index 4e16bbfdc..7804ec795 100644 --- a/api/.eslintrc +++ b/api/.eslintrc @@ -7,5 +7,7 @@ "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], - "rules": {} + "rules": { + "@typescript-eslint/no-empty-interface": "off" + } } diff --git a/api/src/ports/profile.repository.ts b/api/src/ports/profile.repository.ts new file mode 100644 index 000000000..181ccef33 --- /dev/null +++ b/api/src/ports/profile.repository.ts @@ -0,0 +1,12 @@ +import { Profile } from '@src/core/entities/profile.entity'; +import { TransactionClient } from '@src/ports/transaction.manager'; + +export interface ProfileRepository { + create(name: string, transactionClient?: TransactionClient): Promise; + findById(id: string, transactionClient?: TransactionClient): Promise; + updateById( + id: string, + name: string, + transactionClient?: TransactionClient + ): Promise; +} diff --git a/api/src/ports/transaction.manager.ts b/api/src/ports/transaction.manager.ts new file mode 100644 index 000000000..3620a9086 --- /dev/null +++ b/api/src/ports/transaction.manager.ts @@ -0,0 +1,11 @@ +import { IsolationLevel } from '@src/infrastructure/repositories/types'; + +export interface TransactionClient {} + +export interface TransactionManager { + runInTransaction( + callback: (tx: TransactionClient) => Promise, + isolationLevel: IsolationLevel, + maxRetries?: number + ): Promise; +} diff --git a/api/src/ports/user.repository.ts b/api/src/ports/user.repository.ts new file mode 100644 index 000000000..e57333cb1 --- /dev/null +++ b/api/src/ports/user.repository.ts @@ -0,0 +1,17 @@ +import { User } from '@src/core/entities/user.entity'; +import { TransactionClient } from '@src/ports/transaction.manager'; + +export interface UserRepository { + create( + name: string, + email: string, + transactionClient?: TransactionClient + ): Promise; + findById(id: string, transactionClient?: TransactionClient): Promise; + updateById( + id: string, + name: string, + email: string, + transactionClient?: TransactionClient + ): Promise; +} From 3b317e0241a7fb677a0cfc586e769caedce85c68 Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 12:53:30 +0900 Subject: [PATCH 5/9] feat: implements repository and tx manager --- api/src/infrastructure/prisma/errors.ts | 16 ++++++ .../prisma/prisma.transaction.manager.ts | 44 +++++++++++++++ api/src/infrastructure/repositories/errors.ts | 5 ++ .../postgresql/profile.repository.ts | 53 +++++++++++++++++++ .../postgresql/user.repository.ts | 52 ++++++++++++++++++ api/src/infrastructure/repositories/types.ts | 7 +++ 6 files changed, 177 insertions(+) create mode 100644 api/src/infrastructure/prisma/errors.ts create mode 100644 api/src/infrastructure/prisma/prisma.transaction.manager.ts create mode 100644 api/src/infrastructure/repositories/errors.ts create mode 100644 api/src/infrastructure/repositories/postgresql/profile.repository.ts create mode 100644 api/src/infrastructure/repositories/postgresql/user.repository.ts diff --git a/api/src/infrastructure/prisma/errors.ts b/api/src/infrastructure/prisma/errors.ts new file mode 100644 index 000000000..3c4a5dca7 --- /dev/null +++ b/api/src/infrastructure/prisma/errors.ts @@ -0,0 +1,16 @@ +export enum PrismaErrorCode { + TRANSACTION_CONFLICT = 'P2034' +} + +export interface ErrorWithCode { + code: string; +} + +export function isErrorWithCode(error: unknown): error is ErrorWithCode { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + typeof (error as Record).code === 'string' + ); +} diff --git a/api/src/infrastructure/prisma/prisma.transaction.manager.ts b/api/src/infrastructure/prisma/prisma.transaction.manager.ts new file mode 100644 index 000000000..b5fb544ad --- /dev/null +++ b/api/src/infrastructure/prisma/prisma.transaction.manager.ts @@ -0,0 +1,44 @@ +import { Prisma, PrismaClient } from '@prisma/client'; + +import { + PrismaErrorCode, + isErrorWithCode +} from '@src/infrastructure/prisma/errors'; +import { RepositoryError } from '@src/infrastructure/repositories/errors'; +import { IsolationLevel } from '@src/infrastructure/repositories/types'; +import { TransactionManager } from '@src/ports/transaction.manager'; +import { getErrorMessage } from '@src/util/error'; + +export class PrismaTransactionManager implements TransactionManager { + constructor(private readonly client: PrismaClient) {} + + public runInTransaction = async ( + callback: (tx: Prisma.TransactionClient) => Promise, + isolationLevel: IsolationLevel, + maxRetries = 3 + ): Promise => { + let retries = 0; + let result: T | null = null; + + while (retries < maxRetries) { + try { + result = await this.client.$transaction(callback, { + isolationLevel + }); + break; + } catch (error: unknown) { + if ( + isErrorWithCode(error) && + error.code === PrismaErrorCode.TRANSACTION_CONFLICT + ) { + retries++; + continue; + } + + throw new RepositoryError(getErrorMessage(error)); + } + } + + return result; + }; +} diff --git a/api/src/infrastructure/repositories/errors.ts b/api/src/infrastructure/repositories/errors.ts new file mode 100644 index 000000000..839c76547 --- /dev/null +++ b/api/src/infrastructure/repositories/errors.ts @@ -0,0 +1,5 @@ +export class RepositoryError extends Error { + constructor(message: string) { + super(message); + } +} diff --git a/api/src/infrastructure/repositories/postgresql/profile.repository.ts b/api/src/infrastructure/repositories/postgresql/profile.repository.ts new file mode 100644 index 000000000..3e5c8a974 --- /dev/null +++ b/api/src/infrastructure/repositories/postgresql/profile.repository.ts @@ -0,0 +1,53 @@ +import { Prisma, PrismaClient } from '@prisma/client'; + +import { Profile } from '@src/core/entities/profile.entity'; +import { RepositoryError } from '@src/infrastructure/repositories/errors'; +import { ProfileRepository } from '@src/ports/profile.repository'; +import { getErrorMessage } from '@src/util/error'; + +export class PostgresqlProfileRepository implements ProfileRepository { + constructor(private readonly client: PrismaClient) {} + + public create = async ( + name: string, + transactionClient?: Prisma.TransactionClient + ): Promise => { + try { + const client = transactionClient ?? this.client; + return client.profile.create({ data: { name } }); + } catch (error: unknown) { + throw new RepositoryError(getErrorMessage(error)); + } + }; + + public findById = async ( + id: string, + transactionClient?: Prisma.TransactionClient + ): Promise => { + try { + const client = transactionClient ?? this.client; + const profile = client.profile.findFirstOrThrow({ + where: { id } + }); + return profile; + } catch (error: unknown) { + throw new RepositoryError(getErrorMessage(error)); + } + }; + + public updateById = async ( + id: string, + name: string, + transactionClient?: Prisma.TransactionClient + ): Promise => { + try { + const client = transactionClient ?? this.client; + return client.profile.update({ + where: { id }, + data: { name } + }); + } catch (error: unknown) { + throw new RepositoryError(getErrorMessage(error)); + } + }; +} diff --git a/api/src/infrastructure/repositories/postgresql/user.repository.ts b/api/src/infrastructure/repositories/postgresql/user.repository.ts new file mode 100644 index 000000000..8e2d5ef7a --- /dev/null +++ b/api/src/infrastructure/repositories/postgresql/user.repository.ts @@ -0,0 +1,52 @@ +import { Prisma, PrismaClient } from '@prisma/client'; + +import { User } from '@src/core/entities/user.entity'; +import { RepositoryError } from '@src/infrastructure/repositories/errors'; +import { UserRepository } from '@src/ports/user.repository'; +import { getErrorMessage } from '@src/util/error'; + +export class PostgresqlUserRepository implements UserRepository { + constructor(private readonly client: PrismaClient) {} + + public create = async ( + name: string, + email: string, + transactionClient?: Prisma.TransactionClient + ): Promise => { + try { + const client = transactionClient ?? this.client; + return client.user.create({ data: { name, email } }); + } catch (error: unknown) { + throw new RepositoryError(getErrorMessage(error)); + } + }; + + public findById = async ( + id: string, + transactionClient?: Prisma.TransactionClient + ): Promise => { + try { + const client = transactionClient ?? this.client; + return client.user.findFirstOrThrow({ where: { id } }); + } catch (error: unknown) { + throw new RepositoryError(getErrorMessage(error)); + } + }; + + public updateById = async ( + id: string, + name: string, + email: string, + transactionClient?: Prisma.TransactionClient + ): Promise => { + try { + const client = transactionClient ?? this.client; + return client.user.update({ + where: { id }, + data: { name, email } + }); + } catch (error: unknown) { + throw new RepositoryError(getErrorMessage(error)); + } + }; +} diff --git a/api/src/infrastructure/repositories/types.ts b/api/src/infrastructure/repositories/types.ts index 9e2306de0..694830032 100644 --- a/api/src/infrastructure/repositories/types.ts +++ b/api/src/infrastructure/repositories/types.ts @@ -4,3 +4,10 @@ export interface DatabaseConfig { user: string; password: string; } + +export enum IsolationLevel { + READ_UNCOMMITTED = 'ReadUncommitted', + READ_COMMITTED = 'ReadCommitted', + REPEATABLE_READ = 'RepeatableRead', + SERIALIZABLE = 'Serializable' +} From e0d412b289c1ee53d251d641c2694848f8dc9f8d Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 14:42:50 +0900 Subject: [PATCH 6/9] feat: initialize repository and tx manager --- api/src/main.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/api/src/main.ts b/api/src/main.ts index 620e2c332..3d3714009 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -1,8 +1,13 @@ import { + buildDatabaseConfig, buildHttpConfig, buildLoggerConfig, loadConfig } from '@src/config/loader'; +import { generatePrismaClient } from '@src/infrastructure/prisma/prisma.client'; +import { PrismaTransactionManager } from '@src/infrastructure/prisma/prisma.transaction.manager'; +import { PostgresqlProfileRepository } from '@src/infrastructure/repositories/postgresql/profile.repository'; +import { PostgresqlUserRepository } from '@src/infrastructure/repositories/postgresql/user.repository'; import { initializeLogger } from '@src/logger/logger'; import { HttpServer } from '@controller/http/server'; @@ -17,6 +22,12 @@ async function main() { } const logger = initializeLogger(buildLoggerConfig(config)); + const prismaClient = generatePrismaClient(buildDatabaseConfig(config)); + const prismaTransactionManager = new PrismaTransactionManager(prismaClient); + + const profileRepository = new PostgresqlProfileRepository(prismaClient); + const userRepository = new PostgresqlUserRepository(prismaClient); + const httpServer = new HttpServer(logger, buildHttpConfig(config)); await httpServer.start(); // await httpServer.close(); From 0ff9ecb5670e9224df92028e259e59e2cb6170bb Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 14:43:42 +0900 Subject: [PATCH 7/9] feat: update openapi spec --- api/generate/openapi.json | 4 ++-- api/src/controller/http/dev/dev.v1.controller.ts | 6 ------ api/src/controller/http/server.ts | 8 +++++--- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/api/generate/openapi.json b/api/generate/openapi.json index 603fe0af6..0e0675d19 100644 --- a/api/generate/openapi.json +++ b/api/generate/openapi.json @@ -1,7 +1,7 @@ { "info": { - "title": "Mr.C API", - "version": "1.0.0" + "title": "mrc-api", + "version": "0.0.1" }, "openapi": "3.0.3", "paths": { diff --git a/api/src/controller/http/dev/dev.v1.controller.ts b/api/src/controller/http/dev/dev.v1.controller.ts index 38f7f0cd8..49a31d470 100644 --- a/api/src/controller/http/dev/dev.v1.controller.ts +++ b/api/src/controller/http/dev/dev.v1.controller.ts @@ -1,20 +1,15 @@ import { Request, Response, Router } from 'express'; -import express from 'express'; -import { Logger } from 'winston'; import { GreetingV1Request } from '@controller/http/dev/request/dev.v1.request'; import { GreetingV1Response } from '@controller/http/dev/response/dev.v1.response'; import { methodNotAllowed } from '@controller/http/handler'; export class DevV1Controller { - constructor(public logger: Logger) {} - public routes = (): Router => { const router: Router = Router(); const prefix = '/v1/dev'; router - .use(express.json()) .route(`${prefix}/greeting`) .post(this.greeting) .all(methodNotAllowed); @@ -26,7 +21,6 @@ export class DevV1Controller { req: Request, res: Response ) => { - this.logger.info(req.body); res.send({ message: 'Hello World!' }); }; } diff --git a/api/src/controller/http/server.ts b/api/src/controller/http/server.ts index d48791939..338e979fa 100644 --- a/api/src/controller/http/server.ts +++ b/api/src/controller/http/server.ts @@ -8,6 +8,7 @@ import { Tspec, TspecDocsMiddleware } from 'tspec'; import { Logger } from 'winston'; import apiSpecification from '@root/generate/openapi.json'; +import { name, version } from '@root/package.json'; import { DevV1Controller } from '@controller/http/dev/dev.v1.controller'; import { HealthController } from '@controller/http/health/health.controller'; @@ -30,6 +31,7 @@ export class HttpServer { this.app = express(); this.app.disable('x-powered-by'); this.app.set('trust proxy', 0); + this.app.use(express.json()); await this.buildApiDocument(); this.app.use('/api', this.middleware.accessLog); this.app.use( @@ -63,7 +65,7 @@ export class HttpServer { }; private getApiRouters = (): express.Router[] => { - const routers = [new DevV1Controller(this.logger).routes()]; + const routers = [new DevV1Controller().routes()]; return routers; }; @@ -80,8 +82,8 @@ export class HttpServer { outputPath: './generate/openapi.json', specVersion: 3, openapi: { - title: 'Mr.C API', - version: '1.0.0', + title: name, + version: version, securityDefinitions: { jwt: { type: 'http', From 1f84dde1e291aa39869e01e8aa1e575ae9cea2dd Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Mon, 6 Nov 2023 23:38:12 +0900 Subject: [PATCH 8/9] feat: add types and functions for error message --- api/src/util/error.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 api/src/util/error.ts diff --git a/api/src/util/error.ts b/api/src/util/error.ts new file mode 100644 index 000000000..d077a7215 --- /dev/null +++ b/api/src/util/error.ts @@ -0,0 +1,28 @@ +export interface ErrorWithMessage { + message: string; +} + +export function isErrorWithMessage(error: unknown): error is ErrorWithMessage { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as Record).message === 'string' + ); +} + +export function toErrorWithMessage(maybeError: unknown): ErrorWithMessage { + if (isErrorWithMessage(maybeError)) return maybeError; + + try { + return new Error(JSON.stringify(maybeError)); + } catch { + // fallback in case there's an error stringifying the maybeError + // like with circular references for example. + return new Error(String(maybeError)); + } +} + +export function getErrorMessage(error: unknown) { + return toErrorWithMessage(error).message; +} From 240fdf2a4fa8209d948b961864be0a8cf8737eb1 Mon Sep 17 00:00:00 2001 From: skgndi12 Date: Tue, 7 Nov 2023 17:17:16 +0900 Subject: [PATCH 9/9] feat: add logs for HTTP 4xx, 5xx error --- api/src/controller/http/middleware.ts | 19 ++++++++++++++++++- api/test/controller/http/middleware.test.ts | 6 +++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/api/src/controller/http/middleware.ts b/api/src/controller/http/middleware.ts index 5394e48e1..5a2afe5bc 100644 --- a/api/src/controller/http/middleware.ts +++ b/api/src/controller/http/middleware.ts @@ -3,6 +3,8 @@ import 'express-async-errors'; import { HttpError as ValidationError } from 'express-openapi-validator/dist/framework/types'; import { Logger } from 'winston'; +import { getErrorMessage } from '@src/util/error'; + import { BadRequestErrorType, CustomError, @@ -56,7 +58,7 @@ export class Middleware { } else { customError = new CustomError( InternalErrorType.UNEXPECTED, - 'Unexpected error happened' + `Unexpected error occured, error: ${getErrorMessage(err)}` ); } @@ -129,6 +131,21 @@ export class Middleware { ) => { const statusCode = this.getStatusCode(err); + switch (true) { + case statusCode >= 500: + this.logger.error('5xx error occured', { + error: err, + statusCode: statusCode + }); + break; + case statusCode >= 400: + this.logger.warn('4xx error occured', { + error: err, + statusCode: statusCode + }); + break; + } + res.locals.error = err; res.status(statusCode); res.send({ diff --git a/api/test/controller/http/middleware.test.ts b/api/test/controller/http/middleware.test.ts index a2791cf8d..71cb1a91d 100644 --- a/api/test/controller/http/middleware.test.ts +++ b/api/test/controller/http/middleware.test.ts @@ -176,7 +176,7 @@ describe('Test middleware', () => { let baseUrl: string; beforeAll(async () => { - mockLogger = {}; + mockLogger = { error: jest.fn(), warn: jest.fn() }; testHttpServer = new TestHttpServer(mockLogger as Logger); baseUrl = '/api/v1/dev'; await testHttpServer.start(); @@ -215,7 +215,7 @@ describe('Test middleware', () => { expect(response.status).toEqual(500); expect(response.body).toStrictEqual({ type: InternalErrorType.UNEXPECTED, - messages: ['Unexpected error happened'] + messages: ['Unexpected error occured, error: Error from throwSyncError'] }); }); @@ -226,7 +226,7 @@ describe('Test middleware', () => { expect(response.status).toEqual(500); expect(response.body).toStrictEqual({ type: InternalErrorType.UNEXPECTED, - messages: ['Unexpected error happened'] + messages: ['Unexpected error occured, error: Error from throwAsyncError'] }); });