Skip to content
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
49 changes: 46 additions & 3 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 @@ -44,11 +44,13 @@
"npm": ">=8"
},
"dependencies": {
"@types/cors": "^2.8.19",
"chalk": "5.6.2",
"class-transformer": "0.5.1",
"class-validator": "0.15.1",
"convict": "6.2.5",
"convict-format-with-validator": "6.2.0",
"cors": "^2.8.6",
"dayjs": "1.11.19",
"express": "5.2.1",
"express-async-handler": "1.2.0",
Expand Down
11 changes: 0 additions & 11 deletions specification/project.spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,6 @@ paths:
'401':
$ref: '#/components/responses/Unauthorized'

/users/logout:
post:
tags: [users]
summary: Выход пользователя
description: Завершает пользовательскую сессию
responses:
'204':
description: Успешный выход
'401':
$ref: '#/components/responses/Unauthorized'

/users/{userId}/avatar:
post:
tags: [users]
Expand Down
32 changes: 25 additions & 7 deletions src/rest/rest.application.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import cors from 'cors';
import express, { Express } from 'express';
import { inject, injectable } from 'inversify';
import { Logger } from '../shared/libs/logger/index.js';
import { Config, RestSchema } from '../shared/libs/config/index.js';
import { Component } from '../shared/types/index.js';
import { DatabaseClient } from '../shared/libs/database-client/index.js';
import { getMongoURI } from '../shared/helpers/index.js';
import { getFullServerPath, getMongoURI } from '../shared/helpers/index.js';
import {
Controller,
ExceptionFilter,
ParseTokenMiddleware,
STATIC_FILES_ROUTE,
STATIC_UPLOAD_ROUTE,
} from '../shared/libs/rest/index.js';

@injectable()
Expand All @@ -30,6 +33,10 @@ export class RestApplication {
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 @@ -58,15 +65,20 @@ export class RestApplication {
}

private async _initMiddleware() {
const authenticateMiddleware = new ParseTokenMiddleware(
this.config.get('JWT_SECRET'),
);

this.server.use(cors());
this.server.use(express.json());
this.server.use(
'/upload',
STATIC_UPLOAD_ROUTE,
express.static(this.config.get('UPLOAD_DIRECTORY')),
);
this.server.use(
STATIC_FILES_ROUTE,
express.static(this.config.get('STATIC_DIRECTORY_PATH')),
);

const authenticateMiddleware = new ParseTokenMiddleware(
this.config.get('JWT_SECRET'),
);
this.server.use(
authenticateMiddleware.execute.bind(authenticateMiddleware),
);
Expand All @@ -76,6 +88,12 @@ export class RestApplication {
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 Down Expand Up @@ -103,7 +121,7 @@ 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')}`,
`Server started on ${getFullServerPath(this.config.get('HOST'), this.config.get('PORT'))}`,
);
}
}
15 changes: 15 additions & 0 deletions src/rest/rest.container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
import {
AppExceptionFilter,
ExceptionFilter,
HttpErrorExceptionFilter,
ValidationExceptionFilter,
} from '../shared/libs/rest/index.js';
import { PathTransformer } from '../shared/libs/rest/transform/path-transformer.js';

export function createRestApplicationContainer() {
const restApplicationContainer = new Container();
Expand All @@ -35,6 +38,18 @@ export function createRestApplicationContainer() {
.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;
}
26 changes: 24 additions & 2 deletions src/shared/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { ClassConstructor, plainToInstance } from 'class-transformer';
import { ValidationError } from 'class-validator';
import { ApplicationError, ValidationErrorField } from '../libs/rest/index.js';

export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : '';
}

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

Expand All @@ -15,3 +23,17 @@ export function fillDTO<T, V>(someDto: ClassConstructor<T>, plainObject: V) {
excludeExtraneousValues: true,
});
}

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

const DEFAULT_CONTENT_TYPE = 'application/json';

@injectable()
export abstract class BaseController implements Controller {
private readonly _router: Router;

@inject(Component.PathTransformer)
private pathTransformer: PathTransformer;

constructor(protected readonly logger: Logger) {
this._router = Router();
}
Expand All @@ -39,7 +44,11 @@ export abstract class BaseController implements Controller {
}

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

res.type(DEFAULT_CONTENT_TYPE).status(statusCode).json(modifiedData);
}

public created<T>(res: Response, data: T): void {
Expand Down
1 change: 1 addition & 0 deletions src/shared/libs/rest/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './http-error.js';
export * from './validation.error.js';
12 changes: 12 additions & 0 deletions src/shared/libs/rest/errors/validation.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { StatusCodes } from 'http-status-codes';
import { ValidationErrorField } from '../types/validation-error-field.type.js';
import { HttpError } from './http-error.js';

export class ValidationError extends HttpError {
public details: ValidationErrorField[] = [];

constructor(message: string, errors: ValidationErrorField[]) {
super(StatusCodes.BAD_REQUEST, message);
this.details = errors;
}
}
Loading
Loading