@currentjs/gen 0.5.4 → 0.5.6

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.
@@ -47,6 +47,7 @@ class StoreGenerator {
47
47
  constructor() {
48
48
  this.availableValueObjects = new Map();
49
49
  this.availableAggregates = new Set();
50
+ this.identifiers = 'numeric';
50
51
  }
51
52
  isAggregateField(fieldConfig) {
52
53
  return (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
@@ -130,7 +131,7 @@ class StoreGenerator {
130
131
  const voFields = Object.keys(voConfig.fields);
131
132
  if (voFields.length > 1) {
132
133
  const voArgs = voFields.map(f => `parsed.${f}`).join(', ');
133
- return ` row.${fieldName} ? (() => { const parsed = JSON.parse(row.${fieldName}); return new ${voName}(${voArgs}); })() : undefined`;
134
+ return ` row.${fieldName} ? (() => { const parsed = this.ensureParsed(row.${fieldName}); return new ${voName}(${voArgs}); })() : undefined`;
134
135
  }
135
136
  if (voFields.length === 1) {
136
137
  const singleFieldType = voConfig.fields[voFields[0]];
@@ -141,7 +142,7 @@ class StoreGenerator {
141
142
  }
142
143
  return ` row.${fieldName} ? new ${voName}(row.${fieldName}) : undefined`;
143
144
  }
144
- return ` row.${fieldName} ? new ${voName}(...Object.values(JSON.parse(row.${fieldName}))) : undefined`;
145
+ return ` row.${fieldName} ? new ${voName}(...Object.values(this.ensureParsed(row.${fieldName}))) : undefined`;
145
146
  }
146
147
  /** Deserialization for an array-of-VOs field. */
147
148
  generateArrayVoDeserialization(fieldName, voName, voConfig) {
@@ -149,7 +150,7 @@ class StoreGenerator {
149
150
  const itemArgs = voFields.length > 0
150
151
  ? voFields.map(f => `item.${f}`).join(', ')
151
152
  : '...Object.values(item)';
152
- return ` row.${fieldName} ? (JSON.parse(row.${fieldName}) as any[]).map((item: any) => new ${voName}(${itemArgs})) : []`;
153
+ return ` row.${fieldName} ? (this.ensureParsed(row.${fieldName}) as any[]).map((item: any) => new ${voName}(${itemArgs})) : []`;
153
154
  }
154
155
  /** Deserialization for a union-of-VOs field. Uses _type discriminator. */
155
156
  generateUnionVoDeserialization(fieldName, unionVoNames, unionVoConfigs) {
@@ -161,7 +162,7 @@ class StoreGenerator {
161
162
  : '...Object.values(parsed)';
162
163
  return `if (parsed._type === '${voName}') return new ${voName}(${args});`;
163
164
  }).join(' ');
164
- return ` row.${fieldName} ? (() => { const parsed = JSON.parse(row.${fieldName}); ${cases} return undefined; })() : undefined`;
165
+ return ` row.${fieldName} ? (() => { const parsed = this.ensureParsed(row.${fieldName}); ${cases} return undefined; })() : undefined`;
165
166
  }
166
167
  /** Serialization for an array-of-union-VOs field. Each element tagged with _type discriminator. */
167
168
  generateArrayUnionVoSerialization(fieldName, unionVoNames) {
@@ -181,7 +182,7 @@ class StoreGenerator {
181
182
  : '...Object.values(item)';
182
183
  return `if (item._type === '${voName}') return new ${voName}(${args});`;
183
184
  }).join(' ');
184
- return ` row.${fieldName} ? (JSON.parse(row.${fieldName}) as any[]).map((item: any) => { ${cases} return undefined; }) : []`;
185
+ return ` row.${fieldName} ? (this.ensureParsed(row.${fieldName}) as any[]).map((item: any) => { ${cases} return undefined; }) : []`;
185
186
  }
186
187
  /** Single line for datetime conversion: toDate (row->model) or toMySQL (entity->row). */
187
188
  generateDatetimeConversion(fieldName, direction) {
@@ -200,11 +201,12 @@ class StoreGenerator {
200
201
  }
201
202
  generateRowFields(fields, childInfo) {
202
203
  const result = [];
204
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
203
205
  const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
204
- result.push(` ${ownerOrParentField}: number;`);
206
+ result.push(` ${ownerOrParentField}: ${idTs};`);
205
207
  fields.forEach(([fieldName, fieldConfig]) => {
206
208
  if (this.isAggregateField(fieldConfig)) {
207
- result.push(` ${fieldName}_id?: number;`);
209
+ result.push(` ${fieldName}Id?: ${idTs};`);
208
210
  return;
209
211
  }
210
212
  const tsType = this.mapTypeToRowType(fieldConfig.type);
@@ -214,10 +216,27 @@ class StoreGenerator {
214
216
  return result.join('\n');
215
217
  }
216
218
  generateFieldNamesStr(fields, childInfo) {
217
- const fieldNames = ['id'];
218
- fieldNames.push(childInfo ? childInfo.parentIdField : 'ownerId');
219
- fieldNames.push(...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name));
220
- return fieldNames.map(f => `\\\`${f}\\\``).join(', ');
219
+ const isUuid = this.identifiers === 'uuid';
220
+ const ownerOrParentField = childInfo ? childInfo.parentIdField : 'ownerId';
221
+ const allFields = [
222
+ 'id',
223
+ ownerOrParentField,
224
+ ...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}Id` : name)
225
+ ];
226
+ if (!isUuid) {
227
+ return allFields.map(f => `\\\`${f}\\\``).join(', ');
228
+ }
229
+ // For UUID: id-type columns need BIN_TO_UUID wrapping in SELECT
230
+ const idFields = new Set(['id', ownerOrParentField]);
231
+ fields.forEach(([name, config]) => {
232
+ if (this.isAggregateField(config))
233
+ idFields.add(`${name}Id`);
234
+ });
235
+ return allFields.map(f => {
236
+ if (idFields.has(f))
237
+ return `BIN_TO_UUID(\\\`${f}\\\`, 1) as \\\`${f}\\\``;
238
+ return `\\\`${f}\\\``;
239
+ }).join(', ');
221
240
  }
222
241
  generateRowToModelMapping(modelName, fields, childInfo) {
223
242
  const result = [];
@@ -233,7 +252,7 @@ class StoreGenerator {
233
252
  }
234
253
  // Handle aggregate reference - create stub from FK
235
254
  if (this.isAggregateField(fieldConfig)) {
236
- result.push(` row.${fieldName}_id != null ? ({ id: row.${fieldName}_id } as unknown as ${fieldType}) : undefined`);
255
+ result.push(` row.${fieldName}Id != null ? ({ id: row.${fieldName}Id } as unknown as ${fieldType}) : undefined`);
237
256
  return;
238
257
  }
239
258
  // Handle datetime/date conversion
@@ -283,7 +302,7 @@ class StoreGenerator {
283
302
  result.push(this.generateValueObjectDeserialization(fieldName, voName, voConfig));
284
303
  }
285
304
  else {
286
- result.push(` row.${fieldName} ? new ${voName}(...Object.values(JSON.parse(row.${fieldName}))) : undefined`);
305
+ result.push(` row.${fieldName} ? new ${voName}(...Object.values(this.ensureParsed(row.${fieldName}))) : undefined`);
287
306
  }
288
307
  return;
289
308
  }
@@ -299,7 +318,7 @@ class StoreGenerator {
299
318
  const fieldType = fieldConfig.type;
300
319
  // Handle aggregate reference - extract FK id
301
320
  if (this.isAggregateField(fieldConfig)) {
302
- result.push(` ${fieldName}_id: entity.${fieldName}?.id`);
321
+ result.push(` ${fieldName}Id: entity.${fieldName}?.id`);
303
322
  return;
304
323
  }
305
324
  // Handle datetime/date - convert Date to MySQL DATETIME format
@@ -342,7 +361,7 @@ class StoreGenerator {
342
361
  .map(([fieldName, fieldConfig]) => {
343
362
  const fieldType = fieldConfig.type;
344
363
  if (this.isAggregateField(fieldConfig)) {
345
- return ` ${fieldName}_id: entity.${fieldName}?.id`;
364
+ return ` ${fieldName}Id: entity.${fieldName}?.id`;
346
365
  }
347
366
  if (fieldType === 'datetime' || fieldType === 'date') {
348
367
  return this.generateDatetimeConversion(fieldName, 'toMySQL');
@@ -371,7 +390,7 @@ class StoreGenerator {
371
390
  .join(',\n');
372
391
  }
373
392
  generateUpdateFieldsArray(fields) {
374
- return JSON.stringify(fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name));
393
+ return JSON.stringify(fields.map(([name, config]) => this.isAggregateField(config) ? `${name}Id` : name));
375
394
  }
376
395
  generateValueObjectImports(fields) {
377
396
  const imports = [];
@@ -411,9 +430,15 @@ class StoreGenerator {
411
430
  }
412
431
  generateListMethods(modelName, fieldNamesStr, childInfo) {
413
432
  const isRoot = !childInfo;
414
- const ownerParam = isRoot ? ', ownerId?: number' : '';
433
+ const isUuid = this.identifiers === 'uuid';
434
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
435
+ const ownerParam = isRoot ? `, ownerId?: ${idTs}` : '';
436
+ // UUID_TO_BIN(:ownerId, 1) — the ", 1" is a SQL literal inside the query string, not a JS param key
437
+ const ownerFilterExpr = isUuid
438
+ ? `' AND \\\`ownerId\\\` = UUID_TO_BIN(:ownerId, 1)'`
439
+ : `' AND \\\`ownerId\\\` = :ownerId'`;
415
440
  const ownerFilter = isRoot
416
- ? `\n const ownerFilter = ownerId != null ? ' AND \\\`ownerId\\\` = :ownerId' : '';`
441
+ ? `\n const ownerFilter = ownerId != null ? ${ownerFilterExpr} : '';`
417
442
  : '';
418
443
  const ownerFilterRef = isRoot ? '\${ownerFilter}' : '';
419
444
  const ownerParamsSetup = isRoot
@@ -423,7 +448,7 @@ class StoreGenerator {
423
448
  const offset = (page - 1) * limit;${ownerFilter}
424
449
  const params: Record<string, any> = { limit: String(limit), offset: String(offset) };${ownerParamsSetup}
425
450
  const result = await this.db.query(
426
- \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef} LIMIT :limit OFFSET :offset\`,
451
+ \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deletedAt IS NULL${ownerFilterRef} LIMIT :limit OFFSET :offset\`,
427
452
  params
428
453
  );
429
454
 
@@ -432,10 +457,10 @@ class StoreGenerator {
432
457
  }
433
458
  return [];
434
459
  }`;
435
- const getAll = ` async getAll(${isRoot ? 'ownerId?: number' : ''}): Promise<${modelName}[]> {${ownerFilter}
460
+ const getAll = ` async getAll(${isRoot ? `ownerId?: ${idTs}` : ''}): Promise<${modelName}[]> {${ownerFilter}
436
461
  const params: Record<string, any> = {};${ownerParamsSetup}
437
462
  const result = await this.db.query(
438
- \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef}\`,
463
+ \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deletedAt IS NULL${ownerFilterRef}\`,
439
464
  params
440
465
  );
441
466
 
@@ -444,10 +469,10 @@ class StoreGenerator {
444
469
  }
445
470
  return [];
446
471
  }`;
447
- const count = ` async count(${isRoot ? 'ownerId?: number' : ''}): Promise<number> {${ownerFilter}
472
+ const count = ` async count(${isRoot ? `ownerId?: ${idTs}` : ''}): Promise<number> {${ownerFilter}
448
473
  const params: Record<string, any> = {};${ownerParamsSetup}
449
474
  const result = await this.db.query(
450
- \`SELECT COUNT(*) as count FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef}\`,
475
+ \`SELECT COUNT(*) as count FROM \\\`\${this.tableName}\\\` WHERE deletedAt IS NULL${ownerFilterRef}\`,
451
476
  params
452
477
  );
453
478
 
@@ -461,13 +486,26 @@ class StoreGenerator {
461
486
  generateGetByParentIdMethod(modelName, fields, childInfo) {
462
487
  if (!childInfo)
463
488
  return '';
464
- const fieldList = ['id', childInfo.parentIdField, ...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name)].map(f => '\\`' + f + '\\`').join(', ');
489
+ const isUuid = this.identifiers === 'uuid';
490
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
465
491
  const parentIdField = childInfo.parentIdField;
492
+ // Build field list for SELECT (reuse the same UUID-aware logic)
493
+ const idFields = new Set(['id', parentIdField]);
494
+ fields.forEach(([name, config]) => {
495
+ if (this.isAggregateField(config))
496
+ idFields.add(`${name}Id`);
497
+ });
498
+ const rawFields = ['id', parentIdField, ...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}Id` : name)];
499
+ const bt = '\\`';
500
+ const fieldList = isUuid
501
+ ? rawFields.map(f => idFields.has(f) ? `BIN_TO_UUID(${bt}${f}${bt}, 1) as ${bt}${f}${bt}` : `${bt}${f}${bt}`).join(', ')
502
+ : rawFields.map(f => `${bt}${f}${bt}`).join(', ');
503
+ const whereExpr = isUuid ? `\\\`${parentIdField}\\\` = UUID_TO_BIN(:parentId, 1)` : `\\\`${parentIdField}\\\` = :parentId`;
466
504
  return `
467
505
 
468
- async getByParentId(parentId: number): Promise<${modelName}[]> {
506
+ async getByParentId(parentId: ${idTs}): Promise<${modelName}[]> {
469
507
  const result = await this.db.query(
470
- \`SELECT ${fieldList} FROM \\\`\${this.tableName}\\\` WHERE \\\`${parentIdField}\\\` = :parentId AND deleted_at IS NULL\`,
508
+ \`SELECT ${fieldList} FROM \\\`\${this.tableName}\\\` WHERE ${whereExpr} AND deletedAt IS NULL\`,
471
509
  { parentId }
472
510
  );
473
511
 
@@ -478,23 +516,32 @@ class StoreGenerator {
478
516
  }`;
479
517
  }
480
518
  generateGetResourceOwnerMethod(childInfo) {
519
+ const isUuid = this.identifiers === 'uuid';
520
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
521
+ const whereExpr = isUuid ? 'id = UUID_TO_BIN(:id, 1)' : 'id = :id';
522
+ const ownerSelect = isUuid ? 'BIN_TO_UUID(p.ownerId, 1) as ownerId' : 'p.ownerId';
523
+ const ownerSelectSimple = isUuid ? 'BIN_TO_UUID(ownerId, 1) as ownerId' : 'ownerId';
481
524
  if (childInfo) {
482
525
  const parentTable = childInfo.parentTableName;
483
526
  const parentIdField = childInfo.parentIdField;
527
+ const joinExpr = isUuid
528
+ ? `p.id = UUID_TO_BIN(c.\\\`${parentIdField}\\\`, 1)`
529
+ : `p.id = c.\\\`${parentIdField}\\\``;
530
+ const cWhereExpr = isUuid ? 'c.id = UUID_TO_BIN(:id, 1)' : 'c.id = :id';
484
531
  return `
485
532
 
486
533
  /**
487
534
  * Get the owner ID of a resource by its ID (via parent entity).
488
535
  * Used for pre-mutation authorization checks.
489
536
  */
490
- async getResourceOwner(id: number): Promise<number | null> {
537
+ async getResourceOwner(id: ${idTs}): Promise<${idTs} | null> {
491
538
  const result = await this.db.query(
492
- \`SELECT p.ownerId FROM \\\`\${this.tableName}\\\` c INNER JOIN \\\`${parentTable}\\\` p ON p.id = c.\\\`${parentIdField}\\\` WHERE c.id = :id AND c.deleted_at IS NULL\`,
539
+ \`SELECT ${ownerSelect} FROM \\\`\${this.tableName}\\\` c INNER JOIN \\\`${parentTable}\\\` p ON ${joinExpr} WHERE ${cWhereExpr} AND c.deletedAt IS NULL\`,
493
540
  { id }
494
541
  );
495
542
 
496
543
  if (result.success && result.data && result.data.length > 0) {
497
- return result.data[0].ownerId as number;
544
+ return result.data[0].ownerId as ${idTs};
498
545
  }
499
546
  return null;
500
547
  }`;
@@ -505,35 +552,109 @@ class StoreGenerator {
505
552
  * Get the owner ID of a resource by its ID.
506
553
  * Used for pre-mutation authorization checks.
507
554
  */
508
- async getResourceOwner(id: number): Promise<number | null> {
555
+ async getResourceOwner(id: ${idTs}): Promise<${idTs} | null> {
509
556
  const result = await this.db.query(
510
- \`SELECT ownerId FROM \\\`\${this.tableName}\\\` WHERE id = :id AND deleted_at IS NULL\`,
557
+ \`SELECT ${ownerSelectSimple} FROM \\\`\${this.tableName}\\\` WHERE ${whereExpr} AND deletedAt IS NULL\`,
511
558
  { id }
512
559
  );
513
560
 
514
561
  if (result.success && result.data && result.data.length > 0) {
515
- return result.data[0].ownerId as number;
562
+ return result.data[0].ownerId as ${idTs};
516
563
  }
517
564
  return null;
518
565
  }`;
519
566
  }
567
+ generateIdHelpers() {
568
+ if (this.identifiers === 'nanoid') {
569
+ return `
570
+ private generateNanoId(size = 21): string {
571
+ const alphabet = "useABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
572
+ let id = "";
573
+ const bytes = randomBytes(size);
574
+
575
+ for (let i = 0; i < size; i++) {
576
+ // We use a bitwise AND with 63 because 63 is 00111111 in binary.
577
+ // This maps any byte to a value between 0 and 63.
578
+ id += alphabet[bytes[i] & 63];
579
+ }
580
+
581
+ return id;
582
+ }
583
+ `;
584
+ }
585
+ return '';
586
+ }
587
+ generateInsertIdVariables() {
588
+ switch (this.identifiers) {
589
+ case 'uuid':
590
+ return {
591
+ preLine: ' const newId = randomUUID();',
592
+ dataLine: ' id: newId,\n',
593
+ successCond: '',
594
+ getId: ''
595
+ };
596
+ case 'nanoid':
597
+ return {
598
+ preLine: ' const newId = this.generateNanoId();',
599
+ dataLine: ' id: newId,\n',
600
+ successCond: '',
601
+ getId: ''
602
+ };
603
+ default:
604
+ return {
605
+ preLine: '',
606
+ dataLine: '',
607
+ successCond: ' && result.insertId',
608
+ getId: 'const newId = typeof result.insertId === \'string\' ? parseInt(result.insertId, 10) : result.insertId;'
609
+ };
610
+ }
611
+ }
612
+ generateWhereIdExpr() {
613
+ return this.identifiers === 'uuid' ? 'id = UUID_TO_BIN(:id, 1)' : 'id = :id';
614
+ }
615
+ generateIdParamExpr() {
616
+ // The ", 1" in UUID_TO_BIN(:id, 1) is a literal in the SQL string, NOT a JS params key.
617
+ // The params object always uses just { id }, so this expression is always empty.
618
+ return '';
619
+ }
620
+ generateRowIdExpr() {
621
+ return this.identifiers === 'uuid' ? 'row.id' : 'row.id';
622
+ }
623
+ generateCryptoImport() {
624
+ if (this.identifiers === 'uuid')
625
+ return `\nimport { randomUUID } from 'crypto';`;
626
+ if (this.identifiers === 'nanoid')
627
+ return `\nimport { randomBytes } from 'crypto';`;
628
+ return '';
629
+ }
520
630
  generateStore(modelName, aggregateConfig, childInfo) {
521
631
  const tableName = modelName.toLowerCase();
522
632
  const fields = Object.entries(aggregateConfig.fields);
523
633
  // Sort fields for rowToModel to match entity constructor order (required first, optional second)
524
634
  const sortedFields = this.sortFieldsForConstructor(fields);
525
635
  const fieldNamesStr = this.generateFieldNamesStr(fields, childInfo);
636
+ const idVars = this.generateInsertIdVariables();
637
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
526
638
  const variables = {
527
639
  ENTITY_NAME: modelName,
528
640
  TABLE_NAME: tableName,
641
+ ID_TYPE: idTs,
529
642
  ROW_FIELDS: this.generateRowFields(fields, childInfo),
530
643
  FIELD_NAMES: fieldNamesStr,
644
+ ROW_ID_EXPR: this.generateRowIdExpr(),
645
+ WHERE_ID_EXPR: this.generateWhereIdExpr(),
646
+ ID_PARAM_EXPR: this.generateIdParamExpr(),
531
647
  ROW_TO_MODEL_MAPPING: this.generateRowToModelMapping(modelName, sortedFields, childInfo),
648
+ INSERT_ID_PRE_LOGIC: idVars.preLine,
649
+ INSERT_ID_DATA: idVars.dataLine,
532
650
  INSERT_DATA_MAPPING: this.generateInsertDataMapping(fields, childInfo),
651
+ INSERT_SUCCESS_COND: idVars.successCond,
652
+ INSERT_GET_ID: idVars.getId,
533
653
  UPDATE_DATA_MAPPING: this.generateUpdateDataMapping(fields),
534
654
  UPDATE_FIELDS_ARRAY: this.generateUpdateFieldsArray(fields),
535
655
  VALUE_OBJECT_IMPORTS: this.generateValueObjectImports(fields),
536
656
  AGGREGATE_REF_IMPORTS: this.generateAggregateRefImports(modelName, fields),
657
+ ID_HELPERS: this.generateIdHelpers(),
537
658
  LIST_METHODS: this.generateListMethods(modelName, fieldNamesStr, childInfo),
538
659
  GET_BY_PARENT_ID_METHOD: this.generateGetByParentIdMethod(modelName, fields, childInfo),
539
660
  GET_RESOURCE_OWNER_METHOD: this.generateGetResourceOwnerMethod(childInfo)
@@ -552,12 +673,14 @@ class StoreGenerator {
552
673
  ENTITY_IMPORT_ITEMS: entityImportItems.join(', '),
553
674
  ROW_INTERFACE: rowInterface,
554
675
  STORE_CLASS: storeClass,
676
+ CRYPTO_IMPORT: this.generateCryptoImport(),
555
677
  VALUE_OBJECT_IMPORTS: variables.VALUE_OBJECT_IMPORTS,
556
678
  AGGREGATE_REF_IMPORTS: variables.AGGREGATE_REF_IMPORTS
557
679
  });
558
680
  }
559
- generateFromConfig(config) {
681
+ generateFromConfig(config, identifiers = 'numeric') {
560
682
  const result = {};
683
+ this.identifiers = identifiers;
561
684
  // First, collect all value object names and configs
562
685
  this.availableValueObjects.clear();
563
686
  if (config.domain.valueObjects) {
@@ -582,16 +705,16 @@ class StoreGenerator {
582
705
  }
583
706
  return result;
584
707
  }
585
- generateFromYamlFile(yamlFilePath) {
708
+ generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
586
709
  const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
587
710
  const config = (0, yaml_1.parse)(yamlContent);
588
711
  if (!(0, configTypes_1.isValidModuleConfig)(config)) {
589
712
  throw new Error('Configuration does not match new module format. Expected domain.aggregates structure.');
590
713
  }
591
- return this.generateFromConfig(config);
714
+ return this.generateFromConfig(config, identifiers);
592
715
  }
593
- async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
594
- const storesByModel = this.generateFromYamlFile(yamlFilePath);
716
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
717
+ const storesByModel = this.generateFromYamlFile(yamlFilePath, identifiers);
595
718
  const storesDir = path.join(moduleDir, 'infrastructure', 'stores');
596
719
  fs.mkdirSync(storesDir, { recursive: true });
597
720
  for (const [modelName, code] of Object.entries(storesByModel)) {
@@ -3,5 +3,5 @@ providers:
3
3
  config:
4
4
  database: mysql
5
5
  styling: bootstrap
6
- identifiers: id
6
+ identifiers: numeric
7
7
  modules: {}
@@ -2,4 +2,4 @@ export declare const storeTemplates: {
2
2
  rowInterface: string;
3
3
  storeClass: string;
4
4
  };
5
- export declare const storeFileTemplate = "import { Injectable } from '../../../../system';\nimport { {{ENTITY_IMPORT_ITEMS}} } from '../../domain/entities/{{ENTITY_NAME}}';\nimport type { ISqlProvider } from '@currentjs/provider-mysql';{{VALUE_OBJECT_IMPORTS}}{{AGGREGATE_REF_IMPORTS}}\n\n{{ROW_INTERFACE}}\n\n{{STORE_CLASS}}\n";
5
+ export declare const storeFileTemplate = "import { Injectable } from '../../../../system';\nimport { {{ENTITY_IMPORT_ITEMS}} } from '../../domain/entities/{{ENTITY_NAME}}';\nimport type { ISqlProvider } from '@currentjs/provider-mysql';{{CRYPTO_IMPORT}}{{VALUE_OBJECT_IMPORTS}}{{AGGREGATE_REF_IMPORTS}}\n\n{{ROW_INTERFACE}}\n\n{{STORE_CLASS}}\n";
@@ -3,11 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.storeFileTemplate = exports.storeTemplates = void 0;
4
4
  exports.storeTemplates = {
5
5
  rowInterface: `export interface {{ENTITY_NAME}}Row {
6
- id: number;
6
+ id: {{ID_TYPE}};
7
7
  {{ROW_FIELDS}}
8
- created_at: string;
9
- updated_at: string;
10
- deleted_at?: string;
8
+ createdAt: string;
9
+ updatedAt: string;
10
+ deletedAt?: string;
11
11
  }`,
12
12
  storeClass: `/**
13
13
  * Data access layer for {{ENTITY_NAME}}
@@ -22,19 +22,23 @@ export class {{ENTITY_NAME}}Store {
22
22
  return date.toISOString().slice(0, 19).replace('T', ' ');
23
23
  }
24
24
 
25
+ private ensureParsed(value: any): any {
26
+ return typeof value === 'string' ? JSON.parse(value) : value;
27
+ }
28
+ {{ID_HELPERS}}
25
29
  private rowToModel(row: {{ENTITY_NAME}}Row): {{ENTITY_NAME}} {
26
30
  return new {{ENTITY_NAME}}(
27
- row.id,
31
+ {{ROW_ID_EXPR}},
28
32
  {{ROW_TO_MODEL_MAPPING}}
29
33
  );
30
34
  }
31
35
 
32
36
  {{LIST_METHODS}}
33
37
 
34
- async getById(id: number): Promise<{{ENTITY_NAME}} | null> {
38
+ async getById(id: {{ID_TYPE}}): Promise<{{ENTITY_NAME}} | null> {
35
39
  const result = await this.db.query(
36
- \`SELECT {{FIELD_NAMES}} FROM \\\`\${this.tableName}\\\` WHERE id = :id AND deleted_at IS NULL\`,
37
- { id }
40
+ \`SELECT {{FIELD_NAMES}} FROM \\\`\${this.tableName}\\\` WHERE {{WHERE_ID_EXPR}} AND deletedAt IS NULL\`,
41
+ { id{{ID_PARAM_EXPR}} }
38
42
  );
39
43
 
40
44
  if (result.success && result.data && result.data.length > 0) {
@@ -45,41 +49,45 @@ export class {{ENTITY_NAME}}Store {
45
49
 
46
50
  async insert(entity: {{ENTITY_NAME}}): Promise<{{ENTITY_NAME}}> {
47
51
  const now = new Date();
52
+ {{INSERT_ID_PRE_LOGIC}}
48
53
  const data: Partial<{{ENTITY_NAME}}Row> = {
49
- {{INSERT_DATA_MAPPING}},
50
- created_at: this.toMySQLDatetime(now),
51
- updated_at: this.toMySQLDatetime(now)
54
+ {{INSERT_ID_DATA}}{{INSERT_DATA_MAPPING}},
55
+ createdAt: this.toMySQLDatetime(now),
56
+ updatedAt: this.toMySQLDatetime(now)
52
57
  };
53
58
 
54
- const fieldsList = Object.keys(data).map(f => \`\\\`\${f}\\\`\`).join(', ');
55
- const placeholders = Object.keys(data).map(f => \`:\${f}\`).join(', ');
59
+ const cleanData = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
60
+ const fieldsList = Object.keys(cleanData).map(f => \`\\\`\${f}\\\`\`).join(', ');
61
+ const placeholders = Object.keys(cleanData).map(f => \`:\${f}\`).join(', ');
56
62
 
57
63
  const result = await this.db.query(
58
64
  \`INSERT INTO \\\`\${this.tableName}\\\` (\${fieldsList}) VALUES (\${placeholders})\`,
59
- data
65
+ cleanData
60
66
  );
61
67
 
62
- if (result.success && result.insertId) {
63
- const newId = typeof result.insertId === 'string' ? parseInt(result.insertId, 10) : result.insertId;
68
+ if (result.success{{INSERT_SUCCESS_COND}}) {
69
+ {{INSERT_GET_ID}}
64
70
  return this.getById(newId) as Promise<{{ENTITY_NAME}}>;
65
71
  }
66
72
 
67
73
  throw new Error('Failed to insert {{ENTITY_NAME}}');
68
74
  }
69
75
 
70
- async update(id: number, entity: {{ENTITY_NAME}}): Promise<{{ENTITY_NAME}}> {
76
+ async update(id: {{ID_TYPE}}, entity: {{ENTITY_NAME}}): Promise<{{ENTITY_NAME}}> {
71
77
  const now = new Date();
72
- const data: Partial<{{ENTITY_NAME}}Row> & { id: number } = {
78
+ const rawData: Partial<{{ENTITY_NAME}}Row> = {
73
79
  {{UPDATE_DATA_MAPPING}},
74
- updated_at: this.toMySQLDatetime(now),
75
- id
80
+ updatedAt: this.toMySQLDatetime(now)
76
81
  };
77
82
 
78
- const updateFields = {{UPDATE_FIELDS_ARRAY}}.map(f => \`\\\`\${f}\\\` = :\${f}\`).join(', ');
83
+ const cleanData = Object.fromEntries(Object.entries(rawData).filter(([, v]) => v !== undefined));
84
+ const updateFields = {{UPDATE_FIELDS_ARRAY}}
85
+ .filter(f => f in cleanData)
86
+ .map(f => \`\\\`\${f}\\\` = :\${f}\`).join(', ');
79
87
 
80
88
  const result = await this.db.query(
81
- \`UPDATE \\\`\${this.tableName}\\\` SET \${updateFields}, updated_at = :updated_at WHERE id = :id\`,
82
- data
89
+ \`UPDATE \\\`\${this.tableName}\\\` SET \${updateFields}, updatedAt = :updatedAt WHERE {{WHERE_ID_EXPR}}\`,
90
+ { ...cleanData, id{{ID_PARAM_EXPR}} }
83
91
  );
84
92
 
85
93
  if (result.success) {
@@ -89,20 +97,20 @@ export class {{ENTITY_NAME}}Store {
89
97
  throw new Error('Failed to update {{ENTITY_NAME}}');
90
98
  }
91
99
 
92
- async softDelete(id: number): Promise<boolean> {
100
+ async softDelete(id: {{ID_TYPE}}): Promise<boolean> {
93
101
  const now = new Date();
94
102
  const result = await this.db.query(
95
- \`UPDATE \\\`\${this.tableName}\\\` SET deleted_at = :deleted_at WHERE id = :id\`,
96
- { deleted_at: this.toMySQLDatetime(now), id }
103
+ \`UPDATE \\\`\${this.tableName}\\\` SET deletedAt = :deletedAt WHERE {{WHERE_ID_EXPR}}\`,
104
+ { deletedAt: this.toMySQLDatetime(now), id{{ID_PARAM_EXPR}} }
97
105
  );
98
106
 
99
107
  return result.success;
100
108
  }
101
109
 
102
- async hardDelete(id: number): Promise<boolean> {
110
+ async hardDelete(id: {{ID_TYPE}}): Promise<boolean> {
103
111
  const result = await this.db.query(
104
- \`DELETE FROM \\\`\${this.tableName}\\\` WHERE id = :id\`,
105
- { id }
112
+ \`DELETE FROM \\\`\${this.tableName}\\\` WHERE {{WHERE_ID_EXPR}}\`,
113
+ { id{{ID_PARAM_EXPR}} }
106
114
  );
107
115
 
108
116
  return result.success;
@@ -112,7 +120,7 @@ export class {{ENTITY_NAME}}Store {
112
120
  };
113
121
  exports.storeFileTemplate = `import { Injectable } from '../../../../system';
114
122
  import { {{ENTITY_IMPORT_ITEMS}} } from '../../domain/entities/{{ENTITY_NAME}}';
115
- import type { ISqlProvider } from '@currentjs/provider-mysql';{{VALUE_OBJECT_IMPORTS}}{{AGGREGATE_REF_IMPORTS}}
123
+ import type { ISqlProvider } from '@currentjs/provider-mysql';{{CRYPTO_IMPORT}}{{VALUE_OBJECT_IMPORTS}}{{AGGREGATE_REF_IMPORTS}}
116
124
 
117
125
  {{ROW_INTERFACE}}
118
126
 
@@ -1,13 +1,14 @@
1
- import { ModuleConfig } from '../types/configTypes';
1
+ import { ModuleConfig, IdentifierType } from '../types/configTypes';
2
2
  export declare class UseCaseGenerator {
3
3
  private availableAggregates;
4
+ private identifiers;
4
5
  private generateUseCaseMethod;
5
6
  private generateGetResourceOwnerMethod;
6
7
  private generateUseCase;
7
- generateFromConfig(config: ModuleConfig): Record<string, string>;
8
- generateFromYamlFile(yamlFilePath: string): Record<string, string>;
8
+ generateFromConfig(config: ModuleConfig, identifiers?: IdentifierType): Record<string, string>;
9
+ generateFromYamlFile(yamlFilePath: string, identifiers?: IdentifierType): Record<string, string>;
9
10
  generateAndSaveFiles(yamlFilePath: string, moduleDir: string, opts?: {
10
11
  force?: boolean;
11
12
  skipOnConflict?: boolean;
12
- }): Promise<void>;
13
+ }, identifiers?: IdentifierType): Promise<void>;
13
14
  }
@@ -44,6 +44,7 @@ const typeUtils_1 = require("../utils/typeUtils");
44
44
  class UseCaseGenerator {
45
45
  constructor() {
46
46
  this.availableAggregates = new Map();
47
+ this.identifiers = 'numeric';
47
48
  }
48
49
  generateUseCaseMethod(modelName, actionName, useCaseConfig) {
49
50
  const methodName = actionName;
@@ -100,7 +101,7 @@ class UseCaseGenerator {
100
101
  }).join('\n');
101
102
  const returnStatement = '\n return result;';
102
103
  const methodParams = actionName === 'list'
103
- ? `input: ${inputType}, ownerId?: number`
104
+ ? `input: ${inputType}, ownerId?: ${(0, configTypes_1.idTsType)(this.identifiers)}`
104
105
  : `input: ${inputType}`;
105
106
  return ` async ${methodName}(${methodParams}): Promise<${returnType}> {
106
107
  ${handlerCalls}${returnStatement}
@@ -108,12 +109,13 @@ ${handlerCalls}${returnStatement}
108
109
  }
109
110
  generateGetResourceOwnerMethod(modelName) {
110
111
  const serviceVar = `${modelName.toLowerCase()}Service`;
112
+ const idTs = (0, configTypes_1.idTsType)(this.identifiers);
111
113
  return `
112
114
  /**
113
115
  * Get the owner ID of a resource by its ID.
114
116
  * Used for pre-mutation authorization checks in controllers.
115
117
  */
116
- async getResourceOwner(id: number): Promise<number | null> {
118
+ async getResourceOwner(id: ${idTs}): Promise<${idTs} | null> {
117
119
  return await this.${serviceVar}.getResourceOwner(id);
118
120
  }`;
119
121
  }
@@ -151,9 +153,10 @@ export class ${className} {
151
153
  ${methods}${getResourceOwnerMethod}
152
154
  }`;
153
155
  }
154
- generateFromConfig(config) {
156
+ generateFromConfig(config, identifiers = 'numeric') {
155
157
  var _a;
156
158
  const result = {};
159
+ this.identifiers = identifiers;
157
160
  // Collect all aggregates to know which are roots
158
161
  this.availableAggregates.clear();
159
162
  if ((_a = config.domain) === null || _a === void 0 ? void 0 : _a.aggregates) {
@@ -167,16 +170,16 @@ ${methods}${getResourceOwnerMethod}
167
170
  });
168
171
  return result;
169
172
  }
170
- generateFromYamlFile(yamlFilePath) {
173
+ generateFromYamlFile(yamlFilePath, identifiers = 'numeric') {
171
174
  const yamlContent = fs.readFileSync(yamlFilePath, 'utf8');
172
175
  const config = (0, yaml_1.parse)(yamlContent);
173
176
  if (!(0, configTypes_1.isValidModuleConfig)(config)) {
174
177
  throw new Error('Configuration does not match new module format. Expected useCases structure.');
175
178
  }
176
- return this.generateFromConfig(config);
179
+ return this.generateFromConfig(config, identifiers);
177
180
  }
178
- async generateAndSaveFiles(yamlFilePath, moduleDir, opts) {
179
- const useCasesByModel = this.generateFromYamlFile(yamlFilePath);
181
+ async generateAndSaveFiles(yamlFilePath, moduleDir, opts, identifiers = 'numeric') {
182
+ const useCasesByModel = this.generateFromYamlFile(yamlFilePath, identifiers);
180
183
  const useCasesDir = path.join(moduleDir, 'application', 'useCases');
181
184
  fs.mkdirSync(useCasesDir, { recursive: true });
182
185
  for (const [modelName, code] of Object.entries(useCasesByModel)) {
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Type definitions for the Clean Architecture module configuration
3
3
  */
4
+ export type IdentifierType = 'numeric' | 'uuid' | 'nanoid';
5
+ export declare function normalizeIdentifierType(value: string): IdentifierType;
6
+ export declare function idTsType(identifiers: IdentifierType): 'number' | 'string';
4
7
  export interface FieldDefinition {
5
8
  type: string;
6
9
  constraints?: {