# Инструкции для AI-агента: Backend Документ для агентов при создании и доработке `samreshu-backend`. Общий контекст проекта — в [context.md](context.md). ## Ссылки на документацию | Документ | Что взять | | - | - | | [api/contracts.md](../api/contracts.md) | Все endpoints, request/response, коды ошибок | | [database/schema.md](../database/schema.md) | Таблицы, колонки, связи, индексы | | [principles/security.md](../principles/security.md) | Auth (argon2, JWT, refresh), rate limiting, Helmet, CORS | | [llm/strategy.md](../llm/strategy.md) | Промпты, JSON Schema валидации, fallback, LlmService интерфейс | | [principles/code-style.md](../principles/code-style.md) | ESLint, Prettier, TS config, Vitest | | [onboarding/setup.md](../onboarding/setup.md) | Docker Compose, .env шаблон | --- ## Структура репозитория ```text samreshu-backend/ ├── src/ │ ├── index.ts # Entry point │ ├── app.ts # Fastify app, регистрация плагинов │ ├── config/ │ │ └── env.ts # Валидация и экспорт env переменных │ ├── db/ │ │ ├── index.ts # Drizzle client, экспорт drizzle │ │ ├── schema/ # Drizzle schema (users, sessions, tests, ...) │ │ └── migrations/ # SQL миграции (drizzle-kit) │ ├── plugins/ │ │ ├── database.ts # Подключение к PostgreSQL │ │ ├── redis.ts # Подключение к Redis │ │ ├── auth.ts # JWT verify, decode, refreshToken logic │ │ ├── subscription.ts # Загрузка user.plan из subscriptions │ │ ├── rateLimit.ts # @fastify/rate-limit + Redis │ │ ├── helmet.ts # @fastify/helmet │ │ └── cors.ts # @fastify/cors │ ├── routes/ │ │ ├── auth.ts # POST /auth/register, login, logout, refresh, ... │ │ ├── profile.ts # GET/PATCH /profile, GET /profile/:username │ │ ├── tests.ts # POST/GET /tests, answer, finish, results, history │ │ └── admin/ │ │ └── questions.ts # GET queue, PATCH /admin/questions/:id │ ├── services/ │ │ ├── auth.service.ts # register, login, verifyPassword, createSession │ │ ├── user.service.ts # getProfile, updateProfile │ │ ├── test.service.ts # createTest, getTest, answerQuestion, finishTest │ │ ├── question.service.ts # getFromBank, fallback logic │ │ └── llm.service.ts # generateQuestions, вызов Ollama API, валидация │ ├── lib/ │ │ ├── errors.ts # Кастомные ошибки (AppError) │ │ ├── pagination.ts # Cursor-based helper │ │ └── uuid.ts # UUID v7 генерация (если нет встроенной) │ └── types/ │ └── index.ts # Общие типы (Stack, Level, QuestionType) ├── docker-compose.dev.yml # PostgreSQL + Redis + Ollama ├── .env.example ├── package.json ├── tsconfig.json ├── vitest.config.ts └── drizzle.config.ts ``` Префикс `/api/v1` задаётся при регистрации роутов: `fastify.register(routes, { prefix: '/api/v1' })`. --- ## Порядок разработки Рекомендуемая последовательность (каждый шаг — отдельный PR): ### 1. Каркас - Инициализация проекта (npm init, зависимости) - `config/env.ts` — загрузка и валидация env (zod или аналог) - `app.ts` — Fastify, Pino logger, базовые плагины (helmet, cors) - `db/schema` — Drizzle schema для MVP 0: users, sessions, subscriptions, tests, test_questions, question_bank - `db/index.ts` — Drizzle client - Миграции: `drizzle-kit generate` + `migrate` - Docker Compose (PostgreSQL, Redis) ### 2. Auth - `plugins/auth.ts` — JWT verify, refresh token из cookie - `services/auth.service.ts` — argon2, создание sessions, ротация refresh - `routes/auth.ts` — register, login, logout, refresh, verify-email, forgot-password, reset-password - `plugins/rateLimit.ts` — прогрессивный lockout на login - Email-мок для dev (например, вывод в консоль или mailpit) ### 3. Profile - `plugins/subscription.ts` — middleware, подгружающий user.plan - `services/user.service.ts` - `routes/profile.ts` — GET/PATCH /profile, GET /profile/:username - Защита роутов: `preHandler` с проверкой JWT ### 4. Tests (ядро) - `services/llm.service.ts` — вызов Ollama, JSON Schema валидация, fallback на банк - `services/question.service.ts` — выбор из банка, дедупликация - `services/test.service.ts` — создание теста, снепшот в test_questions, answer, finish - `routes/tests.ts` — все endpoints для тестов - Проверка лимита Free (5 тестов/день) в subscription middleware ### 5. Admin - `routes/admin/questions.ts` — GET queue, PATCH approve/reject - Проверка `user.role === 'admin'` в preHandler ### 6. Seed и прогон - Скрипт `npm run seed:questions` — наполнение question_bank через LLM - Скрипт `npm run db:seed` — тестовый пользователь (опционально) - Проверка полного flow вручную --- ## Ключевые паттерны ### Fastify plugins Каждый плагин — отдельный файл, регистрируется через `fastify.register(plugin)`: ```ts // plugins/auth.ts import type { FastifyPluginAsync } from 'fastify' const authPlugin: FastifyPluginAsync = async (fastify) => { fastify.decorate('verifyJwt', async (request, reply) => { ... }) } export default authPlugin ``` Декорators: `fastify.verifyJwt`, `fastify.db`, `fastify.redis`, `fastify.llm` — доступны после регистрации плагинов. ### Обработка ошибок Единый `setErrorHandler`: ```ts fastify.setErrorHandler((error, request, reply) => { if (error instanceof AppError) { return reply.status(error.statusCode).send({ error: { code: error.code, message: error.message } }) } fastify.log.error(error) return reply.status(500).send({ error: { code: 'INTERNAL_ERROR', message: 'Internal server error' } }) }) ``` `AppError` — класс с полями `statusCode`, `code`, `message`. Коды ошибок — из [api/contracts.md](../api/contracts.md). ### Drizzle - Схема в `db/schema/` — отдельный файл на домен (users.ts, tests.ts, question_bank.ts) - Все запросы через `db.select()`, `db.insert()`, `db.update()` — параметризованные - Даты: `new Date()` в UTC, в БД — `timestamptz` - UUID: `uuidv7()` или аналог для PK ### LlmService - Один класс/объект в `services/llm.service.ts` - Конфиг из env: `LLM_BASE_URL`, `LLM_MODEL`, `LLM_TIMEOUT_MS` - Вызов: `POST ${baseUrl}/chat/completions` с телом OpenAI-формата - Ответ: извлечь `content` из `choices[0].message`, парсить JSON (см. llm/strategy.md — извлечение из markdown) - Валидация: JSON Schema (ajv или zod) перед использованием - Retry: 1 раз при таймауте или невалидном ответе - Fallback: `question.service.getFromBank()` если LLM недоступен ### Валидация request body Fastify schema на каждом route: ```ts schema: { body: { type: 'object', required: ['email', 'password'], properties: { email: { type: 'string', format: 'email' }, password: { type: 'string', minLength: 8 } }, additionalProperties: false } } ``` ### Пагинация cursor-based Использовать `limit` и `cursor` (UUID). Запрос: ```sql SELECT * FROM tests WHERE user_id = :userId AND (cursor IS NULL OR id < :cursor) ORDER BY id DESC LIMIT :limit + 1 ``` Если вернулось `limit + 1` записей — `hasMore = true`, `nextCursor = last.id`. Иначе `hasMore = false`, `nextCursor = null`. --- ## Тестирование - **Vitest** для unit и integration тестов - Покрытие: минимум 70% на сервисном слое (см. code-style.md) - Моки: - LlmService — не вызывать реальный Ollama в тестах, возвращать фиксированный JSON - Email — мок, не отправлять реальные письма - Redis — можно in-memory или реальный Redis в Docker (integration) - Тесты сервисов: мок Drizzle (или тестовая БД с migrater) - Тесты роутов: `fastify.inject()` с моками сервисов --- ## Чеклист перед коммитом - [ ] ESLint и Prettier проходят - [ ] `npm run test` — все тесты зелёные - [ ] Нет `console.log` (только `fastify.log` / `logger`) - [ ] Нет `any` без явного обоснования - [ ] Секреты не в коде (только env) - [ ] Новые endpoints соответствуют [api/contracts.md](../api/contracts.md)