masterrecord 0.2.17 → 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 +124 -10
- package/Migrations/migrations.js +7 -4
- package/package.json +1 -1
- package/readme.md +102 -0
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')
|
|
@@ -569,7 +624,7 @@ program.option('-V', 'output the version');
|
|
|
569
624
|
var executedLocation = process.cwd();
|
|
570
625
|
try{
|
|
571
626
|
// Find all context snapshots and run update per snapshot (avoids unrelated framework contexts)
|
|
572
|
-
var snapshotFiles = globSearch.sync('**/*_contextSnapShot.json', executedLocation);
|
|
627
|
+
var snapshotFiles = globSearch.sync('**/*_contextSnapShot.json', { cwd: executedLocation, dot: true, windowsPathsNoEscape: true, nocase: true });
|
|
573
628
|
if(!(snapshotFiles && snapshotFiles.length)){
|
|
574
629
|
console.log('No context snapshots found. Run enable-migrations for each context first.');
|
|
575
630
|
return;
|
|
@@ -580,14 +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 =
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
642
|
+
const ctxName = contextAbs ? path.basename(contextAbs).replace(/\.js$/i, '').toLowerCase() : nameFromPath;
|
|
643
|
+
// Find migrations in snapshot's migrationFolder; fallback to <ContextDir>/db/migrations
|
|
644
|
+
let migRel = globSearch.sync('**/*_migration.js', { cwd: migBase, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
|
|
645
|
+
if(!(migRel && migRel.length)){
|
|
646
|
+
const defaultFolder = path.join(path.dirname(contextAbs || snapFile), 'db', 'migrations');
|
|
647
|
+
migRel = globSearch.sync('**/*_migration.js', { cwd: defaultFolder, dot: true, windowsPathsNoEscape: true, nocase: true }) || [];
|
|
648
|
+
if(migRel && migRel.length){ migBase = defaultFolder; }
|
|
649
|
+
}
|
|
650
|
+
const migs = migRel.map(f => path.resolve(migBase, f));
|
|
589
651
|
if(!groups[ctxName]) groups[ctxName] = [];
|
|
590
|
-
groups[ctxName].push({ snapFile, cs, ctxName, migs });
|
|
652
|
+
groups[ctxName].push({ snapFile, snapDir, cs, ctxName, migs, contextAbs, migBase });
|
|
591
653
|
}
|
|
592
654
|
|
|
593
655
|
var migration = new Migration();
|
|
@@ -608,8 +670,8 @@ program.option('-V', 'output the version');
|
|
|
608
670
|
var mFile = mFiles[mFiles.length - 1];
|
|
609
671
|
|
|
610
672
|
var ContextCtor;
|
|
611
|
-
try{ ContextCtor = require(entry.
|
|
612
|
-
console.log(`Skipping ${entry.ctxName}: cannot load Context at '${entry.
|
|
673
|
+
try{ ContextCtor = require(entry.contextAbs); }catch(_){
|
|
674
|
+
console.log(`Skipping ${entry.ctxName}: cannot load Context at '${entry.contextAbs}'.`);
|
|
613
675
|
continue;
|
|
614
676
|
}
|
|
615
677
|
var contextInstance;
|
|
@@ -623,7 +685,7 @@ program.option('-V', 'output the version');
|
|
|
623
685
|
var tableObj = migration.buildUpObject(entry.cs.schema, cleanEntities);
|
|
624
686
|
newMigrationProjectInstance.up(tableObj);
|
|
625
687
|
var snap = {
|
|
626
|
-
file : entry.
|
|
688
|
+
file : entry.contextAbs,
|
|
627
689
|
executedLocation : executedLocation,
|
|
628
690
|
context : contextInstance,
|
|
629
691
|
contextEntities : cleanEntities,
|
|
@@ -640,6 +702,58 @@ program.option('-V', 'output the version');
|
|
|
640
702
|
}
|
|
641
703
|
});
|
|
642
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
|
+
|
|
643
757
|
|
|
644
758
|
program.parse(process.argv);
|
|
645
759
|
|
package/Migrations/migrations.js
CHANGED
|
@@ -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:
|
|
223
|
-
migrationFolder:
|
|
224
|
-
snapShotLocation:
|
|
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.
|
|
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
|
|