diff --git a/package.json b/package.json index 1ab4226..c1f8936 100644 --- a/package.json +++ b/package.json @@ -12,13 +12,14 @@ "start:cli": "npm run build && node ./dist/main.cli.js", "start:rest": "nodemon -L", "build": "npm run clean && npm run compile", - "cli:help": "npx tsx src/main.cli.ts --help", - "cli:version": "npx tsx src/main.cli.ts --version", - "cli:import": "npx tsx src/main.cli.ts --import mocks/mock-data.generated.tsv mongodb://admin:test@127.0.0.1:27017/six-cities?authSource=admin salt", - "cli:import:docker": "npx tsx src/main.cli.ts --import mocks/mock-data.generated.tsv mongodb://admin:test@db:27017/six-cities?authSource=admin salt", - "cli:generate": "npx tsx src/main.cli.ts --generate 10 mocks/mock-data.generated.tsv http://localhost:3000/api", + "cli:help": "npm run ts ./src/main.cli.ts -- --help", + "cli:version": "npm run ts ./src/main.cli.ts --version", + "cli:import": "npm run ts ./src/main.cli.ts --import mocks/mock-data.generated.tsv mongodb://admin:admin@127.0.0.1:27017/six-cities?authSource=admin salt", + "cli:import:docker": "npm run ts ./src/main.cli.ts --import mocks/mock-data.generated.tsv mongodb://admin:admin@db:27017/six-cities?authSource=admin salt", + "cli:generate": "npm run ts ./src/main.cli.ts --generate 10 mocks/mock-data.generated.tsv http://localhost:3000/api", + "docker": "docker compose --file ./docker-compose.yml --env-file ./.env --project-name \"six-cities\" up -d", "mock:server": "json-server ./mocks/mock-server-data.json --port 3000", - "lint": "eslint src/ --ext .ts", + "lint": "eslint ./src/ --ext .ts", "lint:fix": "npm run lint -- --fix", "prettier:fix": "prettier --write ./src", "compile": "tsc -p tsconfig.json", diff --git a/specification/specification.yml b/specification/specification.yml index 7350b5f..451ce63 100644 --- a/specification/specification.yml +++ b/specification/specification.yml @@ -1,4 +1,4 @@ -openapi: 3.1.0 +openapi: 3.0.3 info: title: API-сервер для демо-проекта «Шесть городов» @@ -6,17 +6,16 @@ info: license: name: MIT url: https://opensource.org/licenses/MIT - version: 2.0.0 + version: 1.0.0 tags: - name: offers - description: Действия с объявлениями. - - name: categories - description: Действия с категориями. + description: Действия с предложениями по аренде. - name: comments description: Действия с комментариями. - name: users description: Действия с пользователем. + paths: /users/register: post: @@ -30,18 +29,18 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/createUser" + $ref: '#/components/schemas/createUser' required: true responses: - "201": + '201': description: Пользователь зарегистрирован. Объект пользователя. content: application/json: schema: - $ref: "#/components/schemas/user" + $ref: '#/components/schemas/user' - "409": + '409': description: Пользователь с таким email уже существует. /users/login: @@ -56,18 +55,18 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/login" + $ref: '#/components/schemas/login' required: true responses: - "200": + '200': description: Пользователь аутентифицировался. Тело ответа содержит токен content: application/json: schema: - $ref: "#/components/schemas/token" + $ref: '#/components/schemas/token' - "401": + '401': description: Неверный email или пароль. get: @@ -77,25 +76,25 @@ paths: description: Возвращает информацию по авторизованному пользователю responses: - "200": + '200': description: Пользователь аутентифицирован. Тело ответа содержит информацию о пользователе. content: application/json: schema: - $ref: "#/components/schemas/user" + $ref: '#/components/schemas/user' - "401": + '401': description: Пользователь не аутентифицирован. /users/logout: get: tags: - users - summary: Логаут пользователя + summary: Деавторизация пользователя description: Реализует выход пользователя из приложения responses: - "204": + '204': description: Пользователь вышел из приложения. /users/{userId}/avatar: @@ -117,7 +116,7 @@ paths: format: binary responses: - "201": + '201': description: Аватар пользователя был загружен. /offers/add: @@ -132,18 +131,18 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/createOffer" + $ref: '#/components/schemas/createOffer' required: true responses: - "201": + '201': description: Предложение об аренде создано. В теле ответа информация о новом предложении. content: application/json: schema: - $ref: "#/components/schemas/offer" + $ref: '#/components/schemas/offer' - "400": + '400': description: Отправлены некорректные данные. components: @@ -182,7 +181,7 @@ components: name: type: string - example: "Keks" + example: 'Keks' login: type: object @@ -215,60 +214,50 @@ components: - preview - photos - isPremium - - housingType - - roomsNumber - - guestsNumber + - type + - roomsCount + - guestsCount - price - - facilities - - latitude - - longitude + - comforts + - coordinates properties: title: type: string - example: "Hotel The Westin Seattle" + example: 'Hotel California' minLength: 10 maxLength: 100 description: type: string - example: "Near Pike Place Market" + example: 'Such a lovely place' minLength: 20 maxLength: 1024 city: - $ref: "#/components/schemas/cities" + $ref: '#/components/schemas/cities' preview: type: string example: hotel.jpg photos: - type: array - items: - type: string - example: - - photo1.jpg - - photo2.jpg - - photo3.jpg - - photo4.jpg - - photo5.jpg - - photo6.jpg + $ref: '#/components/schemas/photos' isPremium: type: boolean example: true - housingType: - $ref: "#/components/schemas/housingType" + type: + $ref: '#/components/schemas/type' - roomsNumber: + roomsCount: type: integer example: 4 minimum: 1 maximum: 8 - guestsNumber: + guestsCount: type: integer example: 4 minimum: 1 @@ -280,8 +269,11 @@ components: minimum: 100 maximum: 100000 - facilities: - $ref: "#/components/schemas/facilities" + coordinates: + $ref: '#/components/schemas/coordinates' + + comforts: + $ref: '#/components/schemas/comforts' offer: type: object @@ -295,13 +287,12 @@ components: - isPremium - isFavorite - rating - - housingType - - roomsNumber - - guestsNumber + - type + - roomsCount + - guestsCount - price - - facilities - - latitude - - longitude + - comforts + - coordinates - author - commentsCount @@ -312,34 +303,25 @@ components: title: type: string - example: "The Westin Seattle" + example: 'Hotel California' description: type: string - example: "Near Pike Place Market" + example: 'Such a lovely place' createdAt: type: string - example: "2022-04-06T08:45:40.283Z" + example: '2022-04-06T08:45:40.283Z' city: - $ref: "#/components/schemas/cities" + $ref: '#/components/schemas/cities' preview: type: string example: hotel.jpg photos: - type: array - items: - type: string - example: - - photo1.jpg - - photo2.jpg - - photo3.jpg - - photo4.jpg - - photo5.jpg - - photo6.jpg + $ref: '#/components/schemas/photos' isPremium: type: boolean @@ -353,14 +335,14 @@ components: type: number example: 4.5 - housingType: - $ref: "#/components/schemas/housingType" + type: + $ref: '#/components/schemas/type' - roomsNumber: + roomsCount: type: integer example: 4 - guestsNumber: + guestsCount: type: integer example: 4 @@ -368,23 +350,18 @@ components: type: integer example: 10000 - facilities: - $ref: "#/components/schemas/facilities" + comforts: + $ref: '#/components/schemas/comforts' author: - $ref: "#/components/schemas/user" + $ref: '#/components/schemas/user' commentsCount: type: integer example: 50 - latitude: - type: number - example: 53.550341 - - longitude: - type: number - example: 10.000654 + coordinates: + $ref: '#/components/schemas/coordinates' cities: type: string @@ -396,7 +373,7 @@ components: - Hamburg - Dusseldorf - housingType: + type: type: string enum: - Apartment @@ -404,7 +381,7 @@ components: - Room - Hotel - facilities: + comforts: type: array items: type: string @@ -416,3 +393,23 @@ components: - Washer - Towels - Fridge + + photos: + type: array + items: + type: string + example: + - photo1.jpg + - photo2.jpg + - photo3.jpg + - photo4.jpg + - photo5.jpg + - photo6.jpg + + coordinates: + type: array + items: + type: string + example: + - 53.550341 # latitude + - 10.000654 # longitude diff --git a/src/cli/commands/help.command.ts b/src/cli/commands/help.command.ts index 90cff15..313f297 100644 --- a/src/cli/commands/help.command.ts +++ b/src/cli/commands/help.command.ts @@ -17,10 +17,7 @@ export class HelpCommand implements Command { } public async execute(_args: string[], commands: CommandInfo[]) { - const list = commands.map((command) => ({ - ...command, - pattern: this.getPattern(command), - })); + const list = commands.map((command) => ({ ...command, pattern: this.getPattern(command) })); this.logger.info( 'Подготовка данных для REST API сервера.\nПример: {main.js -- [--arguments]}' diff --git a/src/cli/utils/index.ts b/src/cli/utils/index.ts index cdc6d3d..09040c9 100644 --- a/src/cli/utils/index.ts +++ b/src/cli/utils/index.ts @@ -1,2 +1 @@ -export * from './cli-logger.js'; export * from './parser.js'; diff --git a/src/main.rest.ts b/src/main.rest.ts index 475914a..4dadf15 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -2,9 +2,10 @@ import 'reflect-metadata'; import { Container } from 'inversify'; import { createRestApplicationContainer } from './rest/rest.container.js'; -import { createCommentContainer } from './shared/modules/comment/index.js'; -import { createOfferContainer } from './shared/modules/offer/index.js'; import { createUserContainer } from './shared/modules/user/index.js'; +import { createOfferContainer } from './shared/modules/offer/index.js'; +import { createCommentContainer } from './shared/modules/comment/index.js'; +import { createCategoryContainer } from './shared/modules/category/index.js'; import { Component } from './shared/types/index.js'; import { RestApplication } from './rest/index.js'; @@ -13,7 +14,8 @@ async function bootstrap() { createRestApplicationContainer(), createUserContainer(), createOfferContainer(), - createCommentContainer() + createCommentContainer(), + createCategoryContainer() ); const application = container.get(Component.RestApplication); diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 3d784bc..a5a1d22 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -9,23 +9,27 @@ import { Component } from '../shared/types/index.js'; @injectable() export class RestApplication { - private readonly server: Express; - constructor( @inject(Component.Logger) private readonly logger: Logger, @inject(Component.Config) private readonly config: Config, @inject(Component.DatabaseClient) - @inject(Component.ExceptionFilter) - private readonly appExceptionFilter: ExceptionFilter, @inject(Component.UserController) - private readonly userController: Controller, @inject(Component.OfferController) + @inject(Component.CommentController) + @inject(Component.CategoryController) + @inject(Component.ExceptionFilter) + private readonly databaseClient: DatabaseClient, + private readonly userController: Controller, private readonly offerController: Controller, - private readonly databaseClient: DatabaseClient + private readonly commentController: Controller, + private readonly categoryController: Controller, + private readonly appExceptionFilter: ExceptionFilter ) { this.server = express(); } + private readonly server: Express; + private initServer() { this.logger.info('Инициализация сервера…'); @@ -39,6 +43,8 @@ export class RestApplication { this.logger.info('Инициализация контроллеров'); this.server.use('/users', this.userController.router); this.server.use('/offers', this.offerController.router); + this.server.use('/comment', this.commentController.router); + this.server.use('/category', this.categoryController.router); this.logger.info('Инициализация контроллеров завершена'); } diff --git a/src/rest/rest.container.ts b/src/rest/rest.container.ts index 65b630f..c1eeb2a 100644 --- a/src/rest/rest.container.ts +++ b/src/rest/rest.container.ts @@ -2,6 +2,7 @@ import { Container } from 'inversify'; import { RestApplication } from './rest.application.js'; import { Logger, PinoLogger } from '../shared/libs/logger/index.js'; import { Config, RestConfig, RestSchema } from '../shared/libs/config/index.js'; +import { AppExceptionFilter, ExceptionFilter } from '../shared/libs/rest/index.js'; import { DatabaseClient, MongoDatabaseClient } from '../shared/libs/database-client/index.js'; import { Component } from '../shared/types/index.js'; @@ -12,15 +13,23 @@ export function createRestApplicationContainer() { .bind(Component.RestApplication) .to(RestApplication) .inSingletonScope(); + restApplicationContainer.bind(Component.Logger).to(PinoLogger).inSingletonScope(); + restApplicationContainer .bind>(Component.Config) .to(RestConfig) .inSingletonScope(); + restApplicationContainer .bind(Component.DatabaseClient) .to(MongoDatabaseClient) .inSingletonScope(); + restApplicationContainer + .bind(Component.ExceptionFilter) + .to(AppExceptionFilter) + .inSingletonScope(); + return restApplicationContainer; } diff --git a/src/shared/helpers/common.ts b/src/shared/helpers/common.ts index 6e0d368..ddbb9ec 100644 --- a/src/shared/helpers/common.ts +++ b/src/shared/helpers/common.ts @@ -1,3 +1,4 @@ +import { ClassConstructor, plainToInstance } from 'class-transformer'; import dayjs from 'dayjs'; import { DELIMITER, @@ -5,7 +6,6 @@ import { FIRST_WEEK_DAY, StringBooleanValue, } from '../../constants/index.js'; -import { ClassConstructor, plainToInstance } from "class-transformer"; export function generateRandomNumber(min: number, max: number, fractionDigits = 0) { return +(Math.random() * (max - min) + min).toFixed(fractionDigits); diff --git a/src/shared/libs/config/rest.config.ts b/src/shared/libs/config/rest.config.ts index 566afe5..1fc944e 100644 --- a/src/shared/libs/config/rest.config.ts +++ b/src/shared/libs/config/rest.config.ts @@ -14,14 +14,14 @@ export class RestConfig implements Config { const parsedOutput = config(); if (parsedOutput.error) { - throw new Error("Can't read .env file. Perhaps the file does not exists."); + throw new Error('Файл «.env» — не найден.'); } configRestSchema.load({}); configRestSchema.validate({ allowed: 'strict', output: this.logger.info }); this.config = configRestSchema.getProperties(); - this.logger.info('.env file found and successfully parsed!'); + this.logger.info('Файл «.env» — успешно проанализирован!'); } public get(key: T): RestSchema[T] { diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 4108adb..54ee2f7 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -42,7 +42,7 @@ export const configRestSchema = convict({ doc: 'Password to connect to the database', format: String, env: 'DB_PASSWORD', - default: null, + default: '', }, DB_PORT: { doc: 'Port to connect to the database', diff --git a/src/shared/libs/logger/console-logger.ts b/src/shared/libs/logger/console-logger.ts index a7d05fe..0668321 100644 --- a/src/shared/libs/logger/console-logger.ts +++ b/src/shared/libs/logger/console-logger.ts @@ -39,12 +39,7 @@ export class ConsoleLogger implements Logger { const resultMessage = error instanceof Error ? error.message : error; console.error( - chalk.red( - ConsoleLogger.stylizeMessage( - resultMessage || DEFAULT_ERROR, - 'whiteBright' - ) - ), + chalk.red(ConsoleLogger.stylizeMessage(resultMessage || DEFAULT_ERROR, 'whiteBright')), ...attrs ); } diff --git a/src/shared/libs/rest/controller/base-controller.abstract.ts b/src/shared/libs/rest/controller/base-controller.abstract.ts index 2f96468..56f4c01 100644 --- a/src/shared/libs/rest/controller/base-controller.abstract.ts +++ b/src/shared/libs/rest/controller/base-controller.abstract.ts @@ -2,7 +2,7 @@ import { injectable } from 'inversify'; import { Controller } from './controller.interface.js'; import { Response, Router } from 'express'; import { Logger } from '../../logger/index.js'; -import { Route } from '../types/route.interface.js'; +import { Route } from '../types/index.js'; import { StatusCodes } from 'http-status-codes'; import aAsyncHandler from 'express-async-handler'; diff --git a/src/shared/libs/rest/controller/controller.interface.ts b/src/shared/libs/rest/controller/controller.interface.ts index 7f94b68..ae72232 100644 --- a/src/shared/libs/rest/controller/controller.interface.ts +++ b/src/shared/libs/rest/controller/controller.interface.ts @@ -1,5 +1,5 @@ import { Response, Router } from 'express'; -import { Route } from '../types/route.interface.js'; +import { Route } from '../types/index.js'; export interface Controller { readonly router: Router; diff --git a/src/shared/modules/category/category.container.ts b/src/shared/modules/category/category.container.ts index 04ab900..35703d6 100644 --- a/src/shared/modules/category/category.container.ts +++ b/src/shared/modules/category/category.container.ts @@ -1,10 +1,12 @@ import { Container } from 'inversify'; import { types } from '@typegoose/typegoose'; -import { Component } from '../../types/index.js'; -import { CategoryService } from './category-service.interface.js'; import { DefaultCategoryService } from './default-category.service.js'; import { CategoryEntity, CategoryModel } from './category.entity.js'; +import { CategoryService } from './category-service.interface.js'; +import { CategoryController } from './category.controller.js'; +import { Controller } from '../../libs/rest/index.js'; +import { Component } from '../../types/index.js'; export function createCategoryContainer() { const categoryContainer = new Container(); @@ -14,5 +16,10 @@ export function createCategoryContainer() { .bind>(Component.CategoryModel) .toConstantValue(CategoryModel); + categoryContainer + .bind(Component.CategoryController) + .to(CategoryController) + .inSingletonScope(); + return categoryContainer; } diff --git a/src/shared/modules/category/category.controller.ts b/src/shared/modules/category/category.controller.ts new file mode 100644 index 0000000..46dbad1 --- /dev/null +++ b/src/shared/modules/category/category.controller.ts @@ -0,0 +1,39 @@ +import { inject, injectable } from 'inversify'; +import { Request, Response } from 'express'; +import { fillDTO } from '../../helpers/common.js'; + +import { BaseController, HttpError, HttpMethod } from '../../libs/rest/index.js'; +import { CategoryService } from './category-service.interface.js'; +import { Logger } from '../../libs/logger/index.js'; +import { StatusCodes } from 'http-status-codes'; +import { Component } from '../../types/index.js'; +import { CategoryRdo } from './rdo/category.rdo.js'; + +@injectable() +export class CategoryController extends BaseController { + constructor( + @inject(Component.Logger) protected readonly logger: Logger, + @inject(Component.CategoryService) + protected readonly categoryService: CategoryService + ) { + super(logger); + + this.logger.info('Регистрация маршрутов для CategoryController…'); + + this.addRoute({ path: '/id/:id', method: HttpMethod.Get, handler: this.findById }); + } + + public async findById({ params }: Request, res: Response) { + const offer = await this.categoryService.findByCategoryId(params.id); + + if (!offer) { + throw new HttpError( + StatusCodes.NOT_FOUND, + `Категория ${params.id} не найдена.`, + 'CategoryService' + ); + } + + this.ok(res, fillDTO(CategoryRdo, offer)); + } +} diff --git a/src/shared/modules/category/rdo/category.rdo.ts b/src/shared/modules/category/rdo/category.rdo.ts new file mode 100644 index 0000000..f183be2 --- /dev/null +++ b/src/shared/modules/category/rdo/category.rdo.ts @@ -0,0 +1,9 @@ +import { Expose } from 'class-transformer'; + +export class CategoryRdo { + @Expose() + id: string; + + @Expose() + name: string; +} diff --git a/src/shared/modules/comment/comment.container.ts b/src/shared/modules/comment/comment.container.ts index 5e91e6b..79b27e9 100644 --- a/src/shared/modules/comment/comment.container.ts +++ b/src/shared/modules/comment/comment.container.ts @@ -5,6 +5,8 @@ import { Component } from '../../types/index.js'; import { CommentService } from './comment-service.interface.js'; import { DefaultCommentService } from './default-comment.service.js'; import { CommentEntity, CommentModel } from './comment.entity.js'; +import { CommentController } from './comment.controller.js'; +import { Controller } from '../../libs/rest/index.js'; export function createCommentContainer() { const commentContainer = new Container(); @@ -18,5 +20,10 @@ export function createCommentContainer() { .bind>(Component.CommentModel) .toConstantValue(CommentModel); + commentContainer + .bind(Component.CommentController) + .to(CommentController) + .inSingletonScope(); + return commentContainer; } diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts new file mode 100644 index 0000000..d6de363 --- /dev/null +++ b/src/shared/modules/comment/comment.controller.ts @@ -0,0 +1,39 @@ +import { inject, injectable } from 'inversify'; +import { Request, Response } from 'express'; +import { fillDTO } from '../../helpers/common.js'; + +import { BaseController, HttpError, HttpMethod } from '../../libs/rest/index.js'; +import { CommentService } from './comment-service.interface.js'; +import { Logger } from '../../libs/logger/index.js'; +import { StatusCodes } from 'http-status-codes'; +import { Component } from '../../types/index.js'; +import { CommentRdo } from './rdo/comment.rdo.js'; + +@injectable() +export class CommentController extends BaseController { + constructor( + @inject(Component.Logger) protected readonly logger: Logger, + @inject(Component.CommentService) + protected readonly commentService: CommentService + ) { + super(logger); + + this.logger.info('Регистрация маршрутов для CommentController…'); + + this.addRoute({ path: '/id/:id', method: HttpMethod.Get, handler: this.findById }); + } + + public async findById({ params }: Request, res: Response) { + const offer = await this.commentService.findByOfferId(params.id); + + if (!offer) { + throw new HttpError( + StatusCodes.NOT_FOUND, + `Комментарии к предложению №${params.id} не найдены.`, + 'CommentService' + ); + } + + this.ok(res, fillDTO(CommentRdo, offer)); + } +} diff --git a/src/shared/modules/comment/default-comment.service.ts b/src/shared/modules/comment/default-comment.service.ts index 5ae6192..9044111 100644 --- a/src/shared/modules/comment/default-comment.service.ts +++ b/src/shared/modules/comment/default-comment.service.ts @@ -2,10 +2,10 @@ import { inject, injectable } from 'inversify'; import { DocumentType, types } from '@typegoose/typegoose'; import { CommentService } from './comment-service.interface.js'; +import { CreateCommentDto } from './dto/create-comment.dto.js'; import { Component, SortType } from '../../types/index.js'; -import { Logger } from '../../libs/logger/index.js'; import { CommentEntity } from './comment.entity.js'; -import { CreateCommentDto } from './dto/create-comment.dto.js'; +import { Logger } from '../../libs/logger/index.js'; const DEFAULT_COMMENT_COUNT = 50; @@ -18,9 +18,9 @@ export class DefaultCommentService implements CommentService { public async create(dto: CreateCommentDto): Promise> { const result = await this.commentModel.create(dto); - this.logger.info(`Новый комментарий создан: ${dto.text}`); - return result; + + return result.populate('author'); } public async findByOfferId(offerId: string): Promise[]> { diff --git a/src/shared/modules/comment/rdo/comment.rdo.ts b/src/shared/modules/comment/rdo/comment.rdo.ts new file mode 100644 index 0000000..a6ff77f --- /dev/null +++ b/src/shared/modules/comment/rdo/comment.rdo.ts @@ -0,0 +1,18 @@ +import { Expose } from 'class-transformer'; + +export class CommentRdo { + @Expose() + id: string; + + @Expose() + text: string; + + @Expose() + rating: number; + + @Expose() + offerId: string; + + @Expose() + userId: string; +} diff --git a/src/shared/modules/offer/index.ts b/src/shared/modules/offer/index.ts index 5b7cedc..4626aa4 100644 --- a/src/shared/modules/offer/index.ts +++ b/src/shared/modules/offer/index.ts @@ -2,4 +2,5 @@ export * from './offer.entity.js'; export * from './dto/create-offer.dto.js'; export * from './offer-service.interface.js'; export * from './default-offer.service.js'; +export * from './offer.controller.js'; export * from './offer.container.js'; diff --git a/src/shared/modules/offer/offer.container.ts b/src/shared/modules/offer/offer.container.ts index 500c62e..90fde8a 100644 --- a/src/shared/modules/offer/offer.container.ts +++ b/src/shared/modules/offer/offer.container.ts @@ -4,6 +4,8 @@ import { Container } from 'inversify'; import { DefaultOfferService } from './default-offer.service.js'; import { OfferEntity, OfferModel } from './offer.entity.js'; import { OfferService } from './offer-service.interface.js'; +import { OfferController } from './offer.controller.js'; +import { Controller } from '../../libs/rest/index.js'; import { Component } from '../../types/index.js'; export function createOfferContainer() { @@ -13,9 +15,12 @@ export function createOfferContainer() { .bind(Component.OfferService) .to(DefaultOfferService) .inSingletonScope(); + offerContainer .bind>(Component.OfferModel) .toConstantValue(OfferModel); + offerContainer.bind(Component.OfferController).to(OfferController).inSingletonScope(); + return offerContainer; } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts new file mode 100644 index 0000000..33815ad --- /dev/null +++ b/src/shared/modules/offer/offer.controller.ts @@ -0,0 +1,41 @@ +import { inject, injectable } from 'inversify'; +import { Request, Response } from 'express'; +import { fillDTO } from '../../helpers/common.js'; + +import { BaseController, HttpError, HttpMethod } from '../../libs/rest/index.js'; +import { OfferService } from './offer-service.interface.js'; +import { Logger } from '../../libs/logger/index.js'; +import { Component } from '../../types/index.js'; +import { StatusCodes } from 'http-status-codes'; +import { OfferRdo } from './rdo/offer.rdo.js'; + +@injectable() +export class OfferController extends BaseController { + constructor( + @inject(Component.Logger) protected readonly logger: Logger, + @inject(Component.OfferService) + protected readonly offerService: OfferService + ) { + super(logger); + + this.logger.info('Регистрация маршрутов для OfferController…'); + + this.addRoute({ path: '/id/:id', method: HttpMethod.Get, handler: this.findById }); + } + + public async findById({ params }: Request, res: Response) { + const id = params.id; + + const offer = await this.offerService.findById(id); + + if (!offer) { + throw new HttpError( + StatusCodes.NOT_FOUND, + `Предложение с идентификатором ${id} не найдено.`, + 'OfferController' + ); + } + + this.ok(res, fillDTO(OfferRdo, offer)); + } +} diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index 961bd9e..769543f 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -4,12 +4,7 @@ import { UserEntity } from '../user/index.js'; export interface OfferEntity extends defaultClasses.Base {} -@modelOptions({ - schemaOptions: { - collection: 'offers', - timestamps: true, - }, -}) +@modelOptions({ schemaOptions: { collection: 'offers', timestamps: true } }) export class OfferEntity extends defaultClasses.TimeStamps { @prop({ trim: true, required: true }) public title: string; @@ -50,7 +45,7 @@ export class OfferEntity extends defaultClasses.TimeStamps { @prop({ ref: UserEntity, required: true }) public author: Ref; - @prop({ required: true }) + @prop({ type: () => Number, required: true }) public coordinates: number[]; } diff --git a/src/shared/modules/offer/offer.http b/src/shared/modules/offer/offer.http new file mode 100644 index 0000000..976ac62 --- /dev/null +++ b/src/shared/modules/offer/offer.http @@ -0,0 +1,5 @@ +# Предложения +## Найти предложение по id + +GET http://localhost:5000/offers/id/xxx HTTP/1.1 +Content-Type: application/json diff --git a/src/shared/modules/offer/rdo/offer.rdo.ts b/src/shared/modules/offer/rdo/offer.rdo.ts new file mode 100644 index 0000000..e635e71 --- /dev/null +++ b/src/shared/modules/offer/rdo/offer.rdo.ts @@ -0,0 +1,55 @@ +import { Expose } from 'class-transformer'; +import { City, OfferComfort, OfferType, User } from '../../../types/index.js'; + +export class OfferRdo { + @Expose() + id: string; + + @Expose() + title: string; + + @Expose() + description: string; + + @Expose() + createdAt?: Date; + + @Expose() + city: City; + + @Expose() + preview: string; + + @Expose() + photos: string[]; + + @Expose() + isPremium: boolean; + + @Expose() + commentsCount: number; + + @Expose() + rating: number; + + @Expose() + type: OfferType; + + @Expose() + roomsCount: number; + + @Expose() + guestsCount: number; + + @Expose() + price: number; + + @Expose() + comforts: OfferComfort[]; + + @Expose() + author: User; + + @Expose() + coordinates: number[]; +} diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 3de9677..1629ca8 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -2,6 +2,7 @@ import { inject, injectable } from 'inversify'; import { StatusCodes } from 'http-status-codes'; import { Response } from 'express'; import { Component } from '../../types/index.js'; +import { fillDTO } from '../../helpers/common.js'; import { BaseController, HttpError, HttpMethod } from '../../libs/rest/index.js'; import { CreateUserRequest } from './create-user-request.type.js'; @@ -9,7 +10,6 @@ import { Config, RestSchema } from '../../libs/config/index.js'; import { LoginUserRequest } from './login-user-request.type.js'; import { UserService } from './user-service.interface.js'; import { Logger } from '../../libs/logger/index.js'; -import { fillDTO } from '../../helpers/common.js'; import { UserRdo } from './rdo/user.rdo.js'; @injectable() @@ -22,7 +22,7 @@ export class UserController extends BaseController { ) { super(logger); - this.logger.info('Регистрация маршрутов…'); + this.logger.info('Регистрация маршрутов для UserController…'); this.addRoute({ path: '/register', method: HttpMethod.Post, handler: this.create }); this.addRoute({ path: '/login', method: HttpMethod.Post, handler: this.login }); diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index 3223f20..3a94ec3 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -6,14 +6,19 @@ export const Component = { UserModel: Symbol.for('UserModel'), UserService: Symbol.for('UserService'), + UserController: Symbol.for('UserController'), + OfferModel: Symbol.for('OfferModel'), OfferService: Symbol.for('OfferService'), + OfferController: Symbol.for('OfferController'), + CommentModel: Symbol.for('CommentModel'), CommentService: Symbol.for('CommentService'), + CommentController: Symbol.for('CommentController'), + CategoryModel: Symbol.for('CategoryModel'), CategoryService: Symbol.for('CategoryService'), + CategoryController: Symbol.for('CategoryController'), - UserController: Symbol.for('UserController'), - OfferController: Symbol.for('OfferController'), ExceptionFilter: Symbol.for('ExceptionFilter'), } as const;