From 32c12fb10d4f739c6af0ce32b8f2ea6e94c42090 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 15 Sep 2026 20:45:39 +0300 Subject: [PATCH] feat: generate OpenAPI without peer repositories --- .ai/architecture.md | 12 ++--- .ai/conventions/data.md | 12 ++--- .ai/conventions/engineering.md | 74 +++++++++++++++++++++++++++++ .ai/conventions/error-handling.md | 4 +- .ai/conventions/http-api.md | 29 ++++++++++-- .ai/conventions/integrations.md | 17 +++---- .ai/conventions/observability.md | 2 - .ai/conventions/security-auth.md | 23 ++++----- .ai/conventions/testing.md | 6 +-- .ci/refs.json | 3 -- .gitea/workflows/verify.yml | 32 ++----------- .gitignore | 3 ++ AGENTS.md | 36 +++++++------- README.md | 79 ++++++++++++++++++++++++------- package.json | 5 +- scripts/generate-openapi.cjs | 40 ++++++++++++++++ src/main.ts | 13 ++--- src/openapi.ts | 11 +++++ test/openapi-contract.e2e-spec.ts | 67 ++++++++++++++------------ 19 files changed, 317 insertions(+), 151 deletions(-) create mode 100644 .ai/conventions/engineering.md delete mode 100644 .ci/refs.json create mode 100644 scripts/generate-openapi.cjs create mode 100644 src/openapi.ts diff --git a/.ai/architecture.md b/.ai/architecture.md index a9a2311..b89cd57 100644 --- a/.ai/architecture.md +++ b/.ai/architecture.md @@ -39,11 +39,12 @@ connection generation. Integration clients владеют protocol/deadline/retr provider records преобразуются перед чистыми domain calculations. Пользовательский `tbank-connection` не владеет низкоуровневым gRPC client. -Runtime OpenAPI из Nest → предоставленный frontend snapshot → generated types. +Nest DTO/metadata → `npm run openapi:generate` → игнорируемый Git `openapi.json` +для публичных consumers. Problem Details фильтруются общей HTTP boundary; StructuredLogger добавляет request context и ограничивает поля событий. Jest unit tests, изолированные SQLite/HTTP suites и OpenAPI parity доказывают разные части поведения. -Системные потоки и ADR выбираются через [Источники](../AGENTS.md#источники). +Локальные источники выбираются через [Источники](../AGENTS.md#источники). ## Module-first organization @@ -216,7 +217,7 @@ Mapping и stable codes — по [Errors](conventions/error-handling.md). сводкой через repository и чистый расчёт. Не разрывай денежную transaction, общий cache/limiter или pending promises при extraction. -Расширение abstractions — по [change scope](../../.ai/conventions/code-health.md#change-scope). +Расширение abstractions — по [change scope](conventions/engineering.md#change-scope). ## Common @@ -230,7 +231,7 @@ protobuf units/nano conversion — T-Bank integration. Domain используе ## Domain literals and magic values Общий выбор symbolic values и числовых policies — -[Domain values](../../.ai/conventions/code-health.md#domain-values). +[Domain values](conventions/engineering.md#domain-values). Сохраняй strict TS и explicit boundary types. Для недоверенного input выбирай `unknown` + runtime narrowing; отключённый lint `no-explicit-any` не делает `any` нормой domain/application model. `declare` допустим для заполняемых извне @@ -239,8 +240,7 @@ DTO fields, не для сокрытия неинициализированно OpenAPI enum metadata и runtime validation должны читать тот же vocabulary, что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain, а DTO получает допустимые значения через `Object.values(BankOperationKind)`. -Frontend переиспользует generated OpenAPI types/values по возможностям генератора, -а не определяет независимый backend-owned словарь. Внешние/generated enums +Публичные enum values входят в OpenAPI artifact. Внешние/generated enums сохраняй; generated Prisma/protocol code вручную не меняй. `@typescript-eslint/no-magic-numbers` включён как error; базовое ESLint rule diff --git a/.ai/conventions/data.md b/.ai/conventions/data.md index 83a044c..b79cc76 100644 --- a/.ai/conventions/data.md +++ b/.ai/conventions/data.md @@ -51,16 +51,16 @@ application checks. Предварительный lookup не заменяет Transaction не охватывает сетевой T-Bank вызов как будто это одна atomic DB операция. Планируй порядок и compensation по текущей задаче; не удерживай DB lock во время retry внешней сети. Access/cache generation semantics — у -[T-Bank source](../../../docs/integrations/tbank.md#граница-финансового-кэша). +[T-Bank integration](integrations.md#retry-cache-и-protocol). ## Деньги и точность Для денежных значений используй existing decimal helpers и строковый wire value, не JS floating-point арифметику для сумм. Currency — отдельная часть Money; разные валюты не складываются без явно заданного conversion contract. -Источник: [Money](../../../docs/glossary.md#money), -пример: [decimal.ts](../../src/common/decimal.ts). UI formatting не меняет -backend ownership расчётов и доменную семантику. +Источник формы — `src/common/money.types.ts`; +пример точной арифметики: [decimal.ts](../../src/common/decimal.ts). +Форматирование ответа не меняет доменную семантику и точность расчёта. ## Schema и migration @@ -73,8 +73,8 @@ client обновляй из schema. Не переписывай уже прим **Безопасный путь:** additive изменение, заполнение/проверка данных и только затем удаление прежней формы, если задача этого требует. Примеры schema/migrations: -[Prisma source](../../prisma/schema.prisma); исторический rationale — -[ADR 001](../../../docs/adr/001-prisma-sqlite-local-mvp.md), не текущий список таблиц. +[Prisma source](../../prisma/schema.prisma). SQLite — embedded storage текущего +приложения; файл БД и encryption key требуют согласованного backup/restore. ## Проверка diff --git a/.ai/conventions/engineering.md b/.ai/conventions/engineering.md new file mode 100644 index 0000000..98f5a1b --- /dev/null +++ b/.ai/conventions/engineering.md @@ -0,0 +1,74 @@ +# Работа в репозитории + +## Граница и порядок работы + +Репозиторий сам содержит инструкции, исходники, конфигурацию и входы своих +проверок. Сборка, CI и обязательный маршрут чтения работают без соседних +checkout. Внешний API задаётся предоставленным публичным контрактом; +исходники или инструкции его реализации не являются зависимостью разработки. +При нехватке требования сообщи, какое решение не определено, до его реализации. + +Начни с результата и affected areas. До edits проследи flow/invariants, найди +owner и 1–3 примера. Код показывает механизм; случайный аналог не меняет контракт. +Работай напрямую; OpenSpec/SDD lifecycle — только по явному запросу. +Перед Node-командами активируй местную `.nvmrc`: `nvm use --silent`. +Сохраняй чужие staged/unstaged/untracked изменения и read-only архивы. + +## Change scope + +Сначала расширь существующего владельца. Иначе используй standard library, +платформу или установленную dependency; небольшая новая реализация допустима +при собственной ответственности. Не создавай helper/interface/wrapper ради +возможного reuse. Общее имя не означает общую бизнес-ответственность. +Сохраняй validation, authorization, atomicity, accessibility и migration safety. + +После diff выполни Boy Scout review: проверь новое дублирование, owner, callers, +небольшие связанные legacy issues и соответствие local conventions. Исправляй +непосредственно связанные low-risk проблемы; широкий cleanup — отдельная задача. + +## Domain values + +Finite business vocabulary принадлежит одному domain/module/entity owner. +Переиспользуй его symbols; для нового словаря без аналога — `as const` + union. +Сохраняй wire values; generated/external enums не переписывай вручную. +Numeric policy называй по смыслу и единицам. Индексы, `Math.max(value, 0)` и +обычные labels не выноси в constants без semantic benefit. +Локальный TypeScript/lint профиль определяет дополнительные ограничения. + +## Dependencies + +Проверь аналоги и direct dependencies в местном manifest до добавления пакета. +Новая dependency оправдана текущей потребностью и меньшей общей сложностью; +оцени compatibility, runtime/bundle и maintenance/security. Импортируемый пакет +объявляется напрямую; транзитивная установка не является контрактом. +Сохраняй exact versions и lockfile; воспроизводимая установка — `npm ci`. +Peer-конфликт решай совместимыми версиями без `--force`/`--legacy-peer-deps`. +Используй существующий analyzer; не добавляй второй для той же проверки. + +## Test Intent + +Перед нетривиальным тестом кратко определи наблюдаемый исход, regression risk, +значимые edge/error/concurrency cases и самую дешёвую надёжную boundary. +Отдельный artifact не нужен. Проверяй результат, а не private call order, +декларации, тривиальные wrappers или поведение framework. +При bug сначала воспроизведи исход. Mock race не доказывает DB atomicity. +Уровни и runnable commands выбирай по местной testing convention. + +## Проверки и завершение + +Code/dependency change: `npm ci`, полный `npm run verify` и дополнительные +risk checks из местной testing convention. Targeted tests ускоряют итерацию, +но не заменяют финальный gate. `lint:fix`/format — edits, не proof. +Markdown-only: проверь новые/изменённые тексты, локальные ссылки/якоря, маршруты +чтения и `git diff --check`; application build ради текста не нужен. +Повтори routing по staged/unstaged/new files. Запиши краткие команды, результаты +и ограничения. Зелёный lint не заменяет semantic/security review. + +## Git и релизы + +Работа — в `codex/` от актуального `develop`, затем PR в `develop`. +Сохраняй чужие изменения; перед commit просмотри staged paths и diff. +Commits и PR titles — Conventional Commits; допустим `spec:`. Каждый commit +содержит одну связанную задачу. Root другого checkout не управляет этим repo. +Прямой push в `main`, force-push и перенос опубликованных тегов не входят в +обычную работу. Перед release/hotfix прочитай местный README, раздел «Релизы». diff --git a/.ai/conventions/error-handling.md b/.ai/conventions/error-handling.md index 5c417dc..7996c42 100644 --- a/.ai/conventions/error-handling.md +++ b/.ai/conventions/error-handling.md @@ -9,7 +9,7 @@ application/domain outcome → application mapping → HttpExceptionFilter Сохраняй принятый профиль RFC 9457: `status`, стабильный публичный `code`, для validation — `errors: [{ pointer, detail }]`. Точный контракт и известное -расхождение validation описаны в [HTTP API](../../../docs/architecture/http-api.md). +расхождение validation описаны в [HTTP API](http-api.md#источник-контракта). Не добавляй `type/title/instance` как обязательные поля без contract change. Чистый domain не импортирует Nest HTTP exceptions. Существующий application @@ -21,7 +21,7 @@ error framework/registry. Если domain outcome требует HTTP interpreta - Используй existing code владельца, не сравнивай внутренние сообщения. Новое значение имеет один symbolic owner; metadata/validation и consumers - согласуются по [domain values](../../../.ai/conventions/code-health.md#domain-values). + согласуются по [domain values](engineering.md#domain-values). - Общий envelope, default status mapping и unexpected errors принадлежат [HttpExceptionFilter](../../src/common/filters/http-exception.filter.ts). Не дублируй `res.status().json()` в controllers/services. diff --git a/.ai/conventions/http-api.md b/.ai/conventions/http-api.md index 81e3353..7e24e75 100644 --- a/.ai/conventions/http-api.md +++ b/.ai/conventions/http-api.md @@ -3,8 +3,8 @@ ## Выбор boundary Сначала определи, меняется wire contract или только реализация существующего -use case. Для route/request/response/status/error shape читай -[HTTP semantics и источник контракта](../../../docs/architecture/http-api.md). +use case. Для route/request/response/status/error shape используй +[локальный публичный контракт](#источник-контракта) и configured HTTP tests. | Проверка | Где выполнять | | --- | --- | @@ -57,11 +57,32 @@ helper ради первого endpoint. Новая pagination явно опре [Operations controller](../../src/modules/investments/operations/presentation/brokerage-operations.controller.ts) показывает guard, user.sub, DTO query и service delegation. -Для нового field обновляй canonical source и generated artifacts в определённом -направлении; body type frontend не исправляется вручную. +Для нового field обновляй Nest DTO/metadata и публикуемый OpenAPI artifact. +Потребители получают публичный контракт независимо от внутреннего устройства API. Проверь happy path и применимые invalid input/access/not-found/domain/upstream errors на boundary, где они возникают. Unit вызов controller не запускает pipes, guards или serialization. `npm run test:openapi-contract` проверяет schema parity, но не поведение invalid request; выбирай соответствующий test по [Testing](testing.md). Финальный gate не заменяет semantic review ошибок. + +## Источник контракта + +Nest DTO/controllers/Swagger metadata определяют публичный HTTP API. +`npm run openapi:generate` собирает backend и записывает корневой `openapi.json`. +Nest preview читает metadata без запуска HTTP-порта, провайдеров, БД и T-Bank; +команда не требует deployment environment. Artifact исключён из Git. + +При изменении API обнови DTO/metadata и выполни генерацию. Потребители получают +artifact отдельно; backend не читает их репозитории. `test:openapi-contract` +сравнивает вывод генератора с runtime schema configured app во временном каталоге. +Проверка подтверждает правильность генерации, но без versioned baseline не +выявляет breaking change относительно предыдущего выпуска. Для такого изменения +согласуй переход consumers и проверь relevant HTTP behavior. + +HTTP prefix — `/api/v1`; Swagger доступен согласно `src/config/swagger.config.ts`. +Общий error response — `application/problem+json` со `status` и публичным `code`. +Validation требует `400 / VALIDATION_ERROR` и `errors: [{ pointer, detail }]`. +Известное ограничение: ValidationPipe сейчас формирует строковые сообщения, +а filter ожидает структурированные field errors; schema parity не доказывает +правильность `errors`. Изменение требует проверки реального HTTP pipeline. diff --git a/.ai/conventions/integrations.md b/.ai/conventions/integrations.md index ebc2951..d742ab0 100644 --- a/.ai/conventions/integrations.md +++ b/.ai/conventions/integrations.md @@ -5,10 +5,9 @@ T-Bank gRPC принадлежит TbankClient; MOEX metadata — MoexClient; NCR ratings — NcrRatingsClient. Сервисы use case используют объявленные клиентом records; SDK clients, metadata и transport errors -остаются внутри integration. Frontend не вызывает -T-Bank и не получает credentials/proto client. Reference source — -[ADR 003](../../../docs/adr/003-tbank-server-side-grpc-boundary.md) и -[connection behavior](../../../docs/integrations/tbank.md). +остаются внутри integration. Credentials и proto client не входят в публичный +HTTP API. Connection lifecycle проверяется на границе соответствующего module +и [Security](security-auth.md). `modules/investments/tbank-connection` — пользовательская capability чтения, создания/замены и удаления подключения. `integrations/tbank` — технические @@ -39,7 +38,8 @@ Existing client уже владеет bounded FIFO queue, rate limit, deadline классификацией. Новый RPC проходит тот же путь; не добавляй вложенный retry loop в service. Не отключай TLS verification ради окружения: используй документированный CA mechanism. Точные параметры и ограничения принадлежат -[connection configuration](../../../docs/integrations/tbank.md#конфигурация-и-ограничения). +[validated environment](../../src/config/env.validation.ts) и +[TbankClient](../../src/integrations/tbank/tbank.client.ts). Эта queue/retry policy относится к T-Bank. MOEX/NCR HTTP clients используют собственный bounded timeout и безопасный unavailable outcome; не приписывай им @@ -47,9 +47,10 @@ gRPC retry policy. При изменении upstream сначала прочи При query/cache change установи key, user/account/generation ownership, freshness и поведение late response. Старый результат не может обслужить новую -connection generation. Client Cached Snapshot frontend отличается от backend -cache/stale policy; semantics конкретного use case читай через -[feature index](../../../docs/README.md#предметные-области). +connection generation. Успешная замена/deletion подключения меняет generation +даже при сохранении прежнего token; ошибка оставляет прежнее подключение. +Кэш конкретного use case принадлежит его service и tests. Не переноси +сохранённый snapshot между пользователями или поколениями подключения. Внешний auth failure и временная недоступность имеют разные public codes; raw gRPC exception не уходит клиенту. Existing translation — в TbankTokenService diff --git a/.ai/conventions/observability.md b/.ai/conventions/observability.md index ae028cb..66d56b9 100644 --- a/.ai/conventions/observability.md +++ b/.ai/conventions/observability.md @@ -17,8 +17,6 @@ - Для ошибки используй public code и безопасный context операции/попыток; [error mapping](error-handling.md) не отдаёт внутренние детали клиенту. -Причины — [ADR 008](../../../docs/adr/008-pino-backend-logging.md), системное -поведение — [HTTP observability](../../../docs/architecture/http-api.md#наблюдаемость). Проверяй новые поля/redaction/correlation existing logging tests; не добавляй тест на каждую строку log. Existing ESLint не запрещает все вызовы `console` отдельным правилом: до настройки встроенного `no-console` это semantic review. diff --git a/.ai/conventions/security-auth.md b/.ai/conventions/security-auth.md index f4ecf59..6c571d0 100644 --- a/.ai/conventions/security-auth.md +++ b/.ai/conventions/security-auth.md @@ -10,21 +10,20 @@ account access/connection generation — TbankAccessService; token crypto — TbankTokenService/TbankCrypto. Расширяй существующую boundary, не добавляй второй JWT middleware, decrypt helper или refresh coordinator. -[Authentication](../../../docs/domains/authentication/overview.md) и -[ADR 006](../../../docs/adr/006-auth-session-policy.md)/[009](../../../docs/adr/009-concurrent-refresh-rotation-policy.md) -задают session semantics. Access хранится только в памяти frontend; refresh -передаётся httpOnly cookie. Source code конфигурации cookie — +Access проверяется JWT strategy; refresh передаётся httpOnly cookie. +Сроки token/session принадлежат auth source и configured HTTP tests. +Source code конфигурации cookie — [AuthController](../../src/modules/auth/presentation/auth.controller.ts); изменение flags/path требует проверки browser/HTTP поведения, не только TypeScript. Refresh rotation атомарна: один победитель, проигравший/superseded token получает 401, но сам по себе не отзывает session победителя. Не вводи replay history/grace -или иной security contract без текущей задачи. Logout идемпотентен; in-memory -frontend session/cache очищается и при network failure. +или иной security contract без текущей задачи. Logout идемпотентен, отзывает +сессию и очищает refresh cookie на HTTP-границе. ## Trust boundaries -Frontend не является security boundary. DTO runtime validation выполняется +Клиентский интерфейс не является security boundary. DTO runtime validation выполняется server-side; whitelist не заменяет access checks. Environment/secret inputs проверяет existing config owner; реальные credentials не попадают в fixtures. @@ -36,8 +35,10 @@ cases для соответствующего изменения. T-Bank credentials остаются server-side и хранятся encrypted. Используй existing AES-256-GCM boundary с validated 32-byte key; не печатай plaintext, шифротекст, cookie, Authorization или connection generation в logs/errors/API. -[Connection source](../../../docs/integrations/tbank.md) определяет -cache invalidation при замене/deletion и ограничение key rotation. +Успешная замена/deletion меняет connection generation и инвалидирует её кэши; +ошибка сохраняет прежнее подключение. Ключ не ротируется автоматически: +новый ключ не расшифрует сохранённые credentials. Нужен исходный ключ или +повторное сохранение token пользователем; сохраняй ключ отдельно от backup БД. Не передавай Prisma User/Session целиком наружу; выбирай public DTO. Пароль обрабатывает существующий bcrypt flow. Не копируй требования о нормализации @@ -56,5 +57,5 @@ cache invalidation при замене/deletion и ограничение key ro message безопасным. Targeted auth/access/crypto/logging tests + [Testing](testing.md). -Cookie/guard/HTTP changes требуют configured HTTP proof; frontend Bearer/retry -проверяется MSW. Schema parity не доказывает отсутствие утечки secret. +Cookie/guard/HTTP changes требуют configured HTTP proof. +Schema parity не доказывает отсутствие утечки secret. diff --git a/.ai/conventions/testing.md b/.ai/conventions/testing.md index 0b64f93..69dd122 100644 --- a/.ai/conventions/testing.md +++ b/.ai/conventions/testing.md @@ -1,7 +1,7 @@ # Backend testing Перед нетривиальными тестами сформулируй -[общий Test Intent](../../../.ai/conventions/testing.md); отдельный документ +[общий Test Intent](engineering.md#test-intent); отдельный документ не требуется. Trivial low-risk изменения обходятся без него. ## Выбор уровня @@ -15,7 +15,7 @@ | Pure mapper/decimal/domain branch | Unit со значимыми edge cases | | Service orchestration/access/retry policy | Jest с mocks внешних boundaries и observable outcome | | Pipe/guard/cookie/status/HTTP error | Configured Nest HTTP boundary; прямого вызова метода controller недостаточно | -| OpenAPI metadata/wire schema | `test:openapi-contract` + потребительский contract check | +| OpenAPI metadata/wire schema | `test:openapi-contract` + configured HTTP tests | | DB constraint/CAS/transaction/migration | Integration с изолированной реальной DB | | Только DTO declarations/thin wiring | Existing contract/type/build без искусственного unit test | @@ -29,7 +29,7 @@ Unit specs colocated в `src/**/*.spec.ts`; HTTP/integration — `test/`. - [BrokerageAccountsService tests](../../src/modules/investments/brokerage-accounts/brokerage-accounts.service.spec.ts): отсутствие connection не вызывает upstream, нормализация и точные суммы. - [AuthService tests](../../src/modules/auth/application/auth.service.spec.ts): invalid/expired/superseded token и concurrent rotation; mock race test не является доказательством DB transaction. - [HTTP filter tests](../../src/common/filters/http-exception.filter.spec.ts): безопасный error envelope. -- [OpenAPI integration](../../test/openapi-contract.e2e-spec.ts): configured app + frontend snapshot, без записи snapshot и без T-Bank. +- [OpenAPI integration](../../test/openapi-contract.e2e-spec.ts): генератор без сервера + configured app; временный JSON, без versioned snapshot и без T-Bank. При bug сначала воспроизведи именно его исход, затем исправь responsible owner. Не создавай tests для каждой декларации, logs строки или поведения Prisma/Nest diff --git a/.ci/refs.json b/.ci/refs.json deleted file mode 100644 index a25a11c..0000000 --- a/.ci/refs.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "frontend": "0244acf429c525b23117f165bbe52ef5403eefd3" -} diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml index 245e3fd..7878842 100644 --- a/.gitea/workflows/verify.yml +++ b/.gitea/workflows/verify.yml @@ -8,39 +8,13 @@ on: jobs: verify: runs-on: ubuntu-latest - defaults: - run: - working-directory: backend steps: - uses: https://github.com/actions/checkout@v4 - with: - path: backend - - name: Checkout compatible peer revisions - env: - SOURCE_BRANCH: ${{ github.head_ref || github.ref_name }} - TARGET_BRANCH: ${{ github.base_ref || github.ref_name }} - run: | - set -eu - for repo in frontend; do - ref=develop - case "$SOURCE_BRANCH:$TARGET_BRANCH" in - release/*:*|*:main) - ref=$(jq -er --arg repo "$repo" '.[$repo]' .ci/refs.json) - printf '%s\n' "$ref" | grep -Eq '^[0-9a-f]{40}$' - ;; - esac - target="../$repo" - if [ "$repo" = main ]; then target=..; fi - git init "$target" - git -C "$target" remote add origin "https://git.personal.ksv741.ru/WealthLedger/$repo.git" - git -C "$target" fetch --depth 1 origin "$ref" - git -C "$target" checkout --detach FETCH_HEAD - git -C "$target" rev-parse HEAD - done - git rev-parse HEAD + - name: Record checked revision + run: git rev-parse HEAD - uses: https://github.com/actions/setup-node@v4 with: - node-version-file: backend/.nvmrc + node-version-file: .nvmrc - run: npm ci - name: Generate Prisma client before typechecks run: npx --no-install prisma generate diff --git a/.gitignore b/.gitignore index bd5b20f..c01f41d 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json /dev.db-* /prisma/*.db /prisma/*.db-* + +# Generated public API artifact +/openapi.json diff --git a/AGENTS.md b/AGENTS.md index 2c50351..9d0a188 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,8 +1,9 @@ # Backend router -Общие инструкции — [workspace AGENTS](../AGENTS.md). Объединяй triggers задачи, -путей и обнаруженных boundaries; читай только нужные темы до решения. -Системные факты/spec/ADR выбирай в разделе [Источники](#источники) по вопросу. +Репозиторий самостоятельный: обязательные инструкции и проверки используют +только его файлы и предоставленные публичные контракты. Перед работой прочитай +[порядок работы](.ai/conventions/engineering.md#граница-и-порядок-работы), затем +объединяй triggers задачи и путей; читай активированные темы до решения. | Решение / сигнал | Читать | | --- | --- | @@ -15,28 +16,23 @@ | JWT/cookie/session/guard/access, encryption, secrets | [Security](.ai/conventions/security-auth.md) | | External client/gateway/SDK/proto, retry/rate limit/cache | [Integrations](.ai/conventions/integrations.md) | | Logging/context/levels, sensitive logs | [Observability](.ai/conventions/observability.md); sensitive data также Security | -| Изменение публичного wire contract / Swagger metadata | HTTP API + [HTTP contract source](../docs/architecture/http-api.md#источник-контракта) | -| Helper/dependency/новый pattern, refactor/scope | [Code health](../.ai/conventions/code-health.md), для пакета — [Dependencies](../.ai/conventions/dependencies.md) | -| Выбор gate, tooling, завершение | [Guardrails](../.ai/conventions/guardrails.md) | +| Изменение публичного wire contract / Swagger metadata | [HTTP API](.ai/conventions/http-api.md#источник-контракта) | +| Helper/dependency/новый pattern, refactor/scope | [Change scope](.ai/conventions/engineering.md#change-scope), для пакета — [Dependencies](.ai/conventions/engineering.md#dependencies) | +| Выбор gate, tooling, завершение | [Проверки](.ai/conventions/engineering.md#проверки-и-завершение) | Новый endpoint обычно требует HTTP API + Testing; Data/Security добавляются по persistence/access trigger. Auth refresh активирует Security + Data + Testing. -Повтори routing по итоговому diff, включая новые файлы. Migration backlog -читается только при соответствующем finding через -[plan](../.ai/migrations/conventions-v1/plan.md). +Перед завершением повтори routing по новым и изменённым файлам. Для Git/release +прочитай [Git и релизы](.ai/conventions/engineering.md#git-и-релизы). ## Источники -Открывай только источник по возникшему вопросу. Code показывает реализацию, -docs — текущую систему, ADR — rationale, specs — requirements. Старый пример -не отменяет контракт; conflicting security/data facts выясняй до решения. - -| Вопрос | Источник | +| Вопрос | Локальный источник | | --- | --- | -| HTTP errors/DTO/wire shape | [HTTP API](../docs/architecture/http-api.md); editable OpenAPI генерирует Nest | -| Auth/session, account access, secrets | [Authentication](../docs/domains/authentication/overview.md), [T-Bank](../docs/integrations/tbank.md) | -| Prisma/data ownership, хранение и доступ | [Containers](../docs/architecture/containers.md#хранимые-данные-и-границы-доступа) | -| T-Bank/cache/logging | [T-Bank](../docs/integrations/tbank.md), [Freshness](../docs/architecture/cross-cutting/data-freshness.md), [Observability](../docs/architecture/http-api.md#наблюдаемость) | -| Money, account/position/income/cache meanings | [Глоссарий](../docs/glossary.md) | -| Поведение области / требования | [Области docs](../docs/README.md#предметные-области), [Specs index](../specs/AI_INDEX.md) | +| HTTP errors/DTO/wire shape | [HTTP API](.ai/conventions/http-api.md), генерация `npm run openapi:generate`, configured HTTP tests | +| Auth/session, account access, secrets | [Security](.ai/conventions/security-auth.md) и auth/access tests | +| Prisma/data ownership | [Data](.ai/conventions/data.md), Prisma schema/migrations | +| T-Bank/cache/logging | [Integrations](.ai/conventions/integrations.md), [Observability](.ai/conventions/observability.md) | +| Money и финансовые расчёты | [Data](.ai/conventions/data.md#деньги-и-точность), domain source и tests владельца | +| Поведение области | Module source и tests; неясное ожидание уточни до изменения | | Versions/scripts/check scope | Manifest/lockfile, ESLint/TypeScript/Jest/Prisma configs | diff --git a/README.md b/README.md index 1d5d6a3..bb819cd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # WealthLedger backend NestJS API WealthLedger. Backend владеет auth, SQLite-данными и server-side -интеграцией T-Bank; frontend не обращается к T-Bank напрямую. +интеграциями. Потребители используют публичный HTTP API и OpenAPI artifact; +их репозитории не нужны для разработки, сборки и проверок backend. ## Быстрый старт @@ -33,7 +34,7 @@ npm run verify application/test typechecks, Jest и build. `npm run lint:fix` — отдельная записывающая команда, `npm run test:cov` — диагностика без общего числового threshold. `npm run test:e2e` отдельно проверяет все HTTP suites на новых базах -и runtime OpenAPI с соседним frontend checkout. Для полной backend-проверки +и соответствие генерируемого OpenAPI настроенному приложению. Для полной backend-проверки выполните обе команды. Nest 12 поставляется как ESM. Скрипты Jest включают `--experimental-vm-modules`, @@ -44,11 +45,9 @@ TypeScript 7 требует поддержки со стороны `ts-jest` и ### CI и обновления зависимостей `.gitea/workflows/verify.yml` запускает `npm ci`, генерацию Prisma client, -`npm run verify` и `npm run test:e2e` на push и PR. Frontend snapshot для -разработки берётся из `develop`; для `main`, PR в `main` и `release/*` — из -полного SHA в `.ci/refs.json`. Лог сохраняет оба SHA. Изменение -контракта согласуется с этой frontend revision. Успешный unit gate не заменяет -SQLite/e2e; ошибка любой suite завершает job с FAIL. +`npm run verify` и `npm run test:e2e` на push и PR. Используется только checkout +backend и его зависимости; OpenAPI генерируется во время проверки, SHA записывается в лог. +Успешный unit gate не заменяет SQLite/e2e; ошибка любой suite завершает job с FAIL. `security.yml` сканирует всю Git-историю Gitleaks 8.30.1 с полностью скрытыми значениями и выполняет `npm audit --include=dev --audit-level=low` при push, @@ -83,20 +82,34 @@ runtime-валидацию. В обычном коде нужны инициал - [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) -- [Карта проектных фактов](../docs/README.md) -- [API contract и runtime OpenAPI](../docs/architecture/http-api.md#источник-контракта) -- [Обзор поставленной системы](../docs/overview.md) -- [Архитектурные решения](../docs/adr/README.md) +- [Генерация OpenAPI](#генерация-openapi) -Из backend runtime OpenAPI можно read-only сверить с frontend snapshot: +Генерация без сервера сверяется с OpenAPI настроенного приложения: ```bash npm run test:openapi-contract ``` -Разработка идёт напрямую по задаче пользователя. Каждый affected repo проходит -свой verify; при изменении wire contract дополнительно выполняется contract check. -Обязательного OpenSpec/SDD lifecycle нет. +## Генерация 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 и сохранность данных @@ -161,4 +174,38 @@ npm run test:durability -- "$baseline_dir" Durability оставляет временные DB, логи и `result.json` по пути из вывода для диагностики; он не откатывает schema пользовательской базы. -Версии и ветки: [процесс релиза](../README.md#релизы-и-ветки). +## Релизы + +Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые +релизы. Работа: `codex/` от актуального `develop`, PR в `develop`. +Обычные PR допускают squash; release и обратная синхронизация сохраняют историю. +Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай. + +1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что + `develop` содержит последний релиз. Создай `release/` от проверенного + `develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в + `release/hotfix-` и проходит тот же процесс. +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 -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 не включены. diff --git a/package.json b/package.json index b227fd9..1458b1e 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "scripts": { "build": "prisma generate && nest build", + "openapi:generate": "npm run build && node scripts/generate-openapi.cjs", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "start": "nest start", "start:dev": "NODE_ENV=development nest start --watch --path tsconfig.dev.json", @@ -24,10 +25,10 @@ "test:watch": "npm test -- --watch", "test:cov": "npm test -- --coverage", "test:debug": "node --experimental-vm-modules --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "node test/sqlite.cjs --all", + "test:e2e": "npm run build && node test/sqlite.cjs --all", "test:sqlite": "node test/sqlite.cjs", "test:durability": "node test/bank-cards-durability.cjs", - "test:openapi-contract": "node test/sqlite.cjs openapi-contract.e2e-spec.ts", + "test:openapi-contract": "npm run build && node test/sqlite.cjs openapi-contract.e2e-spec.ts", "verify": "npm run lint && npm run typecheck:app && npm run typecheck:test && npm test -- --runInBand && npm run build", "release:preview": "release-it --dry-run --ci", "release:prepare": "release-it --ci", diff --git a/scripts/generate-openapi.cjs b/scripts/generate-openapi.cjs new file mode 100644 index 0000000..3845d46 --- /dev/null +++ b/scripts/generate-openapi.cjs @@ -0,0 +1,40 @@ +const { randomBytes } = require('node:crypto'); +const { writeFile } = require('node:fs/promises'); + +// Schema generation needs no deployment configuration or real credentials. +Object.assign(process.env, { + DATABASE_URL: 'file::memory:', + FRONTEND_ORIGIN: 'http://localhost', + JWT_SECRET: randomBytes(32).toString('hex'), + TOKEN_ENCRYPTION_KEY: randomBytes(32).toString('base64'), + PORT: '3000', + LOG_LEVEL: 'error', + TBANK_GRPC_ENDPOINT: 'localhost:443', + TBANK_RATE_LIMIT_PER_SECOND: '5', + TBANK_CA_CERT_PATH: '', +}); + +const { NestFactory } = require('@nestjs/core'); +const { AppModule } = require('../dist/src/app.module.js'); +const { API_PREFIX, createOpenApiDocument } = require('../dist/src/openapi.js'); + +async function generate() { + const app = await NestFactory.create(AppModule, { + preview: true, + logger: false, + }); + try { + app.setGlobalPrefix(API_PREFIX); + await writeFile( + 'openapi.json', + JSON.stringify(createOpenApiDocument(app), null, 2) + '\n', + ); + } finally { + await app.close(); + } +} + +generate().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/src/main.ts b/src/main.ts index fbe3dba..e80447a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,8 @@ import { INestApplication, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import cookieParser from 'cookie-parser'; import { ConfigService } from '@nestjs/config'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { SwaggerModule } from '@nestjs/swagger'; +import { API_PREFIX, createOpenApiDocument } from './openapi'; import { Logger } from 'nestjs-pino'; import { AppModule } from './app.module'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; @@ -21,7 +22,7 @@ export function configureApp(app: INestApplication): void { credentials: true, exposedHeaders: ['X-Request-ID'], }); - app.setGlobalPrefix('api/v1'); + app.setGlobalPrefix(API_PREFIX); app.use(cookieParser()); const requestLogging = app.get(RequestLoggingMiddleware); app.use(requestLogging.use.bind(requestLogging)); @@ -36,13 +37,7 @@ async function bootstrap() { const config = app.get>(ConfigService); if (shouldExposeSwagger(process.env['NODE_ENV'])) { - const document = SwaggerModule.createDocument( - app, - new DocumentBuilder() - .setTitle('WealthLedger API') - .setVersion('v1') - .build(), - ); + const document = createOpenApiDocument(app); SwaggerModule.setup('api/docs', app, document); } diff --git a/src/openapi.ts b/src/openapi.ts new file mode 100644 index 0000000..46e13fb --- /dev/null +++ b/src/openapi.ts @@ -0,0 +1,11 @@ +import { INestApplication } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +export const API_PREFIX = 'api/v1'; + +export function createOpenApiDocument(app: INestApplication) { + return SwaggerModule.createDocument( + app, + new DocumentBuilder().setTitle('WealthLedger API').setVersion('v1').build(), + ); +} diff --git a/test/openapi-contract.e2e-spec.ts b/test/openapi-contract.e2e-spec.ts index c225252..a201c2c 100644 --- a/test/openapi-contract.e2e-spec.ts +++ b/test/openapi-contract.e2e-spec.ts @@ -1,22 +1,12 @@ import { INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; -import { readFile } from 'node:fs/promises'; +import { execFileSync } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import { resolve } from 'node:path'; import { AppModule } from '../src/app.module'; import { configureApp } from '../src/main'; - -function canonicalize(value: unknown): unknown { - if (Array.isArray(value)) return value.map(canonicalize); - if (value && typeof value === 'object') { - return Object.fromEntries( - Object.entries(value) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([key, nestedValue]) => [key, canonicalize(nestedValue)]), - ); - } - return value; -} +import { createOpenApiDocument } from '../src/openapi'; describe('OpenAPI contract (integration)', () => { let app: INestApplication; @@ -32,21 +22,38 @@ describe('OpenAPI contract (integration)', () => { afterAll(() => app.close()); - it('matches the committed frontend OpenAPI snapshot', async () => { - const runtimeDocument = SwaggerModule.createDocument( - app, - new DocumentBuilder() - .setTitle('WealthLedger API') - .setVersion('v1') - .build(), - ); - const snapshot = JSON.parse( - await readFile( - resolve(__dirname, '../../frontend/src/shared/api/spec/openapi.json'), - 'utf8', - ), - ) as unknown; - - expect(canonicalize(runtimeDocument)).toEqual(canonicalize(snapshot)); + it('generates the runtime contract without server startup or deployment configuration', async () => { + const directory = await mkdtemp(resolve(tmpdir(), 'wealthledger-openapi-')); + try { + const networkGuard = resolve(directory, 'no-network.cjs'); + await writeFile( + networkGuard, + ` + const net = require('node:net'); + const forbidden = () => { throw new Error('Schema generation must not use the network'); }; + net.Server.prototype.listen = forbidden; + net.Socket.prototype.connect = forbidden; + `, + ); + execFileSync( + process.execPath, + [ + '--require', + networkGuard, + resolve(__dirname, '../scripts/generate-openapi.cjs'), + ], + { + cwd: directory, + env: { PATH: process.env.PATH }, + timeout: 30_000, + }, + ); + const generated = JSON.parse( + await readFile(resolve(directory, 'openapi.json'), 'utf8'), + ) as unknown; + expect(generated).toEqual(createOpenApiDocument(app)); + } finally { + await rm(directory, { recursive: true, force: true }); + } }); }); -- 2.54.0