@currentjs/gen 0.5.4 → 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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.5] - 2026-04-07
4
+
5
+ - data access layer generation fixes and improvements
6
+
3
7
  ## [0.5.4] - 2026-04-07
4
8
 
5
9
  - Array and union value objects
@@ -42,11 +42,12 @@ const cliUtils_1 = require("../utils/cliUtils");
42
42
  const commandUtils_1 = require("../utils/commandUtils");
43
43
  const configTypes_1 = require("../types/configTypes");
44
44
  const migrationUtils_1 = require("../utils/migrationUtils");
45
- function collectAggregatesFromModules(appYamlPath) {
45
+ function collectSchemaFromModules(appYamlPath) {
46
46
  const appConfig = (0, commandUtils_1.loadAppConfig)(appYamlPath);
47
47
  const moduleEntries = (0, commandUtils_1.getModuleEntries)(appConfig);
48
48
  const projectRoot = path.dirname(appYamlPath);
49
49
  const allAggregates = {};
50
+ const allValueObjects = new Set();
50
51
  const sources = [];
51
52
  for (const entry of moduleEntries) {
52
53
  const moduleYamlPath = path.isAbsolute(entry.path)
@@ -67,13 +68,18 @@ function collectAggregatesFromModules(appYamlPath) {
67
68
  const aggregates = moduleConfig.domain.aggregates;
68
69
  const count = Object.keys(aggregates).length;
69
70
  Object.assign(allAggregates, aggregates);
71
+ if (moduleConfig.domain.valueObjects) {
72
+ for (const voName of Object.keys(moduleConfig.domain.valueObjects)) {
73
+ allValueObjects.add(voName);
74
+ }
75
+ }
70
76
  sources.push(`${entry.name} (${count} aggregate(s))`);
71
77
  }
72
78
  if (sources.length > 0) {
73
79
  // eslint-disable-next-line no-console
74
80
  console.log(colors_1.colors.gray(` Sources: ${sources.join(', ')}`));
75
81
  }
76
- return allAggregates;
82
+ return { aggregates: allAggregates, valueObjects: allValueObjects };
77
83
  }
78
84
  function handleMigrateCommit(yamlPath) {
79
85
  try {
@@ -94,7 +100,7 @@ function handleMigrateCommit(yamlPath) {
94
100
  }
95
101
  // eslint-disable-next-line no-console
96
102
  console.log(colors_1.colors.cyan('\nšŸ“‹ Collecting aggregates from all modules...'));
97
- const currentAggregates = collectAggregatesFromModules(resolvedYamlPath);
103
+ const { aggregates: currentAggregates, valueObjects: currentValueObjects } = collectSchemaFromModules(resolvedYamlPath);
98
104
  if (Object.keys(currentAggregates).length === 0) {
99
105
  // eslint-disable-next-line no-console
100
106
  console.log(colors_1.colors.yellow('āš ļø No aggregates found in module configuration.'));
@@ -113,7 +119,7 @@ function handleMigrateCommit(yamlPath) {
113
119
  }
114
120
  // eslint-disable-next-line no-console
115
121
  console.log(colors_1.colors.cyan('\nšŸ” Comparing schemas...'));
116
- const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates);
122
+ const sqlStatements = (0, migrationUtils_1.compareSchemas)(oldState, currentAggregates, currentValueObjects);
117
123
  if (sqlStatements.length === 0 || sqlStatements.every(s => s.trim() === '' || s.startsWith('--'))) {
118
124
  // eslint-disable-next-line no-console
119
125
  console.log(colors_1.colors.yellow('āš ļø No changes detected. Schema is up to date.'));
@@ -132,7 +138,7 @@ function handleMigrateCommit(yamlPath) {
132
138
  const newState = {
133
139
  aggregates: currentAggregates,
134
140
  version: timestamp,
135
- timestamp: new Date().toISOString()
141
+ timestamp: new Date().toISOString(),
136
142
  };
137
143
  (0, migrationUtils_1.saveSchemaState)(stateFilePath, newState);
138
144
  // eslint-disable-next-line no-console
@@ -130,7 +130,7 @@ class StoreGenerator {
130
130
  const voFields = Object.keys(voConfig.fields);
131
131
  if (voFields.length > 1) {
132
132
  const voArgs = voFields.map(f => `parsed.${f}`).join(', ');
133
- return ` row.${fieldName} ? (() => { const parsed = JSON.parse(row.${fieldName}); return new ${voName}(${voArgs}); })() : undefined`;
133
+ return ` row.${fieldName} ? (() => { const parsed = this.ensureParsed(row.${fieldName}); return new ${voName}(${voArgs}); })() : undefined`;
134
134
  }
135
135
  if (voFields.length === 1) {
136
136
  const singleFieldType = voConfig.fields[voFields[0]];
@@ -141,7 +141,7 @@ class StoreGenerator {
141
141
  }
142
142
  return ` row.${fieldName} ? new ${voName}(row.${fieldName}) : undefined`;
143
143
  }
144
- return ` row.${fieldName} ? new ${voName}(...Object.values(JSON.parse(row.${fieldName}))) : undefined`;
144
+ return ` row.${fieldName} ? new ${voName}(...Object.values(this.ensureParsed(row.${fieldName}))) : undefined`;
145
145
  }
146
146
  /** Deserialization for an array-of-VOs field. */
147
147
  generateArrayVoDeserialization(fieldName, voName, voConfig) {
@@ -149,7 +149,7 @@ class StoreGenerator {
149
149
  const itemArgs = voFields.length > 0
150
150
  ? voFields.map(f => `item.${f}`).join(', ')
151
151
  : '...Object.values(item)';
152
- return ` row.${fieldName} ? (JSON.parse(row.${fieldName}) as any[]).map((item: any) => new ${voName}(${itemArgs})) : []`;
152
+ return ` row.${fieldName} ? (this.ensureParsed(row.${fieldName}) as any[]).map((item: any) => new ${voName}(${itemArgs})) : []`;
153
153
  }
154
154
  /** Deserialization for a union-of-VOs field. Uses _type discriminator. */
155
155
  generateUnionVoDeserialization(fieldName, unionVoNames, unionVoConfigs) {
@@ -161,7 +161,7 @@ class StoreGenerator {
161
161
  : '...Object.values(parsed)';
162
162
  return `if (parsed._type === '${voName}') return new ${voName}(${args});`;
163
163
  }).join(' ');
164
- return ` row.${fieldName} ? (() => { const parsed = JSON.parse(row.${fieldName}); ${cases} return undefined; })() : undefined`;
164
+ return ` row.${fieldName} ? (() => { const parsed = this.ensureParsed(row.${fieldName}); ${cases} return undefined; })() : undefined`;
165
165
  }
166
166
  /** Serialization for an array-of-union-VOs field. Each element tagged with _type discriminator. */
167
167
  generateArrayUnionVoSerialization(fieldName, unionVoNames) {
@@ -181,7 +181,7 @@ class StoreGenerator {
181
181
  : '...Object.values(item)';
182
182
  return `if (item._type === '${voName}') return new ${voName}(${args});`;
183
183
  }).join(' ');
184
- return ` row.${fieldName} ? (JSON.parse(row.${fieldName}) as any[]).map((item: any) => { ${cases} return undefined; }) : []`;
184
+ return ` row.${fieldName} ? (this.ensureParsed(row.${fieldName}) as any[]).map((item: any) => { ${cases} return undefined; }) : []`;
185
185
  }
186
186
  /** Single line for datetime conversion: toDate (row->model) or toMySQL (entity->row). */
187
187
  generateDatetimeConversion(fieldName, direction) {
@@ -204,7 +204,7 @@ class StoreGenerator {
204
204
  result.push(` ${ownerOrParentField}: number;`);
205
205
  fields.forEach(([fieldName, fieldConfig]) => {
206
206
  if (this.isAggregateField(fieldConfig)) {
207
- result.push(` ${fieldName}_id?: number;`);
207
+ result.push(` ${fieldName}Id?: number;`);
208
208
  return;
209
209
  }
210
210
  const tsType = this.mapTypeToRowType(fieldConfig.type);
@@ -216,7 +216,7 @@ class StoreGenerator {
216
216
  generateFieldNamesStr(fields, childInfo) {
217
217
  const fieldNames = ['id'];
218
218
  fieldNames.push(childInfo ? childInfo.parentIdField : 'ownerId');
219
- fieldNames.push(...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name));
219
+ fieldNames.push(...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}Id` : name));
220
220
  return fieldNames.map(f => `\\\`${f}\\\``).join(', ');
221
221
  }
222
222
  generateRowToModelMapping(modelName, fields, childInfo) {
@@ -233,7 +233,7 @@ class StoreGenerator {
233
233
  }
234
234
  // Handle aggregate reference - create stub from FK
235
235
  if (this.isAggregateField(fieldConfig)) {
236
- result.push(` row.${fieldName}_id != null ? ({ id: row.${fieldName}_id } as unknown as ${fieldType}) : undefined`);
236
+ result.push(` row.${fieldName}Id != null ? ({ id: row.${fieldName}Id } as unknown as ${fieldType}) : undefined`);
237
237
  return;
238
238
  }
239
239
  // Handle datetime/date conversion
@@ -283,7 +283,7 @@ class StoreGenerator {
283
283
  result.push(this.generateValueObjectDeserialization(fieldName, voName, voConfig));
284
284
  }
285
285
  else {
286
- result.push(` row.${fieldName} ? new ${voName}(...Object.values(JSON.parse(row.${fieldName}))) : undefined`);
286
+ result.push(` row.${fieldName} ? new ${voName}(...Object.values(this.ensureParsed(row.${fieldName}))) : undefined`);
287
287
  }
288
288
  return;
289
289
  }
@@ -299,7 +299,7 @@ class StoreGenerator {
299
299
  const fieldType = fieldConfig.type;
300
300
  // Handle aggregate reference - extract FK id
301
301
  if (this.isAggregateField(fieldConfig)) {
302
- result.push(` ${fieldName}_id: entity.${fieldName}?.id`);
302
+ result.push(` ${fieldName}Id: entity.${fieldName}?.id`);
303
303
  return;
304
304
  }
305
305
  // Handle datetime/date - convert Date to MySQL DATETIME format
@@ -342,7 +342,7 @@ class StoreGenerator {
342
342
  .map(([fieldName, fieldConfig]) => {
343
343
  const fieldType = fieldConfig.type;
344
344
  if (this.isAggregateField(fieldConfig)) {
345
- return ` ${fieldName}_id: entity.${fieldName}?.id`;
345
+ return ` ${fieldName}Id: entity.${fieldName}?.id`;
346
346
  }
347
347
  if (fieldType === 'datetime' || fieldType === 'date') {
348
348
  return this.generateDatetimeConversion(fieldName, 'toMySQL');
@@ -371,7 +371,7 @@ class StoreGenerator {
371
371
  .join(',\n');
372
372
  }
373
373
  generateUpdateFieldsArray(fields) {
374
- return JSON.stringify(fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name));
374
+ return JSON.stringify(fields.map(([name, config]) => this.isAggregateField(config) ? `${name}Id` : name));
375
375
  }
376
376
  generateValueObjectImports(fields) {
377
377
  const imports = [];
@@ -423,7 +423,7 @@ class StoreGenerator {
423
423
  const offset = (page - 1) * limit;${ownerFilter}
424
424
  const params: Record<string, any> = { limit: String(limit), offset: String(offset) };${ownerParamsSetup}
425
425
  const result = await this.db.query(
426
- \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef} LIMIT :limit OFFSET :offset\`,
426
+ \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deletedAt IS NULL${ownerFilterRef} LIMIT :limit OFFSET :offset\`,
427
427
  params
428
428
  );
429
429
 
@@ -435,7 +435,7 @@ class StoreGenerator {
435
435
  const getAll = ` async getAll(${isRoot ? 'ownerId?: number' : ''}): Promise<${modelName}[]> {${ownerFilter}
436
436
  const params: Record<string, any> = {};${ownerParamsSetup}
437
437
  const result = await this.db.query(
438
- \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef}\`,
438
+ \`SELECT ${fieldNamesStr} FROM \\\`\${this.tableName}\\\` WHERE deletedAt IS NULL${ownerFilterRef}\`,
439
439
  params
440
440
  );
441
441
 
@@ -447,7 +447,7 @@ class StoreGenerator {
447
447
  const count = ` async count(${isRoot ? 'ownerId?: number' : ''}): Promise<number> {${ownerFilter}
448
448
  const params: Record<string, any> = {};${ownerParamsSetup}
449
449
  const result = await this.db.query(
450
- \`SELECT COUNT(*) as count FROM \\\`\${this.tableName}\\\` WHERE deleted_at IS NULL${ownerFilterRef}\`,
450
+ \`SELECT COUNT(*) as count FROM \\\`\${this.tableName}\\\` WHERE deletedAt IS NULL${ownerFilterRef}\`,
451
451
  params
452
452
  );
453
453
 
@@ -461,13 +461,13 @@ class StoreGenerator {
461
461
  generateGetByParentIdMethod(modelName, fields, childInfo) {
462
462
  if (!childInfo)
463
463
  return '';
464
- const fieldList = ['id', childInfo.parentIdField, ...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}_id` : name)].map(f => '\\`' + f + '\\`').join(', ');
464
+ const fieldList = ['id', childInfo.parentIdField, ...fields.map(([name, config]) => this.isAggregateField(config) ? `${name}Id` : name)].map(f => '\\`' + f + '\\`').join(', ');
465
465
  const parentIdField = childInfo.parentIdField;
466
466
  return `
467
467
 
468
468
  async getByParentId(parentId: number): Promise<${modelName}[]> {
469
469
  const result = await this.db.query(
470
- \`SELECT ${fieldList} FROM \\\`\${this.tableName}\\\` WHERE \\\`${parentIdField}\\\` = :parentId AND deleted_at IS NULL\`,
470
+ \`SELECT ${fieldList} FROM \\\`\${this.tableName}\\\` WHERE \\\`${parentIdField}\\\` = :parentId AND deletedAt IS NULL\`,
471
471
  { parentId }
472
472
  );
473
473
 
@@ -489,7 +489,7 @@ class StoreGenerator {
489
489
  */
490
490
  async getResourceOwner(id: number): Promise<number | null> {
491
491
  const result = await this.db.query(
492
- \`SELECT p.ownerId FROM \\\`\${this.tableName}\\\` c INNER JOIN \\\`${parentTable}\\\` p ON p.id = c.\\\`${parentIdField}\\\` WHERE c.id = :id AND c.deleted_at IS NULL\`,
492
+ \`SELECT p.ownerId FROM \\\`\${this.tableName}\\\` c INNER JOIN \\\`${parentTable}\\\` p ON p.id = c.\\\`${parentIdField}\\\` WHERE c.id = :id AND c.deletedAt IS NULL\`,
493
493
  { id }
494
494
  );
495
495
 
@@ -507,7 +507,7 @@ class StoreGenerator {
507
507
  */
508
508
  async getResourceOwner(id: number): Promise<number | null> {
509
509
  const result = await this.db.query(
510
- \`SELECT ownerId FROM \\\`\${this.tableName}\\\` WHERE id = :id AND deleted_at IS NULL\`,
510
+ \`SELECT ownerId FROM \\\`\${this.tableName}\\\` WHERE id = :id AND deletedAt IS NULL\`,
511
511
  { id }
512
512
  );
513
513
 
@@ -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;
@@ -22,18 +22,24 @@ export interface ForeignKeyInfo {
22
22
  REFERENCED_COLUMN_NAME: string;
23
23
  }
24
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;
@@ -56,8 +57,12 @@ const typeUtils_1 = require("./typeUtils");
56
57
  const TYPE_MAPPING = {
57
58
  string: 'VARCHAR(255)',
58
59
  number: 'INT',
60
+ integer: 'INT',
61
+ decimal: 'DECIMAL(10,2)',
59
62
  boolean: 'TINYINT(1)',
60
63
  datetime: 'DATETIME',
64
+ date: 'DATETIME',
65
+ id: 'INT',
61
66
  json: 'JSON',
62
67
  array: 'JSON',
63
68
  object: 'JSON'
@@ -78,8 +83,9 @@ function mapYamlTypeToSql(yamlType, availableAggregates, availableValueObjects)
78
83
  }
79
84
  return TYPE_MAPPING[yamlType] || 'VARCHAR(255)';
80
85
  }
86
+ /** Table name matches the store convention: singular lowercase aggregate name. */
81
87
  function getTableName(aggregateName) {
82
- return aggregateName.toLowerCase() + 's';
88
+ return aggregateName.toLowerCase();
83
89
  }
84
90
  function getForeignKeyFieldName(fieldName) {
85
91
  return fieldName + 'Id';
@@ -87,12 +93,34 @@ function getForeignKeyFieldName(fieldName) {
87
93
  function isRelationshipField(fieldType, availableAggregates) {
88
94
  return availableAggregates.has(fieldType);
89
95
  }
90
- 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) {
91
110
  const tableName = getTableName(name);
92
111
  const columns = [];
93
112
  const indexes = [];
94
113
  const foreignKeys = [];
95
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
+ }
96
124
  for (const [fieldName, field] of Object.entries(aggregate.fields)) {
97
125
  if (isRelationshipField(field.type, availableAggregates)) {
98
126
  const foreignKeyName = getForeignKeyFieldName(fieldName);
@@ -107,50 +135,50 @@ function generateCreateTableSQL(name, aggregate, availableAggregates) {
107
135
  ` ON UPDATE CASCADE`);
108
136
  }
109
137
  else {
110
- const sqlType = mapYamlTypeToSql(field.type, availableAggregates);
138
+ const sqlType = mapYamlTypeToSql(field.type, availableAggregates, availableValueObjects);
111
139
  const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
112
140
  columns.push(` ${fieldName} ${sqlType} ${nullable}`);
113
- if (['string', 'number'].includes(field.type)) {
141
+ if (['string', 'number', 'integer', 'id'].includes(field.type)) {
114
142
  indexes.push(` INDEX idx_${tableName}_${fieldName} (${fieldName})`);
115
143
  }
116
144
  }
117
145
  }
118
- columns.push(' created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP');
119
- columns.push(' updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
120
- columns.push(' deleted_at DATETIME NULL DEFAULT NULL');
121
- indexes.push(` INDEX idx_${tableName}_deleted_at (deleted_at)`);
122
- 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)`);
123
151
  const allParts = [...columns, ...indexes, ...foreignKeys];
124
- 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;`;
125
153
  }
126
154
  function generateDropTableSQL(tableName) {
127
- return `DROP TABLE IF EXISTS ${tableName};`;
155
+ return `DROP TABLE IF EXISTS \`${tableName}\`;`;
128
156
  }
129
- function generateAddColumnSQL(tableName, fieldName, field, availableAggregates) {
157
+ function generateAddColumnSQL(tableName, fieldName, field, availableAggregates, availableValueObjects) {
130
158
  if (isRelationshipField(field.type, availableAggregates)) {
131
159
  const foreignKeyName = getForeignKeyFieldName(fieldName);
132
160
  const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
133
- return `ALTER TABLE ${tableName} ADD COLUMN ${foreignKeyName} INT ${nullable};`;
161
+ return `ALTER TABLE \`${tableName}\` ADD COLUMN \`${foreignKeyName}\` INT ${nullable};`;
134
162
  }
135
163
  else {
136
- const sqlType = mapYamlTypeToSql(field.type, availableAggregates);
164
+ const sqlType = mapYamlTypeToSql(field.type, availableAggregates, availableValueObjects);
137
165
  const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
138
- return `ALTER TABLE ${tableName} ADD COLUMN ${fieldName} ${sqlType} ${nullable};`;
166
+ return `ALTER TABLE \`${tableName}\` ADD COLUMN \`${fieldName}\` ${sqlType} ${nullable};`;
139
167
  }
140
168
  }
141
169
  function generateDropColumnSQL(tableName, columnName) {
142
- return `ALTER TABLE ${tableName} DROP COLUMN ${columnName};`;
170
+ return `ALTER TABLE \`${tableName}\` DROP COLUMN \`${columnName}\`;`;
143
171
  }
144
- function generateModifyColumnSQL(tableName, fieldName, field, availableAggregates) {
172
+ function generateModifyColumnSQL(tableName, fieldName, field, availableAggregates, availableValueObjects) {
145
173
  if (isRelationshipField(field.type, availableAggregates)) {
146
174
  const foreignKeyName = getForeignKeyFieldName(fieldName);
147
175
  const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
148
- return `ALTER TABLE ${tableName} MODIFY COLUMN ${foreignKeyName} INT ${nullable};`;
176
+ return `ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${foreignKeyName}\` INT ${nullable};`;
149
177
  }
150
178
  else {
151
- const sqlType = mapYamlTypeToSql(field.type, availableAggregates);
179
+ const sqlType = mapYamlTypeToSql(field.type, availableAggregates, availableValueObjects);
152
180
  const nullable = field.required === false ? 'NULL DEFAULT NULL' : 'NOT NULL';
153
- return `ALTER TABLE ${tableName} MODIFY COLUMN ${fieldName} ${sqlType} ${nullable};`;
181
+ return `ALTER TABLE \`${tableName}\` MODIFY COLUMN \`${fieldName}\` ${sqlType} ${nullable};`;
154
182
  }
155
183
  }
156
184
  function loadSchemaState(stateFilePath) {
@@ -197,14 +225,18 @@ function sortAggregatesByDependencies(aggregates, availableAggregates) {
197
225
  }
198
226
  return sorted;
199
227
  }
200
- function compareSchemas(oldState, newAggregates) {
228
+ function compareSchemas(oldState, newAggregates, availableValueObjects) {
201
229
  const sqlStatements = [];
202
230
  const availableAggregates = new Set(Object.keys(newAggregates));
231
+ const childToParent = buildChildToParentMap(newAggregates);
203
232
  if (!oldState || !oldState.aggregates || Object.keys(oldState.aggregates).length === 0) {
204
233
  const sorted = sortAggregatesByDependencies(newAggregates, availableAggregates);
205
234
  for (const [name, aggregate] of sorted) {
206
- sqlStatements.push(`-- Create ${name.toLowerCase()}s table`);
207
- 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));
208
240
  sqlStatements.push('');
209
241
  }
210
242
  return sqlStatements;
@@ -223,9 +255,11 @@ function compareSchemas(oldState, newAggregates) {
223
255
  for (const [name, newAggregate] of Object.entries(newAggregates)) {
224
256
  const oldAggregate = oldAggregates[name];
225
257
  const tableName = getTableName(name);
258
+ const parentName = childToParent.get(name);
259
+ const parentIdField = parentName ? `${parentName.toLowerCase()}Id` : undefined;
226
260
  if (!oldAggregate) {
227
261
  sqlStatements.push(`-- Create ${tableName} table`);
228
- sqlStatements.push(generateCreateTableSQL(name, newAggregate, availableAggregates));
262
+ sqlStatements.push(generateCreateTableSQL(name, newAggregate, availableAggregates, availableValueObjects, parentIdField));
229
263
  sqlStatements.push('');
230
264
  }
231
265
  else {
@@ -247,7 +281,7 @@ function compareSchemas(oldState, newAggregates) {
247
281
  const oldField = oldFields[fieldName];
248
282
  if (!oldField) {
249
283
  sqlStatements.push(`-- Add column ${fieldName} to ${tableName}`);
250
- sqlStatements.push(generateAddColumnSQL(tableName, fieldName, newField, availableAggregates));
284
+ sqlStatements.push(generateAddColumnSQL(tableName, fieldName, newField, availableAggregates, availableValueObjects));
251
285
  sqlStatements.push('');
252
286
  }
253
287
  else {
@@ -255,7 +289,7 @@ function compareSchemas(oldState, newAggregates) {
255
289
  const requiredChanged = oldField.required !== newField.required;
256
290
  if (typeChanged || requiredChanged) {
257
291
  sqlStatements.push(`-- Modify column ${fieldName} in ${tableName}`);
258
- sqlStatements.push(generateModifyColumnSQL(tableName, fieldName, newField, availableAggregates));
292
+ sqlStatements.push(generateModifyColumnSQL(tableName, fieldName, newField, availableAggregates, availableValueObjects));
259
293
  sqlStatements.push('');
260
294
  }
261
295
  }
@@ -36,9 +36,9 @@ exports.ROW_TYPE_MAPPING = {
36
36
  datetime: 'string',
37
37
  date: 'string',
38
38
  id: 'number',
39
- json: 'string',
40
- array: 'string',
41
- object: 'string',
39
+ json: 'any',
40
+ array: 'any[]',
41
+ object: 'any',
42
42
  enum: 'string'
43
43
  };
44
44
  function capitalize(str) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@currentjs/gen",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "CLI code generator",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "Konstantin Zavalny",