Skip to content

Commit

Permalink
Merge pull request #12 from FoxMalder-coder/module9-task1
Browse files Browse the repository at this point in the history
  • Loading branch information
keksobot authored Nov 4, 2024
2 parents 549f5b3 + 895e435 commit ab7be91
Show file tree
Hide file tree
Showing 39 changed files with 388 additions and 109 deletions.
22 changes: 20 additions & 2 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"devDependencies": {
"@types/convict": "6.1.6",
"@types/convict-format-with-validator": "6.0.5",
"@types/cors": "2.8.17",
"@types/express": "5.0.0",
"@types/mime-types": "2.1.4",
"@types/multer": "1.4.12",
Expand Down Expand Up @@ -53,6 +54,7 @@
"class-validator": "0.14.1",
"convict": "6.2.4",
"convict-format-with-validator": "6.2.0",
"cors": "2.8.5",
"dayjs": "1.11.13",
"dotenv": "16.4.5",
"express": "4.21.1",
Expand Down
13 changes: 11 additions & 2 deletions src/rest/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import express, { Express } from 'express';
import { ExceptionFilter } from '../shared/libs/rest/exception-filter/exception-filter.interface.js';
import { Controller } from '../shared/libs/rest/controller/controller.interface.js';
import { ParseTokenMiddleware } from '../shared/libs/rest/middleware/parse-token.middleware.js';
import { getFullServerPath } from '../shared/helpers/common.js';
import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from './rest.constant.js';
import cors from 'cors';

@injectable()
export class RESTApplication {
Expand All @@ -23,6 +26,8 @@ export class RESTApplication {
@inject(Component.OfferController) private readonly offerController: Controller,
@inject(Component.CommentController) private readonly commentController: Controller,
@inject(Component.AuthExceptionFilter) private readonly authExceptionFilter: ExceptionFilter,
@inject(Component.HttpExceptionFilter) private readonly httpExceptionFilter: ExceptionFilter,
@inject(Component.ValidationExceptionFilter) private readonly validationExceptionFilter: ExceptionFilter,
) {
this.server = express();
}
Expand Down Expand Up @@ -53,12 +58,16 @@ export class RESTApplication {
private async initMiddleware() {
const authenticateMiddleware = new ParseTokenMiddleware(this.config.get('JWT_SECRET'));
this.server.use(express.json());
this.server.use('/upload', express.static(this.config.get('UPLOAD_DIRECTORY')));
this.server.use(STATIC_UPLOAD_ROUTE, express.static(this.config.get('UPLOAD_DIRECTORY')));
this.server.use(STATIC_FILES_ROUTE, express.static(this.config.get('STATIC_DIRECTORY_PATH')));
this.server.use(authenticateMiddleware.execute.bind(authenticateMiddleware));
this.server.use(cors());
}

private async initExceptionFilters() {
this.server.use(this.authExceptionFilter.catch.bind(this.authExceptionFilter));
this.server.use(this.validationExceptionFilter.catch.bind(this.validationExceptionFilter));
this.server.use(this.httpExceptionFilter.catch.bind(this.httpExceptionFilter));
this.server.use(this.appExceptionFilter.catch.bind(this.appExceptionFilter));
}

Expand All @@ -83,6 +92,6 @@ export class RESTApplication {

this.logger.info('Try to init server...');
await this.initServer();
this.logger.info(`🚀 Server started on http://localhost:${this.config.get('PORT')}`);
this.logger.info(`🚀 Server started on ${getFullServerPath(this.config.get('HOST'), this.config.get('PORT'))}`);
}
}
2 changes: 2 additions & 0 deletions src/rest/rest.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const STATIC_UPLOAD_ROUTE = '/upload';
export const STATIC_FILES_ROUTE = '/static';
5 changes: 4 additions & 1 deletion src/rest/rest.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Config, RestConfig, RestSchema } from '../shared/libs/config/index.js';
import { DatabaseClient } from '../shared/libs/database-client/database-client.interface.js';
import { MongoDatabaseClient } from '../shared/libs/database-client/mongo.database-client.js';
import { RESTApplication } from './rest.application.js';
import { AppExceptionFilter, ExceptionFilter } from '../shared/libs/rest/index.js';
import { AppExceptionFilter, ExceptionFilter, HttpErrorExceptionFilter, PathTransformer, ValidationExceptionFilter } from '../shared/libs/rest/index.js';

export function createRestApplicationContainer() {
const restApplicationContainer = new Container();
Expand All @@ -16,6 +16,9 @@ export function createRestApplicationContainer() {
restApplicationContainer.bind<Config<RestSchema>>(Component.Config).to(RestConfig).inSingletonScope();
restApplicationContainer.bind<DatabaseClient>(Component.DatabaseClient).to(MongoDatabaseClient).inSingletonScope();
restApplicationContainer.bind<ExceptionFilter>(Component.ExceptionFilter).to(AppExceptionFilter).inSingletonScope();
restApplicationContainer.bind<ExceptionFilter>(Component.HttpExceptionFilter).to(HttpErrorExceptionFilter).inSingletonScope();
restApplicationContainer.bind<ExceptionFilter>(Component.ValidationExceptionFilter).to(ValidationExceptionFilter).inSingletonScope();
restApplicationContainer.bind<PathTransformer>(Component.PathTransformer).to(PathTransformer).inSingletonScope();

return restApplicationContainer;
}
3 changes: 3 additions & 0 deletions src/shared/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export const Component = {
CommentController: Symbol.for('CommentController'),
AuthService: Symbol.for('AuthService'),
AuthExceptionFilter: Symbol.for('AuthExceptionFilter'),
HttpExceptionFilter: Symbol.for('HttpExceptionFilter'),
ValidationExceptionFilter: Symbol.for('ValidationExceptionFilter'),
PathTransformer: Symbol.for('PathTransformer'),
} as const;

export enum SortType {
Expand Down
20 changes: 16 additions & 4 deletions src/shared/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ClassConstructor, plainToInstance } from 'class-transformer';
import { ValidationError } from 'class-validator';
import { ApplicationError, ValidationErrorField } from '../libs/rest/index.js';

export function getRandomNumber(a: number, b: number, rank: number = 0) {
return +(Math.random() * (b - a) + a).toFixed(rank);
Expand All @@ -25,8 +27,18 @@ export function fillDTO<T, V>(someDto: ClassConstructor<T>, plainObject: V) {
return plainToInstance(someDto, plainObject, { excludeExtraneousValues: true });
}

export function createErrorObject(message: string) {
return {
error: message,
};
export function createErrorObject(errorType: ApplicationError, error: string, details: ValidationErrorField[] = []) {
return { errorType, error, details };
}

export function reduceValidationErrors(errors: ValidationError[]): ValidationErrorField[] {
return errors.map(({ property, value, constraints }) => ({
property,
value,
messages: constraints ? Object.values(constraints) : []
}));
}

export function getFullServerPath(host: string, port: number) {
return `http://${host}:${port}`;
}
14 changes: 14 additions & 0 deletions src/shared/libs/config/rest.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type RestSchema = {
DB_NAME: string;
UPLOAD_DIRECTORY: string;
JWT_SECRET: string;
HOST: string;
STATIC_DIRECTORY_PATH: string;
}

export const configRestSchema = convict<RestSchema>({
Expand Down Expand Up @@ -70,4 +72,16 @@ export const configRestSchema = convict<RestSchema>({
env: 'JWT_SECRET',
default: null
},
HOST: {
doc: 'Host where started service',
format: String,
env: 'HOST',
default: 'localhost'
},
STATIC_DIRECTORY_PATH: {
doc: 'Path to directory with static resources',
format: String,
env: 'STATIC_DIRECTORY_PATH',
default: 'static'
},
});
8 changes: 6 additions & 2 deletions src/shared/libs/rest/controller/base-controller.abstract.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { injectable } from 'inversify';
import { inject, injectable } from 'inversify';
import { Controller } from './controller.interface.js';
import { Response, Router } from 'express';
import { Logger } from '../../logger/logger.interface.js';
import { Route } from '../route.interface.js';
import { StatusCodes } from 'http-status-codes';
import asyncHandler from 'express-async-handler';
import { PathTransformer } from '../transform/path-transformer.js';
import { Component } from '../../../const.js';

@injectable()
export abstract class BaseController implements Controller {
private readonly DEFAULT_CONTENT_TYPE = 'application/json';
private readonly _router: Router;
@inject(Component.PathTransformer) private pathTranformer: PathTransformer;

constructor(
protected readonly logger: Logger
Expand All @@ -30,10 +33,11 @@ export abstract class BaseController implements Controller {
}

public send<T>(res: Response, statusCode: number, data: T): void {
const modifiedData = this.pathTranformer.execute(data as Record<string, unknown>);
res
.type(this.DEFAULT_CONTENT_TYPE)
.status(statusCode)
.json(data);
.json(modifiedData);
}

public created<T>(res: Response, data: T): void {
Expand Down
39 changes: 0 additions & 39 deletions src/shared/libs/rest/exception-filter/app-exception-filter.ts

This file was deleted.

24 changes: 24 additions & 0 deletions src/shared/libs/rest/exception-filter/app.exception-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Request, Response, NextFunction } from 'express';
import { StatusCodes } from 'http-status-codes';
import { injectable, inject } from 'inversify';
import { Component } from '../../../const.js';
import { createErrorObject } from '../../../helpers/common.js';
import { ApplicationError } from '../types/application-error.enum.js';
import { ExceptionFilter } from './exception-filter.interface.js';
import { Logger } from '../../logger/logger.interface.js';

@injectable()
export class AppExceptionFilter implements ExceptionFilter {
constructor(
@inject(Component.Logger) private readonly logger: Logger
) {
this.logger.info('Register AppExceptionFilter');
}

public catch(error: Error, _req: Request, res: Response, _next: NextFunction): void {
this.logger.error(error.message, error);
res
.status(StatusCodes.INTERNAL_SERVER_ERROR)
.json(createErrorObject(ApplicationError.ServiceError, error.message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Request, Response, NextFunction } from 'express';
import { StatusCodes } from 'http-status-codes';
import { injectable, inject } from 'inversify';
import { Component } from '../../../const.js';
import { createErrorObject } from '../../../helpers/common.js';
import { Logger } from '../../logger/logger.interface.js';
import { HttpError } from '../http-error.js';
import { ApplicationError } from '../types/application-error.enum.js';
import { ExceptionFilter } from './exception-filter.interface.js';

@injectable()
export class HttpErrorExceptionFilter implements ExceptionFilter {
constructor(
@inject(Component.Logger) private readonly logger: Logger
) {
this.logger.info('Register HttpErrorExceptionFilter');
}

public catch(error: unknown, req: Request, res: Response, next: NextFunction): void {
if (!(error instanceof HttpError)) {
return next(error);
}

this.logger.error(`[HttpErrorException]: ${req.path} # ${error.message}`, error);

res
.status(StatusCodes.BAD_REQUEST)
.json(createErrorObject(ApplicationError.CommonError, error.message));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Request, Response, NextFunction } from 'express';
import { StatusCodes } from 'http-status-codes';
import { injectable, inject } from 'inversify';
import { Component } from '../../../const.js';
import { createErrorObject } from '../../../helpers/common.js';
import { ApplicationError } from '../types/application-error.enum.js';
import { ExceptionFilter } from './exception-filter.interface.js';
import { Logger } from '../../logger/logger.interface.js';
import { ValidationError } from '../validation-error.js';

@injectable()
export class ValidationExceptionFilter implements ExceptionFilter {
constructor(
@inject(Component.Logger) private readonly logger: Logger
) {
this.logger.info('Register ValidationExceptionFilter');
}

public catch(error: unknown, _req: Request, res: Response, next: NextFunction): void {
if (!(error instanceof ValidationError)) {
return next(error);
}

this.logger.error(`[ValidationException]: ${error.message}`, error);

error.details.forEach(
(errorField) => this.logger.warn(`[${errorField.property}] — ${errorField.messages}`)
);

res
.status(StatusCodes.BAD_REQUEST)
.json(createErrorObject(ApplicationError.ValidationError, error.message, error.details));
}
}
7 changes: 6 additions & 1 deletion src/shared/libs/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { ValidateObjectIdMiddleware } from './middleware/validate-objectid.middl
export { ValidateDtoMiddleware } from './middleware/validate-dto.middleware.js';
export { Middleware } from './middleware/middleware.interface.js';
export { ExceptionFilter } from './exception-filter/exception-filter.interface.js';
export { AppExceptionFilter } from './exception-filter/app-exception-filter.js';
export { AppExceptionFilter } from './exception-filter/app.exception-filter.js';
export { Controller } from './controller/controller.interface.js';
export { BaseController } from './controller/base-controller.abstract.js';
export { Route } from './route.interface.js';
Expand All @@ -11,3 +11,8 @@ export { DocumentExistsMiddleware } from './middleware/document-exists.middlewar
export { UploadFileMiddleware } from './middleware/upload-file.middleware.js';
export { ParseTokenMiddleware } from './middleware/parse-token.middleware.js';
export { PrivateRouteMiddleware } from './middleware/private-route.middleware.js';
export { ValidationErrorField } from './types/validation-error-field.type.js';
export { ApplicationError } from './types/application-error.enum.js';
export { ValidationExceptionFilter } from './exception-filter/validation.exception-filter.js';
export { HttpErrorExceptionFilter } from './exception-filter/http-error.exception-filter.js';
export { PathTransformer } from './transform/path-transformer.js';
Loading

0 comments on commit ab7be91

Please sign in to comment.