masterrecord 0.3.30 → 0.3.32
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/Entity/entityModelBuilder.js +44 -1
- package/Migrations/cli.js +17 -4
- package/Migrations/dependencyGraph.js +108 -0
- package/Migrations/migrationMySQLQuery.js +10 -0
- package/Migrations/migrationPostgresQuery.js +10 -0
- package/Migrations/migrationSQLiteQuery.js +10 -0
- package/Migrations/migrationTemplate.js +176 -0
- package/Migrations/migrations.js +121 -12
- package/Migrations/schema.js +63 -0
- package/context.js +288 -0
- package/package.json +1 -1
- package/readme.md +812 -37
- package/test/seed-data-test.js +212 -0
- package/test/seed-features-integration-test.js +418 -0
- package/test/seed-migration-template-test.js +220 -0
|
@@ -46,17 +46,60 @@ class EntityModelBuilder {
|
|
|
46
46
|
MDB.obj.name = methodName;
|
|
47
47
|
obj[methodName] = MDB.obj;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
// Extract composite indexes from static property (Option A)
|
|
51
|
+
if (model.compositeIndexes) {
|
|
52
|
+
obj.__compositeIndexes = this.#normalizeCompositeIndexes(
|
|
53
|
+
model.compositeIndexes,
|
|
54
|
+
model.name
|
|
55
|
+
);
|
|
56
|
+
} else {
|
|
57
|
+
obj.__compositeIndexes = []; // Initialize empty array
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
return obj;
|
|
50
61
|
}
|
|
51
62
|
|
|
52
63
|
static cleanNull(obj) {
|
|
53
|
-
for (var propName in obj) {
|
|
64
|
+
for (var propName in obj) {
|
|
54
65
|
if (obj[propName] === null) {
|
|
55
66
|
delete obj[propName];
|
|
56
67
|
}
|
|
57
68
|
}
|
|
58
69
|
}
|
|
59
70
|
|
|
71
|
+
static #normalizeCompositeIndexes(indexes, tableName) {
|
|
72
|
+
if (!Array.isArray(indexes)) {
|
|
73
|
+
throw new Error(`compositeIndexes must be an array`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return indexes.map((index, i) => {
|
|
77
|
+
// Simple array: ['col1', 'col2'] -> auto-generate name
|
|
78
|
+
if (Array.isArray(index)) {
|
|
79
|
+
const colNames = index.join('_');
|
|
80
|
+
return {
|
|
81
|
+
columns: index,
|
|
82
|
+
name: `idx_${tableName.toLowerCase()}_${colNames}`,
|
|
83
|
+
unique: false
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Object: { columns: [...], name?, unique? }
|
|
88
|
+
if (!index.columns || !Array.isArray(index.columns)) {
|
|
89
|
+
throw new Error(`Composite index must have 'columns' array`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const name = index.name ||
|
|
93
|
+
`idx_${tableName.toLowerCase()}_${index.columns.join('_')}`;
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
columns: index.columns,
|
|
97
|
+
name: name,
|
|
98
|
+
unique: index.unique || false
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
60
103
|
}
|
|
61
104
|
|
|
62
105
|
module.exports = EntityModelBuilder;
|
package/Migrations/cli.js
CHANGED
|
@@ -275,15 +275,17 @@ program.option('-V', 'output the version');
|
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
var cleanEntities = migration.cleanEntities(contextInstance.__entities);
|
|
278
|
+
var seedData = contextInstance.__contextSeedData || {};
|
|
279
|
+
var seedConfig = contextInstance.__contextSeedConfig || {};
|
|
278
280
|
|
|
279
281
|
// Skip if no changes between snapshot schema and current entities
|
|
280
|
-
const has = migration.hasChanges(contextSnapshot.schema || [], cleanEntities || []);
|
|
282
|
+
const has = migration.hasChanges(contextSnapshot.schema || [], cleanEntities || [], seedData);
|
|
281
283
|
if(!has){
|
|
282
284
|
console.log(`No changes detected for ${path.basename(contextAbs)}. Skipping.`);
|
|
283
285
|
return;
|
|
284
286
|
}
|
|
285
287
|
|
|
286
|
-
var newEntity = migration.template(name, contextSnapshot.schema, cleanEntities);
|
|
288
|
+
var newEntity = migration.template(name, contextSnapshot.schema, cleanEntities, seedData, seedConfig);
|
|
287
289
|
if(!fs.existsSync(migBase)){
|
|
288
290
|
try{ fs.mkdirSync(migBase, { recursive: true }); }catch(_){ /* ignore */ }
|
|
289
291
|
}
|
|
@@ -465,6 +467,7 @@ program.option('-V', 'output the version');
|
|
|
465
467
|
executedLocation : executedLocation,
|
|
466
468
|
context : contextInstance,
|
|
467
469
|
contextEntities : cleanEntities,
|
|
470
|
+
contextSeedData: contextInstance.__contextSeedData || {},
|
|
468
471
|
contextFileName: contextFileName
|
|
469
472
|
}
|
|
470
473
|
|
|
@@ -582,6 +585,8 @@ program.option('-V', 'output the version');
|
|
|
582
585
|
executedLocation : executedLocation,
|
|
583
586
|
context : contextInstance,
|
|
584
587
|
contextEntities : cleanEntities,
|
|
588
|
+
contextSeedData: contextInstance.__contextSeedData || {},
|
|
589
|
+
contextSeedConfig: contextInstance.__contextSeedConfig || {},
|
|
585
590
|
contextFileName: contextFileName
|
|
586
591
|
}
|
|
587
592
|
migration.createSnapShot(snap);
|
|
@@ -832,6 +837,8 @@ program.option('-V', 'output the version');
|
|
|
832
837
|
executedLocation : executedLocation,
|
|
833
838
|
context : contextInstance,
|
|
834
839
|
contextEntities : cleanEntities,
|
|
840
|
+
contextSeedData: contextInstance.__contextSeedData || {},
|
|
841
|
+
contextSeedConfig: contextInstance.__contextSeedConfig || {},
|
|
835
842
|
contextFileName: path.basename(snapshotFile).replace('_contextSnapShot.json','')
|
|
836
843
|
}
|
|
837
844
|
migration.createSnapShot(snap);
|
|
@@ -887,13 +894,15 @@ program.option('-V', 'output the version');
|
|
|
887
894
|
}
|
|
888
895
|
var migration = new Migration();
|
|
889
896
|
var cleanEntities = migration.cleanEntities(contextInstance.__entities);
|
|
897
|
+
var seedData = contextInstance.__contextSeedData || {};
|
|
898
|
+
var seedConfig = contextInstance.__contextSeedConfig || {};
|
|
890
899
|
// If no changes, skip with message
|
|
891
|
-
const has = migration.hasChanges(cs.schema || [], cleanEntities || []);
|
|
900
|
+
const has = migration.hasChanges(cs.schema || [], cleanEntities || [], seedData);
|
|
892
901
|
if(!has){
|
|
893
902
|
console.log(`No changes detected for ${path.basename(contextAbs)}. Skipping.`);
|
|
894
903
|
continue;
|
|
895
904
|
}
|
|
896
|
-
var newEntity = migration.template(name, cs.schema, cleanEntities);
|
|
905
|
+
var newEntity = migration.template(name, cs.schema, cleanEntities, seedData, seedConfig);
|
|
897
906
|
if(!fs.existsSync(migBase)){
|
|
898
907
|
try{ fs.mkdirSync(migBase, { recursive: true }); }catch(_){ /* ignore */ }
|
|
899
908
|
}
|
|
@@ -1010,6 +1019,8 @@ program.option('-V', 'output the version');
|
|
|
1010
1019
|
executedLocation : executedLocation,
|
|
1011
1020
|
context : contextInstance,
|
|
1012
1021
|
contextEntities : cleanEntities,
|
|
1022
|
+
contextSeedData: contextInstance.__contextSeedData || {},
|
|
1023
|
+
contextSeedConfig: contextInstance.__contextSeedConfig || {},
|
|
1013
1024
|
contextFileName: entry.ctxName
|
|
1014
1025
|
}
|
|
1015
1026
|
migration.createSnapShot(snap);
|
|
@@ -1072,6 +1083,8 @@ program.option('-V', 'output the version');
|
|
|
1072
1083
|
file : abs,
|
|
1073
1084
|
executedLocation : executedLocation,
|
|
1074
1085
|
contextEntities : [],
|
|
1086
|
+
contextSeedData: {},
|
|
1087
|
+
contextSeedConfig: {},
|
|
1075
1088
|
contextFileName: key
|
|
1076
1089
|
};
|
|
1077
1090
|
migration.createSnapShot(snap);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency graph for topological sorting of seed data
|
|
3
|
+
* Uses Kahn's algorithm to order tables by foreign key dependencies
|
|
4
|
+
*/
|
|
5
|
+
class DependencyGraph {
|
|
6
|
+
constructor(entities) {
|
|
7
|
+
this.entities = entities;
|
|
8
|
+
this.graph = new Map(); // adjacency list: parent -> [children]
|
|
9
|
+
this.inDegree = new Map(); // in-degree for each table (number of dependencies)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build dependency graph from entity relationships
|
|
14
|
+
* Tables with belongsTo relationships depend on their foreign tables
|
|
15
|
+
*/
|
|
16
|
+
buildFromEntities() {
|
|
17
|
+
// Initialize graph structure for all entities
|
|
18
|
+
this.entities.forEach(entity => {
|
|
19
|
+
const tableName = entity.__name;
|
|
20
|
+
if (!tableName) return;
|
|
21
|
+
|
|
22
|
+
this.graph.set(tableName, []);
|
|
23
|
+
this.inDegree.set(tableName, 0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Build edges from belongsTo relationships
|
|
27
|
+
this.entities.forEach(entity => {
|
|
28
|
+
const tableName = entity.__name;
|
|
29
|
+
if (!tableName) return;
|
|
30
|
+
|
|
31
|
+
// Find belongsTo relationships (dependencies)
|
|
32
|
+
Object.keys(entity).forEach(key => {
|
|
33
|
+
const field = entity[key];
|
|
34
|
+
|
|
35
|
+
// Check if this is a belongsTo relationship with a foreign table
|
|
36
|
+
if (field && typeof field === 'object' &&
|
|
37
|
+
field.relationshipType === 'belongsTo' &&
|
|
38
|
+
field.foreignTable) {
|
|
39
|
+
|
|
40
|
+
const foreignTable = field.foreignTable;
|
|
41
|
+
|
|
42
|
+
// Ensure foreign table exists in graph
|
|
43
|
+
if (!this.graph.has(foreignTable)) {
|
|
44
|
+
this.graph.set(foreignTable, []);
|
|
45
|
+
this.inDegree.set(foreignTable, 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add edge: foreignTable -> tableName (tableName depends on foreignTable)
|
|
49
|
+
this.graph.get(foreignTable).push(tableName);
|
|
50
|
+
this.inDegree.set(tableName, this.inDegree.get(tableName) + 1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Perform topological sort using Kahn's algorithm
|
|
58
|
+
* @returns {Array<string>} Ordered list of table names
|
|
59
|
+
* @throws {Error} If circular dependency detected
|
|
60
|
+
*/
|
|
61
|
+
topologicalSort() {
|
|
62
|
+
const result = [];
|
|
63
|
+
const queue = [];
|
|
64
|
+
const inDegreeCopy = new Map(this.inDegree);
|
|
65
|
+
|
|
66
|
+
// Start with nodes that have no dependencies (in-degree = 0)
|
|
67
|
+
for (const [node, degree] of inDegreeCopy.entries()) {
|
|
68
|
+
if (degree === 0) {
|
|
69
|
+
queue.push(node);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
while (queue.length > 0) {
|
|
74
|
+
const current = queue.shift();
|
|
75
|
+
result.push(current);
|
|
76
|
+
|
|
77
|
+
// Process all neighbors (tables that depend on current)
|
|
78
|
+
const neighbors = this.graph.get(current) || [];
|
|
79
|
+
neighbors.forEach(neighbor => {
|
|
80
|
+
inDegreeCopy.set(neighbor, inDegreeCopy.get(neighbor) - 1);
|
|
81
|
+
if (inDegreeCopy.get(neighbor) === 0) {
|
|
82
|
+
queue.push(neighbor);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Detect cycles: if we couldn't visit all nodes, there's a cycle
|
|
88
|
+
if (result.length !== this.inDegree.size) {
|
|
89
|
+
const unvisited = Array.from(this.inDegree.keys()).filter(k => !result.includes(k));
|
|
90
|
+
throw new Error(`Circular dependency detected in tables: ${unvisited.join(' <-> ')}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get topologically sorted list filtered to only tables with seed data
|
|
98
|
+
* @param {Object} seedData - Object with table names as keys
|
|
99
|
+
* @returns {Array<string>} Ordered list of table names that have seed data
|
|
100
|
+
*/
|
|
101
|
+
filterToSeededTables(seedData) {
|
|
102
|
+
const sorted = this.topologicalSort();
|
|
103
|
+
const seededTables = Object.keys(seedData);
|
|
104
|
+
return sorted.filter(table => seededTables.includes(table));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = DependencyGraph;
|
|
@@ -246,6 +246,16 @@ class migrationMySQLQuery {
|
|
|
246
246
|
return `DROP INDEX \`${indexName}\` ON \`${indexInfo.tableName}\``;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
createCompositeIndex(indexInfo){
|
|
250
|
+
const columns = indexInfo.columns.map(c => `\`${c}\``).join(', ');
|
|
251
|
+
const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
|
|
252
|
+
return `CREATE ${uniqueKeyword}INDEX \`${indexInfo.indexName}\` ON \`${indexInfo.tableName}\`(${columns})`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
dropCompositeIndex(indexInfo){
|
|
256
|
+
return `DROP INDEX \`${indexInfo.indexName}\` ON \`${indexInfo.tableName}\``;
|
|
257
|
+
}
|
|
258
|
+
|
|
249
259
|
/**
|
|
250
260
|
* SEED DATA METHODS
|
|
251
261
|
* Support for inserting seed data during migrations
|
|
@@ -233,6 +233,16 @@ class migrationPostgresQuery {
|
|
|
233
233
|
return `DROP INDEX IF EXISTS "${indexName}"`;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
createCompositeIndex(indexInfo){
|
|
237
|
+
const columns = indexInfo.columns.map(c => `"${c}"`).join(', ');
|
|
238
|
+
const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
|
|
239
|
+
return `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS "${indexInfo.indexName}" ON "${indexInfo.tableName}"(${columns})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
dropCompositeIndex(indexInfo){
|
|
243
|
+
return `DROP INDEX IF EXISTS "${indexInfo.indexName}"`;
|
|
244
|
+
}
|
|
245
|
+
|
|
236
246
|
/**
|
|
237
247
|
* SEED DATA METHODS
|
|
238
248
|
* Support for inserting seed data during migrations
|
|
@@ -201,6 +201,16 @@ class migrationSQLiteQuery {
|
|
|
201
201
|
return `DROP INDEX IF EXISTS ${indexName}`;
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
createCompositeIndex(indexInfo){
|
|
205
|
+
const columns = indexInfo.columns.join(', ');
|
|
206
|
+
const uniqueKeyword = indexInfo.unique ? 'UNIQUE ' : '';
|
|
207
|
+
return `CREATE ${uniqueKeyword}INDEX IF NOT EXISTS ${indexInfo.indexName} ON ${indexInfo.tableName}(${columns})`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
dropCompositeIndex(indexInfo){
|
|
211
|
+
return `DROP INDEX IF EXISTS ${indexInfo.indexName}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
204
214
|
/**
|
|
205
215
|
* SEED DATA METHODS
|
|
206
216
|
* Support for inserting seed data during migrations
|
|
@@ -120,6 +120,182 @@ module.exports = ${this.name};
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
createCompositeIndex(type, indexInfo){
|
|
124
|
+
const indexInfoStr = JSON.stringify({
|
|
125
|
+
tableName: indexInfo.tableName,
|
|
126
|
+
columns: indexInfo.columns,
|
|
127
|
+
indexName: indexInfo.indexName,
|
|
128
|
+
unique: indexInfo.unique
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if(type === "up"){
|
|
132
|
+
this.#up += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
|
|
133
|
+
}
|
|
134
|
+
else{
|
|
135
|
+
this.#down += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
dropCompositeIndex(type, indexInfo){
|
|
140
|
+
const indexInfoStr = JSON.stringify({
|
|
141
|
+
tableName: indexInfo.tableName,
|
|
142
|
+
columns: indexInfo.columns,
|
|
143
|
+
indexName: indexInfo.indexName,
|
|
144
|
+
unique: indexInfo.unique
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if(type === "up"){
|
|
148
|
+
this.#up += os.EOL + ` this.dropCompositeIndex(${indexInfoStr});`
|
|
149
|
+
}
|
|
150
|
+
else{
|
|
151
|
+
this.#down += os.EOL + ` this.createCompositeIndex(${indexInfoStr});`
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
seedData(type, tableName, records, currentEnv = 'development'){
|
|
156
|
+
if(!records || records.length === 0) return;
|
|
157
|
+
|
|
158
|
+
if(type === "up"){
|
|
159
|
+
// Filter records by environment first
|
|
160
|
+
const filteredRecords = records.filter(record => {
|
|
161
|
+
const envCondition = record.__seedEnv;
|
|
162
|
+
if (envCondition && envCondition.strategy === 'generation-time') {
|
|
163
|
+
return envCondition.conditions.includes(currentEnv);
|
|
164
|
+
}
|
|
165
|
+
return true; // No environment condition, include record
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (filteredRecords.length === 0) return;
|
|
169
|
+
|
|
170
|
+
// Check if all records are factory-generated
|
|
171
|
+
const allGenerated = filteredRecords.every(r => r.__seedMeta?.generated);
|
|
172
|
+
|
|
173
|
+
// Use optimized loop syntax for bulk factory data (10+ records)
|
|
174
|
+
if (allGenerated && filteredRecords.length >= 10) {
|
|
175
|
+
this.#up += os.EOL + ` const factoryRecords = [`;
|
|
176
|
+
|
|
177
|
+
filteredRecords.forEach((record, i) => {
|
|
178
|
+
const cleanRecord = { ...record };
|
|
179
|
+
delete cleanRecord.__rollback;
|
|
180
|
+
delete cleanRecord.__seedEnv;
|
|
181
|
+
delete cleanRecord.__seedStrategy;
|
|
182
|
+
delete cleanRecord.__seedMeta;
|
|
183
|
+
|
|
184
|
+
const recordStr = JSON.stringify(cleanRecord);
|
|
185
|
+
this.#up += os.EOL + ` ${recordStr}${i < filteredRecords.length - 1 ? ',' : ''}`;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
this.#up += os.EOL + ` ];`;
|
|
189
|
+
this.#up += os.EOL + ` for (const record of factoryRecords) {`;
|
|
190
|
+
this.#up += os.EOL + ` await table.${tableName}.create(record);`;
|
|
191
|
+
this.#up += os.EOL + ` }`;
|
|
192
|
+
} else {
|
|
193
|
+
// Standard individual inserts for non-factory or small batches
|
|
194
|
+
filteredRecords.forEach(record => {
|
|
195
|
+
const strategy = record.__seedStrategy;
|
|
196
|
+
|
|
197
|
+
// Clean up metadata before generating migration code
|
|
198
|
+
const cleanRecord = { ...record };
|
|
199
|
+
delete cleanRecord.__rollback;
|
|
200
|
+
delete cleanRecord.__seedEnv;
|
|
201
|
+
delete cleanRecord.__seedStrategy;
|
|
202
|
+
delete cleanRecord.__seedMeta;
|
|
203
|
+
|
|
204
|
+
// Handle upsert strategy
|
|
205
|
+
if (strategy && strategy.type === 'upsert') {
|
|
206
|
+
this._generateUpsert(tableName, cleanRecord, strategy);
|
|
207
|
+
} else {
|
|
208
|
+
// Standard insert
|
|
209
|
+
const recordStr = JSON.stringify(cleanRecord);
|
|
210
|
+
|
|
211
|
+
// Check if record is too long for single line (> 80 chars)
|
|
212
|
+
if (recordStr.length > 80) {
|
|
213
|
+
// Multi-line format with proper indentation
|
|
214
|
+
const formattedRecord = JSON.stringify(cleanRecord, null, 12)
|
|
215
|
+
.split('\n')
|
|
216
|
+
.join(os.EOL + ' ');
|
|
217
|
+
this.#up += os.EOL + ` await table.${tableName}.create(${formattedRecord});`;
|
|
218
|
+
} else {
|
|
219
|
+
// Single-line format
|
|
220
|
+
this.#up += os.EOL + ` await table.${tableName}.create(${recordStr});`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
_generateUpsert(tableName, cleanRecord, strategy) {
|
|
229
|
+
const conflictKey = strategy.conflictKey === 'primaryKey'
|
|
230
|
+
? (cleanRecord.id !== undefined ? 'id' : Object.keys(cleanRecord)[0])
|
|
231
|
+
: strategy.conflictKey;
|
|
232
|
+
|
|
233
|
+
const conflictValue = cleanRecord[conflictKey];
|
|
234
|
+
if (conflictValue === undefined) {
|
|
235
|
+
throw new Error(`Upsert requires a value for conflict key: ${conflictKey}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.#up += os.EOL + ` {`;
|
|
239
|
+
this.#up += os.EOL + ` const existing = await table.${tableName}.where(r => r.${conflictKey} == ${JSON.stringify(conflictValue)}).single();`;
|
|
240
|
+
this.#up += os.EOL + ` if (existing) {`;
|
|
241
|
+
|
|
242
|
+
// Update logic
|
|
243
|
+
if (strategy.updateFields && Array.isArray(strategy.updateFields)) {
|
|
244
|
+
strategy.updateFields.forEach(field => {
|
|
245
|
+
if (cleanRecord[field] !== undefined) {
|
|
246
|
+
this.#up += os.EOL + ` existing.${field} = ${JSON.stringify(cleanRecord[field])};`;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
// Update all fields except conflict key
|
|
251
|
+
Object.keys(cleanRecord).forEach(field => {
|
|
252
|
+
if (field !== conflictKey) {
|
|
253
|
+
this.#up += os.EOL + ` existing.${field} = ${JSON.stringify(cleanRecord[field])};`;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.#up += os.EOL + ` await existing.save();`;
|
|
259
|
+
this.#up += os.EOL + ` } else {`;
|
|
260
|
+
this.#up += os.EOL + ` await table.${tableName}.create(${JSON.stringify(cleanRecord)});`;
|
|
261
|
+
this.#up += os.EOL + ` }`;
|
|
262
|
+
this.#up += os.EOL + ` }`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
seedDataDown(type, tableName, records, config){
|
|
266
|
+
if(type !== "down" || !config || !config.generateDownMigrations) return;
|
|
267
|
+
if(!records || records.length === 0) return;
|
|
268
|
+
|
|
269
|
+
// Reverse order for safe FK deletion (children before parents)
|
|
270
|
+
const reversed = [...records].reverse();
|
|
271
|
+
|
|
272
|
+
reversed.forEach(record => {
|
|
273
|
+
const rollback = record.__rollback;
|
|
274
|
+
if (!rollback || !rollback.value) {
|
|
275
|
+
// Skip if no rollback metadata (e.g., no primary key specified)
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const pkValue = rollback.value;
|
|
280
|
+
const pkKey = rollback.key || 'id';
|
|
281
|
+
|
|
282
|
+
// Generate delete code with error handling
|
|
283
|
+
this.#down += os.EOL + ` try {`;
|
|
284
|
+
this.#down += os.EOL + ` const record = await table.${tableName}.findById(${JSON.stringify(pkValue)});`;
|
|
285
|
+
this.#down += os.EOL + ` if (record) await record.delete();`;
|
|
286
|
+
this.#down += os.EOL + ` } catch (e) {`;
|
|
287
|
+
|
|
288
|
+
if (config.onRollbackError === 'throw') {
|
|
289
|
+
this.#down += os.EOL + ` throw new Error('Seed rollback failed: ${tableName} id=${pkValue} - ' + e.message);`;
|
|
290
|
+
} else if (config.onRollbackError === 'warn') {
|
|
291
|
+
this.#down += os.EOL + ` console.warn('Seed rollback: ${tableName} id=${pkValue} not found or error:', e.message);`;
|
|
292
|
+
}
|
|
293
|
+
// else ignore (onRollbackError === 'ignore')
|
|
294
|
+
|
|
295
|
+
this.#down += os.EOL + ` }`;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
123
299
|
}
|
|
124
300
|
|
|
125
301
|
module.exports = MigrationTemplate;
|