masterrecord 0.2.19 → 0.2.20

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
@@ -561,6 +561,61 @@ program.option('-V', 'output the version');
561
561
  });
562
562
 
563
563
 
564
+ program
565
+ .command('add-migration-all <name>')
566
+ .alias('ama')
567
+ .description('Create a migration with the given name for all detected contexts')
568
+ .action(function(name){
569
+ var executedLocation = process.cwd();
570
+ try{
571
+ var snapshotFiles = globSearch.sync('**/*_contextSnapShot.json', { cwd: executedLocation, dot: true, windowsPathsNoEscape: true, nocase: true });
572
+ if(!(snapshotFiles && snapshotFiles.length)){
573
+ console.log('No context snapshots found. Run enable-migrations-all first.');
574
+ return;
575
+ }
576
+ var created = 0;
577
+ for(const snapRel of snapshotFiles){
578
+ try{
579
+ const snapFile = path.resolve(executedLocation, snapRel);
580
+ let cs;
581
+ try{ cs = require(snapFile); }catch(_){ continue; }
582
+ const snapDir = path.dirname(snapFile);
583
+ const contextAbs = path.resolve(snapDir, cs.contextLocation || '');
584
+ const migBase = path.resolve(snapDir, cs.migrationFolder || '.');
585
+ // Load context
586
+ let ContextCtor;
587
+ try{ ContextCtor = require(contextAbs); }catch(_){
588
+ console.log(`Skipping: cannot load Context at '${contextAbs}'.`);
589
+ continue;
590
+ }
591
+ let contextInstance;
592
+ try{ contextInstance = new ContextCtor(); }catch(_){
593
+ console.log(`Skipping: failed to construct Context from '${contextAbs}'.`);
594
+ continue;
595
+ }
596
+ var migration = new Migration();
597
+ var cleanEntities = migration.cleanEntities(contextInstance.__entities);
598
+ var newEntity = migration.template(name, cs.schema, cleanEntities);
599
+ if(!fs.existsSync(migBase)){
600
+ try{ fs.mkdirSync(migBase, { recursive: true }); }catch(_){ /* ignore */ }
601
+ }
602
+ var migrationDate = Date.now();
603
+ var outputFile = path.join(migBase, `${migrationDate}_${name}_migration.js`);
604
+ fs.writeFileSync(outputFile, newEntity, 'utf8');
605
+ console.log(`Created migration '${path.basename(outputFile)}' for ${path.basename(contextAbs)}`);
606
+ created++;
607
+ }catch(err){
608
+ console.log('Skipping snapshot due to error: ', err);
609
+ }
610
+ }
611
+ if(created === 0){
612
+ console.log('No migrations created.');
613
+ }
614
+ }catch(e){
615
+ console.log('Error - Cannot create migrations for all contexts ', e);
616
+ }
617
+ });
618
+
564
619
  program
565
620
  .command('update-database-all')
566
621
  .alias('uda')
@@ -580,20 +635,21 @@ program.option('-V', 'output the version');
580
635
  const snapFile = path.resolve(executedLocation, snapRel);
581
636
  let cs;
582
637
  try{ cs = require(snapFile); }catch(_){ continue; }
638
+ const snapDir = path.dirname(snapFile);
639
+ const contextAbs = path.resolve(snapDir, cs.contextLocation || '');
640
+ let migBase = path.resolve(snapDir, cs.migrationFolder || '.');
583
641
  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;
642
+ const ctxName = contextAbs ? path.basename(contextAbs).replace(/\.js$/i, '').toLowerCase() : nameFromPath;
587
643
  // 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 }) || [];
644
+ let migRel = globSearch.sync('**/*_migration.js', { cwd: migBase, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
589
645
  if(!(migRel && migRel.length)){
590
- const defaultFolder = path.join(path.dirname(cs.contextLocation || snapFile), 'db', 'migrations');
646
+ const defaultFolder = path.join(path.dirname(contextAbs || snapFile), 'db', 'migrations');
591
647
  migRel = globSearch.sync('**/*_migration.js', { cwd: defaultFolder, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
592
- if(migRel && migRel.length){ cs.migrationFolder = defaultFolder; }
648
+ if(migRel && migRel.length){ migBase = defaultFolder; }
593
649
  }
594
- const migs = migRel.map(f => path.resolve(cs.migrationFolder, f));
650
+ const migs = migRel.map(f => path.resolve(migBase, f));
595
651
  if(!groups[ctxName]) groups[ctxName] = [];
596
- groups[ctxName].push({ snapFile, cs, ctxName, migs });
652
+ groups[ctxName].push({ snapFile, snapDir, cs, ctxName, migs, contextAbs, migBase });
597
653
  }
598
654
 
599
655
  var migration = new Migration();
@@ -614,8 +670,8 @@ program.option('-V', 'output the version');
614
670
  var mFile = mFiles[mFiles.length - 1];
615
671
 
616
672
  var ContextCtor;
617
- try{ ContextCtor = require(entry.cs.contextLocation); }catch(_){
618
- console.log(`Skipping ${entry.ctxName}: cannot load Context at '${entry.cs.contextLocation}'.`);
673
+ try{ ContextCtor = require(entry.contextAbs); }catch(_){
674
+ console.log(`Skipping ${entry.ctxName}: cannot load Context at '${entry.contextAbs}'.`);
619
675
  continue;
620
676
  }
621
677
  var contextInstance;
@@ -629,7 +685,7 @@ program.option('-V', 'output the version');
629
685
  var tableObj = migration.buildUpObject(entry.cs.schema, cleanEntities);
630
686
  newMigrationProjectInstance.up(tableObj);
631
687
  var snap = {
632
- file : entry.cs.contextLocation,
688
+ file : entry.contextAbs,
633
689
  executedLocation : executedLocation,
634
690
  context : contextInstance,
635
691
  contextEntities : cleanEntities,
@@ -646,6 +702,58 @@ program.option('-V', 'output the version');
646
702
  }
647
703
  });
648
704
 
705
+ program
706
+ .command('enable-migrations-all')
707
+ .alias('ema')
708
+ .description('Enable migrations for all detected MasterRecord Context files')
709
+ .action(function(){
710
+ var executedLocation = process.cwd();
711
+ try{
712
+ // Find candidate Context files
713
+ var candidates = globSearch.sync('**/*Context.js', { cwd: executedLocation, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
714
+ if(!(candidates && candidates.length)){
715
+ console.log('No Context files found.');
716
+ return;
717
+ }
718
+ var seen = new Set();
719
+ var enabled = 0;
720
+ var migration = new Migration();
721
+ for(const rel of candidates){
722
+ try{
723
+ const abs = path.resolve(executedLocation, rel);
724
+ // Skip node_modules
725
+ if(abs.indexOf('node_modules') !== -1){ continue; }
726
+ // Heuristic filter: file must look like a MasterRecord context
727
+ let text = '';
728
+ try{ text = fs.readFileSync(abs, 'utf8'); }catch(_){ continue; }
729
+ const looksLikeContext = /extends\s+masterrecord\.context/i.test(text) || /require\(['"]masterrecord['"]\)/i.test(text);
730
+ if(!looksLikeContext){ continue; }
731
+ const ctxName = path.basename(abs).replace(/\.js$/i,'');
732
+ const key = ctxName.toLowerCase();
733
+ if(seen.has(key)){ continue; }
734
+ seen.add(key);
735
+ // Create snapshot relative to the context file directory
736
+ var snap = {
737
+ file : abs,
738
+ executedLocation : executedLocation,
739
+ contextEntities : [],
740
+ contextFileName: key
741
+ };
742
+ migration.createSnapShot(snap);
743
+ console.log(`migrations enabled for ${ctxName}`);
744
+ enabled++;
745
+ }catch(err){
746
+ console.log('Skipping candidate due to error: ', err);
747
+ }
748
+ }
749
+ if(enabled === 0){
750
+ console.log('No eligible MasterRecord Contexts detected.');
751
+ }
752
+ }catch(e){
753
+ console.log('Error - Failed to enable migrations for all contexts ', e);
754
+ }
755
+ });
756
+
649
757
 
650
758
  program.parse(process.argv);
651
759
 
@@ -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
 
@@ -317,7 +321,6 @@ class Migrations{
317
321
  return MT.get();
318
322
  }
319
323
 
320
-
321
324
  }
322
325
 
323
326
  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.20",
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