feat: generate OpenAPI without peer repositories
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 2m28s
Conventional commits / commits (pull_request) Successful in 25s
Backend security / security (push) Successful in 6s
Backend verify / verify (push) Successful in 2m16s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 2m28s
Conventional commits / commits (pull_request) Successful in 25s
Backend security / security (push) Successful in 6s
Backend verify / verify (push) Successful in 2m16s
This commit was merged in pull request #19.
This commit is contained in:
+6
-6
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
## Проверка
|
||||
|
||||
|
||||
@@ -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/<task>` от актуального `develop`, затем PR в `develop`.
|
||||
Сохраняй чужие изменения; перед commit просмотри staged paths и diff.
|
||||
Commits и PR titles — Conventional Commits; допустим `spec:`. Каждый commit
|
||||
содержит одну связанную задачу. Root другого checkout не управляет этим repo.
|
||||
Прямой push в `main`, force-push и перенос опубликованных тегов не входят в
|
||||
обычную работу. Перед release/hotfix прочитай местный README, раздел «Релизы».
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"frontend": "0244acf429c525b23117f165bbe52ef5403eefd3"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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/<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 не включены.
|
||||
|
||||
+3
-2
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
+4
-9
@@ -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<Environment, true>>(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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user