323 lines
24 KiB
Markdown
323 lines
24 KiB
Markdown
# WealthLedger backend
|
||
|
||
NestJS API WealthLedger. Backend владеет auth, SQLite-данными и server-side
|
||
интеграциями. Потребители используют публичный HTTP API и OpenAPI artifact;
|
||
их репозитории не нужны для разработки, сборки и проверок backend.
|
||
|
||
## Быстрый старт
|
||
|
||
Требуется Node 24 (`.nvmrc`). Создайте `.env` из примера, замените placeholder
|
||
секреты и установите зависимости строго из lockfile:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
nvm use --silent
|
||
npm ci
|
||
npx prisma migrate deploy
|
||
npm run start:dev
|
||
```
|
||
|
||
`JWT_SECRET` должен быть секретом не короче 32 символов, а
|
||
`TOKEN_ENCRYPTION_KEY` — base64-кодированным 32-байтным ключом. Не добавляйте
|
||
T-Bank token в `.env`: он вводится пользователем и хранится в зашифрованном
|
||
виде.
|
||
|
||
## Контейнерный image
|
||
|
||
`Dockerfile` имеет targets `development` (Nest watch, generated Prisma client и Linux native modules)
|
||
и `production` (compiled Node process от UID 1000, Prisma CLI/migrations и T-Bank proto assets). Node соответствует `.nvmrc`.
|
||
Build context ограничен `.dockerignore`; `.env`, local DB и host node_modules
|
||
не попадают в image.
|
||
|
||
```bash
|
||
docker build --target development -t wealthledger-backend:development .
|
||
docker build --target production -t wealthledger-backend:production .
|
||
nvm use --silent
|
||
npm run test:container -- wealthledger-backend:production
|
||
```
|
||
|
||
Gitea `image.yml` запускается на защищённом release tag `vX.Y.Z`, проверяет
|
||
package/lock version и canonical gates. Runner `ubuntu-latest` должен иметь
|
||
Docker/buildx. Образы хранятся в Nexus:
|
||
`nexus.personal.ksv741.ru/docker-hosted/wealthledger/backend`. Пользователь `wealthledger-ci`
|
||
задаётся Actions secrets `NEXUS_USERNAME`/`NEXUS_PASSWORD`; эта же учётная
|
||
запись используется сервером для загрузки образов. Docker endpoint и token realm должны использовать
|
||
HTTPS; в `docker-hosted` включается запрет redeploy. Прежние Gitea secrets
|
||
сохраняются на время перехода и не передаются в Nexus.
|
||
Публикуются semver tags, без `latest`; существующие release tags не перезаписываются. Production выбирает опубликованный version tag.
|
||
|
||
Compose topology принадлежит workspace, эксплуатационная последовательность —
|
||
[документация](https://git.personal.ksv741.ru/WealthLedger/docs),
|
||
страница `architecture/deployment.md`.
|
||
Проверка публикации требует настроенного registry/runner; локальный build
|
||
не доказывает package permissions.
|
||
|
||
## Проверка
|
||
|
||
Единый completion gate не изменяет versioned-файлы:
|
||
|
||
```bash
|
||
npm run verify
|
||
```
|
||
|
||
Он запускает zero-warning lint,
|
||
application/test typechecks, Jest с покрытием и build. `npm run lint:fix` — отдельная
|
||
записывающая команда. `npm run test:cov` также доступен отдельно; общего числового
|
||
threshold нет. `npm run test:e2e` отдельно проверяет все HTTP suites на новых базах
|
||
и соответствие генерируемого OpenAPI настроенному приложению. Для полной backend-проверки
|
||
выполните обе команды.
|
||
|
||
Nest 12 поставляется как ESM. Скрипты Jest включают `--experimental-vm-modules`,
|
||
включая watch/coverage/debug; SQLite runner включает тот же флаг. TypeScript 6
|
||
использует явные `rootDir` и типы Node/Jest в `tsconfig.json`. Переход на
|
||
TypeScript 7 требует поддержки со стороны `ts-jest` и `typescript-eslint`.
|
||
|
||
### CI и обновления зависимостей
|
||
|
||
Единый `.gitea/workflows/verify.yml` запускает `commits → security → verify`
|
||
последовательно на PR; на push в `develop`/`main` — `security → verify`.
|
||
Рабочие ветки проверяются после открытия PR, без дублирующего push-запуска.
|
||
Изменение заголовка PR тоже повторяет проверки Conventional Commits.
|
||
Новый запуск отменяет устаревший для того же PR/ветки и типа события
|
||
(требуется Gitea 1.26+). Ежедневно в 05:47 UTC выполняется только `security`.
|
||
Job `verify` запускает `npm ci`, генерацию Prisma client,
|
||
`npm run verify` и `npm run test:e2e`. Используется только checkout
|
||
backend и его зависимости; OpenAPI генерируется во время проверки, SHA записывается в лог.
|
||
Успешный unit gate не заменяет SQLite/e2e; ошибка любой suite завершает job с FAIL.
|
||
|
||
Job `security` сканирует всю Git-историю Gitleaks 8.30.1 с полностью скрытыми
|
||
значениями и выполняет `npm audit --include=dev --audit-level=low`.
|
||
`.gitleaksignore` перечисляет только проверенные исторические
|
||
совпадения: детерминированные fixtures, placeholder и badge из Nest starter.
|
||
Новые коммиты этими исключениями не покрываются.
|
||
|
||
Имена jobs `commits`, `security`, `verify` сохранены, но Gitea включает имя
|
||
workflow в статус. При внедрении администратор должен заменить required checks
|
||
для защищённых веток на `Backend verify / commits (pull_request)`,
|
||
`Backend verify / security (pull_request)` и
|
||
`Backend verify / verify (pull_request)`, удалив старые имена и push-статусы,
|
||
если они требовались. Все три PR-проверки должны оставаться обязательными.
|
||
Публикация образов по тегам остаётся в `image.yml`.
|
||
Последовательность jobs ограничивает один запуск; для ограничения нагрузки всего сервера настройте
|
||
`runner.capacity: 1` у общего runner и учтите остальные зарегистрированные
|
||
runners. Конфигурация runner управляется вне этого репозитория.
|
||
|
||
Renovate группирует minor/patch updates, а major требует согласования в
|
||
Dependency Dashboard. Автоматический merge не включён. Прямые зависимости
|
||
фиксируются точными версиями: `.npmrc` задаёт `save-exact=true` для npm install,
|
||
Renovate использует `rangeStrategy: pin`. Обновляйте `package.json` вместе с
|
||
`package-lock.json`. Overrides в
|
||
`package.json` закрывают advisories транзитивных `multer`, `deepmerge-ts` и
|
||
`mysql2`; удаляйте их, когда родительские пакеты примут исправленные
|
||
версии. Совместимость Prisma override проверяется генерацией и SQLite migrations.
|
||
Required checks настраиваются отдельно в Gitea: наличие workflow само по себе
|
||
не блокирует merge.
|
||
|
||
Общий `tsconfig.json` включает `strict: true` и `noFallthroughCasesInSwitch`
|
||
для приложения, тестов и сборок. ESLint запрещает non-null assertions
|
||
(`value!`) и definite-assignment assertions (`field!: Type`).
|
||
Поля DTO, заполняемые вне конструктора, описываются через `declare`:
|
||
входные значения заполняет и проверяет Nest, ответные DTO описывают форму
|
||
возвращаемых объектов. `declare` не создаёт значение и не заменяет
|
||
runtime-валидацию. В обычном коде нужны инициализация или сужение типа
|
||
после проверки значения.
|
||
|
||
## ESLint
|
||
|
||
`npm run lint` проверяет весь authored TypeScript и JavaScript, включая scripts
|
||
и configs. `npm run lint:fix` (также `npm run format`) применяет безопасные
|
||
автоисправления. Форматирование принадлежит ESLint Stylistic: два пробела,
|
||
одинарные кавычки и точки с запятой; Prettier не используется.
|
||
|
||
Профиль: ESLint recommended, typescript-eslint recommendedTypeChecked,
|
||
Node, NestJS typed, Jest, import-x с TypeScript resolver, SonarJS, RegExp,
|
||
Security, Perfectionist recommended-natural и Stylistic customize.
|
||
NestJS проверяется в production TS, Jest — в specs/e2e; generated Prisma,
|
||
dist и coverage исключены. DI imports сохраняют runtime metadata.
|
||
|
||
Исключения конфигурации: private backend не публикуется в npm;
|
||
TS imports разрешает import-x вместо Node resolver; IsIn проверяет domain
|
||
unions. Автосортировка объектов, классов, модулей и декораторов отключена,
|
||
поскольку порядок может влиять на runtime. Остальные точечные исключения
|
||
объясняют false positives непосредственно в коде.
|
||
|
||
`eslint-suppressions.json` — штатный ESLint baseline только для существующих
|
||
SonarJS cognitive-complexity, no-nested-conditional и no-nested-template-literals.
|
||
Это отложенный refactor, а не исправленные нарушения. Baseline ограничен
|
||
файлами и количеством сообщений; новые файлы и превышение количества блокируют
|
||
lint. Он не защищает от усложнения уже учтённой функции при прежнем количестве
|
||
сообщений — это остаётся предметом review. При сокращении долга обновляйте
|
||
baseline командой `npx eslint . --prune-suppressions`; не пересоздавайте его
|
||
для обхода новых ошибок. Security, TypeScript и Jest в baseline не включены.
|
||
|
||
Рост baseline — изменение quality gate: он требует отдельного обоснования
|
||
и review затронутого поведения. При рефакторинге сохраняйте или сокращайте
|
||
baseline; прежнее число сообщений не доказывает прежнюю сложность функции.
|
||
|
||
## SonarQube
|
||
|
||
Проект `wealthledger-backend` на `https://sonarqube.personal.ksv741.ru`
|
||
использует `sonar-project.properties`. Анализируются исходники `src`; unit specs
|
||
и HTTP specs учитываются как тесты. Внешние T-Bank proto contracts исключены,
|
||
generated Prisma client находится за пределами `sonar.sources`.
|
||
|
||
`npm run verify` запускает unit tests с покрытием один раз. Jest штатно создаёт
|
||
`coverage/lcov.info`; scanner импортирует этот отчёт. Покрытие HTTP/e2e tests
|
||
в него не входит, сами HTTP suites остаются обязательным отдельным шагом CI.
|
||
|
||
Создайте в SonarQube проект с ключом `wealthledger-backend`, а в Gitea → backend →
|
||
Settings → Actions → Secrets добавьте `SONAR_TOKEN` с правом анализа этого проекта.
|
||
Токен не сохраняется в файлах. Community Edition 9.9.8 получает анализ только
|
||
при push в `develop`, после успешных `verify` и SQLite/e2e checks. CI ожидает
|
||
Quality Gate; отрицательный результат или ошибка scanner завершает job с ошибкой.
|
||
PR и другие ветки проходят штатные проверки без отправки анализа.
|
||
Переименование основной ветки SonarQube не требуется; `sonar.branch.name` не задаётся.
|
||
|
||
Runner должен поддерживать Docker actions и иметь доступ к GitHub, Docker Hub
|
||
и HTTPS-адресу SonarQube с действующим сертификатом для этого имени.
|
||
Официальный Docker action закреплён на commit версии 2.3.0, как в frontend.
|
||
`tsconfig.sonar.json` содержит совместимые со старым анализатором настройки
|
||
модулей и decorators с target ES2022, без новых compiler options приложения.
|
||
Штатная компиляция приложения остаётся на ES2023.
|
||
После обновления сервера пересмотрите scanner и необходимость отдельного tsconfig.
|
||
|
||
Локально после `nvm use --silent`, `npm ci` и генерации Prisma client выполните
|
||
`npm run verify`, затем `sonar-scanner` с совместимым SonarScanner CLI и
|
||
`SONAR_TOKEN` в окружении. Первый запуск CI после merge должен подтвердить
|
||
загрузку отчёта и покрытия: локальные проверки не доказывают доступность сервера.
|
||
|
||
## Документация
|
||
|
||
- [AGENTS: правила и источники](AGENTS.md)
|
||
- [Backend architecture](.ai/architecture.md)
|
||
- [HTTP API](.ai/conventions/http-api.md), [data](.ai/conventions/data.md) и [testing](.ai/conventions/testing.md)
|
||
- [Security/auth](.ai/conventions/security-auth.md) и [integrations](.ai/conventions/integrations.md)
|
||
|
||
- [Генерация OpenAPI](#генерация-openapi)
|
||
|
||
Генерация без сервера сверяется с OpenAPI настроенного приложения:
|
||
|
||
```bash
|
||
npm run test:openapi-contract
|
||
```
|
||
|
||
## Генерация OpenAPI
|
||
|
||
```bash
|
||
nvm use --silent
|
||
npm run openapi:generate
|
||
```
|
||
|
||
Команда собирает backend и записывает `openapi.json` в корень репозитория.
|
||
Сервер и HTTP-порт не запускаются; Nest preview читает metadata без создания
|
||
провайдеров и подключения к БД/T-Bank. Настроенный `.env` и реальные секреты
|
||
не нужны. `openapi.json` исключён из Git и каждый раз создаётся из DTO/Swagger
|
||
metadata; передай полученный artifact потребителю отдельно.
|
||
|
||
`npm run test:openapi-contract` собирает backend и сравнивает результат генератора
|
||
с runtime schema настроенного приложения, используя временный каталог и SQLite.
|
||
`npm run test:e2e` также сначала собирает backend. Изменение публичного API
|
||
проверяй relevant HTTP tests: без versioned snapshot сравнение не отслеживает
|
||
breaking changes относительно предыдущего выпуска.
|
||
|
||
Разработка идёт напрямую по задаче пользователя; OpenSpec/SDD — по явному запросу.
|
||
|
||
## Реальная SQLite и сохранность данных
|
||
|
||
После `nvm use --silent` запускайте `npm run test:sqlite` при изменении auth,
|
||
карточных операций или DB boundary, `npm run test:e2e` — для всех HTTP suites.
|
||
Обе команды используют один runner: отдельный каталог `wealthledger-e2e-*`
|
||
в системном temp на каждую suite, один `DATABASE_URL` для migrations и Jest.
|
||
Внешний `DATABASE_URL` заменяется временным; `.env` и пользовательские данные
|
||
не изменяются. Прямой Jest без runner отклоняется до обращения к базе.
|
||
При ошибке migration/Jest или SIGINT/SIGTERM runner останавливает дочерние
|
||
процессы, удаляет свой каталог и возвращает ненулевой exit code.
|
||
Нужен доступ к локальным HTTP-портам; sandbox может его запрещать.
|
||
|
||
`prisma.config.ts` удаляет унаследованный `RUST_LOG` только в процессе Prisma CLI:
|
||
на Prisma 7.9.1 уровни `warn`/`error` вызывают пустой `Schema engine error`
|
||
при fresh migration, без переменной миграция проходит. Постоянный
|
||
`RUST_LOG=info` и обновление зависимостей не нужны. Logging приложения
|
||
настраивается отдельно. История migrations применяется штатным `migrate deploy`.
|
||
|
||
Регрессия самого запуска: `node test/sqlite-regression.cjs`. Она выполняет
|
||
три последовательных и два параллельных полных e2e, проверяет уникальность DB,
|
||
сохранность вымышленного внешнего файла, отказ migration и SIGINT/SIGTERM cleanup.
|
||
Логи остаются в каталоге из вывода. Для одной suite:
|
||
`node test/sqlite.cjs app.e2e-spec.ts`.
|
||
|
||
| Риск | Проверка реальной DB |
|
||
| --- | --- |
|
||
| Чужой пользователь | `bank-cards.e2e-spec.ts`: owner каждой карты, категории, покупки и стороны перевода |
|
||
| Повтор и конкурентный update | Те же tests: fingerprint, tombstone, версии, один refund в пределах покупки, archive/write |
|
||
| Конкурентный refresh | `app.e2e-spec.ts`: оба запроса читают прежний hash до CAS; один 200, один 401, cookie победителя работает после replay |
|
||
| Rollback и деньги | Карточные tests: откат source lock, атомарный перевод, точные суммы выше 2^53 и границы signed64 |
|
||
| Migration, restart, backup/restore | `test:durability`: прежнее приложение, новая schema, история, откат приложения и восстановление backup |
|
||
|
||
Prisma в этих suites не заменяется mocks. В refresh тесте барьер задерживает
|
||
настоящую подпись JWT, чтобы оба запроса дошли до реальной конкурентной записи.
|
||
Unit tests с mocks отдельно проверяют orchestration и не доказывают DB concurrency.
|
||
|
||
При изменении schema/migrations или сохранения данных дополнительно выполните
|
||
`npm run test:durability -- /absolute/path/to/baseline`. Он не входит в lint/verify.
|
||
Baseline — отдельная сборка до проверяемых миграций с неизменённым префиксом
|
||
migration history. Для текущей карточной схемы подходит revision
|
||
`8ec172f48eefa2237dc0c16919fbb2e726b39a65` (до добавления карт):
|
||
|
||
```bash
|
||
# Из backend; экспорт не содержит .env или локальную базу.
|
||
baseline_dir=$(mktemp -d /tmp/wealthledger-baseline-XXXXXX)
|
||
git archive 8ec172f48eefa2237dc0c16919fbb2e726b39a65 | tar -x -C "$baseline_dir"
|
||
(
|
||
cd "$baseline_dir"
|
||
nvm use --silent
|
||
npm ci
|
||
npm run build
|
||
)
|
||
nvm use --silent
|
||
npm run build
|
||
npm run test:durability -- "$baseline_dir"
|
||
```
|
||
|
||
Не подменяйте baseline текущей сборкой: runner отвергает тот же путь,
|
||
отсутствующую сборку и неподходящую migration history. Для следующей schema
|
||
выберите совместимую предыдущую revision и проверьте её API-сценарии runner.
|
||
Durability оставляет временные DB, логи и `result.json` по пути из вывода для
|
||
диагностики; он не откатывает schema пользовательской базы.
|
||
|
||
## Релизы
|
||
|
||
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
|
||
релизы. Работа: `codex/<task>` от актуального `develop`, PR в `develop`.
|
||
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
|
||
Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай.
|
||
|
||
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что
|
||
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
|
||
`develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в
|
||
`release/hotfix-<name>` и проходит тот же процесс.
|
||
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
|
||
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
|
||
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
|
||
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
|
||
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
|
||
открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных
|
||
`verify`, `security`, `commits` выполни fast-forward-only merge. Если `main`
|
||
изменился, согласуй историю и повтори проверки кандидата.
|
||
4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
|
||
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
|
||
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
|
||
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
|
||
5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь,
|
||
что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после
|
||
merge создай отсутствующий тег без нового bump; до merge исправь и проверь
|
||
существующий кандидат без повторного повышения версии.
|
||
|
||
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
|
||
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
|
||
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
|
||
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
|
||
администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены.
|
||
Workflow сам не включает эти настройки. Отдельное review approval для одного
|
||
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
|