@currentjs/gen 0.5.5 → 0.5.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.7] - 2026-04-08
4
+
5
+ - HTTP errors in generated controllers
6
+
7
+ ## [0.5.6] - 2026-04-08
8
+
9
+ - database identifier types (numeric, uuid, nanoid)
10
+
3
11
  ## [0.5.5] - 2026-04-07
4
12
 
5
13
  - data access layer generation fixes and improvements
package/README.md CHANGED
@@ -492,6 +492,8 @@ Collects all aggregate definitions from module YAMLs, compares them against the
492
492
 
493
493
  The migration file contains `CREATE TABLE`, `ALTER TABLE ADD/MODIFY/DROP COLUMN`, and `DROP TABLE` statements as needed. Foreign keys, indexes, and standard timestamp columns (`created_at`, `updated_at`, `deleted_at`) are handled automatically.
494
494
 
495
+ The SQL types of primary keys and foreign keys are determined by the `config.identifiers` setting in `app.yaml`.
496
+
495
497
  After generating the file, the schema state is updated so the next `migrate commit` only produces a diff of subsequent changes.
496
498
 
497
499
  ### `migrate push` *(not yet implemented)*
@@ -81,18 +81,19 @@ async function handleGenerateAll(yamlPathArg, _outArg, moduleName, opts) {
81
81
  const moduleDir = path.dirname(moduleYamlPath);
82
82
  // eslint-disable-next-line no-console
83
83
  console.log(colors_1.colors.blue(`\nGenerating module: ${path.basename(moduleDir)}`));
84
+ const identifiers = entry.identifiers;
84
85
  // eslint-disable-next-line no-await-in-loop
85
- await domainGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
86
+ await domainGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
86
87
  // eslint-disable-next-line no-await-in-loop
87
- await dtoGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
88
+ await dtoGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
88
89
  // eslint-disable-next-line no-await-in-loop
89
- await useCaseGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
90
+ await useCaseGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
90
91
  // eslint-disable-next-line no-await-in-loop
91
- await serviceGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
92
+ await serviceGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
92
93
  // eslint-disable-next-line no-await-in-loop
93
- await storeGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
94
+ await storeGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
94
95
  // eslint-disable-next-line no-await-in-loop
95
- await controllerGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts);
96
+ await controllerGen.generateAndSaveFiles(moduleYamlPath, moduleDir, opts, identifiers);
96
97
  // eslint-disable-next-line no-await-in-loop
97
98
  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
99
  // Find srcDir by probing upward for app.ts
@@ -43,12 +43,15 @@ const commandUtils_1 = require("../utils/commandUtils");
43
43
  const configTypes_1 = require("../types/configTypes");
44
44
  const migrationUtils_1 = require("../utils/migrationUtils");
45
45
  function collectSchemaFromModules(appYamlPath) {
46
+ var _a, _b;
46
47
  const appConfig = (0, commandUtils_1.loadAppConfig)(appYamlPath);
47
48
  const moduleEntries = (0, commandUtils_1.getModuleEntries)(appConfig);
48
49
  const projectRoot = path.dirname(appYamlPath);
49
50
  const allAggregates = {};
50
51
  const allValueObjects = new Set();
51
52
  const sources = [];
53
+ const rawIdentifiers = (_b = (_a = appConfig.config) === null || _a === void 0 ? void 0 : _a.identifiers) !== null && _b !== void 0 ? _b : 'numeric';
54
+ const identifiers = (0, configTypes_1.normalizeIdentifierType)(rawIdentifiers);
52
55
  for (const entry of moduleEntries) {
53
56
  const moduleYamlPath = path.isAbsolute(entry.path)
54
57
  ? entry.path
@@ -79,7 +82,7 @@ function collectSchemaFromModules(appYamlPath) {
79
82
  // eslint-disable-next-line no-console
80
83
  console.log(colors_1.colors.gray(` Sources: ${sources.join(', ')}`));
81
84
  }
82
- return { aggregates: allAggregates, valueObjects: allValueObjects };
85
+ return { aggregates: allAggregates, valueObjects: allValueObjects, identifiers };
83
86
  }
84
87
  function handleMigrateCommit(yamlPath) {
85
88
  try {
@@ -100,7 +103,7 @@ function handleMigrateCommit(yamlPath) {
100
103
  }
101
104
  // eslint-disable-next-line no-console
102
105
  console.log(colors_1.colors.cyan('\nšŸ“‹ Collecting aggregates from all modules...'));
103
- const { aggregates: currentAggregates, valueObjects: currentValueObjects } = collectSchemaFromModules(resolvedYamlPath);
106
+ const { aggregates: currentAggregates, valueObjects: currentValueObjects, identifiers } = collectSchemaFromModules(resolvedYamlPath);
104
107
  if (Object.keys(currentAggregates).length === 0) {
105
108
  // eslint-disable-next-line no-console
106
109
  console.log(colors_1.colors.yellow('āš ļø No aggregates found in module configuration.'));
@@ -119,7 +122,7 @@ function handleMigrateCommit(yamlPath) {
119
122
  }
120
123
  // eslint-disable-next-line no-console
121
124
  console.log(colors_1.colors.cyan('\nšŸ” Comparing schemas...'));
122
- const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates, currentValueObjects);
125
+ const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates, currentValueObjects, identifiers);
123
126
  if (sqlStatements.length === 0 || sqlStatements.every(s => s.trim() === '' || s.startsWith('--'))) {
124
127
  // eslint-disable-next-line no-console
125
128
  console.log(colors_1.colors.yellow('āš ļø No changes detected. Schema is up to date.'));
@@ -1,5 +1,6 @@
1
- import { ModuleConfig } from '../types/configTypes';
1
+ import { ModuleConfig, IdentifierType } from '../types/configTypes';
2
2
  export declare class ControllerGenerator {
3
+ private identifiers;
3
4
  private getHttpDecorator;
4
5
  private parseUseCase;
5
6
  /**
@@ -10,6 +11,11 @@ export declare class ControllerGenerator {
10
11
  * Check if auth config includes owner permission
11
12
  */
12
13
  private hasOwnerAuth;
14
+ /**
15
+ * Determine which HTTP error classes from @currentjs/router are needed
16
+ * based on the auth configs of all endpoints in a controller.
17
+ */
18
+ private getNeededHttpErrorImports;
13
19
  /**
14
20
  * Generate pre-fetch authentication/authorization check code.
15
21
  * This runs before fetching the entity and validates authentication and role-based access.
@@ -52,10 +58,10 @@ export declare class ControllerGenerator {
52
58
  private resolveLayout;
53
59
  private generateApiController;
54
60
  private generateWebController;
55
- generateFromConfig(config: ModuleConfig): Record<string, string>;
56
- generateFromYamlFile(yamlFilePath: string): Record<string, string>;
61
+ generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, string>;
62
+ generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, string>;
57
63
  generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
58
64
  force?: boolean;
59
65
  skipOnConflict?: boolean;
60
- }): Promise<string[]>;
66
+ }, identifiers?: IdentifierType): Promise<string[]>;
61
67
  }
@@ -44,6 +44,9 @@ const childEntityUtils_1 = require("../utils/childEntityUtils");
44
44
  const typeUtils_1 = require("../utils/typeUtils");
45
45
  const constants_1 = require("../utils/constants");
46
46
  class ControllerGenerator {
47
+ constructor() {
48
+ this.identifiers = 'numeric';
49
+ }
47
50
  getHttpDecorator(method) {
48
51
  switch (method.toUpperCase()) {
49
52
  case 'GET': return 'Get';
@@ -75,6 +78,28 @@ class ControllerGenerator {
75
78
  const roles = this.normalizeAuth(auth);
76
79
  return roles.includes(constants_1.AUTH_ROLES.OWNER);
77
80
  }
81
+ /**
82
+ * Determine which HTTP error classes from @currentjs/router are needed
83
+ * based on the auth configs of all endpoints in a controller.
84
+ */
85
+ getNeededHttpErrorImports(auths) {
86
+ const needed = new Set();
87
+ for (const auth of auths) {
88
+ const roles = this.normalizeAuth(auth);
89
+ if (roles.length === 0 || (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.ALL))
90
+ continue;
91
+ needed.add('UnauthorizedError');
92
+ const roleChecks = roles.filter(r => r !== constants_1.AUTH_ROLES.OWNER && r !== constants_1.AUTH_ROLES.ALL && r !== constants_1.AUTH_ROLES.AUTHENTICATED);
93
+ if (roleChecks.length > 0) {
94
+ needed.add('ForbiddenError');
95
+ }
96
+ if (roles.includes(constants_1.AUTH_ROLES.OWNER)) {
97
+ needed.add('ForbiddenError');
98
+ needed.add('NotFoundError');
99
+ }
100
+ }
101
+ return Array.from(needed).sort();
102
+ }
78
103
  /**
79
104
  * Generate pre-fetch authentication/authorization check code.
80
105
  * This runs before fetching the entity and validates authentication and role-based access.
@@ -89,14 +114,14 @@ class ControllerGenerator {
89
114
  // If only 'authenticated' is specified
90
115
  if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.AUTHENTICATED) {
91
116
  return `if (!context.request.user) {
92
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
117
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
93
118
  }`;
94
119
  }
95
120
  // If only 'owner' is specified - just require authentication here
96
121
  // (owner check happens post-fetch)
97
122
  if (roles.length === 1 && roles[0] === constants_1.AUTH_ROLES.OWNER) {
98
123
  return `if (!context.request.user) {
99
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
124
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
100
125
  }`;
101
126
  }
102
127
  // Filter out 'owner' and 'all' for role checks (owner is checked post-fetch)
@@ -108,16 +133,16 @@ class ControllerGenerator {
108
133
  if (roleChecks.length === 0) {
109
134
  // Only owner (and maybe authenticated) - just require auth
110
135
  return `if (!context.request.user) {
111
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
136
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
112
137
  }`;
113
138
  }
114
139
  if (roleChecks.length === 1 && !hasOwner) {
115
140
  // Single role check
116
141
  return `if (!context.request.user) {
117
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
142
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
118
143
  }
119
144
  if (context.request.user.role !== '${roleChecks[0]}') {
120
- throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: ${roleChecks[0]} role required');
145
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: ${roleChecks[0]} role required');
121
146
  }`;
122
147
  }
123
148
  // Multiple roles OR owner - use OR logic
@@ -126,22 +151,22 @@ class ControllerGenerator {
126
151
  if (hasOwner) {
127
152
  // With owner: require auth, role check will be combined with owner check post-fetch
128
153
  return `if (!context.request.user) {
129
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
154
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
130
155
  }`;
131
156
  }
132
157
  // Multiple roles without owner - check if user has ANY of the roles
133
158
  const roleConditions = roleChecks.map(r => `context.request.user.role === '${r}'`).join(' || ');
134
159
  return `if (!context.request.user) {
135
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
160
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
136
161
  }
137
162
  if (!(${roleConditions})) {
138
- throw new Error('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: one of [${roleChecks.join(', ')}] role required');
163
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.INSUFFICIENT_PERMISSIONS}: one of [${roleChecks.join(', ')}] role required');
139
164
  }`;
140
165
  }
141
166
  // Only 'authenticated' in the mix
142
167
  if (hasAuthenticated) {
143
168
  return `if (!context.request.user) {
144
- throw new Error('${constants_1.AUTH_ERRORS.REQUIRED}');
169
+ throw new UnauthorizedError('${constants_1.AUTH_ERRORS.REQUIRED}');
145
170
  }`;
146
171
  }
147
172
  return '';
@@ -165,10 +190,10 @@ class ControllerGenerator {
165
190
  // Owner validation (post-fetch for reads, via parent)
166
191
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
167
192
  if (resourceOwnerId === null) {
168
- throw new Error('Resource not found');
193
+ throw new NotFoundError('Resource not found');
169
194
  }
170
195
  if (resourceOwnerId !== context.request.user?.id) {
171
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
196
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
172
197
  }`;
173
198
  }
174
199
  const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
@@ -176,12 +201,12 @@ class ControllerGenerator {
176
201
  // Owner validation (post-fetch for reads, via parent, bypassed for: ${bypassRoles.join(', ')})
177
202
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(${resultVar}.id);
178
203
  if (resourceOwnerId === null) {
179
- throw new Error('Resource not found');
204
+ throw new NotFoundError('Resource not found');
180
205
  }
181
206
  const isOwner = resourceOwnerId === context.request.user?.id;
182
207
  const hasPrivilegedRole = ${bypassConditions};
183
208
  if (!isOwner && !hasPrivilegedRole) {
184
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
209
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
185
210
  }`;
186
211
  }
187
212
  // Root entity: result has ownerId
@@ -189,7 +214,7 @@ class ControllerGenerator {
189
214
  return `
190
215
  // Owner validation (post-fetch for reads)
191
216
  if (${resultVar}.ownerId !== context.request.user?.id) {
192
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
217
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
193
218
  }`;
194
219
  }
195
220
  const bypassConditions = bypassRoles.map(r => `context.request.user?.role === '${r}'`).join(' || ');
@@ -198,7 +223,7 @@ class ControllerGenerator {
198
223
  const isOwner = ${resultVar}.ownerId === context.request.user?.id;
199
224
  const hasPrivilegedRole = ${bypassConditions};
200
225
  if (!isOwner && !hasPrivilegedRole) {
201
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
226
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
202
227
  }`;
203
228
  }
204
229
  /**
@@ -222,10 +247,10 @@ class ControllerGenerator {
222
247
  // Pre-mutation owner validation
223
248
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
224
249
  if (resourceOwnerId === null) {
225
- throw new Error('Resource not found');
250
+ throw new NotFoundError('Resource not found');
226
251
  }
227
252
  if (resourceOwnerId !== context.request.user?.id) {
228
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
253
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
229
254
  }
230
255
  `;
231
256
  }
@@ -235,12 +260,12 @@ class ControllerGenerator {
235
260
  // Pre-mutation owner validation (bypassed for: ${bypassRoles.join(', ')})
236
261
  const resourceOwnerId = await this.${useCaseVar}.getResourceOwner(input.id);
237
262
  if (resourceOwnerId === null) {
238
- throw new Error('Resource not found');
263
+ throw new NotFoundError('Resource not found');
239
264
  }
240
265
  const isOwner = resourceOwnerId === context.request.user?.id;
241
266
  const hasPrivilegedRole = ${bypassConditions};
242
267
  if (!isOwner && !hasPrivilegedRole) {
243
- throw new Error('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
268
+ throw new ForbiddenError('${constants_1.AUTH_ERRORS.ACCESS_DENIED}');
244
269
  }
245
270
  `;
246
271
  }
@@ -278,7 +303,8 @@ class ControllerGenerator {
278
303
  parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
279
304
  }
280
305
  else {
281
- parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
306
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
307
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id as ${idTs} });`;
282
308
  }
283
309
  }
284
310
  else if (action === 'update') {
@@ -428,7 +454,8 @@ class ControllerGenerator {
428
454
  parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ${childInfo.parentIdField}: context.request.parameters.${childInfo.parentIdField} });`;
429
455
  }
430
456
  else {
431
- parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id });`;
457
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
458
+ parseLogic = `const input = ${inputClass}.parse({ ...context.request.body, ownerId: context.request.user?.id as ${idTs} });`;
432
459
  }
433
460
  }
434
461
  else {
@@ -554,7 +581,9 @@ class ControllerGenerator {
554
581
  const constructorParams = Array.from(useCaseModels)
555
582
  .map(model => `private ${model.toLowerCase()}UseCase: ${model}UseCase`)
556
583
  .join(',\n ');
557
- return `import { Controller, Get, Post, Put, Delete, type IContext } from '@currentjs/router';
584
+ const errorImports = this.getNeededHttpErrorImports(sortedEndpoints.map(e => e.auth));
585
+ const routerImports = ['Controller', 'Get', 'Post', 'Put', 'Delete', 'type IContext', ...errorImports].join(', ');
586
+ return `import { ${routerImports} } from '@currentjs/router';
558
587
  ${useCaseImports}
559
588
  ${dtoImportStatements}
560
589
 
@@ -621,7 +650,9 @@ ${methods.join('\n\n')}
621
650
  ${constructorParams.join(',\n ')}
622
651
  ) {}`
623
652
  : 'constructor() {}';
624
- return `import { Controller, Get, Post, Render, type IContext } from '@currentjs/router';
653
+ const errorImports = this.getNeededHttpErrorImports(sortedPages.map(p => p.auth));
654
+ const routerImports = ['Controller', 'Get', 'Post', 'Render', 'type IContext', ...errorImports].join(', ');
655
+ return `import { ${routerImports} } from '@currentjs/router';
625
656
  ${useCaseImports}
626
657
  ${serviceImports.join('\n')}
627
658
  ${dtoImportStatements}
@@ -633,8 +664,9 @@ export class ${controllerName} {
633
664
  ${methods.join('\n\n')}
634
665
  }`;
635
666
  }
636
- generateFromConfig(config) {
667
+ generateFromConfig(config, identifiers = 'numeric') {
637
668
  const result = {};
669
+ this.identifiers = identifiers;
638
670
  const childEntityMap = (0, childEntityUtils_1.buildChildEntityMap)(config);
639
671
  // Generate API controllers
640
672
  if (config.api) {
@@ -655,16 +687,16 @@ ${methods.join('\n\n')}
655
687
  }
656
688
  return result;
657
689
  }
658
- generateFromYamlFile(yamlFilePath) {
690
+ generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
659
691
  const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
660
692
  const config = (0, yaml_1.parse)(yamlContent);
661
693
  if (!(0, configTypes_1.isValidModuleConfig)(config)) {
662
694
  throw new Error('Configuration does not match new module format. Expected domain/useCases/api/web structure.');
663
695
  }
664
- return this.generateFromConfig(config);
696
+ return this.generateFromConfig(config, identifiers);
665
697
  }
666
- async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
667
- const controllersByName = this.generateFromYamlFile(yamlFilePath);
698
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
699
+ const controllersByName = this.generateFromYamlFile(yamlFilePath, identifiers);
668
700
  const controllersDir = path.join(moduleDir, 'infrastructure', 'controllers');
669
701
  fs.mkdirSync(controllersDir, { recursive: true });
670
702
  const generatedPaths = [];
@@ -1,21 +1,22 @@
1
- import { ModuleConfig } from '../types/configTypes';
1
+ import { ModuleConfig, IdentifierType } from '../types/configTypes';
2
2
  export declare class DomainLayerGenerator {
3
3
  private availableAggregates;
4
4
  private availableValueObjects;
5
+ private identifiers;
5
6
  private mapType;
6
7
  private getDefaultValue;
7
8
  private generateValueObject;
8
9
  private generateAggregate;
9
- generateFromConfig(config: ModuleConfig): Record<string, {
10
+ generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, {
10
11
  code: string;
11
12
  type: 'entity' | 'valueObject';
12
13
  }>;
13
- generateFromYamlFile(yamlFilePath: string): Record<string, {
14
+ generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, {
14
15
  code: string;
15
16
  type: 'entity' | 'valueObject';
16
17
  }>;
17
18
  generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
18
19
  force?: boolean;
19
20
  skipOnConflict?: boolean;
20
- }): Promise<void>;
21
+ }, identifiers?: IdentifierType): Promise<void>;
21
22
  }
@@ -46,6 +46,7 @@ class DomainLayerGenerator {
46
46
  constructor() {
47
47
  this.availableAggregates = new Set();
48
48
  this.availableValueObjects = new Set();
49
+ this.identifiers = 'numeric';
49
50
  }
50
51
  mapType(yamlType) {
51
52
  return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates, this.availableValueObjects);
@@ -154,12 +155,13 @@ class DomainLayerGenerator {
154
155
  .join('\n');
155
156
  const imports = [entityImports, valueObjectImports, aggregateRefImports].filter(Boolean).join('\n');
156
157
  // Generate constructor parameters: id, then ownerId (root) or parentId field (child)
157
- const constructorParams = ['public id: number'];
158
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
159
+ const constructorParams = [`public id: ${idTs}`];
158
160
  if (childInfo) {
159
- constructorParams.push(`public ${childInfo.parentIdField}: number`);
161
+ constructorParams.push(`public ${childInfo.parentIdField}: ${idTs}`);
160
162
  }
161
163
  else {
162
- constructorParams.push('public ownerId: number');
164
+ constructorParams.push(`public ownerId: ${idTs}`);
163
165
  }
164
166
  // Sort fields: required first, then optional
165
167
  // Fields are required by default unless required: false
@@ -256,8 +258,9 @@ class DomainLayerGenerator {
256
258
  ${setterMethods}
257
259
  }`;
258
260
  }
259
- generateFromConfig(config) {
261
+ generateFromConfig(config, identifiers = 'numeric') {
260
262
  const result = {};
263
+ this.identifiers = identifiers;
261
264
  // First pass: collect all aggregate and value object names
262
265
  if (config.domain.aggregates) {
263
266
  Object.keys(config.domain.aggregates).forEach(name => {
@@ -291,16 +294,16 @@ ${setterMethods}
291
294
  }
292
295
  return result;
293
296
  }
294
- generateFromYamlFile(yamlFilePath) {
297
+ generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
295
298
  const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
296
299
  const config = (0, yaml_1.parse)(yamlContent);
297
300
  if (!(0, configTypes_1.isValidModuleConfig)(config)) {
298
301
  throw new Error('Configuration does not match new module format. Expected domain.aggregates structure.');
299
302
  }
300
- return this.generateFromConfig(config);
303
+ return this.generateFromConfig(config, identifiers);
301
304
  }
302
- async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
303
- const codeByEntity = this.generateFromYamlFile(yamlFilePath);
305
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
306
+ const codeByEntity = this.generateFromYamlFile(yamlFilePath, identifiers);
304
307
  const entitiesDir = path.join(moduleDir, 'domain', 'entities');
305
308
  const valueObjectsDir = path.join(moduleDir, 'domain', 'valueObjects');
306
309
  fs.mkdirSync(entitiesDir, { recursive: true });
@@ -1,7 +1,8 @@
1
- import { ModuleConfig } from '../types/configTypes';
1
+ import { ModuleConfig, IdentifierType } from '../types/configTypes';
2
2
  export declare class DtoGenerator {
3
3
  private availableAggregates;
4
4
  private availableValueObjects;
5
+ private identifiers;
5
6
  private mapType;
6
7
  private isValueObjectType;
7
8
  private getValidationCode;
@@ -12,10 +13,10 @@ export declare class DtoGenerator {
12
13
  * Collect types that need to be imported for a use case DTO.
13
14
  */
14
15
  private collectRequiredImports;
15
- generateFromConfig(config: ModuleConfig): Record<string, string>;
16
- generateFromYamlFile(yamlFilePath: string): Record<string, string>;
16
+ generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, string>;
17
+ generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, string>;
17
18
  generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
18
19
  force?: boolean;
19
20
  skipOnConflict?: boolean;
20
- }): Promise<void>;
21
+ }, identifiers?: IdentifierType): Promise<void>;
21
22
  }
@@ -46,6 +46,7 @@ class DtoGenerator {
46
46
  constructor() {
47
47
  this.availableAggregates = new Map();
48
48
  this.availableValueObjects = new Map();
49
+ this.identifiers = 'numeric';
49
50
  }
50
51
  mapType(yamlType) {
51
52
  return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates, this.availableValueObjects);
@@ -116,13 +117,17 @@ class DtoGenerator {
116
117
  // Handle identifier (for get, update, delete)
117
118
  if (inputConfig.identifier) {
118
119
  const fieldName = inputConfig.identifier;
119
- fieldDeclarations.push(` readonly ${fieldName}: number;`);
120
- constructorParams.push(`${fieldName}: number`);
120
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
121
+ const idTransform = this.identifiers === 'numeric'
122
+ ? `typeof b.${fieldName} === 'string' ? parseInt(b.${fieldName}, 10) : b.${fieldName} as number`
123
+ : `b.${fieldName} as string`;
124
+ fieldDeclarations.push(` readonly ${fieldName}: ${idTs};`);
125
+ constructorParams.push(`${fieldName}: ${idTs}`);
121
126
  constructorAssignments.push(` this.${fieldName} = ${fieldName};`);
122
127
  validationChecks.push(` if (b.${fieldName} === undefined || b.${fieldName} === null) {
123
128
  throw new Error('${fieldName} is required');
124
129
  }`);
125
- fieldTransforms.push(` ${fieldName}: typeof b.${fieldName} === 'string' ? parseInt(b.${fieldName}, 10) : b.${fieldName} as number`);
130
+ fieldTransforms.push(` ${fieldName}: ${idTransform}`);
126
131
  }
127
132
  // Handle pagination
128
133
  if (inputConfig.pagination) {
@@ -158,21 +163,26 @@ class DtoGenerator {
158
163
  const isCreateAction = !inputConfig.identifier && !inputConfig.partial;
159
164
  if (isCreateAction) {
160
165
  const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
161
- fieldDeclarations.push(` readonly ${ownerOrParentField}: number;`);
162
- constructorParams.push(`${ownerOrParentField}: number`);
166
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
167
+ const ownerTransform = this.identifiers === 'numeric'
168
+ ? `typeof b.${ownerOrParentField} === 'string' ? parseInt(b.${ownerOrParentField}, 10) : b.${ownerOrParentField} as number`
169
+ : `b.${ownerOrParentField} as string`;
170
+ fieldDeclarations.push(` readonly ${ownerOrParentField}: ${idTs};`);
171
+ constructorParams.push(`${ownerOrParentField}: ${idTs}`);
163
172
  constructorAssignments.push(` this.${ownerOrParentField} = ${ownerOrParentField};`);
164
173
  validationChecks.push(` if (b.${ownerOrParentField} === undefined || b.${ownerOrParentField} === null) {
165
174
  throw new Error('${ownerOrParentField} is required');
166
175
  }`);
167
- fieldTransforms.push(` ${ownerOrParentField}: typeof b.${ownerOrParentField} === 'string' ? parseInt(b.${ownerOrParentField}, 10) : b.${ownerOrParentField} as number`);
176
+ fieldTransforms.push(` ${ownerOrParentField}: ${ownerTransform}`);
168
177
  }
169
178
  // Add fields
179
+ const aggIdTs = (0, configTypes_1.idTsType)(this.identifiers);
170
180
  fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
171
181
  if (fieldName === 'id' || fieldConfig.auto)
172
182
  return;
173
183
  const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
174
- const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
175
- const effectiveFieldType = isAggRef ? 'number' : fieldConfig.type;
184
+ const tsType = isAggRef ? aggIdTs : this.mapType(fieldConfig.type);
185
+ const effectiveFieldType = isAggRef ? (this.identifiers === 'numeric' ? 'number' : 'string') : fieldConfig.type;
176
186
  // Aggregate references are always optional in DTOs; other fields default to required
177
187
  const isRequired = !isAggRef && !inputConfig.partial && fieldConfig.required !== false;
178
188
  const optional = isRequired ? '' : '?';
@@ -252,8 +262,9 @@ ${transformsStr}
252
262
  const fromMappings = [];
253
263
  // Always include id for entity outputs
254
264
  if (outputConfig.from) {
255
- fieldDeclarations.push(` readonly id: number;`);
256
- constructorParams.push(`id: number`);
265
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
266
+ fieldDeclarations.push(` readonly id: ${idTs};`);
267
+ constructorParams.push(`id: ${idTs}`);
257
268
  fromMappings.push(` id: entity.id`);
258
269
  }
259
270
  // Handle fields from aggregate (pick)
@@ -265,11 +276,12 @@ ${transformsStr}
265
276
  fieldsToInclude = fieldsToInclude.filter(([fieldName]) => outputConfig.pick.includes(fieldName));
266
277
  }
267
278
  // Add fields
279
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
268
280
  fieldsToInclude.forEach(([fieldName, fieldConfig]) => {
269
281
  if (fieldName === 'id')
270
282
  return;
271
283
  const isAggRef = (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
272
- const tsType = isAggRef ? 'number' : this.mapType(fieldConfig.type);
284
+ const tsType = isAggRef ? idTs : this.mapType(fieldConfig.type);
273
285
  const isOptional = fieldConfig.required === false || isAggRef;
274
286
  const optional = isOptional ? '?' : '';
275
287
  fieldDeclarations.push(` readonly ${fieldName}${optional}: ${tsType};`);
@@ -447,8 +459,9 @@ ${mappingsStr}
447
459
  }
448
460
  return { valueObjects, entities };
449
461
  }
450
- generateFromConfig(config) {
462
+ generateFromConfig(config, identifiers = 'numeric') {
451
463
  const result = {};
464
+ this.identifiers = identifiers;
452
465
  // Collect all aggregates
453
466
  if (config.domain.aggregates) {
454
467
  Object.entries(config.domain.aggregates).forEach(([name, aggConfig]) => {
@@ -498,16 +511,16 @@ ${mappingsStr}
498
511
  });
499
512
  return result;
500
513
  }
501
- generateFromYamlFile(yamlFilePath) {
514
+ generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
502
515
  const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
503
516
  const config = (0, yaml_1.parse)(yamlContent);
504
517
  if (!(0, configTypes_1.isValidModuleConfig)(config)) {
505
518
  throw new Error('Configuration does not match new module format. Expected useCases structure.');
506
519
  }
507
- return this.generateFromConfig(config);
520
+ return this.generateFromConfig(config, identifiers);
508
521
  }
509
- async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
510
- const dtosByName = this.generateFromYamlFile(yamlFilePath);
522
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
523
+ const dtosByName = this.generateFromYamlFile(yamlFilePath, identifiers);
511
524
  const dtoDir = path.join(moduleDir, 'application', 'dto');
512
525
  fs.mkdirSync(dtoDir, { recursive: true });
513
526
  for (const [name, code] of Object.entries(dtosByName)) {
@@ -1,6 +1,7 @@
1
- import { ModuleConfig } from '../types/configTypes';
1
+ import { ModuleConfig, IdentifierType } from '../types/configTypes';
2
2
  export declare class ServiceGenerator {
3
3
  private availableAggregates;
4
+ private identifiers;
4
5
  private mapType;
5
6
  private getDefaultHandlerReturnType;
6
7
  private buildHandlerContextMap;
@@ -19,10 +20,10 @@ export declare class ServiceGenerator {
19
20
  private generateListByParentMethod;
20
21
  private generateGetResourceOwnerMethod;
21
22
  private generateService;
22
- generateFromConfig(config: ModuleConfig): Record<string, string>;
23
- generateFromYamlFile(yamlFilePath: string): Record<string, string>;
23
+ generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, string>;
24
+ generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, string>;
24
25
  generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
25
26
  force?: boolean;
26
27
  skipOnConflict?: boolean;
27
- }): Promise<void>;
28
+ }, identifiers?: IdentifierType): Promise<void>;
28
29
  }