@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 +4 -0
- package/dist/commands/migrateCommit.js +11 -5
- package/dist/generators/storeGenerator.js +19 -19
- package/dist/generators/templates/storeTemplates.js +24 -17
- package/dist/utils/migrationUtils.d.ts +10 -4
- package/dist/utils/migrationUtils.js +60 -26
- package/dist/utils/typeUtils.js +3 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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} ? (
|
|
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 =
|
|
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} ? (
|
|
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}
|
|
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}
|
|
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}
|
|
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(
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
deletedAt?: string;
|
|
11
11
|
}`,
|
|
12
12
|
storeClass: `/**
|
|
13
13
|
* Data access layer for {{ENTITY_NAME}}
|
|
@@ -22,6 +22,10 @@ export class {{ENTITY_NAME}}Store {
|
|
|
22
22
|
return date.toISOString().slice(0, 19).replace('T', ' ');
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
private ensureParsed(value: any): any {
|
|
26
|
+
return typeof value === 'string' ? JSON.parse(value) : value;
|
|
27
|
+
}
|
|
28
|
+
|
|
25
29
|
private rowToModel(row: {{ENTITY_NAME}}Row): {{ENTITY_NAME}} {
|
|
26
30
|
return new {{ENTITY_NAME}}(
|
|
27
31
|
row.id,
|
|
@@ -33,7 +37,7 @@ export class {{ENTITY_NAME}}Store {
|
|
|
33
37
|
|
|
34
38
|
async getById(id: number): Promise<{{ENTITY_NAME}} | null> {
|
|
35
39
|
const result = await this.db.query(
|
|
36
|
-
\`SELECT {{FIELD_NAMES}} FROM \\\`\${this.tableName}\\\` WHERE id = :id AND
|
|
40
|
+
\`SELECT {{FIELD_NAMES}} FROM \\\`\${this.tableName}\\\` WHERE id = :id AND deletedAt IS NULL\`,
|
|
37
41
|
{ id }
|
|
38
42
|
);
|
|
39
43
|
|
|
@@ -47,16 +51,17 @@ export class {{ENTITY_NAME}}Store {
|
|
|
47
51
|
const now = new Date();
|
|
48
52
|
const data: Partial<{{ENTITY_NAME}}Row> = {
|
|
49
53
|
{{INSERT_DATA_MAPPING}},
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
createdAt: this.toMySQLDatetime(now),
|
|
55
|
+
updatedAt: this.toMySQLDatetime(now)
|
|
52
56
|
};
|
|
53
57
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
58
|
+
const cleanData = Object.fromEntries(Object.entries(data).filter(([, v]) => v !== undefined));
|
|
59
|
+
const fieldsList = Object.keys(cleanData).map(f => \`\\\`\${f}\\\`\`).join(', ');
|
|
60
|
+
const placeholders = Object.keys(cleanData).map(f => \`:\${f}\`).join(', ');
|
|
56
61
|
|
|
57
62
|
const result = await this.db.query(
|
|
58
63
|
\`INSERT INTO \\\`\${this.tableName}\\\` (\${fieldsList}) VALUES (\${placeholders})\`,
|
|
59
|
-
|
|
64
|
+
cleanData
|
|
60
65
|
);
|
|
61
66
|
|
|
62
67
|
if (result.success && result.insertId) {
|
|
@@ -69,17 +74,19 @@ export class {{ENTITY_NAME}}Store {
|
|
|
69
74
|
|
|
70
75
|
async update(id: number, entity: {{ENTITY_NAME}}): Promise<{{ENTITY_NAME}}> {
|
|
71
76
|
const now = new Date();
|
|
72
|
-
const
|
|
77
|
+
const rawData: Partial<{{ENTITY_NAME}}Row> = {
|
|
73
78
|
{{UPDATE_DATA_MAPPING}},
|
|
74
|
-
|
|
75
|
-
id
|
|
79
|
+
updatedAt: this.toMySQLDatetime(now)
|
|
76
80
|
};
|
|
77
81
|
|
|
78
|
-
const
|
|
82
|
+
const cleanData = Object.fromEntries(Object.entries(rawData).filter(([, v]) => v !== undefined));
|
|
83
|
+
const updateFields = {{UPDATE_FIELDS_ARRAY}}
|
|
84
|
+
.filter(f => f in cleanData)
|
|
85
|
+
.map(f => \`\\\`\${f}\\\` = :\${f}\`).join(', ');
|
|
79
86
|
|
|
80
87
|
const result = await this.db.query(
|
|
81
|
-
\`UPDATE \\\`\${this.tableName}\\\` SET \${updateFields},
|
|
82
|
-
|
|
88
|
+
\`UPDATE \\\`\${this.tableName}\\\` SET \${updateFields}, updatedAt = :updatedAt WHERE id = :id\`,
|
|
89
|
+
{ ...cleanData, id }
|
|
83
90
|
);
|
|
84
91
|
|
|
85
92
|
if (result.success) {
|
|
@@ -92,8 +99,8 @@ export class {{ENTITY_NAME}}Store {
|
|
|
92
99
|
async softDelete(id: number): Promise<boolean> {
|
|
93
100
|
const now = new Date();
|
|
94
101
|
const result = await this.db.query(
|
|
95
|
-
\`UPDATE \\\`\${this.tableName}\\\` SET
|
|
96
|
-
{
|
|
102
|
+
\`UPDATE \\\`\${this.tableName}\\\` SET deletedAt = :deletedAt WHERE id = :id\`,
|
|
103
|
+
{ deletedAt: this.toMySQLDatetime(now), id }
|
|
97
104
|
);
|
|
98
105
|
|
|
99
106
|
return result.success;
|
|
@@ -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
|
-
|
|
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()
|
|
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
|
-
|
|
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('
|
|
119
|
-
columns.push('
|
|
120
|
-
columns.push('
|
|
121
|
-
indexes.push(` INDEX idx_${tableName}
|
|
122
|
-
indexes.push(` INDEX idx_${tableName}
|
|
146
|
+
columns.push(' createdAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP');
|
|
147
|
+
columns.push(' updatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP');
|
|
148
|
+
columns.push(' deletedAt DATETIME NULL DEFAULT NULL');
|
|
149
|
+
indexes.push(` INDEX idx_${tableName}_deletedAt (deletedAt)`);
|
|
150
|
+
indexes.push(` INDEX idx_${tableName}_createdAt (createdAt)`);
|
|
123
151
|
const allParts = [...columns, ...indexes, ...foreignKeys];
|
|
124
|
-
return `CREATE TABLE IF NOT EXISTS
|
|
152
|
+
return `CREATE TABLE IF NOT EXISTS \`${tableName}\` (\n${allParts.join(',\n')}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;`;
|
|
125
153
|
}
|
|
126
154
|
function generateDropTableSQL(tableName) {
|
|
127
|
-
return `DROP TABLE IF EXISTS
|
|
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
|
|
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
|
|
166
|
+
return `ALTER TABLE \`${tableName}\` ADD COLUMN \`${fieldName}\` ${sqlType} ${nullable};`;
|
|
139
167
|
}
|
|
140
168
|
}
|
|
141
169
|
function generateDropColumnSQL(tableName, columnName) {
|
|
142
|
-
return `ALTER TABLE
|
|
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
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
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
|
}
|
package/dist/utils/typeUtils.js
CHANGED
|
@@ -36,9 +36,9 @@ exports.ROW_TYPE_MAPPING = {
|
|
|
36
36
|
datetime: 'string',
|
|
37
37
|
date: 'string',
|
|
38
38
|
id: 'number',
|
|
39
|
-
json: '
|
|
40
|
-
array: '
|
|
41
|
-
object: '
|
|
39
|
+
json: 'any',
|
|
40
|
+
array: 'any[]',
|
|
41
|
+
object: 'any',
|
|
42
42
|
enum: 'string'
|
|
43
43
|
};
|
|
44
44
|
function capitalize(str) {
|