refactor: enforce repository boundary in application (WealthLedger/backend#171)
Backend verify / commits (pull_request) Successful in 1m29s
Backend verify / security (pull_request) Successful in 1m5s
Backend verify / verify (pull_request) Successful in 6m13s
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m11s
Backend verify / verify (push) Failing after 7m34s
Backend verify / commits (pull_request) Successful in 1m29s
Backend verify / security (pull_request) Successful in 1m5s
Backend verify / verify (pull_request) Successful in 6m13s
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m11s
Backend verify / verify (push) Failing after 7m34s
This commit was merged in pull request #192.
This commit is contained in:
+6
-6
@@ -29,9 +29,9 @@ market — котировками, income — выплатами/личными
|
|||||||
domain из-за extraction в функцию. Boundary mappers преобразуют внешние records;
|
domain из-за extraction в функцию. Boundary mappers преобразуют внешние records;
|
||||||
чистые расчёты portfolio/banking остаются у domain owner.
|
чистые расчёты portfolio/banking остаются у domain owner.
|
||||||
|
|
||||||
PrismaService владеет DB lifecycle. Самостоятельные use cases получают данные
|
PrismaService владеет DB lifecycle. Application services/use cases получают
|
||||||
через concrete repositories; существующие связные CRUD/transaction services
|
данные через concrete repositories; запросы и CAS живут в infrastructure
|
||||||
сохраняют прямые queries по [data convention](conventions/data.md).
|
по [data convention](conventions/data.md).
|
||||||
Money — currency + decimal string; расчёты используют точные общие helpers.
|
Money — currency + decimal string; расчёты используют точные общие helpers.
|
||||||
|
|
||||||
Auth управляет identity/session; TbankAccessService проверяет доступ к счёту и
|
Auth управляет identity/session; TbankAccessService проверяет доступ к счёту и
|
||||||
@@ -201,9 +201,9 @@ Circular dependencies устраняй изменением ownership/compositio
|
|||||||
конкретные infrastructure providers, зарегистрированные module. Infrastructure
|
конкретные infrastructure providers, зарегистрированные module. Infrastructure
|
||||||
может использовать application types/input и domain rules; новые interfaces
|
может использовать application types/input и domain rules; новые interfaces
|
||||||
ради инверсии зависимостей не обязательны. Domain не импортирует presentation или
|
ради инверсии зависимостей не обязательны. Domain не импортирует presentation или
|
||||||
infrastructure. Существующие связные CRUD/transaction services могут сохранять прямые queries
|
infrastructure. Application не строит Prisma queries: use case и связный CRUD
|
||||||
по [Data](conventions/data.md); use case получает данные через repository и сам
|
service обращаются к репозиторию по [Data](conventions/data.md). Entity/interface
|
||||||
Prisma queries не выполняет. Entity/interface на каждую таблицу не требуется.
|
на каждую таблицу не требуется.
|
||||||
DTO остаются транспортными runtime classes; существующее использование DTO
|
DTO остаются транспортными runtime classes; существующее использование DTO
|
||||||
на application boundary допустимо по [HTTP API](conventions/http-api.md).
|
на application boundary допустимо по [HTTP API](conventions/http-api.md).
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -34,19 +34,19 @@ candidate, не повод дублировать их при новом use cas
|
|||||||
вызывает repository, затем чистый `bankSummary` и формирует ответ. Prisma records
|
вызывает repository, затем чистый `bankSummary` и формирует ответ. Prisma records
|
||||||
не становятся domain entities: расчёт получает только нужные ему значения.
|
не становятся domain entities: расчёт получает только нужные ему значения.
|
||||||
|
|
||||||
Не создавай обёртку на каждый вызов Prisma. Существующие связные CRUD/transaction
|
Application-код не импортирует PrismaService и не строит Prisma queries.
|
||||||
services (auth, lifecycle подключения, изменения карт/операций/категорий)
|
Репозиторий владеет запросом и CAS-проверкой результата, а application —
|
||||||
сохраняют queries рядом с CAS и проверками инварианта, если их перенос требует
|
правилами и формированием ответа. Типы Prisma допустимы только в type-only
|
||||||
лишь набора pass-through методов. Новая самостоятельная read/write capability
|
импортах для существующей транзакционной границы. Не создавай обёртку
|
||||||
получает repository method; самостоятельный use case не выполняет Prisma queries.
|
на каждый вызов Prisma без текущего потребителя. `BankCardsRepository.write`
|
||||||
`BankCardsRepository.write` сохраняет общую retry/transaction boundary для
|
сохраняет retry/transaction boundary для банковских записей; callback с
|
||||||
существующих banking services. Такой callback с Prisma transaction client —
|
transaction client — локальная техническая граница, не API для domain.
|
||||||
локальная техническая граница этих services, не API для domain или use cases.
|
|
||||||
|
|
||||||
Выбирай только нужные данные, сохраняй user/account ownership в запросах и
|
Выбирай только нужные данные, сохраняй user/account ownership в запросах и
|
||||||
application checks. Предварительный lookup не заменяет unique constraint при race.
|
application checks. Предварительный lookup не заменяет unique constraint при race.
|
||||||
Пример: AuthService обрабатывает P2002 и использует conditional `updateMany`
|
Пример: AuthRepository преобразует P2002 в результат регистрации и выполняет
|
||||||
для refresh rotation. Сначала пойми invariant; не копируй его flow в другой use case.
|
conditional `updateMany` для refresh rotation; AuthService сохраняет HTTP
|
||||||
|
error contract. Сначала пойми invariant; не копируй его flow в другой use case.
|
||||||
|
|
||||||
Transaction не охватывает сетевой T-Bank вызов как будто это одна atomic DB
|
Transaction не охватывает сетевой T-Bank вызов как будто это одна atomic DB
|
||||||
операция. Планируй порядок и compensation по текущей задаче; не удерживай DB
|
операция. Планируй порядок и compensation по текущей задаче; не удерживай DB
|
||||||
|
|||||||
+3
-2
@@ -158,13 +158,14 @@ export default defineConfig(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['src/**/*.use-case.ts'],
|
files: ['src/**/application/**/*.ts'],
|
||||||
ignores: tests,
|
ignores: tests,
|
||||||
rules: {
|
rules: {
|
||||||
'no-restricted-imports': ['error', {
|
'no-restricted-imports': ['error', {
|
||||||
patterns: [{
|
patterns: [{
|
||||||
group: prismaImports,
|
group: prismaImports,
|
||||||
message: 'Use cases obtain data through concrete repositories.',
|
allowTypeImports: true,
|
||||||
|
message: 'Application code obtains data through concrete repositories; type-only imports are allowed.',
|
||||||
}],
|
}],
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { JwtService } from '@nestjs/jwt';
|
|||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { createHash, randomUUID } from 'crypto';
|
import { createHash, randomUUID } from 'crypto';
|
||||||
|
|
||||||
import { Session, User } from '../../../../generated/prisma/client';
|
import type { Session, User } from '../../../../generated/prisma/client';
|
||||||
|
|
||||||
import { HttpErrorCode } from '../../../common/error-code';
|
import { HttpErrorCode } from '../../../common/error-code';
|
||||||
import { AuthRepository } from '../infrastructure/auth.repository';
|
import { AuthRepository } from '../infrastructure/auth.repository';
|
||||||
import { AuthErrorCode } from './auth-error-code';
|
import { AuthErrorCode } from './auth-error-code';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { BankCard, Prisma } from '../../../../../generated/prisma/client';
|
import type { BankCard, Prisma } from '../../../../../generated/prisma/client';
|
||||||
|
|
||||||
import { bankBalance } from '../domain/bank-card.calculations';
|
import { bankBalance } from '../domain/bank-card.calculations';
|
||||||
import { rubMoney } from '../domain/rub-money';
|
import { rubMoney } from '../domain/rub-money';
|
||||||
import { BankCardsRepository } from '../infrastructure/bank-cards.repository';
|
import { BankCardsRepository } from '../infrastructure/bank-cards.repository';
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { GoneException, Injectable } from '@nestjs/common';
|
import { GoneException, Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
BankCardOperation,
|
BankCardOperation,
|
||||||
Prisma,
|
Prisma,
|
||||||
} from '../../../../../generated/prisma/client';
|
} from '../../../../../generated/prisma/client';
|
||||||
|
|
||||||
import { FIRST_PAGE } from '../domain/bank-card-limits';
|
import { FIRST_PAGE } from '../domain/bank-card-limits';
|
||||||
import { BankOperationKind } from '../domain/bank-operation-kind';
|
import { BankOperationKind } from '../domain/bank-operation-kind';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ test('architecture imports enforce direct boundaries without banning DTOs or pur
|
|||||||
[controller, 'import { PrismaService } from "../../../prisma/prisma.service";'],
|
[controller, 'import { PrismaService } from "../../../prisma/prisma.service";'],
|
||||||
[controller, 'import { TbankClient } from "../../../integrations/tbank/tbank.api";'],
|
[controller, 'import { TbankClient } from "../../../integrations/tbank/tbank.api";'],
|
||||||
[useCase, 'import { PrismaService } from "../../../../prisma/prisma.service";'],
|
[useCase, 'import { PrismaService } from "../../../../prisma/prisma.service";'],
|
||||||
|
['src/modules/banking/bank-cards/application/bank-cards.service.ts', 'import { PrismaService } from "../../../../prisma/prisma.service";'],
|
||||||
]) {
|
]) {
|
||||||
const [result] = await eslint.lintText(code, { filePath });
|
const [result] = await eslint.lintText(code, { filePath });
|
||||||
assert.equal(result.fatalErrorCount, 0);
|
assert.equal(result.fatalErrorCount, 0);
|
||||||
@@ -113,7 +114,7 @@ test('architecture imports enforce direct boundaries without banning DTOs or pur
|
|||||||
[controller, 'import { AuthService } from "../application/auth.service";'],
|
[controller, 'import { AuthService } from "../application/auth.service";'],
|
||||||
[useCase, 'import { BankCardsRepository } from "../infrastructure/bank-cards.repository";'],
|
[useCase, 'import { BankCardsRepository } from "../infrastructure/bank-cards.repository";'],
|
||||||
[useCase, 'import { BankSummaryQueryDto } from "../presentation/bank-card.dto";'],
|
[useCase, 'import { BankSummaryQueryDto } from "../presentation/bank-card.dto";'],
|
||||||
['src/modules/banking/bank-cards/application/bank-cards.service.ts', 'import { PrismaService } from "../../../../prisma/prisma.service";'],
|
['src/modules/banking/bank-cards/application/bank-cards.service.ts', 'import type { Prisma } from "../../../../../generated/prisma/client";'],
|
||||||
]) {
|
]) {
|
||||||
const [result] = await eslint.lintText(code, { filePath });
|
const [result] = await eslint.lintText(code, { filePath });
|
||||||
assert.equal(result.fatalErrorCount, 0);
|
assert.equal(result.fatalErrorCount, 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user