Files
dating-app-backend/CLAUDE.md
2026-06-02 15:52:22 +03:00

154 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md — Архитектурный журнал
Этот файл ведётся автоматически. Фиксирует архитектурные решения,
отступления от ТЗ и причины этих отступлений.
---
## Стек
| Слой | Выбор | Версия |
|---|---|---|
| Runtime | Node.js + NestJS | NestJS 10 |
| HTTP-сервер | Fastify (не Express) | fastify 4 |
| WebSocket | Socket.io (`@nestjs/platform-socket.io`) | socket.io 4 |
| ORM | Drizzle ORM | 0.38.4 |
| База данных | PostgreSQL | — |
| Кеш / очереди | Redis (ioredis) | ioredis 5 |
| Хранилище файлов | MinIO (S3-совместимое) | minio 8 |
| Push-уведомления | Firebase Admin SDK (FCM) | firebase-admin 12 |
| Авторизация | JWT (passport-jwt) + refresh-token в Redis | — |
| Пакетный менеджер | pnpm | 11.x |
| API-документация | OpenAPI через `@nestjs/swagger` | swagger 7 |
---
## Архитектурные решения
### 1. Глобальный JWT-guard через `APP_GUARD`
Все маршруты закрыты по умолчанию. Публичные эндпоинты помечаются
декоратором `@Public()`. Это предотвращает случайное открытие нового
роута без авторизации.
### 2. Глобальный response wrapper `{ data: ... }`
`TransformInterceptor` оборачивает все успешные ответы в `{ data: T }`.
Унифицирует контракт клиента: клиент всегда читает `response.data`.
### 3. Refresh-токены в Redis, не в БД
Refresh-токен хранится в Redis с TTL 30 дней (`refresh_token:<userId>`).
При logout ключ удаляется — токен немедленно инвалидируется без
изменений в PostgreSQL. Это быстрее и не засоряет основную БД.
### 4. `user.active_chat_id` — ограничение на уровне приложения, не FK
В схеме `activeChatId` — простой UUID без `REFERENCES chat(id)`.
Причина: Drizzle не поддерживает отложенные FK (DEFERRABLE), а при
создании чата нужно сначала INSERT в `chat`, потом UPDATE `user`.
Добавление жёсткого FK создало бы проблему курицы и яйца.
Инвариант «один активный чат» соблюдается на уровне сервиса
(`chat.service.ts → createChat`).
### 5. `chat.profile1_id / profile2_id` хранят `user_id`, не `profile_id`
**Отступление от ТЗ.** ТЗ называет поля `profile1_id → profile`,
но семантически чат привязан к паре пользователей (через матч).
Поиск участника чата по userId напрямую не требует JOIN с `profile`.
Схема сохраняет исходные имена полей, но ссылается на `user.id`.
Если потребуется строгая привязка к профилю — нужен отдельный FK.
### 6. Лимит матчей проверяется при лайке, не при входе в ленту
ТЗ: «если у пользователя больше N матчей — поиск становится неактивным».
Реализовано как `BadRequestException` в `LikesService.createLike()`.
Лента (`FeedService`) возвращает профили без проверки лимита — клиент
сам скрывает кнопку лайка на основе флага из профиля пользователя.
Значение N задаётся через `MAX_MATCHES_BEFORE_PAUSE` в `.env`.
### 7. Сообщения отправляются и через REST, и через WebSocket
Клиент может отправить сообщение двумя путями:
- `POST /api/v1/chats/:id/messages` — REST (HTTP)
- событие `send_message` через Socket.io
Оба пути вызывают `ChatService.sendMessage()`. Socket.io-событие
дополнительно бродкастит `new_message` всем участникам комнаты.
REST-ответ не бродкастит — клиент должен сам обновить UI или слушать WS.
### 8. Файловые загрузки через Fastify Multipart, не Multer
ТЗ не оговаривает конкретный механизм. Multer работает только с Express.
Поскольку выбран Fastify, используется `@fastify/multipart`, зарегистрированный
в `main.ts`. Файл читается вручную через `req.file()` в контроллере.
### 9. WebSocket-gateway работает в пространстве имён `/chat`
Одно пространство имён вместо нескольких (чат + уведомления).
Уведомления о матчах и системные события отправляются через
`ChatGateway.emitToUser()` напрямую по `socketId`.
Если потребуется изолировать уведомления — вынести в отдельный gateway.
### 10. Feed использует `ORDER BY RANDOM()`
Лента случайно перемешивает профили при каждом запросе. Это дёшево
на малых данных и соответствует духу ТЗ («уникальная лента»).
При росте таблицы нужно заменить на cursor-based с детерминированным
seed (например, хешем userId + дата).
---
## Отступления от ТЗ
| # | Пункт ТЗ | Как реализовано | Причина |
|---|---|---|---|
| 1 | `chat.profile1_id → profile` | Поле хранит `user_id` | Матч создаётся между пользователями, а не профилями; JOIN с профилем не нужен на горячем пути |
| 2 | Поиск «неактивен» при превышении лимита матчей | `BadRequestException` при лайке | Проще контракт с клиентом: ошибка явная, не нужно отдельного флага `searchActive` |
| 3 | Тарифный план один, но оплата предусмотрена | Таблицы `tariff` и `payment` созданы, логика оплаты не реализована | ТЗ: «регистрация открыта для всех, тарифный план один» — бизнес-логики оплаты нет |
| 4 | `radiusKm` в фильтре ленты | Параметр принимается, но фильтрация по радиусу не применяется | Требует PostGIS или формулы Haversine; добавить в следующей итерации |
---
## Известные технические долги
| Файл | Проблема | Причина |
|---|---|---|
| `*/services/*.ts` | `as any` в `.insert().values()` и `.update().set()` | Drizzle ORM 0.38 не включает nullable и enum-колонки с `.default()` в TypeScript insert-тип; при апгрейде Drizzle — убрать касты |
| `feed.service.ts` | N+1 запрос: теги загружаются по одному на профиль | Достаточно для MVP; исправить через один JOIN с GROUP BY при росте нагрузки |
| `chat.service.ts` | `getMyChats` возвращает только `active` чаты | Закрытые чаты недоступны через API; добавить query-параметр `status` |
| `pnpm-workspace.yaml` | `allowBuilds` пополняется вручную при новых зависимостях с postinstall | Требование безопасности pnpm 11 |
---
## Команды
```bash
# Инфраструктура
docker-compose up -d
# Зависимости
pnpm install
# Первый запуск БД
pnpm db:generate # создать SQL-миграции
pnpm db:migrate # применить миграции
pnpm db:seed # заполнить справочники (roles, tariffs, date_statuses, greetings)
# Разработка
pnpm start:dev # hot-reload dev-сервер
pnpm build # компиляция в dist/
# Инструменты
pnpm db:studio # Drizzle Studio (GUI для БД)
```
## Точки входа
| URL | Описание |
|---|---|
| `http://localhost:3000/api/v1` | REST API |
| `http://localhost:3000/api/docs` | Swagger UI (только dev/staging) |
| `ws://localhost:3000/chat` | Socket.io namespace |
| `http://localhost:9001` | MinIO Console |