Compare commits
12
Commits
d4fc28cbbf
...
v0.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
386c91670e | ||
|
|
9437b6a311 | ||
|
|
0ec7ae51af | ||
|
|
1946192be9 | ||
|
|
0557f4ad1f | ||
|
|
d7cb7e3e20 | ||
|
|
1a6be432f2 | ||
|
|
32c12fb10d | ||
|
|
016782657a | ||
|
|
9810d47e42 | ||
|
|
660b250ecd | ||
|
|
f01b82d366 |
@@ -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 файлы.
|
|
||||||
@@ -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
|
# 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
|
## Module-first organization
|
||||||
|
|
||||||
Backend — domain-oriented modular monolith с лёгким разделением
|
Сначала выбери business owner в `modules/`, затем ответственность внутри него.
|
||||||
по ответственности. `src/modules` группирует код по бизнес-возможности: `auth`, `banking/bank-cards`, области `investments`.
|
|
||||||
В инвестициях отдельные Nest modules владеют `brokerage-accounts`, `portfolio`,
|
|
||||||
`operations`, `income`, `upcoming-events`, `instruments`, `tbank-connection`.
|
|
||||||
`instruments/stocks` и `instruments/bonds` — slices внутри одного модуля.
|
|
||||||
|
|
||||||
### Progressive structure
|
### Progressive structure
|
||||||
|
|
||||||
@@ -36,15 +78,6 @@ feature/
|
|||||||
Для одного controller или пары DTO дополнительная поддиректория не нужна.
|
Для одного controller или пары DTO дополнительная поддиректория не нужна.
|
||||||
Пустые папки, новые barrels, CQRS и interfaces не требуются для этой организации.
|
Пустые папки, новые 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 проходи владельцев в порядке:
|
При работе над feature проходи владельцев в порядке:
|
||||||
@@ -116,7 +149,7 @@ Cross-module consumers во всех строках ограничены пуб
|
|||||||
`*.decorator.ts`, `*.config.ts`, `*.validation.ts`) сохраняют соответствующие
|
`*.decorator.ts`, `*.config.ts`, `*.validation.ts`) сохраняют соответствующие
|
||||||
существующие роли в common/config. `tbank.crypto.ts` — локальная реализация
|
существующие роли в common/config. `tbank.crypto.ts` — локальная реализация
|
||||||
шифрования токена; имя не является разрешением создавать generic crypto layer.
|
шифрования токена; имя не является разрешением создавать generic crypto layer.
|
||||||
HTTP/e2e naming и расположение — в [Testing](backend-testing.md).
|
HTTP/e2e naming и расположение — в [Testing](conventions/testing.md).
|
||||||
|
|
||||||
Без зафиксированной необходимости не вводи `*.helper.ts`, `*.utils.ts`,
|
Без зафиксированной необходимости не вводи `*.helper.ts`, `*.utils.ts`,
|
||||||
`*.manager.ts`, `*.processor.ts`, `*.handler.ts`, `*.provider.ts`, `*.factory.ts`.
|
`*.manager.ts`, `*.processor.ts`, `*.handler.ts`, `*.provider.ts`, `*.factory.ts`.
|
||||||
@@ -134,6 +167,10 @@ Tests могут импортировать внутренний owner для п
|
|||||||
Новый export требует текущего потребителя; controller и persistence остаются
|
Новый export требует текущего потребителя; controller и persistence остаются
|
||||||
приватными. Не добавляй `index.ts` на каждом уровне.
|
приватными. Не добавляй `index.ts` на каждом уровне.
|
||||||
|
|
||||||
|
Circular dependencies устраняй изменением ownership/composition. `forwardRef()`
|
||||||
|
не является стандартным исправлением boundaries; исключение требует конкретной
|
||||||
|
неизбежной связи и проверки DI initialization.
|
||||||
|
|
||||||
Каждый provider регистрируется у одного владельца. Потребители импортируют
|
Каждый provider регистрируется у одного владельца. Потребители импортируют
|
||||||
его module, а не повторяют provider: это сохраняет единственный кэш и лимитер.
|
его module, а не повторяют provider: это сохраняет единственный кэш и лимитер.
|
||||||
`AuthModule` экспортирует guard; наличие JWT не заменяет account access check.
|
`AuthModule` экспортирует guard; наличие JWT не заменяет account access check.
|
||||||
@@ -162,37 +199,25 @@ Tests могут импортировать внутренний owner для п
|
|||||||
может использовать application types/input и domain rules; новые interfaces
|
может использовать application types/input и domain rules; новые interfaces
|
||||||
ради инверсии зависимостей не обязательны. Domain не импортирует presentation или
|
ради инверсии зависимостей не обязательны. Domain не импортирует presentation или
|
||||||
infrastructure. Существующие связные CRUD/transaction services могут сохранять прямые queries
|
infrastructure. Существующие связные CRUD/transaction services могут сохранять прямые queries
|
||||||
по [Data](backend-data.md); use case получает данные через repository и сам
|
по [Data](conventions/data.md); use case получает данные через repository и сам
|
||||||
Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется.
|
Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется.
|
||||||
DTO остаются транспортными runtime classes; существующее использование DTO
|
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/
|
Сохраняй существующий error contract: HTTP exceptions остаются на application/
|
||||||
transport boundary. Чистому расчёту не нужен новый framework ошибок.
|
transport boundary. Чистому расчёту не нужен новый framework ошибок.
|
||||||
Точные ограничения — [HTTP](backend-http-api.md), [Data](backend-data.md),
|
Mapping и stable codes — по [Errors](conventions/error-handling.md).
|
||||||
[Security](security-auth.md), [Integrations](backend-integrations.md).
|
Точные ограничения — [HTTP](conventions/http-api.md), [Data](conventions/data.md),
|
||||||
|
[Security](conventions/security-auth.md), [Integrations](conventions/integrations.md).
|
||||||
|
|
||||||
## Сценарии и размер сервисов
|
## Сценарии и размер сервисов
|
||||||
|
|
||||||
Разделяй независимые операции, а не строки. `BankCardsService` отвечает за карты,
|
Разделяй независимые операции, а не строки. Пример: `BankCardsService` владеет
|
||||||
`BankOperationsService` — журнал/переводы/возвраты, `ExpenseCategoriesService` —
|
картами, `BankOperationsService` — журналом/переводами, `GetBankSummaryUseCase` —
|
||||||
категории, `GetBankSummaryUseCase` — сводку. `BankCardsRepository` сохраняет
|
сводкой через repository и чистый расчёт. Не разрывай денежную transaction,
|
||||||
общий transaction retry и owner lookup; денежные транзакции не разрываются.
|
общий cache/limiter или pending promises при extraction.
|
||||||
Чистые расчёты банковских карт находятся в `domain` этого модуля.
|
|
||||||
|
|
||||||
У инструментов `StockCatalogService` / `BondCatalogService` хранят идентичность
|
Расширение abstractions — по [change scope](conventions/engineering.md#change-scope).
|
||||||
листинга и общий кэш 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
|
|
||||||
не нужны для структурного переноса.
|
|
||||||
|
|
||||||
## Common
|
## Common
|
||||||
|
|
||||||
@@ -205,45 +230,17 @@ protobuf units/nano conversion — T-Bank integration. Domain используе
|
|||||||
|
|
||||||
## Domain literals and magic values
|
## Domain literals and magic values
|
||||||
|
|
||||||
Finite business vocabulary must be modeled explicitly; incidental implementation
|
Общий выбор symbolic values и числовых policies —
|
||||||
literals stay inline. Это относится к error codes, statuses, operation/account/
|
[Domain values](conventions/engineering.md#domain-values).
|
||||||
instrument/transaction types, permissions, roles, feature states и бизнес-режимам.
|
Сохраняй strict TS и explicit boundary types. Для недоверенного input выбирай
|
||||||
|
`unknown` + runtime narrowing; отключённый lint `no-explicit-any` не делает
|
||||||
Перед новым определением найди existing owner/type/value. В branching и comparisons
|
`any` нормой domain/application model. `declare` допустим для заполняемых извне
|
||||||
используй символическое значение владельца; не повторяй raw domain literals.
|
DTO fields, не для сокрытия неинициализированного owned state.
|
||||||
Храни словарь у 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 проверяют поведение.
|
|
||||||
|
|
||||||
OpenAPI enum metadata и runtime validation должны читать тот же vocabulary,
|
OpenAPI enum metadata и runtime validation должны читать тот же vocabulary,
|
||||||
что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain,
|
что application/domain. Например, `BankOperationKind` принадлежит bank-cards/domain,
|
||||||
а DTO получает допустимые значения через `Object.values(BankOperationKind)`.
|
а DTO получает допустимые значения через `Object.values(BankOperationKind)`.
|
||||||
Frontend переиспользует generated OpenAPI types/values по возможностям генератора,
|
Публичные enum values входят в OpenAPI artifact. Внешние/generated enums
|
||||||
а не определяет независимый backend-owned словарь. Внешние/generated enums
|
|
||||||
сохраняй; generated Prisma/protocol code вручную не меняй.
|
сохраняй; generated Prisma/protocol code вручную не меняй.
|
||||||
|
|
||||||
`@typescript-eslint/no-magic-numbers` включён как error; базовое ESLint rule
|
`@typescript-eslint/no-magic-numbers` включён как error; базовое ESLint rule
|
||||||
@@ -255,14 +252,8 @@ semantic magic strings проверяются conventions/review без ново
|
|||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
|
|
||||||
Единственный logger — nestjs-pino; application events используют существующий
|
События, levels, correlation и safe fields — по
|
||||||
[StructuredLogger](../../src/common/logging/structured-logger.service.ts) и
|
[Observability](conventions/observability.md).
|
||||||
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.
|
|
||||||
|
|
||||||
## Проверка
|
## Проверка
|
||||||
|
|
||||||
@@ -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 ради внешнего вида |
|
| Независимые reads | Не добавляй transaction ради внешнего вида |
|
||||||
| Общая persistence abstraction | Только при реальной общей ответственности; generic repository для одной Prisma реализации не нужен |
|
| Общая persistence abstraction | Только при реальной общей ответственности; generic repository для одной Prisma реализации не нужен |
|
||||||
|
|
||||||
Размещение repository определяется [Architecture](backend-architecture.md):
|
Размещение repository определяется [Architecture](../architecture.md):
|
||||||
в слоистом module это `infrastructure/`.
|
в слоистом module это `infrastructure/`.
|
||||||
Repository — конкретный класс `*.repository.ts` у business owner, без обязательных
|
Repository — конкретный класс `*.repository.ts` у business owner, без обязательных
|
||||||
interface/port/adapter. Он владеет queries, includes/filtering, DB mapping и
|
interface/port/adapter. Он владеет queries, includes/filtering, DB mapping и
|
||||||
согласованным чтением. Суффиксы `*.persistence.ts`, `*.store.ts`, `*.dao.ts`
|
согласованным чтением. Суффиксы `*.persistence.ts`, `*.store.ts`, `*.dao.ts`
|
||||||
для той же роли не используем. Nest module регистрирует repository приватно.
|
для той же роли не используем. 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` проверяет владельца карты и получает операции
|
`BankCardsRepository.summaryData` проверяет владельца карты и получает операции
|
||||||
с категориями в одной read transaction. `GetBankSummaryUseCase` выбирает период,
|
с категориями в одной read transaction. `GetBankSummaryUseCase` выбирает период,
|
||||||
вызывает repository, затем чистый `bankSummary` и формирует ответ. Prisma records
|
вызывает repository, затем чистый `bankSummary` и формирует ответ. Prisma records
|
||||||
@@ -44,16 +51,16 @@ application checks. Предварительный lookup не заменяет
|
|||||||
Transaction не охватывает сетевой T-Bank вызов как будто это одна atomic DB
|
Transaction не охватывает сетевой T-Bank вызов как будто это одна atomic DB
|
||||||
операция. Планируй порядок и compensation по текущей задаче; не удерживай DB
|
операция. Планируй порядок и compensation по текущей задаче; не удерживай DB
|
||||||
lock во время retry внешней сети. Access/cache generation semantics — у
|
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,
|
Для денежных значений используй existing decimal helpers и строковый wire value,
|
||||||
не JS floating-point арифметику для сумм. Currency — отдельная часть Money;
|
не JS floating-point арифметику для сумм. Currency — отдельная часть Money;
|
||||||
разные валюты не складываются без явно заданного conversion contract.
|
разные валюты не складываются без явно заданного conversion contract.
|
||||||
Источник: [Money](../../../docs/glossary.md#money),
|
Источник формы — `src/common/money.types.ts`;
|
||||||
пример: [decimal.ts](../../src/common/decimal.ts). UI formatting не меняет
|
пример точной арифметики: [decimal.ts](../../src/common/decimal.ts).
|
||||||
backend ownership расчётов и доменную семантику.
|
Форматирование ответа не меняет доменную семантику и точность расчёта.
|
||||||
|
|
||||||
## Schema и migration
|
## Schema и migration
|
||||||
|
|
||||||
@@ -66,12 +73,12 @@ client обновляй из schema. Не переписывай уже прим
|
|||||||
|
|
||||||
**Безопасный путь:** additive изменение, заполнение/проверка данных и только затем
|
**Безопасный путь:** additive изменение, заполнение/проверка данных и только затем
|
||||||
удаление прежней формы, если задача этого требует. Примеры schema/migrations:
|
удаление прежней формы, если задача этого требует. Примеры schema/migrations:
|
||||||
[Prisma source](../../prisma/schema.prisma); исторический rationale —
|
[Prisma source](../../prisma/schema.prisma). SQLite — embedded storage текущего
|
||||||
[ADR 001](../../../docs/adr/001-prisma-sqlite-local-mvp.md), не текущий список таблиц.
|
приложения; файл БД и encryption key требуют согласованного backup/restore.
|
||||||
|
|
||||||
## Проверка
|
## Проверка
|
||||||
|
|
||||||
`npx prisma validate`; после schema change `npx prisma generate` и
|
`npx prisma validate`; после schema change `npx prisma generate` и
|
||||||
`npm run typecheck:app`. Invariants/atomicity/migration safety требуют tests
|
`npm run typecheck:app`. Invariants/atomicity/migration safety требуют tests
|
||||||
на соответствующей boundary: mock Prisma не доказывает реальную transaction.
|
на соответствующей boundary: mock Prisma не доказывает реальную transaction.
|
||||||
Выбор integration/e2e и известные ограничения — в [Testing](backend-testing.md).
|
Выбор integration/e2e и известные ограничения — в [Testing](testing.md).
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Работа в репозитории
|
||||||
|
|
||||||
|
## Граница и порядок работы
|
||||||
|
|
||||||
|
Репозиторий сам содержит инструкции, исходники, конфигурацию и входы своих
|
||||||
|
проверок. Сборка, CI и обязательный маршрут чтения работают без соседних
|
||||||
|
checkout. Внешний API задаётся предоставленным публичным контрактом;
|
||||||
|
исходники или инструкции его реализации не являются зависимостью разработки.
|
||||||
|
При нехватке требования сообщи, какое решение не определено, до его реализации.
|
||||||
|
|
||||||
|
Начни с результата и affected areas. До edits проследи flow/invariants, найди
|
||||||
|
owner и 1–3 примера. Код показывает механизм; случайный аналог не меняет контракт.
|
||||||
|
Работай напрямую; OpenSpec/SDD lifecycle — только по явному запросу.
|
||||||
|
Перед Node-командами активируй местную `.nvmrc`: `nvm use --silent`.
|
||||||
|
Сохраняй чужие staged/unstaged/untracked изменения и read-only архивы.
|
||||||
|
|
||||||
|
## Change scope
|
||||||
|
|
||||||
|
Сначала расширь существующего владельца. Иначе используй standard library,
|
||||||
|
платформу или установленную dependency; небольшая новая реализация допустима
|
||||||
|
при собственной ответственности. Не создавай helper/interface/wrapper ради
|
||||||
|
возможного reuse. Общее имя не означает общую бизнес-ответственность.
|
||||||
|
Сохраняй validation, authorization, atomicity, accessibility и migration safety.
|
||||||
|
|
||||||
|
После diff выполни Boy Scout review: проверь новое дублирование, owner, callers,
|
||||||
|
небольшие связанные legacy issues и соответствие local conventions. Исправляй
|
||||||
|
непосредственно связанные low-risk проблемы; широкий cleanup — отдельная задача.
|
||||||
|
|
||||||
|
## Domain values
|
||||||
|
|
||||||
|
Finite business vocabulary принадлежит одному domain/module/entity owner.
|
||||||
|
Переиспользуй его symbols; для нового словаря без аналога — `as const` + union.
|
||||||
|
Сохраняй wire values; generated/external enums не переписывай вручную.
|
||||||
|
Numeric policy называй по смыслу и единицам. Индексы, `Math.max(value, 0)` и
|
||||||
|
обычные labels не выноси в constants без semantic benefit.
|
||||||
|
Локальный TypeScript/lint профиль определяет дополнительные ограничения.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
Проверь аналоги и direct dependencies в местном manifest до добавления пакета.
|
||||||
|
Новая dependency оправдана текущей потребностью и меньшей общей сложностью;
|
||||||
|
оцени compatibility, runtime/bundle и maintenance/security. Импортируемый пакет
|
||||||
|
объявляется напрямую; транзитивная установка не является контрактом.
|
||||||
|
Сохраняй exact versions и lockfile; воспроизводимая установка — `npm ci`.
|
||||||
|
Peer-конфликт решай совместимыми версиями без `--force`/`--legacy-peer-deps`.
|
||||||
|
Используй существующий analyzer; не добавляй второй для той же проверки.
|
||||||
|
|
||||||
|
## Test Intent
|
||||||
|
|
||||||
|
Перед нетривиальным тестом кратко определи наблюдаемый исход, regression risk,
|
||||||
|
значимые edge/error/concurrency cases и самую дешёвую надёжную boundary.
|
||||||
|
Отдельный artifact не нужен. Проверяй результат, а не private call order,
|
||||||
|
декларации, тривиальные wrappers или поведение framework.
|
||||||
|
При bug сначала воспроизведи исход. Mock race не доказывает DB atomicity.
|
||||||
|
Уровни и runnable commands выбирай по местной testing convention.
|
||||||
|
|
||||||
|
## Проверки и завершение
|
||||||
|
|
||||||
|
Code/dependency change: `npm ci`, полный `npm run verify` и дополнительные
|
||||||
|
risk checks из местной testing convention. Targeted tests ускоряют итерацию,
|
||||||
|
но не заменяют финальный gate. `lint:fix`/format — edits, не proof.
|
||||||
|
Markdown-only: проверь новые/изменённые тексты, локальные ссылки/якоря, маршруты
|
||||||
|
чтения и `git diff --check`; application build ради текста не нужен.
|
||||||
|
Повтори routing по staged/unstaged/new files. Запиши краткие команды, результаты
|
||||||
|
и ограничения. Зелёный lint не заменяет semantic/security review.
|
||||||
|
|
||||||
|
## Git и релизы
|
||||||
|
|
||||||
|
Работа — в `codex/<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, раздел «Релизы».
|
||||||
@@ -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).
|
||||||
@@ -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
|
## Выбор 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
|
используют объявленные клиентом records; SDK clients, metadata и transport errors
|
||||||
остаются внутри integration. Frontend не вызывает
|
остаются внутри integration. Credentials и proto client не входят в публичный
|
||||||
T-Bank и не получает credentials/proto client. Reference source —
|
HTTP API. Connection lifecycle проверяется на границе соответствующего module
|
||||||
[ADR 003](../../../docs/adr/003-tbank-server-side-grpc-boundary.md) и
|
и [Security](security-auth.md).
|
||||||
[connection behavior](../../../docs/integrations/tbank.md).
|
|
||||||
|
|
||||||
`modules/investments/tbank-connection` — пользовательская capability чтения,
|
`modules/investments/tbank-connection` — пользовательская capability чтения,
|
||||||
создания/замены и удаления подключения. `integrations/tbank` — технические
|
создания/замены и удаления подключения. `integrations/tbank` — технические
|
||||||
@@ -17,7 +17,7 @@ RPC, credentials/access и resilience. Эти владельцы не объед
|
|||||||
`*.client.ts` содержит transport и его retries; `*.api.ts` только перечисляет
|
`*.client.ts` содержит transport и его retries; `*.api.ts` только перечисляет
|
||||||
публичные exports. Все шесть `auth/operations/portfolio/tbank/moex/ncr.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`.
|
в эти файлы не помещаются. Type exports обозначаются `export type`.
|
||||||
|
|
||||||
Преобразование provider records происходит на boundary: например,
|
Преобразование provider records происходит на boundary: например,
|
||||||
@@ -30,7 +30,7 @@ client или mapper для простого копирования полей.
|
|||||||
Для нового account use case сначала используй TbankAccessService: access,
|
Для нового account use case сначала используй TbankAccessService: access,
|
||||||
connection generation и invalid-token translation не дублируются в callers.
|
connection generation и invalid-token translation не дублируются в callers.
|
||||||
Mapper внешних финансовых значений остаётся внутри business capability,
|
Mapper внешних финансовых значений остаётся внутри business capability,
|
||||||
обычно в infrastructure по [Architecture](backend-architecture.md).
|
обычно в infrastructure по [Architecture](../architecture.md).
|
||||||
|
|
||||||
## Retry, cache и protocol
|
## Retry, cache и protocol
|
||||||
|
|
||||||
@@ -38,13 +38,19 @@ Existing client уже владеет bounded FIFO queue, rate limit, deadline
|
|||||||
классификацией. Новый RPC проходит тот же путь; не добавляй вложенный retry loop
|
классификацией. Новый RPC проходит тот же путь; не добавляй вложенный retry loop
|
||||||
в service. Не отключай TLS verification ради окружения: используй документированный
|
в service. Не отключай TLS verification ради окружения: используй документированный
|
||||||
CA mechanism. Точные параметры и ограничения принадлежат
|
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,
|
При query/cache change установи key, user/account/generation ownership,
|
||||||
freshness и поведение late response. Старый результат не может обслужить новую
|
freshness и поведение late response. Старый результат не может обслужить новую
|
||||||
connection generation. Client Cached Snapshot frontend отличается от backend
|
connection generation. Успешная замена/deletion подключения меняет generation
|
||||||
cache/stale policy; semantics конкретного use case читай через
|
даже при сохранении прежнего token; ошибка оставляет прежнее подключение.
|
||||||
[feature index](../../../docs/README.md#предметные-области).
|
Кэш конкретного use case принадлежит его service и tests. Не переноси
|
||||||
|
сохранённый snapshot между пользователями или поколениями подключения.
|
||||||
|
|
||||||
Внешний auth failure и временная недоступность имеют разные public codes;
|
Внешний auth failure и временная недоступность имеют разные public codes;
|
||||||
raw gRPC exception не уходит клиенту. Existing translation — в TbankTokenService
|
raw gRPC exception не уходит клиенту. Existing translation — в TbankTokenService
|
||||||
@@ -64,4 +70,4 @@ Generated artifacts, если есть, создавай из source, без р
|
|||||||
upstream responses. Проверь queue/deadline/retry limit, token translation,
|
upstream responses. Проверь queue/deadline/retry limit, token translation,
|
||||||
cache isolation и late response для затронутых ветвей. Fixtures синтетические;
|
cache isolation и late response для затронутых ветвей. Fixtures синтетические;
|
||||||
реальная сеть, секреты и пользовательские accounts не нужны.
|
реальная сеть, секреты и пользовательские accounts не нужны.
|
||||||
Logging использует [existing logger](backend-architecture.md#logging).
|
Logging использует [existing logger](../architecture.md#logging).
|
||||||
@@ -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.
|
||||||
@@ -10,20 +10,23 @@ account access/connection generation — TbankAccessService; token crypto —
|
|||||||
TbankTokenService/TbankCrypto. Расширяй существующую boundary, не добавляй
|
TbankTokenService/TbankCrypto. Расширяй существующую boundary, не добавляй
|
||||||
второй JWT middleware, decrypt helper или refresh coordinator.
|
второй JWT middleware, decrypt helper или refresh coordinator.
|
||||||
|
|
||||||
[Authentication](../../../docs/domains/authentication/overview.md) и
|
Access проверяется JWT strategy; refresh передаётся httpOnly cookie.
|
||||||
[ADR 006](../../../docs/adr/006-auth-session-policy.md)/[009](../../../docs/adr/009-concurrent-refresh-rotation-policy.md)
|
Сроки token/session принадлежат auth source и configured HTTP tests.
|
||||||
задают session semantics. Access хранится только в памяти frontend; refresh
|
Source code конфигурации cookie —
|
||||||
передаётся httpOnly cookie. Source code конфигурации cookie —
|
|
||||||
[AuthController](../../src/modules/auth/presentation/auth.controller.ts); изменение flags/path требует
|
[AuthController](../../src/modules/auth/presentation/auth.controller.ts); изменение flags/path требует
|
||||||
проверки browser/HTTP поведения, не только TypeScript.
|
проверки browser/HTTP поведения, не только TypeScript.
|
||||||
|
|
||||||
Refresh rotation атомарна: один победитель, проигравший/superseded token получает
|
Refresh rotation атомарна: один победитель, проигравший/superseded token получает
|
||||||
401, но сам по себе не отзывает session победителя. Не вводи replay history/grace
|
401, но сам по себе не отзывает session победителя. Не вводи replay history/grace
|
||||||
или иной security contract без текущей задачи. Logout идемпотентен; in-memory
|
или иной security contract без текущей задачи. Logout идемпотентен, отзывает
|
||||||
frontend session/cache очищается и при network failure.
|
сессию и очищает refresh cookie на HTTP-границе.
|
||||||
|
|
||||||
## Trust boundaries
|
## 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 и
|
Verified JWT не доказывает доступ к accountId из URL. Используй user.sub и
|
||||||
existing account access policy; не доверяй userId/token из клиента как authority.
|
existing account access policy; не доверяй userId/token из клиента как authority.
|
||||||
Cross-user, invalid/expired/superseded token и concurrent requests — обязательные
|
Cross-user, invalid/expired/superseded token и concurrent requests — обязательные
|
||||||
@@ -32,13 +35,20 @@ cases для соответствующего изменения.
|
|||||||
T-Bank credentials остаются server-side и хранятся encrypted. Используй
|
T-Bank credentials остаются server-side и хранятся encrypted. Используй
|
||||||
existing AES-256-GCM boundary с validated 32-byte key; не печатай plaintext,
|
existing AES-256-GCM boundary с validated 32-byte key; не печатай plaintext,
|
||||||
шифротекст, cookie, Authorization или connection generation в logs/errors/API.
|
шифротекст, cookie, Authorization или connection generation в logs/errors/API.
|
||||||
[Connection source](../../../docs/integrations/tbank.md) определяет
|
Успешная замена/deletion меняет connection generation и инвалидирует её кэши;
|
||||||
cache invalidation при замене/deletion и ограничение key rotation.
|
ошибка сохраняет прежнее подключение. Ключ не ротируется автоматически:
|
||||||
|
новый ключ не расшифрует сохранённые credentials. Нужен исходный ключ или
|
||||||
|
повторное сохранение token пользователем; сохраняй ключ отдельно от backup БД.
|
||||||
|
|
||||||
Не передавай Prisma User/Session целиком наружу; выбирай public DTO. Пароль
|
Не передавай Prisma User/Session целиком наружу; выбирай public DTO. Пароль
|
||||||
обрабатывает существующий bcrypt flow. Не копируй требования о нормализации
|
обрабатывает существующий bcrypt flow. Не копируй требования о нормализации
|
||||||
или длине из tutorial: сверяй runtime DTO, tests и feature source.
|
или длине из 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.
|
При неясной ownership/crypto/session семантике прочитай exact source до edits.
|
||||||
@@ -46,6 +56,6 @@ cache invalidation при замене/deletion и ограничение key ro
|
|||||||
фиктивным success или log. Logger allowlist/redaction не делает произвольный
|
фиктивным success или log. Logger allowlist/redaction не делает произвольный
|
||||||
message безопасным.
|
message безопасным.
|
||||||
|
|
||||||
Targeted auth/access/crypto/logging tests + [Testing](backend-testing.md).
|
Targeted auth/access/crypto/logging tests + [Testing](testing.md).
|
||||||
Cookie/guard/HTTP changes требуют configured HTTP proof; frontend Bearer/retry
|
Cookie/guard/HTTP changes требуют configured HTTP proof.
|
||||||
проверяется MSW. Schema parity не доказывает отсутствие утечки secret.
|
Schema parity не доказывает отсутствие утечки secret.
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Backend testing
|
# Backend testing
|
||||||
|
|
||||||
|
Перед нетривиальными тестами сформулируй
|
||||||
|
[общий Test Intent](engineering.md#test-intent); отдельный документ
|
||||||
|
не требуется. Trivial low-risk изменения обходятся без него.
|
||||||
|
|
||||||
## Выбор уровня
|
## Выбор уровня
|
||||||
|
|
||||||
Проверяй риски доступа, денежных расчётов, целостности данных, конкурентных
|
Проверяй риски доступа, денежных расчётов, целостности данных, конкурентных
|
||||||
@@ -11,7 +15,7 @@
|
|||||||
| Pure mapper/decimal/domain branch | Unit со значимыми edge cases |
|
| Pure mapper/decimal/domain branch | Unit со значимыми edge cases |
|
||||||
| Service orchestration/access/retry policy | Jest с mocks внешних boundaries и observable outcome |
|
| Service orchestration/access/retry policy | Jest с mocks внешних boundaries и observable outcome |
|
||||||
| Pipe/guard/cookie/status/HTTP error | Configured Nest HTTP boundary; прямого вызова метода controller недостаточно |
|
| 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 |
|
| DB constraint/CAS/transaction/migration | Integration с изолированной реальной DB |
|
||||||
| Только DTO declarations/thin wiring | Existing contract/type/build без искусственного unit test |
|
| Только 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, нормализация и точные суммы.
|
- [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.
|
- [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.
|
- [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.
|
При bug сначала воспроизведи именно его исход, затем исправь responsible owner.
|
||||||
Не создавай tests для каждой декларации, logs строки или поведения Prisma/Nest
|
Не создавай 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:e2e` запускает все HTTP suites через изолированный SQLite runner;
|
||||||
`npm run test:sqlite` — только auth/cards. Каждая suite получает новую временную
|
`npm run test:sqlite` — только auth/cards. Каждая suite получает новую временную
|
||||||
базу со штатными migrations. Прямой Jest без runner отклоняется до DB-запросов.
|
базу со штатными migrations. Прямой Jest без runner отклоняется до DB-запросов.
|
||||||
`prisma.config.ts` исключает влияние унаследованного `RUST_LOG=warn/error`,
|
`prisma.config.ts` исключает влияние унаследованного `RUST_LOG=warn/error`
|
||||||
вызывающего ошибку fresh migration на Prisma 7.9.1. Подробности и регрессия запуска —
|
на fresh migration; точная Prisma version принадлежит manifest/lockfile. Подробности и регрессия запуска —
|
||||||
[Backend README](../../README.md#реальная-sqlite-и-сохранность-данных).
|
[Backend README](../../README.md#реальная-sqlite-и-сохранность-данных).
|
||||||
HTTP suites требуют разрешения открывать локальные порты. Ошибка sandbox
|
HTTP suites требуют разрешения открывать локальные порты. Ошибка sandbox
|
||||||
не считается успешным тестом; повторите проверку в подходящем окружении.
|
не считается успешным тестом; повторите проверку в подходящем окружении.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
**
|
||||||
|
!package.json
|
||||||
|
!package-lock.json
|
||||||
|
!.nvmrc
|
||||||
|
!nest-cli.json
|
||||||
|
!tsconfig*.json
|
||||||
|
!prisma.config.ts
|
||||||
|
!prisma/
|
||||||
|
!prisma/schema.prisma
|
||||||
|
!prisma/migrations/
|
||||||
|
!prisma/migrations/**
|
||||||
|
!src/
|
||||||
|
!src/**
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Conventional commits
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
commits:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: https://github.com/actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
- run: npm ci --ignore-scripts
|
||||||
|
- name: Check PR title and commits
|
||||||
|
env:
|
||||||
|
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: |
|
||||||
|
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict
|
||||||
|
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Backend image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: backend-image-publication
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
IMAGE: nexus.ksv741.keenetic.pro/wealthledger/backend
|
||||||
|
RELEASE_TAG: ${{ gitea.ref_name }}
|
||||||
|
REVISION: ${{ gitea.sha }}
|
||||||
|
DOCKER_CONFIG: ${{ runner.temp }}/wealthledger-backend-docker
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
- uses: https://github.com/actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version-file: .nvmrc
|
||||||
|
- name: Validate release version
|
||||||
|
run: |
|
||||||
|
node --input-type=commonjs -e 'const p = require("./package.json"); const l = require("./package-lock.json"); if (!/^v\d+\.\d+\.\d+$/.test(process.env.RELEASE_TAG) || process.env.RELEASE_TAG !== `v${p.version}` || l.version !== p.version || l.packages[""].version !== p.version) process.exit(1)'
|
||||||
|
test "$(git rev-parse HEAD)" = "$REVISION"
|
||||||
|
docker version
|
||||||
|
docker buildx version
|
||||||
|
- run: npm ci
|
||||||
|
- run: npx --no-install prisma generate
|
||||||
|
- run: npm run verify
|
||||||
|
- run: npm run test:e2e
|
||||||
|
- name: Login with dedicated Nexus writer
|
||||||
|
env:
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.NEXUS_USERNAME }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.NEXUS_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
test -n "$REGISTRY_USERNAME" && test -n "$REGISTRY_TOKEN"
|
||||||
|
install -d -m 700 "$DOCKER_CONFIG"
|
||||||
|
printf '%s' "$REGISTRY_TOKEN" | docker login nexus.ksv741.keenetic.pro --username "$REGISTRY_USERNAME" --password-stdin
|
||||||
|
- name: Publish immutable release tag
|
||||||
|
run: |
|
||||||
|
VERSION="${RELEASE_TAG#v}"
|
||||||
|
if docker manifest inspect "$IMAGE:$VERSION" >/dev/null 2>manifest-error.log; then
|
||||||
|
echo 'Refusing to overwrite an existing release tag'; exit 1
|
||||||
|
fi
|
||||||
|
if ! grep -Eq 'manifest unknown|no such manifest' manifest-error.log; then
|
||||||
|
echo 'Cannot prove tag absence; refusing publication'; exit 1
|
||||||
|
fi
|
||||||
|
docker buildx build --target production --build-arg VERSION="$VERSION" --build-arg REVISION="$REVISION" --tag "$IMAGE:$VERSION" --metadata-file image-metadata.json --push .
|
||||||
|
DIGEST="$(node --input-type=commonjs -p 'require("./image-metadata.json")["containerimage.digest"]')"
|
||||||
|
docker pull "$IMAGE@$DIGEST"
|
||||||
|
test "$(docker image inspect "$IMAGE@$DIGEST" --format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$REVISION"
|
||||||
|
npm run test:container -- "$IMAGE@$DIGEST"
|
||||||
|
printf 'Backend image: %s@%s\n' "$IMAGE" "$DIGEST"
|
||||||
|
- name: Remove registry session
|
||||||
|
if: always()
|
||||||
|
run: rm -rf "$DOCKER_CONFIG"
|
||||||
+14
-13
@@ -2,28 +2,21 @@ name: Backend verify
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: ['**']
|
||||||
- '**'
|
pull_request:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
verify:
|
verify:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: backend
|
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: backend
|
fetch-depth: 0
|
||||||
- name: Checkout frontend contract
|
- name: Record checked revision
|
||||||
run: git clone --depth 1 --branch main https://git.personal.ksv741.ru/WealthLedger/frontend.git ../frontend
|
run: git rev-parse HEAD
|
||||||
- name: Record checked revisions
|
|
||||||
run: |
|
|
||||||
git rev-parse HEAD
|
|
||||||
git -C ../frontend rev-parse HEAD
|
|
||||||
- uses: https://github.com/actions/setup-node@v4
|
- uses: https://github.com/actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version-file: backend/.nvmrc
|
node-version-file: .nvmrc
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Generate Prisma client before typechecks
|
- name: Generate Prisma client before typechecks
|
||||||
run: npx --no-install prisma generate
|
run: npx --no-install prisma generate
|
||||||
@@ -31,3 +24,11 @@ jobs:
|
|||||||
run: npm run verify
|
run: npm run verify
|
||||||
- name: Run isolated SQLite and runtime OpenAPI suites
|
- name: Run isolated SQLite and runtime OpenAPI suites
|
||||||
run: npm run test:e2e
|
run: npm run test:e2e
|
||||||
|
- name: Analyze develop with SonarQube
|
||||||
|
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/develop'
|
||||||
|
# v2.3.0: same Docker-based scanner used for SonarQube 9.9.
|
||||||
|
uses: https://github.com/SonarSource/sonarqube-scan-action@aecaf43ae57e412bd97d70ef9ce6076e672fe0a9
|
||||||
|
env:
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
with:
|
||||||
|
args: -Dsonar.qualitygate.wait=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ lerna-debug.log*
|
|||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
/coverage
|
/coverage
|
||||||
|
/.scannerwork
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
@@ -64,3 +65,6 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|||||||
/dev.db-*
|
/dev.db-*
|
||||||
/prisma/*.db
|
/prisma/*.db
|
||||||
/prisma/*.db-*
|
/prisma/*.db-*
|
||||||
|
|
||||||
|
# Generated public API artifact
|
||||||
|
/openapi.json
|
||||||
|
|||||||
@@ -15,3 +15,5 @@ dfca11c81f7e6dadce3ca6f961d265c5e3f8ff8e:src/tbank/portfolio.service.spec.ts:gen
|
|||||||
# Historical CircleCI badge copied verbatim from nestjs/typescript-starter README.
|
# Historical CircleCI badge copied verbatim from nestjs/typescript-starter README.
|
||||||
# Verified against https://github.com/nestjs/typescript-starter/blob/master/README.md
|
# Verified against https://github.com/nestjs/typescript-starter/blob/master/README.md
|
||||||
f8dcfb71c1767696a9aa6741ddaf6575416c901e:README.md:generic-api-key:5
|
f8dcfb71c1767696a9aa6741ddaf6575416c901e:README.md:generic-api-key:5
|
||||||
|
# Container smoke fixture: same deterministic 32-byte test key, never a runtime secret.
|
||||||
|
1a6be432f24f757728a03415051392069112af1f:test/container-smoke.cjs:generic-api-key:14
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
{
|
||||||
|
"git": {
|
||||||
|
"requireBranch": "release/*",
|
||||||
|
"requireCleanWorkingDir": true,
|
||||||
|
"requireUpstream": false,
|
||||||
|
"requireCommits": true,
|
||||||
|
"commitMessage": "chore(release): ${version}",
|
||||||
|
"tagName": "v${version}",
|
||||||
|
"tag": false,
|
||||||
|
"push": false
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"publish": false
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": false
|
||||||
|
},
|
||||||
|
"gitlab": {
|
||||||
|
"release": false
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"after:git:init": "node scripts/check-release.cjs"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"@release-it/conventional-changelog": {
|
||||||
|
"infile": "CHANGELOG.md",
|
||||||
|
"preset": {
|
||||||
|
"name": "conventionalcommits",
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "chore",
|
||||||
|
"scope": "release",
|
||||||
|
"effect": "hidden"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "feat",
|
||||||
|
"section": "Features",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "fix",
|
||||||
|
"section": "Bug Fixes",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "perf",
|
||||||
|
"section": "Performance",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "docs",
|
||||||
|
"section": "Documentation",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spec",
|
||||||
|
"section": "Specifications",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "refactor",
|
||||||
|
"section": "Refactoring",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "build",
|
||||||
|
"section": "Build",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "ci",
|
||||||
|
"section": "CI",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "test",
|
||||||
|
"section": "Tests",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "style",
|
||||||
|
"section": "Styles",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "chore",
|
||||||
|
"section": "Maintenance",
|
||||||
|
"effect": "bump"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "revert",
|
||||||
|
"section": "Reverts",
|
||||||
|
"effect": "bump"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"host": "https://git.personal.ksv741.ru",
|
||||||
|
"owner": "WealthLedger",
|
||||||
|
"repository": "backend",
|
||||||
|
"linkReferences": true,
|
||||||
|
"commit": "commit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +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`.
|
Новый endpoint обычно требует HTTP API + Testing; Data/Security добавляются
|
||||||
После lockfile install `npm ci` полный gate — `npm run verify`:
|
по persistence/access trigger. Auth refresh активирует Security + Data + Testing.
|
||||||
zero-warning ESLint, application/test TypeScript, Jest и build.
|
Перед завершением повтори routing по новым и изменённым файлам. Для Git/release
|
||||||
`lint:fix`/`format` пишут файлы и запускаются отдельно от доказательства проверки.
|
прочитай [Git и релизы](.ai/conventions/engineering.md#git-и-релизы).
|
||||||
`test:cov` — диагностика без общего threshold; scripts принадлежат `package.json`.
|
|
||||||
|
|
||||||
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.
|
| --- | --- |
|
||||||
|
| 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 |
|
||||||
|
|||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [0.2.1](https://git.personal.ksv741.ru/WealthLedger/backend/compare/v0.2.0...v0.2.1) (2026-09-18)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* use shared Nexus account [#25](https://git.personal.ksv741.ru/WealthLedger/backend/issues/25) ([9437b6a](https://git.personal.ksv741.ru/WealthLedger/backend/commit/9437b6a311cd51230b8381c8643c6ea308454d38))
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
* add backend SonarQube analysis [#23](https://git.personal.ksv741.ru/WealthLedger/backend/issues/23) ([1946192](https://git.personal.ksv741.ru/WealthLedger/backend/commit/1946192be975eeea75432ec39b27d19952742b15))
|
||||||
|
* publish backend images to Nexus [#25](https://git.personal.ksv741.ru/WealthLedger/backend/issues/25) ([0ec7ae5](https://git.personal.ksv741.ru/WealthLedger/backend/commit/0ec7ae51af080288a31dc6e65a738a4bd6396436))
|
||||||
|
|
||||||
|
## [0.2.0](https://git.personal.ksv741.ru/WealthLedger/backend/compare/v0.1.0...v0.2.0) (2026-09-16)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add backend container delivery ([1a6be43](https://git.personal.ksv741.ru/WealthLedger/backend/commit/1a6be432f24f757728a03415051392069112af1f))
|
||||||
|
* generate OpenAPI without peer repositories ([32c12fb](https://git.personal.ksv741.ru/WealthLedger/backend/commit/32c12fb10d4f739c6af0ce32b8f2ea6e94c42090))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow reviewed container smoke fixture ([d7cb7e3](https://git.personal.ksv741.ru/WealthLedger/backend/commit/d7cb7e3e206c995ef2fa6f1ee61ef7e93599d88f))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* refine backend conventions and routing ([9810d47](https://git.personal.ksv741.ru/WealthLedger/backend/commit/9810d47e42d9275cf2520556f517d411058ddcb3))
|
||||||
|
* simplify conventions layout ([0167826](https://git.personal.ksv741.ru/WealthLedger/backend/commit/016782657acfd35ff3fd755462007938fac35156))
|
||||||
|
|
||||||
|
## 0.1.0 (2026-09-14)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add authorization ([4cbfc26](https://git.personal.ksv741.ru/WealthLedger/backend/commit/4cbfc2621cfc54df3d09786905130c28a2323a04))
|
||||||
|
* add bond details, yields and credit ratings ([235c179](https://git.personal.ksv741.ru/WealthLedger/backend/commit/235c179ff813dac3312b849928d11734bccba823))
|
||||||
|
* add brocker account list ([c0a4a93](https://git.personal.ksv741.ru/WealthLedger/backend/commit/c0a4a9388074bdce20138dd5649805ed24a9e9fa))
|
||||||
|
* add logging ([25d7c57](https://git.personal.ksv741.ru/WealthLedger/backend/commit/25d7c5780b60343fecb4f1ae9a30392776eb5aec))
|
||||||
|
* add manual bank card ledger ([e6f58ef](https://git.personal.ksv741.ru/WealthLedger/backend/commit/e6f58ef0eef3fbae079270a0d429afd17e4cd3ac))
|
||||||
|
* add operations list ([326fb67](https://git.personal.ksv741.ru/WealthLedger/backend/commit/326fb67def9b3878d5447ef838ae96d50bf493d6))
|
||||||
|
* add portfolio ([dfca11c](https://git.personal.ksv741.ru/WealthLedger/backend/commit/dfca11c81f7e6dadce3ca6f961d265c5e3f8ff8e))
|
||||||
|
* add stock details and ticker resolution ([f24fd98](https://git.personal.ksv741.ru/WealthLedger/backend/commit/f24fd98fbcbbf952b11bb61f607ae0aec4456839))
|
||||||
|
* add tbank tokens ([31cbba8](https://git.personal.ksv741.ru/WealthLedger/backend/commit/31cbba8654f01adf11c52e12c2b286200d6cad7d))
|
||||||
|
* add агегку events ([34c2b79](https://git.personal.ksv741.ru/WealthLedger/backend/commit/34c2b79e276c9575996604f3f862bc866cfe44e7))
|
||||||
|
* aggregate portfolio data across accounts ([eaf2f67](https://git.personal.ksv741.ru/WealthLedger/backend/commit/eaf2f67b34b2f8b45a9f5d53c544b91304b333d3))
|
||||||
|
* **cards:** support inclusive date range queries ([3cf883e](https://git.personal.ksv741.ru/WealthLedger/backend/commit/3cf883efbc6fceddc889c5aadc3c8ffb410c6de5))
|
||||||
|
* enrich ledger events with logo metadata ([84fabd2](https://git.personal.ksv741.ru/WealthLedger/backend/commit/84fabd264fa539dfefb1172e27a038870a30d845))
|
||||||
|
* establish backend foundation ([ab21426](https://git.personal.ksv741.ru/WealthLedger/backend/commit/ab21426478233b1cdfc7571c796f76d347874501))
|
||||||
|
* **events:** support date range filtering ([a79f59a](https://git.personal.ksv741.ru/WealthLedger/backend/commit/a79f59a468dd113147b6f1a04ed0efd9828046c6))
|
||||||
|
* expose instrument brand logo name in portfolio positions ([5375e55](https://git.personal.ksv741.ru/WealthLedger/backend/commit/5375e5598a284238b995661dd2c0e9ce5901d190))
|
||||||
|
* expose instrument isin in portfolio positions ([5aa9bd3](https://git.personal.ksv741.ru/WealthLedger/backend/commit/5aa9bd395a8e49e423764eb7c4a88afe56332ad4))
|
||||||
|
* expose logos and ISINs for upcoming events ([33f703d](https://git.personal.ksv741.ru/WealthLedger/backend/commit/33f703d9a375e3d88e492fbb338e609d6c499d7f))
|
||||||
|
* **income:** add account income analytics API ([7ba4da0](https://git.personal.ksv741.ru/WealthLedger/backend/commit/7ba4da0d1b49afcb0db3d1756254e5d5e04f0c35))
|
||||||
|
* initial commit ([f8dcfb7](https://git.personal.ksv741.ru/WealthLedger/backend/commit/f8dcfb71c1767696a9aa6741ddaf6575416c901e))
|
||||||
|
* **portfolio:** expose asset allocation by currency ([05747dc](https://git.personal.ksv741.ru/WealthLedger/backend/commit/05747dc3757cdfd4c3d840ad0500861063a76b09))
|
||||||
|
* **portfolio:** expose position tickers ([924f60f](https://git.personal.ksv741.ru/WealthLedger/backend/commit/924f60f65357781df967bfbaf1538f3400ad620a))
|
||||||
|
* **tbank:** add brokerage account summaries ([e489d45](https://git.personal.ksv741.ru/WealthLedger/backend/commit/e489d45f889656183e88ad68d964f721b715cbdb))
|
||||||
|
* use money for upcoming event projections ([f1df104](https://git.personal.ksv741.ru/WealthLedger/backend/commit/f1df1044623862770ecb40fd735e1aea08dc2a68))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* align auth success status contract ([9bd3400](https://git.personal.ksv741.ru/WealthLedger/backend/commit/9bd3400264de40a90b45d0cd997c2c752548c0bb))
|
||||||
|
* allow multi-year income analytics periods ([9fcc0ec](https://git.personal.ksv741.ru/WealthLedger/backend/commit/9fcc0ec01068372e6756f60bb49d83643e07466d))
|
||||||
|
* harden auth and cache concurrency ([c77b3cf](https://git.personal.ksv741.ru/WealthLedger/backend/commit/c77b3cf782f24ad3007add43f768d721e8f02386))
|
||||||
|
* harden backend runtime artifact hygiene ([6052565](https://git.personal.ksv741.ru/WealthLedger/backend/commit/60525656b8ecdc4b63956f0d7b77e4286961edc4))
|
||||||
|
* **income:** expose authoritative entry membership ([bdc3847](https://git.personal.ksv741.ru/WealthLedger/backend/commit/bdc384766649bb48513055bb809f0d1b45b91f54))
|
||||||
|
* isolate dev output from production builds ([320e8a0](https://git.personal.ksv741.ru/WealthLedger/backend/commit/320e8a04b0ad8e0d57a89e07768e600bc16bab36))
|
||||||
|
* little fixes ([e77ebc6](https://git.personal.ksv741.ru/WealthLedger/backend/commit/e77ebc6e8f78f96a4ee57c2684769ea4ec923017))
|
||||||
|
* make backend local verification safe ([71d57d6](https://git.personal.ksv741.ru/WealthLedger/backend/commit/71d57d60bcf2186d4f8f02169d1bea4ea4d672e3))
|
||||||
|
* make SQLite migrations and e2e reproducible ([ce24a84](https://git.personal.ksv741.ru/WealthLedger/backend/commit/ce24a84b7fb48091d2b91836eb7b702a2f811c96))
|
||||||
|
* recover portfolio instrument metadata on backend ([8ec172f](https://git.personal.ksv741.ru/WealthLedger/backend/commit/8ec172f48eefa2237dc0c16919fbb2e726b39a65))
|
||||||
|
* **tbank:** isolate caches by connection generation ([19fe7e7](https://git.personal.ksv741.ru/WealthLedger/backend/commit/19fe7e75d59792e9c6d6b702ab192fa2d5ccc936))
|
||||||
|
* **tbank:** type connectedAt as date-time ([55b100d](https://git.personal.ksv741.ru/WealthLedger/backend/commit/55b100d5f6bed126633432654a3353f862e6f428))
|
||||||
|
* type T-Bank client config ([18d7cfc](https://git.personal.ksv741.ru/WealthLedger/backend/commit/18d7cfcf9fecb89e99f7ba1f5d43602807ae98b2))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
* **agents:** link audit remediation registry ([595fc04](https://git.personal.ksv741.ru/WealthLedger/backend/commit/595fc0458b3af37ef5eb0c647c401f39f34fe5f0))
|
||||||
|
* **ai:** add scoped backend conventions ([a83e26e](https://git.personal.ksv741.ru/WealthLedger/backend/commit/a83e26e5ef5d7cae3179a182fb5359a440538177))
|
||||||
|
* define canonical OpenSpec store ([fee4f8c](https://git.personal.ksv741.ru/WealthLedger/backend/commit/fee4f8c8e6e3aa8fc4c41522dc6697825316f8f6))
|
||||||
|
* refresh backend onboarding ([fec542a](https://git.personal.ksv741.ru/WealthLedger/backend/commit/fec542a275ed6c8273bb253ca4b58a497f74cd99))
|
||||||
|
* remove useless ([da17893](https://git.personal.ksv741.ru/WealthLedger/backend/commit/da178933509512c05c501773947f35397783f249))
|
||||||
|
* update routes to system documentation ([b1906c8](https://git.personal.ksv741.ru/WealthLedger/backend/commit/b1906c8e3ca242a0e30bd63519504c0c070f0354))
|
||||||
|
* update system documentation references ([d4fc28c](https://git.personal.ksv741.ru/WealthLedger/backend/commit/d4fc28cbbf0bb76a8c11a68f09bbe41151067d88))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
* centralize domain values and enforce numeric lint ([8637ad2](https://git.personal.ksv741.ru/WealthLedger/backend/commit/8637ad2f346c7e03e709b3a52d1d67995415a707))
|
||||||
|
* enforce strict backend typing ([346a6f0](https://git.personal.ksv741.ru/WealthLedger/backend/commit/346a6f0057178a2fe157c45ecf93740140c10440))
|
||||||
|
* organize backend by business responsibility ([8ba0053](https://git.personal.ksv741.ru/WealthLedger/backend/commit/8ba005346dbbd515e2c1d71bf99fdacf4b73947f))
|
||||||
|
* refactor future event list ([e005541](https://git.personal.ksv741.ru/WealthLedger/backend/commit/e0055414d6947e46fb58db1abe96f4ec3ea74f8c))
|
||||||
|
* **tbank:** centralize access rules ([130b303](https://git.personal.ksv741.ru/WealthLedger/backend/commit/130b303ec123f144d3833e5c7afa88d40f16f081))
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
* verify backend and secure dependency updates [#1](https://git.personal.ksv741.ru/WealthLedger/backend/issues/1) [#5](https://git.personal.ksv741.ru/WealthLedger/backend/issues/5) [#6](https://git.personal.ksv741.ru/WealthLedger/backend/issues/6) ([4c8ba2f](https://git.personal.ksv741.ru/WealthLedger/backend/commit/4c8ba2fac903c883849359aa354f3ffa749ca085))
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
* cover isolated SQLite invariants and durability ([7fde3ec](https://git.personal.ksv741.ru/WealthLedger/backend/commit/7fde3ece6ec7143723140357602861cd2e20b3a5))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
* codify backend local verification ([2772d53](https://git.personal.ksv741.ru/WealthLedger/backend/commit/2772d53647132e6119e411c6d18234db4301e541))
|
||||||
|
* configure releases and develop workflow ([f01b82d](https://git.personal.ksv741.ru/WealthLedger/backend/commit/f01b82d36686ac27495263071bc82b34ae2f9b01))
|
||||||
|
* **deps:** pin node.js to v24.21.0 ([97e864b](https://git.personal.ksv741.ru/WealthLedger/backend/commit/97e864bc06a58737cae6669d42255f278d93e887))
|
||||||
|
* **deps:** update and pin backend dependencies ([2bc8745](https://git.personal.ksv741.ru/WealthLedger/backend/commit/2bc87454954d0bd3eeab11634ed16eed869dbaab))
|
||||||
|
* **deps:** upgrade backend to Nest 12 and TypeScript 6 ([121af70](https://git.personal.ksv741.ru/WealthLedger/backend/commit/121af701b722467353a33bf05e0b27a40ed5736e))
|
||||||
|
* save pending changes ([8c074d8](https://git.personal.ksv741.ru/WealthLedger/backend/commit/8c074d85de3bc55d0ba750b86bc7b026649fa6bf))
|
||||||
|
* simplify backend verification ([fd4572a](https://git.personal.ksv741.ru/WealthLedger/backend/commit/fd4572a64563c588029af38cd7b4cddc3e014cdf))
|
||||||
|
* upgrade Node to 24 LTS ([197ffe2](https://git.personal.ksv741.ru/WealthLedger/backend/commit/197ffe21368b337f27d27c22e473570258ebdc3b))
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
FROM node:24.21.0-bookworm-slim AS base
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
FROM base AS dependencies
|
||||||
|
# Native SQLite/bcrypt builds happen only here; compilers stay out of runtime.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends python3=3.11.2-1+b1 make=4.3-4.1 g++=4:12.2.0-3 ca-certificates=20250419~deb12u1 openssl=3.0.20-1~deb12u2 && rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npx --no-install prisma generate
|
||||||
|
|
||||||
|
FROM dependencies AS development
|
||||||
|
# Nest watch uses ps to stop the previous process tree before restarting.
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends procps=2:4.0.2-3 && rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN mkdir /data && chown node:node /data && chown -R node:node /app
|
||||||
|
USER node
|
||||||
|
ENV NODE_ENV=development DATABASE_URL=file:/data/wealthledger.db
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["npm", "run", "start:dev"]
|
||||||
|
|
||||||
|
FROM dependencies AS build
|
||||||
|
RUN npm run build && npm prune --omit=dev --omit=optional
|
||||||
|
|
||||||
|
FROM base AS production
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates=20250419~deb12u1 openssl=3.0.20-1~deb12u2 && rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& mkdir /data && chown node:node /data
|
||||||
|
ARG VERSION
|
||||||
|
ARG REVISION
|
||||||
|
LABEL org.opencontainers.image.version=$VERSION \
|
||||||
|
org.opencontainers.image.revision=$REVISION
|
||||||
|
COPY --from=build /app/node_modules ./node_modules
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/prisma ./prisma
|
||||||
|
COPY --from=build /app/prisma.config.ts ./prisma.config.ts
|
||||||
|
COPY --from=build /app/package.json /app/package-lock.json ./
|
||||||
|
ENV NODE_ENV=production DATABASE_URL=file:/data/wealthledger.db
|
||||||
|
USER node
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/src/main.js"]
|
||||||
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=20s --retries=6 \
|
||||||
|
CMD node -e 'fetch("http://127.0.0.1:3000/api/v1/auth/me").then(async r=>{await r.arrayBuffer();process.exit(r.status===401?0:1)}).catch(()=>process.exit(1))'
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
# WealthLedger backend
|
# WealthLedger backend
|
||||||
|
|
||||||
NestJS API WealthLedger. Backend владеет auth, SQLite-данными и server-side
|
NestJS API WealthLedger. Backend владеет auth, SQLite-данными и server-side
|
||||||
интеграцией T-Bank; frontend не обращается к T-Bank напрямую.
|
интеграциями. Потребители используют публичный HTTP API и OpenAPI artifact;
|
||||||
|
их репозитории не нужны для разработки, сборки и проверок backend.
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
@@ -21,6 +22,36 @@ npm run start:dev
|
|||||||
T-Bank token в `.env`: он вводится пользователем и хранится в зашифрованном
|
T-Bank token в `.env`: он вводится пользователем и хранится в зашифрованном
|
||||||
виде.
|
виде.
|
||||||
|
|
||||||
|
## Контейнерный image
|
||||||
|
|
||||||
|
`Dockerfile` имеет targets `development` (Nest watch, generated Prisma client и Linux native modules)
|
||||||
|
и `production` (compiled Node process от UID 1000, Prisma CLI/migrations и T-Bank proto assets). Node соответствует `.nvmrc`.
|
||||||
|
Build context ограничен `.dockerignore`; `.env`, local DB и host node_modules
|
||||||
|
не попадают в image.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --target development -t wealthledger-backend:development .
|
||||||
|
docker build --target production -t wealthledger-backend:production .
|
||||||
|
nvm use --silent
|
||||||
|
npm run test:container -- wealthledger-backend:production
|
||||||
|
```
|
||||||
|
|
||||||
|
Gitea `image.yml` запускается на защищённом release tag `vX.Y.Z`, проверяет
|
||||||
|
package/lock version и canonical gates. Runner `ubuntu-latest` должен иметь
|
||||||
|
Docker/buildx. Образы хранятся в Nexus:
|
||||||
|
`nexus.ksv741.keenetic.pro/wealthledger/backend`. Пользователь `wealthledger-ci`
|
||||||
|
задаётся Actions secrets `NEXUS_USERNAME`/`NEXUS_PASSWORD`; эта же учётная
|
||||||
|
запись используется сервером для загрузки образов. Docker endpoint и token realm должны использовать
|
||||||
|
HTTPS; в `docker-hosted` включается запрет redeploy. Прежние Gitea secrets
|
||||||
|
сохраняются на время перехода и не передаются в Nexus.
|
||||||
|
Публикуются semver tags, без `latest`; существующие release tags не перезаписываются. Production выбирает опубликованный version tag.
|
||||||
|
|
||||||
|
Compose topology принадлежит workspace, эксплуатационная последовательность —
|
||||||
|
[документация](https://git.personal.ksv741.ru/WealthLedger/docs),
|
||||||
|
страница `architecture/deployment.md`.
|
||||||
|
Проверка публикации требует настроенного registry/runner; локальный build
|
||||||
|
не доказывает package permissions.
|
||||||
|
|
||||||
## Проверка
|
## Проверка
|
||||||
|
|
||||||
Единый completion gate не изменяет versioned-файлы:
|
Единый completion gate не изменяет versioned-файлы:
|
||||||
@@ -30,10 +61,10 @@ npm run verify
|
|||||||
```
|
```
|
||||||
|
|
||||||
Он запускает zero-warning lint,
|
Он запускает zero-warning lint,
|
||||||
application/test typechecks, Jest и build. `npm run lint:fix` — отдельная
|
application/test typechecks, Jest с покрытием и build. `npm run lint:fix` — отдельная
|
||||||
записывающая команда, `npm run test:cov` — диагностика без общего числового
|
записывающая команда. `npm run test:cov` также доступен отдельно; общего числового
|
||||||
threshold. `npm run test:e2e` отдельно проверяет все HTTP suites на новых базах
|
threshold нет. `npm run test:e2e` отдельно проверяет все HTTP suites на новых базах
|
||||||
и runtime OpenAPI с соседним frontend checkout. Для полной backend-проверки
|
и соответствие генерируемого OpenAPI настроенному приложению. Для полной backend-проверки
|
||||||
выполните обе команды.
|
выполните обе команды.
|
||||||
|
|
||||||
Nest 12 поставляется как ESM. Скрипты Jest включают `--experimental-vm-modules`,
|
Nest 12 поставляется как ESM. Скрипты Jest включают `--experimental-vm-modules`,
|
||||||
@@ -44,10 +75,9 @@ TypeScript 7 требует поддержки со стороны `ts-jest` и
|
|||||||
### CI и обновления зависимостей
|
### CI и обновления зависимостей
|
||||||
|
|
||||||
`.gitea/workflows/verify.yml` запускает `npm ci`, генерацию Prisma client,
|
`.gitea/workflows/verify.yml` запускает `npm ci`, генерацию Prisma client,
|
||||||
`npm run verify` и `npm run test:e2e` на каждом push ветки. Frontend snapshot
|
`npm run verify` и `npm run test:e2e` на push и PR. Используется только checkout
|
||||||
берётся из `main` соседнего репозитория; лог сохраняет оба SHA. Изменение
|
backend и его зависимости; OpenAPI генерируется во время проверки, SHA записывается в лог.
|
||||||
контракта согласуется с этой frontend revision. Успешный unit gate не заменяет
|
Успешный unit gate не заменяет SQLite/e2e; ошибка любой suite завершает job с FAIL.
|
||||||
SQLite/e2e; ошибка любой suite завершает job с FAIL.
|
|
||||||
|
|
||||||
`security.yml` сканирует всю Git-историю Gitleaks 8.30.1 с полностью скрытыми
|
`security.yml` сканирует всю Git-историю Gitleaks 8.30.1 с полностью скрытыми
|
||||||
значениями и выполняет `npm audit --include=dev --audit-level=low` при push,
|
значениями и выполняет `npm audit --include=dev --audit-level=low` при push,
|
||||||
@@ -75,27 +105,73 @@ Required checks настраиваются отдельно в Gitea: налич
|
|||||||
runtime-валидацию. В обычном коде нужны инициализация или сужение типа
|
runtime-валидацию. В обычном коде нужны инициализация или сужение типа
|
||||||
после проверки значения.
|
после проверки значения.
|
||||||
|
|
||||||
|
## SonarQube
|
||||||
|
|
||||||
|
Проект `wealthledger-backend` на `https://sonarqube.ksv741.keenetic.pro`
|
||||||
|
использует `sonar-project.properties`. Анализируются исходники `src`; unit specs
|
||||||
|
и HTTP specs учитываются как тесты. Внешние T-Bank proto contracts исключены,
|
||||||
|
generated Prisma client находится за пределами `sonar.sources`.
|
||||||
|
|
||||||
|
`npm run verify` запускает unit tests с покрытием один раз. Jest штатно создаёт
|
||||||
|
`coverage/lcov.info`; scanner импортирует этот отчёт. Покрытие HTTP/e2e tests
|
||||||
|
в него не входит, сами HTTP suites остаются обязательным отдельным шагом CI.
|
||||||
|
|
||||||
|
Создайте в SonarQube проект с ключом `wealthledger-backend`, а в Gitea → backend →
|
||||||
|
Settings → Actions → Secrets добавьте `SONAR_TOKEN` с правом анализа этого проекта.
|
||||||
|
Токен не сохраняется в файлах. Community Edition 9.9.8 получает анализ только
|
||||||
|
при push в `develop`, после успешных `verify` и SQLite/e2e checks. CI ожидает
|
||||||
|
Quality Gate; отрицательный результат или ошибка scanner завершает job с ошибкой.
|
||||||
|
PR и другие ветки проходят штатные проверки без отправки анализа.
|
||||||
|
Переименование основной ветки SonarQube не требуется; `sonar.branch.name` не задаётся.
|
||||||
|
|
||||||
|
Runner должен поддерживать Docker actions и иметь доступ к GitHub, Docker Hub
|
||||||
|
и HTTPS-адресу SonarQube с действующим сертификатом для этого имени.
|
||||||
|
Официальный Docker action закреплён на commit версии 2.3.0, как в frontend.
|
||||||
|
`tsconfig.sonar.json` содержит совместимые со старым анализатором настройки
|
||||||
|
модулей и decorators с target ES2022, без новых compiler options приложения.
|
||||||
|
Штатная компиляция приложения остаётся на ES2023.
|
||||||
|
После обновления сервера пересмотрите scanner и необходимость отдельного tsconfig.
|
||||||
|
|
||||||
|
Локально после `nvm use --silent`, `npm ci` и генерации Prisma client выполните
|
||||||
|
`npm run verify`, затем `sonar-scanner` с совместимым SonarScanner CLI и
|
||||||
|
`SONAR_TOKEN` в окружении. Первый запуск CI после merge должен подтвердить
|
||||||
|
загрузку отчёта и покрытия: локальные проверки не доказывают доступность сервера.
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
- [Router conventions](.ai/CONVENTIONS.md) и [источники](.ai/SOURCES.md)
|
- [AGENTS: правила и источники](AGENTS.md)
|
||||||
- [Backend architecture](.ai/conventions/backend-architecture.md)
|
- [Backend architecture](.ai/architecture.md)
|
||||||
- [HTTP API](.ai/conventions/backend-http-api.md), [data](.ai/conventions/backend-data.md) и [testing](.ai/conventions/backend-testing.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/backend-integrations.md)
|
- [Security/auth](.ai/conventions/security-auth.md) и [integrations](.ai/conventions/integrations.md)
|
||||||
|
|
||||||
- [Карта проектных фактов](../docs/README.md)
|
- [Генерация OpenAPI](#генерация-openapi)
|
||||||
- [API contract и runtime OpenAPI](../docs/architecture/http-api.md#источник-контракта)
|
|
||||||
- [Обзор поставленной системы](../docs/overview.md)
|
|
||||||
- [Архитектурные решения](../docs/adr/README.md)
|
|
||||||
|
|
||||||
Из backend runtime OpenAPI можно read-only сверить с frontend snapshot:
|
Генерация без сервера сверяется с OpenAPI настроенного приложения:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run test:openapi-contract
|
npm run test:openapi-contract
|
||||||
```
|
```
|
||||||
|
|
||||||
Разработка идёт напрямую по задаче пользователя. Каждый affected repo проходит
|
## Генерация OpenAPI
|
||||||
свой verify; при изменении wire contract дополнительно выполняется contract check.
|
|
||||||
Обязательного OpenSpec/SDD lifecycle нет.
|
```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 и сохранность данных
|
## Реальная SQLite и сохранность данных
|
||||||
|
|
||||||
@@ -159,3 +235,39 @@ npm run test:durability -- "$baseline_dir"
|
|||||||
выберите совместимую предыдущую revision и проверьте её API-сценарии runner.
|
выберите совместимую предыдущую revision и проверьте её API-сценарии runner.
|
||||||
Durability оставляет временные DB, логи и `result.json` по пути из вывода для
|
Durability оставляет временные DB, логи и `result.json` по пути из вывода для
|
||||||
диагностики; он не откатывает schema пользовательской базы.
|
диагностики; он не откатывает schema пользовательской базы.
|
||||||
|
|
||||||
|
## Релизы
|
||||||
|
|
||||||
|
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
|
||||||
|
релизы. Работа: `codex/<task>` от актуального `develop`, PR в `develop`.
|
||||||
|
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
|
||||||
|
Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай.
|
||||||
|
|
||||||
|
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что
|
||||||
|
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
|
||||||
|
`develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в
|
||||||
|
`release/hotfix-<name>` и проходит тот же процесс.
|
||||||
|
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
|
||||||
|
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
|
||||||
|
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
|
||||||
|
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
|
||||||
|
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
|
||||||
|
открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных
|
||||||
|
`verify`, `security`, `commits` выполни fast-forward-only merge. Если `main`
|
||||||
|
изменился, согласуй историю и повтори проверки кандидата.
|
||||||
|
4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
|
||||||
|
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
|
||||||
|
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
|
||||||
|
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
|
||||||
|
5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь,
|
||||||
|
что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после
|
||||||
|
merge создай отсутствующий тег без нового bump; до merge исправь и проверь
|
||||||
|
существующий кандидат без повторного повышения версии.
|
||||||
|
|
||||||
|
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
|
||||||
|
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
|
||||||
|
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
|
||||||
|
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
|
||||||
|
администратором, обязательны `verify`, `security`, `commits`; теги `v*` защищены.
|
||||||
|
Workflow сам не включает эти настройки. Отдельное review approval для одного
|
||||||
|
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["@commitlint/config-conventional"],
|
||||||
|
rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] },
|
||||||
|
};
|
||||||
Generated
+1962
-131
File diff suppressed because it is too large
Load Diff
+15
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wealth-ledger-backend",
|
"name": "wealth-ledger-backend",
|
||||||
"version": "0.0.1",
|
"version": "0.2.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "prisma generate && nest build",
|
"build": "prisma generate && nest build",
|
||||||
|
"openapi:generate": "npm run build && node scripts/generate-openapi.cjs",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "NODE_ENV=development nest start --watch --path tsconfig.dev.json",
|
"start:dev": "NODE_ENV=development nest start --watch --path tsconfig.dev.json",
|
||||||
@@ -24,11 +25,15 @@
|
|||||||
"test:watch": "npm test -- --watch",
|
"test:watch": "npm test -- --watch",
|
||||||
"test:cov": "npm test -- --coverage",
|
"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: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:sqlite": "node test/sqlite.cjs",
|
||||||
|
"test:container": "node test/container-smoke.cjs",
|
||||||
"test:durability": "node test/bank-cards-durability.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"
|
"verify": "npm run lint && npm run typecheck:app && npm run typecheck:test && npm run test:cov -- --runInBand && npm run build",
|
||||||
|
"release:preview": "release-it --dry-run --ci",
|
||||||
|
"release:prepare": "release-it --ci",
|
||||||
|
"commitlint": "commitlint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grpc/grpc-js": "1.14.4",
|
"@grpc/grpc-js": "1.14.4",
|
||||||
@@ -53,15 +58,19 @@
|
|||||||
"passport": "0.7.0",
|
"passport": "0.7.0",
|
||||||
"passport-jwt": "4.0.1",
|
"passport-jwt": "4.0.1",
|
||||||
"pino-http": "11.0.0",
|
"pino-http": "11.0.0",
|
||||||
|
"prisma": "7.10.0",
|
||||||
"reflect-metadata": "0.2.2",
|
"reflect-metadata": "0.2.2",
|
||||||
"rxjs": "7.8.2"
|
"rxjs": "7.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "21.2.2",
|
||||||
|
"@commitlint/config-conventional": "21.2.2",
|
||||||
"@eslint/eslintrc": "3.3.7",
|
"@eslint/eslintrc": "3.3.7",
|
||||||
"@eslint/js": "10.0.1",
|
"@eslint/js": "10.0.1",
|
||||||
"@nestjs/cli": "12.0.0",
|
"@nestjs/cli": "12.0.0",
|
||||||
"@nestjs/schematics": "12.0.1",
|
"@nestjs/schematics": "12.0.1",
|
||||||
"@nestjs/testing": "12.0.1",
|
"@nestjs/testing": "12.0.1",
|
||||||
|
"@release-it/conventional-changelog": "12.0.0",
|
||||||
"@types/bcrypt": "6.0.0",
|
"@types/bcrypt": "6.0.0",
|
||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/express": "5.0.6",
|
"@types/express": "5.0.6",
|
||||||
@@ -69,13 +78,14 @@
|
|||||||
"@types/node": "24.13.4",
|
"@types/node": "24.13.4",
|
||||||
"@types/passport-jwt": "4.0.1",
|
"@types/passport-jwt": "4.0.1",
|
||||||
"@types/supertest": "7.2.1",
|
"@types/supertest": "7.2.1",
|
||||||
|
"conventional-changelog-conventionalcommits": "10.4.0",
|
||||||
"eslint": "10.10.0",
|
"eslint": "10.10.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-prettier": "5.5.6",
|
"eslint-plugin-prettier": "5.5.6",
|
||||||
"globals": "17.12.0",
|
"globals": "17.12.0",
|
||||||
"jest": "30.5.1",
|
"jest": "30.5.1",
|
||||||
"prettier": "3.9.6",
|
"prettier": "3.9.6",
|
||||||
"prisma": "7.10.0",
|
"release-it": "21.0.2",
|
||||||
"source-map-support": "0.5.21",
|
"source-map-support": "0.5.21",
|
||||||
"supertest": "7.2.2",
|
"supertest": "7.2.2",
|
||||||
"ts-jest": "29.4.12",
|
"ts-jest": "29.4.12",
|
||||||
|
|||||||
@@ -24,5 +24,8 @@
|
|||||||
],
|
],
|
||||||
"dependencyDashboardApproval": true
|
"dependencyDashboardApproval": true
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"baseBranchPatterns": [
|
||||||
|
"develop"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const { execFileSync } = require("node:child_process");
|
||||||
|
const { version } = require("../package.json");
|
||||||
|
const git = (...args) => execFileSync("git", args, { encoding: "utf8" }).trim();
|
||||||
|
|
||||||
|
assert.equal(git("status", "--porcelain"), "", "Commit all changes before preparing a release");
|
||||||
|
const tags = git("tag", "--merged", "HEAD", "--list", "v[0-9]*", "--sort=-version:refname")
|
||||||
|
.split("\n").filter(tag => /^v\d+\.\d+\.\d+$/.test(tag));
|
||||||
|
assert.equal(version, tags[0]?.slice(1) ?? "0.0.0",
|
||||||
|
"Version is already prepared or release tags are missing. Fetch tags; finish the existing release before preparing another.");
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
sonar.projectKey=wealthledger-backend
|
||||||
|
sonar.projectName=WealthLedger backend
|
||||||
|
sonar.host.url=https://sonarqube.ksv741.keenetic.pro
|
||||||
|
sonar.sourceEncoding=UTF-8
|
||||||
|
|
||||||
|
sonar.sources=src
|
||||||
|
sonar.tests=src,test
|
||||||
|
sonar.test.inclusions=src/**/*.spec.ts,test/**/*.e2e-spec.ts
|
||||||
|
sonar.exclusions=src/**/*.spec.ts,src/integrations/tbank/proto/contracts/**
|
||||||
|
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||||
|
# SonarQube 9.9 embeds an older TypeScript analyzer than the application compiler.
|
||||||
|
sonar.typescript.tsconfigPaths=tsconfig.sonar.json
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
ServiceUnavailableException,
|
ServiceUnavailableException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { existsSync, readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { StructuredLogger } from '../../common/logging/structured-logger.service';
|
import { StructuredLogger } from '../../common/logging/structured-logger.service';
|
||||||
import { Environment } from '../../config/env.validation';
|
import { Environment } from '../../config/env.validation';
|
||||||
@@ -172,13 +172,7 @@ export class TbankClient {
|
|||||||
config: ConfigService<Environment, true>,
|
config: ConfigService<Environment, true>,
|
||||||
private readonly logger: StructuredLogger,
|
private readonly logger: StructuredLogger,
|
||||||
) {
|
) {
|
||||||
const sourceProtoRoot = join(
|
const protoRoot = join(__dirname, 'proto/contracts');
|
||||||
process.cwd(),
|
|
||||||
'src/integrations/tbank/proto/contracts',
|
|
||||||
);
|
|
||||||
const protoRoot = existsSync(sourceProtoRoot)
|
|
||||||
? sourceProtoRoot
|
|
||||||
: join(process.cwd(), 'dist/integrations/tbank/proto/contracts');
|
|
||||||
const definition = loadSync(
|
const definition = loadSync(
|
||||||
[
|
[
|
||||||
'users.proto',
|
'users.proto',
|
||||||
|
|||||||
+4
-9
@@ -2,7 +2,8 @@ import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import { ConfigService } from '@nestjs/config';
|
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 { Logger } from 'nestjs-pino';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
|
||||||
@@ -21,7 +22,7 @@ export function configureApp(app: INestApplication): void {
|
|||||||
credentials: true,
|
credentials: true,
|
||||||
exposedHeaders: ['X-Request-ID'],
|
exposedHeaders: ['X-Request-ID'],
|
||||||
});
|
});
|
||||||
app.setGlobalPrefix('api/v1');
|
app.setGlobalPrefix(API_PREFIX);
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
const requestLogging = app.get(RequestLoggingMiddleware);
|
const requestLogging = app.get(RequestLoggingMiddleware);
|
||||||
app.use(requestLogging.use.bind(requestLogging));
|
app.use(requestLogging.use.bind(requestLogging));
|
||||||
@@ -36,13 +37,7 @@ async function bootstrap() {
|
|||||||
const config = app.get<ConfigService<Environment, true>>(ConfigService);
|
const config = app.get<ConfigService<Environment, true>>(ConfigService);
|
||||||
|
|
||||||
if (shouldExposeSwagger(process.env['NODE_ENV'])) {
|
if (shouldExposeSwagger(process.env['NODE_ENV'])) {
|
||||||
const document = SwaggerModule.createDocument(
|
const document = createOpenApiDocument(app);
|
||||||
app,
|
|
||||||
new DocumentBuilder()
|
|
||||||
.setTitle('WealthLedger API')
|
|
||||||
.setVersion('v1')
|
|
||||||
.build(),
|
|
||||||
);
|
|
||||||
SwaggerModule.setup('api/docs', app, document);
|
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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Real image boundary: catches missing native modules, proto assets and migrations.
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { execFileSync } = require('node:child_process');
|
||||||
|
const { randomUUID } = require('node:crypto');
|
||||||
|
|
||||||
|
const image = process.argv[2];
|
||||||
|
assert.ok(image, 'Pass a built backend image reference');
|
||||||
|
const name = `wealthledger-smoke-${randomUUID()}`;
|
||||||
|
const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8' }).trim();
|
||||||
|
const environment = [
|
||||||
|
'-e', 'DATABASE_URL=file:/data/wealthledger.db',
|
||||||
|
'-e', 'FRONTEND_ORIGIN=http://localhost',
|
||||||
|
'-e', 'JWT_SECRET=disposable-image-smoke-secret-at-least-32-characters',
|
||||||
|
'-e', 'TOKEN_ENCRYPTION_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
|
||||||
|
'-e', 'LOG_LEVEL=error',
|
||||||
|
];
|
||||||
|
|
||||||
|
async function ready() {
|
||||||
|
for (let attempt = 0; attempt < 90; attempt++) {
|
||||||
|
const state = JSON.parse(docker('inspect', name, '--format', '{{json .State}}'));
|
||||||
|
assert.equal(state.Running, true, 'Image exited before readiness');
|
||||||
|
if (state.Health.Status === 'healthy') return;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
throw new Error('Image readiness timed out');
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
docker('volume', 'create', name);
|
||||||
|
try {
|
||||||
|
assert.throws(() => docker('run', '--rm', '--read-only', image),
|
||||||
|
'Missing runtime configuration must prevent startup');
|
||||||
|
const migrate = () => docker('run', '--rm', '--read-only', '-v', `${name}:/data`,
|
||||||
|
...environment, image, 'node', 'node_modules/prisma/build/index.js', 'migrate', 'deploy');
|
||||||
|
migrate();
|
||||||
|
docker('run', '-d', '--name', name, '--read-only', '-v', `${name}:/data`, ...environment, image);
|
||||||
|
await ready();
|
||||||
|
assert.deepEqual(JSON.parse(docker('inspect', name, '--format', '{{json .HostConfig.PortBindings}}')), {});
|
||||||
|
docker('exec', name, 'node', '-e', `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
assert.notEqual(process.getuid(), 0);
|
||||||
|
assert.equal(fs.existsSync('src'), false);
|
||||||
|
assert.equal(fs.existsSync('.env'), false);
|
||||||
|
assert.equal(fs.existsSync('node_modules/typescript'), false);
|
||||||
|
assert.equal(fs.existsSync('node_modules/@nestjs/cli'), false);
|
||||||
|
fs.accessSync('dist/src/integrations/tbank/proto/contracts/users.proto');
|
||||||
|
fetch('http://localhost:3000/api/v1/auth/register', {
|
||||||
|
method: 'POST', headers: {'content-type':'application/json'},
|
||||||
|
body: JSON.stringify({email:'image-smoke@example.test',password:'fictional-image-smoke-password'})
|
||||||
|
}).then(async r => {assert.equal(r.status, 200); await r.json()}).catch(e => {console.error(e); process.exitCode=1});
|
||||||
|
`);
|
||||||
|
docker('stop', name);
|
||||||
|
migrate();
|
||||||
|
docker('start', name);
|
||||||
|
await ready();
|
||||||
|
docker('exec', name, 'node', '-e', `
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const db = require('better-sqlite3')('/data/wealthledger.db');
|
||||||
|
assert.equal(db.prepare('SELECT count(*) AS n FROM User').get().n, 1);
|
||||||
|
assert.equal(db.prepare('SELECT count(*) AS n FROM Session').get().n, 1);
|
||||||
|
assert.equal(db.pragma('integrity_check', {simple:true}), 'ok');
|
||||||
|
db.close();
|
||||||
|
`);
|
||||||
|
console.log('PASS image: fresh/existing migration, non-root read-only runtime, auth, persistence, proto assets');
|
||||||
|
} finally {
|
||||||
|
try { docker('rm', '-f', name); } finally { docker('volume', 'rm', name); }
|
||||||
|
}
|
||||||
|
})().catch((error) => { console.error(error); process.exitCode = 1; });
|
||||||
@@ -1,22 +1,12 @@
|
|||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { execFileSync } from 'node:child_process';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { AppModule } from '../src/app.module';
|
import { AppModule } from '../src/app.module';
|
||||||
import { configureApp } from '../src/main';
|
import { configureApp } from '../src/main';
|
||||||
|
import { createOpenApiDocument } from '../src/openapi';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('OpenAPI contract (integration)', () => {
|
describe('OpenAPI contract (integration)', () => {
|
||||||
let app: INestApplication;
|
let app: INestApplication;
|
||||||
@@ -32,21 +22,38 @@ describe('OpenAPI contract (integration)', () => {
|
|||||||
|
|
||||||
afterAll(() => app.close());
|
afterAll(() => app.close());
|
||||||
|
|
||||||
it('matches the committed frontend OpenAPI snapshot', async () => {
|
it('generates the runtime contract without server startup or deployment configuration', async () => {
|
||||||
const runtimeDocument = SwaggerModule.createDocument(
|
const directory = await mkdtemp(resolve(tmpdir(), 'wealthledger-openapi-'));
|
||||||
app,
|
try {
|
||||||
new DocumentBuilder()
|
const networkGuard = resolve(directory, 'no-network.cjs');
|
||||||
.setTitle('WealthLedger API')
|
await writeFile(
|
||||||
.setVersion('v1')
|
networkGuard,
|
||||||
.build(),
|
`
|
||||||
);
|
const net = require('node:net');
|
||||||
const snapshot = JSON.parse(
|
const forbidden = () => { throw new Error('Schema generation must not use the network'); };
|
||||||
await readFile(
|
net.Server.prototype.listen = forbidden;
|
||||||
resolve(__dirname, '../../frontend/src/shared/api/spec/openapi.json'),
|
net.Socket.prototype.connect = forbidden;
|
||||||
'utf8',
|
`,
|
||||||
),
|
);
|
||||||
) as unknown;
|
execFileSync(
|
||||||
|
process.execPath,
|
||||||
expect(canonicalize(runtimeDocument)).toEqual(canonicalize(snapshot));
|
[
|
||||||
|
'--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 });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"target": "ES2022",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "test/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "generated"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user