Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#53] Design repository layer #70

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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