@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.
- package/CHANGELOG.md +8 -0
- package/README.md +2 -0
- package/dist/commands/generateAll.js +7 -6
- package/dist/commands/migrateCommit.js +14 -5
- package/dist/generators/controllerGenerator.d.ts +5 -4
- package/dist/generators/controllerGenerator.js +13 -7
- package/dist/generators/domainLayerGenerator.d.ts +5 -4
- package/dist/generators/domainLayerGenerator.js +11 -8
- package/dist/generators/dtoGenerator.d.ts +5 -4
- package/dist/generators/dtoGenerator.js +29 -16
- package/dist/generators/serviceGenerator.d.ts +5 -4
- package/dist/generators/serviceGenerator.js +23 -14
- package/dist/generators/storeGenerator.d.ts +11 -4
- package/dist/generators/storeGenerator.js +160 -37
- package/dist/generators/templates/data/appYamlTemplate +1 -1
- package/dist/generators/templates/storeTemplates.d.ts +1 -1
- package/dist/generators/templates/storeTemplates.js +38 -30
- package/dist/generators/useCaseGenerator.d.ts +5 -4
- package/dist/generators/useCaseGenerator.js +10 -7
- package/dist/types/configTypes.d.ts +3 -0
- package/dist/types/configTypes.js +15 -0
- package/dist/utils/commandUtils.d.ts +1 -1
- package/dist/utils/commandUtils.js +5 -3
- package/dist/utils/migrationUtils.d.ts +14 -6
- package/dist/utils/migrationUtils.js +82 -31
- package/dist/utils/typeUtils.js +3 -3
- package/package.json +1 -1
|
@@ -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 =
|
|
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(
|
|
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} ? (
|
|
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 =
|
|
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} ? (
|
|
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}:
|
|
206
|
+
result.push(` ${ownerOrParentField}: ${idTs};`);
|
|
205
207
|
fields.forEach(([fieldName, fieldConfig]) => {
|
|
206
208
|
if (this.isAggregateField(fieldConfig)) {
|
|
207
|
-
result.push(` ${fieldName}
|
|
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
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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}
|
|
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(
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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 ?
|
|
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
|
|
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 ?
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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:
|
|
506
|
+
async getByParentId(parentId: ${idTs}): Promise<${modelName}[]> {
|
|
469
507
|
const result = await this.db.query(
|
|
470
|
-
\`SELECT ${fieldList} FROM \\\`\${this.tableName}\\\` WHERE
|
|
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:
|
|
537
|
+
async getResourceOwner(id: ${idTs}): Promise<${idTs} | null> {
|
|
491
538
|
const result = await this.db.query(
|
|
492
|
-
\`SELECT
|
|
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
|
|
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:
|
|
555
|
+
async getResourceOwner(id: ${idTs}): Promise<${idTs} | null> {
|
|
509
556
|
const result = await this.db.query(
|
|
510
|
-
\`SELECT
|
|
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
|
|
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)) {
|
|
@@ -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:
|
|
6
|
+
id: {{ID_TYPE}};
|
|
7
7
|
{{ROW_FIELDS}}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
54
|
+
{{INSERT_ID_DATA}}{{INSERT_DATA_MAPPING}},
|
|
55
|
+
createdAt: this.toMySQLDatetime(now),
|
|
56
|
+
updatedAt: this.toMySQLDatetime(now)
|
|
52
57
|
};
|
|
53
58
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
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
|
-
|
|
65
|
+
cleanData
|
|
60
66
|
);
|
|
61
67
|
|
|
62
|
-
if (result.success
|
|
63
|
-
|
|
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:
|
|
76
|
+
async update(id: {{ID_TYPE}}, entity: {{ENTITY_NAME}}): Promise<{{ENTITY_NAME}}> {
|
|
71
77
|
const now = new Date();
|
|
72
|
-
const
|
|
78
|
+
const rawData: Partial<{{ENTITY_NAME}}Row> = {
|
|
73
79
|
{{UPDATE_DATA_MAPPING}},
|
|
74
|
-
|
|
75
|
-
id
|
|
80
|
+
updatedAt: this.toMySQLDatetime(now)
|
|
76
81
|
};
|
|
77
82
|
|
|
78
|
-
const
|
|
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},
|
|
82
|
-
|
|
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:
|
|
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
|
|
96
|
-
{
|
|
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:
|
|
110
|
+
async hardDelete(id: {{ID_TYPE}}): Promise<boolean> {
|
|
103
111
|
const result = await this.db.query(
|
|
104
|
-
\`DELETE FROM \\\`\${this.tableName}\\\` WHERE
|
|
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?:
|
|
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:
|
|
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?: {
|