masterrecord 0.2.19 → 0.2.21

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/Migrations/cli.js CHANGED
@@ -191,24 +191,40 @@ program.option('-V', 'output the version');
191
191
  return;
192
192
  }
193
193
 
194
+ // Resolve relative paths from the snapshot directory (portable snapshots)
195
+ const snapDir = path.dirname(file);
196
+ const contextAbs = path.resolve(snapDir, contextSnapshot.contextLocation || '');
197
+ const migBase = path.resolve(snapDir, contextSnapshot.migrationFolder || '.');
198
+
194
199
  let ContextCtor;
195
200
  try{
196
- ContextCtor = require(contextSnapshot.contextLocation);
201
+ ContextCtor = require(contextAbs);
197
202
  }catch(_){
198
- console.log(`Error - Cannot load Context file at '${contextSnapshot.contextLocation}'.`);
203
+ console.log(`Error - Cannot load Context file at '${contextAbs}'.`);
199
204
  return;
200
205
  }
201
206
  let contextInstance;
202
207
  try{
203
208
  contextInstance = new ContextCtor();
204
209
  }catch(_){
205
- console.log(`Error - Failed to construct Context from '${contextSnapshot.contextLocation}'.`);
210
+ console.log(`Error - Failed to construct Context from '${contextAbs}'.`);
206
211
  return;
207
212
  }
208
213
  var cleanEntities = migration.cleanEntities(contextInstance.__entities);
214
+
215
+ // Skip if no changes between snapshot schema and current entities
216
+ const has = migration.hasChanges(contextSnapshot.schema || [], cleanEntities || []);
217
+ if(!has){
218
+ console.log(`No changes detected for ${path.basename(contextAbs)}. Skipping.`);
219
+ return;
220
+ }
221
+
209
222
  var newEntity = migration.template(name, contextSnapshot.schema, cleanEntities);
223
+ if(!fs.existsSync(migBase)){
224
+ try{ fs.mkdirSync(migBase, { recursive: true }); }catch(_){ /* ignore */ }
225
+ }
210
226
  var migrationDate = Date.now();
211
- var outputFile = `${contextSnapshot.migrationFolder}/${migrationDate}_${name}_migration.js`
227
+ var outputFile = `${migBase}/${migrationDate}_${name}_migration.js`
212
228
  fs.writeFile(outputFile, newEntity, 'utf8', function (err) {
213
229
  if (err) return console.log("--- Error running cammand, re-run command add-migration ---- ", err);
214
230
  });
@@ -561,6 +577,67 @@ program.option('-V', 'output the version');
561
577
  });
562
578
 
563
579
 
580
+ program
581
+ .command('add-migration-all <name>')
582
+ .alias('ama')
583
+ .description('Create a migration with the given name for all detected contexts')
584
+ .action(function(name){
585
+ var executedLocation = process.cwd();
586
+ try{
587
+ var snapshotFiles = globSearch.sync('**/*_contextSnapShot.json', { cwd: executedLocation, dot: true, windowsPathsNoEscape: true, nocase: true });
588
+ if(!(snapshotFiles && snapshotFiles.length)){
589
+ console.log('No context snapshots found. Run enable-migrations-all first.');
590
+ return;
591
+ }
592
+ var created = 0;
593
+ for(const snapRel of snapshotFiles){
594
+ try{
595
+ const snapFile = path.resolve(executedLocation, snapRel);
596
+ let cs;
597
+ try{ cs = require(snapFile); }catch(_){ continue; }
598
+ const snapDir = path.dirname(snapFile);
599
+ const contextAbs = path.resolve(snapDir, cs.contextLocation || '');
600
+ const migBase = path.resolve(snapDir, cs.migrationFolder || '.');
601
+ // Load context
602
+ let ContextCtor;
603
+ try{ ContextCtor = require(contextAbs); }catch(_){
604
+ console.log(`Skipping: cannot load Context at '${contextAbs}'.`);
605
+ continue;
606
+ }
607
+ let contextInstance;
608
+ try{ contextInstance = new ContextCtor(); }catch(_){
609
+ console.log(`Skipping: failed to construct Context from '${contextAbs}'.`);
610
+ continue;
611
+ }
612
+ var migration = new Migration();
613
+ var cleanEntities = migration.cleanEntities(contextInstance.__entities);
614
+ // If no changes, skip with message
615
+ const has = migration.hasChanges(cs.schema || [], cleanEntities || []);
616
+ if(!has){
617
+ console.log(`No changes detected for ${path.basename(contextAbs)}. Skipping.`);
618
+ continue;
619
+ }
620
+ var newEntity = migration.template(name, cs.schema, cleanEntities);
621
+ if(!fs.existsSync(migBase)){
622
+ try{ fs.mkdirSync(migBase, { recursive: true }); }catch(_){ /* ignore */ }
623
+ }
624
+ var migrationDate = Date.now();
625
+ var outputFile = path.join(migBase, `${migrationDate}_${name}_migration.js`);
626
+ fs.writeFileSync(outputFile, newEntity, 'utf8');
627
+ console.log(`Created migration '${path.basename(outputFile)}' for ${path.basename(contextAbs)}`);
628
+ created++;
629
+ }catch(err){
630
+ console.log('Skipping snapshot due to error: ', err);
631
+ }
632
+ }
633
+ if(created === 0){
634
+ console.log('No migrations created.');
635
+ }
636
+ }catch(e){
637
+ console.log('Error - Cannot create migrations for all contexts ', e);
638
+ }
639
+ });
640
+
564
641
  program
565
642
  .command('update-database-all')
566
643
  .alias('uda')
@@ -580,20 +657,21 @@ program.option('-V', 'output the version');
580
657
  const snapFile = path.resolve(executedLocation, snapRel);
581
658
  let cs;
582
659
  try{ cs = require(snapFile); }catch(_){ continue; }
660
+ const snapDir = path.dirname(snapFile);
661
+ const contextAbs = path.resolve(snapDir, cs.contextLocation || '');
662
+ let migBase = path.resolve(snapDir, cs.migrationFolder || '.');
583
663
  const nameFromPath = path.basename(snapFile).replace(/_contextSnapShot\.json$/i, '').toLowerCase();
584
- const ctxName = (cs && cs.contextLocation)
585
- ? path.basename(cs.contextLocation).replace(/\.js$/i, '').toLowerCase()
586
- : nameFromPath;
664
+ const ctxName = contextAbs ? path.basename(contextAbs).replace(/\.js$/i, '').toLowerCase() : nameFromPath;
587
665
  // Find migrations in snapshot's migrationFolder; fallback to <ContextDir>/db/migrations
588
- let migRel = globSearch.sync('**/*_migration.js', { cwd: cs.migrationFolder, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
666
+ let migRel = globSearch.sync('**/*_migration.js', { cwd: migBase, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
589
667
  if(!(migRel && migRel.length)){
590
- const defaultFolder = path.join(path.dirname(cs.contextLocation || snapFile), 'db', 'migrations');
668
+ const defaultFolder = path.join(path.dirname(contextAbs || snapFile), 'db', 'migrations');
591
669
  migRel = globSearch.sync('**/*_migration.js', { cwd: defaultFolder, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
592
- if(migRel && migRel.length){ cs.migrationFolder = defaultFolder; }
670
+ if(migRel && migRel.length){ migBase = defaultFolder; }
593
671
  }
594
- const migs = migRel.map(f => path.resolve(cs.migrationFolder, f));
672
+ const migs = migRel.map(f => path.resolve(migBase, f));
595
673
  if(!groups[ctxName]) groups[ctxName] = [];
596
- groups[ctxName].push({ snapFile, cs, ctxName, migs });
674
+ groups[ctxName].push({ snapFile, snapDir, cs, ctxName, migs, contextAbs, migBase });
597
675
  }
598
676
 
599
677
  var migration = new Migration();
@@ -614,8 +692,8 @@ program.option('-V', 'output the version');
614
692
  var mFile = mFiles[mFiles.length - 1];
615
693
 
616
694
  var ContextCtor;
617
- try{ ContextCtor = require(entry.cs.contextLocation); }catch(_){
618
- console.log(`Skipping ${entry.ctxName}: cannot load Context at '${entry.cs.contextLocation}'.`);
695
+ try{ ContextCtor = require(entry.contextAbs); }catch(_){
696
+ console.log(`Skipping ${entry.ctxName}: cannot load Context at '${entry.contextAbs}'.`);
619
697
  continue;
620
698
  }
621
699
  var contextInstance;
@@ -629,7 +707,7 @@ program.option('-V', 'output the version');
629
707
  var tableObj = migration.buildUpObject(entry.cs.schema, cleanEntities);
630
708
  newMigrationProjectInstance.up(tableObj);
631
709
  var snap = {
632
- file : entry.cs.contextLocation,
710
+ file : entry.contextAbs,
633
711
  executedLocation : executedLocation,
634
712
  context : contextInstance,
635
713
  contextEntities : cleanEntities,
@@ -646,6 +724,58 @@ program.option('-V', 'output the version');
646
724
  }
647
725
  });
648
726
 
727
+ program
728
+ .command('enable-migrations-all')
729
+ .alias('ema')
730
+ .description('Enable migrations for all detected MasterRecord Context files')
731
+ .action(function(){
732
+ var executedLocation = process.cwd();
733
+ try{
734
+ // Find candidate Context files
735
+ var candidates = globSearch.sync('**/*Context.js', { cwd: executedLocation, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
736
+ if(!(candidates && candidates.length)){
737
+ console.log('No Context files found.');
738
+ return;
739
+ }
740
+ var seen = new Set();
741
+ var enabled = 0;
742
+ var migration = new Migration();
743
+ for(const rel of candidates){
744
+ try{
745
+ const abs = path.resolve(executedLocation, rel);
746
+ // Skip node_modules
747
+ if(abs.indexOf('node_modules') !== -1){ continue; }
748
+ // Heuristic filter: file must look like a MasterRecord context
749
+ let text = '';
750
+ try{ text = fs.readFileSync(abs, 'utf8'); }catch(_){ continue; }
751
+ const looksLikeContext = /extends\s+masterrecord\.context/i.test(text) || /require\(['"]masterrecord['"]\)/i.test(text);
752
+ if(!looksLikeContext){ continue; }
753
+ const ctxName = path.basename(abs).replace(/\.js$/i,'');
754
+ const key = ctxName.toLowerCase();
755
+ if(seen.has(key)){ continue; }
756
+ seen.add(key);
757
+ // Create snapshot relative to the context file directory
758
+ var snap = {
759
+ file : abs,
760
+ executedLocation : executedLocation,
761
+ contextEntities : [],
762
+ contextFileName: key
763
+ };
764
+ migration.createSnapShot(snap);
765
+ console.log(`migrations enabled for ${ctxName}`);
766
+ enabled++;
767
+ }catch(err){
768
+ console.log('Skipping candidate due to error: ', err);
769
+ }
770
+ }
771
+ if(enabled === 0){
772
+ console.log('No eligible MasterRecord Contexts detected.');
773
+ }
774
+ }catch(e){
775
+ console.log('Error - Failed to enable migrations for all contexts ', e);
776
+ }
777
+ });
778
+
649
779
 
650
780
  program.parse(process.argv);
651
781
 
@@ -218,10 +218,14 @@ class Migrations{
218
218
  }
219
219
 
220
220
  const snapshotPath = path.join(migrationsDirectory, `${snap.contextFileName}_contextSnapShot.json`);
221
+ // Store relative paths (portable): values are relative to the snapshot file directory (migrationsDirectory)
222
+ const relContextLocation = path.relative(migrationsDirectory, snap.file);
223
+ const relMigrationFolder = '.'; // the snapshot sits inside migrationsDirectory
224
+ const relSnapshotLocation = path.basename(snapshotPath);
221
225
  var content = {
222
- contextLocation: snap.file,
223
- migrationFolder: migrationsDirectory,
224
- snapShotLocation: snapshotPath,
226
+ contextLocation: relContextLocation,
227
+ migrationFolder: relMigrationFolder,
228
+ snapShotLocation: relSnapshotLocation,
225
229
  schema : snap.contextEntities
226
230
  };
227
231
 
@@ -273,6 +277,22 @@ class Migrations{
273
277
  return tableObj;
274
278
  }
275
279
 
280
+ // Returns true if there are any changes between old and new schema
281
+ hasChanges(oldSchema, newSchema){
282
+ const tables = this.#buildMigrationObject(oldSchema, newSchema);
283
+ for(const t of tables){
284
+ if(!t) continue;
285
+ if((t.newTables && t.newTables.length) ||
286
+ (t.newColumns && t.newColumns.length) ||
287
+ (t.deletedColumns && t.deletedColumns.length) ||
288
+ (t.updatedColumns && t.updatedColumns.length) ||
289
+ (t.old === null) || (t.new === null)){
290
+ return true;
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+
276
296
  template(name, oldSchema, newSchema){
277
297
  var MT = new MigrationTemplate(name);
278
298
  var tables = this.#buildMigrationObject(oldSchema, newSchema);
@@ -317,7 +337,6 @@ class Migrations{
317
337
  return MT.get();
318
338
  }
319
339
 
320
-
321
340
  }
322
341
 
323
342
  module.exports = Migrations;
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "app-root-path": "^3.1.0",
10
10
  "better-sqlite3": "^12.4.1"
11
11
  },
12
- "version": "0.2.19",
12
+ "version": "0.2.21",
13
13
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
14
14
  "homepage": "https://github.com/Tailor/MasterRecord#readme",
15
15
  "repository": {
package/readme.md CHANGED
@@ -179,4 +179,106 @@ Notes:
179
179
  - For large SQLite tables, a rebuild copies data; consider maintenance windows.
180
180
  - Use `master=development masterrecord get-migrations AppContext` to inspect migration order.
181
181
 
182
+ ## Multi-context (multi-database) projects
183
+
184
+ When your project defines multiple Context files (e.g., `userContext.js`, `modelContext.js`, `mailContext.js`, `chatContext.js`) across different packages or feature directories, MasterRecord can auto-detect and operate on all of them.
185
+
186
+ ### New bulk commands
187
+
188
+ - enable-migrations-all (alias: ema)
189
+ - Scans the project for MasterRecord Context files (heuristic) and enables migrations for each by writing a portable snapshot next to the context at `<ContextDir>/db/migrations/<context>_contextSnapShot.json`.
190
+
191
+ - add-migration-all <Name> (alias: ama)
192
+ - Creates a migration named `<Name>` (e.g., `Init`) for every detected context that has a snapshot. Migrations are written into each context’s own migrations folder.
193
+
194
+ - update-database-all (alias: uda)
195
+ - Applies the latest migration for every detected context with migrations.
196
+
197
+ - update-database-down <ContextName> (alias: udd)
198
+ - Runs the latest migration’s `down()` for the specified context.
199
+
200
+ - update-database-target <migrationFileName> (alias: udt)
201
+ - Rolls back migrations newer than the given migration file within that context’s migrations folder.
202
+
203
+ - ensure-database <ContextName> (alias: ed)
204
+ - For MySQL contexts, ensures the database exists (like EF’s `Database.EnsureCreated`). Auto-detects connection info from your Context env settings.
205
+
206
+ ### Portable snapshots (no hardcoded absolute paths)
207
+
208
+ Snapshots are written with relative paths, so moving/renaming the project root does not break CLI resolution:
209
+ - `contextLocation`: path from the migrations folder to the Context file
210
+ - `migrationFolder`: `.` (the snapshot resides in the migrations folder)
211
+ - `snapShotLocation`: the snapshot filename
212
+
213
+ ### Typical flow for multiple contexts
214
+
215
+ 1) Enable migrations everywhere:
216
+ ```bash
217
+ # macOS/Linux
218
+ master=development masterrecord enable-migrations-all
219
+
220
+ # Windows PowerShell
221
+ $env:master = 'development'
222
+ masterrecord enable-migrations-all
223
+ ```
224
+
225
+ 2) Create an initial migration for all contexts:
226
+ ```bash
227
+ # macOS/Linux
228
+ master=development masterrecord add-migration-all Init
229
+
230
+ # Windows PowerShell
231
+ $env:master = 'development'
232
+ masterrecord add-migration-all Init
233
+ ```
234
+
235
+ 3) Apply migrations everywhere:
236
+ ```bash
237
+ # macOS/Linux
238
+ master=development masterrecord update-database-all
239
+
240
+ # Windows PowerShell
241
+ $env:master = 'development'
242
+ masterrecord update-database-all
243
+ ```
244
+
245
+ 4) Inspect migrations for a specific context:
246
+ ```bash
247
+ # macOS/Linux
248
+ master=development masterrecord get-migrations userContext
249
+
250
+ # Windows PowerShell
251
+ $env:master = 'development'
252
+ masterrecord get-migrations userContext
253
+ ```
254
+
255
+ 5) Roll back latest for a specific context:
256
+ ```bash
257
+ # macOS/Linux
258
+ master=development masterrecord update-database-down userContext
259
+
260
+ # Windows PowerShell
261
+ $env:master = 'development'
262
+ masterrecord update-database-down userContext
263
+ ```
264
+
265
+ ### Environment selection (cross-platform)
266
+ - macOS/Linux prefix: `master=development ...` or `NODE_ENV=development ...`
267
+ - Windows PowerShell:
268
+ ```powershell
269
+ $env:master = 'development'
270
+ masterrecord update-database-all
271
+ ```
272
+ - Windows cmd.exe:
273
+ ```cmd
274
+ set master=development && masterrecord update-database-all
275
+ ```
276
+
277
+ ### Notes and tips
278
+ - Each Context should define its own env settings and tables; `update-database-all` operates context-by-context so separate databases are handled cleanly.
279
+ - For SQLite contexts, the `connection` path will be created if the directory does not exist.
280
+ - For MySQL contexts, `ensure-database <ContextName>` can create the DB (permissions required) before migrations run.
281
+ - If you rename/move the project root, re-run `enable-migrations-all` or any single-context command once; snapshots use relative paths and will continue working.
282
+ - If `update-database-all` reports “no migration files found” for a context, run `get-migrations <ContextName>`. If empty, create a migration with `add-migration <Name> <ContextName>` or use `add-migration-all <Name>`.
283
+
182
284