masterrecord 0.3.31 → 0.3.33

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.
@@ -58,7 +58,8 @@
58
58
  "Bash(wc:*)",
59
59
  "Bash(npm link)",
60
60
  "Bash(npm link:*)",
61
- "Bash(1)"
61
+ "Bash(1)",
62
+ "Bash(masterrecord update-database:*)"
62
63
  ],
63
64
  "deny": [],
64
65
  "ask": []
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;
@@ -193,6 +193,11 @@ class migrationMySQLQuery {
193
193
  var queryVar = "";
194
194
  //console.log("Dsfdsfdsf---------", table)
195
195
  for (var key in table) {
196
+ // Skip metadata properties (indexes, __compositeIndexes, __name, etc.)
197
+ if(key === 'indexes' || key.startsWith('__')){
198
+ continue;
199
+ }
200
+
196
201
  if(typeof table[key] === "object"){
197
202
 
198
203
  if(table[key].type !== "hasOne" && table[key].type !== "hasMany" && table[key].type !== "hasManyThrough"){
@@ -196,6 +196,11 @@ class migrationPostgresQuery {
196
196
  var queryVar = "";
197
197
 
198
198
  for (var key in table) {
199
+ // Skip metadata properties (indexes, __compositeIndexes, __name, etc.)
200
+ if(key === 'indexes' || key.startsWith('__')){
201
+ continue;
202
+ }
203
+
199
204
  if(typeof table[key] === "object"){
200
205
  if(table[key].type !== "hasOne" && table[key].type !== "hasMany" && table[key].type !== "hasManyThrough"){
201
206
  queryVar += `${this.#columnMapping(table[key])}, `;
@@ -147,6 +147,11 @@ class migrationSQLiteQuery {
147
147
  createTable(table){
148
148
  var queryVar = "";
149
149
  for (var key in table) {
150
+ // Skip metadata properties (indexes, __compositeIndexes, __name, etc.)
151
+ if(key === 'indexes' || key.startsWith('__')){
152
+ continue;
153
+ }
154
+
150
155
  if(typeof table[key] === "object"){
151
156
  var col = table[key];
152
157
  // Skip relationship-only fields
@@ -156,7 +161,7 @@ class migrationSQLiteQuery {
156
161
  queryVar += `${this.#columnMapping(col)}, `;
157
162
  }
158
163
  }
159
-
164
+
160
165
  return `CREATE TABLE IF NOT EXISTS ${table.__name} (${queryVar.replace(/,\s*$/, "")});`;
161
166
 
162
167
  /*
@@ -152,6 +152,150 @@ module.exports = ${this.name};
152
152
  }
153
153
  }
154
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
+
155
299
  }
156
300
 
157
301
  module.exports = MigrationTemplate;
@@ -28,7 +28,8 @@ class Migrations{
28
28
  newIndexes : [],
29
29
  deletedIndexes : [],
30
30
  newCompositeIndexes : [],
31
- deletedCompositeIndexes : []
31
+ deletedCompositeIndexes : [],
32
+ newSeedData : []
32
33
  }
33
34
  tables.push(table);
34
35
  });
@@ -46,7 +47,8 @@ class Migrations{
46
47
  newIndexes : [],
47
48
  deletedIndexes : [],
48
49
  newCompositeIndexes : [],
49
- deletedCompositeIndexes : []
50
+ deletedCompositeIndexes : [],
51
+ newSeedData : []
50
52
  }
51
53
 
52
54
  oldSchema.forEach(function (oldItem, index) {
@@ -161,7 +163,7 @@ class Migrations{
161
163
  }
162
164
 
163
165
  // build table to build new migration snapshot
164
- #buildMigrationObject(oldSchema, newSchema){
166
+ #buildMigrationObject(oldSchema, newSchema, newSeedData = {}){
165
167
 
166
168
  var tables = this.#organizeSchemaByTables(oldSchema, newSchema);
167
169
 
@@ -173,6 +175,7 @@ class Migrations{
173
175
  tables = this.#findDeletedIndexes(tables);
174
176
  tables = this.#findNewCompositeIndexes(tables);
175
177
  tables = this.#findDeletedCompositeIndexes(tables);
178
+ tables = this.#findNewSeedData(tables, newSeedData);
176
179
  return tables;
177
180
  }
178
181
 
@@ -325,6 +328,17 @@ class Migrations{
325
328
  return tables;
326
329
  }
327
330
 
331
+ #findNewSeedData(tables, newSeedData) {
332
+ // newSeedData is from schema snapshot: { tableName: [records] }
333
+ tables.forEach(function(item) {
334
+ const tableSeedData = newSeedData[item.name];
335
+ if (tableSeedData && tableSeedData.length > 0) {
336
+ item.newSeedData = tableSeedData;
337
+ }
338
+ });
339
+ return tables;
340
+ }
341
+
328
342
 
329
343
 
330
344
  findContextFile(executedLocation, contextFileName){
@@ -397,11 +411,18 @@ class Migrations{
397
411
  const relMigrationFolder = '.'; // the snapshot sits inside migrationsDirectory
398
412
  const relSnapshotLocation = path.basename(snapshotPath);
399
413
 
414
+ // Order seed data by dependencies if context instance is available
415
+ const orderedSeedData = snap.context && snap.context.getOrderedSeedData
416
+ ? snap.context.getOrderedSeedData()
417
+ : snap.contextSeedData || {};
418
+
400
419
  const content = {
401
420
  contextLocation: relContextLocation,
402
421
  migrationFolder: relMigrationFolder,
403
422
  snapShotLocation: relSnapshotLocation,
404
- schema : snap.contextEntities
423
+ schema : snap.contextEntities,
424
+ seedData: orderedSeedData,
425
+ seedConfig: snap.contextSeedConfig || {}
405
426
  };
406
427
 
407
428
  const jsonContent = JSON.stringify(content, null, 2);
@@ -455,8 +476,8 @@ class Migrations{
455
476
  }
456
477
 
457
478
  // Returns true if there are any changes between old and new schema
458
- hasChanges(oldSchema, newSchema){
459
- const tables = this.#buildMigrationObject(oldSchema, newSchema);
479
+ hasChanges(oldSchema, newSchema, newSeedData = {}){
480
+ const tables = this.#buildMigrationObject(oldSchema, newSchema, newSeedData);
460
481
  for(const t of tables){
461
482
  if(!t) continue;
462
483
  if((t.newTables && t.newTables.length) ||
@@ -467,6 +488,7 @@ class Migrations{
467
488
  (t.deletedIndexes && t.deletedIndexes.length) ||
468
489
  (t.newCompositeIndexes && t.newCompositeIndexes.length) ||
469
490
  (t.deletedCompositeIndexes && t.deletedCompositeIndexes.length) ||
491
+ (t.newSeedData && t.newSeedData.length) ||
470
492
  (t.old === null) || (t.new === null)){
471
493
  return true;
472
494
  }
@@ -474,16 +496,20 @@ class Migrations{
474
496
  return false;
475
497
  }
476
498
 
477
- template(name, oldSchema, newSchema){
499
+ template(name, oldSchema, newSchema, newSeedData = {}, seedConfig = {}, currentEnv = null){
478
500
  var MT = new MigrationTemplate(name);
479
- var tables = this.#buildMigrationObject(oldSchema, newSchema);
480
-
501
+ // Determine current environment if not provided
502
+ if (!currentEnv) {
503
+ currentEnv = process.env.NODE_ENV || process.env.master || 'development';
504
+ }
505
+ var tables = this.#buildMigrationObject(oldSchema, newSchema, newSeedData);
506
+
481
507
  tables.forEach(function (item, index) {
482
508
  if(item.old === null){
483
509
  MT.createTable("up", column, item.name);
484
510
  MT.dropTable("down", column, item.name);
485
511
  }
486
-
512
+
487
513
  if(item.new === null){
488
514
  MT.dropTable("up", column, item.name);
489
515
  MT.createTable("down", column, item.name);
@@ -529,6 +555,12 @@ class Migrations{
529
555
  MT.dropCompositeIndex("up", indexInfo);
530
556
  });
531
557
 
558
+ // Generate seed data code
559
+ if (item.newSeedData && item.newSeedData.length > 0) {
560
+ MT.seedData("up", item.name, item.newSeedData, currentEnv);
561
+ MT.seedDataDown("down", item.name, item.newSeedData, seedConfig);
562
+ }
563
+
532
564
  });
533
565
 
534
566
  return MT.get();