chore: guard backend responsibility boundaries
This commit is contained in:
+5
-2
@@ -267,5 +267,8 @@ Tests сохраняют независимые literal wire assertions; обы
|
|||||||
## Проверка
|
## Проверка
|
||||||
|
|
||||||
`npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case.
|
`npm run typecheck:app`, `npm run lint`, targeted Jest по изменённому use case.
|
||||||
Смысл boundaries проверяется review: backend не имеет отдельного автоматического
|
ESLint проверяет прямые imports: domain не зависит от transport/persistence,
|
||||||
FSD/DI architecture checker. Existing tests и build не доказывают правильный owner.
|
controllers делегируют application, use cases не импортируют Prisma. Чистые
|
||||||
|
protocol inputs разрешены; existing CRUD/transaction services сохраняют DB owner.
|
||||||
|
Транзитивные зависимости, DI composition и смысл ownership проверяются review:
|
||||||
|
backend не имеет полного FSD/DI graph checker. Tests/build не доказывают owner.
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ lint. Он не защищает от усложнения уже учтённо
|
|||||||
baseline командой `npx eslint . --prune-suppressions`; не пересоздавайте его
|
baseline командой `npx eslint . --prune-suppressions`; не пересоздавайте его
|
||||||
для обхода новых ошибок. Security, TypeScript и Jest в baseline не включены.
|
для обхода новых ошибок. Security, TypeScript и Jest в baseline не включены.
|
||||||
|
|
||||||
|
Рост baseline — изменение quality gate: он требует отдельного обоснования
|
||||||
|
и review затронутого поведения. При рефакторинге сохраняйте или сокращайте
|
||||||
|
baseline; прежнее число сообщений не доказывает прежнюю сложность функции.
|
||||||
|
|
||||||
## SonarQube
|
## SonarQube
|
||||||
|
|
||||||
Проект `wealthledger-backend` на `https://sonarqube.personal.ksv741.ru`
|
Проект `wealthledger-backend` на `https://sonarqube.personal.ksv741.ru`
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import globals from 'globals';
|
|||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
const tests = ['**/*.spec.ts', 'test/**/*.ts'];
|
const tests = ['**/*.spec.ts', 'test/**/*.ts'];
|
||||||
|
const prismaImports = [
|
||||||
|
'@prisma/*', '**/generated/prisma/**', '**/prisma.service',
|
||||||
|
];
|
||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(
|
||||||
globalIgnores(['dist/**', 'dist-dev/**', 'build/**', 'generated/**', 'coverage/**', 'node_modules/**']),
|
globalIgnores(['dist/**', 'dist-dev/**', 'build/**', 'generated/**', 'coverage/**', 'node_modules/**']),
|
||||||
@@ -119,6 +122,52 @@ export default defineConfig(
|
|||||||
'jest/expect-expect': ['error', { assertFunctionNames: ['expect', '**.expect'] }],
|
'jest/expect-expect': ['error', { assertFunctionNames: ['expect', '**.expect'] }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
files: ['src/modules/**/domain/**/*.ts'],
|
||||||
|
ignores: tests,
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': ['error', {
|
||||||
|
patterns: [{
|
||||||
|
group: [
|
||||||
|
'@nestjs/*', ...prismaImports, '**/*.dto', '**/*.repository',
|
||||||
|
'**/*.client', '**/application/**', '**/infrastructure/**',
|
||||||
|
],
|
||||||
|
message: 'Domain uses pure rules and value types, not transport or persistence.',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/**/*.controller.ts'],
|
||||||
|
ignores: tests,
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': ['error', {
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
group: [...prismaImports, '**/*.repository', '**/*.client'],
|
||||||
|
message: 'Controllers delegate to application services or use cases.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
group: ['**'],
|
||||||
|
importNamePattern: '(?:Client|Repository|PrismaService)$',
|
||||||
|
message: 'Use application orchestration rather than data providers in controllers.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['src/**/*.use-case.ts'],
|
||||||
|
ignores: tests,
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': ['error', {
|
||||||
|
patterns: [{
|
||||||
|
group: prismaImports,
|
||||||
|
message: 'Use cases obtain data through concrete repositories.',
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
files: [
|
files: [
|
||||||
'src/**/*.ts',
|
'src/**/*.ts',
|
||||||
|
|||||||
@@ -91,6 +91,36 @@ test('numeric literals require names, including previously ignored numbers and b
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('architecture imports enforce direct boundaries without banning DTOs or pure protocol inputs', async () => {
|
||||||
|
const domain = 'src/modules/banking/bank-cards/domain/rub-money.ts';
|
||||||
|
const controller = 'src/modules/auth/presentation/auth.controller.ts';
|
||||||
|
const useCase = 'src/modules/banking/bank-cards/application/get-bank-summary.use-case.ts';
|
||||||
|
for (const [filePath, code] of [
|
||||||
|
[domain, 'import { Injectable } from "@nestjs/common";'],
|
||||||
|
[domain, 'import { MoneyDto } from "../../../../common/money.dto";'],
|
||||||
|
[domain, 'import { PrismaService } from "../../../../prisma/prisma.service";'],
|
||||||
|
[controller, 'import { PrismaService } from "../../../prisma/prisma.service";'],
|
||||||
|
[controller, 'import { TbankClient } from "../../../integrations/tbank/tbank.api";'],
|
||||||
|
[useCase, 'import { PrismaService } from "../../../../prisma/prisma.service";'],
|
||||||
|
]) {
|
||||||
|
const [result] = await eslint.lintText(code, { filePath });
|
||||||
|
assert.equal(result.fatalErrorCount, 0);
|
||||||
|
assert.ok(result.messages.some(({ ruleId }) => ruleId === 'no-restricted-imports'), code);
|
||||||
|
}
|
||||||
|
for (const [filePath, code] of [
|
||||||
|
[domain, 'import type { Money } from "../../../../common/money.types";'],
|
||||||
|
[domain, 'import { TbankOperationState } from "../../../../integrations/tbank/tbank-protocol";'],
|
||||||
|
[controller, 'import { AuthService } from "../application/auth.service";'],
|
||||||
|
[useCase, 'import { BankCardsRepository } from "../infrastructure/bank-cards.repository";'],
|
||||||
|
[useCase, 'import { BankSummaryQueryDto } from "../presentation/bank-card.dto";'],
|
||||||
|
['src/modules/banking/bank-cards/application/bank-cards.service.ts', 'import { PrismaService } from "../../../../prisma/prisma.service";'],
|
||||||
|
]) {
|
||||||
|
const [result] = await eslint.lintText(code, { filePath });
|
||||||
|
assert.equal(result.fatalErrorCount, 0);
|
||||||
|
assert.ok(result.messages.every(({ ruleId }) => ruleId !== 'no-restricted-imports'), code);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Single-use codes must be caught too; fixture literals remain independent wire assertions.
|
// Single-use codes must be caught too; fixture literals remain independent wire assertions.
|
||||||
test('machine codes require enum members without banning ordinary text or Prisma selects', async () => {
|
test('machine codes require enum members without banning ordinary text or Prisma selects', async () => {
|
||||||
const eslint = new ESLint();
|
const eslint = new ESLint();
|
||||||
|
|||||||
Reference in New Issue
Block a user