3 Commits
Author SHA1 Message Date
ksv741 32c12fb10d 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
2026-09-15 20:45:39 +03:00
ksv741 016782657a docs: simplify conventions layout
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 2m3s
Conventional commits / commits (pull_request) Successful in 24s
Backend security / security (push) Successful in 7s
Backend verify / verify (push) Successful in 2m10s
2026-09-15 05:58:33 +03:00
ksv741 9810d47e42 docs: refine backend conventions and routing
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 2m12s
Conventional commits / commits (pull_request) Successful in 25s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 2m3s
2026-09-15 05:42:16 +03:00
22 changed files with 558 additions and 335 deletions
-25
View File
@@ -1,25 +0,0 @@
# Backend router
Объединяй triggers из смысла задачи, путей и обнаруженных boundaries.
Читай выбранные guides до решения; якорь допускает чтение только нужного раздела.
Просто backend-файл не требует всех conventions.
| Решение / сигнал | Читать |
| --- | --- |
| Новый файл/suffix/layer; поиск feature owner; progressive structure; перенос responsibility | [Architecture](conventions/backend-architecture.md) |
| TS vocabulary, domain literals, magic values | [Domain literals](conventions/backend-architecture.md#domain-literals-and-magic-values) |
| Controller, DTO, route/status, validation/exception, публичная HTTP-семантика | [HTTP API](conventions/backend-http-api.md) |
| Repository/data access; Prisma/schema/query, transaction, migration, atomic write | [Data](conventions/backend-data.md) |
| Тесты или behavior с testing risk | [Testing](conventions/backend-testing.md) |
| JWT, cookie, login/refresh/logout, guard/access, token encryption, sensitive logs | [Security](conventions/security-auth.md) |
| External client/integration boundary; T-Bank/gRPC/proto, retry/rate limit/cache | [Integrations](conventions/backend-integrations.md) |
| Logging/request context без изменения secrets/auth | [Logging](conventions/backend-architecture.md#logging) |
| Новый abstraction/dependency, refactor, неясный scope | [Общий выбор реализации](../../AGENTS.md) |
| Изменение request/response/route/status/error shape или Swagger metadata | HTTP API + [Источник HTTP-контракта](../../docs/architecture/http-api.md#источник-контракта) |
Новый endpoint обычно требует HTTP API + Testing; Architecture/Data/Security
добавляются по новым ответственности, persistence или access rules.
Изменение одного Prisma query без HTTP/auth границы не требует HTTP API/Security.
Auth refresh активирует Security + Data + Testing; HTTP API добавляется, если
меняется controller/HTTP contract. Exact docs/spec sources выбирай через SOURCES.
Повтори routing при расширении diff, включая новые и unstaged файлы.
-19
View File
@@ -1,19 +0,0 @@
# Источники backend
| Что нужно установить | Владелец |
| --- | --- |
| Implementation decision и безопасный pattern | [CONVENTIONS](CONVENTIONS.md) |
| HTTP errors, DTO semantics, public wire shape | [docs: HTTP API](../../docs/architecture/http-api.md); runtime OpenAPI из Nest — единственный editable source |
| Auth/session, account access, token encryption | [Аутентификация](../../docs/domains/authentication/overview.md), [доступ к данным](../../docs/architecture/containers.md#хранимые-данные-и-границы-доступа), [подключение брокера](../../docs/integrations/tbank.md) |
| Prisma schema/data ownership и migration context | [docs: Data](../../docs/architecture/containers.md#хранимые-данные-и-границы-доступа) |
| T-Bank/gRPC/cache, logging | [T-Bank](../../docs/integrations/tbank.md), [свежесть данных](../../docs/architecture/cross-cutting/data-freshness.md), [наблюдаемость](../../docs/architecture/http-api.md#наблюдаемость) |
| Money, account/position/income/cache meanings | [docs: Domain](../../docs/glossary.md) |
| Поведение конкретной feature | [docs: Features](../../docs/README.md#предметные-области) |
| Существующие behavioral requirements при необходимости | [specs index](../../specs/AI_INDEX.md) |
| Versions/scripts и scope автоматических checks | package.json/lockfile, ESLint/TypeScript/Jest/Prisma configs |
SOURCES — карта, не требование прочитать все ссылки. Code показывает реализацию,
ADR — rationale, docs — текущие факты; старый пример не отменяет контракт.
Исследование предлагает варианты, а actual source разрешает выбор. При missing
или conflicting security/data facts сначала установи источник. Для обычной
работы OpenSpec/SDD не требуется.
@@ -1,12 +1,54 @@
# Backend architecture
## Устройство и data flow
NestJS + TypeScript modular monolith. Composition root `AppModule` подключает
бизнес-модули, validated configuration, Prisma и Pino. Общая `configureApp`
задаёт `/api/v1`, CORS/cookies, validation, request logging и error filter.
Точные versions/scripts — в [package.json](../package.json); source code
определяет фактическую реализацию.
```text
HTTP controller → application service/use case → pure domain rules
↓
repository/data access → Prisma → SQLite
↓
integration client → T-Bank gRPC / MOEX / NCR HTTP
```
`modules/auth`, `modules/banking/bank-cards` и области `modules/investments`
владеют пользовательскими возможностями. Сложные модули разделены на
presentation/application/domain/infrastructure; компактные остаются плоскими.
`*.module.ts` — Nest DI, `*.api.ts` — используемые cross-module exports.
Application использует конкретные providers; обязательных ports на каждый
класс нет.
Stocks/bonds — slices одного instruments module: catalog владеет identity/cache,
market — котировками, income — выплатами/личными позициями. Общая orchestration
в `instruments/application` использует DTO/provider records и не становится pure
domain из-за extraction в функцию. Boundary mappers преобразуют внешние records;
чистые расчёты portfolio/banking остаются у domain owner.
PrismaService владеет DB lifecycle. Самостоятельные use cases получают данные
через concrete repositories; существующие связные CRUD/transaction services
сохраняют прямые queries по [data convention](conventions/data.md).
Money — currency + decimal string; расчёты используют точные общие helpers.
Auth управляет identity/session; TbankAccessService проверяет доступ к счёту и
connection generation. Integration clients владеют protocol/deadline/retry;
provider records преобразуются перед чистыми domain calculations.
Пользовательский `tbank-connection` не владеет низкоуровневым gRPC client.
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 доказывают разные части поведения.
Локальные источники выбираются через [Источники](../AGENTS.md#источники).
## Module-first organization
Backend — domain-oriented modular monolith с лёгким разделением
по ответственности. `src/modules` группирует код по бизнес-возможности: `auth`, `banking/bank-cards`, области `investments`.
В инвестициях отдельные Nest modules владеют `brokerage-accounts`, `portfolio`,
`operations`, `income`, `upcoming-events`, `instruments`, `tbank-connection`.
`instruments/stocks` и `instruments/bonds` — slices внутри одного модуля.
Сначала выбери business owner в `modules/`, затем ответственность внутри него.
### Progressive structure
@@ -36,15 +78,6 @@ feature/
Для одного controller или пары DTO дополнительная поддиректория не нужна.
Пустые папки, новые barrels, CQRS и interfaces не требуются для этой организации.
В текущем коде слои используют `auth`, `bank-cards`, `portfolio`, `operations`
и slices `instruments/stocks`, `instruments/bonds`. Общие application helpers
инструментов находятся в `instruments/application`; `instruments.module.ts`
компонует оба slice. `stock-position.ts` и `instrument-position.ts` формируют
результат из portfolio DTO; `instrument-market.ts` обрабатывает внешние данные
и ошибки Nest. Это application code, а не независимая domain model.
`brokerage-accounts`, `income`, `upcoming-events`, `tbank-connection` остаются
компактными плоскими modules. Наличие tests само по себе не требует слоёв.
## Поиск и создание файла
При работе над feature проходи владельцев в порядке:
@@ -116,7 +149,7 @@ Cross-module consumers во всех строках ограничены пуб
`*.decorator.ts`, `*.config.ts`, `*.validation.ts`) сохраняют соответствующие
существующие роли в common/config. `tbank.crypto.ts` — локальная реализация
шифрования токена; имя не является разрешением создавать generic crypto layer.
HTTP/e2e naming и расположение — в [Testing](backend-testing.md).
HTTP/e2e naming и расположение — в [Testing](conventions/testing.md).
Без зафиксированной необходимости не вводи `*.helper.ts`, `*.utils.ts`,
`*.manager.ts`, `*.processor.ts`, `*.handler.ts`, `*.provider.ts`, `*.factory.ts`.
@@ -134,6 +167,10 @@ Tests могут импортировать внутренний owner для п
Новый export требует текущего потребителя; controller и persistence остаются
приватными. Не добавляй `index.ts` на каждом уровне.
Circular dependencies устраняй изменением ownership/composition. `forwardRef()`
не является стандартным исправлением boundaries; исключение требует конкретной
неизбежной связи и проверки DI initialization.
Каждый provider регистрируется у одного владельца. Потребители импортируют
его module, а не повторяют provider: это сохраняет единственный кэш и лимитер.
`AuthModule` экспортирует guard; наличие JWT не заменяет account access check.
@@ -162,37 +199,25 @@ Tests могут импортировать внутренний owner для п
может использовать application types/input и domain rules; новые interfaces
ради инверсии зависимостей не обязательны. Domain не импортирует presentation или
infrastructure. Существующие связные CRUD/transaction services могут сохранять прямые queries
по [Data](backend-data.md); use case получает данные через repository и сам
по [Data](conventions/data.md); use case получает данные через repository и сам
Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется.
DTO остаются транспортными runtime classes; существующее использование DTO
на application boundary допустимо по [HTTP API](backend-http-api.md).
на application boundary допустимо по [HTTP API](conventions/http-api.md).
Сохраняй существующий error contract: HTTP exceptions остаются на application/
transport boundary. Чистому расчёту не нужен новый framework ошибок.
Точные ограничения — [HTTP](backend-http-api.md), [Data](backend-data.md),
[Security](security-auth.md), [Integrations](backend-integrations.md).
Mapping и stable codes — по [Errors](conventions/error-handling.md).
Точные ограничения — [HTTP](conventions/http-api.md), [Data](conventions/data.md),
[Security](conventions/security-auth.md), [Integrations](conventions/integrations.md).
## Сценарии и размер сервисов
Разделяй независимые операции, а не строки. `BankCardsService` отвечает за карты,
`BankOperationsService` — журнал/переводы/возвраты, `ExpenseCategoriesService` —
категории, `GetBankSummaryUseCase` — сводку. `BankCardsRepository` сохраняет
общий transaction retry и owner lookup; денежные транзакции не разрываются.
Чистые расчёты банковских карт находятся в `domain` этого модуля.
Разделяй независимые операции, а не строки. Пример: `BankCardsService` владеет
картами, `BankOperationsService` — журналом/переводами, `GetBankSummaryUseCase` —
сводкой через repository и чистый расчёт. Не разрывай денежную transaction,
общий cache/limiter или pending promises при extraction.
У инструментов `StockCatalogService` / `BondCatalogService` хранят идентичность
листинга и общий кэш slice; market service — котировки/свечи, income service — выплаты/личные позиции.
`GetStockCompanyUseCase` владеет сведениями об эмитенте. Ключи, TTL и pending
promises разделяют прежний экземпляр кэша; extract не должен менять generation.
Portfolio и operations сохраняют связную координацию снимков/страниц и кэшей.
Расчёты портфеля находятся в `portfolio/domain`; преобразование брокерских
позиций — в `tbank-portfolio.mapper.ts`. `bond-money.mapper.ts` преобразует
ответ клиента и не является value object.
Выбирай обычную функцию для чистого вычисления; repository contract — только
для реальной границы. CQRS/buses/events, BaseRepository/BaseService, DI token
для каждого класса, mapper/presenter для каждого DTO и новые dependencies
не нужны для структурного переноса.
Расширение abstractions — по [change scope](conventions/engineering.md#change-scope).
## Common
@@ -205,45 +230,17 @@ protobuf units/nano conversion — T-Bank integration. Domain используе
## Domain literals and magic values
Finite business vocabulary must be modeled explicitly; incidental implementation
literals stay inline. Это относится к error codes, statuses, operation/account/
instrument/transaction types, permissions, roles, feature states и бизнес-режимам.
Перед новым определением найди existing owner/type/value. В branching и comparisons
используй символическое значение владельца; не повторяй raw domain literals.
Храни словарь у domain/module/entity с соблюдением границ слоёв, без глобальной
свалки `constants.ts`. Сохраняй принятый pattern; для нового project-owned словаря
без подходящего аналога используй `as const` с derived union:
```ts
const BankErrorCode = {
InvalidDate: "BANK_INVALID_DATE",
ImmutableOperation: "BANK_IMMUTABLE_OPERATION",
} as const;
type BankErrorCode = (typeof BankErrorCode)[keyof typeof BankErrorCode];
// Вместо code === "BANK_INVALID_DATE":
if (code === BankErrorCode.InvalidDate) { /* existing handling */ }
```
Значимые numeric policies (timeout, retry, page size, threshold, coefficient)
называй по смыслу и единицам: `REQUEST_TIMEOUT_MS`, `CACHE_TTL_SECONDS`,
`MAX_RETRY_COUNT`, `PAGE_SIZE`. Оставляй inline UI labels в принятом месте,
log messages, индексы, `index + 1`, `Math.max(value, 0)`, `page ?? 1`,
`percentage * 100` и очевидные локальные детали без semantic benefit.
При неясном owner/смысле оставь значение и укажи ограничение в review.
Не мигрируй корректные existing enum/union styles массово. Не добавляй ради
правила factories, registries, schemas, loaders, abstraction layers, dependencies,
custom lint/AST rules, generation/validation scripts или тесты существования
констант. Semantic review проверяет ownership, единое написание и неизменность
wire values; обычные behavior tests проверяют поведение.
Общий выбор symbolic values и числовых policies —
[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` допустим для заполняемых извне
DTO fields, не для сокрытия неинициализированного owned state.
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
@@ -255,14 +252,8 @@ semantic magic strings проверяются conventions/review без ново
## Logging
Единственный logger — nestjs-pino; application events используют существующий
[StructuredLogger](../../src/common/logging/structured-logger.service.ts) и
request context. Выбирай стабильное имя события и разрешённые scalar fields.
Wrapper отбрасывает неизвестные поля, но это не разрешение помещать secret
в `message`/`stack`: review sensitive logging обязателен.
Не добавляй второй logger или сырой request/body/error payload.
Источник rationale — [ADR 008](../../../docs/adr/008-pino-backend-logging.md);
при secrets/user data читай также Security.
События, levels, correlation и safe fields — по
[Observability](conventions/observability.md).
## Проверка
-58
View File
@@ -1,58 +0,0 @@
# Backend HTTP API
## Выбор boundary
Сначала определи, меняется wire contract или только реализация существующего
use case. Для route/request/response/status/error shape читай
[HTTP semantics и источник контракта](../../../docs/architecture/http-api.md).
| Проверка | Где выполнять |
| --- | --- |
| Тип/формат/диапазон входа | Runtime DTO class с class-validator/class-transformer |
| Согласованность полей, бизнес-ограничение | Feature service/domain helper |
| Session и identity | Existing guard/strategy |
| Доступ user к конкретному account | Existing access service |
| HTTP envelope/безопасная ошибка | Общий HttpExceptionFilter |
Controller объявляет contract и делегирует; не обращается напрямую к Prisma/gRPC
и не выполняет финансовые расчёты или сложную оркестрацию.
Controller вызывает application use case/service. Identity берётся из verified
`request.user`, не из body/userId, предоставленного клиентом. Access rules активируют Security.
## DTO и errors
DTO — runtime class: TS interface не предоставляет validation metadata.
`*.controller.ts` и `*.dto.ts` остаются около HTTP boundary, в root компактного
module или presentation. DTO можно передать application boundary при совпадении
формы; отдельный Input с теми же полями не нужен. Domain не импортирует DTO:
например, чистые денежные расчёты используют `Money`, а Swagger — `MoneyDto`.
Нормализованная форма input нужна только при смысловом отличии от wire contract;
выбор suffix принадлежит [Architecture](backend-architecture.md).
ValidationPipe уже настроен в `configureApp` (`transform`, `whitelist`).
Не обещай rejection неизвестных fields: `forbidNonWhitelisted` не включён.
Для query numbers/arrays выбирай явное преобразование и валидаторы, как в
[operations DTO](../../src/modules/investments/operations/presentation/brokerage-operation.dto.ts). Swagger metadata
описывает те же required/optional/nullable/enum/range значения.
Используй типизированные response DTO, `ApiOkResponse`/другой фактический status
и существующий `ApiProblemResponses`. При новых ошибках задай публичный stable
`code`; сообщение внутреннего exception не является UI contract. Не возвращай
raw upstream error, Prisma record с secrets или stack. Текущая форма ошибок
описана у HTTP owner; не добавляй поля RFC только потому, что они встречаются в tutorial.
**Безопасный путь:** используй exception и общий filter, а не локальный
`res.status(...).json(...)`. Исключение — транспортная операция вроде cookie;
следуй существующему auth controller с `passthrough` и явно заданным status.
## Пример и проверка
[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 не исправляется вручную.
Проверь 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](backend-testing.md). Финальный gate не заменяет semantic review ошибок.
@@ -15,13 +15,20 @@ PrismaService — существующий shared DB provider с lifecycle пр
| Независимые reads | Не добавляй transaction ради внешнего вида |
| Общая persistence abstraction | Только при реальной общей ответственности; generic repository для одной Prisma реализации не нужен |
Размещение repository определяется [Architecture](backend-architecture.md):
Размещение repository определяется [Architecture](../architecture.md):
в слоистом module это `infrastructure/`.
Repository — конкретный класс `*.repository.ts` у business owner, без обязательных
interface/port/adapter. Он владеет queries, includes/filtering, DB mapping и
согласованным чтением. Суффиксы `*.persistence.ts`, `*.store.ts`, `*.dao.ts`
для той же роли не используем. Nest module регистрирует repository приватно.
Новые business decisions (можно ли менять архивную карту, допустим ли перевод)
принадлежат application/domain; repository сохраняет persistence и ownership
ограничения. Existing checks/HTTP helpers в BankCardsRepository — migration
candidate, не повод дублировать их при новом use case или разрывать transaction.
`BaseRepository<T>`, `GenericRepository<T>` и RepositoryFactory поверх Prisma
не нужны без отдельной общей ответственности.
`BankCardsRepository.summaryData` проверяет владельца карты и получает операции
с категориями в одной read transaction. `GetBankSummaryUseCase` выбирает период,
вызывает repository, затем чистый `bankSummary` и формирует ответ. Prisma records
@@ -44,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
@@ -66,12 +73,12 @@ 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.
## Проверка
`npx prisma validate`; после schema change `npx prisma generate` и
`npm run typecheck:app`. Invariants/atomicity/migration safety требуют tests
на соответствующей boundary: mock Prisma не доказывает реальную transaction.
Выбор integration/e2e и известные ограничения — в [Testing](backend-testing.md).
Выбор integration/e2e и известные ограничения — в [Testing](testing.md).
+74
View File
@@ -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, раздел «Релизы».
+40
View File
@@ -0,0 +1,40 @@
# Backend errors
## Поток
```text
application/domain outcome → application mapping → HttpExceptionFilter
→ application/problem+json
```
Сохраняй принятый профиль RFC 9457: `status`, стабильный публичный `code`,
для validation — `errors: [{ pointer, detail }]`. Точный контракт и известное
расхождение validation описаны в [HTTP API](http-api.md#источник-контракта).
Не добавляй `type/title/instance` как обязательные поля без contract change.
Чистый domain не импортирует Nest HTTP exceptions. Существующий application
service может бросить подходящий `HttpException` с public code; не нужен новый
error framework/registry. Если domain outcome требует HTTP interpretation,
маппинг принадлежит application/transport boundary.
## Codes и безопасность
- Используй existing code владельца, не сравнивай внутренние сообщения.
Новое значение имеет один symbolic owner; metadata/validation и consumers
согласуются по [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.
- Неожиданный exception → `500 / INTERNAL_SERVER_ERROR`, без message/stack,
SQL, credentials или raw upstream payload в ответе.
- External auth failure и временная недоступность имеют разные outcomes;
translation выполняется на existing integration/access boundary.
- Логи используют [observability](observability.md). Redaction не
делает произвольное сообщение ошибки безопасным.
## Проверка
Для изменённого кода проверь observable status/code и сохранность данных.
Для validation нужен request через `configureApp`: fabricated `ValidationError`
в unit test filter не доказывает форму, выходящую из реального ValidationPipe.
OpenAPI parity проверяет schema, а не response pipeline; [Testing](testing.md).
+88
View File
@@ -0,0 +1,88 @@
# Backend HTTP API
## Выбор boundary
Сначала определи, меняется wire contract или только реализация существующего
use case. Для route/request/response/status/error shape используй
[локальный публичный контракт](#источник-контракта) и configured HTTP tests.
| Проверка | Где выполнять |
| --- | --- |
| Тип/формат/диапазон входа | Runtime DTO class с class-validator/class-transformer |
| Согласованность полей, бизнес-ограничение | Feature service/domain helper |
| Session и identity | Existing guard/strategy |
| Доступ user к конкретному account | Existing access service |
| HTTP envelope/безопасная ошибка | Общий HttpExceptionFilter |
Controller объявляет contract и делегирует; не обращается напрямую к Prisma/gRPC
и не выполняет финансовые расчёты или сложную оркестрацию.
Controller вызывает application use case/service. Identity берётся из verified
`request.user`, не из body/userId, предоставленного клиентом. Access rules активируют Security.
## DTO и errors
DTO — runtime class: TS interface не предоставляет validation metadata.
`*.controller.ts` и `*.dto.ts` остаются около HTTP boundary, в root компактного
module или presentation. DTO можно передать application boundary при совпадении
формы; отдельный Input с теми же полями не нужен. Domain не импортирует DTO:
например, чистые денежные расчёты используют `Money`, а Swagger — `MoneyDto`.
Нормализованная форма input нужна только при смысловом отличии от wire contract;
выбор suffix принадлежит [Architecture](../architecture.md).
ValidationPipe уже настроен в `configureApp` (`transform`, `whitelist`).
Не обещай rejection неизвестных fields: `forbidNonWhitelisted` не включён.
Для query numbers/arrays выбирай явное преобразование и валидаторы, как в
[operations DTO](../../src/modules/investments/operations/presentation/brokerage-operation.dto.ts). Swagger metadata
описывает те же required/optional/nullable/enum/range значения.
Используй типизированные response DTO, `ApiOkResponse`/другой фактический status
и существующий `ApiProblemResponses`. Prisma model не является public contract:
response выбирает поля явно. Stable codes, RFC 9457 профиль и mapping принадлежат
[Errors](error-handling.md).
**Безопасный путь:** используй exception и общий filter, а не локальный
`res.status(...).json(...)`. Исключение — транспортная операция вроде cookie;
следуй существующему auth controller с `passthrough` и явно заданным status.
## Пример и проверка
Prefix `/api/v1` задаёт `configureApp`. Route/status/request/response/OpenAPI
change — изменение публичного backend contract; несовместимое изменение требует
согласованного перехода consumers, а не незаметной правки DTO. Новый API version
не вводи для внутреннего refactor.
Pagination сохраняет contract endpoint: operations использует opaque cursor,
limit и DTO bounds. Не заменяй cursor offset/page и не добавляй global pagination
helper ради первого endpoint. Новая pagination явно определяет порядок,
границу страницы и end-of-list; reused cursor не должен менять filters/owner.
[Operations controller](../../src/modules/investments/operations/presentation/brokerage-operations.controller.ts)
показывает guard, user.sub, DTO query и service delegation.
Для нового 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.
@@ -2,12 +2,12 @@
## Выбор boundary
T-Bank gRPC принадлежит TbankClient; MOEX metadata — MoexClient. Сервисы use case
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` — технические
@@ -17,7 +17,7 @@ RPC, credentials/access и resilience. Эти владельцы не объед
`*.client.ts` содержит transport и его retries; `*.api.ts` только перечисляет
публичные exports. Все шесть `auth/operations/portfolio/tbank/moex/ncr.api.ts`
имеют одинаковую роль программной границы, определённую в
[Architecture](backend-architecture.md). Логика, DTO definitions и HTTP routes
[Architecture](../architecture.md). Логика, DTO definitions и HTTP routes
в эти файлы не помещаются. Type exports обозначаются `export type`.
Преобразование provider records происходит на boundary: например,
@@ -30,7 +30,7 @@ client или mapper для простого копирования полей.
Для нового account use case сначала используй TbankAccessService: access,
connection generation и invalid-token translation не дублируются в callers.
Mapper внешних финансовых значений остаётся внутри business capability,
обычно в infrastructure по [Architecture](backend-architecture.md).
обычно в infrastructure по [Architecture](../architecture.md).
## Retry, cache и protocol
@@ -38,13 +38,19 @@ 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; не приписывай им
gRPC retry policy. При изменении upstream сначала прочитай конкретный client.
При 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
@@ -64,4 +70,4 @@ Generated artifacts, если есть, создавай из source, без р
upstream responses. Проверь queue/deadline/retry limit, token translation,
cache isolation и late response для затронутых ветвей. Fixtures синтетические;
реальная сеть, секреты и пользовательские accounts не нужны.
Logging использует [existing logger](backend-architecture.md#logging).
Logging использует [existing logger](../architecture.md#logging).
+22
View File
@@ -0,0 +1,22 @@
# Observability
- Единственный logger — nestjs-pino. Application events используют
[StructuredLogger](../../src/common/logging/structured-logger.service.ts),
стабильное имя события и разрешённые scalar fields. `console.log` и второй
logger в production не используются.
- `info` — значимый lifecycle/outcome; `debug` — подробности успешного RPC;
`warn` — восстановимая проблема/retry; `error` — окончательный сбой.
Не добавляй log на каждый вход/выход функции или дубликат HTTP completion.
- RequestContextService и request middleware владеют correlation: серверный
request ID возвращается как `X-Request-ID`, входящий client ID хранится отдельно.
Передавай существующий context, не создавай собственный UUID в каждом service.
- Wrapper ограничивает поля, Pino redacts request secrets. Не логируй passwords,
tokens, cookie, Authorization, credentials, connection generation, raw body
или upstream error. Secret в `message/stack` не становится безопасным из-за
allowlist; правила [Security](security-auth.md) обязательны для sensitive data.
- Для ошибки используй public code и безопасный context операции/попыток;
[error mapping](error-handling.md) не отдаёт внутренние детали клиенту.
Проверяй новые поля/redaction/correlation existing logging tests; не добавляй
тест на каждую строку log. Existing ESLint не запрещает все вызовы `console`
отдельным правилом: до настройки встроенного `no-console` это semantic review.
+21 -11
View File
@@ -10,20 +10,23 @@ 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
Клиентский интерфейс не является security boundary. DTO runtime validation выполняется
server-side; whitelist не заменяет access checks. Environment/secret inputs
проверяет existing config owner; реальные credentials не попадают в fixtures.
Verified JWT не доказывает доступ к accountId из URL. Используй user.sub и
existing account access policy; не доверяй userId/token из клиента как authority.
Cross-user, invalid/expired/superseded token и concurrent requests — обязательные
@@ -32,13 +35,20 @@ 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. Не копируй требования о нормализации
или длине из tutorial: сверяй runtime DTO, tests и feature source.
Предпочитай Prisma query API. Raw SQL допускается только по конкретной нужде
с параметризованными значениями; не конкатенируй untrusted input и не передавай
его в `queryRawUnsafe`/`executeRawUnsafe`. Dynamic identifiers выбирай из
закрытого server-owned набора. Проверка user ownership сохраняется и в SQL.
## Безопасный путь и проверка
При неясной ownership/crypto/session семантике прочитай exact source до edits.
@@ -46,6 +56,6 @@ cache invalidation при замене/deletion и ограничение key ro
фиктивным success или log. Logger allowlist/redaction не делает произвольный
message безопасным.
Targeted auth/access/crypto/logging tests + [Testing](backend-testing.md).
Cookie/guard/HTTP changes требуют configured HTTP proof; frontend Bearer/retry
проверяется MSW. Schema parity не доказывает отсутствие утечки secret.
Targeted auth/access/crypto/logging tests + [Testing](testing.md).
Cookie/guard/HTTP changes требуют configured HTTP proof.
Schema parity не доказывает отсутствие утечки secret.
@@ -1,5 +1,9 @@
# Backend testing
Перед нетривиальными тестами сформулируй
[общий Test Intent](engineering.md#test-intent); отдельный документ
не требуется. Trivial low-risk изменения обходятся без него.
## Выбор уровня
Проверяй риски доступа, денежных расчётов, целостности данных, конкурентных
@@ -11,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 |
@@ -25,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
@@ -40,8 +44,8 @@ Unit specs colocated в `src/**/*.spec.ts`; HTTP/integration — `test/`.
`npm run test:e2e` запускает все HTTP suites через изолированный SQLite runner;
`npm run test:sqlite` — только auth/cards. Каждая suite получает новую временную
базу со штатными migrations. Прямой Jest без runner отклоняется до DB-запросов.
`prisma.config.ts` исключает влияние унаследованного `RUST_LOG=warn/error`,
вызывающего ошибку fresh migration на Prisma 7.9.1. Подробности и регрессия запуска —
`prisma.config.ts` исключает влияние унаследованного `RUST_LOG=warn/error`
на fresh migration; точная Prisma version принадлежит manifest/lockfile. Подробности и регрессия запуска —
[Backend README](../../README.md#реальная-sqlite-и-сохранность-данных).
HTTP suites требуют разрешения открывать локальные порты. Ошибка sandbox
не считается успешным тестом; повторите проверку в подходящем окружении.
-3
View File
@@ -1,3 +0,0 @@
{
"frontend": "0244acf429c525b23117f165bbe52ef5403eefd3"
}
+3 -29
View File
@@ -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
+3
View File
@@ -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
+33 -25
View File
@@ -1,30 +1,38 @@
# Backend
# Backend router
NestJS API; общие правила работы — в root `AGENTS.md`.
До backend-решения прочитай [router](.ai/CONVENTIONS.md) и
[sources](.ai/SOURCES.md), затем только применимые документы/разделы.
Репозиторий самостоятельный: обязательные инструкции и проверки используют
только его файлы и предоставленные публичные контракты. Перед работой прочитай
[порядок работы](.ai/conventions/engineering.md#граница-и-порядок-работы), затем
объединяй triggers задачи и путей; читай активированные темы до решения.
## Проверка
| Решение / сигнал | Читать |
| --- | --- |
| Устройство/data flow, module/layer/public API/DI/suffix, owner | [Architecture](.ai/architecture.md) |
| TS vocabulary, domain literals, magic values | [Types/values](.ai/architecture.md#domain-literals-and-magic-values) |
| Controller/DTO/validation, route/status/response, OpenAPI | [HTTP API](.ai/conventions/http-api.md) |
| Application/domain/HTTP error, exception, code | [Errors](.ai/conventions/error-handling.md) |
| Repository/Prisma/query, transaction, migration, atomic write | [Data](.ai/conventions/data.md) |
| Tests или behavior с regression risk | [Testing](.ai/conventions/testing.md) |
| 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](.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#проверки-и-завершение) |
Перед Node-командами: `nvm use --silent` по `.nvmrc`.
После lockfile install `npm ci` полный gate — `npm run verify`:
zero-warning ESLint, application/test TypeScript, Jest и build.
`lint:fix`/`format` пишут файлы и запускаются отдельно от доказательства проверки.
`test:cov` — диагностика без общего threshold; scripts принадлежат `package.json`.
Новый endpoint обычно требует HTTP API + Testing; Data/Security добавляются
по persistence/access trigger. Auth refresh активирует Security + Data + Testing.
Перед завершением повтори routing по новым и изменённым файлам. Для Git/release
прочитай [Git и релизы](.ai/conventions/engineering.md#git-и-релизы).
HTTP contract changes сохраняют [направление контракта](../docs/architecture/http-api.md#источник-контракта)
и проходят contract check. Общий e2e запускается на отдельных временных DB;
применимость и маршрут проверки
описаны в [backend testing](.ai/conventions/backend-testing.md).
Generated Prisma client не редактируется вручную. Credentials, авторизация
доступа к счетам и внешние интеграции остаются server-side.
## Источники
ESLint/TypeScript не доказывают правильность domain boundaries, security или
денежных вычислений: для них нужны semantic review и risk-based tests.
## Ветки и релизы
Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую
не пушить. Release/hotfix и синхронизация веток — по [root README](../README.md#релизы-и-ветки).
Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`.
Перед подготовкой релиза прочитай root README; версии вручную не повышать.
| Вопрос | Локальный источник |
| --- | --- |
| 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 |
+67 -20
View File
@@ -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,
@@ -78,25 +77,39 @@ runtime-валидацию. В обычном коде нужны инициал
## Документация
- [Router conventions](.ai/CONVENTIONS.md) и [источники](.ai/SOURCES.md)
- [Backend architecture](.ai/conventions/backend-architecture.md)
- [HTTP API](.ai/conventions/backend-http-api.md), [data](.ai/conventions/backend-data.md) и [testing](.ai/conventions/backend-testing.md)
- [Security/auth](.ai/conventions/security-auth.md) и [integrations](.ai/conventions/backend-integrations.md)
- [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)
- [Карта проектных фактов](../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
View File
@@ -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",
+40
View File
@@ -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
View File
@@ -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);
}
+11
View File
@@ -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(),
);
}
+37 -30
View File
@@ -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 });
}
});
});