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 +145 -15
- package/Migrations/migrations.js +23 -4
- package/package.json +1 -1
- package/readme.md +102 -0
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(
|
|
201
|
+
ContextCtor = require(contextAbs);
|
|
197
202
|
}catch(_){
|
|
198
|
-
console.log(`Error - Cannot load Context file at '${
|
|
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 '${
|
|
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 = `${
|
|
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 =
|
|
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:
|
|
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(
|
|
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){
|
|
670
|
+
if(migRel && migRel.length){ migBase = defaultFolder; }
|
|
593
671
|
}
|
|
594
|
-
const migs = migRel.map(f => path.resolve(
|
|
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.
|
|
618
|
-
console.log(`Skipping ${entry.ctxName}: cannot load Context at '${entry.
|
|
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.
|
|
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
|
|
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
|
|
|
@@ -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.
|
|
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
|
|