@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.
Files changed (90) hide show
  1. package/README.md +18 -0
  2. package/dist/boilerplates/angular-template/src/app/core/services/auth.service.ts +1 -1
  3. package/dist/boilerplates/angular-template/src/app/services/role.service.ts +1 -1
  4. package/dist/boilerplates/angular-template/src/app/services/user.service.ts +1 -1
  5. package/dist/boilerplates/nest-template/.dockerignore +8 -0
  6. package/dist/boilerplates/nest-template/package.json +13 -15
  7. package/dist/boilerplates/nest-template/src/app.module.ts +7 -10
  8. package/dist/boilerplates/nest-template/src/database/data-source.ts +4 -1
  9. package/dist/boilerplates/nest-template/src/database/migrations/1600000000000-create-roles.ts +2 -1
  10. package/dist/boilerplates/nest-template/src/database/migrations/1700000000000-create-users.ts +2 -2
  11. package/dist/boilerplates/nest-template/src/modules/auth/auth.controller.ts +1 -1
  12. package/dist/boilerplates/nest-template/src/modules/auth/auth.module.ts +2 -7
  13. package/dist/boilerplates/nest-template/src/modules/auth/auth.service.ts +13 -11
  14. package/dist/boilerplates/nest-template/src/modules/auth/jwt-auth.guard.ts +5 -6
  15. package/dist/boilerplates/nest-template/src/modules/core/core.module.ts +9 -0
  16. package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.orm-entity.ts +4 -1
  17. package/dist/boilerplates/nest-template/src/modules/role/infra/database/role.repository.adapter.ts +2 -2
  18. package/dist/boilerplates/nest-template/src/modules/role/infra/http/role.controller.ts +1 -1
  19. package/dist/boilerplates/nest-template/src/modules/storage/infra/http/storage.controller.ts +1 -1
  20. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.orm-entity.ts +4 -1
  21. package/dist/boilerplates/nest-template/src/modules/user/infra/database/user.repository.adapter.ts +2 -2
  22. package/dist/boilerplates/nest-template/src/modules/user/infra/http/user.controller.ts +1 -1
  23. package/dist/boilerplates/nest-template/src/modules/user/service/user.service.ts +6 -3
  24. package/dist/boilerplates/nest-template/src/platform/security/security.module.ts +20 -0
  25. package/dist/boilerplates/nest-template/src/platform/security/security.tokens.ts +4 -0
  26. package/dist/boilerplates/nest-template/src/platform/security/token-verification.module.ts +22 -0
  27. package/dist/boilerplates/nest-template/tsconfig.json +1 -1
  28. package/dist/generation/backend-generator.integration.test.d.ts +1 -0
  29. package/dist/generation/backend-generator.integration.test.js +232 -0
  30. package/dist/generation/backend-layout.d.ts +15 -0
  31. package/dist/generation/backend-layout.js +77 -0
  32. package/dist/generation/backend-layout.test.d.ts +1 -0
  33. package/dist/generation/backend-layout.test.js +48 -0
  34. package/dist/index.d.ts +4 -1
  35. package/dist/index.js +364 -205
  36. package/dist/migrations/entity-module-move.d.ts +16 -0
  37. package/dist/migrations/entity-module-move.js +98 -0
  38. package/dist/migrations/entity-module-move.test.d.ts +1 -0
  39. package/dist/migrations/entity-module-move.test.js +30 -0
  40. package/dist/rules/linter-rules.js +252 -22
  41. package/dist/rules/linter-rules.test.js +125 -0
  42. package/dist/templates/angular/service.template.d.ts +1 -1
  43. package/dist/templates/angular/service.template.js +2 -2
  44. package/dist/templates/generator-characterization.test.d.ts +1 -0
  45. package/dist/templates/generator-characterization.test.js +42 -0
  46. package/dist/templates/nest/crud.templates.d.ts +3 -3
  47. package/dist/templates/nest/crud.templates.js +20 -13
  48. package/dist/templates/nest/functional-module.templates.d.ts +10 -0
  49. package/dist/templates/nest/functional-module.templates.js +261 -0
  50. package/dist/templates/nest/index.d.ts +1 -1
  51. package/dist/templates/nest/index.js +1 -1
  52. package/dist/templates/nest/migration.template.js +6 -3
  53. package/dist/templates/relationship-templates.test.js +0 -4
  54. package/package.json +4 -4
  55. package/dist/boilerplates/angular-template/node_modules/.bin/acorn +0 -21
  56. package/dist/boilerplates/angular-template/node_modules/.bin/browserslist +0 -21
  57. package/dist/boilerplates/angular-template/node_modules/.bin/eslint +0 -21
  58. package/dist/boilerplates/angular-template/node_modules/.bin/jiti +0 -21
  59. package/dist/boilerplates/angular-template/node_modules/.bin/js-yaml +0 -21
  60. package/dist/boilerplates/angular-template/node_modules/.bin/lessc +0 -21
  61. package/dist/boilerplates/angular-template/node_modules/.bin/ng +0 -21
  62. package/dist/boilerplates/angular-template/node_modules/.bin/ng-xi18n +0 -21
  63. package/dist/boilerplates/angular-template/node_modules/.bin/ngc +0 -21
  64. package/dist/boilerplates/angular-template/node_modules/.bin/nx +0 -21
  65. package/dist/boilerplates/angular-template/node_modules/.bin/nx-cloud +0 -21
  66. package/dist/boilerplates/angular-template/node_modules/.bin/sass +0 -21
  67. package/dist/boilerplates/angular-template/node_modules/.bin/terser +0 -21
  68. package/dist/boilerplates/angular-template/node_modules/.bin/tsc +0 -21
  69. package/dist/boilerplates/angular-template/node_modules/.bin/tsserver +0 -21
  70. package/dist/boilerplates/angular-template/node_modules/.bin/vite +0 -21
  71. package/dist/boilerplates/angular-template/node_modules/.bin/vitest +0 -21
  72. package/dist/boilerplates/angular-template/node_modules/.bin/webpack +0 -21
  73. package/dist/boilerplates/angular-template/node_modules/.bin/yaml +0 -21
  74. package/dist/boilerplates/nest-template/node_modules/.bin/acorn +0 -21
  75. package/dist/boilerplates/nest-template/node_modules/.bin/nest +0 -21
  76. package/dist/boilerplates/nest-template/node_modules/.bin/prettier +0 -21
  77. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node +0 -21
  78. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-cwd +0 -21
  79. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-esm +0 -21
  80. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-script +0 -21
  81. package/dist/boilerplates/nest-template/node_modules/.bin/ts-node-transpile-only +0 -21
  82. package/dist/boilerplates/nest-template/node_modules/.bin/ts-script +0 -21
  83. package/dist/boilerplates/nest-template/node_modules/.bin/tsc +0 -21
  84. package/dist/boilerplates/nest-template/node_modules/.bin/tsserver +0 -21
  85. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm +0 -21
  86. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-commonjs +0 -21
  87. package/dist/boilerplates/nest-template/node_modules/.bin/typeorm-ts-node-esm +0 -21
  88. package/dist/boilerplates/nest-template/node_modules/.bin/webpack +0 -21
  89. package/dist/templates/nest/microservice.templates.d.ts +0 -4
  90. 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 lines = content.split('\n');
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
- lines.forEach((line, index) => {
94
- if (/\bsynchronize\s*:\s*true\b/.test(line)) {
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: index + 1,
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
- lines.forEach((line, index) => {
109
- const trimmed = line.trim();
110
- if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*'))
111
- return;
112
- // Import contendo infra, controller, module ou similar
113
- if (/import\s+.*\s+from\s+['"].*(?:\/infra\/|\/controllers\/|\/modules\/|\.controller|\.module).*['"]/i.test(line)) {
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: index + 1,
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
- lines.forEach((line, index) => {
130
- const trimmed = line.trim();
131
- if (trimmed.startsWith('//') || trimmed.startsWith('*') || trimmed.startsWith('/*'))
324
+ const hasPhysicalLimit = callsMethod(sourceFile, 'take') || callsMethod(sourceFile, 'limit');
325
+ visit(sourceFile, (node) => {
326
+ if (!typescript_1.default.isCallExpression(node))
132
327
  return;
133
- // .find() sem argumentos ou sem filtros limitados, ou getMany() direto no queryBuilder sem skip/take/limit/take
134
- if (/\.find\(\s*(?:\{\s*\})?\s*\)/.test(line)) {
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: index + 1,
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 (/\.getMany\(\)/.test(line) && !content.includes('.take(') && !content.includes('.limit(')) {
341
+ if (method === 'getMany' && !hasPhysicalLimit) {
144
342
  violations.push({
145
343
  file: relativePath,
146
- line: index + 1,
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
- if (file.endsWith('.controller.ts') && !file.endsWith('auth.controller.ts') && !file.endsWith('storage.controller.ts') && !file.endsWith('storage-download.controller.ts') && content.includes('@Controller') && !content.includes('extends BaseCrudController')) {
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;