@currentjs/gen 0.5.3 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.4] - 2026-04-07
4
+
5
+ - Array and union value objects
6
+
3
7
  ## [0.5.3] - 2026-04-07
4
8
 
5
9
  - expand autowiring (DI) for providers
@@ -137,14 +137,13 @@ class DomainLayerGenerator {
137
137
  .filter(entityName => entityName !== name && allAggregates[entityName])
138
138
  .map(entityName => `import { ${entityName} } from './${entityName}';`)
139
139
  .join('\n');
140
- // Generate imports for value objects used in fields
141
- const valueObjectImports = fields
142
- .filter(([, fieldConfig]) => this.availableValueObjects.has((0, typeUtils_1.capitalize)(fieldConfig.type)))
143
- .map(([, fieldConfig]) => {
144
- const voName = (0, typeUtils_1.capitalize)(fieldConfig.type);
145
- return `import { ${voName} } from '../valueObjects/${voName}';`;
146
- })
147
- .filter((imp, idx, arr) => arr.indexOf(imp) === idx) // dedupe
140
+ // Generate imports for value objects used in fields (including compound types like Foo[] and Foo | Bar)
141
+ const referencedVoNames = new Set();
142
+ fields.forEach(([, fieldConfig]) => {
143
+ (0, typeUtils_1.getReferencedValueObjects)(fieldConfig.type, this.availableValueObjects).forEach(name => referencedVoNames.add(name));
144
+ });
145
+ const valueObjectImports = [...referencedVoNames]
146
+ .map(voName => `import { ${voName} } from '../valueObjects/${voName}';`)
148
147
  .join('\n');
149
148
  // Generate imports for aggregate references in fields (e.g. idea: { type: Idea })
150
149
  const aggregateRefImports = fields
@@ -51,8 +51,7 @@ class DtoGenerator {
51
51
  return (0, typeUtils_1.mapType)(yamlType, this.availableAggregates, this.availableValueObjects);
52
52
  }
53
53
  isValueObjectType(yamlType) {
54
- const capitalizedType = (0, typeUtils_1.capitalize)(yamlType);
55
- return this.availableValueObjects.has(capitalizedType);
54
+ return (0, typeUtils_1.isValueObjectFieldType)(yamlType, this.availableValueObjects);
56
55
  }
57
56
  getValidationCode(fieldName, fieldType, isRequired) {
58
57
  const checks = [];
@@ -431,11 +430,11 @@ ${mappingsStr}
431
430
  if (outputConfig.pick && outputConfig.pick.length > 0) {
432
431
  fieldsToCheck = fieldsToCheck.filter(([fieldName]) => outputConfig.pick.includes(fieldName));
433
432
  }
434
- // Check each field for value object types
433
+ // Check each field for value object types (including compound types like Foo[] and Foo | Bar)
435
434
  fieldsToCheck.forEach(([, fieldConfig]) => {
436
- if (this.isValueObjectType(fieldConfig.type)) {
437
- valueObjects.add((0, typeUtils_1.capitalize)(fieldConfig.type));
438
- }
435
+ (0, typeUtils_1.getReferencedValueObjects)(fieldConfig.type, this.availableValueObjects).forEach(voName => {
436
+ valueObjects.add(voName);
437
+ });
439
438
  });
440
439
  }
441
440
  // Check include for child entities
@@ -4,6 +4,8 @@ export declare class StoreGenerator {
4
4
  private availableAggregates;
5
5
  private isAggregateField;
6
6
  private isValueObjectType;
7
+ private isArrayVoType;
8
+ private isUnionVoType;
7
9
  private getValueObjectName;
8
10
  private getValueObjectConfig;
9
11
  /**
@@ -21,8 +23,20 @@ export declare class StoreGenerator {
21
23
  private mapTypeToRowType;
22
24
  /** Single line for serializing a value object field to DB (insert/update). */
23
25
  private generateValueObjectSerialization;
26
+ /** Serialization for an array-of-VOs field. */
27
+ private generateArrayVoSerialization;
28
+ /** Serialization for a union-of-VOs field. Uses _type discriminator. */
29
+ private generateUnionVoSerialization;
24
30
  /** Single line for deserializing a value object from row (rowToModel). */
25
31
  private generateValueObjectDeserialization;
32
+ /** Deserialization for an array-of-VOs field. */
33
+ private generateArrayVoDeserialization;
34
+ /** Deserialization for a union-of-VOs field. Uses _type discriminator. */
35
+ private generateUnionVoDeserialization;
36
+ /** Serialization for an array-of-union-VOs field. Each element tagged with _type discriminator. */
37
+ private generateArrayUnionVoSerialization;
38
+ /** Deserialization for an array-of-union-VOs field. Each element reconstructed via _type. */
39
+ private generateArrayUnionVoDeserialization;
26
40
  /** Single line for datetime conversion: toDate (row->model) or toMySQL (entity->row). */
27
41
  private generateDatetimeConversion;
28
42
  private replaceTemplateVars;
@@ -52,8 +52,16 @@ class StoreGenerator {
52
52
  return (0, typeUtils_1.isAggregateReference)(fieldConfig.type, this.availableAggregates);
53
53
  }
54
54
  isValueObjectType(fieldType) {
55
- const capitalizedType = (0, typeUtils_1.capitalize)(fieldType);
56
- return this.availableValueObjects.has(capitalizedType);
55
+ const { baseTypes } = (0, typeUtils_1.parseFieldType)(fieldType);
56
+ return baseTypes.some(bt => this.availableValueObjects.has((0, typeUtils_1.capitalize)(bt)));
57
+ }
58
+ isArrayVoType(fieldType) {
59
+ const parsed = (0, typeUtils_1.parseFieldType)(fieldType);
60
+ return parsed.isArray && parsed.baseTypes.some(bt => this.availableValueObjects.has((0, typeUtils_1.capitalize)(bt)));
61
+ }
62
+ isUnionVoType(fieldType) {
63
+ const parsed = (0, typeUtils_1.parseFieldType)(fieldType);
64
+ return parsed.isUnion && parsed.baseTypes.some(bt => this.availableValueObjects.has((0, typeUtils_1.capitalize)(bt)));
57
65
  }
58
66
  getValueObjectName(fieldType) {
59
67
  return (0, typeUtils_1.capitalize)(fieldType);
@@ -105,6 +113,18 @@ class StoreGenerator {
105
113
  }
106
114
  return ` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}) : undefined`;
107
115
  }
116
+ /** Serialization for an array-of-VOs field. */
117
+ generateArrayVoSerialization(fieldName) {
118
+ return ` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}) : undefined`;
119
+ }
120
+ /** Serialization for a union-of-VOs field. Uses _type discriminator. */
121
+ generateUnionVoSerialization(fieldName, unionVoNames) {
122
+ const checks = unionVoNames
123
+ .map(voName => `entity.${fieldName} instanceof ${voName} ? '${voName}'`)
124
+ .join(' : ');
125
+ const discriminator = `${checks} : 'unknown'`;
126
+ return ` ${fieldName}: entity.${fieldName} ? JSON.stringify({ _type: ${discriminator}, ...entity.${fieldName} }) : undefined`;
127
+ }
108
128
  /** Single line for deserializing a value object from row (rowToModel). */
109
129
  generateValueObjectDeserialization(fieldName, voName, voConfig) {
110
130
  const voFields = Object.keys(voConfig.fields);
@@ -123,6 +143,46 @@ class StoreGenerator {
123
143
  }
124
144
  return ` row.${fieldName} ? new ${voName}(...Object.values(JSON.parse(row.${fieldName}))) : undefined`;
125
145
  }
146
+ /** Deserialization for an array-of-VOs field. */
147
+ generateArrayVoDeserialization(fieldName, voName, voConfig) {
148
+ const voFields = Object.keys(voConfig.fields);
149
+ const itemArgs = voFields.length > 0
150
+ ? voFields.map(f => `item.${f}`).join(', ')
151
+ : '...Object.values(item)';
152
+ return ` row.${fieldName} ? (JSON.parse(row.${fieldName}) as any[]).map((item: any) => new ${voName}(${itemArgs})) : []`;
153
+ }
154
+ /** Deserialization for a union-of-VOs field. Uses _type discriminator. */
155
+ generateUnionVoDeserialization(fieldName, unionVoNames, unionVoConfigs) {
156
+ const cases = unionVoNames.map(voName => {
157
+ const cfg = unionVoConfigs[voName];
158
+ const voFields = cfg ? Object.keys(cfg.fields) : [];
159
+ const args = voFields.length > 0
160
+ ? voFields.map(f => `parsed.${f}`).join(', ')
161
+ : '...Object.values(parsed)';
162
+ return `if (parsed._type === '${voName}') return new ${voName}(${args});`;
163
+ }).join(' ');
164
+ return ` row.${fieldName} ? (() => { const parsed = JSON.parse(row.${fieldName}); ${cases} return undefined; })() : undefined`;
165
+ }
166
+ /** Serialization for an array-of-union-VOs field. Each element tagged with _type discriminator. */
167
+ generateArrayUnionVoSerialization(fieldName, unionVoNames) {
168
+ const checks = unionVoNames
169
+ .map(voName => `item instanceof ${voName} ? '${voName}'`)
170
+ .join(' : ');
171
+ const discriminator = `${checks} : 'unknown'`;
172
+ return ` ${fieldName}: entity.${fieldName} ? JSON.stringify(entity.${fieldName}.map((item: any) => ({ _type: ${discriminator}, ...item }))) : undefined`;
173
+ }
174
+ /** Deserialization for an array-of-union-VOs field. Each element reconstructed via _type. */
175
+ generateArrayUnionVoDeserialization(fieldName, unionVoNames, unionVoConfigs) {
176
+ const cases = unionVoNames.map(voName => {
177
+ const cfg = unionVoConfigs[voName];
178
+ const voFields = cfg ? Object.keys(cfg.fields) : [];
179
+ const args = voFields.length > 0
180
+ ? voFields.map(f => `item.${f}`).join(', ')
181
+ : '...Object.values(item)';
182
+ return `if (item._type === '${voName}') return new ${voName}(${args});`;
183
+ }).join(' ');
184
+ return ` row.${fieldName} ? (JSON.parse(row.${fieldName}) as any[]).map((item: any) => { ${cases} return undefined; }) : []`;
185
+ }
126
186
  /** Single line for datetime conversion: toDate (row->model) or toMySQL (entity->row). */
127
187
  generateDatetimeConversion(fieldName, direction) {
128
188
  if (direction === 'toDate') {
@@ -186,8 +246,37 @@ class StoreGenerator {
186
246
  result.push(` Boolean(row.${fieldName})`);
187
247
  return;
188
248
  }
189
- // Handle value object conversion - deserialize from JSON
249
+ // Handle value object conversion - deserialize from JSON (simple, array, union, or array-of-union)
190
250
  if (this.isValueObjectType(fieldType)) {
251
+ const parsed = (0, typeUtils_1.parseFieldType)(fieldType);
252
+ if (parsed.isArray && parsed.isUnion) {
253
+ const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(name => this.availableValueObjects.has(name));
254
+ const unionVoConfigs = {};
255
+ unionVoNames.forEach(name => {
256
+ const cfg = this.availableValueObjects.get(name);
257
+ if (cfg)
258
+ unionVoConfigs[name] = cfg;
259
+ });
260
+ result.push(this.generateArrayUnionVoDeserialization(fieldName, unionVoNames, unionVoConfigs));
261
+ return;
262
+ }
263
+ if (parsed.isArray) {
264
+ const voName = (0, typeUtils_1.capitalize)(parsed.baseTypes[0]);
265
+ const voConfig = this.availableValueObjects.get(voName);
266
+ result.push(this.generateArrayVoDeserialization(fieldName, voName, voConfig || { fields: {} }));
267
+ return;
268
+ }
269
+ if (parsed.isUnion) {
270
+ const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(name => this.availableValueObjects.has(name));
271
+ const unionVoConfigs = {};
272
+ unionVoNames.forEach(name => {
273
+ const cfg = this.availableValueObjects.get(name);
274
+ if (cfg)
275
+ unionVoConfigs[name] = cfg;
276
+ });
277
+ result.push(this.generateUnionVoDeserialization(fieldName, unionVoNames, unionVoConfigs));
278
+ return;
279
+ }
191
280
  const voName = this.getValueObjectName(fieldType);
192
281
  const voConfig = this.getValueObjectConfig(fieldType);
193
282
  if (voConfig) {
@@ -218,8 +307,23 @@ class StoreGenerator {
218
307
  result.push(this.generateDatetimeConversion(fieldName, 'toMySQL'));
219
308
  return;
220
309
  }
221
- // Handle value object - serialize to JSON
310
+ // Handle value object - serialize to JSON (simple, array, union, or array-of-union)
222
311
  if (this.isValueObjectType(fieldType)) {
312
+ const parsed = (0, typeUtils_1.parseFieldType)(fieldType);
313
+ if (parsed.isArray && parsed.isUnion) {
314
+ const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(name => this.availableValueObjects.has(name));
315
+ result.push(this.generateArrayUnionVoSerialization(fieldName, unionVoNames));
316
+ return;
317
+ }
318
+ if (parsed.isArray) {
319
+ result.push(this.generateArrayVoSerialization(fieldName));
320
+ return;
321
+ }
322
+ if (parsed.isUnion) {
323
+ const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(name => this.availableValueObjects.has(name));
324
+ result.push(this.generateUnionVoSerialization(fieldName, unionVoNames));
325
+ return;
326
+ }
223
327
  const voConfig = this.getValueObjectConfig(fieldType);
224
328
  if (voConfig) {
225
329
  result.push(this.generateValueObjectSerialization(fieldName, this.getValueObjectName(fieldType), voConfig));
@@ -244,6 +348,18 @@ class StoreGenerator {
244
348
  return this.generateDatetimeConversion(fieldName, 'toMySQL');
245
349
  }
246
350
  if (this.isValueObjectType(fieldType)) {
351
+ const parsed = (0, typeUtils_1.parseFieldType)(fieldType);
352
+ if (parsed.isArray && parsed.isUnion) {
353
+ const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(name => this.availableValueObjects.has(name));
354
+ return this.generateArrayUnionVoSerialization(fieldName, unionVoNames);
355
+ }
356
+ if (parsed.isArray) {
357
+ return this.generateArrayVoSerialization(fieldName);
358
+ }
359
+ if (parsed.isUnion) {
360
+ const unionVoNames = parsed.baseTypes.map(bt => (0, typeUtils_1.capitalize)(bt)).filter(name => this.availableValueObjects.has(name));
361
+ return this.generateUnionVoSerialization(fieldName, unionVoNames);
362
+ }
247
363
  const voConfig = this.getValueObjectConfig(fieldType);
248
364
  if (voConfig) {
249
365
  return this.generateValueObjectSerialization(fieldName, this.getValueObjectName(fieldType), voConfig);
@@ -260,26 +376,25 @@ class StoreGenerator {
260
376
  generateValueObjectImports(fields) {
261
377
  const imports = [];
262
378
  fields.forEach(([, fieldConfig]) => {
263
- if (this.isValueObjectType(fieldConfig.type)) {
264
- const voName = this.getValueObjectName(fieldConfig.type);
265
- const voConfig = this.getValueObjectConfig(fieldConfig.type);
266
- // Import the class
379
+ if (!this.isValueObjectType(fieldConfig.type))
380
+ return;
381
+ // Collect all VO names referenced in this field type (handles Foo[], Foo | Bar)
382
+ const referencedVoNames = (0, typeUtils_1.getReferencedValueObjects)(fieldConfig.type, this.availableValueObjects);
383
+ referencedVoNames.forEach(voName => {
384
+ const voConfig = this.availableValueObjects.get(voName);
267
385
  const importItems = [voName];
268
- // Also import the type if it's an enum
269
- if (voConfig) {
386
+ // Also import enum type alias if present (only for simple single-VO fields)
387
+ if (voConfig && !(0, typeUtils_1.parseFieldType)(fieldConfig.type).isArray && !(0, typeUtils_1.parseFieldType)(fieldConfig.type).isUnion) {
270
388
  const enumTypeName = this.getValueObjectFieldTypeName(voName, voConfig);
271
- if (enumTypeName) {
389
+ if (enumTypeName)
272
390
  importItems.push(enumTypeName);
273
- }
274
391
  }
275
392
  imports.push(`import { ${importItems.join(', ')} } from '../../domain/valueObjects/${voName}';`);
276
- }
393
+ });
277
394
  });
278
- // Dedupe imports
279
395
  const uniqueImports = [...new Set(imports)];
280
- if (uniqueImports.length === 0) {
396
+ if (uniqueImports.length === 0)
281
397
  return '';
282
- }
283
398
  return '\n' + uniqueImports.join('\n');
284
399
  }
285
400
  generateAggregateRefImports(modelName, fields) {
@@ -19,6 +19,21 @@ export declare class TemplateGenerator {
19
19
  private renderEditTemplate;
20
20
  private getInputType;
21
21
  private renderValueObjectField;
22
+ /**
23
+ * Render an array-of-VOs field as checkboxes.
24
+ * If the VO has a single enum field: one checkbox per enum value.
25
+ * If the VO has multiple / non-enum fields: one labeled checkbox group per VO subfield.
26
+ */
27
+ private renderArrayVoField;
28
+ /**
29
+ * Render a union-of-VOs field as a type selector with sub-fields for each VO type.
30
+ */
31
+ private renderUnionVoField;
32
+ /**
33
+ * Render an array-of-union-VOs field as a repeatable group where each item
34
+ * has a type selector and conditionally-shown sub-fields per VO type.
35
+ */
36
+ private renderArrayUnionVoField;
22
37
  private renderFormField;
23
38
  private getEnumValuesMap;
24
39
  generateFromConfig(config: ModuleConfig): Record<string, string>;
@@ -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);
@@ -21,7 +21,7 @@ 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
25
  export declare function getTableName(aggregateName: string): string;
26
26
  export declare function getForeignKeyFieldName(fieldName: string): string;
27
27
  export declare function isRelationshipField(fieldType: string, availableAggregates: Set<string>): boolean;
@@ -52,6 +52,7 @@ exports.getMigrationFileName = getMigrationFileName;
52
52
  const fs = __importStar(require("fs"));
53
53
  const path = __importStar(require("path"));
54
54
  const yaml_1 = require("yaml");
55
+ const typeUtils_1 = require("./typeUtils");
55
56
  const TYPE_MAPPING = {
56
57
  string: 'VARCHAR(255)',
57
58
  number: 'INT',
@@ -61,10 +62,20 @@ const TYPE_MAPPING = {
61
62
  array: 'JSON',
62
63
  object: 'JSON'
63
64
  };
64
- function mapYamlTypeToSql(yamlType, availableAggregates) {
65
+ function mapYamlTypeToSql(yamlType, availableAggregates, availableValueObjects) {
66
+ // Simple aggregate reference → foreign key INT
65
67
  if (availableAggregates.has(yamlType)) {
66
68
  return 'INT';
67
69
  }
70
+ // Compound types: array ("Foo[]") or union ("Foo | Bar") → JSON column
71
+ const parsed = (0, typeUtils_1.parseFieldType)(yamlType);
72
+ if (parsed.isArray || parsed.isUnion) {
73
+ return 'JSON';
74
+ }
75
+ // Named value object → stored as JSON
76
+ if (availableValueObjects && availableValueObjects.has(yamlType)) {
77
+ return 'JSON';
78
+ }
68
79
  return TYPE_MAPPING[yamlType] || 'VARCHAR(255)';
69
80
  }
70
81
  function getTableName(aggregateName) {
@@ -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;
@@ -5,6 +5,9 @@
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ROW_TYPE_MAPPING = exports.TYPE_MAPPING = void 0;
7
7
  exports.capitalize = capitalize;
8
+ exports.parseFieldType = parseFieldType;
9
+ exports.isValueObjectFieldType = isValueObjectFieldType;
10
+ exports.getReferencedValueObjects = getReferencedValueObjects;
8
11
  exports.mapType = mapType;
9
12
  exports.isAggregateReference = isAggregateReference;
10
13
  exports.mapRowType = mapRowType;
@@ -41,20 +44,111 @@ exports.ROW_TYPE_MAPPING = {
41
44
  function capitalize(str) {
42
45
  return str.charAt(0).toUpperCase() + str.slice(1);
43
46
  }
47
+ /**
48
+ * Parse a YAML field type string into its structural components.
49
+ * Handles simple types ("Money"), array types ("Money[]"), union types ("Foo | Bar"),
50
+ * and array-of-union types ("(Foo | Bar)[]").
51
+ */
52
+ function parseFieldType(typeStr) {
53
+ const trimmed = typeStr.trim();
54
+ // Array of union: "(Foo | Bar)[]"
55
+ if (trimmed.startsWith('(') && trimmed.endsWith(')[]')) {
56
+ const inner = trimmed.slice(1, -3).trim();
57
+ const parts = inner.split('|').map(p => p.trim()).filter(Boolean);
58
+ return { baseTypes: parts, isArray: true, isUnion: true };
59
+ }
60
+ if (trimmed.endsWith('[]')) {
61
+ const base = trimmed.slice(0, -2).trim();
62
+ return { baseTypes: [base], isArray: true, isUnion: false };
63
+ }
64
+ if (trimmed.includes('|')) {
65
+ const parts = trimmed.split('|').map(p => p.trim()).filter(Boolean);
66
+ return { baseTypes: parts, isArray: false, isUnion: true };
67
+ }
68
+ return { baseTypes: [trimmed], isArray: false, isUnion: false };
69
+ }
70
+ /**
71
+ * Returns true when any base type in the (possibly compound) type expression is a known value object.
72
+ */
73
+ function isValueObjectFieldType(typeStr, valueObjects) {
74
+ const { baseTypes } = parseFieldType(typeStr);
75
+ return baseTypes.some(bt => {
76
+ const cap = capitalize(bt);
77
+ return valueObjects instanceof Set ? valueObjects.has(cap) : valueObjects.has(cap);
78
+ });
79
+ }
80
+ /**
81
+ * Returns the set of value object names referenced in a (possibly compound) type expression.
82
+ */
83
+ function getReferencedValueObjects(typeStr, valueObjects) {
84
+ const { baseTypes } = parseFieldType(typeStr);
85
+ const result = new Set();
86
+ for (const bt of baseTypes) {
87
+ const cap = capitalize(bt);
88
+ const has = valueObjects instanceof Set ? valueObjects.has(cap) : valueObjects.has(cap);
89
+ if (has)
90
+ result.add(cap);
91
+ }
92
+ return result;
93
+ }
44
94
  /**
45
95
  * Map a YAML field type to TypeScript type, resolving aggregates and value objects by name.
96
+ * Supports compound types: "Foo[]" -> "Foo[]", "Foo | Bar" -> "Foo | Bar".
46
97
  */
47
98
  function mapType(yamlType, aggregates, valueObjects) {
48
- var _a;
99
+ var _a, _b;
100
+ // Simple aggregate reference (no compound syntax)
49
101
  if (aggregates === null || aggregates === void 0 ? void 0 : aggregates.has(yamlType))
50
102
  return yamlType;
103
+ const parsed = parseFieldType(yamlType);
104
+ // Array of union of value objects: "(Foo | Bar)[]"
105
+ if (parsed.isArray && parsed.isUnion) {
106
+ const resolvedParts = parsed.baseTypes.map(bt => {
107
+ var _a;
108
+ const cap = capitalize(bt);
109
+ if (valueObjects) {
110
+ const has = valueObjects instanceof Set ? valueObjects.has(cap) : valueObjects.has(cap);
111
+ if (has)
112
+ return cap;
113
+ }
114
+ return (_a = exports.TYPE_MAPPING[bt]) !== null && _a !== void 0 ? _a : 'any';
115
+ });
116
+ return `(${resolvedParts.join(' | ')})[]`;
117
+ }
118
+ // Array of value objects: "Foo[]"
119
+ if (parsed.isArray) {
120
+ const [base] = parsed.baseTypes;
121
+ const capitalizedBase = capitalize(base);
122
+ if (valueObjects) {
123
+ const has = valueObjects instanceof Set ? valueObjects.has(capitalizedBase) : valueObjects.has(capitalizedBase);
124
+ if (has)
125
+ return `${capitalizedBase}[]`;
126
+ }
127
+ // Fall back: treat as plain mapped type array
128
+ return `${(_a = exports.TYPE_MAPPING[base]) !== null && _a !== void 0 ? _a : 'any'}[]`;
129
+ }
130
+ // Union of value objects: "Foo | Bar"
131
+ if (parsed.isUnion) {
132
+ const resolvedParts = parsed.baseTypes.map(bt => {
133
+ var _a;
134
+ const cap = capitalize(bt);
135
+ if (valueObjects) {
136
+ const has = valueObjects instanceof Set ? valueObjects.has(cap) : valueObjects.has(cap);
137
+ if (has)
138
+ return cap;
139
+ }
140
+ return (_a = exports.TYPE_MAPPING[bt]) !== null && _a !== void 0 ? _a : 'any';
141
+ });
142
+ return resolvedParts.join(' | ');
143
+ }
144
+ // Simple type
51
145
  const capitalizedType = capitalize(yamlType);
52
146
  if (valueObjects) {
53
147
  const has = valueObjects instanceof Set ? valueObjects.has(capitalizedType) : valueObjects.has(capitalizedType);
54
148
  if (has)
55
149
  return capitalizedType;
56
150
  }
57
- return (_a = exports.TYPE_MAPPING[yamlType]) !== null && _a !== void 0 ? _a : 'any';
151
+ return (_b = exports.TYPE_MAPPING[yamlType]) !== null && _b !== void 0 ? _b : 'any';
58
152
  }
59
153
  /**
60
154
  * Check if a YAML field type references another aggregate entity.
@@ -63,14 +157,14 @@ function isAggregateReference(yamlType, aggregates) {
63
157
  return !!(aggregates === null || aggregates === void 0 ? void 0 : aggregates.has(yamlType));
64
158
  }
65
159
  /**
66
- * Map a YAML type to the store row TypeScript type (value objects become string).
160
+ * Map a YAML type to the store row TypeScript type.
161
+ * Value objects (including compound types) become "string" (stored as JSON).
67
162
  */
68
163
  function mapRowType(yamlType, valueObjects) {
69
164
  var _a;
70
165
  if (valueObjects) {
71
- const capitalizedType = capitalize(yamlType);
72
- const has = valueObjects instanceof Set ? valueObjects.has(capitalizedType) : valueObjects.has(capitalizedType);
73
- if (has)
166
+ // Any compound type containing a VO name is stored as JSON string
167
+ if (isValueObjectFieldType(yamlType, valueObjects))
74
168
  return 'string';
75
169
  }
76
170
  return (_a = exports.ROW_TYPE_MAPPING[yamlType]) !== null && _a !== void 0 ? _a : 'string';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@currentjs/gen",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "CLI code generator",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "Konstantin Zavalny",