Skip to content

Commit

Permalink
Merge pull request #70 from skgndi12/feature/issue-53/design-reposito…
Browse files Browse the repository at this point in the history
…ry-layer

[#53] Design repository layer
  • Loading branch information
skgndi12 authored Nov 7, 2023
2 parents 776346e + 240fdf2 commit bcbdb17
Show file tree
Hide file tree
Showing 23 changed files with 323 additions and 43 deletions.
4 changes: 3 additions & 1 deletion api/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {}
"rules": {
"@typescript-eslint/no-empty-interface": "off"
}
}
6 changes: 3 additions & 3 deletions api/config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ logger:
format: 'text'
database:
host: '127.0.0.1'
port: 5432
user: 'admin'
password: 'Admin123!'
port: 5435
user: 'mrc-client'
password: 'Client123!'
4 changes: 2 additions & 2 deletions api/generate/openapi.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
32 changes: 16 additions & 16 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 0 additions & 6 deletions api/src/controller/http/dev/dev.v1.controller.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -26,7 +21,6 @@ export class DevV1Controller {
req: Request<any, any, GreetingV1Request>,
res: Response<GreetingV1Response>
) => {
this.logger.info(req.body);
res.send({ message: 'Hello World!' });
};
}
19 changes: 18 additions & 1 deletion api/src/controller/http/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -56,7 +58,7 @@ export class Middleware {
} else {
customError = new CustomError(
InternalErrorType.UNEXPECTED,
'Unexpected error happened'
`Unexpected error occured, error: ${getErrorMessage(err)}`
);
}

Expand Down Expand Up @@ -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({
Expand Down
8 changes: 5 additions & 3 deletions api/src/controller/http/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -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;
};

Expand All @@ -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',
Expand Down
4 changes: 1 addition & 3 deletions api/src/core/entities/profile.entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { UUID } from 'crypto';

export interface Profile {
id: UUID;
id: string;
name: string;
}
4 changes: 1 addition & 3 deletions api/src/core/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { UUID } from 'crypto';

export interface User {
id: UUID;
id: string;
name: string;
email: string;
}
16 changes: 16 additions & 0 deletions api/src/infrastructure/prisma/errors.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>).code === 'string'
);
}
13 changes: 13 additions & 0 deletions api/src/infrastructure/prisma/prisma.client.ts
Original file line number Diff line number Diff line change
@@ -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`
}
}
});
}
44 changes: 44 additions & 0 deletions api/src/infrastructure/prisma/prisma.transaction.manager.ts
Original file line number Diff line number Diff line change
@@ -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 <T>(
callback: (tx: Prisma.TransactionClient) => Promise<T>,
isolationLevel: IsolationLevel,
maxRetries = 3
): Promise<T | null> => {
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;
};
}
5 changes: 5 additions & 0 deletions api/src/infrastructure/repositories/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class RepositoryError extends Error {
constructor(message: string) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -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<Profile> => {
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<Profile> => {
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<Profile> => {
try {
const client = transactionClient ?? this.client;
return client.profile.update({
where: { id },
data: { name }
});
} catch (error: unknown) {
throw new RepositoryError(getErrorMessage(error));
}
};
}
Loading

0 comments on commit bcbdb17

Please sign in to comment.