From ac112b3ef19cb609b5956729d427621a819ae85e Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 18:34:43 +0300 Subject: [PATCH 01/35] renames env example --- .env-example => .env.example | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .env-example => .env.example (100%) diff --git a/.env-example b/.env.example similarity index 100% rename from .env-example rename to .env.example From 44096700aa4fdf284373f10443a43b0ca5ed1354 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 18:40:42 +0300 Subject: [PATCH 02/35] adds cli script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 5ddbee6..9257db0 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "compile": "tsc -p tsconfig.json", "clean": "rimraf dist", "ts": "node --loader ts-node/esm", + "cli": "npm run build && node ./dist/main.cli.js", "mock:server": "json-server ./mocks/mock-server-data.json --port 3123" }, "devDependencies": { From d8de39a89513be334bf033c4796484eddcc24e42 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 18:48:23 +0300 Subject: [PATCH 03/35] changes naming of variables --- src/shared/libs/offer-generator/tsv-offer-generator.ts | 4 ++-- src/shared/modules/favorite/default-favorite.service.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shared/libs/offer-generator/tsv-offer-generator.ts b/src/shared/libs/offer-generator/tsv-offer-generator.ts index 43e96d1..140c916 100644 --- a/src/shared/libs/offer-generator/tsv-offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv-offer-generator.ts @@ -40,7 +40,7 @@ export class TSVOfferGenerator implements OfferGenerator { const email = getRandomItem(this.mockData.emails); const avatarPath = getRandomItem(this.mockData.avatars); const userType = generateRandomBoolean() ? 'pro' : 'base'; - const location = [ + const coordinates = [ generateRandomValue(LATITUDE.min, LATITUDE.max, 6), generateRandomValue(LONGITUDE.min, LONGITUDE.max, 6), ].join(';'); @@ -63,7 +63,7 @@ export class TSVOfferGenerator implements OfferGenerator { email, avatarPath, userType, - location, + coordinates, ].join('\t'); } } diff --git a/src/shared/modules/favorite/default-favorite.service.ts b/src/shared/modules/favorite/default-favorite.service.ts index d6921bb..552e330 100644 --- a/src/shared/modules/favorite/default-favorite.service.ts +++ b/src/shared/modules/favorite/default-favorite.service.ts @@ -63,8 +63,8 @@ export class DefaultFavoriteService implements FavoriteService { }) .lean(); - return favorites.map((fav) => ({ - ...(fav.offerId as OfferEntity), + return favorites.map((favorite) => ({ + ...(favorite.offerId as OfferEntity), isFavorite: true, })); } From 9fdf4cb19ea36fdd991a61cf1ec55e6542c71907 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 18:50:22 +0300 Subject: [PATCH 04/35] removes booleanFromString helper --- src/shared/helpers/offer.ts | 3 +-- src/shared/helpers/types-helpers.ts | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/shared/helpers/offer.ts b/src/shared/helpers/offer.ts index 34410fe..44a6bde 100644 --- a/src/shared/helpers/offer.ts +++ b/src/shared/helpers/offer.ts @@ -5,7 +5,6 @@ import { parseHousing, UserType, } from '../types/index.js'; -import { booleanFromString } from './types-helpers.js'; export function createOffer(offerData: string): Offer { const [ @@ -36,7 +35,7 @@ export function createOffer(offerData: string): Offer { city: City[city as keyof typeof City], previewImage, images: images.split(';'), - isPremium: booleanFromString(isPremium), + isPremium: isPremium.toLowerCase() === 'true', type: parseHousing(type), rooms: parseInt(rooms, 10), guests: parseInt(guests, 10), diff --git a/src/shared/helpers/types-helpers.ts b/src/shared/helpers/types-helpers.ts index a23c556..e8eb02b 100644 --- a/src/shared/helpers/types-helpers.ts +++ b/src/shared/helpers/types-helpers.ts @@ -1,5 +1,3 @@ -export const booleanFromString = (str: string) => str.toLowerCase() === 'true'; - export function createStringUnionGuard(values: T) { const set = new Set(values); From ac03a95906bea820d6cd725535445be4cc412be9 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 19:03:47 +0300 Subject: [PATCH 05/35] renames favorite service methods --- src/shared/modules/favorite/default-favorite.service.ts | 7 ++----- src/shared/modules/favorite/favorite-service.interface.ts | 4 ++-- src/shared/modules/offer/offer.controller.ts | 4 +--- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/shared/modules/favorite/default-favorite.service.ts b/src/shared/modules/favorite/default-favorite.service.ts index 552e330..2f30028 100644 --- a/src/shared/modules/favorite/default-favorite.service.ts +++ b/src/shared/modules/favorite/default-favorite.service.ts @@ -36,10 +36,7 @@ export class DefaultFavoriteService implements FavoriteService { this.logger.info(`All favorites removed for offer ${offerId}`); } - public async getFavoriteStatus( - userId: string, - offerId: string, - ): Promise { + public async getStatus(userId: string, offerId: string): Promise { const result = await this.favoriteModel.exists({ userId, offerId }).exec(); return result !== null; @@ -54,7 +51,7 @@ export class DefaultFavoriteService implements FavoriteService { return favorites.map((item) => item.offerId.toString()); } - public async getFavoriteOffers(userId: string): Promise { + public async getOffers(userId: string): Promise { const favorites = await this.favoriteModel .find({ userId }) .populate({ diff --git a/src/shared/modules/favorite/favorite-service.interface.ts b/src/shared/modules/favorite/favorite-service.interface.ts index 4aa0b54..00bc10c 100644 --- a/src/shared/modules/favorite/favorite-service.interface.ts +++ b/src/shared/modules/favorite/favorite-service.interface.ts @@ -4,8 +4,8 @@ export interface FavoriteService { add(userId: string, offerId: string): Promise; remove(userId: string, offerId: string): Promise; removeByOfferId(offerId: string): Promise; - getFavoriteStatus(userId: string, offerId: string): Promise; + getStatus(userId: string, offerId: string): Promise; getOfferIdsByUser(userId: string): Promise; - getFavoriteOffers(userId: string): Promise; + getOffers(userId: string): Promise; exists(documentId: string): Promise; } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index f7d52d0..e9312fa 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -283,9 +283,7 @@ export class OfferController extends BaseController { { tokenPayload }: Request, res: Response, ): Promise { - const offers = await this.favoriteService.getFavoriteOffers( - tokenPayload.id, - ); + const offers = await this.favoriteService.getOffers(tokenPayload.id); this.ok(res, fillDTO(OfferRdo, offers)); } From 9ff86b01e891013cda5f02228d5ae7955173e42b Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 19:05:16 +0300 Subject: [PATCH 06/35] renames token payload type file --- src/shared/modules/auth/default-auth.service.ts | 2 +- src/shared/modules/auth/index.ts | 2 +- .../auth/types/{TokenPayload.ts => token-payload.type.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/shared/modules/auth/types/{TokenPayload.ts => token-payload.type.ts} (100%) diff --git a/src/shared/modules/auth/default-auth.service.ts b/src/shared/modules/auth/default-auth.service.ts index a5a1cb9..52775eb 100644 --- a/src/shared/modules/auth/default-auth.service.ts +++ b/src/shared/modules/auth/default-auth.service.ts @@ -5,7 +5,7 @@ import { Logger } from '../../libs/logger/index.js'; import { Component } from '../../types/index.js'; import { LoginUserDto, UserEntity, UserService } from '../user/index.js'; import { Config, RestSchema } from '../../libs/config/index.js'; -import { TokenPayload } from './types/TokenPayload.js'; +import { TokenPayload } from './types/token-payload.type.js'; import { SignJWT } from 'jose'; import { JWT_ALGORITHM, JWT_EXPIRED } from './auth.constant.js'; import { diff --git a/src/shared/modules/auth/index.ts b/src/shared/modules/auth/index.ts index ea59cdb..ebd528e 100644 --- a/src/shared/modules/auth/index.ts +++ b/src/shared/modules/auth/index.ts @@ -1,4 +1,4 @@ export * from './auth-service.interface.js'; -export * from './types/TokenPayload.js'; +export * from './types/token-payload.type.js'; export * from './default-auth.service.js'; export * from './auth.container.js'; diff --git a/src/shared/modules/auth/types/TokenPayload.ts b/src/shared/modules/auth/types/token-payload.type.ts similarity index 100% rename from src/shared/modules/auth/types/TokenPayload.ts rename to src/shared/modules/auth/types/token-payload.type.ts From 66236dddcb56aa90e60c003c0070fbaba6f3e32f Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 21:49:12 +0300 Subject: [PATCH 07/35] rollbacks create offer dto --- .../modules/offer/dto/create-offer.dto.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index 0b2eaa1..d2db3f3 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -1,5 +1,6 @@ /* eslint-disable indent */ import { + ArrayMaxSize, ArrayMinSize, IsArray, IsBoolean, @@ -7,6 +8,7 @@ import { IsEnum, IsIn, IsInt, + IsUrl, Max, MaxLength, Min, @@ -46,6 +48,21 @@ export class CreateOfferDTO { @IsEnum(City, { message: CreateOfferValidationMessage.city.invalid }) city: City; + @IsUrl( + {}, + { message: CreateOfferValidationMessage.previewImage.invalidFormat }, + ) + public previewImage?: string; + + @IsArray({ message: CreateOfferValidationMessage.images.invalidFormat }) + @ArrayMinSize(6, { + message: CreateOfferValidationMessage.images.invalidLength, + }) + @ArrayMaxSize(6, { + message: CreateOfferValidationMessage.images.invalidLength, + }) + public images?: string[]; + @IsBoolean({ message: CreateOfferValidationMessage.isPremium.invalidFormat }) isPremium: boolean; From 5d6ae429235f0b5825846322f9c7b92b6c06a352 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 21:49:39 +0300 Subject: [PATCH 08/35] updates workflow --- .env.example | 1 + Workflow.md | 114 ++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 87 insertions(+), 28 deletions(-) diff --git a/.env.example b/.env.example index 5d2969c..7c55d28 100644 --- a/.env.example +++ b/.env.example @@ -10,5 +10,6 @@ DB_PASSWORD=password DB_NAME=six-cities-2 DB_PORT=27017 UPLOAD_DIRECTORY=upload +STATIC_DIRECTORY_PATH=static JWT_SECRET=secret diff --git a/Workflow.md b/Workflow.md index 95bd6e2..2f13850 100644 --- a/Workflow.md +++ b/Workflow.md @@ -10,27 +10,51 @@ npm install Команда запустит процесс установки зависимостей проекта из **npm**. -### Сценарии +## Запуск проекта -В `package.json` предопределено несколько сценариев. +Ниже приведён рекомендуемый порядок локального запуска проекта. -#### Скомпилировать проект +### 1. Установить зависимости + +Если зависимости ещё не установлены, выполните: ```bash -npm run compile +npm install ``` -Создаст директорию `dist` и скомпилирует проект. +### 2. Подготовить файл `.env` + +Приложение читает настройки из файла `.env`. Создайте его в корне проекта и укажите основные переменные окружения: + +```env +PORT=3000 +SALT=iamsalt +HOST=27.0.0.1 +DB_HOST=localhost +DB_USER=admin +DB_PASSWORD=test +DB_PORT=27017 +DB_NAME=six-cities-2 +UPLOAD_DIRECTORY=upload +STATIC_DIRECTORY_PATH=static +JWT_SECRET=secret +``` -#### Удалить скомпилированный проект +Назначение переменных: -```bash -npm run clean -``` +- `PORT` - порт, на котором будет запущен REST API. +- `SALT` - соль для хеширования паролей. +- `HOST` - хост приложения. +- `DB_HOST`, `DB_USER`, `DB_PASSWORD`, `DB_PORT`, `DB_NAME` - параметры подключения к MongoDB. +- `UPLOAD_DIRECTORY` - директория для загружаемых файлов. +- `STATIC_DIRECTORY_PATH` - директория со статическими файлами. +- `JWT_SECRET` - секрет для подписи JWT. + +### 3. Запустить MongoDB -Удаляет директорию `dist`. Используется перед компиляцией. +Перед стартом приложения убедитесь, что сервер MongoDB запущен и доступен по параметрам из `.env`. Если база не будет доступна, REST API не сможет завершить инициализацию. -#### Собрать проект +### 4. Собрать проект ```bash npm run build @@ -38,41 +62,75 @@ npm run build Выполняет сборку проекта: удаляет ранее скомпилированный проект и компилирует заново. -#### Проверить линтером +### 5. Запустить REST API + +Для обычного запуска используйте: ```bash -npm run lint +npm start ``` -Запуск проверки проекта статическим анализатором кода **ESLint**. +Команда выполнит сборку и запустит сервер из `dist/main.rest.js`. -Линтер проверяет файлы только внутри директории `src`. +### 6. Запустить проект в режиме разработки -**Обратите внимание**, при запуске данной команды, ошибки выводятся в терминал. - -#### Запустить ts-модуль без компиляции +Во время разработки удобнее использовать: ```bash -npm run ts -- <Путь к модулю с ts-кодом> +npm run start:dev ``` -Пакет `ts-node` позволяет выполнить TS-код в Node.js без предварительной компиляции. Используется только на этапе разработки. +Этот сценарий также пересобирает проект перед запуском, а затем поднимает сервер через `nodemon` и выводит форматированные логи через `pino-pretty`. + +### 7. При необходимости поднять мок-сервер -#### Запустить проект +Если нужен локальный источник тестовых данных, выполните: ```bash -npm start +npm run mock:server ``` -В процессе запуска проекта будет выполнен процесс «Сборки проекта» и запуска результирующего кода. +После этого `json-server` поднимет мок-сервер на порту `3123`. -#### Запустить моковый сервер +## Сценарии -```bash -npm run mock:server -``` +В `package.json` предопределены следующие сценарии: + +### `npm start` + +Собирает проект и запускает REST API из `dist/main.rest.js`. Основной сценарий для локального запуска приложения. + +### `npm run start:dev` + +Собирает проект и запускает REST API в режиме разработки через `nodemon`. Подходит для разработки, когда важно автоматически перезапускать сервер и видеть читаемые логи. + +### `npm run build` + +Полная сборка проекта: сначала удаляет старую директорию `dist`, затем компилирует TypeScript-код заново. + +### `npm run lint` + +Проверяет код в директории `src` линтером **ESLint**. Используется после изменений и перед сдачей проекта, чтобы убедиться, что код соответствует правилам. + +### `npm run compile` + +Компилирует TypeScript в JavaScript по конфигурации из `tsconfig.json`. Удобно использовать, если очистка `dist` не требуется. + +### `npm run clean` + +Удаляет директорию `dist` с результатом предыдущей сборки. Обычно используется как подготовительный шаг перед новой компиляцией. + +### `npm run ts -- <путь-к-файлу>` + +Запускает TypeScript-файл напрямую через `ts-node` без предварительной сборки. Полезно для отладки отдельных модулей и запуска служебных скриптов во время разработки. + +### `npm run cli -- <аргументы>` + +Собирает проект и запускает CLI-приложение из `dist/main.cli.js`. Используется для работы с консольными командами проекта. + +### `npm run mock:server` -Используется для работы команды `--generate` для генерации тестовых данных. +Запускает `json-server` на порту `3123` с данными из `mocks/mock-server-data.json`. Используется для работы команды `--generate` для генерации тестовых данных. ## Структура проекта From b4678c3d44dfb174713ff3d48f942a7df6e16d85 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 22:33:48 +0300 Subject: [PATCH 09/35] updates import command with rest config --- .env.example | 1 + src/cli/commands/command.constant.ts | 2 -- src/cli/commands/help.command.ts | 8 ++++---- src/cli/commands/import.command.ts | 18 +++++++++++++++--- src/shared/libs/config/rest.schema.ts | 7 +++++++ 5 files changed, 27 insertions(+), 9 deletions(-) delete mode 100644 src/cli/commands/command.constant.ts diff --git a/.env.example b/.env.example index 7c55d28..bfe22c5 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,4 @@ UPLOAD_DIRECTORY=upload STATIC_DIRECTORY_PATH=static JWT_SECRET=secret +DEFAULT_USER_PASSWORD = password diff --git a/src/cli/commands/command.constant.ts b/src/cli/commands/command.constant.ts deleted file mode 100644 index c6c2d83..0000000 --- a/src/cli/commands/command.constant.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DEFAULT_DB_PORT = '27017'; -export const DEFAULT_USER_PASSWORD = 'password'; diff --git a/src/cli/commands/help.command.ts b/src/cli/commands/help.command.ts index 88248e5..8051326 100644 --- a/src/cli/commands/help.command.ts +++ b/src/cli/commands/help.command.ts @@ -12,10 +12,10 @@ export class HelpCommand implements Command { Пример: cli.js -- [--arguments] Команды: - --version: # выводит номер версии - --help: # печатает этот текст - --import : # импортирует данные из TSV - --generate # генерирует произвольное количество тестовых данных + --version: # выводит номер версии + --help: # печатает этот текст + --import : # импортирует данные из TSV + --generate # генерирует произвольное количество тестовых данных `); } } diff --git a/src/cli/commands/import.command.ts b/src/cli/commands/import.command.ts index 719f6cd..8e882fb 100644 --- a/src/cli/commands/import.command.ts +++ b/src/cli/commands/import.command.ts @@ -22,9 +22,13 @@ import { } from '../../shared/libs/database-client/index.js'; import { ConsoleLogger, Logger } from '../../shared/libs/logger/index.js'; import { Offer } from '../../shared/types/index.js'; -import { DEFAULT_DB_PORT, DEFAULT_USER_PASSWORD } from './command.constant.js'; import { CommentModel } from '../../shared/modules/comment/index.js'; import { FavoriteModel } from '../../shared/modules/favorite/index.js'; +import { + Config, + RestConfig, + RestSchema, +} from '../../shared/libs/config/index.js'; export class ImportCommand implements Command { private userService: UserService; @@ -32,6 +36,7 @@ export class ImportCommand implements Command { private databaseClient: DatabaseClient; private logger: Logger; private salt: string; + private config: Config; constructor() { this.onImportedLine = this.onImportedLine.bind(this); @@ -46,6 +51,7 @@ export class ImportCommand implements Command { ); this.userService = new DefaultUserService(this.logger, UserModel); this.databaseClient = new MongoDatabaseClient(this.logger); + this.config = new RestConfig(this.logger); } public getName(): string { @@ -54,7 +60,7 @@ export class ImportCommand implements Command { private async saveOffer(offer: Offer) { const user = await this.userService.findOrCreate( - { ...offer.host, password: DEFAULT_USER_PASSWORD }, + { ...offer.host, password: this.config.get('DEFAULT_USER_PASSWORD') }, this.salt, ); @@ -97,7 +103,13 @@ export class ImportCommand implements Command { ): Promise { this.logger.info('Starting import...'); - const uri = getMongoURI(login, password, host, DEFAULT_DB_PORT, dbname); + const uri = getMongoURI( + login, + password, + host, + this.config.get('DB_PORT'), + dbname, + ); this.salt = salt; await this.databaseClient.connect(uri); diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index 74c52db..afa542b 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -29,6 +29,7 @@ export type RestSchema = { JWT_SECRET: string; HOST: string; STATIC_DIRECTORY_PATH: string; + DEFAULT_USER_PASSWORD: string; }; export const configRestSchema = convict({ @@ -98,4 +99,10 @@ export const configRestSchema = convict({ env: 'STATIC_DIRECTORY_PATH', default: 'static', }, + DEFAULT_USER_PASSWORD: { + doc: 'Default user password for import', + format: String, + env: 'DEFAULT_USER_PASSWORD', + default: 'password', + }, }); From 11b3a0a8234ac760f9ed93026e6fef96c1467afc Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 22:35:30 +0300 Subject: [PATCH 10/35] updates workflow with new env --- Workflow.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Workflow.md b/Workflow.md index 2f13850..b671042 100644 --- a/Workflow.md +++ b/Workflow.md @@ -49,6 +49,7 @@ JWT_SECRET=secret - `UPLOAD_DIRECTORY` - директория для загружаемых файлов. - `STATIC_DIRECTORY_PATH` - директория со статическими файлами. - `JWT_SECRET` - секрет для подписи JWT. +- `DEFAULT_USER_PASSWORD` - пароль по умолчанию для создания новых пользователей при выполнении импорта. ### 3. Запустить MongoDB From a4f1363961c4bcbbd1431d60019affe1c68fb8a5 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 22:39:00 +0300 Subject: [PATCH 11/35] removes unused type guards and helper --- src/shared/helpers/types-helpers.ts | 6 ------ src/shared/types/facility.type.ts | 7 +------ src/shared/types/housing.type.ts | 7 +------ 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/shared/helpers/types-helpers.ts b/src/shared/helpers/types-helpers.ts index e8eb02b..f60ba5e 100644 --- a/src/shared/helpers/types-helpers.ts +++ b/src/shared/helpers/types-helpers.ts @@ -1,9 +1,3 @@ -export function createStringUnionGuard(values: T) { - const set = new Set(values); - - return (value: string): value is T[number] => set.has(value as T[number]); -} - export function createStringUnionParser( values: T, ) { diff --git a/src/shared/types/facility.type.ts b/src/shared/types/facility.type.ts index 353a0ad..3c85456 100644 --- a/src/shared/types/facility.type.ts +++ b/src/shared/types/facility.type.ts @@ -1,7 +1,4 @@ -import { - createStringUnionGuard, - createStringUnionParser, -} from '../helpers/index.js'; +import { createStringUnionParser } from '../helpers/index.js'; export const FacilityValues = [ 'Breakfast', @@ -15,6 +12,4 @@ export const FacilityValues = [ export type Facility = (typeof FacilityValues)[number]; -export const isFacility = createStringUnionGuard(FacilityValues); - export const parseFacility = createStringUnionParser(FacilityValues); diff --git a/src/shared/types/housing.type.ts b/src/shared/types/housing.type.ts index 45cd0fd..4eaac48 100644 --- a/src/shared/types/housing.type.ts +++ b/src/shared/types/housing.type.ts @@ -1,12 +1,7 @@ -import { - createStringUnionGuard, - createStringUnionParser, -} from '../helpers/index.js'; +import { createStringUnionParser } from '../helpers/index.js'; export const HousingValues = ['apartment', 'house', 'room', 'hotel'] as const; export type Housing = (typeof HousingValues)[number]; -export const isHousing = createStringUnionGuard(HousingValues); - export const parseHousing = createStringUnionParser(HousingValues); From ee54c7f0a10006f53bc2028c4653ee2a1b2e6bf6 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 22:43:04 +0300 Subject: [PATCH 12/35] removes unused favorite service methods --- .../modules/favorite/default-favorite.service.ts | 15 --------------- .../favorite/favorite-service.interface.ts | 2 -- 2 files changed, 17 deletions(-) diff --git a/src/shared/modules/favorite/default-favorite.service.ts b/src/shared/modules/favorite/default-favorite.service.ts index 2f30028..3868640 100644 --- a/src/shared/modules/favorite/default-favorite.service.ts +++ b/src/shared/modules/favorite/default-favorite.service.ts @@ -36,21 +36,6 @@ export class DefaultFavoriteService implements FavoriteService { this.logger.info(`All favorites removed for offer ${offerId}`); } - public async getStatus(userId: string, offerId: string): Promise { - const result = await this.favoriteModel.exists({ userId, offerId }).exec(); - - return result !== null; - } - - public async getOfferIdsByUser(userId: string): Promise { - const favorites = await this.favoriteModel - .find({ userId }) - .select('offerId') - .lean(); - - return favorites.map((item) => item.offerId.toString()); - } - public async getOffers(userId: string): Promise { const favorites = await this.favoriteModel .find({ userId }) diff --git a/src/shared/modules/favorite/favorite-service.interface.ts b/src/shared/modules/favorite/favorite-service.interface.ts index 00bc10c..a7bf136 100644 --- a/src/shared/modules/favorite/favorite-service.interface.ts +++ b/src/shared/modules/favorite/favorite-service.interface.ts @@ -4,8 +4,6 @@ export interface FavoriteService { add(userId: string, offerId: string): Promise; remove(userId: string, offerId: string): Promise; removeByOfferId(offerId: string): Promise; - getStatus(userId: string, offerId: string): Promise; - getOfferIdsByUser(userId: string): Promise; getOffers(userId: string): Promise; exists(documentId: string): Promise; } From 457fca8a061f442d3025e13342a6b14bc54fc5e9 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 22:48:52 +0300 Subject: [PATCH 13/35] updates favorite service --- .../modules/favorite/default-favorite.service.ts | 10 ++-------- .../modules/favorite/favorite-service.interface.ts | 4 ++-- src/shared/modules/offer/offer.controller.ts | 7 ++++--- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/shared/modules/favorite/default-favorite.service.ts b/src/shared/modules/favorite/default-favorite.service.ts index 3868640..d353b26 100644 --- a/src/shared/modules/favorite/default-favorite.service.ts +++ b/src/shared/modules/favorite/default-favorite.service.ts @@ -20,20 +20,14 @@ export class DefaultFavoriteService implements FavoriteService { { $setOnInsert: { userId, offerId } }, { upsert: true }, ); - - this.logger.info(`Offer ${offerId} added to favorites for user ${userId}`); } - public async remove(userId: string, offerId: string): Promise { + public async delete(userId: string, offerId: string): Promise { await this.favoriteModel.deleteOne({ userId, offerId }).exec(); - this.logger.info( - `Offer ${offerId} removed from favorites for user ${userId}`, - ); } - public async removeByOfferId(offerId: string): Promise { + public async deleteByOfferId(offerId: string): Promise { await this.favoriteModel.deleteMany({ offerId }).exec(); - this.logger.info(`All favorites removed for offer ${offerId}`); } public async getOffers(userId: string): Promise { diff --git a/src/shared/modules/favorite/favorite-service.interface.ts b/src/shared/modules/favorite/favorite-service.interface.ts index a7bf136..ea03527 100644 --- a/src/shared/modules/favorite/favorite-service.interface.ts +++ b/src/shared/modules/favorite/favorite-service.interface.ts @@ -2,8 +2,8 @@ import { OfferWithFavorite } from '../offer/index.js'; export interface FavoriteService { add(userId: string, offerId: string): Promise; - remove(userId: string, offerId: string): Promise; - removeByOfferId(offerId: string): Promise; + delete(userId: string, offerId: string): Promise; + deleteByOfferId(offerId: string): Promise; getOffers(userId: string): Promise; exists(documentId: string): Promise; } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index e9312fa..3063274 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -125,7 +125,7 @@ export class OfferController extends BaseController { this.addRoute({ path: '/:offerId/favorite', method: HttpMethod.Delete, - handler: this.removeFavorite, + handler: this.deleteFavorite, middlewares: [ new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('offerId'), @@ -209,6 +209,7 @@ export class OfferController extends BaseController { const { offerId } = params; const offer = await this.offerService.deleteById(offerId); await this.commentService.deleteByOfferId(offerId); + await this.favoriteService.deleteByOfferId(offerId); this.noContent(res, offer); } @@ -268,11 +269,11 @@ export class OfferController extends BaseController { this.noContent(res, favorite); } - public async removeFavorite( + public async deleteFavorite( { params, tokenPayload }: Request, res: Response, ): Promise { - const favorite = await this.favoriteService.remove( + const favorite = await this.favoriteService.delete( tokenPayload.id, params.offerId, ); From 39319becf40974a577803fbf1e5972e42ddfb77b Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 22:53:15 +0300 Subject: [PATCH 14/35] removes logger from favorite service --- src/shared/modules/favorite/default-favorite.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/shared/modules/favorite/default-favorite.service.ts b/src/shared/modules/favorite/default-favorite.service.ts index d353b26..c80f2ba 100644 --- a/src/shared/modules/favorite/default-favorite.service.ts +++ b/src/shared/modules/favorite/default-favorite.service.ts @@ -1,7 +1,6 @@ import { inject, injectable } from 'inversify'; import { FavoriteService } from './favorite-service.interface.js'; import { Component } from '../../types/index.js'; -import { Logger } from '../../libs/logger/index.js'; import { types } from '@typegoose/typegoose'; import { FavoriteEntity } from './favorite.entity.js'; import { OfferEntity, OfferWithFavorite } from '../offer/index.js'; @@ -9,7 +8,6 @@ import { OfferEntity, OfferWithFavorite } from '../offer/index.js'; @injectable() export class DefaultFavoriteService implements FavoriteService { constructor( - @inject(Component.Logger) private readonly logger: Logger, @inject(Component.FavoriteModel) private readonly favoriteModel: types.ModelType, ) {} From 3ceb39884085c49a85bd34cb7189915aa1b98d7d Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 23:07:31 +0300 Subject: [PATCH 15/35] refactors tsv file reader for readability --- src/shared/libs/file-reader/tsv-file-reader.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/shared/libs/file-reader/tsv-file-reader.ts b/src/shared/libs/file-reader/tsv-file-reader.ts index 297bf88..a2ffaa6 100644 --- a/src/shared/libs/file-reader/tsv-file-reader.ts +++ b/src/shared/libs/file-reader/tsv-file-reader.ts @@ -22,14 +22,20 @@ export class TSVFileReader extends EventEmitter implements FileReader { for await (const chunk of readStream) { remainingData += chunk.toString(); - while ((nextLinePosition = remainingData.indexOf('\n')) >= 0) { + nextLinePosition = remainingData.indexOf('\n'); + + while (nextLinePosition >= 0) { const completeRow = remainingData.slice(0, nextLinePosition + 1); - remainingData = remainingData.slice(++nextLinePosition); + + remainingData = remainingData.slice(nextLinePosition + 1); + importedRowCount++; await new Promise((resolve) => { this.emit('line', completeRow, resolve); }); + + nextLinePosition = remainingData.indexOf('\n'); } } From 73bb6acf265bcfe3a7a91ed1867e1415c279e226 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 23:26:42 +0300 Subject: [PATCH 16/35] moves register routes from constructor to method in controllers --- .../modules/comment/comment.controller.ts | 26 +- src/shared/modules/offer/offer.controller.ts | 240 +++++++++--------- src/shared/modules/user/user.controller.ts | 69 ++--- 3 files changed, 174 insertions(+), 161 deletions(-) diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index 750442f..a6ed363 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -26,17 +26,7 @@ export default class CommentController extends BaseController { ) { super(logger); - this.logger.info('Register routes for CommentController...'); - this.addRoute({ - path: '/', - method: HttpMethod.Post, - handler: this.create, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateDtoMiddleware(CreateCommentDTO), - new ValidateObjectIdMiddleware('offerId'), - ], - }); + this.registerRoutes(); } public async create( @@ -52,4 +42,18 @@ export default class CommentController extends BaseController { await this.offerService.calcRating(offerId); this.created(res, fillDTO(CommentRdo, comment)); } + + private registerRoutes(): void { + this.logger.info('Register routes for CommentController...'); + this.addRoute({ + path: '/', + method: HttpMethod.Post, + handler: this.create, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateDtoMiddleware(CreateCommentDTO), + new ValidateObjectIdMiddleware('offerId'), + ], + }); + } } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 3063274..6537d81 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -47,124 +47,7 @@ export class OfferController extends BaseController { ) { super(logger); - this.logger.info('Register routes for OfferController...'); - this.addRoute({ - path: '/', - method: HttpMethod.Get, - handler: this.index, - }); - this.addRoute({ - path: '/premium', - method: HttpMethod.Get, - handler: this.getPremiumOffersByCity, - }); - this.addRoute({ - path: '/favorites', - method: HttpMethod.Get, - handler: this.getFavorites, - middlewares: [new PrivateRouteMiddleware()], - }); - this.addRoute({ - path: '/', - method: HttpMethod.Post, - handler: this.create, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateDtoMiddleware(CreateOfferDTO), - ], - }); - this.addRoute({ - path: '/:offerId', - method: HttpMethod.Get, - handler: this.show, - middlewares: [ - new ValidateObjectIdMiddleware('offerId'), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - ], - }); - this.addRoute({ - path: '/:offerId', - method: HttpMethod.Delete, - handler: this.delete, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateObjectIdMiddleware('offerId'), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - ], - }); - this.addRoute({ - path: '/:offerId', - method: HttpMethod.Patch, - handler: this.update, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateObjectIdMiddleware('offerId'), - new ValidateDtoMiddleware(UpdateOfferDTO), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - ], - }); - this.addRoute({ - path: '/:offerId/comments', - method: HttpMethod.Get, - handler: this.getComments, - middlewares: [ - new ValidateObjectIdMiddleware('offerId'), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - ], - }); - this.addRoute({ - path: '/:offerId/favorite', - method: HttpMethod.Post, - handler: this.addFavorite, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateObjectIdMiddleware('offerId'), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - ], - }); - this.addRoute({ - path: '/:offerId/favorite', - method: HttpMethod.Delete, - handler: this.deleteFavorite, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateObjectIdMiddleware('offerId'), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - ], - }); - this.addRoute({ - path: '/:offerId/preview', - method: HttpMethod.Post, - handler: this.uploadPreview, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateObjectIdMiddleware('offerId'), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - new UploadFileMiddleware( - this.configService.get('UPLOAD_DIRECTORY'), - 'preview', - ALLOWED_IMAGES, - MAX_SINGLE_FILE_SIZE, - ), - ], - }); - this.addRoute({ - path: '/:offerId/images', - method: HttpMethod.Post, - handler: this.uploadImages, - middlewares: [ - new PrivateRouteMiddleware(), - new ValidateObjectIdMiddleware('offerId'), - new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), - new UploadFilesMiddleware( - this.configService.get('UPLOAD_DIRECTORY'), - 'images', - ALLOWED_IMAGES, - MAX_SINGLE_FILE_SIZE, - OFFER_IMAGES_LENGTH, - ), - ], - }); + this.registerRoutes(); } public async show( @@ -332,4 +215,125 @@ export class OfferController extends BaseController { const imagesRdo = fillDTO(UploadImagesRdo, updatedOffer); this.created(res, imagesRdo); } + + private registerRoutes(): void { + this.logger.info('Register routes for OfferController...'); + this.addRoute({ + path: '/', + method: HttpMethod.Get, + handler: this.index, + }); + this.addRoute({ + path: '/premium', + method: HttpMethod.Get, + handler: this.getPremiumOffersByCity, + }); + this.addRoute({ + path: '/favorites', + method: HttpMethod.Get, + handler: this.getFavorites, + middlewares: [new PrivateRouteMiddleware()], + }); + this.addRoute({ + path: '/', + method: HttpMethod.Post, + handler: this.create, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateDtoMiddleware(CreateOfferDTO), + ], + }); + this.addRoute({ + path: '/:offerId', + method: HttpMethod.Get, + handler: this.show, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId', + method: HttpMethod.Delete, + handler: this.delete, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId', + method: HttpMethod.Patch, + handler: this.update, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new ValidateDtoMiddleware(UpdateOfferDTO), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId/comments', + method: HttpMethod.Get, + handler: this.getComments, + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId/favorite', + method: HttpMethod.Post, + handler: this.addFavorite, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId/favorite', + method: HttpMethod.Delete, + handler: this.deleteFavorite, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + ], + }); + this.addRoute({ + path: '/:offerId/preview', + method: HttpMethod.Post, + handler: this.uploadPreview, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + new UploadFileMiddleware( + this.configService.get('UPLOAD_DIRECTORY'), + 'preview', + ALLOWED_IMAGES, + MAX_SINGLE_FILE_SIZE, + ), + ], + }); + this.addRoute({ + path: '/:offerId/images', + method: HttpMethod.Post, + handler: this.uploadImages, + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + new UploadFilesMiddleware( + this.configService.get('UPLOAD_DIRECTORY'), + 'images', + ALLOWED_IMAGES, + MAX_SINGLE_FILE_SIZE, + OFFER_IMAGES_LENGTH, + ), + ], + }); + } } diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 4cd416c..34c82b3 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -36,38 +36,8 @@ export class UserController extends BaseController { @inject(Component.AuthService) private readonly authService: AuthService, ) { super(logger); - this.logger.info('Register routes for UserController...'); - this.addRoute({ - path: '/register', - method: HttpMethod.Post, - handler: this.create, - middlewares: [new ValidateDtoMiddleware(CreateUserDto)], - }); - this.addRoute({ - path: '/login', - method: HttpMethod.Post, - handler: this.login, - middlewares: [new ValidateDtoMiddleware(LoginUserDto)], - }); - this.addRoute({ - path: '/login', - method: HttpMethod.Get, - handler: this.checkAuth, - }); - this.addRoute({ - path: '/:userId/avatar', - method: HttpMethod.Post, - handler: this.uploadAvatar, - middlewares: [ - new ValidateObjectIdMiddleware('userId'), - new UploadFileMiddleware( - this.configService.get('UPLOAD_DIRECTORY'), - 'avatar', - ALLOWED_IMAGES, - MAX_SINGLE_FILE_SIZE, - ), - ], - }); + + this.registerRoutes(); } public async create( @@ -124,4 +94,39 @@ export class UserController extends BaseController { this.ok(res, fillDTO(LoggedUserRdo, user)); } + + private registerRoutes(): void { + this.logger.info('Register routes for UserController...'); + this.addRoute({ + path: '/register', + method: HttpMethod.Post, + handler: this.create, + middlewares: [new ValidateDtoMiddleware(CreateUserDto)], + }); + this.addRoute({ + path: '/login', + method: HttpMethod.Post, + handler: this.login, + middlewares: [new ValidateDtoMiddleware(LoginUserDto)], + }); + this.addRoute({ + path: '/login', + method: HttpMethod.Get, + handler: this.checkAuth, + }); + this.addRoute({ + path: '/:userId/avatar', + method: HttpMethod.Post, + handler: this.uploadAvatar, + middlewares: [ + new ValidateObjectIdMiddleware('userId'), + new UploadFileMiddleware( + this.configService.get('UPLOAD_DIRECTORY'), + 'avatar', + ALLOWED_IMAGES, + MAX_SINGLE_FILE_SIZE, + ), + ], + }); + } } From 63c2b03c726b4ad4461283b9ce63c46c98b5d7de Mon Sep 17 00:00:00 2001 From: Adonariz Date: Sun, 24 May 2026 23:45:58 +0300 Subject: [PATCH 17/35] refactors tsv offer generator --- .../offer-generator/tsv-offer-generator.ts | 68 +++++++++++++++---- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/src/shared/libs/offer-generator/tsv-offer-generator.ts b/src/shared/libs/offer-generator/tsv-offer-generator.ts index 140c916..4a64f47 100644 --- a/src/shared/libs/offer-generator/tsv-offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv-offer-generator.ts @@ -8,13 +8,32 @@ import { import { MockServerData } from '../../types/mock-server-data.type.js'; import { OfferGenerator } from './offer-generator.interface.js'; -const PRICE = { min: 100, max: 100000 }; -const GUESTS = { min: 1, max: 10 }; -const ROOMS = { min: 1, max: 8 }; -const FIRST_WEEK_DAY = 1; -const LAST_WEEK_DAY = 7; -const LONGITUDE = { min: 2, max: 11 }; -const LATITUDE = { min: 48, max: 54 }; +const OfferGenerationRange = { + Price: { + min: 100, + max: 100000, + }, + Guests: { + min: 1, + max: 10, + }, + Rooms: { + min: 1, + max: 8, + }, + Longitude: { + min: 2, + max: 11, + }, + Latitude: { + min: 48, + max: 54, + }, + PublishDays: { + min: 1, + max: 7, + }, +} as const; export class TSVOfferGenerator implements OfferGenerator { constructor(private readonly mockData: MockServerData) {} @@ -23,16 +42,31 @@ export class TSVOfferGenerator implements OfferGenerator { const title = getRandomItem(this.mockData.titles); const description = getRandomItem(this.mockData.descriptions); const publishDate = dayjs() - .subtract(generateRandomValue(FIRST_WEEK_DAY, LAST_WEEK_DAY), 'day') + .subtract( + generateRandomValue( + OfferGenerationRange.PublishDays.min, + OfferGenerationRange.PublishDays.max, + ), + 'day', + ) .toISOString(); const city = getRandomItem(this.mockData.cities); const previewImage = getRandomItem(this.mockData.previewImages); const images = getRandomItems(this.mockData.offerImages).join(';'); const isPremium = generateRandomBoolean(); const type = getRandomItem(this.mockData.housingTypes); - const rooms = generateRandomValue(ROOMS.min, ROOMS.max).toString(); - const guests = generateRandomValue(GUESTS.min, GUESTS.max).toString(); - const price = generateRandomValue(PRICE.min, PRICE.max).toString(); + const rooms = generateRandomValue( + OfferGenerationRange.Rooms.min, + OfferGenerationRange.Rooms.max, + ).toString(); + const guests = generateRandomValue( + OfferGenerationRange.Guests.min, + OfferGenerationRange.Rooms.max, + ).toString(); + const price = generateRandomValue( + OfferGenerationRange.Price.min, + OfferGenerationRange.Price.max, + ).toString(); const facilities = getRandomItems(this.mockData.facilities).join( ';', ); @@ -41,8 +75,16 @@ export class TSVOfferGenerator implements OfferGenerator { const avatarPath = getRandomItem(this.mockData.avatars); const userType = generateRandomBoolean() ? 'pro' : 'base'; const coordinates = [ - generateRandomValue(LATITUDE.min, LATITUDE.max, 6), - generateRandomValue(LONGITUDE.min, LONGITUDE.max, 6), + generateRandomValue( + OfferGenerationRange.Latitude.min, + OfferGenerationRange.Latitude.max, + 6, + ), + generateRandomValue( + OfferGenerationRange.Longitude.min, + OfferGenerationRange.Longitude.max, + 6, + ), ].join(';'); return [ From c52c66332816f3b6d1065cf7bbdf489e6afdd359 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 00:29:41 +0300 Subject: [PATCH 18/35] refactors path transformer --- .../libs/rest/transform/path-transformer.ts | 24 +++++++++++++++---- src/shared/modules/offer/offer.controller.ts | 2 ++ src/shared/modules/offer/offers.http | 6 ++--- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/shared/libs/rest/transform/path-transformer.ts b/src/shared/libs/rest/transform/path-transformer.ts index a9d483c..7f32d86 100644 --- a/src/shared/libs/rest/transform/path-transformer.ts +++ b/src/shared/libs/rest/transform/path-transformer.ts @@ -9,7 +9,7 @@ import { import { getFullServerPath } from '../../../helpers/index.js'; import { STATIC_FILES_ROUTE, STATIC_UPLOAD_ROUTE } from '../rest.constant.js'; -function isObject(value: unknown): value is Record { +function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null; } @@ -30,18 +30,33 @@ export class PathTransformer { return STATIC_RESOURCE_FIELDS.includes(property); } - public execute(data: Record): Record { - const stack = [data]; + public execute(data: T): T { + if (!isObject(data)) { + return data; + } + + const stack: Record[] = [data]; + const visited = new WeakSet(); + + visited.add(data); while (stack.length > 0) { const current = stack.pop(); + if (!current) { + continue; + } + for (const key in current) { if (Object.hasOwn(current, key)) { const value = current[key]; if (isObject(value)) { - stack.push(value); + if (!visited.has(value)) { + visited.add(value); + stack.push(value); + } + continue; } @@ -54,6 +69,7 @@ export class PathTransformer { const rootPath = this.hasDefaultImage(value) ? staticPath : uploadPath; + current[key] = `${getFullServerPath(serverHost, serverPort)}${rootPath}/${value}`; } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 6537d81..ed4c018 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -90,9 +90,11 @@ export class OfferController extends BaseController { res: Response, ): Promise { const { offerId } = params; + const offer = await this.offerService.deleteById(offerId); await this.commentService.deleteByOfferId(offerId); await this.favoriteService.deleteByOfferId(offerId); + this.noContent(res, offer); } diff --git a/src/shared/modules/offer/offers.http b/src/shared/modules/offer/offers.http index 0747110..0283172 100644 --- a/src/shared/modules/offer/offers.http +++ b/src/shared/modules/offer/offers.http @@ -6,7 +6,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ ## Получить список объявлений GET http://localhost:3000/offers?limit=10 HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjU2OTg4LCJleHAiOjE3Nzk4Mjk3ODh9.x5fodXNwaoXr9sWf5MrWDOcqPw11srZt8cbTZlW8E8g ### ## Создать новое объявление @@ -52,8 +52,8 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ ### ## Удалить объявление по идентификатору - -DELETE http://localhost:3000/offers/69efaafe88e8c8826882b397 HTTP/1.1 +DELETE http://localhost:3000/offers/6a135a60cf8b052690407eaa HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjU2OTg4LCJleHAiOjE3Nzk4Mjk3ODh9.x5fodXNwaoXr9sWf5MrWDOcqPw11srZt8cbTZlW8E8g ### From 1c3828c2295fea1edc6445fd9805b8ca674fcef5 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 00:43:59 +0300 Subject: [PATCH 19/35] refactors parse token middleware --- .../rest/middleware/parse-token.middleware.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/shared/libs/rest/middleware/parse-token.middleware.ts b/src/shared/libs/rest/middleware/parse-token.middleware.ts index 798aef5..dd81065 100644 --- a/src/shared/libs/rest/middleware/parse-token.middleware.ts +++ b/src/shared/libs/rest/middleware/parse-token.middleware.ts @@ -43,12 +43,18 @@ export class ParseTokenMiddleware implements Middleware { createSecretKey(this.jwtSecret, 'utf-8'), ); - if (isTokenPayload(payload)) { - req.tokenPayload = { ...payload }; - return next(); - } else { - throw new Error('Bad token'); + if (!isTokenPayload(payload)) { + return next( + new HttpError( + StatusCodes.UNAUTHORIZED, + 'Invalid token payload', + 'AuthenticateMiddleware', + ), + ); } + + req.tokenPayload = { ...payload }; + return next(); } catch { return next( new HttpError( From 33915610a61e9f4a47daae84f9be1b9d61ec8d5b Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 01:01:39 +0300 Subject: [PATCH 20/35] adds token check for checkAuth --- src/shared/modules/user/user.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 34c82b3..03c2de0 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -7,6 +7,7 @@ import { HttpError, HttpMethod, MAX_SINGLE_FILE_SIZE, + PrivateRouteMiddleware, UploadFileMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware, @@ -113,6 +114,7 @@ export class UserController extends BaseController { path: '/login', method: HttpMethod.Get, handler: this.checkAuth, + middlewares: [new PrivateRouteMiddleware()], }); this.addRoute({ path: '/:userId/avatar', From 810edc430e1d4873e46929bfb9bb975ab9838a86 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 09:36:37 +0300 Subject: [PATCH 21/35] fixes http exception filter status code --- src/shared/libs/rest/exception-filter/http.exception-filter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shared/libs/rest/exception-filter/http.exception-filter.ts b/src/shared/libs/rest/exception-filter/http.exception-filter.ts index eadf442..cbd25ff 100644 --- a/src/shared/libs/rest/exception-filter/http.exception-filter.ts +++ b/src/shared/libs/rest/exception-filter/http.exception-filter.ts @@ -1,5 +1,4 @@ import { Request, Response, NextFunction } from 'express'; -import { StatusCodes } from 'http-status-codes'; import { inject, injectable } from 'inversify'; import { ExceptionFilter } from './exception-filter.interface.js'; import { Component } from '../../../types/index.js'; @@ -30,7 +29,7 @@ export class HttpErrorExceptionFilter implements ExceptionFilter { ); res - .status(StatusCodes.BAD_REQUEST) + .status(error.httpStatusCode) .json(createErrorObject(ApplicationError.CommonError, error.message)); } } From 24a97e70bb67c3e96704f18419ec7f10f6c8c291 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 10:09:33 +0300 Subject: [PATCH 22/35] refactors adding favorite flag in offer service --- .../modules/offer/default-offer.service.ts | 38 ++++++++++++------- src/shared/modules/offer/offers.http | 8 ++-- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index d40dbf3..f1f1710 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -75,19 +75,15 @@ export class DefaultOfferService implements OfferService { .lean(); if (!userId) { - return offers.map((offer) => ({ ...offer, isFavorite: false })); + return offers.map((offer) => ({ + ...offer, + isFavorite: false, + })); } - const favoriteIds = await this.favoriteModel.distinct('offerId', { - userId, - }); + const favoriteIds = await this.getFavoriteIds(userId); - const set = new Set(favoriteIds.map(String)); - - return offers.map((offer) => ({ - ...offer, - isFavorite: set.has(offer._id.toString()), - })); + return this.addFavoriteFlag(offers, favoriteIds); } public async deleteById( @@ -164,18 +160,32 @@ export class DefaultOfferService implements OfferService { .exec(); if (!userId) { - return premiumOffers.map((offer) => ({ ...offer, isFavorite: false })); + return premiumOffers.map((offer) => ({ + ...offer, + isFavorite: false, + })); } + const favoriteIds = await this.getFavoriteIds(userId); + + return this.addFavoriteFlag(premiumOffers, favoriteIds); + } + + private async getFavoriteIds(userId: string): Promise> { const favoriteIds = await this.favoriteModel.distinct('offerId', { userId, }); - const set = new Set(favoriteIds.map(String)); + return new Set(favoriteIds.map(String)); + } - return premiumOffers.map((offer) => ({ + private addFavoriteFlag( + offers: OfferEntity[], + favoriteIds: Set, + ): OfferWithFavorite[] { + return offers.map((offer) => ({ ...offer, - isFavorite: set.has(offer._id.toString()), + isFavorite: favoriteIds.has(offer._id.toString()), })); } } diff --git a/src/shared/modules/offer/offers.http b/src/shared/modules/offer/offers.http index 0283172..b5814ac 100644 --- a/src/shared/modules/offer/offers.http +++ b/src/shared/modules/offer/offers.http @@ -6,7 +6,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ ## Получить список объявлений GET http://localhost:3000/offers?limit=10 HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjU2OTg4LCJleHAiOjE3Nzk4Mjk3ODh9.x5fodXNwaoXr9sWf5MrWDOcqPw11srZt8cbTZlW8E8g +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjkyODA2LCJleHAiOjE3Nzk4NjU2MDZ9.a5IhWu7p3c_7BAjrtENFqoSkRTBAlKlAlfS7jGiPAF4 ### ## Создать новое объявление @@ -81,8 +81,8 @@ GET http://localhost:3000/offers/premium?city=Paris&limit=10 HTTP/1.1 ## Добавить в избранное -POST http://localhost:3000/offers/69c98315edbb02aef834bfd1/favorite HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE +POST http://localhost:3000/offers/6a135a60cf8b052690407eae/favorite HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjkyMjAyLCJleHAiOjE3Nzk4NjUwMDJ9.4JQGX0uO4A8oYBLiE0i2y8jV_QCcR1fsVOVADKa1OP4 ### @@ -96,6 +96,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ ## Получить избранные объявления GET http://localhost:3000/offers/favorites HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im1pY2hhZWwuYnJvd25AZXhhbXBsZS5jb20iLCJmaXJzdG5hbWUiOiJNaWNoYWVsIiwibGFzdG5hbWUiOiJCcm93biIsImlkIjoiNmExMzVhNjBjZjhiMDUyNjkwNDA3ZTlhIiwiaWF0IjoxNzc5NjkyNjY2LCJleHAiOjE3Nzk4NjU0NjZ9.4kJV6dKYs7NGEZsgTiO9W9e1cuV5YZcoUaf85S3PZ9c ### From 8398390cacf58d77251bfa404c85620f0bcf2284 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 10:33:50 +0300 Subject: [PATCH 23/35] moves offer validation properties to constant --- .../offer-generator/tsv-offer-generator.ts | 48 +++++++++---------- .../modules/offer/dto/create-offer.dto.ts | 43 ++++++++++++----- .../modules/offer/dto/update-offer.dto.ts | 43 ++++++++++++----- src/shared/modules/offer/offer.constant.ts | 26 ++++++++++ 4 files changed, 110 insertions(+), 50 deletions(-) diff --git a/src/shared/libs/offer-generator/tsv-offer-generator.ts b/src/shared/libs/offer-generator/tsv-offer-generator.ts index 4a64f47..f52c417 100644 --- a/src/shared/libs/offer-generator/tsv-offer-generator.ts +++ b/src/shared/libs/offer-generator/tsv-offer-generator.ts @@ -10,28 +10,28 @@ import { OfferGenerator } from './offer-generator.interface.js'; const OfferGenerationRange = { Price: { - min: 100, - max: 100000, + Min: 100, + Max: 100000, }, Guests: { - min: 1, - max: 10, + Min: 1, + Max: 10, }, Rooms: { - min: 1, - max: 8, + Min: 1, + Max: 8, }, Longitude: { - min: 2, - max: 11, + Min: 2, + Max: 11, }, Latitude: { - min: 48, - max: 54, + Min: 48, + Max: 54, }, PublishDays: { - min: 1, - max: 7, + Min: 1, + Max: 7, }, } as const; @@ -44,8 +44,8 @@ export class TSVOfferGenerator implements OfferGenerator { const publishDate = dayjs() .subtract( generateRandomValue( - OfferGenerationRange.PublishDays.min, - OfferGenerationRange.PublishDays.max, + OfferGenerationRange.PublishDays.Min, + OfferGenerationRange.PublishDays.Max, ), 'day', ) @@ -56,16 +56,16 @@ export class TSVOfferGenerator implements OfferGenerator { const isPremium = generateRandomBoolean(); const type = getRandomItem(this.mockData.housingTypes); const rooms = generateRandomValue( - OfferGenerationRange.Rooms.min, - OfferGenerationRange.Rooms.max, + OfferGenerationRange.Rooms.Min, + OfferGenerationRange.Rooms.Max, ).toString(); const guests = generateRandomValue( - OfferGenerationRange.Guests.min, - OfferGenerationRange.Rooms.max, + OfferGenerationRange.Guests.Min, + OfferGenerationRange.Rooms.Max, ).toString(); const price = generateRandomValue( - OfferGenerationRange.Price.min, - OfferGenerationRange.Price.max, + OfferGenerationRange.Price.Min, + OfferGenerationRange.Price.Max, ).toString(); const facilities = getRandomItems(this.mockData.facilities).join( ';', @@ -76,13 +76,13 @@ export class TSVOfferGenerator implements OfferGenerator { const userType = generateRandomBoolean() ? 'pro' : 'base'; const coordinates = [ generateRandomValue( - OfferGenerationRange.Latitude.min, - OfferGenerationRange.Latitude.max, + OfferGenerationRange.Latitude.Min, + OfferGenerationRange.Latitude.Max, 6, ), generateRandomValue( - OfferGenerationRange.Longitude.min, - OfferGenerationRange.Longitude.max, + OfferGenerationRange.Longitude.Min, + OfferGenerationRange.Longitude.Max, 6, ), ].join(';'); diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index d2db3f3..2cd4807 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -25,16 +25,21 @@ import { import { CreateOfferValidationMessage } from './create-offer.messages.js'; import { Type } from 'class-transformer'; import { CoordinatesDto } from './coordinates.dto.js'; +import { OFFER_IMAGES_LENGTH, OfferValidation } from '../offer.constant.js'; export class CreateOfferDTO { - @MinLength(10, { message: CreateOfferValidationMessage.title.minLength }) - @MaxLength(100, { message: CreateOfferValidationMessage.title.maxLength }) + @MinLength(OfferValidation.Title.MinLength, { + message: CreateOfferValidationMessage.title.minLength, + }) + @MaxLength(OfferValidation.Title.MaxLength, { + message: CreateOfferValidationMessage.title.maxLength, + }) title: string; - @MinLength(20, { + @MinLength(OfferValidation.Description.MinLength, { message: CreateOfferValidationMessage.description.minLength, }) - @MaxLength(1024, { + @MaxLength(OfferValidation.Description.MaxLength, { message: CreateOfferValidationMessage.description.maxLength, }) description: string; @@ -55,10 +60,10 @@ export class CreateOfferDTO { public previewImage?: string; @IsArray({ message: CreateOfferValidationMessage.images.invalidFormat }) - @ArrayMinSize(6, { + @ArrayMinSize(OFFER_IMAGES_LENGTH, { message: CreateOfferValidationMessage.images.invalidLength, }) - @ArrayMaxSize(6, { + @ArrayMaxSize(OFFER_IMAGES_LENGTH, { message: CreateOfferValidationMessage.images.invalidLength, }) public images?: string[]; @@ -70,22 +75,34 @@ export class CreateOfferDTO { type: Housing; @IsInt({ message: CreateOfferValidationMessage.rooms.invalidFormat }) - @Min(1, { message: CreateOfferValidationMessage.rooms.minValue }) - @Max(8, { message: CreateOfferValidationMessage.rooms.maxValue }) + @Min(OfferValidation.Rooms.Min, { + message: CreateOfferValidationMessage.rooms.minValue, + }) + @Max(OfferValidation.Rooms.Max, { + message: CreateOfferValidationMessage.rooms.maxValue, + }) rooms: number; @IsInt({ message: CreateOfferValidationMessage.guests.invalidFormat }) - @Min(1, { message: CreateOfferValidationMessage.guests.minValue }) - @Max(10, { message: CreateOfferValidationMessage.guests.maxValue }) + @Min(OfferValidation.Guests.Min, { + message: CreateOfferValidationMessage.guests.minValue, + }) + @Max(OfferValidation.Guests.Max, { + message: CreateOfferValidationMessage.guests.maxValue, + }) guests: number; @IsInt({ message: CreateOfferValidationMessage.price.invalidFormat }) - @Min(100, { message: CreateOfferValidationMessage.price.minValue }) - @Max(100000, { message: CreateOfferValidationMessage.price.maxValue }) + @Min(OfferValidation.Price.Min, { + message: CreateOfferValidationMessage.price.minValue, + }) + @Max(OfferValidation.Price.Max, { + message: CreateOfferValidationMessage.price.maxValue, + }) price: number; @IsArray({ message: CreateOfferValidationMessage.facilities.invalidFormat }) - @ArrayMinSize(1, { + @ArrayMinSize(OfferValidation.Facilities.MinLength, { message: CreateOfferValidationMessage.facilities.invalidLength, }) @IsIn(FacilityValues, { diff --git a/src/shared/modules/offer/dto/update-offer.dto.ts b/src/shared/modules/offer/dto/update-offer.dto.ts index 7453e78..9493ecc 100644 --- a/src/shared/modules/offer/dto/update-offer.dto.ts +++ b/src/shared/modules/offer/dto/update-offer.dto.ts @@ -26,18 +26,23 @@ import { } from '../../../types/index.js'; import { CoordinatesDto } from './coordinates.dto.js'; import { CreateUpdateOfferMessage } from './update-offer.messages.js'; +import { OFFER_IMAGES_LENGTH, OfferValidation } from '../offer.constant.js'; export class UpdateOfferDTO { @IsOptional() - @MinLength(10, { message: CreateUpdateOfferMessage.title.minLength }) - @MaxLength(100, { message: CreateUpdateOfferMessage.title.maxLength }) + @MinLength(OfferValidation.Title.MinLength, { + message: CreateUpdateOfferMessage.title.minLength, + }) + @MaxLength(OfferValidation.Title.MaxLength, { + message: CreateUpdateOfferMessage.title.maxLength, + }) public title?: string; @IsOptional() - @MinLength(20, { + @MinLength(OfferValidation.Description.MinLength, { message: CreateUpdateOfferMessage.description.minLength, }) - @MaxLength(1024, { + @MaxLength(OfferValidation.Description.MaxLength, { message: CreateUpdateOfferMessage.description.maxLength, }) public description?: string; @@ -59,10 +64,10 @@ export class UpdateOfferDTO { @IsOptional() @IsArray({ message: CreateUpdateOfferMessage.images.invalidFormat }) - @ArrayMinSize(6, { + @ArrayMinSize(OFFER_IMAGES_LENGTH, { message: CreateUpdateOfferMessage.images.invalidLength, }) - @ArrayMaxSize(6, { + @ArrayMaxSize(OFFER_IMAGES_LENGTH, { message: CreateUpdateOfferMessage.images.invalidLength, }) public images?: string[]; @@ -77,25 +82,37 @@ export class UpdateOfferDTO { @IsOptional() @IsInt({ message: CreateUpdateOfferMessage.rooms.invalidFormat }) - @Min(1, { message: CreateUpdateOfferMessage.rooms.minValue }) - @Max(8, { message: CreateUpdateOfferMessage.rooms.maxValue }) + @Min(OfferValidation.Rooms.Min, { + message: CreateUpdateOfferMessage.rooms.minValue, + }) + @Max(OfferValidation.Rooms.Max, { + message: CreateUpdateOfferMessage.rooms.maxValue, + }) public rooms?: number; @IsOptional() @IsInt({ message: CreateUpdateOfferMessage.guests.invalidFormat }) - @Min(1, { message: CreateUpdateOfferMessage.guests.minValue }) - @Max(10, { message: CreateUpdateOfferMessage.guests.maxValue }) + @Min(OfferValidation.Guests.Min, { + message: CreateUpdateOfferMessage.guests.minValue, + }) + @Max(OfferValidation.Guests.Max, { + message: CreateUpdateOfferMessage.guests.maxValue, + }) public guests?: number; @IsOptional() @IsInt({ message: CreateUpdateOfferMessage.price.invalidFormat }) - @Min(100, { message: CreateUpdateOfferMessage.price.minValue }) - @Max(100000, { message: CreateUpdateOfferMessage.price.maxValue }) + @Min(OfferValidation.Price.Min, { + message: CreateUpdateOfferMessage.price.minValue, + }) + @Max(OfferValidation.Price.Max, { + message: CreateUpdateOfferMessage.price.maxValue, + }) public price?: number; @IsOptional() @IsArray({ message: CreateUpdateOfferMessage.facilities.invalidFormat }) - @ArrayMinSize(1, { + @ArrayMinSize(OfferValidation.Facilities.MinLength, { message: CreateUpdateOfferMessage.facilities.invalidLength, }) @IsIn(FacilityValues, { diff --git a/src/shared/modules/offer/offer.constant.ts b/src/shared/modules/offer/offer.constant.ts index ddbcd56..01f8ce9 100644 --- a/src/shared/modules/offer/offer.constant.ts +++ b/src/shared/modules/offer/offer.constant.ts @@ -1,3 +1,29 @@ export const DEFAULT_OFFER_COUNT = 60; export const DEFAULT_PREMIUM_OFFER_COUNT = 3; export const OFFER_IMAGES_LENGTH = 6; + +export const OfferValidation = { + Title: { + MinLength: 10, + MaxLength: 100, + }, + Description: { + MinLength: 20, + MaxLength: 1024, + }, + Rooms: { + Min: 1, + Max: 8, + }, + Guests: { + Min: 1, + Max: 10, + }, + Price: { + Min: 100, + Max: 100000, + }, + Facilities: { + MinLength: 1, + }, +} as const; From 528872d1b43b5a97d8cbca3af515dcf6ea8a0edd Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 10:41:11 +0300 Subject: [PATCH 24/35] moves comment validation properties to constant --- src/shared/modules/comment/comment.constant.ts | 10 ++++++++++ src/shared/modules/comment/dto/create-comment.dto.ts | 5 ++++- .../modules/comment/dto/create-comment.messages.ts | 8 +++++--- 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 src/shared/modules/comment/comment.constant.ts diff --git a/src/shared/modules/comment/comment.constant.ts b/src/shared/modules/comment/comment.constant.ts new file mode 100644 index 0000000..36df0a1 --- /dev/null +++ b/src/shared/modules/comment/comment.constant.ts @@ -0,0 +1,10 @@ +export const CommentValidation = { + Text: { + MinLength: 5, + MaxLength: 1024, + }, + Rating: { + Min: 0, + Max: 5, + }, +} as const; diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts index 168a851..4c6c6f3 100644 --- a/src/shared/modules/comment/dto/create-comment.dto.ts +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -1,9 +1,12 @@ import { IsInt, IsMongoId, IsString, Length, Max, Min } from 'class-validator'; import { CreateCommentMessages } from './create-comment.messages.js'; +import { CommentValidation } from '../comment.constant.js'; export class CreateCommentDTO { @IsString({ message: CreateCommentMessages.text.invalidFormat }) - @Length(5, 1024, { message: 'min is 5, max is 1024 ' }) + @Length(CommentValidation.Text.MinLength, CommentValidation.Text.MaxLength, { + message: CreateCommentMessages.text.lengthField, + }) public text: string; @IsInt({ message: CreateCommentMessages.rating.invalidFormat }) diff --git a/src/shared/modules/comment/dto/create-comment.messages.ts b/src/shared/modules/comment/dto/create-comment.messages.ts index c19771a..325105f 100644 --- a/src/shared/modules/comment/dto/create-comment.messages.ts +++ b/src/shared/modules/comment/dto/create-comment.messages.ts @@ -1,7 +1,9 @@ +import { CommentValidation } from '../comment.constant.js'; + export const CreateCommentMessages = { text: { invalidFormat: 'text is required', - lengthField: 'min length is 5, max is 2024', + lengthField: `min length is ${CommentValidation.Text.MinLength}, max is ${CommentValidation.Text.MaxLength}`, }, offerId: { invalidFormat: 'offerId field must be a valid id', @@ -11,7 +13,7 @@ export const CreateCommentMessages = { }, rating: { invalidFormat: 'rating must be an integer', - minValue: 'Minimum rating is 0', - maxValue: 'Maximum rating is 5', + minValue: `Minimum rating is ${CommentValidation.Rating.Min}`, + maxValue: `Maximum rating is ${CommentValidation.Rating.Min}`, }, } as const; From 215943b6262ca20c8ecf1845ca68f3784bd5aefa Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 10:55:44 +0300 Subject: [PATCH 25/35] updates offer dto validation messages --- .../modules/offer/dto/coordinates.dto.ts | 17 ++++++-- .../offer/dto/create-offer.messages.ts | 34 ++++++++-------- .../offer/dto/update-offer.messages.ts | 39 +++++++++---------- src/shared/modules/offer/offer.constant.ts | 10 +++++ 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/shared/modules/offer/dto/coordinates.dto.ts b/src/shared/modules/offer/dto/coordinates.dto.ts index 3c8f1be..66cf5ae 100644 --- a/src/shared/modules/offer/dto/coordinates.dto.ts +++ b/src/shared/modules/offer/dto/coordinates.dto.ts @@ -1,21 +1,30 @@ /* eslint-disable indent */ import { IsNumber, Max, Min } from 'class-validator'; import { CreateOfferValidationMessage } from './create-offer.messages.js'; +import { OfferValidation } from '../offer.constant.js'; export class CoordinatesDto { @IsNumber( {}, { message: CreateOfferValidationMessage.latitude.invalidFormat }, ) - @Min(-90, { message: CreateOfferValidationMessage.latitude.minValue }) - @Max(90, { message: CreateOfferValidationMessage.latitude.maxValue }) + @Min(OfferValidation.Location.Latitude.Min, { + message: CreateOfferValidationMessage.latitude.minValue, + }) + @Max(OfferValidation.Location.Latitude.Max, { + message: CreateOfferValidationMessage.latitude.maxValue, + }) latitude: number; @IsNumber( {}, { message: CreateOfferValidationMessage.longitude.invalidFormat }, ) - @Min(-180, { message: CreateOfferValidationMessage.longitude.minValue }) - @Max(180, { message: CreateOfferValidationMessage.longitude.maxValue }) + @Min(OfferValidation.Location.Longitude.Min, { + message: CreateOfferValidationMessage.longitude.minValue, + }) + @Max(OfferValidation.Location.Longitude.Max, { + message: CreateOfferValidationMessage.longitude.maxValue, + }) longitude: number; } diff --git a/src/shared/modules/offer/dto/create-offer.messages.ts b/src/shared/modules/offer/dto/create-offer.messages.ts index 08306e3..5ed68f0 100644 --- a/src/shared/modules/offer/dto/create-offer.messages.ts +++ b/src/shared/modules/offer/dto/create-offer.messages.ts @@ -1,11 +1,13 @@ +import { OFFER_IMAGES_LENGTH, OfferValidation } from '../offer.constant.js'; + export const CreateOfferValidationMessage = { title: { - minLength: 'Minimum title length must be 10', - maxLength: 'Maximum title length must be 100', + minLength: `Minimum title length must be ${OfferValidation.Title.MinLength}`, + maxLength: `Maximum title length must be ${OfferValidation.Title.MinLength}`, }, description: { - minLength: 'Minimum description length must be 20', - maxLength: 'Maximum description length must be 1024', + minLength: `Minimum description length must be ${OfferValidation.Description.MinLength}`, + maxLength: `Maximum description length must be ${OfferValidation.Description.MinLength}`, }, publishDate: { invalidFormat: 'publishDate must be a valid ISO date', @@ -18,7 +20,7 @@ export const CreateOfferValidationMessage = { }, images: { invalidFormat: 'images must be an array of strings', - invalidLength: 'Exactly 6 images are required', + invalidLength: `Exactly ${OFFER_IMAGES_LENGTH} images are required`, }, isPremium: { invalidFormat: 'isPremium must be a boolean', @@ -28,21 +30,21 @@ export const CreateOfferValidationMessage = { }, rooms: { invalidFormat: 'Rooms must be an integer', - minValue: 'Minimum rooms is 1', - maxValue: 'Maximum rooms is 8', + minValue: `Minimum rooms is ${OfferValidation.Rooms.Min}`, + maxValue: `Maximum rooms is ${OfferValidation.Rooms.Max}`, }, guests: { invalidFormat: 'Guests must be an integer', - minValue: 'Minimum guests is 1', - maxValue: 'Maximum guests is 10', + minValue: `Minimum guests is ${OfferValidation.Guests.Min}`, + maxValue: `Maximum guests is ${OfferValidation.Guests.Min}`, }, price: { invalidFormat: 'Price must be an integer', - minValue: 'Minimum price is 100', - maxValue: 'Maximum price is 100000', + minValue: `Minimum price is ${OfferValidation.Price.Min}`, + maxValue: `Maximum price is ${OfferValidation.Guests.Max}`, }, facilities: { - invalidLength: 'At least 1 item required', + invalidLength: `At least ${OfferValidation.Facilities.MinLength} item required`, invalidFormat: 'facilities must be an array', invalidValue: 'facilities must contain valid Facility values', }, @@ -54,12 +56,12 @@ export const CreateOfferValidationMessage = { }, latitude: { invalidFormat: 'latitude must be a number', - minValue: 'Minimum latitude is -90', - maxValue: 'Maximum latitude is 90', + minValue: `Minimum latitude is ${OfferValidation.Location.Latitude.Min}`, + maxValue: `Maximum latitude is ${OfferValidation.Location.Latitude.Max}`, }, longitude: { invalidFormat: 'longitude must be a number', - minValue: 'Minimum longitude is -180', - maxValue: 'Maximum longitude is 180', + minValue: `Minimum longitude is ${OfferValidation.Location.Longitude.Min}`, + maxValue: `Maximum longitude is ${OfferValidation.Location.Longitude.Max}`, }, } as const; diff --git a/src/shared/modules/offer/dto/update-offer.messages.ts b/src/shared/modules/offer/dto/update-offer.messages.ts index db2ea42..e3627b8 100644 --- a/src/shared/modules/offer/dto/update-offer.messages.ts +++ b/src/shared/modules/offer/dto/update-offer.messages.ts @@ -1,11 +1,13 @@ +import { OFFER_IMAGES_LENGTH, OfferValidation } from '../offer.constant.js'; + export const CreateUpdateOfferMessage = { title: { - minLength: 'Minimum title length must be 10', - maxLength: 'Maximum title length must be 100', + minLength: `Minimum title length must be ${OfferValidation.Title.MinLength}`, + maxLength: `Maximum title length must be ${OfferValidation.Title.MinLength}`, }, description: { - minLength: 'Minimum description length must be 20', - maxLength: 'Maximum description length must be 1024', + minLength: `Minimum description length must be ${OfferValidation.Description.MinLength}`, + maxLength: `Maximum description length must be ${OfferValidation.Description.MinLength}`, }, publishDate: { invalidFormat: 'publishDate must be a valid ISO date', @@ -18,36 +20,31 @@ export const CreateUpdateOfferMessage = { }, images: { invalidFormat: 'images must be an array of strings', - invalidLength: 'Exactly 6 images are required', + invalidLength: `Exactly ${OFFER_IMAGES_LENGTH} images are required`, }, isPremium: { invalidFormat: 'isPremium must be a boolean', }, - rating: { - invalidFormat: 'rating must be a number with max 1 decimal place', - minValue: 'Minimum rating is 0', - maxValue: 'Maximum rating is 5', - }, type: { invalid: 'type must be one of Housing', }, rooms: { invalidFormat: 'Rooms must be an integer', - minValue: 'Minimum rooms is 1', - maxValue: 'Maximum rooms is 8', + minValue: `Minimum rooms is ${OfferValidation.Rooms.Min}`, + maxValue: `Maximum rooms is ${OfferValidation.Rooms.Max}`, }, guests: { invalidFormat: 'Guests must be an integer', - minValue: 'Minimum guests is 1', - maxValue: 'Maximum guests is 10', + minValue: `Minimum guests is ${OfferValidation.Guests.Min}`, + maxValue: `Maximum guests is ${OfferValidation.Guests.Min}`, }, price: { invalidFormat: 'Price must be an integer', - minValue: 'Minimum price is 100', - maxValue: 'Maximum price is 100000', + minValue: `Minimum price is ${OfferValidation.Price.Min}`, + maxValue: `Maximum price is ${OfferValidation.Guests.Max}`, }, facilities: { - invalidLength: 'At least 1 item required', + invalidLength: `At least ${OfferValidation.Facilities.MinLength} item required`, invalidFormat: 'facilities must be an array', invalidValue: 'facilities must contain valid Facility values', }, @@ -56,12 +53,12 @@ export const CreateUpdateOfferMessage = { }, latitude: { invalidFormat: 'latitude must be a number', - minValue: 'Minimum latitude is -90', - maxValue: 'Maximum latitude is 90', + minValue: `Minimum latitude is ${OfferValidation.Location.Latitude.Min}`, + maxValue: `Maximum latitude is ${OfferValidation.Location.Latitude.Max}`, }, longitude: { invalidFormat: 'longitude must be a number', - minValue: 'Minimum longitude is -180', - maxValue: 'Maximum longitude is 180', + minValue: `Minimum longitude is ${OfferValidation.Location.Longitude.Min}`, + maxValue: `Maximum longitude is ${OfferValidation.Location.Longitude.Max}`, }, } as const; diff --git a/src/shared/modules/offer/offer.constant.ts b/src/shared/modules/offer/offer.constant.ts index 01f8ce9..b6bbff4 100644 --- a/src/shared/modules/offer/offer.constant.ts +++ b/src/shared/modules/offer/offer.constant.ts @@ -26,4 +26,14 @@ export const OfferValidation = { Facilities: { MinLength: 1, }, + Location: { + Latitude: { + Min: -90, + Max: 90, + }, + Longitude: { + Min: -180, + Max: 180, + }, + }, } as const; From f38dd6f655254d5bd38a02ffd599af18251f8599 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 11:01:31 +0300 Subject: [PATCH 26/35] moves user validation properties to constant --- .../modules/user/dto/create-user.dto.ts | 19 ++++++++++++++++--- .../modules/user/dto/create-user.messages.ts | 8 +++++--- src/shared/modules/user/user.constant.ts | 14 ++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/shared/modules/user/dto/create-user.dto.ts b/src/shared/modules/user/dto/create-user.dto.ts index 3064d59..9dad6a2 100644 --- a/src/shared/modules/user/dto/create-user.dto.ts +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -1,21 +1,34 @@ import { IsEmail, IsIn, IsString, Length } from 'class-validator'; import { UserType, UserTypeValues } from '../../../types/index.js'; import { CreateUserMessages } from './create-user.messages.js'; +import { UserValidation } from '../user.constant.js'; export class CreateUserDto { @IsEmail({}, { message: CreateUserMessages.email.invalidFormat }) public email: string; @IsString({ message: CreateUserMessages.firstname.invalidFormat }) - @Length(1, 15, { message: CreateUserMessages.firstname.lengthField }) + @Length( + UserValidation.Firstname.MinLength, + UserValidation.Firstname.MaxLength, + { message: CreateUserMessages.firstname.lengthField }, + ) public firstname: string; @IsString({ message: CreateUserMessages.lastname.invalidFormat }) - @Length(1, 15, { message: CreateUserMessages.lastname.lengthField }) + @Length( + UserValidation.Lastname.MinLength, + UserValidation.Lastname.MaxLength, + { message: CreateUserMessages.lastname.lengthField }, + ) public lastname: string; @IsString({ message: CreateUserMessages.password.invalidFormat }) - @Length(6, 12, { message: CreateUserMessages.password.lengthField }) + @Length( + UserValidation.Password.MinLength, + UserValidation.Password.MaxLength, + { message: CreateUserMessages.password.lengthField }, + ) public password: string; @IsIn(UserTypeValues, { message: CreateUserMessages.type.invalid }) diff --git a/src/shared/modules/user/dto/create-user.messages.ts b/src/shared/modules/user/dto/create-user.messages.ts index e28658f..2c788d4 100644 --- a/src/shared/modules/user/dto/create-user.messages.ts +++ b/src/shared/modules/user/dto/create-user.messages.ts @@ -1,18 +1,20 @@ +import { UserValidation } from '../user.constant.js'; + export const CreateUserMessages = { email: { invalidFormat: 'email must be a valid address', }, firstname: { invalidFormat: 'firstname is required', - lengthField: 'min length is 1, max is 15', + lengthField: `min length is ${UserValidation.Firstname.MinLength}, max is ${UserValidation.Firstname.MaxLength}`, }, lastname: { invalidFormat: 'lastname is required', - lengthField: 'min length is 1, max is 15', + lengthField: `min length is ${UserValidation.Lastname.MinLength}, max is ${UserValidation.Lastname.MaxLength}`, }, password: { invalidFormat: 'password is required', - lengthField: 'min length for password is 6, max is 12', + lengthField: `min length for password is ${UserValidation.Password.MinLength}, max is ${UserValidation.Password.MaxLength}`, }, type: { invalid: 'type must be one of UserType', diff --git a/src/shared/modules/user/user.constant.ts b/src/shared/modules/user/user.constant.ts index 29633af..452df10 100644 --- a/src/shared/modules/user/user.constant.ts +++ b/src/shared/modules/user/user.constant.ts @@ -1 +1,15 @@ export const DEFAULT_AVATAR_FILE_NAME = 'default-avatar.jpg'; +export const UserValidation = { + Firstname: { + MinLength: 1, + MaxLength: 15, + }, + Lastname: { + MinLength: 1, + MaxLength: 15, + }, + Password: { + MinLength: 6, + MaxLength: 12, + }, +}; From 5aa40f0450178897ed010fbac36cb37dcf1fa0f8 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 11:12:41 +0300 Subject: [PATCH 27/35] adds city check to findPremiumByCity --- src/shared/modules/offer/default-offer.service.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index f1f1710..74a8c93 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -1,4 +1,5 @@ import { inject, injectable } from 'inversify'; +import { StatusCodes } from 'http-status-codes'; import { OfferService } from './offer-service.interface.js'; import { City, Component, SortType } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; @@ -13,6 +14,7 @@ import { import { CommentEntity } from '../comment/index.js'; import { Types } from 'mongoose'; import { FavoriteEntity } from '../favorite/index.js'; +import { HttpError } from '../../libs/rest/index.js'; @injectable() export class DefaultOfferService implements OfferService { @@ -147,8 +149,12 @@ export class DefaultOfferService implements OfferService { userId?: string, count?: number, ): Promise { - if (!city) { - return []; + if (!Object.values(City).includes(city)) { + throw new HttpError( + StatusCodes.BAD_REQUEST, + 'City is missing or incorrect', + 'OfferController', + ); } const premiumOffers = await this.offerModel From 0f758f6ded2b30f20fb83f0241cfedb3b984b744 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 11:15:32 +0300 Subject: [PATCH 28/35] adds sort to find offers --- src/shared/modules/offer/default-offer.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index 74a8c93..bcd3de7 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -72,6 +72,7 @@ export class DefaultOfferService implements OfferService { ): Promise { const offers = await this.offerModel .find() + .sort({ createdAt: SortType.Down }) .limit(count ?? DEFAULT_OFFER_COUNT) .populate(['userId']) .lean(); From 39f2175acf56007ff26a27c7e303746a7d611c1e Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 18:38:07 +0300 Subject: [PATCH 29/35] adds owner check to offer delete and update --- .../modules/offer/default-offer.service.ts | 22 ++++++++ .../modules/offer/offer-service.interface.ts | 6 ++- src/shared/modules/offer/offer.controller.ts | 51 ++++++++++++++----- src/shared/modules/offer/offers.http | 5 +- 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index bcd3de7..ecca411 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -91,14 +91,20 @@ export class DefaultOfferService implements OfferService { public async deleteById( offerId: string, + userId: string, ): Promise | null> { + await this.checkOwner(offerId, userId); + return this.offerModel.findByIdAndDelete(offerId).exec(); } public async updateById( offerId: string, + userId: string, dto: UpdateOfferDTO, ): Promise | null> { + await this.checkOwner(offerId, userId); + return this.offerModel .findByIdAndUpdate(offerId, dto, { new: true }) .exec(); @@ -195,4 +201,20 @@ export class DefaultOfferService implements OfferService { isFavorite: favoriteIds.has(offer._id.toString()), })); } + + private async checkOwner(offerId: string, userId: string): Promise { + const offer = await this.offerModel.findById(offerId); + + if (!offer) { + throw new HttpError( + StatusCodes.NOT_FOUND, + 'Offer not found', + 'OfferService', + ); + } + + if (offer.userId.toString() !== userId) { + throw new HttpError(StatusCodes.FORBIDDEN, 'Forbidden', 'OfferService'); + } + } } diff --git a/src/shared/modules/offer/offer-service.interface.ts b/src/shared/modules/offer/offer-service.interface.ts index b85a07f..e438520 100644 --- a/src/shared/modules/offer/offer-service.interface.ts +++ b/src/shared/modules/offer/offer-service.interface.ts @@ -8,9 +8,13 @@ export interface OfferService extends DocumentExists { create(dto: CreateOfferDTO): Promise>; findById(offerId: string, userId?: string): Promise; find(count?: number, userId?: string): Promise; - deleteById(offerId: string): Promise | null>; + deleteById( + offerId: string, + userId: string, + ): Promise | null>; updateById( offerId: string, + userId: string, dto: UpdateOfferDTO, ): Promise | null>; incCommentCount(offerId: string): Promise | null>; diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index ed4c018..4680422 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -86,24 +86,39 @@ export class OfferController extends BaseController { } public async delete( - { params }: Request, + { params, tokenPayload }: Request, res: Response, ): Promise { const { offerId } = params; + const currentUserId = tokenPayload?.id; - const offer = await this.offerService.deleteById(offerId); + const deletedOffer = await this.offerService.deleteById( + offerId, + currentUserId, + ); await this.commentService.deleteByOfferId(offerId); await this.favoriteService.deleteByOfferId(offerId); - this.noContent(res, offer); + this.noContent(res, deletedOffer); } public async update( - { body, params }: Request, + { + body, + params, + tokenPayload, + }: Request, res: Response, ): Promise { const { offerId } = params; - const updatedOffer = await this.offerService.updateById(offerId, body); + + const currentUserId = tokenPayload?.id; + + const updatedOffer = await this.offerService.updateById( + offerId, + currentUserId, + body, + ); this.ok(res, fillDTO(OfferRdo, updatedOffer)); } @@ -175,10 +190,11 @@ export class OfferController extends BaseController { } public async uploadPreview( - { params, file }: Request, + { params, file, tokenPayload }: Request, res: Response, ): Promise { const { offerId } = params; + const currentUserId = tokenPayload?.id; if (!file) { throw new HttpError( @@ -188,18 +204,23 @@ export class OfferController extends BaseController { ); } - const updatedOffer = await this.offerService.updateById(offerId, { - previewImage: file.filename, - }); + const updatedOffer = await this.offerService.updateById( + offerId, + currentUserId, + { + previewImage: file.filename, + }, + ); const previewRdo = fillDTO(UploadPreviewRdo, updatedOffer); this.created(res, previewRdo); } public async uploadImages( - { params, files }: Request, + { params, files, tokenPayload }: Request, res: Response, ): Promise { const { offerId } = params; + const currentUserId = tokenPayload?.id; const filesToUpload = Array.isArray(files) ? files : undefined; if (!filesToUpload || filesToUpload.length !== OFFER_IMAGES_LENGTH) { @@ -211,9 +232,13 @@ export class OfferController extends BaseController { } const images = filesToUpload.map((file) => file.filename); - const updatedOffer = await this.offerService.updateById(offerId, { - images, - }); + const updatedOffer = await this.offerService.updateById( + offerId, + currentUserId, + { + images, + }, + ); const imagesRdo = fillDTO(UploadImagesRdo, updatedOffer); this.created(res, imagesRdo); } diff --git a/src/shared/modules/offer/offers.http b/src/shared/modules/offer/offers.http index b5814ac..31a7097 100644 --- a/src/shared/modules/offer/offers.http +++ b/src/shared/modules/offer/offers.http @@ -52,15 +52,16 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ ### ## Удалить объявление по идентификатору -DELETE http://localhost:3000/offers/6a135a60cf8b052690407eaa HTTP/1.1 +DELETE http://localhost:3000/offers/6a13635c0df9cd8bf6105552 HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjU2OTg4LCJleHAiOjE3Nzk4Mjk3ODh9.x5fodXNwaoXr9sWf5MrWDOcqPw11srZt8cbTZlW8E8g ### ## Обновить объявление -PATCH http://localhost:3000/offers/69c98315edbb02aef834bfd1 HTTP/1.1 +PATCH http://localhost:3000/offers/6a13635c0df9cd8bf6105552 HTTP/1.1 Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjU2OTg4LCJleHAiOjE3Nzk4Mjk3ODh9.x5fodXNwaoXr9sWf5MrWDOcqPw11srZt8cbTZlW8E8g { "isPremium": true, From f0226a75dc6aa560af1a6238833159d038f30a53 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 18:38:23 +0300 Subject: [PATCH 30/35] adds owner check to user update --- src/shared/modules/user/default-user.service.ts | 7 +++++++ src/shared/modules/user/user-service.interface.ts | 1 + src/shared/modules/user/user.controller.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/shared/modules/user/default-user.service.ts b/src/shared/modules/user/default-user.service.ts index 7348580..e8a6b5a 100644 --- a/src/shared/modules/user/default-user.service.ts +++ b/src/shared/modules/user/default-user.service.ts @@ -7,6 +7,8 @@ import { Component } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; import { UpdateUserDTO } from './dto/update-user.dto.js'; import { DEFAULT_AVATAR_FILE_NAME } from './user.constant.js'; +import { HttpError } from '../../libs/rest/index.js'; +import { StatusCodes } from 'http-status-codes'; @injectable() export class DefaultUserService implements UserService { @@ -53,8 +55,13 @@ export class DefaultUserService implements UserService { public async updateById( userId: string, + tokenPayloadId: string, dto: UpdateUserDTO, ): Promise | null> { + if (userId !== tokenPayloadId) { + throw new HttpError(StatusCodes.FORBIDDEN, 'Forbidden', 'UserService'); + } + return this.userModel.findByIdAndUpdate(userId, dto, { new: true }).exec(); } } diff --git a/src/shared/modules/user/user-service.interface.ts b/src/shared/modules/user/user-service.interface.ts index 9a7c544..9614de0 100644 --- a/src/shared/modules/user/user-service.interface.ts +++ b/src/shared/modules/user/user-service.interface.ts @@ -12,6 +12,7 @@ export interface UserService { ): Promise>; updateById( userId: string, + tokenPayloadId: string, dto: UpdateUserDTO, ): Promise | null>; } diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index 03c2de0..f87c25b 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -70,12 +70,15 @@ export class UserController extends BaseController { } public async uploadAvatar( - { params, file }: Request, + { params, file, tokenPayload }: Request, res: Response, ) { const { userId } = params; + const currentUserId = tokenPayload?.id; const uploadFile = { avatarPath: file?.filename }; - await this.userService.updateById(userId, uploadFile); + + await this.userService.updateById(userId, currentUserId, uploadFile); + this.created( res, fillDTO(UploadUserAvatarRdo, { filepath: uploadFile.avatarPath }), @@ -121,6 +124,7 @@ export class UserController extends BaseController { method: HttpMethod.Post, handler: this.uploadAvatar, middlewares: [ + new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('userId'), new UploadFileMiddleware( this.configService.get('UPLOAD_DIRECTORY'), From 866f561ed55b541f025864cbe6cd0bcf35db14ad Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 19:37:05 +0300 Subject: [PATCH 31/35] adds comments sort and limit --- .../modules/comment/comment-service.interface.ts | 5 ++++- src/shared/modules/comment/comment.controller.ts | 2 -- src/shared/modules/comment/comment.entity.ts | 2 +- src/shared/modules/comment/comment.http | 9 ++++----- .../modules/comment/default-comment.service.ts | 14 ++++++++++++-- .../modules/comment/dto/create-comment.dto.ts | 8 ++++++-- 6 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/shared/modules/comment/comment-service.interface.ts b/src/shared/modules/comment/comment-service.interface.ts index 782079c..426259a 100644 --- a/src/shared/modules/comment/comment-service.interface.ts +++ b/src/shared/modules/comment/comment-service.interface.ts @@ -4,6 +4,9 @@ import { CommentEntity } from './comment.entity.js'; export interface CommentService { create(dto: CreateCommentDTO): Promise>; - findByOfferId(offerId: string): Promise[]>; + findByOfferId( + offerId: string, + count?: number, + ): Promise[]>; deleteByOfferId(offerId: string): Promise; } diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index a6ed363..0e08179 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -5,7 +5,6 @@ import { HttpMethod, ValidateDtoMiddleware, PrivateRouteMiddleware, - ValidateObjectIdMiddleware, } from '../../libs/rest/index.js'; import { Component } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; @@ -52,7 +51,6 @@ export default class CommentController extends BaseController { middlewares: [ new PrivateRouteMiddleware(), new ValidateDtoMiddleware(CreateCommentDTO), - new ValidateObjectIdMiddleware('offerId'), ], }); } diff --git a/src/shared/modules/comment/comment.entity.ts b/src/shared/modules/comment/comment.entity.ts index 718df10..7f5b2c0 100644 --- a/src/shared/modules/comment/comment.entity.ts +++ b/src/shared/modules/comment/comment.entity.ts @@ -22,7 +22,7 @@ export class CommentEntity extends defaultClasses.TimeStamps { @prop({ trim: true, required: true }) public text: string; - @prop({ required: true, min: 1, max: 5, default: 0 }) + @prop({ required: true, default: 0 }) public rating: number; @prop({ ref: OfferEntity, required: true }) diff --git a/src/shared/modules/comment/comment.http b/src/shared/modules/comment/comment.http index 4755fc8..7e749d0 100644 --- a/src/shared/modules/comment/comment.http +++ b/src/shared/modules/comment/comment.http @@ -3,13 +3,12 @@ POST http://localhost:3000/comments HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3NTc1NjIzLCJleHAiOjE3Nzc3NDg0MjN9.CDkRYtP0vK_gcJ5CRZwEDj9FVqpQVNt0S0qoHgMPnUM +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzI1Nzg3LCJleHAiOjE3Nzk4OTg1ODd9.hFl1pYW4ARmBGxkFB4JuxP48xWCg4GI80RUqDCbyX_w { - "text": "Грязища, фу!", - "rating": 1, - "offerId": "69c98315edbb02aef834bfd1", - "userId": "69c982e424f79692fbaad886" + "text": "Great 2!", + "rating": 4, + "offerId": "6a1475f5e8df7c0412530a85" } ### diff --git a/src/shared/modules/comment/default-comment.service.ts b/src/shared/modules/comment/default-comment.service.ts index 514eff5..913800f 100644 --- a/src/shared/modules/comment/default-comment.service.ts +++ b/src/shared/modules/comment/default-comment.service.ts @@ -1,6 +1,6 @@ import { inject, injectable } from 'inversify'; import { CommentService } from './comment-service.interface.js'; -import { Component } from '../../types/index.js'; +import { Component, SortType } from '../../types/index.js'; import { DocumentType, types } from '@typegoose/typegoose'; import { CommentEntity } from './comment.entity.js'; import { CreateCommentDTO } from './dto/create-comment.dto.js'; @@ -21,8 +21,18 @@ export class DefaultCommentService implements CommentService { public async findByOfferId( offerId: string, + count?: number, ): Promise[]> { - return this.commentModel.find({ offerId }).populate('userId'); + const query = this.commentModel + .find({ offerId }) + .sort({ createdAt: SortType.Down }) + .populate('userId'); + + if (count) { + query.limit(count); + } + + return query.exec(); } public async deleteByOfferId(offerId: string): Promise { diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts index 4c6c6f3..76bfeb9 100644 --- a/src/shared/modules/comment/dto/create-comment.dto.ts +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -10,8 +10,12 @@ export class CreateCommentDTO { public text: string; @IsInt({ message: CreateCommentMessages.rating.invalidFormat }) - @Min(0, { message: CreateCommentMessages.rating.minValue }) - @Max(5, { message: CreateCommentMessages.rating.maxValue }) + @Min(CommentValidation.Rating.Min, { + message: CreateCommentMessages.rating.minValue, + }) + @Max(CommentValidation.Rating.Max, { + message: CreateCommentMessages.rating.maxValue, + }) public rating: number; @IsMongoId({ message: CreateCommentMessages.offerId.invalidFormat }) From a7257d487ff3fd86d63413cd4ffd671c3a96794f Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 19:37:40 +0300 Subject: [PATCH 32/35] fixes comments count in offers response --- src/shared/modules/offer/default-offer.service.ts | 2 +- src/shared/modules/offer/offer.constant.ts | 1 + src/shared/modules/offer/offer.controller.ts | 10 ++++++++-- src/shared/modules/offer/offer.entity.ts | 2 +- src/shared/modules/offer/offers.http | 13 ++++++------- src/shared/modules/offer/rdo/offer.rdo.ts | 3 +++ 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/shared/modules/offer/default-offer.service.ts b/src/shared/modules/offer/default-offer.service.ts index ecca411..6982598 100644 --- a/src/shared/modules/offer/default-offer.service.ts +++ b/src/shared/modules/offer/default-offer.service.ts @@ -120,7 +120,7 @@ export class DefaultOfferService implements OfferService { return this.offerModel .findByIdAndUpdate(offerId, { $inc: { - commentCount: 1, + commentsCount: 1, }, }) .exec(); diff --git a/src/shared/modules/offer/offer.constant.ts b/src/shared/modules/offer/offer.constant.ts index b6bbff4..832941d 100644 --- a/src/shared/modules/offer/offer.constant.ts +++ b/src/shared/modules/offer/offer.constant.ts @@ -1,6 +1,7 @@ export const DEFAULT_OFFER_COUNT = 60; export const DEFAULT_PREMIUM_OFFER_COUNT = 3; export const OFFER_IMAGES_LENGTH = 6; +export const DEFAULT_COMMENTS_COUNT = 50; export const OfferValidation = { Title: { diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 4680422..1fe1cb0 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -28,7 +28,10 @@ import { CommentRdo, CommentService } from '../comment/index.js'; import { CreateOfferDTO } from './dto/create-offer.dto.js'; import { FavoriteService } from '../favorite/index.js'; import { Config, RestSchema } from '../../libs/config/index.js'; -import { OFFER_IMAGES_LENGTH } from './offer.constant.js'; +import { + DEFAULT_COMMENTS_COUNT, + OFFER_IMAGES_LENGTH, +} from './offer.constant.js'; import { UploadImagesRdo } from './rdo/upload-images.rdo.js'; import { UploadPreviewRdo } from './rdo/upload-preview.rdo.js'; @@ -127,7 +130,10 @@ export class OfferController extends BaseController { res: Response, ): Promise { const { offerId } = params; - const comments = await this.commentService.findByOfferId(offerId); + const comments = await this.commentService.findByOfferId( + offerId, + DEFAULT_COMMENTS_COUNT, + ); this.ok(res, fillDTO(CommentRdo, comments)); } diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index f460f56..46bfb3c 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -54,7 +54,7 @@ export class OfferEntity extends defaultClasses.TimeStamps { public rating!: number; @prop({ default: 0 }) - public commentCount!: number; + public commentsCount!: number; @prop({ required: true, type: () => String, enum: HousingValues }) public type!: Housing; diff --git a/src/shared/modules/offer/offers.http b/src/shared/modules/offer/offers.http index 31a7097..3f58d0a 100644 --- a/src/shared/modules/offer/offers.http +++ b/src/shared/modules/offer/offers.http @@ -1,6 +1,6 @@ # Получить объявление по идентификатору -GET http://localhost:3000/offers/69c98315edbb02aef834bfd1 HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE +GET http://localhost:3000/offers/6a1475f5e8df7c0412530a85 HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjU2OTg4LCJleHAiOjE3Nzk4Mjk3ODh9.x5fodXNwaoXr9sWf5MrWDOcqPw11srZt8cbTZlW8E8g ### @@ -13,7 +13,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ POST http://localhost:3000/offers HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzI1Nzg3LCJleHAiOjE3Nzk4OTg1ODd9.hFl1pYW4ARmBGxkFB4JuxP48xWCg4GI80RUqDCbyX_w { "title": "Modern apartment in city center", @@ -42,7 +42,6 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ "Towels", "Fridge" ], - "userId": "69ee5f35f61313c1ddf0d82d", "location": { "latitude": 50.846557, "longitude": 4.351697 @@ -59,9 +58,9 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ ## Обновить объявление -PATCH http://localhost:3000/offers/6a13635c0df9cd8bf6105552 HTTP/1.1 +PATCH http://localhost:3000/offers/6a1475f5e8df7c0412530a85 HTTP/1.1 Content-Type: application/json -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjU2OTg4LCJleHAiOjE3Nzk4Mjk3ODh9.x5fodXNwaoXr9sWf5MrWDOcqPw11srZt8cbTZlW8E8g +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzI1Nzg3LCJleHAiOjE3Nzk4OTg1ODd9.hFl1pYW4ARmBGxkFB4JuxP48xWCg4GI80RUqDCbyX_w { "isPremium": true, @@ -71,7 +70,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZ ### ## Список комментариев к объявлению -GET http://localhost:3000/offers/69c98315edbb02aef834bfd1/comments HTTP/1.1 +GET http://localhost:3000/offers/6a1475f5e8df7c0412530a85/comments HTTP/1.1 ### diff --git a/src/shared/modules/offer/rdo/offer.rdo.ts b/src/shared/modules/offer/rdo/offer.rdo.ts index fa611af..aec213a 100644 --- a/src/shared/modules/offer/rdo/offer.rdo.ts +++ b/src/shared/modules/offer/rdo/offer.rdo.ts @@ -38,6 +38,9 @@ export class OfferRdo { @Expose() public rating: number; + @Expose() + public commentsCount: number; + @Expose() public type: Housing; From 3a684d3aa0ce1e080b00e32f7a31989f8b8838eb Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 19:48:48 +0300 Subject: [PATCH 33/35] adds constants to entities --- src/shared/modules/comment/comment.entity.ts | 8 +++++++- src/shared/modules/offer/offer.entity.ts | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/shared/modules/comment/comment.entity.ts b/src/shared/modules/comment/comment.entity.ts index 7f5b2c0..23bfb6c 100644 --- a/src/shared/modules/comment/comment.entity.ts +++ b/src/shared/modules/comment/comment.entity.ts @@ -7,6 +7,7 @@ import { } from '@typegoose/typegoose'; import { OfferEntity } from '../offer/index.js'; import { UserEntity } from '../user/index.js'; +import { CommentValidation } from './comment.constant.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface CommentEntity extends defaultClasses.Base {} @@ -22,7 +23,12 @@ export class CommentEntity extends defaultClasses.TimeStamps { @prop({ trim: true, required: true }) public text: string; - @prop({ required: true, default: 0 }) + @prop({ + required: true, + default: 0, + min: CommentValidation.Rating.Min, + max: CommentValidation.Rating.Max, + }) public rating: number; @prop({ ref: OfferEntity, required: true }) diff --git a/src/shared/modules/offer/offer.entity.ts b/src/shared/modules/offer/offer.entity.ts index 46bfb3c..38b0eff 100644 --- a/src/shared/modules/offer/offer.entity.ts +++ b/src/shared/modules/offer/offer.entity.ts @@ -14,6 +14,7 @@ import { HousingValues, } from '../../types/index.js'; import { UserEntity } from '../user/index.js'; +import { OfferValidation } from './offer.constant.js'; // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface OfferEntity extends defaultClasses.Base {} @@ -59,13 +60,25 @@ export class OfferEntity extends defaultClasses.TimeStamps { @prop({ required: true, type: () => String, enum: HousingValues }) public type!: Housing; - @prop({ required: true, min: 1, max: 8 }) + @prop({ + required: true, + min: OfferValidation.Rooms.Min, + max: OfferValidation.Rooms.Max, + }) public rooms!: number; - @prop({ required: true, min: 1, max: 10 }) + @prop({ + required: true, + min: OfferValidation.Guests.Min, + max: OfferValidation.Guests.Max, + }) public guests!: number; - @prop({ required: true, min: 100, max: 100000 }) + @prop({ + required: true, + min: OfferValidation.Price.Min, + max: OfferValidation.Price.Max, + }) public price!: number; @prop({ required: true, type: () => [String], enum: FacilityValues }) From 06ee93d839ba28871fd0910558ac4a41551dd7b2 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 21:12:07 +0300 Subject: [PATCH 34/35] changes user path specification and comments path specification --- specification/project.spec.yml | 170 +++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 50 deletions(-) diff --git a/specification/project.spec.yml b/specification/project.spec.yml index aa785a4..d650ee5 100644 --- a/specification/project.spec.yml +++ b/specification/project.spec.yml @@ -9,12 +9,12 @@ info: version: 2.0.0 tags: + - name: users + description: Действия с пользователем. - name: offers description: Действия с объявлениями. - name: comments description: Действия с комментариями. - - name: users - description: Действия с пользователем. paths: /users/register: @@ -35,10 +35,10 @@ paths: application/json: schema: $ref: '#/components/schemas/User' + '400': + description: Ошибка валидации '409': description: Пользователь уже существует - '422': - $ref: '#/components/responses/ValidationError' /users/login: post: @@ -57,21 +57,25 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/AuthToken' + $ref: '#/components/schemas/LoggedUser' '401': description: Неверный логин или пароль + '404': + description: Пользователь не найден get: tags: [users] summary: Проверка авторизации description: Возвращает информацию по авторизованному пользователю + security: + - bearerAuth: [] responses: '200': description: Пользователь авторизован content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/LoggedUser' '401': $ref: '#/components/responses/Unauthorized' @@ -79,21 +83,36 @@ paths: post: tags: [users] summary: Загрузка аватара - description: Загружает изображение аватара пользователя. Изображение - аватара должно быть в формате `png` или `jpg`. + description: Загружает jpg/png аватар пользователя + security: + - bearerAuth: [] parameters: - $ref: '#/components/parameters/userId' + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - avatar + properties: + avatar: + type: string + format: binary responses: - '200': - description: Аватар загружен + '201': + description: Аватар успешно загружен content: application/json: schema: - $ref: '#/components/schemas/User' + $ref: '#/components/schemas/UploadUserAvatar' '400': description: Неверный формат файла '401': $ref: '#/components/responses/Unauthorized' + '403': + description: Доступ запрещён /offers: get: @@ -117,6 +136,8 @@ paths: post: tags: [offers] summary: Создать предложение + security: + - bearerAuth: [] requestBody: required: true content: @@ -156,6 +177,8 @@ paths: tags: [offers] summary: Обновить предложение description: Обновляет информацию о предложении + security: + - bearerAuth: [] parameters: - $ref: '#/components/parameters/offerId' requestBody: @@ -181,6 +204,8 @@ paths: delete: tags: [offers] summary: Удалить предложение + security: + - bearerAuth: [] parameters: - $ref: '#/components/parameters/offerId' responses: @@ -211,30 +236,6 @@ paths: items: $ref: '#/components/schemas/Comment' - post: - tags: [comments] - summary: Добавить комментарий - description: Добавляет новый комментарий к предложению - parameters: - - $ref: '#/components/parameters/offerId' - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateComment' - responses: - '201': - description: Комментарий создан - content: - application/json: - schema: - $ref: '#/components/schemas/Comment' - '401': - $ref: '#/components/responses/Unauthorized' - '422': - $ref: '#/components/responses/ValidationError' - /offers/premium: get: tags: [offers] @@ -260,6 +261,8 @@ paths: tags: [offers] summary: Избранные предложения description: Возвращает список предложений, добавленных в избранное + security: + - bearerAuth: [] responses: '200': description: Список избранных предложений @@ -277,6 +280,8 @@ paths: tags: [offers] summary: Добавить предложение в избранное description: Добавляет предложение в список избранных + security: + - bearerAuth: [] parameters: - $ref: '#/components/parameters/offerId' responses: @@ -295,6 +300,8 @@ paths: tags: [offers] summary: Удалить предложение из избранного description: Удаляет предложение из списка избранных + security: + - bearerAuth: [] parameters: - $ref: '#/components/parameters/offerId' responses: @@ -309,7 +316,40 @@ paths: '404': $ref: '#/components/responses/NotFound' + /comments: + post: + tags: [comments] + summary: Добавить комментарий + description: Добавляет новый комментарий к предложению + security: + - bearerAuth: [] + parameters: + - $ref: '#/components/parameters/offerId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComment' + responses: + '201': + description: Комментарий создан + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + parameters: offerId: name: offerId @@ -337,7 +377,7 @@ components: in: query required: true schema: - $ref: '#/components/schemas/CityName' + $ref: '#/components/schemas/City' responses: Unauthorized: @@ -350,22 +390,49 @@ components: description: Ошибка валидации данных schemas: - CityName: + Error: + type: object + required: + - error + - message + - statusCode + properties: + error: + type: string + message: + type: string + statusCode: + type: integer + City: type: string enum: [Paris, Cologne, Brussels, Amsterdam, Hamburg, Dusseldorf] UserBase: type: object + required: + - email + - firstname + - lastname + - avatarPath + - type properties: email: type: string format: email + firstname: type: string + minLength: 1 + maxLength: 15 + lastname: type: string - avatarUrl: + minLength: 1 + maxLength: 15 + + avatarPath: type: string + type: type: string enum: [pro, base] @@ -381,7 +448,7 @@ components: CreateUser: type: object - required: [email, firstname, password, type] + required: [email, firstname, lastname, password, type] properties: email: type: string @@ -402,6 +469,16 @@ components: type: string enum: [pro, base] + LoggedUser: + allOf: + - $ref: '#/components/schemas/UserBase' + - type: object + required: + - token + properties: + token: + type: string + LoginUser: type: object required: [email, password] @@ -412,10 +489,12 @@ components: password: type: string - AuthToken: + UploadUserAvatar: type: object + required: + - filepath properties: - token: + filepath: type: string Coordinates: @@ -427,15 +506,6 @@ components: longitude: type: number - City: - type: object - required: [name, location] - properties: - name: - $ref: '#/components/schemas/CityName' - location: - $ref: '#/components/schemas/Coordinates' - Comment: type: object required: [id, text, date, rating, user] From a0b8511601770752465c2e3abcd82b8b2526f545 Mon Sep 17 00:00:00 2001 From: Adonariz Date: Mon, 25 May 2026 22:50:30 +0300 Subject: [PATCH 35/35] updates specification --- specification/project.spec.yml | 148 ++++++++++++--------------- src/shared/modules/offer/offers.http | 12 +-- src/shared/modules/user/user.http | 5 +- 3 files changed, 75 insertions(+), 90 deletions(-) diff --git a/specification/project.spec.yml b/specification/project.spec.yml index d650ee5..78347cc 100644 --- a/specification/project.spec.yml +++ b/specification/project.spec.yml @@ -131,7 +131,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/OfferPreview' + $ref: '#/components/schemas/Offer' post: tags: [offers] @@ -151,10 +151,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Offer' + '400': + $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' - '422': - $ref: '#/components/responses/ValidationError' /offers/{offerId}: get: @@ -254,7 +254,9 @@ paths: type: array maxItems: 3 items: - $ref: '#/components/schemas/OfferPreview' + $ref: '#/components/schemas/Offer' + '400': + description: Неверно выбран город /offers/favorites: get: @@ -271,7 +273,7 @@ paths: schema: type: array items: - $ref: '#/components/schemas/OfferPreview' + $ref: '#/components/schemas/Offer' '401': $ref: '#/components/responses/Unauthorized' @@ -285,12 +287,8 @@ paths: parameters: - $ref: '#/components/parameters/offerId' responses: - '200': + '204': description: Добавлено в избранное - content: - application/json: - schema: - $ref: '#/components/schemas/Offer' '401': $ref: '#/components/responses/Unauthorized' '404': @@ -305,12 +303,8 @@ paths: parameters: - $ref: '#/components/parameters/offerId' responses: - '200': + '204': description: Удалено из избранного - content: - application/json: - schema: - $ref: '#/components/schemas/Offer' '401': $ref: '#/components/responses/Unauthorized' '404': @@ -323,8 +317,6 @@ paths: description: Добавляет новый комментарий к предложению security: - bearerAuth: [] - parameters: - - $ref: '#/components/parameters/offerId' requestBody: required: true content: @@ -338,10 +330,10 @@ paths: application/json: schema: $ref: '#/components/schemas/Comment' + '400': + $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' - '422': - $ref: '#/components/responses/ValidationError' components: securitySchemes: @@ -390,30 +382,20 @@ components: description: Ошибка валидации данных schemas: - Error: - type: object - required: - - error - - message - - statusCode - properties: - error: - type: string - message: - type: string - statusCode: - type: integer City: type: string enum: [Paris, Cologne, Brussels, Amsterdam, Hamburg, Dusseldorf] + Housing: + type: string + enum: [apartment, house, room, hotel] + UserBase: type: object required: - email - firstname - lastname - - avatarPath - type properties: email: @@ -430,9 +412,6 @@ components: minLength: 1 maxLength: 15 - avatarPath: - type: string - type: type: string enum: [pro, base] @@ -440,10 +419,12 @@ components: User: allOf: - type: object - required: [id] + required: [id, avatarPath] properties: id: type: string + avatarPath: + type: string - $ref: '#/components/schemas/UserBase' CreateUser: @@ -508,7 +489,7 @@ components: Comment: type: object - required: [id, text, date, rating, user] + required: [id, text, postDate, rating, user] properties: id: type: string @@ -516,28 +497,32 @@ components: type: string minLength: 5 maxLength: 1024 - date: - type: string - format: date-time rating: - type: integer + type: number + format: float minimum: 1 maximum: 5 + postDate: + type: string + format: date-time user: $ref: '#/components/schemas/User' CreateComment: type: object - required: [text, rating] + required: [text, rating, offerId] properties: text: type: string minLength: 5 maxLength: 1024 rating: - type: integer + type: number + format: float minimum: 1 maximum: 5 + offerId: + type: string OfferBase: type: object @@ -558,11 +543,8 @@ components: type: boolean isFavorite: type: boolean - rating: - type: number type: - type: string - enum: [apartment, house, room, hotel] + $ref: '#/components/schemas/Housing' rooms: type: integer guests: @@ -578,8 +560,9 @@ components: Offer: allOf: + - $ref: '#/components/schemas/OfferBase' - type: object - required: [id, publishDate, host, commentsCount] + required: [id, publishDate, host, rating, commentsCount] properties: id: type: string @@ -590,39 +573,9 @@ components: format: date-time commentsCount: type: integer - - $ref: '#/components/schemas/OfferBase' - - OfferPreview: - allOf: - - type: object - required: [id, publishDate, commentsCount] - properties: - id: - type: string - publishDate: - type: string - format: date-time - commentsCount: - type: integer - - type: object - properties: - title: - type: string - type: - type: string - enum: [apartment, house, room, hotel] - price: - type: integer - city: - $ref: '#/components/schemas/City' - previewImage: - type: string - isPremium: - type: boolean - isFavorite: - type: boolean rating: type: number + format: float CreateOffer: allOf: @@ -643,5 +596,36 @@ components: - location UpdateOffer: - allOf: - - $ref: '#/components/schemas/OfferBase' + type: object + additionalProperties: false + properties: + title: + type: string + description: + type: string + city: + $ref: '#/components/schemas/City' + previewImage: + type: string + images: + type: array + items: + type: string + isPremium: + type: boolean + isFavorite: + type: boolean + type: + $ref: '#/components/schemas/Housing' + rooms: + type: integer + guests: + type: integer + price: + type: integer + facilities: + type: array + items: + type: string + location: + $ref: '#/components/schemas/Coordinates' diff --git a/src/shared/modules/offer/offers.http b/src/shared/modules/offer/offers.http index 3f58d0a..7e1a282 100644 --- a/src/shared/modules/offer/offers.http +++ b/src/shared/modules/offer/offers.http @@ -75,27 +75,27 @@ GET http://localhost:3000/offers/6a1475f5e8df7c0412530a85/comments HTTP/1.1 ### ## Получить список премиальных объявлений по городу -GET http://localhost:3000/offers/premium?city=Paris&limit=10 HTTP/1.1 +GET http://localhost:3000/offers/premium?city=Brussels&limit=10 HTTP/1.1 ### ## Добавить в избранное -POST http://localhost:3000/offers/6a135a60cf8b052690407eae/favorite HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExMzY5MTQwMjUxZGFhZGFlZTVlMTM2IiwiaWF0IjoxNzc5NjkyMjAyLCJleHAiOjE3Nzk4NjUwMDJ9.4JQGX0uO4A8oYBLiE0i2y8jV_QCcR1fsVOVADKa1OP4 +POST http://localhost:3000/offers/6a1475f5e8df7c0412530a85/favorite HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzI1Nzg3LCJleHAiOjE3Nzk4OTg1ODd9.hFl1pYW4ARmBGxkFB4JuxP48xWCg4GI80RUqDCbyX_w ### ## Удалить из избранного -DELETE http://localhost:3000/offers/69c98315edbb02aef834bfd1/favorite HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3ODk4MDMyLCJleHAiOjE3NzgwNzA4MzJ9.ZJqG9wZG5xLb3gyaJ3R5nWLgiVABfWiWbMpqd2-sILE +DELETE http://localhost:3000/offers/6a1475f5e8df7c0412530a85/favorite HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzI1Nzg3LCJleHAiOjE3Nzk4OTg1ODd9.hFl1pYW4ARmBGxkFB4JuxP48xWCg4GI80RUqDCbyX_w ### ## Получить избранные объявления GET http://localhost:3000/offers/favorites HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im1pY2hhZWwuYnJvd25AZXhhbXBsZS5jb20iLCJmaXJzdG5hbWUiOiJNaWNoYWVsIiwibGFzdG5hbWUiOiJCcm93biIsImlkIjoiNmExMzVhNjBjZjhiMDUyNjkwNDA3ZTlhIiwiaWF0IjoxNzc5NjkyNjY2LCJleHAiOjE3Nzk4NjU0NjZ9.4kJV6dKYs7NGEZsgTiO9W9e1cuV5YZcoUaf85S3PZ9c +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzI1Nzg3LCJleHAiOjE3Nzk4OTg1ODd9.hFl1pYW4ARmBGxkFB4JuxP48xWCg4GI80RUqDCbyX_w ### diff --git a/src/shared/modules/user/user.http b/src/shared/modules/user/user.http index d9abad0..498377f 100644 --- a/src/shared/modules/user/user.http +++ b/src/shared/modules/user/user.http @@ -29,7 +29,8 @@ Content-Type: application/json ## Отправить изображение -POST http://localhost:3000/users/69ee5f35f61313c1ddf0d82d/avatar HTTP/1.1 +POST http://localhost:3000/users/6a1475d5e8df7c0412530a82/avatar HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzMxODcwLCJleHAiOjE3Nzk5MDQ2NzB9.U9V1c9hjgC5d-jEpPUgNqor3qDItgrp9pq_svWrgezk Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ------WebKitFormBoundary7MA4YWxkTrZu0gW @@ -45,6 +46,6 @@ Content-Type: image/png ## Проверить токен пользователя GET http://localhost:3000/users/login HTTP/1.1 -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNjllZTVmMzVmNjEzMTNjMWRkZjBkODJkIiwiaWF0IjoxNzc3NTc1NjIzLCJleHAiOjE3Nzc3NDg0MjN9.CDkRYtP0vK_gcJ5CRZwEDj9FVqpQVNt0S0qoHgMPnUM +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJmaXJzdG5hbWUiOiJKYWNrIiwibGFzdG5hbWUiOiJUb3JyYW5jZSIsImlkIjoiNmExNDc1ZDVlOGRmN2MwNDEyNTMwYTgyIiwiaWF0IjoxNzc5NzMxODcwLCJleHAiOjE3Nzk5MDQ2NzB9.U9V1c9hjgC5d-jEpPUgNqor3qDItgrp9pq_svWrgezk ###