@currentjs/gen 0.5.0 → 0.5.2

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 (70) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +374 -996
  3. package/dist/cli.js +28 -10
  4. package/dist/commands/createModel.d.ts +1 -0
  5. package/dist/commands/createModel.js +764 -0
  6. package/dist/commands/createModule.js +13 -0
  7. package/dist/commands/generateAll.d.ts +1 -0
  8. package/dist/commands/generateAll.js +1 -1
  9. package/dist/commands/init.d.ts +1 -0
  10. package/dist/commands/{createApp.js → init.js} +2 -2
  11. package/dist/commands/migrateCommit.js +33 -68
  12. package/dist/generators/controllerGenerator.d.ts +7 -0
  13. package/dist/generators/controllerGenerator.js +56 -17
  14. package/dist/generators/domainLayerGenerator.js +51 -8
  15. package/dist/generators/dtoGenerator.js +13 -8
  16. package/dist/generators/serviceGenerator.d.ts +6 -0
  17. package/dist/generators/serviceGenerator.js +219 -23
  18. package/dist/generators/storeGenerator.d.ts +4 -0
  19. package/dist/generators/storeGenerator.js +116 -9
  20. package/dist/generators/templateGenerator.d.ts +1 -0
  21. package/dist/generators/templateGenerator.js +8 -2
  22. package/dist/generators/templates/appTemplates.js +1 -1
  23. package/dist/generators/templates/data/cursorRulesTemplate +11 -755
  24. package/dist/generators/templates/data/frontendScriptTemplate +11 -4
  25. package/dist/generators/templates/data/mainViewTemplate +1 -0
  26. package/dist/generators/templates/storeTemplates.d.ts +1 -1
  27. package/dist/generators/templates/storeTemplates.js +3 -26
  28. package/dist/generators/useCaseGenerator.js +6 -3
  29. package/dist/types/configTypes.d.ts +6 -0
  30. package/dist/utils/migrationUtils.d.ts +9 -19
  31. package/dist/utils/migrationUtils.js +80 -110
  32. package/dist/utils/promptUtils.d.ts +37 -0
  33. package/dist/utils/promptUtils.js +149 -0
  34. package/dist/utils/typeUtils.d.ts +4 -0
  35. package/dist/utils/typeUtils.js +7 -0
  36. package/package.json +1 -1
  37. package/dist/commands/createApp.d.ts +0 -1
  38. package/dist/commands/migratePush.d.ts +0 -1
  39. package/dist/commands/migratePush.js +0 -135
  40. package/dist/commands/migrateUpdate.d.ts +0 -1
  41. package/dist/commands/migrateUpdate.js +0 -147
  42. package/dist/commands/newGenerateAll.d.ts +0 -4
  43. package/dist/commands/newGenerateAll.js +0 -336
  44. package/dist/generators/domainModelGenerator.d.ts +0 -41
  45. package/dist/generators/domainModelGenerator.js +0 -242
  46. package/dist/generators/newControllerGenerator.d.ts +0 -55
  47. package/dist/generators/newControllerGenerator.js +0 -644
  48. package/dist/generators/newServiceGenerator.d.ts +0 -19
  49. package/dist/generators/newServiceGenerator.js +0 -266
  50. package/dist/generators/newStoreGenerator.d.ts +0 -39
  51. package/dist/generators/newStoreGenerator.js +0 -408
  52. package/dist/generators/newTemplateGenerator.d.ts +0 -29
  53. package/dist/generators/newTemplateGenerator.js +0 -510
  54. package/dist/generators/storeGeneratorV2.d.ts +0 -31
  55. package/dist/generators/storeGeneratorV2.js +0 -190
  56. package/dist/generators/templates/controllerTemplates.d.ts +0 -43
  57. package/dist/generators/templates/controllerTemplates.js +0 -82
  58. package/dist/generators/templates/newStoreTemplates.d.ts +0 -5
  59. package/dist/generators/templates/newStoreTemplates.js +0 -141
  60. package/dist/generators/templates/serviceTemplates.d.ts +0 -16
  61. package/dist/generators/templates/serviceTemplates.js +0 -59
  62. package/dist/generators/templates/validationTemplates.d.ts +0 -25
  63. package/dist/generators/templates/validationTemplates.js +0 -66
  64. package/dist/generators/templates/viewTemplates.d.ts +0 -25
  65. package/dist/generators/templates/viewTemplates.js +0 -491
  66. package/dist/generators/validationGenerator.d.ts +0 -29
  67. package/dist/generators/validationGenerator.js +0 -250
  68. package/dist/utils/new_parts_of_migrationUtils.d.ts +0 -0
  69. package/dist/utils/new_parts_of_migrationUtils.js +0 -164
  70. package/howto.md +0 -667
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const cliUtils_1 = require("../utils/cliUtils");
40
40
  const yaml_1 = require("yaml");
41
+ const colors_1 = require("../utils/colors");
41
42
  function moduleYamlTemplate(moduleName) {
42
43
  const entityName = moduleName.charAt(0).toUpperCase() + moduleName.slice(1);
43
44
  const lower = moduleName.charAt(0).toLowerCase() + moduleName.slice(1);
@@ -226,4 +227,16 @@ function handleCreateModule(name) {
226
227
  appConfig.modules[moduleKey] = { path: moduleYamlRel };
227
228
  }
228
229
  fs.writeFileSync(appYamlPath, (0, yaml_1.stringify)(appConfig), 'utf8');
230
+ const output = `
231
+ ${colors_1.colors.green(`Module ${colors_1.colors.bold(name)} has been created`)}
232
+
233
+ Run command ${colors_1.colors.green(colors_1.colors.bold(`current create model ${name}:modelname`))}
234
+ where ${colors_1.colors.italic(colors_1.colors.yellow('modelname'))} is name of the model in your module.
235
+ At the end, that command will suggest you to create CRUD actions for the model and further steps.
236
+ You can run this command as many times as needed.
237
+
238
+ Alternatively, you may consider modifying module's config manually, and then run ${colors_1.colors.bold(colors_1.colors.green('current generate'))}
239
+ ${colors_1.colors.gray('config:')} ${colors_1.colors.cyan(colors_1.colors.italic(moduleYamlFile))}
240
+ `;
241
+ console.log(output);
229
242
  }
@@ -1,4 +1,5 @@
1
1
  export declare function handleGenerateAll(yamlPathArg?: string, _outArg?: string, moduleName?: string, opts?: {
2
2
  force?: boolean;
3
3
  skip?: boolean;
4
+ withTemplates?: boolean;
4
5
  }): Promise<void>;
@@ -94,7 +94,7 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
94
94
  // eslint-disable-next-line no-await-in-loop
95
95
  await controllerGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
96
96
  // eslint-disable-next-line no-await-in-loop
97
- await templateGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
97
+ await templateGen.generateAndSaveFiles(moduleYamlPath, moduleDir, { force: opts === null || opts === void 0 ? void 0 : opts.force, skipOnConflict: opts === null || opts === void 0 ? void 0 : opts.skip, onlyIfMissing: !(opts === null || opts === void 0 ? void 0 : opts.withTemplates) });
98
98
  // Find srcDir by probing upward for app.ts
99
99
  let probeDir = moduleDir;
100
100
  let srcDir = null;
@@ -0,0 +1 @@
1
+ export declare function handleInit(rawName?: string): void;
@@ -33,11 +33,11 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.handleCreateApp = handleCreateApp;
36
+ exports.handleInit = handleInit;
37
37
  const path = __importStar(require("path"));
38
38
  const cliUtils_1 = require("../utils/cliUtils");
39
39
  const appTemplates_1 = require("../generators/templates/appTemplates");
40
- function handleCreateApp(rawName) {
40
+ function handleInit(rawName) {
41
41
  const targetRoot = rawName ? (0, cliUtils_1.toAbsolute)(rawName) : process.cwd();
42
42
  (0, cliUtils_1.ensureDir)(targetRoot);
43
43
  // Basic structure using constants
@@ -39,70 +39,41 @@ const path = __importStar(require("path"));
39
39
  const yaml_1 = require("yaml");
40
40
  const colors_1 = require("../utils/colors");
41
41
  const cliUtils_1 = require("../utils/cliUtils");
42
+ const commandUtils_1 = require("../utils/commandUtils");
43
+ const configTypes_1 = require("../types/configTypes");
42
44
  const migrationUtils_1 = require("../utils/migrationUtils");
43
- function collectModelsFromYaml(yamlPath) {
44
- const yamlContent = fs.readFileSync(yamlPath, 'utf8');
45
- const config = (0, yaml_1.parse)(yamlContent);
46
- const projectRoot = path.dirname(yamlPath);
47
- const allModels = [];
45
+ function collectAggregatesFromModules(appYamlPath) {
46
+ const appConfig = (0, commandUtils_1.loadAppConfig)(appYamlPath);
47
+ const moduleEntries = (0, commandUtils_1.getModuleEntries)(appConfig);
48
+ const projectRoot = path.dirname(appYamlPath);
49
+ const allAggregates = {};
48
50
  const sources = [];
49
- // Check if it's a module YAML (has models directly)
50
- if (config.models) {
51
- allModels.push(...config.models);
52
- sources.push(`app.yaml (${config.models.length} model(s))`);
53
- }
54
- // App YAML: modules as Record<string, { path }> — resolve path and read module YAML for .models
55
- if (config.modules && typeof config.modules === 'object' && !Array.isArray(config.modules)) {
56
- let moduleCount = 0;
57
- for (const entry of Object.values(config.modules)) {
58
- const modulePath = entry.path;
59
- if (!modulePath)
60
- continue;
61
- const moduleYamlPath = path.isAbsolute(modulePath)
62
- ? modulePath
63
- : path.resolve(projectRoot, modulePath);
64
- if (fs.existsSync(moduleYamlPath)) {
65
- const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
66
- const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
67
- if (moduleConfig.models) {
68
- allModels.push(...moduleConfig.models);
69
- moduleCount++;
70
- }
71
- }
72
- }
73
- if (moduleCount > 0) {
74
- sources.push(`app.yaml modules section (${moduleCount} module(s))`);
75
- }
76
- }
77
- // Also check for module YAMLs in src/modules/*/module.yaml (as fallback)
78
- const modulesDir = path.join(projectRoot, 'src', 'modules');
79
- if (fs.existsSync(modulesDir)) {
80
- const moduleFolders = fs.readdirSync(modulesDir).filter(f => {
81
- const stat = fs.statSync(path.join(modulesDir, f));
82
- return stat.isDirectory();
83
- });
84
- let moduleYamlCount = 0;
85
- for (const moduleFolder of moduleFolders) {
86
- const moduleYamlPath = path.join(modulesDir, moduleFolder, 'module.yaml');
87
- if (fs.existsSync(moduleYamlPath)) {
88
- const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
89
- const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
90
- if (moduleConfig.models) {
91
- allModels.push(...moduleConfig.models);
92
- moduleYamlCount++;
93
- }
94
- }
51
+ for (const entry of moduleEntries) {
52
+ const moduleYamlPath = path.isAbsolute(entry.path)
53
+ ? entry.path
54
+ : path.resolve(projectRoot, entry.path);
55
+ if (!fs.existsSync(moduleYamlPath)) {
56
+ // eslint-disable-next-line no-console
57
+ console.warn(colors_1.colors.yellow(` Module YAML not found: ${moduleYamlPath}`));
58
+ continue;
95
59
  }
96
- if (moduleYamlCount > 0) {
97
- sources.push(`src/modules/*/module.yaml (${moduleYamlCount} module(s))`);
60
+ const moduleYamlContent = fs.readFileSync(moduleYamlPath, 'utf8');
61
+ const moduleConfig = (0, yaml_1.parse)(moduleYamlContent);
62
+ if (!(0, configTypes_1.isValidModuleConfig)(moduleConfig)) {
63
+ // eslint-disable-next-line no-console
64
+ console.warn(colors_1.colors.yellow(` Skipping ${moduleYamlPath}: not a valid module config (missing domain/useCases)`));
65
+ continue;
98
66
  }
67
+ const aggregates = moduleConfig.domain.aggregates;
68
+ const count = Object.keys(aggregates).length;
69
+ Object.assign(allAggregates, aggregates);
70
+ sources.push(`${entry.name} (${count} aggregate(s))`);
99
71
  }
100
- // Log sources
101
72
  if (sources.length > 0) {
102
73
  // eslint-disable-next-line no-console
103
74
  console.log(colors_1.colors.gray(` Sources: ${sources.join(', ')}`));
104
75
  }
105
- return allModels;
76
+ return allAggregates;
106
77
  }
107
78
  function handleMigrateCommit(yamlPath) {
108
79
  try {
@@ -116,24 +87,21 @@ function handleMigrateCommit(yamlPath) {
116
87
  console.log(colors_1.colors.gray(` Project root: ${projectRoot}`));
117
88
  // eslint-disable-next-line no-console
118
89
  console.log(colors_1.colors.gray(` Migrations dir: ${migrationsDir}`));
119
- // Ensure migrations directory exists
120
90
  if (!fs.existsSync(migrationsDir)) {
121
91
  fs.mkdirSync(migrationsDir, { recursive: true });
122
92
  // eslint-disable-next-line no-console
123
93
  console.log(colors_1.colors.green(` ✓ Created migrations directory`));
124
94
  }
125
- // Collect all models from YAML files
126
95
  // eslint-disable-next-line no-console
127
- console.log(colors_1.colors.cyan('\n📋 Collecting models from all modules...'));
128
- const currentModels = collectModelsFromYaml(resolvedYamlPath);
129
- if (currentModels.length === 0) {
96
+ console.log(colors_1.colors.cyan('\n📋 Collecting aggregates from all modules...'));
97
+ const currentAggregates = collectAggregatesFromModules(resolvedYamlPath);
98
+ if (Object.keys(currentAggregates).length === 0) {
130
99
  // eslint-disable-next-line no-console
131
- console.log(colors_1.colors.yellow('⚠️ No models found in YAML configuration.'));
100
+ console.log(colors_1.colors.yellow('⚠️ No aggregates found in module configuration.'));
132
101
  return;
133
102
  }
134
103
  // eslint-disable-next-line no-console
135
- console.log(colors_1.colors.green(`✓ Found ${currentModels.length} model(s): ${currentModels.map(m => m.name).join(', ')}`));
136
- // Load previous state
104
+ console.log(colors_1.colors.green(`✓ Found ${Object.keys(currentAggregates).length} aggregate(s): ${Object.keys(currentAggregates).join(', ')}`));
137
105
  const oldState = (0, migrationUtils_1.loadSchemaState)(stateFilePath);
138
106
  if (oldState) {
139
107
  // eslint-disable-next-line no-console
@@ -143,16 +111,14 @@ function handleMigrateCommit(yamlPath) {
143
111
  // eslint-disable-next-line no-console
144
112
  console.log(colors_1.colors.cyan('📖 No previous schema state found - will generate initial migration'));
145
113
  }
146
- // Compare schemas and generate SQL
147
114
  // eslint-disable-next-line no-console
148
115
  console.log(colors_1.colors.cyan('\n🔍 Comparing schemas...'));
149
- const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentModels);
116
+ const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates);
150
117
  if (sqlStatements.length === 0 || sqlStatements.every(s => s.trim() === '' || s.startsWith('--'))) {
151
118
  // eslint-disable-next-line no-console
152
119
  console.log(colors_1.colors.yellow('⚠️ No changes detected. Schema is up to date.'));
153
120
  return;
154
121
  }
155
- // Generate migration file
156
122
  const timestamp = (0, migrationUtils_1.generateTimestamp)();
157
123
  const migrationFileName = (0, migrationUtils_1.getMigrationFileName)(timestamp);
158
124
  const migrationFilePath = path.join(migrationsDir, migrationFileName);
@@ -163,9 +129,8 @@ function handleMigrateCommit(yamlPath) {
163
129
  `;
164
130
  const migrationContent = migrationHeader + sqlStatements.join('\n');
165
131
  fs.writeFileSync(migrationFilePath, migrationContent);
166
- // Update state file
167
132
  const newState = {
168
- models: currentModels,
133
+ aggregates: currentAggregates,
169
134
  version: timestamp,
170
135
  timestamp: new Date().toISOString()
171
136
  };
@@ -43,6 +43,13 @@ export declare class ControllerGenerator {
43
43
  * meant for static routes (e.g. /create).
44
44
  */
45
45
  private sortRoutesBySpecificity;
46
+ /**
47
+ * Resolve layout from YAML value.
48
+ * - undefined => use fallback (if provided)
49
+ * - "none" or "" => no layout
50
+ * - other values => use explicit layout name
51
+ */
52
+ private resolveLayout;
46
53
  private generateApiController;
47
54
  private generateWebController;
48
55
  generateFromConfig(config: ModuleConfig): Record<string, string>;
@@ -244,18 +244,26 @@ class ControllerGenerator {
244
244
  }
245
245
  `;
246
246
  }
247
- generateApiEndpointMethod(endpoint, resourceName, childInfo) {
247
+ generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo) {
248
+ var _a;
248
249
  const { model, action } = this.parseUseCase(endpoint.useCase);
249
250
  const methodName = action;
250
251
  const decorator = this.getHttpDecorator(endpoint.method);
251
252
  const useCaseVar = `${model.toLowerCase()}UseCase`;
252
253
  const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
253
254
  const outputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Output`;
255
+ const useCaseDef = (_a = useCasesConfig[model]) === null || _a === void 0 ? void 0 : _a[action];
256
+ const isVoidOutput = !(useCaseDef === null || useCaseDef === void 0 ? void 0 : useCaseDef.output) || useCaseDef.output === 'void';
254
257
  const dtoImports = new Set();
258
+ const voidOutputDtos = new Set();
255
259
  dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
260
+ if (isVoidOutput) {
261
+ voidOutputDtos.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
262
+ }
256
263
  // Generate auth check (pre-fetch)
257
264
  const authCheck = this.generateAuthCheck(endpoint.auth);
258
265
  const authLine = authCheck ? `\n ${authCheck}\n` : '';
266
+ const hasOwner = this.hasOwnerAuth(endpoint.auth);
259
267
  // Build parsing logic
260
268
  // For create: root gets ownerId from user, child gets parentId from URL params
261
269
  let parseLogic;
@@ -282,7 +290,6 @@ class ControllerGenerator {
282
290
  // Generate owner checks:
283
291
  // - For mutations (update, delete): PRE-mutation check (before operation)
284
292
  // - For reads (get): POST-fetch check (after fetching)
285
- const hasOwner = this.hasOwnerAuth(endpoint.auth);
286
293
  const isMutation = action === 'update' || action === 'delete';
287
294
  const isRead = action === 'get';
288
295
  // Pre-mutation owner check for write operations
@@ -295,22 +302,25 @@ class ControllerGenerator {
295
302
  : '';
296
303
  // Generate output transformation based on action
297
304
  let outputTransform;
298
- if (action === 'list') {
299
- outputTransform = `return ${outputClass}.from(result);`;
300
- }
301
- else if (action === 'delete') {
305
+ if (isVoidOutput || action === 'delete') {
302
306
  outputTransform = `return result;`;
303
307
  }
308
+ else if (action === 'list') {
309
+ outputTransform = `return ${outputClass}.from(result);`;
310
+ }
304
311
  else {
305
312
  outputTransform = `return ${outputClass}.from(result);`;
306
313
  }
314
+ const useCaseArgs = (hasOwner && action === 'list')
315
+ ? 'input, context.request.user?.id as number'
316
+ : 'input';
307
317
  const method = ` @${decorator}('${endpoint.path}')
308
318
  async ${methodName}(context: IContext): Promise<any> {${authLine}
309
319
  ${parseLogic}${preMutationOwnerCheck}
310
- const result = await this.${useCaseVar}.${action}(input);${postFetchOwnerCheck}
320
+ const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}
311
321
  ${outputTransform}
312
322
  }`;
313
- return { method, dtoImports };
323
+ return { method, dtoImports, voidOutputDtos };
314
324
  }
315
325
  generateWebPageMethod(page, resourceName, layout, methodIndex, childInfo, withChildChildren) {
316
326
  const method = page.method || 'GET';
@@ -333,12 +343,16 @@ class ControllerGenerator {
333
343
  const authLine = authCheck ? `\n ${authCheck}\n` : '';
334
344
  // For GET requests with views (display pages)
335
345
  if (method === 'GET' && page.view) {
336
- const renderDecorator = `\n @Render("${page.view}", "${layout}")`;
346
+ const pageLayout = this.resolveLayout(page.layout, layout);
347
+ const renderDecorator = pageLayout
348
+ ? `\n @Render("${page.view}", "${pageLayout}")`
349
+ : `\n @Render("${page.view}")`;
337
350
  if (page.useCase) {
338
351
  const { model, action } = this.parseUseCase(page.useCase);
339
352
  const useCaseVar = `${model.toLowerCase()}UseCase`;
340
353
  const inputClass = `${model}${(0, typeUtils_1.capitalize)(action)}Input`;
341
354
  dtoImports.add(`${model}${(0, typeUtils_1.capitalize)(action)}`);
355
+ const hasOwner = this.hasOwnerAuth(page.auth);
342
356
  let parseLogic;
343
357
  if (page.path.includes(':id')) {
344
358
  parseLogic = `const input = ${inputClass}.parse({ id: context.request.parameters.id });`;
@@ -349,8 +363,6 @@ class ControllerGenerator {
349
363
  else {
350
364
  parseLogic = `const input = ${inputClass}.parse({});`;
351
365
  }
352
- // Generate post-fetch owner check for GET pages (reads only)
353
- const hasOwner = this.hasOwnerAuth(page.auth);
354
366
  const isReadAction = action === 'get' || action === 'list';
355
367
  const postFetchOwnerCheck = (hasOwner && isReadAction)
356
368
  ? this.generatePostFetchOwnerCheck(page.auth, 'result', useCaseVar, childInfo)
@@ -374,12 +386,15 @@ class ControllerGenerator {
374
386
  else {
375
387
  returnExpr = 'result';
376
388
  }
389
+ const useCaseArgs = (hasOwner && action === 'list')
390
+ ? 'input, context.request.user?.id as number'
391
+ : 'input';
377
392
  const loadChildCode = loadChildBlocks.length ? '\n ' + loadChildBlocks.join('\n ') + '\n ' : '';
378
393
  const methodCode = `${renderDecorator}
379
394
  @${decorator}('${page.path}')
380
395
  async ${methodName}(context: IContext): Promise<any> {${authLine}
381
396
  ${parseLogic}
382
- const result = await this.${useCaseVar}.${action}(input);${postFetchOwnerCheck}${loadChildCode}
397
+ const result = await this.${useCaseVar}.${action}(${useCaseArgs});${postFetchOwnerCheck}${loadChildCode}
383
398
  return ${returnExpr};
384
399
  }`;
385
400
  return { method: methodCode, dtoImports };
@@ -491,26 +506,49 @@ class ControllerGenerator {
491
506
  return aParamCount - bParamCount;
492
507
  });
493
508
  }
494
- generateApiController(resourceName, prefix, endpoints, childInfo) {
509
+ /**
510
+ * Resolve layout from YAML value.
511
+ * - undefined => use fallback (if provided)
512
+ * - "none" or "" => no layout
513
+ * - other values => use explicit layout name
514
+ */
515
+ resolveLayout(layout, fallback) {
516
+ if (layout === undefined) {
517
+ return fallback;
518
+ }
519
+ const normalized = layout.trim();
520
+ if (!normalized || normalized.toLowerCase() === 'none') {
521
+ return undefined;
522
+ }
523
+ return normalized;
524
+ }
525
+ generateApiController(resourceName, prefix, endpoints, useCasesConfig, childInfo) {
495
526
  const controllerName = `${resourceName}ApiController`;
496
527
  // Determine which use cases and DTOs are referenced
497
528
  const useCaseModels = new Set();
498
529
  const allDtoImports = new Set();
530
+ const allVoidOutputDtos = new Set();
499
531
  const methods = [];
500
532
  const sortedEndpoints = this.sortRoutesBySpecificity(endpoints);
501
533
  sortedEndpoints.forEach(endpoint => {
502
534
  const { model } = this.parseUseCase(endpoint.useCase);
503
535
  useCaseModels.add(model);
504
- const { method, dtoImports } = this.generateApiEndpointMethod(endpoint, resourceName, childInfo);
536
+ const { method, dtoImports, voidOutputDtos } = this.generateApiEndpointMethod(endpoint, resourceName, useCasesConfig, childInfo);
505
537
  methods.push(method);
506
538
  dtoImports.forEach(d => allDtoImports.add(d));
539
+ voidOutputDtos.forEach(d => allVoidOutputDtos.add(d));
507
540
  });
508
541
  // Generate imports
509
542
  const useCaseImports = Array.from(useCaseModels)
510
543
  .map(model => `import { ${model}UseCase } from '../../application/useCases/${model}UseCase';`)
511
544
  .join('\n');
512
545
  const dtoImportStatements = Array.from(allDtoImports)
513
- .map(dto => `import { ${dto}Input, ${dto}Output } from '../../application/dto/${dto}';`)
546
+ .map(dto => {
547
+ if (allVoidOutputDtos.has(dto)) {
548
+ return `import { ${dto}Input } from '../../application/dto/${dto}';`;
549
+ }
550
+ return `import { ${dto}Input, ${dto}Output } from '../../application/dto/${dto}';`;
551
+ })
514
552
  .join('\n');
515
553
  // Generate constructor parameters
516
554
  const constructorParams = Array.from(useCaseModels)
@@ -602,7 +640,7 @@ ${methods.join('\n\n')}
602
640
  if (config.api) {
603
641
  Object.entries(config.api).forEach(([resourceName, resourceConfig]) => {
604
642
  const childInfo = childEntityMap.get(resourceName);
605
- const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, childInfo);
643
+ const code = this.generateApiController(resourceName, resourceConfig.prefix, resourceConfig.endpoints, config.useCases, childInfo);
606
644
  result[`${resourceName}Api`] = code;
607
645
  });
608
646
  }
@@ -610,7 +648,8 @@ ${methods.join('\n\n')}
610
648
  if (config.web) {
611
649
  Object.entries(config.web).forEach(([resourceName, resourceConfig]) => {
612
650
  const childInfo = childEntityMap.get(resourceName);
613
- const code = this.generateWebController(resourceName, resourceConfig.prefix, resourceConfig.layout || 'main_view', resourceConfig.pages, config, childInfo);
651
+ const moduleLayout = this.resolveLayout(resourceConfig.layout, 'main_view');
652
+ const code = this.generateWebController(resourceName, resourceConfig.prefix, moduleLayout, resourceConfig.pages, config, childInfo);
614
653
  result[`${resourceName}Web`] = code;
615
654
  });
616
655
  }
@@ -146,7 +146,14 @@ class DomainLayerGenerator {
146
146
  })
147
147
  .filter((imp, idx, arr) => arr.indexOf(imp) === idx) // dedupe
148
148
  .join('\n');
149
- const imports = [entityImports, valueObjectImports].filter(Boolean).join('\n');
149
+ // Generate imports for aggregate references in fields (e.g. idea: { type: Idea })
150
+ const aggregateRefImports = fields
151
+ .filter(([, fieldConfig]) => (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates) &&
152
+ fieldConfig.type !== name)
153
+ .map(([, fieldConfig]) => `import { ${fieldConfig.type} } from './${fieldConfig.type}';`)
154
+ .filter((imp, idx, arr) => arr.indexOf(imp) === idx)
155
+ .join('\n');
156
+ const imports = [entityImports, valueObjectImports, aggregateRefImports].filter(Boolean).join('\n');
150
157
  // Generate constructor parameters: id, then ownerId (root) or parentId field (child)
151
158
  const constructorParams = ['public id: number'];
152
159
  if (childInfo) {
@@ -157,9 +164,12 @@ class DomainLayerGenerator {
157
164
  }
158
165
  // Sort fields: required first, then optional
159
166
  // Fields are required by default unless required: false
167
+ // Aggregate references are always treated as optional (store can't populate them from FK alone)
160
168
  const sortedFields = fields.sort((a, b) => {
161
- const aRequired = a[1].required !== false && !a[1].auto;
162
- const bRequired = b[1].required !== false && !b[1].auto;
169
+ const aIsAggRef = (0, typeUtils_1.isAggregateReference)(a[1].type, this.availableAggregates);
170
+ const bIsAggRef = (0, typeUtils_1.isAggregateReference)(b[1].type, this.availableAggregates);
171
+ const aRequired = a[1].required !== false && !a[1].auto && !aIsAggRef;
172
+ const bRequired = b[1].required !== false && !b[1].auto && !bIsAggRef;
163
173
  if (aRequired === bRequired)
164
174
  return 0;
165
175
  return aRequired ? -1 : 1;
@@ -176,9 +186,9 @@ class DomainLayerGenerator {
176
186
  }
177
187
  });
178
188
  sortedFields.forEach(([fieldName, fieldConfig]) => {
189
+ const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
179
190
  const tsType = enumTypeNames[fieldName] || this.mapType(fieldConfig.type);
180
- // Fields are required by default, only optional if explicitly set to required: false
181
- const isOptional = fieldConfig.required === false;
191
+ const isOptional = fieldConfig.required === false || isAggRef;
182
192
  const hasDefault = fieldConfig.auto;
183
193
  let param = `public ${fieldName}`;
184
194
  if (isOptional && !hasDefault) {
@@ -195,22 +205,55 @@ class DomainLayerGenerator {
195
205
  const setterMethods = sortedFields
196
206
  .filter(([fieldName, fieldConfig]) => !fieldConfig.auto && fieldName !== 'id')
197
207
  .map(([fieldName, fieldConfig]) => {
208
+ const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
198
209
  const tsType = enumTypeNames[fieldName] || this.mapType(fieldConfig.type);
199
210
  const methodName = `set${(0, typeUtils_1.capitalize)(fieldName)}`;
200
- // Fields are required by default, only optional if explicitly set to required: false
201
- const isOptional = fieldConfig.required === false;
211
+ const isOptional = fieldConfig.required === false || isAggRef;
202
212
  return `
203
213
  ${methodName}(${fieldName}: ${tsType}${isOptional ? ' | undefined' : ''}): void {
204
214
  this.${fieldName} = ${fieldName};
205
215
  }`;
206
216
  })
207
217
  .join('\n');
218
+ // Generate validation logic from field constraints
219
+ // we don't use constraints at models, since we use DTOs (use cases) for validation
220
+ // Model – is a place not for validation, but for business logic!
221
+ // kept this code for reference
222
+ /*
223
+ const validations: string[] = [];
224
+ sortedFields.forEach(([fieldName, fieldConfig]) => {
225
+ const { constraints } = fieldConfig;
226
+ if (!constraints) return;
227
+
228
+ if (constraints.min !== undefined) {
229
+ validations.push(` if (this.${fieldName} < ${constraints.min}) {
230
+ throw new Error('${name}.${fieldName} must be at least ${constraints.min}');
231
+ }`);
232
+ }
233
+ if (constraints.max !== undefined) {
234
+ validations.push(` if (this.${fieldName} > ${constraints.max}) {
235
+ throw new Error('${name}.${fieldName} must be at most ${constraints.max}');
236
+ }`);
237
+ }
238
+ if (constraints.pattern) {
239
+ validations.push(` if (!/${constraints.pattern}/.test(String(this.${fieldName}))) {
240
+ throw new Error('${name}.${fieldName} does not match required pattern');
241
+ }`);
242
+ }
243
+ });
244
+
245
+ const constructorBody = validations.length > 0
246
+ ? `\n this.validate();\n }\n\n private validate(): void {\n${validations.join('\n')}\n }`
247
+ : ' }';
248
+ */
249
+ const constructorBody = '';
208
250
  const rootComment = config.root ? '// Aggregate Root\n' : '';
209
251
  const enumTypeDefsCode = enumTypeDefinitions.length > 0 ? enumTypeDefinitions.join('\n') + '\n\n' : '';
210
252
  return `${imports ? imports + '\n\n' : ''}${enumTypeDefsCode}${rootComment}export class ${name} {
211
253
  public constructor(
212
254
  ${constructorParamsStr}
213
- ) { }
255
+ ) {${constructorBody}
256
+ }
214
257
  ${setterMethods}
215
258
  }`;
216
259
  }
@@ -171,15 +171,17 @@ class DtoGenerator {
171
171
  fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
172
172
  if (fieldName === 'id' || fieldConfig.auto)
173
173
  return;
174
- const tsType = this.mapType(fieldConfig.type);
175
- // Fields are required by default unless partial or required: false
176
- const isRequired = !inputConfig.partial && fieldConfig.required !== false;
174
+ const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
175
+ const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
176
+ const effectiveFieldType = isAggRef ? 'number' : fieldConfig.type;
177
+ // Aggregate references are always optional in DTOs; other fields default to required
178
+ const isRequired = !isAggRef && !inputConfig.partial && fieldConfig.required !== false;
177
179
  const optional = isRequired ? '' : '?';
178
180
  fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
179
181
  constructorParams.push(`${fieldName}${optional}: ${tsType}`);
180
182
  constructorAssignments.push(` this.${fieldName} = ${fieldName};`);
181
- validationChecks.push(...this.getValidationCode(fieldName, fieldConfig.type, isRequired));
182
- fieldTransforms.push(` ${fieldName}: ${this.getTransformCode(fieldName, fieldConfig.type)}`);
183
+ validationChecks.push(...this.getValidationCode(fieldName, effectiveFieldType, isRequired));
184
+ fieldTransforms.push(` ${fieldName}: ${this.getTransformCode(fieldName, effectiveFieldType)}`);
183
185
  });
184
186
  }
185
187
  // Handle filters
@@ -267,12 +269,15 @@ ${transformsStr}
267
269
  fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
268
270
  if (fieldName === 'id')
269
271
  return;
270
- const tsType = this.mapType(fieldConfig.type);
271
- const isOptional = fieldConfig.required === false;
272
+ const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
273
+ const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
274
+ const isOptional = fieldConfig.required === false || isAggRef;
272
275
  const optional = isOptional ? '?' : '';
273
276
  fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
274
277
  constructorParams.push(`${fieldName}${optional}: ${tsType}`);
275
- fromMappings.push(` ${fieldName}: entity.${fieldName}`);
278
+ fromMappings.push(isAggRef
279
+ ? ` ${fieldName}: entity.${fieldName}?.id`
280
+ : ` ${fieldName}: entity.${fieldName}`);
276
281
  });
277
282
  }
278
283
  // Handle includes (nested objects)
@@ -2,6 +2,12 @@ import { ModuleConfig } from '../types/configTypes';
2
2
  export declare class ServiceGenerator {
3
3
  private availableAggregates;
4
4
  private mapType;
5
+ private getDefaultHandlerReturnType;
6
+ private buildHandlerContextMap;
7
+ private deriveInputType;
8
+ private deriveCustomHandlerTypes;
9
+ private getInputDtoFields;
10
+ private computeDtoFieldsForHandler;
5
11
  private generateListHandler;
6
12
  private generateGetHandler;
7
13
  private generateCreateHandler;