@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.
@@ -69,7 +69,12 @@ class TemplateGenerator {
69
69
  .filter(([name]) => name !== 'id')
70
70
  .slice(0, 5)
71
71
  .map(([name, config]) => {
72
- const voConfig = this.valueObjects[(0, typeUtils_1.capitalize)((config.type || 'string'))];
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 voConfig = this.valueObjects[(0, typeUtils_1.capitalize)(((config === null || config === void 0 ? void 0 : config.type) || 'string'))];
140
- if (voConfig) {
141
- const parts = Object.keys(voConfig.fields)
142
- .map(sub => `{{ childItem.${name}.${sub} }}`)
143
- .join(' ');
144
- return ` <td>${parts}</td>`;
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 voConfig = this.valueObjects[(0, typeUtils_1.capitalize)((config.type || 'string'))];
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').toLowerCase();
328
- const capitalizedType = (0, typeUtils_1.capitalize)(fieldType);
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
- 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,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 deleted_at IS NULL\`,
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
- created_at: this.toMySQLDatetime(now),
51
- updated_at: this.toMySQLDatetime(now)
54
+ createdAt: this.toMySQLDatetime(now),
55
+ updatedAt: this.toMySQLDatetime(now)
52
56
  };
53
57
 
54
- const fieldsList = Object.keys(data).map(f => \`\\\`\${f}\\\`\`).join(', ');
55
- const placeholders = Object.keys(data).map(f => \`:\${f}\`).join(', ');
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
- data
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 data: Partial<{{ENTITY_NAME}}Row> & { id: number } = {
77
+ const rawData: Partial<{{ENTITY_NAME}}Row> = {
73
78
  {{UPDATE_DATA_MAPPING}},
74
- updated_at: this.toMySQLDatetime(now),
75
- id
79
+ updatedAt: this.toMySQLDatetime(now)
76
80
  };
77
81
 
78
- const updateFields = {{UPDATE_FIELDS_ARRAY}}.map(f => \`\\\`\${f}\\\` = :\${f}\`).join(', ');
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}, updated_at = :updated_at WHERE id = :id\`,
82
- data
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 deleted_at = :deleted_at WHERE id = :id\`,
96
- { deleted_at: this.toMySQLDatetime(now), id }
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
- export declare function generateCreateTableSQL(name: string, aggregate: AggregateConfig, availableAggregates: Set<string>): string;
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() + 's';
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
- function generateCreateTableSQL(name, aggregate, availableAggregates) {
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(' created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP');
108
- columns.push(' updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
109
- columns.push(' deleted_at DATETIME NULL DEFAULT NULL');
110
- indexes.push(` INDEX idx_${tableName}_deleted_at (deleted_at)`);
111
- indexes.push(` INDEX idx_${tableName}_created_at (created_at)`);
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 ${tableName} (\n${allParts.join(',\n')}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`;
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 ${tableName};`;
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 ${tableName} ADD COLUMN ${foreignKeyName} INT ${nullable};`;
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 ${tableName} ADD COLUMN ${fieldName} ${sqlType} ${nullable};`;
166
+ return `ALTER TABLE \`${tableName}\` ADD COLUMN \`${fieldName}\` ${sqlType} ${nullable};`;
128
167
  }
129
168
  }
130
169
  function generateDropColumnSQL(tableName, columnName) {
131
- return `ALTER TABLE ${tableName} DROP COLUMN ${columnName};`;
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 ${tableName} MODIFY COLUMN ${foreignKeyName} INT ${nullable};`;
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 ${tableName} MODIFY COLUMN ${fieldName} ${sqlType} ${nullable};`;
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
- sqlStatements.push(`-- Create ${name.toLowerCase()}s table`);
196
- sqlStatements.push(generateCreateTableSQL(name, aggregate, availableAggregates));
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 (value objects become string).
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;