@currentjs/gen 0.5.3 → 0.5.5
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/dist/commands/migrateCommit.js +11 -5
- package/dist/generators/domainLayerGenerator.js +7 -8
- package/dist/generators/dtoGenerator.js +5 -6
- package/dist/generators/storeGenerator.d.ts +14 -0
- package/dist/generators/storeGenerator.js +147 -32
- package/dist/generators/templateGenerator.d.ts +15 -0
- package/dist/generators/templateGenerator.js +215 -10
- package/dist/generators/templates/storeTemplates.js +24 -17
- package/dist/utils/migrationUtils.d.ts +11 -5
- package/dist/utils/migrationUtils.js +72 -27
- package/dist/utils/typeUtils.d.ts +29 -1
- package/dist/utils/typeUtils.js +103 -9
- package/package.json +1 -1
|
@@ -69,7 +69,12 @@ class TemplateGenerator {
|
|
|
69
69
|
.filter(([name]) => name !== 'id')
|
|
70
70
|
.slice(0, 5)
|
|
71
71
|
.map(([name, config]) => {
|
|
72
|
-
const
|
|
72
|
+
const typeStr = (config.type || 'string');
|
|
73
|
+
const parsed = (0, typeUtils_1.parseFieldType)(typeStr);
|
|
74
|
+
if (parsed.isArray || parsed.isUnion) {
|
|
75
|
+
return ` <td>{{ item.${name} }}</td>`;
|
|
76
|
+
}
|
|
77
|
+
const voConfig = this.valueObjects[(0, typeUtils_1.capitalize)(typeStr)];
|
|
73
78
|
if (voConfig) {
|
|
74
79
|
const parts = Object.keys(voConfig.fields)
|
|
75
80
|
.map(sub => `{{ item.${name}.${sub} }}`)
|
|
@@ -136,12 +141,16 @@ ${fieldCells}
|
|
|
136
141
|
const fieldEntries = Object.entries(child.childFields).filter(([name]) => name !== 'id').slice(0, 5);
|
|
137
142
|
const headers = fieldEntries.map(([name]) => ` <th>${(0, typeUtils_1.capitalize)(name)}</th>`).join('\n');
|
|
138
143
|
const cells = fieldEntries.map(([name, config]) => {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
const typeStr = ((config === null || config === void 0 ? void 0 : config.type) || 'string');
|
|
145
|
+
const parsedType = (0, typeUtils_1.parseFieldType)(typeStr);
|
|
146
|
+
if (!parsedType.isArray && !parsedType.isUnion) {
|
|
147
|
+
const voConfig = this.valueObjects[(0, typeUtils_1.capitalize)(typeStr)];
|
|
148
|
+
if (voConfig) {
|
|
149
|
+
const parts = Object.keys(voConfig.fields)
|
|
150
|
+
.map(sub => `{{ childItem.${name}.${sub} }}`)
|
|
151
|
+
.join(' ');
|
|
152
|
+
return ` <td>${parts}</td>`;
|
|
153
|
+
}
|
|
145
154
|
}
|
|
146
155
|
return ` <td>{{ childItem.${name} }}</td>`;
|
|
147
156
|
}).join('\n');
|
|
@@ -177,7 +186,15 @@ ${actionLinks}
|
|
|
177
186
|
renderDetailTemplate(modelName, viewName, fields, basePath, withChildChildren) {
|
|
178
187
|
const fieldRows = fields
|
|
179
188
|
.map(([name, config]) => {
|
|
180
|
-
const
|
|
189
|
+
const typeStr = (config.type || 'string');
|
|
190
|
+
const parsed = (0, typeUtils_1.parseFieldType)(typeStr);
|
|
191
|
+
if (parsed.isArray || parsed.isUnion) {
|
|
192
|
+
return ` <div class="row mb-2">
|
|
193
|
+
<div class="col-4"><strong>${(0, typeUtils_1.capitalize)(name)}:</strong></div>
|
|
194
|
+
<div class="col-8">{{ ${name} }}</div>
|
|
195
|
+
</div>`;
|
|
196
|
+
}
|
|
197
|
+
const voConfig = this.valueObjects[(0, typeUtils_1.capitalize)(typeStr)];
|
|
181
198
|
if (voConfig) {
|
|
182
199
|
const parts = Object.keys(voConfig.fields)
|
|
183
200
|
.map(sub => `{{ ${name}.${sub} }}`)
|
|
@@ -215,6 +232,15 @@ ${fieldRows}
|
|
|
215
232
|
buildFieldTypesJson(safeFields) {
|
|
216
233
|
return JSON.stringify(safeFields.reduce((acc, [name, config]) => {
|
|
217
234
|
const typeStr = typeof (config === null || config === void 0 ? void 0 : config.type) === 'string' ? config.type : 'string';
|
|
235
|
+
const parsed = (0, typeUtils_1.parseFieldType)(typeStr);
|
|
236
|
+
// Array or union of VOs → stored as JSON, emit single "json" entry
|
|
237
|
+
if (parsed.isArray || parsed.isUnion) {
|
|
238
|
+
const anyVoReferenced = parsed.baseTypes.some(bt => this.valueObjects[(0, typeUtils_1.capitalize)(bt)]);
|
|
239
|
+
if (anyVoReferenced) {
|
|
240
|
+
acc[name] = 'json';
|
|
241
|
+
return acc;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
218
244
|
const capitalizedType = (0, typeUtils_1.capitalize)(typeStr);
|
|
219
245
|
const voConfig = this.valueObjects[capitalizedType];
|
|
220
246
|
if (voConfig) {
|
|
@@ -319,13 +345,192 @@ ${options}
|
|
|
319
345
|
<div class="row g-2">
|
|
320
346
|
${columns}
|
|
321
347
|
</div>
|
|
348
|
+
</div>`;
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Render an array-of-VOs field as checkboxes.
|
|
352
|
+
* If the VO has a single enum field: one checkbox per enum value.
|
|
353
|
+
* If the VO has multiple / non-enum fields: one labeled checkbox group per VO subfield.
|
|
354
|
+
*/
|
|
355
|
+
renderArrayVoField(name, label, voName, voConfig, isEdit) {
|
|
356
|
+
const subFields = Object.entries(voConfig.fields);
|
|
357
|
+
// Single enum field: render one checkbox per enum value
|
|
358
|
+
if (subFields.length === 1) {
|
|
359
|
+
const [subName, subConfig] = subFields[0];
|
|
360
|
+
if (typeof subConfig === 'object' && 'values' in subConfig) {
|
|
361
|
+
const uniqueValues = [...new Set(subConfig.values)];
|
|
362
|
+
const checkboxes = uniqueValues.map(v => {
|
|
363
|
+
const checkedExpr = isEdit ? ` {{ (${name} || []).some(function(item){ return item.${subName} === '${v}'; }) ? 'checked' : '' }}` : '';
|
|
364
|
+
return ` <div class="form-check form-check-inline">
|
|
365
|
+
<input type="checkbox" class="form-check-input" name="${name}[]" value="${v}"${checkedExpr}>
|
|
366
|
+
<label class="form-check-label">${v}</label>
|
|
367
|
+
</div>`;
|
|
368
|
+
}).join('\n');
|
|
369
|
+
return ` <div class="mb-3">
|
|
370
|
+
<label class="form-label">${label}</label>
|
|
371
|
+
<div>
|
|
372
|
+
${checkboxes}
|
|
373
|
+
</div>
|
|
374
|
+
</div>`;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Multi-field VO: render a repeatable entry group with one checkbox-style row per sub-field
|
|
378
|
+
const subInputs = subFields.map(([subName, subConfig]) => {
|
|
379
|
+
const subLabel = (0, typeUtils_1.capitalize)(subName);
|
|
380
|
+
if (typeof subConfig === 'object' && 'values' in subConfig) {
|
|
381
|
+
const uniqueValues = [...new Set(subConfig.values)];
|
|
382
|
+
const options = uniqueValues.map(v => `<option value="${v}">${v}</option>`).join('');
|
|
383
|
+
return ` <div class="col-auto">
|
|
384
|
+
<label class="form-label">${subLabel}</label>
|
|
385
|
+
<select class="form-select form-select-sm" name="${name}[0].${subName}"><option value="">--</option>${options}</select>
|
|
386
|
+
</div>`;
|
|
387
|
+
}
|
|
388
|
+
const inputType = this.getInputType(subConfig.type);
|
|
389
|
+
return ` <div class="col">
|
|
390
|
+
<label class="form-label">${subLabel}</label>
|
|
391
|
+
<input type="${inputType}" class="form-control form-control-sm" name="${name}[0].${subName}" placeholder="${subLabel}">
|
|
392
|
+
</div>`;
|
|
393
|
+
}).join('\n');
|
|
394
|
+
return ` <div class="mb-3">
|
|
395
|
+
<label class="form-label">${label}</label>
|
|
396
|
+
<div class="border rounded p-2">
|
|
397
|
+
<div class="row g-2 align-items-end">
|
|
398
|
+
${subInputs}
|
|
399
|
+
</div>
|
|
400
|
+
<small class="text-muted">Add multiple ${voName} entries as needed.</small>
|
|
401
|
+
</div>
|
|
402
|
+
</div>`;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Render a union-of-VOs field as a type selector with sub-fields for each VO type.
|
|
406
|
+
*/
|
|
407
|
+
renderUnionVoField(name, label, unionVoNames, isEdit) {
|
|
408
|
+
const typeOptions = unionVoNames.map(voName => {
|
|
409
|
+
const sel = isEdit ? ` {{ ${name}._type === '${voName}' ? 'selected' : '' }}` : '';
|
|
410
|
+
return ` <option value="${voName}"${sel}>${voName}</option>`;
|
|
411
|
+
}).join('\n');
|
|
412
|
+
const subFieldGroups = unionVoNames.map(voName => {
|
|
413
|
+
const voConfig = this.valueObjects[voName];
|
|
414
|
+
if (!voConfig)
|
|
415
|
+
return '';
|
|
416
|
+
const subInputs = Object.entries(voConfig.fields).map(([subName, subConfig]) => {
|
|
417
|
+
const subLabel = (0, typeUtils_1.capitalize)(subName);
|
|
418
|
+
const fullName = `${name}.${subName}`;
|
|
419
|
+
if (typeof subConfig === 'object' && 'values' in subConfig) {
|
|
420
|
+
const uniqueValues = [...new Set(subConfig.values)];
|
|
421
|
+
const options = uniqueValues.map(v => {
|
|
422
|
+
const sel = isEdit ? ` {{ ${name}.${subName} === '${v}' ? 'selected' : '' }}` : '';
|
|
423
|
+
return ` <option value="${v}"${sel}>${v}</option>`;
|
|
424
|
+
}).join('\n');
|
|
425
|
+
return ` <div class="col-auto">
|
|
426
|
+
<label class="form-label">${subLabel}</label>
|
|
427
|
+
<select class="form-select" id="${fullName}" name="${fullName}">
|
|
428
|
+
<option value="">-- ${subLabel} --</option>
|
|
429
|
+
${options}
|
|
430
|
+
</select>
|
|
431
|
+
</div>`;
|
|
432
|
+
}
|
|
433
|
+
const inputType = this.getInputType(subConfig.type);
|
|
434
|
+
const value = isEdit ? ` value="{{ ${name}.${subName} || '' }}"` : '';
|
|
435
|
+
return ` <div class="col">
|
|
436
|
+
<label class="form-label">${subLabel}</label>
|
|
437
|
+
<input type="${inputType}" class="form-control" id="${fullName}" name="${fullName}" placeholder="${subLabel}"${value}>
|
|
438
|
+
</div>`;
|
|
439
|
+
}).join('\n');
|
|
440
|
+
return ` <div class="${name}-fields-${voName}">
|
|
441
|
+
<div class="row g-2">
|
|
442
|
+
${subInputs}
|
|
443
|
+
</div>
|
|
444
|
+
</div>`;
|
|
445
|
+
}).join('\n');
|
|
446
|
+
return ` <div class="mb-3">
|
|
447
|
+
<label for="${name}_type" class="form-label">${label} Type</label>
|
|
448
|
+
<select class="form-select mb-2" id="${name}_type" name="${name}._type">
|
|
449
|
+
<option value="">-- Select type --</option>
|
|
450
|
+
${typeOptions}
|
|
451
|
+
</select>
|
|
452
|
+
${subFieldGroups}
|
|
453
|
+
</div>`;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Render an array-of-union-VOs field as a repeatable group where each item
|
|
457
|
+
* has a type selector and conditionally-shown sub-fields per VO type.
|
|
458
|
+
*/
|
|
459
|
+
renderArrayUnionVoField(name, label, unionVoNames, isEdit) {
|
|
460
|
+
const typeOptions = unionVoNames.map(voName => {
|
|
461
|
+
return ` <option value="${voName}">${voName}</option>`;
|
|
462
|
+
}).join('\n');
|
|
463
|
+
const subFieldGroups = unionVoNames.map(voName => {
|
|
464
|
+
const voConfig = this.valueObjects[voName];
|
|
465
|
+
if (!voConfig)
|
|
466
|
+
return '';
|
|
467
|
+
const subInputs = Object.entries(voConfig.fields).map(([subName, subConfig]) => {
|
|
468
|
+
const subLabel = (0, typeUtils_1.capitalize)(subName);
|
|
469
|
+
const fullName = `${name}[0].${subName}`;
|
|
470
|
+
if (typeof subConfig === 'object' && 'values' in subConfig) {
|
|
471
|
+
const uniqueValues = [...new Set(subConfig.values)];
|
|
472
|
+
const options = uniqueValues.map(v => `<option value="${v}">${v}</option>`).join('');
|
|
473
|
+
return ` <div class="col-auto">
|
|
474
|
+
<label class="form-label">${subLabel}</label>
|
|
475
|
+
<select class="form-select form-select-sm" name="${fullName}"><option value="">--</option>${options}</select>
|
|
476
|
+
</div>`;
|
|
477
|
+
}
|
|
478
|
+
const inputType = this.getInputType(subConfig.type);
|
|
479
|
+
return ` <div class="col">
|
|
480
|
+
<label class="form-label">${subLabel}</label>
|
|
481
|
+
<input type="${inputType}" class="form-control form-control-sm" name="${fullName}" placeholder="${subLabel}">
|
|
482
|
+
</div>`;
|
|
483
|
+
}).join('\n');
|
|
484
|
+
return ` <div class="${name}-fields-${voName}">
|
|
485
|
+
<div class="row g-2">
|
|
486
|
+
${subInputs}
|
|
487
|
+
</div>
|
|
488
|
+
</div>`;
|
|
489
|
+
}).join('\n');
|
|
490
|
+
const editHint = isEdit ? ` <!-- existing items rendered server-side -->` : '';
|
|
491
|
+
return ` <div class="mb-3">
|
|
492
|
+
<label class="form-label">${label}</label>
|
|
493
|
+
<div class="border rounded p-2" id="${name}-container">${editHint}
|
|
494
|
+
<div class="${name}-entry mb-2">
|
|
495
|
+
<select class="form-select form-select-sm mb-1" name="${name}[0]._type">
|
|
496
|
+
<option value="">-- Select type --</option>
|
|
497
|
+
${typeOptions}
|
|
498
|
+
</select>
|
|
499
|
+
${subFieldGroups}
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
<small class="text-muted">Add multiple ${label} entries as needed.</small>
|
|
322
503
|
</div>`;
|
|
323
504
|
}
|
|
324
505
|
renderFormField(name, config, enumValues = [], isEdit = false) {
|
|
325
506
|
const required = config.required ? 'required' : '';
|
|
326
507
|
const label = (0, typeUtils_1.capitalize)(name);
|
|
327
|
-
const fieldType = (config.type || 'string')
|
|
328
|
-
const
|
|
508
|
+
const fieldType = (config.type || 'string');
|
|
509
|
+
const parsed = (0, typeUtils_1.parseFieldType)(fieldType);
|
|
510
|
+
// Array of union of value objects
|
|
511
|
+
if (parsed.isArray && parsed.isUnion) {
|
|
512
|
+
const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(n => this.valueObjects[n]);
|
|
513
|
+
if (unionVoNames.length > 0) {
|
|
514
|
+
return this.renderArrayUnionVoField(name, label, unionVoNames, isEdit);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Array of value objects
|
|
518
|
+
if (parsed.isArray) {
|
|
519
|
+
const voName = (0, typeUtils_1.capitalize)(parsed.baseTypes[0]);
|
|
520
|
+
const voConfig = this.valueObjects[voName];
|
|
521
|
+
if (voConfig) {
|
|
522
|
+
return this.renderArrayVoField(name, label, voName, voConfig, isEdit);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// Union of value objects
|
|
526
|
+
if (parsed.isUnion) {
|
|
527
|
+
const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(n => this.valueObjects[n]);
|
|
528
|
+
if (unionVoNames.length > 0) {
|
|
529
|
+
return this.renderUnionVoField(name, label, unionVoNames, isEdit);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Simple value object
|
|
533
|
+
const capitalizedType = (0, typeUtils_1.capitalize)(fieldType.toLowerCase());
|
|
329
534
|
const voConfig = this.valueObjects[capitalizedType];
|
|
330
535
|
if (voConfig) {
|
|
331
536
|
return this.renderValueObjectField(name, label, voConfig, required, isEdit);
|
|
@@ -5,9 +5,9 @@ exports.storeTemplates = {
|
|
|
5
5
|
rowInterface: `export interface {{ENTITY_NAME}}Row {
|
|
6
6
|
id: number;
|
|
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,6 +22,10 @@ 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
|
+
|
|
25
29
|
private rowToModel(row: {{ENTITY_NAME}}Row): {{ENTITY_NAME}} {
|
|
26
30
|
return new {{ENTITY_NAME}}(
|
|
27
31
|
row.id,
|
|
@@ -33,7 +37,7 @@ export class {{ENTITY_NAME}}Store {
|
|
|
33
37
|
|
|
34
38
|
async getById(id: number): Promise<{{ENTITY_NAME}} | null> {
|
|
35
39
|
const result = await this.db.query(
|
|
36
|
-
\`SELECT {{FIELD_NAMES}} FROM \\\`\${this.tableName}\\\` WHERE id = :id AND
|
|
40
|
+
\`SELECT {{FIELD_NAMES}} FROM \\\`\${this.tableName}\\\` WHERE id = :id AND deletedAt IS NULL\`,
|
|
37
41
|
{ id }
|
|
38
42
|
);
|
|
39
43
|
|
|
@@ -47,16 +51,17 @@ export class {{ENTITY_NAME}}Store {
|
|
|
47
51
|
const now = new Date();
|
|
48
52
|
const data: Partial<{{ENTITY_NAME}}Row> = {
|
|
49
53
|
{{INSERT_DATA_MAPPING}},
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
createdAt: this.toMySQLDatetime(now),
|
|
55
|
+
updatedAt: this.toMySQLDatetime(now)
|
|
52
56
|
};
|
|
53
57
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
58
|
+
const cleanData = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
|
|
59
|
+
const fieldsList = Object.keys(cleanData).map(f => \`\\\`\${f}\\\`\`).join(', ');
|
|
60
|
+
const placeholders = Object.keys(cleanData).map(f => \`:\${f}\`).join(', ');
|
|
56
61
|
|
|
57
62
|
const result = await this.db.query(
|
|
58
63
|
\`INSERT INTO \\\`\${this.tableName}\\\` (\${fieldsList}) VALUES (\${placeholders})\`,
|
|
59
|
-
|
|
64
|
+
cleanData
|
|
60
65
|
);
|
|
61
66
|
|
|
62
67
|
if (result.success && result.insertId) {
|
|
@@ -69,17 +74,19 @@ export class {{ENTITY_NAME}}Store {
|
|
|
69
74
|
|
|
70
75
|
async update(id: number, entity: {{ENTITY_NAME}}): Promise<{{ENTITY_NAME}}> {
|
|
71
76
|
const now = new Date();
|
|
72
|
-
const
|
|
77
|
+
const rawData: Partial<{{ENTITY_NAME}}Row> = {
|
|
73
78
|
{{UPDATE_DATA_MAPPING}},
|
|
74
|
-
|
|
75
|
-
id
|
|
79
|
+
updatedAt: this.toMySQLDatetime(now)
|
|
76
80
|
};
|
|
77
81
|
|
|
78
|
-
const
|
|
82
|
+
const cleanData = Object.fromEntries(Object.entries(rawData).filter(([, v]) => v !== undefined));
|
|
83
|
+
const updateFields = {{UPDATE_FIELDS_ARRAY}}
|
|
84
|
+
.filter(f => f in cleanData)
|
|
85
|
+
.map(f => \`\\\`\${f}\\\` = :\${f}\`).join(', ');
|
|
79
86
|
|
|
80
87
|
const result = await this.db.query(
|
|
81
|
-
\`UPDATE \\\`\${this.tableName}\\\` SET \${updateFields},
|
|
82
|
-
|
|
88
|
+
\`UPDATE \\\`\${this.tableName}\\\` SET \${updateFields}, updatedAt = :updatedAt WHERE id = :id\`,
|
|
89
|
+
{ ...cleanData, id }
|
|
83
90
|
);
|
|
84
91
|
|
|
85
92
|
if (result.success) {
|
|
@@ -92,8 +99,8 @@ export class {{ENTITY_NAME}}Store {
|
|
|
92
99
|
async softDelete(id: number): Promise<boolean> {
|
|
93
100
|
const now = new Date();
|
|
94
101
|
const result = await this.db.query(
|
|
95
|
-
\`UPDATE \\\`\${this.tableName}\\\` SET
|
|
96
|
-
{
|
|
102
|
+
\`UPDATE \\\`\${this.tableName}\\\` SET deletedAt = :deletedAt WHERE id = :id\`,
|
|
103
|
+
{ deletedAt: this.toMySQLDatetime(now), id }
|
|
97
104
|
);
|
|
98
105
|
|
|
99
106
|
return result.success;
|
|
@@ -21,19 +21,25 @@ export interface ForeignKeyInfo {
|
|
|
21
21
|
REFERENCED_TABLE_NAME: string;
|
|
22
22
|
REFERENCED_COLUMN_NAME: string;
|
|
23
23
|
}
|
|
24
|
-
export declare function mapYamlTypeToSql(yamlType: string, availableAggregates: Set<string>): string;
|
|
24
|
+
export declare function mapYamlTypeToSql(yamlType: string, availableAggregates: Set<string>, availableValueObjects?: Set<string>): string;
|
|
25
|
+
/** Table name matches the store convention: singular lowercase aggregate name. */
|
|
25
26
|
export declare function getTableName(aggregateName: string): string;
|
|
26
27
|
export declare function getForeignKeyFieldName(fieldName: string): string;
|
|
27
28
|
export declare function isRelationshipField(fieldType: string, availableAggregates: Set<string>): boolean;
|
|
28
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Build a map of child entity name → parent entity name from the aggregates config.
|
|
31
|
+
* Used to determine parent ID column names for child entity tables.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildChildToParentMap(aggregates: Record<string, AggregateConfig>): Map<string, string>;
|
|
34
|
+
export declare function generateCreateTableSQL(name: string, aggregate: AggregateConfig, availableAggregates: Set<string>, availableValueObjects?: Set<string>, parentIdField?: string): string;
|
|
29
35
|
export declare function generateDropTableSQL(tableName: string): string;
|
|
30
|
-
export declare function generateAddColumnSQL(tableName: string, fieldName: string, field: AggregateFieldConfig, availableAggregates: Set<string>): string;
|
|
36
|
+
export declare function generateAddColumnSQL(tableName: string, fieldName: string, field: AggregateFieldConfig, availableAggregates: Set<string>, availableValueObjects?: Set<string>): string;
|
|
31
37
|
export declare function generateDropColumnSQL(tableName: string, columnName: string): string;
|
|
32
|
-
export declare function generateModifyColumnSQL(tableName: string, fieldName: string, field: AggregateFieldConfig, availableAggregates: Set<string>): string;
|
|
38
|
+
export declare function generateModifyColumnSQL(tableName: string, fieldName: string, field: AggregateFieldConfig, availableAggregates: Set<string>, availableValueObjects?: Set<string>): string;
|
|
33
39
|
export declare function loadSchemaState(stateFilePath: string): SchemaState | null;
|
|
34
40
|
export declare function saveSchemaState(stateFilePath: string, state: SchemaState): void;
|
|
35
41
|
export declare function loadMigrationLog(logFilePath: string): MigrationLog;
|
|
36
42
|
export declare function saveMigrationLog(logFilePath: string, log: MigrationLog): void;
|
|
37
|
-
export declare function compareSchemas(oldState: SchemaState | null, newAggregates: Record<string, AggregateConfig>): string[];
|
|
43
|
+
export declare function compareSchemas(oldState: SchemaState | null, newAggregates: Record<string, AggregateConfig>, availableValueObjects?: Set<string>): string[];
|
|
38
44
|
export declare function generateTimestamp(): string;
|
|
39
45
|
export declare function getMigrationFileName(timestamp: string): string;
|
|
@@ -37,6 +37,7 @@ exports.mapYamlTypeToSql = mapYamlTypeToSql;
|
|
|
37
37
|
exports.getTableName = getTableName;
|
|
38
38
|
exports.getForeignKeyFieldName = getForeignKeyFieldName;
|
|
39
39
|
exports.isRelationshipField = isRelationshipField;
|
|
40
|
+
exports.buildChildToParentMap = buildChildToParentMap;
|
|
40
41
|
exports.generateCreateTableSQL = generateCreateTableSQL;
|
|
41
42
|
exports.generateDropTableSQL = generateDropTableSQL;
|
|
42
43
|
exports.generateAddColumnSQL = generateAddColumnSQL;
|
|
@@ -52,23 +53,39 @@ exports.getMigrationFileName = getMigrationFileName;
|
|
|
52
53
|
const fs = __importStar(require("fs"));
|
|
53
54
|
const path = __importStar(require("path"));
|
|
54
55
|
const yaml_1 = require("yaml");
|
|
56
|
+
const typeUtils_1 = require("./typeUtils");
|
|
55
57
|
const TYPE_MAPPING = {
|
|
56
58
|
string: 'VARCHAR(255)',
|
|
57
59
|
number: 'INT',
|
|
60
|
+
integer: 'INT',
|
|
61
|
+
decimal: 'DECIMAL(10,2)',
|
|
58
62
|
boolean: 'TINYINT(1)',
|
|
59
63
|
datetime: 'DATETIME',
|
|
64
|
+
date: 'DATETIME',
|
|
65
|
+
id: 'INT',
|
|
60
66
|
json: 'JSON',
|
|
61
67
|
array: 'JSON',
|
|
62
68
|
object: 'JSON'
|
|
63
69
|
};
|
|
64
|
-
function mapYamlTypeToSql(yamlType, availableAggregates) {
|
|
70
|
+
function mapYamlTypeToSql(yamlType, availableAggregates, availableValueObjects) {
|
|
71
|
+
// Simple aggregate reference → foreign key INT
|
|
65
72
|
if (availableAggregates.has(yamlType)) {
|
|
66
73
|
return 'INT';
|
|
67
74
|
}
|
|
75
|
+
// Compound types: array ("Foo[]") or union ("Foo | Bar") → JSON column
|
|
76
|
+
const parsed = (0, typeUtils_1.parseFieldType)(yamlType);
|
|
77
|
+
if (parsed.isArray || parsed.isUnion) {
|
|
78
|
+
return 'JSON';
|
|
79
|
+
}
|
|
80
|
+
// Named value object → stored as JSON
|
|
81
|
+
if (availableValueObjects && availableValueObjects.has(yamlType)) {
|
|
82
|
+
return 'JSON';
|
|
83
|
+
}
|
|
68
84
|
return TYPE_MAPPING[yamlType] || 'VARCHAR(255)';
|
|
69
85
|
}
|
|
86
|
+
/** Table name matches the store convention: singular lowercase aggregate name. */
|
|
70
87
|
function getTableName(aggregateName) {
|
|
71
|
-
return aggregateName.toLowerCase()
|
|
88
|
+
return aggregateName.toLowerCase();
|
|
72
89
|
}
|
|
73
90
|
function getForeignKeyFieldName(fieldName) {
|
|
74
91
|
return fieldName + 'Id';
|
|
@@ -76,12 +93,34 @@ function getForeignKeyFieldName(fieldName) {
|
|
|
76
93
|
function isRelationshipField(fieldType, availableAggregates) {
|
|
77
94
|
return availableAggregates.has(fieldType);
|
|
78
95
|
}
|
|
79
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Build a map of child entity name → parent entity name from the aggregates config.
|
|
98
|
+
* Used to determine parent ID column names for child entity tables.
|
|
99
|
+
*/
|
|
100
|
+
function buildChildToParentMap(aggregates) {
|
|
101
|
+
const map = new Map();
|
|
102
|
+
for (const [parentName, config] of Object.entries(aggregates)) {
|
|
103
|
+
for (const childName of (config.entities || [])) {
|
|
104
|
+
map.set(childName, parentName);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return map;
|
|
108
|
+
}
|
|
109
|
+
function generateCreateTableSQL(name, aggregate, availableAggregates, availableValueObjects, parentIdField) {
|
|
80
110
|
const tableName = getTableName(name);
|
|
81
111
|
const columns = [];
|
|
82
112
|
const indexes = [];
|
|
83
113
|
const foreignKeys = [];
|
|
84
114
|
columns.push(' id INT AUTO_INCREMENT PRIMARY KEY');
|
|
115
|
+
// Root aggregates get an ownerId column; child entities get a parent ID column.
|
|
116
|
+
if (parentIdField) {
|
|
117
|
+
columns.push(` ${parentIdField} INT NOT NULL`);
|
|
118
|
+
indexes.push(` INDEX idx_${tableName}_${parentIdField} (${parentIdField})`);
|
|
119
|
+
}
|
|
120
|
+
else if (aggregate.root !== false) {
|
|
121
|
+
columns.push(' ownerId INT NOT NULL');
|
|
122
|
+
indexes.push(` INDEX idx_${tableName}_ownerId (ownerId)`);
|
|
123
|
+
}
|
|
85
124
|
for (const [fieldName, field] of Object.entries(aggregate.fields)) {
|
|
86
125
|
if (isRelationshipField(field.type, availableAggregates)) {
|
|
87
126
|
const foreignKeyName = getForeignKeyFieldName(fieldName);
|
|
@@ -96,50 +135,50 @@ function generateCreateTableSQL(name, aggregate, availableAggregates) {
|
|
|
96
135
|
` ON UPDATE CASCADE`);
|
|
97
136
|
}
|
|
98
137
|
else {
|
|
99
|
-
const sqlType = mapYamlTypeToSql(field.type, availableAggregates);
|
|
138
|
+
const sqlType = mapYamlTypeToSql(field.type, availableAggregates, availableValueObjects);
|
|
100
139
|
const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
|
|
101
140
|
columns.push(` ${fieldName} ${sqlType} ${nullable}`);
|
|
102
|
-
if (['string', 'number'].includes(field.type)) {
|
|
141
|
+
if (['string', 'number', 'integer', 'id'].includes(field.type)) {
|
|
103
142
|
indexes.push(` INDEX idx_${tableName}_${fieldName} (${fieldName})`);
|
|
104
143
|
}
|
|
105
144
|
}
|
|
106
145
|
}
|
|
107
|
-
columns.push('
|
|
108
|
-
columns.push('
|
|
109
|
-
columns.push('
|
|
110
|
-
indexes.push(` INDEX idx_${tableName}
|
|
111
|
-
indexes.push(` INDEX idx_${tableName}
|
|
146
|
+
columns.push(' createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP');
|
|
147
|
+
columns.push(' updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
148
|
+
columns.push(' deletedAt DATETIME NULL DEFAULT NULL');
|
|
149
|
+
indexes.push(` INDEX idx_${tableName}_deletedAt (deletedAt)`);
|
|
150
|
+
indexes.push(` INDEX idx_${tableName}_createdAt (createdAt)`);
|
|
112
151
|
const allParts = [...columns, ...indexes, ...foreignKeys];
|
|
113
|
-
return `CREATE TABLE IF NOT EXISTS
|
|
152
|
+
return `CREATE TABLE IF NOT EXISTS \`${tableName}\` (\n${allParts.join(',\n')}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`;
|
|
114
153
|
}
|
|
115
154
|
function generateDropTableSQL(tableName) {
|
|
116
|
-
return `DROP TABLE IF EXISTS
|
|
155
|
+
return `DROP TABLE IF EXISTS \`${tableName}\`;`;
|
|
117
156
|
}
|
|
118
|
-
function generateAddColumnSQL(tableName, fieldName, field, availableAggregates) {
|
|
157
|
+
function generateAddColumnSQL(tableName, fieldName, field, availableAggregates, availableValueObjects) {
|
|
119
158
|
if (isRelationshipField(field.type, availableAggregates)) {
|
|
120
159
|
const foreignKeyName = getForeignKeyFieldName(fieldName);
|
|
121
160
|
const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
|
|
122
|
-
return `ALTER TABLE
|
|
161
|
+
return `ALTER TABLE \`${tableName}\` ADD COLUMN \`${foreignKeyName}\` INT ${nullable};`;
|
|
123
162
|
}
|
|
124
163
|
else {
|
|
125
|
-
const sqlType = mapYamlTypeToSql(field.type, availableAggregates);
|
|
164
|
+
const sqlType = mapYamlTypeToSql(field.type, availableAggregates, availableValueObjects);
|
|
126
165
|
const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
|
|
127
|
-
return `ALTER TABLE
|
|
166
|
+
return `ALTER TABLE \`${tableName}\` ADD COLUMN \`${fieldName}\` ${sqlType} ${nullable};`;
|
|
128
167
|
}
|
|
129
168
|
}
|
|
130
169
|
function generateDropColumnSQL(tableName, columnName) {
|
|
131
|
-
return `ALTER TABLE
|
|
170
|
+
return `ALTER TABLE \`${tableName}\` DROP COLUMN \`${columnName}\`;`;
|
|
132
171
|
}
|
|
133
|
-
function generateModifyColumnSQL(tableName, fieldName, field, availableAggregates) {
|
|
172
|
+
function generateModifyColumnSQL(tableName, fieldName, field, availableAggregates, availableValueObjects) {
|
|
134
173
|
if (isRelationshipField(field.type, availableAggregates)) {
|
|
135
174
|
const foreignKeyName = getForeignKeyFieldName(fieldName);
|
|
136
175
|
const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
|
|
137
|
-
return `ALTER TABLE
|
|
176
|
+
return `ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${foreignKeyName}\` INT ${nullable};`;
|
|
138
177
|
}
|
|
139
178
|
else {
|
|
140
|
-
const sqlType = mapYamlTypeToSql(field.type, availableAggregates);
|
|
179
|
+
const sqlType = mapYamlTypeToSql(field.type, availableAggregates, availableValueObjects);
|
|
141
180
|
const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
|
|
142
|
-
return `ALTER TABLE
|
|
181
|
+
return `ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${fieldName}\` ${sqlType} ${nullable};`;
|
|
143
182
|
}
|
|
144
183
|
}
|
|
145
184
|
function loadSchemaState(stateFilePath) {
|
|
@@ -186,14 +225,18 @@ function sortAggregatesByDependencies(aggregates, availableAggregates) {
|
|
|
186
225
|
}
|
|
187
226
|
return sorted;
|
|
188
227
|
}
|
|
189
|
-
function compareSchemas(oldState, newAggregates) {
|
|
228
|
+
function compareSchemas(oldState, newAggregates, availableValueObjects) {
|
|
190
229
|
const sqlStatements = [];
|
|
191
230
|
const availableAggregates = new Set(Object.keys(newAggregates));
|
|
231
|
+
const childToParent = buildChildToParentMap(newAggregates);
|
|
192
232
|
if (!oldState || !oldState.aggregates || Object.keys(oldState.aggregates).length === 0) {
|
|
193
233
|
const sorted = sortAggregatesByDependencies(newAggregates, availableAggregates);
|
|
194
234
|
for (const [name, aggregate] of sorted) {
|
|
195
|
-
|
|
196
|
-
|
|
235
|
+
const tableName = getTableName(name);
|
|
236
|
+
const parentName = childToParent.get(name);
|
|
237
|
+
const parentIdField = parentName ? `${parentName.toLowerCase()}Id` : undefined;
|
|
238
|
+
sqlStatements.push(`-- Create ${tableName} table`);
|
|
239
|
+
sqlStatements.push(generateCreateTableSQL(name, aggregate, availableAggregates, availableValueObjects, parentIdField));
|
|
197
240
|
sqlStatements.push('');
|
|
198
241
|
}
|
|
199
242
|
return sqlStatements;
|
|
@@ -212,9 +255,11 @@ function compareSchemas(oldState, newAggregates) {
|
|
|
212
255
|
for (const [name, newAggregate] of Object.entries(newAggregates)) {
|
|
213
256
|
const oldAggregate = oldAggregates[name];
|
|
214
257
|
const tableName = getTableName(name);
|
|
258
|
+
const parentName = childToParent.get(name);
|
|
259
|
+
const parentIdField = parentName ? `${parentName.toLowerCase()}Id` : undefined;
|
|
215
260
|
if (!oldAggregate) {
|
|
216
261
|
sqlStatements.push(`-- Create ${tableName} table`);
|
|
217
|
-
sqlStatements.push(generateCreateTableSQL(name, newAggregate, availableAggregates));
|
|
262
|
+
sqlStatements.push(generateCreateTableSQL(name, newAggregate, availableAggregates, availableValueObjects, parentIdField));
|
|
218
263
|
sqlStatements.push('');
|
|
219
264
|
}
|
|
220
265
|
else {
|
|
@@ -236,7 +281,7 @@ function compareSchemas(oldState, newAggregates) {
|
|
|
236
281
|
const oldField = oldFields[fieldName];
|
|
237
282
|
if (!oldField) {
|
|
238
283
|
sqlStatements.push(`-- Add column ${fieldName} to ${tableName}`);
|
|
239
|
-
sqlStatements.push(generateAddColumnSQL(tableName, fieldName, newField, availableAggregates));
|
|
284
|
+
sqlStatements.push(generateAddColumnSQL(tableName, fieldName, newField, availableAggregates, availableValueObjects));
|
|
240
285
|
sqlStatements.push('');
|
|
241
286
|
}
|
|
242
287
|
else {
|
|
@@ -244,7 +289,7 @@ function compareSchemas(oldState, newAggregates) {
|
|
|
244
289
|
const requiredChanged = oldField.required !== newField.required;
|
|
245
290
|
if (typeChanged || requiredChanged) {
|
|
246
291
|
sqlStatements.push(`-- Modify column ${fieldName} in ${tableName}`);
|
|
247
|
-
sqlStatements.push(generateModifyColumnSQL(tableName, fieldName, newField, availableAggregates));
|
|
292
|
+
sqlStatements.push(generateModifyColumnSQL(tableName, fieldName, newField, availableAggregates, availableValueObjects));
|
|
248
293
|
sqlStatements.push('');
|
|
249
294
|
}
|
|
250
295
|
}
|
|
@@ -9,8 +9,35 @@ export declare const TYPE_MAPPING: Record<string, string>;
|
|
|
9
9
|
/** Store-specific: YAML type to DB row TypeScript type (e.g. datetime -> string). */
|
|
10
10
|
export declare const ROW_TYPE_MAPPING: Record<string, string>;
|
|
11
11
|
export declare function capitalize(str: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Parsed representation of a compound YAML field type.
|
|
14
|
+
* Supports array syntax ("Foo[]") and union syntax ("Foo | Bar").
|
|
15
|
+
*/
|
|
16
|
+
export interface ParsedFieldType {
|
|
17
|
+
/** Individual base type names without modifiers. */
|
|
18
|
+
baseTypes: string[];
|
|
19
|
+
/** True when the type ends with "[]" (array of values). */
|
|
20
|
+
isArray: boolean;
|
|
21
|
+
/** True when the type contains "|" (union of types). */
|
|
22
|
+
isUnion: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Parse a YAML field type string into its structural components.
|
|
26
|
+
* Handles simple types ("Money"), array types ("Money[]"), union types ("Foo | Bar"),
|
|
27
|
+
* and array-of-union types ("(Foo | Bar)[]").
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseFieldType(typeStr: string): ParsedFieldType;
|
|
30
|
+
/**
|
|
31
|
+
* Returns true when any base type in the (possibly compound) type expression is a known value object.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isValueObjectFieldType(typeStr: string, valueObjects: Set<string> | Map<string, unknown>): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the set of value object names referenced in a (possibly compound) type expression.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getReferencedValueObjects(typeStr: string, valueObjects: Set<string> | Map<string, unknown>): Set<string>;
|
|
12
38
|
/**
|
|
13
39
|
* Map a YAML field type to TypeScript type, resolving aggregates and value objects by name.
|
|
40
|
+
* Supports compound types: "Foo[]" -> "Foo[]", "Foo | Bar" -> "Foo | Bar".
|
|
14
41
|
*/
|
|
15
42
|
export declare function mapType(yamlType: string, aggregates?: Set<string> | Map<string, unknown>, valueObjects?: Set<string> | Map<string, unknown>): string;
|
|
16
43
|
/**
|
|
@@ -18,6 +45,7 @@ export declare function mapType(yamlType: string, aggregates?: Set<string> | Map
|
|
|
18
45
|
*/
|
|
19
46
|
export declare function isAggregateReference(yamlType: string, aggregates?: Set<string> | Map<string, unknown>): boolean;
|
|
20
47
|
/**
|
|
21
|
-
* Map a YAML type to the store row TypeScript type
|
|
48
|
+
* Map a YAML type to the store row TypeScript type.
|
|
49
|
+
* Value objects (including compound types) become "string" (stored as JSON).
|
|
22
50
|
*/
|
|
23
51
|
export declare function mapRowType(yamlType: string, valueObjects?: Set<string> | Map<string, unknown>): string;
|