Files
samreshu_docs/agents/backend.md
Anton 99cd8ae727 docs: add full project documentation
- Architecture: overview, 7 ADR, tech stack
- Principles: code-style, git-workflow, security
- API contracts: auth, profile, tests, admin endpoints
- Database schema: tables, relationships, indexes
- LLM strategy: prompts, fallback, validation, Qwen 2.5 14B
- Onboarding: setup, Docker, .env template
- Progress: roadmap, changelog
- Agents: context, backend instructions

Made-with: Cursor
2026-03-04 12:07:17 +03:00

230 lines
9.8 KiB
Markdown
Raw Permalink 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.
# Инструкции для 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)