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.
|
||||
Смысл boundaries проверяется review: backend не имеет отдельного автоматического
|
||||
FSD/DI architecture checker. Existing tests и build не доказывают правильный owner.
|
||||
ESLint проверяет прямые imports: domain не зависит от transport/persistence,
|
||||
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`; не пересоздавайте его
|
||||
для обхода новых ошибок. Security, TypeScript и Jest в baseline не включены.
|
||||
|
||||
Рост baseline — изменение quality gate: он требует отдельного обоснования
|
||||
и review затронутого поведения. При рефакторинге сохраняйте или сокращайте
|
||||
baseline; прежнее число сообщений не доказывает прежнюю сложность функции.
|
||||
|
||||
## SonarQube
|
||||
|
||||
Проект `wealthledger-backend` на `https://sonarqube.personal.ksv741.ru`
|
||||
|
||||
@@ -15,6 +15,9 @@ import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const tests = ['**/*.spec.ts', 'test/**/*.ts'];
|
||||
const prismaImports = [
|
||||
'@prisma/*', '**/generated/prisma/**', '**/prisma.service',
|
||||
];
|
||||
|
||||
export default defineConfig(
|
||||
globalIgnores(['dist/**', 'dist-dev/**', 'build/**', 'generated/**', 'coverage/**', 'node_modules/**']),
|
||||
@@ -119,6 +122,52 @@ export default defineConfig(
|
||||
'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: [
|
||||
'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.
|
||||
test('machine codes require enum members without banning ordinary text or Prisma selects', async () => {
|
||||
const eslint = new ESLint();
|
||||
|
||||
Reference in New Issue
Block a user