@devstroupe/devkit-cli 1.1.16 → 1.2.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/boilerplates/angular-template/src/app/core/services/auth.service.ts +1 -1
- package/dist/boilerplates/angular-template/src/app/services/role.service.ts +1 -1
- package/dist/boilerplates/angular-template/src/app/services/user.service.ts +1 -1
- package/dist/boilerplates/nest-template/.dockerignore +8 -0
- package/dist/boilerplates/nest-template/package.json +13 -15
- package/dist/boilerplates/nest-template/src/app.module.ts +7 -10
- package/dist/boilerplates/nest-template/src/database/data-source.ts +4 -1
- package/dist/boilerplates/nest-template/src/database/migrations/1600000000000-create-roles.ts +2 -1
- package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +2 -2
- package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +1 -1
- package/dist/boilerplates/nest-template/src/modules/auth/auth.module.ts +2 -7
- package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +13 -11
- package/dist/boilerplates/nest-template/src/modules/auth/jwt-auth.guard.ts +5 -6
- package/dist/boilerplates/nest-template/src/modules/core/core.module.ts +9 -0
- package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.orm-entity.ts +4 -1
- package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.repository.adapter.ts +2 -2
- package/dist/boilerplates/nest-template/src/modules/role/infra/http/role.controller.ts +1 -1
- package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage.controller.ts +1 -1
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +4 -1
- package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +2 -2
- package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +1 -1
- package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +6 -3
- package/dist/boilerplates/nest-template/src/platform/security/security.module.ts +20 -0
- package/dist/boilerplates/nest-template/src/platform/security/security.tokens.ts +4 -0
- package/dist/boilerplates/nest-template/src/platform/security/token-verification.module.ts +22 -0
- package/dist/boilerplates/nest-template/tsconfig.json +1 -1
- package/dist/generation/backend-generator.integration.test.d.ts +1 -0
- package/dist/generation/backend-generator.integration.test.js +232 -0
- package/dist/generation/backend-layout.d.ts +15 -0
- package/dist/generation/backend-layout.js +77 -0
- package/dist/generation/backend-layout.test.d.ts +1 -0
- package/dist/generation/backend-layout.test.js +48 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +364 -205
- package/dist/migrations/entity-module-move.d.ts +16 -0
- package/dist/migrations/entity-module-move.js +98 -0
- package/dist/migrations/entity-module-move.test.d.ts +1 -0
- package/dist/migrations/entity-module-move.test.js +30 -0
- package/dist/rules/linter-rules.js +252 -22
- package/dist/rules/linter-rules.test.js +125 -0
- package/dist/templates/angular/service.template.d.ts +1 -1
- package/dist/templates/angular/service.template.js +2 -2
- package/dist/templates/generator-characterization.test.d.ts +1 -0
- package/dist/templates/generator-characterization.test.js +42 -0
- package/dist/templates/nest/crud.templates.d.ts +3 -3
- package/dist/templates/nest/crud.templates.js +20 -13
- package/dist/templates/nest/functional-module.templates.d.ts +10 -0
- package/dist/templates/nest/functional-module.templates.js +261 -0
- package/dist/templates/nest/index.d.ts +1 -1
- package/dist/templates/nest/index.js +1 -1
- package/dist/templates/nest/migration.template.js +6 -3
- package/dist/templates/relationship-templates.test.js +0 -4
- package/package.json +4 -4
- package/dist/boilerplates/angular-template/node_modules/.bin/acorn +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/browserslist +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/eslint +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/jiti +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/js-yaml +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/lessc +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/ng +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/ng-xi18n +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/ngc +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/nx +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/nx-cloud +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/sass +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/terser +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/tsc +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/tsserver +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/vite +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/vitest +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/webpack +0 -21
- package/dist/boilerplates/angular-template/node_modules/.bin/yaml +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/acorn +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/nest +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/prettier +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-cwd +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-esm +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-script +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-transpile-only +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/ts-script +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/tsc +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/tsserver +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-commonjs +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-esm +0 -21
- package/dist/boilerplates/nest-template/node_modules/.bin/webpack +0 -21
- package/dist/templates/nest/microservice.templates.d.ts +0 -4
- package/dist/templates/nest/microservice.templates.js +0 -157
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface EntityModuleMoveOperation {
|
|
2
|
+
kind: 'move' | 'update-config' | 'regenerate-module' | 'review-migration';
|
|
3
|
+
from?: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
export interface EntityModuleMovePlan {
|
|
8
|
+
entityName: string;
|
|
9
|
+
sourceModule: string;
|
|
10
|
+
targetModule: string;
|
|
11
|
+
backendType: 'monolith' | 'microservices';
|
|
12
|
+
operations: EntityModuleMoveOperation[];
|
|
13
|
+
conflicts: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function planEntityModuleMove(projectRoot: string, config: any, entityName: string, requestedModule: string): EntityModuleMovePlan;
|
|
16
|
+
export declare function updateEntityModuleInConfig(source: string, entityName: string, targetModule: string): string;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.planEntityModuleMove = planEntityModuleMove;
|
|
7
|
+
exports.updateEntityModuleInConfig = updateEntityModuleInConfig;
|
|
8
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const devkit_core_1 = require("@devstroupe/devkit-core");
|
|
11
|
+
function planEntityModuleMove(projectRoot, config, entityName, requestedModule) {
|
|
12
|
+
const entity = (config.entities ?? []).find((candidate) => candidate.name === entityName);
|
|
13
|
+
if (!entity)
|
|
14
|
+
throw new Error(`Entity "${entityName}" is not configured.`);
|
|
15
|
+
const targetModule = (0, devkit_core_1.normalizeEntityModule)(requestedModule);
|
|
16
|
+
const sourceModule = (0, devkit_core_1.resolveEntityModule)(entity);
|
|
17
|
+
const backendType = config.backendType === 'microservices' ? 'microservices' : 'monolith';
|
|
18
|
+
const backendRoot = node_path_1.default.resolve(projectRoot, config.backendPath ?? './backend');
|
|
19
|
+
const operations = [{
|
|
20
|
+
kind: 'update-config', detail: `Set ${entityName}.module to "${targetModule}".`,
|
|
21
|
+
}];
|
|
22
|
+
const conflicts = [];
|
|
23
|
+
if (sourceModule === targetModule) {
|
|
24
|
+
return { entityName, sourceModule, targetModule, backendType, operations: [], conflicts: [] };
|
|
25
|
+
}
|
|
26
|
+
if (backendType === 'monolith') {
|
|
27
|
+
const from = node_path_1.default.join(backendRoot, 'src', 'modules', sourceModule, entityName);
|
|
28
|
+
const to = node_path_1.default.join(backendRoot, 'src', 'modules', targetModule, entityName);
|
|
29
|
+
operations.push({ kind: 'move', from, to, detail: 'Move the complete entity directory without rewriting custom files.' });
|
|
30
|
+
operations.push({ kind: 'regenerate-module', detail: `Regenerate ${sourceModule}.module.ts and ${targetModule}.module.ts.` });
|
|
31
|
+
operations.push({ kind: 'review-migration', detail: `Update owner-module metadata for ${entityName} migrations.` });
|
|
32
|
+
if (!fs_extra_1.default.existsSync(from))
|
|
33
|
+
conflicts.push(`Source entity directory does not exist: ${from}`);
|
|
34
|
+
if (fs_extra_1.default.existsSync(to))
|
|
35
|
+
conflicts.push(`Target entity directory already exists: ${to}`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const sourceService = node_path_1.default.join(backendRoot, 'apps', `${sourceModule}-service`);
|
|
39
|
+
const migrations = node_path_1.default.join(sourceService, 'src', 'database', 'migrations');
|
|
40
|
+
operations.push({
|
|
41
|
+
kind: 'review-migration',
|
|
42
|
+
detail: 'Moving between service databases requires an explicit data/schema migration and cannot be applied automatically.',
|
|
43
|
+
});
|
|
44
|
+
if (fs_extra_1.default.existsSync(migrations) && fs_extra_1.default.readdirSync(migrations).some((file) => file.endsWith(`-create-${entityName}.ts`))) {
|
|
45
|
+
conflicts.push('Microservice ownership changes require an approved cross-database migration plan.');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
conflicts.push('Automatic microservice moves are intentionally disabled; regenerate after reviewing service contracts and data ownership.');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { entityName, sourceModule, targetModule, backendType, operations, conflicts };
|
|
52
|
+
}
|
|
53
|
+
function entityObjectRange(source, entityName) {
|
|
54
|
+
const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
55
|
+
const nameMatch = new RegExp(`['"]?name['"]?\\s*:\\s*['"]${escaped}['"]`).exec(source);
|
|
56
|
+
if (!nameMatch)
|
|
57
|
+
throw new Error(`Could not locate entity "${entityName}" in devstroupe.config.ts.`);
|
|
58
|
+
const start = source.lastIndexOf('{', nameMatch.index);
|
|
59
|
+
if (start < 0)
|
|
60
|
+
throw new Error(`Could not locate object start for entity "${entityName}".`);
|
|
61
|
+
let depth = 0;
|
|
62
|
+
let quote;
|
|
63
|
+
let escapedCharacter = false;
|
|
64
|
+
for (let index = start; index < source.length; index += 1) {
|
|
65
|
+
const character = source[index];
|
|
66
|
+
if (quote) {
|
|
67
|
+
if (escapedCharacter)
|
|
68
|
+
escapedCharacter = false;
|
|
69
|
+
else if (character === '\\')
|
|
70
|
+
escapedCharacter = true;
|
|
71
|
+
else if (character === quote)
|
|
72
|
+
quote = undefined;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (character === "'" || character === '"' || character === '`') {
|
|
76
|
+
quote = character;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (character === '{')
|
|
80
|
+
depth += 1;
|
|
81
|
+
if (character === '}' && --depth === 0)
|
|
82
|
+
return { start, end: index + 1 };
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Could not locate object end for entity "${entityName}".`);
|
|
85
|
+
}
|
|
86
|
+
function updateEntityModuleInConfig(source, entityName, targetModule) {
|
|
87
|
+
const normalized = (0, devkit_core_1.normalizeEntityModule)(targetModule);
|
|
88
|
+
const range = entityObjectRange(source, entityName);
|
|
89
|
+
const objectSource = source.slice(range.start, range.end);
|
|
90
|
+
const modulePattern = /\n(\s*)['"]?module['"]?\s*:\s*['"][^'"]*['"]\s*,?/;
|
|
91
|
+
const nameLine = /\n(\s*)['"]?name['"]?\s*:\s*['"][^'"]*['"]\s*,?/;
|
|
92
|
+
const updatedObject = modulePattern.test(objectSource)
|
|
93
|
+
? objectSource.replace(modulePattern, (_match, indent) => `\n${indent}module: '${normalized}',`)
|
|
94
|
+
: objectSource.replace(nameLine, (match, indent) => `${match}\n${indent}module: '${normalized}',`);
|
|
95
|
+
if (updatedObject === objectSource)
|
|
96
|
+
throw new Error(`Entity "${entityName}" must use a multiline object literal for safe migration.`);
|
|
97
|
+
return source.slice(0, range.start) + updatedObject + source.slice(range.end);
|
|
98
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
7
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
11
|
+
const entity_module_move_1 = require("./entity-module-move");
|
|
12
|
+
(0, node_test_1.default)('entity module migration defaults to a non-destructive plan', () => {
|
|
13
|
+
const root = fs_extra_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), 'devkit-module-move-'));
|
|
14
|
+
fs_extra_1.default.ensureDirSync(node_path_1.default.join(root, 'backend/src/modules/core/product'));
|
|
15
|
+
const plan = (0, entity_module_move_1.planEntityModuleMove)(root, {
|
|
16
|
+
backendType: 'monolith', backendPath: './backend',
|
|
17
|
+
entities: [{ name: 'product', properties: [] }],
|
|
18
|
+
}, 'product', 'Sales Catalog');
|
|
19
|
+
strict_1.default.equal(plan.sourceModule, 'core');
|
|
20
|
+
strict_1.default.equal(plan.targetModule, 'sales-catalog');
|
|
21
|
+
strict_1.default.equal(plan.conflicts.length, 0);
|
|
22
|
+
strict_1.default.ok(fs_extra_1.default.existsSync(node_path_1.default.join(root, 'backend/src/modules/core/product')));
|
|
23
|
+
strict_1.default.ok(!fs_extra_1.default.existsSync(node_path_1.default.join(root, 'backend/src/modules/sales-catalog/product')));
|
|
24
|
+
});
|
|
25
|
+
(0, node_test_1.default)('config updater changes only the requested entity object', () => {
|
|
26
|
+
const source = `entities: [\n {\n name: 'product',\n properties: [],\n },\n {\n name: 'stock',\n module: 'inventory',\n properties: [],\n },\n]`;
|
|
27
|
+
const updated = (0, entity_module_move_1.updateEntityModuleInConfig)(source, 'product', 'Catalog');
|
|
28
|
+
strict_1.default.match(updated, /name: 'product',\n module: 'catalog',/);
|
|
29
|
+
strict_1.default.match(updated, /name: 'stock',\n module: 'inventory',/);
|
|
30
|
+
});
|
|
@@ -32,10 +32,71 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.runLinter = runLinter;
|
|
37
40
|
const fs = __importStar(require("fs-extra"));
|
|
38
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const devkit_core_1 = require("@devstroupe/devkit-core");
|
|
43
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
44
|
+
function visit(node, inspect) {
|
|
45
|
+
inspect(node);
|
|
46
|
+
typescript_1.default.forEachChild(node, (child) => visit(child, inspect));
|
|
47
|
+
}
|
|
48
|
+
function sourceLine(source, node) {
|
|
49
|
+
return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
50
|
+
}
|
|
51
|
+
function calledMethod(node) {
|
|
52
|
+
if (!typescript_1.default.isCallExpression(node) || !typescript_1.default.isPropertyAccessExpression(node.expression))
|
|
53
|
+
return undefined;
|
|
54
|
+
return node.expression.name.text;
|
|
55
|
+
}
|
|
56
|
+
function nodeCallsMethod(source, method) {
|
|
57
|
+
let found = false;
|
|
58
|
+
visit(source, (node) => { if (calledMethod(node) === method)
|
|
59
|
+
found = true; });
|
|
60
|
+
return found;
|
|
61
|
+
}
|
|
62
|
+
function callsMethod(source, method) {
|
|
63
|
+
return nodeCallsMethod(source, method);
|
|
64
|
+
}
|
|
65
|
+
function hasTenantScopedBase(source) {
|
|
66
|
+
let found = false;
|
|
67
|
+
visit(source, (node) => {
|
|
68
|
+
if (!typescript_1.default.isCallExpression(node) || node.expression.kind !== typescript_1.default.SyntaxKind.SuperKeyword)
|
|
69
|
+
return;
|
|
70
|
+
for (const argument of node.arguments) {
|
|
71
|
+
if (!typescript_1.default.isObjectLiteralExpression(argument))
|
|
72
|
+
continue;
|
|
73
|
+
const scope = argument.properties.find((property) => {
|
|
74
|
+
if (!('name' in property) || !property.name)
|
|
75
|
+
return false;
|
|
76
|
+
return (typescript_1.default.isIdentifier(property.name) || typescript_1.default.isStringLiteralLike(property.name))
|
|
77
|
+
&& property.name.text === 'scope';
|
|
78
|
+
});
|
|
79
|
+
if (scope && nodeCallsMethod(scope, 'requireTenantId'))
|
|
80
|
+
found = true;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return found;
|
|
84
|
+
}
|
|
85
|
+
function usesDecorator(source, decoratorName) {
|
|
86
|
+
let found = false;
|
|
87
|
+
visit(source, (node) => {
|
|
88
|
+
if (!typescript_1.default.canHaveDecorators(node))
|
|
89
|
+
return;
|
|
90
|
+
for (const decorator of typescript_1.default.getDecorators(node) ?? []) {
|
|
91
|
+
const expression = typescript_1.default.isCallExpression(decorator.expression)
|
|
92
|
+
? decorator.expression.expression
|
|
93
|
+
: decorator.expression;
|
|
94
|
+
if (typescript_1.default.isIdentifier(expression) && expression.text === decoratorName)
|
|
95
|
+
found = true;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return found;
|
|
99
|
+
}
|
|
39
100
|
// Helper para ler arquivos recursivamente com filtros de extensão
|
|
40
101
|
function getFilesRecursively(dir, extensions) {
|
|
41
102
|
let results = [];
|
|
@@ -79,22 +140,155 @@ function runLinter(projectRoot, config) {
|
|
|
79
140
|
const severityCleanArch = getRuleSeverity('clean-architecture-imports', 'warn');
|
|
80
141
|
const severitySynchronize = getRuleSeverity('disable-typeorm-synchronize', 'error');
|
|
81
142
|
const severitySpartanRadius = getRuleSeverity('no-custom-spartan-radius', 'error');
|
|
143
|
+
const severityModuleOwnership = getRuleSeverity('backend-module-ownership', 'error');
|
|
144
|
+
const severityModuleIsolation = getRuleSeverity('backend-module-isolation', 'error');
|
|
145
|
+
const severitySoftDelete = getRuleSeverity('require-soft-delete', 'error');
|
|
146
|
+
const severityTenantScope = getRuleSeverity('require-tenant-scope', 'error');
|
|
147
|
+
const severityStatelessGateway = getRuleSeverity('backend-gateway-stateless', 'error');
|
|
82
148
|
const backendRoot = path.resolve(projectRoot, config.backendPath || './backend');
|
|
83
149
|
const frontendRoot = path.resolve(projectRoot, config.frontendPath || './frontend');
|
|
150
|
+
const configuredEntities = config.entities ?? [];
|
|
151
|
+
const entityModules = new Map();
|
|
152
|
+
for (const entity of configuredEntities) {
|
|
153
|
+
try {
|
|
154
|
+
entityModules.set(entity.name, (0, devkit_core_1.resolveEntityModule)(entity));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (severityModuleOwnership !== 'off') {
|
|
158
|
+
violations.push({
|
|
159
|
+
file: 'devstroupe.config.ts', line: 1, rule: 'backend-module-ownership',
|
|
160
|
+
severity: severityModuleOwnership,
|
|
161
|
+
message: error instanceof Error ? error.message : `Módulo inválido para ${entity.name}.`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
84
166
|
// --- 1. Auditando o Backend (NestJS) ---
|
|
85
167
|
if (fs.existsSync(backendRoot)) {
|
|
86
168
|
const backendFiles = getFilesRecursively(backendRoot, ['.ts']);
|
|
87
169
|
filesScanned += backendFiles.length;
|
|
170
|
+
if (severityModuleOwnership !== 'off') {
|
|
171
|
+
for (const [entityName, expectedModule] of entityModules) {
|
|
172
|
+
const ownershipPattern = new RegExp(`(?:^|/)src/modules/([^/]+)/${entityName}/`);
|
|
173
|
+
const misplaced = backendFiles.find((candidate) => {
|
|
174
|
+
const match = path.relative(projectRoot, candidate).replace(/\\/g, '/').match(ownershipPattern);
|
|
175
|
+
return match && match[1] !== expectedModule;
|
|
176
|
+
});
|
|
177
|
+
if (misplaced) {
|
|
178
|
+
violations.push({
|
|
179
|
+
file: path.relative(projectRoot, misplaced), line: 1, rule: 'backend-module-ownership',
|
|
180
|
+
severity: severityModuleOwnership,
|
|
181
|
+
message: `A entidade "${entityName}" pertence ao módulo "${expectedModule}" e não pode ser gerada em outro módulo.`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
88
186
|
for (const file of backendFiles) {
|
|
89
187
|
const content = fs.readFileSync(file, 'utf-8');
|
|
90
|
-
const
|
|
188
|
+
const sourceFile = typescript_1.default.createSourceFile(file, content, typescript_1.default.ScriptTarget.Latest, true, typescript_1.default.ScriptKind.TS);
|
|
91
189
|
const relativePath = path.relative(projectRoot, file);
|
|
190
|
+
const normalizedBackendPath = relativePath.replace(/\\/g, '/');
|
|
191
|
+
const businessCode = !/\.(?:test|spec)\.ts$/.test(file) && !normalizedBackendPath.includes('/migrations/');
|
|
192
|
+
if (severityStatelessGateway !== 'off' && config.backendType === 'microservices') {
|
|
193
|
+
const gatewaySource = normalizedBackendPath.includes('/api-gateway/src/');
|
|
194
|
+
const entityGatewayController = /\/api-gateway\/src\/modules\/[^/]+\/[^/]+\/.*\.controller\.ts$/.test(normalizedBackendPath);
|
|
195
|
+
const statefulGatewayCode = /TypeOrmModule|@Entity\b|\bRepository\s*</.test(content);
|
|
196
|
+
const rpcCrudGatewayCode = /\bClientProxy\b|@MessagePattern\b/.test(content);
|
|
197
|
+
if (gatewaySource && (normalizedBackendPath.includes('/src/database/') || entityGatewayController || statefulGatewayCode || rpcCrudGatewayCode)) {
|
|
198
|
+
violations.push({
|
|
199
|
+
file: relativePath, line: 1, rule: 'backend-gateway-stateless',
|
|
200
|
+
severity: severityStatelessGateway,
|
|
201
|
+
message: 'O API Gateway deve ser stateless e rotear por módulo: sem TypeORM, banco, CRUD por entidade ou tradução RPC por operação.',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const serviceMain = /\/apps\/[^/]+-service\/src\/main\.ts$/.test(normalizedBackendPath);
|
|
205
|
+
if (serviceMain && /createMicroservice|Transport\.TCP/.test(content)) {
|
|
206
|
+
violations.push({
|
|
207
|
+
file: relativePath, line: 1, rule: 'backend-gateway-stateless',
|
|
208
|
+
severity: severityStatelessGateway,
|
|
209
|
+
message: 'CRUD síncrono dos módulos deve ser exposto por HTTP; TCP/mensageria permanece para fluxos assíncronos.',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (severityModuleIsolation !== 'off') {
|
|
214
|
+
const ownerMatch = normalizedBackendPath.match(/(?:^|\/)src\/modules\/([^/]+)\//);
|
|
215
|
+
const ownerModule = ownerMatch?.[1];
|
|
216
|
+
if (ownerModule) {
|
|
217
|
+
for (const statement of sourceFile.statements) {
|
|
218
|
+
if (!typescript_1.default.isImportDeclaration(statement) && !typescript_1.default.isExportDeclaration(statement))
|
|
219
|
+
continue;
|
|
220
|
+
const moduleSpecifier = statement.moduleSpecifier;
|
|
221
|
+
if (!moduleSpecifier || !typescript_1.default.isStringLiteralLike(moduleSpecifier))
|
|
222
|
+
continue;
|
|
223
|
+
const specifier = moduleSpecifier.text;
|
|
224
|
+
if (!specifier.startsWith('.'))
|
|
225
|
+
continue;
|
|
226
|
+
const resolved = path.resolve(path.dirname(file), specifier).replace(/\\/g, '/');
|
|
227
|
+
const targetMatch = resolved.match(/\/src\/modules\/([^/]+)\//);
|
|
228
|
+
const targetModule = targetMatch?.[1];
|
|
229
|
+
const isInfrastructure = /\/(?:infra|infrastructure)\/|\.repository$|\.orm-entity$/.test(resolved);
|
|
230
|
+
const identityModules = new Set(['auth', 'user', 'role']);
|
|
231
|
+
const sameBuiltInIdentityBoundary = identityModules.has(ownerModule) && identityModules.has(targetModule ?? '');
|
|
232
|
+
if (targetModule && targetModule !== ownerModule && !sameBuiltInIdentityBoundary && isInfrastructure) {
|
|
233
|
+
violations.push({
|
|
234
|
+
file: relativePath,
|
|
235
|
+
line: sourceLine(sourceFile, statement),
|
|
236
|
+
rule: 'backend-module-isolation',
|
|
237
|
+
severity: severityModuleIsolation,
|
|
238
|
+
message: `O módulo "${ownerModule}" não pode importar infraestrutura do módulo "${targetModule}". Use ID, contrato ou evento.`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const persistenceCode = /(?:\.repository|\.repository\.adapter|\.reader)\.ts$/.test(file);
|
|
245
|
+
if (severitySoftDelete !== 'off' && businessCode && usesDecorator(sourceFile, 'Entity') && !usesDecorator(sourceFile, 'DeleteDateColumn')) {
|
|
246
|
+
violations.push({
|
|
247
|
+
file: relativePath, line: 1, rule: 'require-soft-delete',
|
|
248
|
+
severity: severitySoftDelete,
|
|
249
|
+
message: 'Toda entity persistente deve declarar @DeleteDateColumn; restore continua somente operacional/manual.',
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (severitySoftDelete !== 'off' && businessCode && persistenceCode) {
|
|
253
|
+
visit(sourceFile, (node) => {
|
|
254
|
+
const method = calledMethod(node);
|
|
255
|
+
if (!method || !['delete', 'remove', 'clear'].includes(method))
|
|
256
|
+
return;
|
|
257
|
+
const call = node;
|
|
258
|
+
const receiver = call.expression.expression;
|
|
259
|
+
const delegatesToSafeBase = method === 'delete' && receiver.kind === typescript_1.default.SyntaxKind.SuperKeyword;
|
|
260
|
+
if (!delegatesToSafeBase) {
|
|
261
|
+
violations.push({
|
|
262
|
+
file: relativePath, line: sourceLine(sourceFile, node), rule: 'require-soft-delete',
|
|
263
|
+
severity: severitySoftDelete,
|
|
264
|
+
message: 'Hard delete não é permitido em código de negócio. Use soft delete; restore permanece operacional/manual.',
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (severityTenantScope !== 'off' && file.endsWith('.repository.ts')) {
|
|
270
|
+
const entityName = path.basename(file, '.repository.ts');
|
|
271
|
+
const entity = configuredEntities.find((candidate) => candidate.name === entityName);
|
|
272
|
+
const tenantScoped = Boolean(config.multiTenant?.enabled && entity && entity.tenantScoped !== false);
|
|
273
|
+
if (tenantScoped && (!callsMethod(sourceFile, 'requireTenantId') || !hasTenantScopedBase(sourceFile))) {
|
|
274
|
+
violations.push({
|
|
275
|
+
file: relativePath, line: 1, rule: 'require-tenant-scope',
|
|
276
|
+
severity: severityTenantScope,
|
|
277
|
+
message: `Repository tenant-scoped "${entityName}" deve fornecer scope fail-closed do BaseRepository com executionContext.requireTenantId() para queries, updates e deletes.`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
92
281
|
if (severitySynchronize !== 'off') {
|
|
93
|
-
|
|
94
|
-
if (
|
|
282
|
+
visit(sourceFile, (node) => {
|
|
283
|
+
if (!typescript_1.default.isPropertyAssignment(node))
|
|
284
|
+
return;
|
|
285
|
+
const name = node.name;
|
|
286
|
+
const isSynchronize = (typescript_1.default.isIdentifier(name) || typescript_1.default.isStringLiteralLike(name))
|
|
287
|
+
&& name.text === 'synchronize';
|
|
288
|
+
if (isSynchronize && node.initializer.kind === typescript_1.default.SyntaxKind.TrueKeyword) {
|
|
95
289
|
violations.push({
|
|
96
290
|
file: relativePath,
|
|
97
|
-
line:
|
|
291
|
+
line: sourceLine(sourceFile, node),
|
|
98
292
|
rule: 'disable-typeorm-synchronize',
|
|
99
293
|
severity: severitySynchronize,
|
|
100
294
|
message: 'Use migrations TypeORM e mantenha "synchronize: false" para evitar alterações automáticas no schema.'
|
|
@@ -105,57 +299,83 @@ function runLinter(projectRoot, config) {
|
|
|
105
299
|
// Regra: clean-architecture-imports
|
|
106
300
|
// Impede que arquivos sob subdiretórios de domínio importem infra/controllers
|
|
107
301
|
if (severityCleanArch !== 'off' && (file.includes('/domain/') || file.includes('/entities/'))) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
302
|
+
for (const statement of sourceFile.statements) {
|
|
303
|
+
if (!typescript_1.default.isImportDeclaration(statement) && !typescript_1.default.isExportDeclaration(statement))
|
|
304
|
+
continue;
|
|
305
|
+
const moduleSpecifier = statement.moduleSpecifier;
|
|
306
|
+
if (!moduleSpecifier || !typescript_1.default.isStringLiteralLike(moduleSpecifier))
|
|
307
|
+
continue;
|
|
308
|
+
if (/(?:\/infra\/|\/infrastructure\/|\/controllers\/|\/modules\/|\.controller|\.module)/i.test(moduleSpecifier.text)) {
|
|
114
309
|
violations.push({
|
|
115
310
|
file: relativePath,
|
|
116
|
-
line:
|
|
311
|
+
line: sourceLine(sourceFile, statement),
|
|
117
312
|
rule: 'clean-architecture-imports',
|
|
118
313
|
severity: severityCleanArch,
|
|
119
314
|
message: 'Camada de Domínio/Entidade não deve importar classes da camada de Infraestrutura, Controladores ou Módulos.'
|
|
120
315
|
});
|
|
121
316
|
}
|
|
122
|
-
}
|
|
317
|
+
}
|
|
123
318
|
}
|
|
124
319
|
// Regra: force-pagination-in-backend
|
|
125
320
|
// Garante que repositórios/controllers evitem .find() brutas ou .getMany() brutas
|
|
126
321
|
if (severityForcePagination !== 'off') {
|
|
127
|
-
const hasRepositoryOrController = file.endsWith('.repository.ts') || file.endsWith('.controller.ts') || file.endsWith('.service.ts');
|
|
322
|
+
const hasRepositoryOrController = file.endsWith('.repository.ts') || file.endsWith('.repository.adapter.ts') || file.endsWith('.controller.ts') || file.endsWith('.service.ts') || file.endsWith('.reader.ts');
|
|
128
323
|
if (hasRepositoryOrController) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
324
|
+
const hasPhysicalLimit = callsMethod(sourceFile, 'take') || callsMethod(sourceFile, 'limit');
|
|
325
|
+
visit(sourceFile, (node) => {
|
|
326
|
+
if (!typescript_1.default.isCallExpression(node))
|
|
132
327
|
return;
|
|
133
|
-
|
|
134
|
-
|
|
328
|
+
const method = calledMethod(node);
|
|
329
|
+
const emptyFindOptions = node.arguments.length === 1
|
|
330
|
+
&& typescript_1.default.isObjectLiteralExpression(node.arguments[0])
|
|
331
|
+
&& node.arguments[0].properties.length === 0;
|
|
332
|
+
if (method === 'find' && (node.arguments.length === 0 || emptyFindOptions)) {
|
|
135
333
|
violations.push({
|
|
136
334
|
file: relativePath,
|
|
137
|
-
line:
|
|
335
|
+
line: sourceLine(sourceFile, node),
|
|
138
336
|
rule: 'force-pagination-in-backend',
|
|
139
337
|
severity: severityForcePagination,
|
|
140
338
|
message: 'Evite usar consultas brutas de tabela cheia ".find()". Prefira a paginação padrão do repositório ("findMany").'
|
|
141
339
|
});
|
|
142
340
|
}
|
|
143
|
-
if (
|
|
341
|
+
if (method === 'getMany' && !hasPhysicalLimit) {
|
|
144
342
|
violations.push({
|
|
145
343
|
file: relativePath,
|
|
146
|
-
line:
|
|
344
|
+
line: sourceLine(sourceFile, node),
|
|
147
345
|
rule: 'force-pagination-in-backend',
|
|
148
346
|
severity: severityForcePagination,
|
|
149
347
|
message: 'Evite chamar ".getMany()" no QueryBuilder sem especificar limites de paginação (".take" ou ".limit").'
|
|
150
348
|
});
|
|
151
349
|
}
|
|
350
|
+
if (method === 'getRawMany' && !hasPhysicalLimit) {
|
|
351
|
+
violations.push({
|
|
352
|
+
file: relativePath,
|
|
353
|
+
line: sourceLine(sourceFile, node),
|
|
354
|
+
rule: 'force-pagination-in-backend',
|
|
355
|
+
severity: severityForcePagination,
|
|
356
|
+
message: 'Evite chamar ".getRawMany()" sem um limite físico explícito.',
|
|
357
|
+
});
|
|
358
|
+
}
|
|
152
359
|
});
|
|
153
360
|
}
|
|
154
361
|
}
|
|
155
362
|
// Regra: use-standard-devkit-queries (Backend)
|
|
156
363
|
if (severityStandardQueries !== 'off') {
|
|
157
364
|
// Controllers devem herdar de BaseCrudController
|
|
158
|
-
|
|
365
|
+
const customInputAdapter = normalizedBackendPath.includes('/api/webhooks/')
|
|
366
|
+
|| file.endsWith('.webhook.controller.ts');
|
|
367
|
+
const configuredCrudController = configuredEntities.some((entity) => {
|
|
368
|
+
const ownerModule = entityModules.get(entity.name);
|
|
369
|
+
return (ownerModule && normalizedBackendPath.endsWith(`/modules/${ownerModule}/${entity.name}/api/${entity.name}.controller.ts`))
|
|
370
|
+
|| normalizedBackendPath.endsWith(`/modules/${entity.name}/infra/http/${entity.name}.controller.ts`);
|
|
371
|
+
});
|
|
372
|
+
const conventionalCrudController = /@(Post|Get|Put|Patch|Delete)\b/.test(content)
|
|
373
|
+
&& /@Delete\b/.test(content)
|
|
374
|
+
&& /@(Put|Patch)\b/.test(content);
|
|
375
|
+
const requiresCrudBase = configuredEntities.length === 0
|
|
376
|
+
|| configuredCrudController
|
|
377
|
+
|| conventionalCrudController;
|
|
378
|
+
if (file.endsWith('.controller.ts') && requiresCrudBase && !customInputAdapter && !file.endsWith('auth.controller.ts') && !file.endsWith('storage.controller.ts') && !file.endsWith('storage-download.controller.ts') && content.includes('@Controller') && !content.includes('@MessagePattern') && !content.includes('GatewayController') && !content.includes('extends BaseCrudController')) {
|
|
159
379
|
violations.push({
|
|
160
380
|
file: relativePath,
|
|
161
381
|
line: 1,
|
|
@@ -176,6 +396,16 @@ function runLinter(projectRoot, config) {
|
|
|
176
396
|
}
|
|
177
397
|
}
|
|
178
398
|
}
|
|
399
|
+
if (severityStatelessGateway !== 'off' && config.backendType === 'microservices') {
|
|
400
|
+
const composePath = path.join(projectRoot, 'docker-compose.yml');
|
|
401
|
+
if (fs.existsSync(composePath) && fs.readFileSync(composePath, 'utf-8').includes('devstroupe_gateway_identity')) {
|
|
402
|
+
violations.push({
|
|
403
|
+
file: 'docker-compose.yml', line: 1, rule: 'backend-gateway-stateless',
|
|
404
|
+
severity: severityStatelessGateway,
|
|
405
|
+
message: 'O gateway não possui banco de identidade; User, Role e Auth pertencem ao Core.',
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
179
409
|
}
|
|
180
410
|
// --- 2. Auditando o Frontend (Angular) ---
|
|
181
411
|
if (fs.existsSync(frontendRoot)) {
|
|
@@ -212,3 +212,128 @@ function teardownSandbox() {
|
|
|
212
212
|
const violations = result.violations.filter(v => v.rule === 'disable-typeorm-synchronize');
|
|
213
213
|
assert.strictEqual(violations.length, 1);
|
|
214
214
|
});
|
|
215
|
+
(0, node_test_1.test)('runLinter enforces module isolation, soft delete and trusted tenant scope', () => {
|
|
216
|
+
setupSandbox();
|
|
217
|
+
const productRoot = path.join(sandboxDir, 'backend/src/modules/catalog/product');
|
|
218
|
+
const stockRoot = path.join(sandboxDir, 'backend/src/modules/inventory/stock');
|
|
219
|
+
fs.ensureDirSync(path.join(productRoot, 'infrastructure/persistence/typeorm'));
|
|
220
|
+
fs.ensureDirSync(path.join(stockRoot, 'infrastructure/persistence/typeorm'));
|
|
221
|
+
fs.ensureDirSync(path.join(sandboxDir, 'backend/src/modules/wrong/product/api'));
|
|
222
|
+
fs.writeFileSync(path.join(sandboxDir, 'backend/src/modules/wrong/product/api/product.controller.ts'), `@Controller('product') export class ProductController extends BaseCrudController<any> {}`);
|
|
223
|
+
fs.writeFileSync(path.join(productRoot, 'infrastructure/persistence/typeorm/product.orm-entity.ts'), 'export class ProductOrmEntity {}');
|
|
224
|
+
fs.writeFileSync(path.join(stockRoot, 'infrastructure/persistence/typeorm/stock.repository.ts'), `import { Injectable } from '@nestjs/common';
|
|
225
|
+
import { ProductOrmEntity } from '../../../../../catalog/product/infrastructure/persistence/typeorm/product.orm-entity';
|
|
226
|
+
@Injectable()
|
|
227
|
+
export class StockRepository extends BaseRepository<any> {
|
|
228
|
+
removeStock() { return this.repository.delete(1); }
|
|
229
|
+
unsafeReport() { return this.queryBuilder.getRawMany(); }
|
|
230
|
+
}`);
|
|
231
|
+
const result = (0, linter_rules_1.runLinter)(sandboxDir, {
|
|
232
|
+
backendPath: './backend', frontendPath: './frontend',
|
|
233
|
+
multiTenant: { enabled: true },
|
|
234
|
+
entities: [
|
|
235
|
+
{ name: 'product', module: 'catalog', properties: [] },
|
|
236
|
+
{ name: 'stock', module: 'inventory', properties: [] },
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
teardownSandbox();
|
|
240
|
+
const rules = result.violations.map((violation) => violation.rule);
|
|
241
|
+
assert.ok(rules.includes('backend-module-isolation'));
|
|
242
|
+
assert.ok(rules.includes('backend-module-ownership'));
|
|
243
|
+
assert.ok(rules.includes('require-soft-delete'));
|
|
244
|
+
assert.ok(rules.includes('require-tenant-scope'));
|
|
245
|
+
assert.ok(rules.includes('force-pagination-in-backend'));
|
|
246
|
+
});
|
|
247
|
+
(0, node_test_1.test)('runLinter accepts generated module boundaries and scoped soft-delete repositories', () => {
|
|
248
|
+
setupSandbox();
|
|
249
|
+
const repositoryDir = path.join(sandboxDir, 'backend/src/modules/catalog/product/infrastructure/persistence/typeorm');
|
|
250
|
+
fs.ensureDirSync(repositoryDir);
|
|
251
|
+
fs.writeFileSync(path.join(repositoryDir, 'product.repository.ts'), `import { Injectable } from '@nestjs/common';
|
|
252
|
+
@Injectable()
|
|
253
|
+
export class ProductRepository extends BaseRepository<any> {
|
|
254
|
+
constructor() { super(repository, { scope: () => ({ tenantId: executionContext.requireTenantId() }) }); }
|
|
255
|
+
}`);
|
|
256
|
+
const result = (0, linter_rules_1.runLinter)(sandboxDir, {
|
|
257
|
+
backendPath: './backend', frontendPath: './frontend',
|
|
258
|
+
multiTenant: { enabled: true },
|
|
259
|
+
entities: [{ name: 'product', module: 'catalog', properties: [] }],
|
|
260
|
+
});
|
|
261
|
+
teardownSandbox();
|
|
262
|
+
assert.equal(result.violations.filter((violation) => [
|
|
263
|
+
'backend-module-isolation', 'backend-module-ownership', 'require-soft-delete', 'require-tenant-scope',
|
|
264
|
+
].includes(violation.rule)).length, 0);
|
|
265
|
+
});
|
|
266
|
+
(0, node_test_1.test)('runLinter audits repository adapters and accepts delegation to the safe base delete', () => {
|
|
267
|
+
setupSandbox();
|
|
268
|
+
const repositoryDir = path.join(sandboxDir, 'backend/src/modules/catalog/product/infrastructure/persistence/typeorm');
|
|
269
|
+
fs.ensureDirSync(repositoryDir);
|
|
270
|
+
const repositoryPath = path.join(repositoryDir, 'product.repository.adapter.ts');
|
|
271
|
+
fs.writeFileSync(repositoryPath, `
|
|
272
|
+
export class ProductRepositoryAdapter extends BaseRepository<any> {
|
|
273
|
+
unbounded() { return this.repository.find(); }
|
|
274
|
+
delete(id: number) { return super.delete(id); }
|
|
275
|
+
}
|
|
276
|
+
`);
|
|
277
|
+
const result = (0, linter_rules_1.runLinter)(sandboxDir, {
|
|
278
|
+
backendPath: './backend', frontendPath: './frontend',
|
|
279
|
+
entities: [{ name: 'product', module: 'catalog', properties: [] }],
|
|
280
|
+
});
|
|
281
|
+
teardownSandbox();
|
|
282
|
+
assert.ok(result.violations.some((violation) => violation.rule === 'force-pagination-in-backend' && violation.file.endsWith('product.repository.adapter.ts')));
|
|
283
|
+
assert.equal(result.violations.filter((violation) => violation.rule === 'require-soft-delete').length, 0);
|
|
284
|
+
});
|
|
285
|
+
(0, node_test_1.test)('runLinter allows a custom webhook input adapter without treating it as CRUD', () => {
|
|
286
|
+
setupSandbox();
|
|
287
|
+
const webhookDir = path.join(sandboxDir, 'backend/src/modules/billing/payments/api/webhooks');
|
|
288
|
+
fs.ensureDirSync(webhookDir);
|
|
289
|
+
fs.writeFileSync(path.join(webhookDir, 'provider-payment.webhook.controller.ts'), `
|
|
290
|
+
import { Controller, Post } from '@nestjs/common';
|
|
291
|
+
@Controller('webhooks/provider-payment')
|
|
292
|
+
export class ProviderPaymentWebhookController {
|
|
293
|
+
@Post() receive() { return processor.process(request); }
|
|
294
|
+
}
|
|
295
|
+
`);
|
|
296
|
+
const result = (0, linter_rules_1.runLinter)(sandboxDir, { backendPath: './backend', frontendPath: './frontend' });
|
|
297
|
+
teardownSandbox();
|
|
298
|
+
assert.equal(result.violations.filter((violation) => violation.file.endsWith('provider-payment.webhook.controller.ts')
|
|
299
|
+
&& violation.rule === 'use-standard-devkit-queries').length, 0);
|
|
300
|
+
});
|
|
301
|
+
(0, node_test_1.test)('runLinter requires DeleteDateColumn on every persistent entity', () => {
|
|
302
|
+
setupSandbox();
|
|
303
|
+
const entityDir = path.join(sandboxDir, 'backend/src/modules/catalog/product/infrastructure/persistence/typeorm');
|
|
304
|
+
fs.ensureDirSync(entityDir);
|
|
305
|
+
fs.writeFileSync(path.join(entityDir, 'product.orm-entity.ts'), `
|
|
306
|
+
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
|
|
307
|
+
@Entity('products')
|
|
308
|
+
export class ProductOrmEntity { @PrimaryGeneratedColumn() id!: number; }
|
|
309
|
+
`);
|
|
310
|
+
const result = (0, linter_rules_1.runLinter)(sandboxDir, {
|
|
311
|
+
backendPath: './backend', frontendPath: './frontend',
|
|
312
|
+
entities: [{ name: 'product', module: 'catalog', properties: [] }],
|
|
313
|
+
});
|
|
314
|
+
teardownSandbox();
|
|
315
|
+
assert.equal(result.violations.filter((violation) => violation.rule === 'require-soft-delete').length, 1);
|
|
316
|
+
assert.match(result.violations.find((violation) => violation.rule === 'require-soft-delete').message, /DeleteDateColumn/);
|
|
317
|
+
});
|
|
318
|
+
(0, node_test_1.test)('runLinter requires a stateless module-routing gateway in microservice mode', () => {
|
|
319
|
+
setupSandbox();
|
|
320
|
+
const gatewayModule = path.join(sandboxDir, 'backend/api-gateway/src');
|
|
321
|
+
const serviceRoot = path.join(sandboxDir, 'backend/apps/catalog-service/src');
|
|
322
|
+
fs.ensureDirSync(path.join(gatewayModule, 'modules/catalog/product'));
|
|
323
|
+
fs.ensureDirSync(serviceRoot);
|
|
324
|
+
fs.writeFileSync(path.join(gatewayModule, 'app.module.ts'), `
|
|
325
|
+
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
326
|
+
export class AppModule {}
|
|
327
|
+
`);
|
|
328
|
+
fs.writeFileSync(path.join(gatewayModule, 'modules/catalog/product/product.controller.ts'), `
|
|
329
|
+
import { ClientProxy } from '@nestjs/microservices';
|
|
330
|
+
export class ProductGatewayController {}
|
|
331
|
+
`);
|
|
332
|
+
fs.writeFileSync(path.join(serviceRoot, 'main.ts'), `NestFactory.createMicroservice(AppModule, { transport: Transport.TCP });`);
|
|
333
|
+
fs.writeFileSync(path.join(sandboxDir, 'docker-compose.yml'), 'DB_NAME: devstroupe_gateway_identity\n');
|
|
334
|
+
const result = (0, linter_rules_1.runLinter)(sandboxDir, {
|
|
335
|
+
backendType: 'microservices', backendPath: './backend', frontendPath: './frontend',
|
|
336
|
+
});
|
|
337
|
+
teardownSandbox();
|
|
338
|
+
assert.ok(result.violations.filter((violation) => violation.rule === 'backend-gateway-stateless').length >= 4);
|
|
339
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function angularServiceTemplate(name: string): string;
|
|
1
|
+
export declare function angularServiceTemplate(name: string, moduleName?: string): string;
|