masterrecord 0.2.9 → 0.2.11
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 +151 -1
- package/Migrations/schema.js +47 -1
- package/mySQLEngine.js +12 -2
- package/mySQLSyncConnect.js +34 -9
- package/package.json +1 -1
package/Migrations/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// version 0.0.
|
|
3
|
+
// version 0.0.7
|
|
4
4
|
// https://docs.microsoft.com/en-us/ef/ef6/modeling/code-first/migrations/
|
|
5
5
|
// how to add environment variables on cli call example - master=development masterrecord add-migration auth authContext
|
|
6
6
|
|
|
@@ -74,6 +74,66 @@ program.option('-V', 'output the version');
|
|
|
74
74
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
program
|
|
78
|
+
.command('ensure-database <contextFileName>')
|
|
79
|
+
.alias('ed')
|
|
80
|
+
.description('Ensure the target database exists for the given context (MySQL)')
|
|
81
|
+
.action(function(contextFileName){
|
|
82
|
+
var executedLocation = process.cwd();
|
|
83
|
+
contextFileName = contextFileName.toLowerCase();
|
|
84
|
+
var migration = new Migration();
|
|
85
|
+
try{
|
|
86
|
+
var search = `${executedLocation}/**/*${contextFileName}_contextSnapShot.json`;
|
|
87
|
+
var files = globSearch.sync(search, executedLocation);
|
|
88
|
+
var file = files && files[0];
|
|
89
|
+
if(!file){
|
|
90
|
+
console.log(`Error - Cannot read or find Context snapshot '${contextFileName}_contextSnapShot.json' in '${executedLocation}'.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
var contextSnapshot;
|
|
94
|
+
try{
|
|
95
|
+
contextSnapshot = require(file);
|
|
96
|
+
}catch(_){
|
|
97
|
+
console.log(`Error - Cannot read context snapshot at '${file}'.`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// Find latest migration file (so we can use its class which extends schema)
|
|
101
|
+
var searchMigration = `${contextSnapshot.migrationFolder}/**/*_migration.js`;
|
|
102
|
+
var migrationFiles = globSearch.sync(searchMigration, contextSnapshot.migrationFolder);
|
|
103
|
+
if(!(migrationFiles && migrationFiles.length)){
|
|
104
|
+
console.log("Error - Cannot read or find migration file");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
var mFiles = migrationFiles.slice().sort(function(a, b){
|
|
108
|
+
return __getMigrationTimestamp(a) - __getMigrationTimestamp(b);
|
|
109
|
+
});
|
|
110
|
+
var mFile = mFiles[mFiles.length -1];
|
|
111
|
+
|
|
112
|
+
let ContextCtor;
|
|
113
|
+
try{
|
|
114
|
+
ContextCtor = require(contextSnapshot.contextLocation);
|
|
115
|
+
}catch(_){
|
|
116
|
+
console.log(`Error - Cannot load Context file at '${contextSnapshot.contextLocation}'.`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Use the migration class (extends schema) so createdatabase is available
|
|
121
|
+
var MigrationCtor = require(mFile);
|
|
122
|
+
var mig = new MigrationCtor(ContextCtor);
|
|
123
|
+
if(typeof mig.createdatabase === 'function'){
|
|
124
|
+
try{ mig.createdatabase(); }catch(_){ /* best-effort */ }
|
|
125
|
+
console.log('database ensured');
|
|
126
|
+
} else if(typeof mig.createDatabase === 'function'){
|
|
127
|
+
try{ mig.createDatabase(); }catch(_){ }
|
|
128
|
+
console.log('database ensured');
|
|
129
|
+
} else {
|
|
130
|
+
console.log('Error - Migration class missing createDatabase method');
|
|
131
|
+
}
|
|
132
|
+
}catch(e){
|
|
133
|
+
console.log('Error - Cannot read or find file ', e);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
77
137
|
// program
|
|
78
138
|
// .command('create-database <contextFileName> <dbName>')
|
|
79
139
|
// .alias('cd')
|
|
@@ -509,6 +569,96 @@ program.option('-V', 'output the version');
|
|
|
509
569
|
});
|
|
510
570
|
|
|
511
571
|
|
|
572
|
+
program
|
|
573
|
+
.command('update-database-all')
|
|
574
|
+
.alias('uda')
|
|
575
|
+
.description('Scan the project for *Context.js files and run update-database on each')
|
|
576
|
+
.action(function(){
|
|
577
|
+
var executedLocation = process.cwd();
|
|
578
|
+
try{
|
|
579
|
+
// Find all Context files (e.g., chatContext.js)
|
|
580
|
+
var allFiles = globSearch.sync(`${executedLocation}/**/*Context.js`, executedLocation);
|
|
581
|
+
if(!(allFiles && allFiles.length)){
|
|
582
|
+
console.log('No Context files found.');
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
// Deduplicate by basename (case-insensitive)
|
|
586
|
+
var seen = new Set();
|
|
587
|
+
var contextFiles = [];
|
|
588
|
+
for(const f of allFiles){
|
|
589
|
+
var name = path.basename(f).toLowerCase();
|
|
590
|
+
if(!seen.has(name)){
|
|
591
|
+
seen.add(name);
|
|
592
|
+
contextFiles.push(f);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
var migration = new Migration();
|
|
596
|
+
// Process each context independently
|
|
597
|
+
for(const ctxFile of contextFiles){
|
|
598
|
+
try{
|
|
599
|
+
var base = path.basename(ctxFile);
|
|
600
|
+
var ctxName = base.replace(/\.js$/i, '').toLowerCase();
|
|
601
|
+
// Locate snapshot for this context
|
|
602
|
+
var snapMatches = globSearch.sync(`${executedLocation}/**/*${ctxName}_contextSnapShot.json`, executedLocation);
|
|
603
|
+
var snapFile = snapMatches && snapMatches[0];
|
|
604
|
+
if(!snapFile){
|
|
605
|
+
console.log(`Skipping ${ctxName}: snapshot not found. Run 'masterrecord enable-migrations ${ctxName}'.`);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
var contextSnapshot;
|
|
609
|
+
try{ contextSnapshot = require(snapFile); }catch(_){
|
|
610
|
+
console.log(`Skipping ${ctxName}: cannot read context snapshot at '${snapFile}'.`);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
// Get migration files for this snapshot folder
|
|
614
|
+
var searchMigration = `${contextSnapshot.migrationFolder}/**/*_migration.js`;
|
|
615
|
+
var migrationFiles = globSearch.sync(searchMigration, contextSnapshot.migrationFolder);
|
|
616
|
+
if(!(migrationFiles && migrationFiles.length)){
|
|
617
|
+
console.log(`Skipping ${ctxName}: no migration files found.`);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
// Pick latest
|
|
621
|
+
var mFiles = migrationFiles.slice().sort(function(a, b){
|
|
622
|
+
return __getMigrationTimestamp(a) - __getMigrationTimestamp(b);
|
|
623
|
+
});
|
|
624
|
+
var mFile = mFiles[mFiles.length - 1];
|
|
625
|
+
|
|
626
|
+
var ContextCtor;
|
|
627
|
+
try{ ContextCtor = require(contextSnapshot.contextLocation); }catch(_){
|
|
628
|
+
console.log(`Skipping ${ctxName}: cannot load Context at '${contextSnapshot.contextLocation}'.`);
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
var contextInstance;
|
|
632
|
+
try{ contextInstance = new ContextCtor(); }catch(_){
|
|
633
|
+
console.log(`Skipping ${ctxName}: failed to construct Context.`);
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
var migrationProjectFile = require(mFile);
|
|
637
|
+
var newMigrationProjectInstance = new migrationProjectFile(ContextCtor);
|
|
638
|
+
var cleanEntities = migration.cleanEntities(contextInstance.__entities);
|
|
639
|
+
var tableObj = migration.buildUpObject(contextSnapshot.schema, cleanEntities);
|
|
640
|
+
// Run up for this context
|
|
641
|
+
newMigrationProjectInstance.up(tableObj);
|
|
642
|
+
// Update snapshot
|
|
643
|
+
var snap = {
|
|
644
|
+
file : contextSnapshot.contextLocation,
|
|
645
|
+
executedLocation : executedLocation,
|
|
646
|
+
context : contextInstance,
|
|
647
|
+
contextEntities : cleanEntities,
|
|
648
|
+
contextFileName: ctxName
|
|
649
|
+
}
|
|
650
|
+
migration.createSnapShot(snap);
|
|
651
|
+
console.log(`database updated for ${ctxName}`);
|
|
652
|
+
}catch(errCtx){
|
|
653
|
+
console.log('Error updating context: ', errCtx);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}catch(e){
|
|
657
|
+
console.log('Error - Cannot read or find file ', e);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
|
|
512
662
|
program.parse(process.argv);
|
|
513
663
|
|
|
514
664
|
// Handle manual '-V' alias
|
package/Migrations/schema.js
CHANGED
|
@@ -3,6 +3,7 @@ class schema{
|
|
|
3
3
|
|
|
4
4
|
constructor(context){
|
|
5
5
|
this.context = new context();
|
|
6
|
+
this._dbEnsured = false;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
|
|
@@ -10,6 +11,10 @@ class schema{
|
|
|
10
11
|
if(table){
|
|
11
12
|
this.fullTable = table.___table;
|
|
12
13
|
}
|
|
14
|
+
// Ensure backing database exists for MySQL before running any DDL
|
|
15
|
+
if(this.context && this.context.isMySQL && this._dbEnsured !== true){
|
|
16
|
+
try{ this.createDatabase(); }catch(_){ /* best-effort */ }
|
|
17
|
+
}
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
// create obj to convert into create sql
|
|
@@ -253,6 +258,39 @@ class schema{
|
|
|
253
258
|
}
|
|
254
259
|
}
|
|
255
260
|
|
|
261
|
+
// EnsureCreated equivalent for MySQL: create DB if missing
|
|
262
|
+
createDatabase(){
|
|
263
|
+
try{
|
|
264
|
+
if(!(this.context && this.context.isMySQL)){ return; }
|
|
265
|
+
const MySQLClient = require('masterrecord/mySQLSyncConnect');
|
|
266
|
+
const client = this.context.db; // main client (may not be connected yet)
|
|
267
|
+
if(!client || !client.config || !client.config.database){ return; }
|
|
268
|
+
const dbName = client.config.database;
|
|
269
|
+
// Build server-level connection (no database)
|
|
270
|
+
const baseConfig = { ...client.config };
|
|
271
|
+
delete baseConfig.database;
|
|
272
|
+
const admin = new MySQLClient(baseConfig);
|
|
273
|
+
admin.connect();
|
|
274
|
+
if(!admin.connection){ return; }
|
|
275
|
+
const check = admin.query(`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '${dbName}'`);
|
|
276
|
+
const exists = Array.isArray(check) ? check.length > 0 : !!check?.length;
|
|
277
|
+
if(!exists){
|
|
278
|
+
// Create with sensible defaults
|
|
279
|
+
admin.query(`CREATE DATABASE \`${dbName}\` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
|
|
280
|
+
}
|
|
281
|
+
admin.close();
|
|
282
|
+
this._dbEnsured = true;
|
|
283
|
+
}catch(err){
|
|
284
|
+
// Non-fatal: migrations may still proceed if DB already exists or permissions blocked
|
|
285
|
+
try{ console.error(err); }catch(_){ }
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Alias for consistency with user expectation
|
|
290
|
+
createdatabase(table){
|
|
291
|
+
return this.createDatabase(table);
|
|
292
|
+
}
|
|
293
|
+
|
|
256
294
|
|
|
257
295
|
//"dbo.People", "Location"
|
|
258
296
|
alterColumn(table){
|
|
@@ -318,7 +356,15 @@ class schema{
|
|
|
318
356
|
const esc = String(v).replace(/'/g, "''");
|
|
319
357
|
return `'${esc}'`;
|
|
320
358
|
}).join(", ");
|
|
321
|
-
|
|
359
|
+
// Idempotent seed: ignore duplicates on unique indexes
|
|
360
|
+
let sql;
|
|
361
|
+
if(this.context.isSQLite){
|
|
362
|
+
sql = `INSERT OR IGNORE INTO ${tableName} (${colList}) VALUES (${vals})`;
|
|
363
|
+
} else if(this.context.isMySQL){
|
|
364
|
+
sql = `INSERT IGNORE INTO ${tableName} (${colList}) VALUES (${vals})`;
|
|
365
|
+
} else {
|
|
366
|
+
sql = `INSERT INTO ${tableName} (${colList}) VALUES (${vals})`;
|
|
367
|
+
}
|
|
322
368
|
this.context._execute(sql);
|
|
323
369
|
}
|
|
324
370
|
}
|
package/mySQLEngine.js
CHANGED
|
@@ -574,14 +574,24 @@ class SQLLiteEngine {
|
|
|
574
574
|
this.db.connect(this.db);
|
|
575
575
|
const res = this.db.query(query);
|
|
576
576
|
if(res === null){
|
|
577
|
-
|
|
577
|
+
const dbName = (this.db && this.db.config && this.db.config.database) ? this.db.config.database : '(unknown)';
|
|
578
|
+
if(this.db && this.db.lastErrorCode === 'ER_BAD_DB_ERROR'){
|
|
579
|
+
console.error(`MySQL execute skipped: database '${dbName}' does not exist`);
|
|
580
|
+
}else{
|
|
581
|
+
console.error('MySQL execute skipped: connection not defined');
|
|
582
|
+
}
|
|
578
583
|
return null;
|
|
579
584
|
}
|
|
580
585
|
return res;
|
|
581
586
|
}catch(err){
|
|
582
587
|
const code = err && err.code ? err.code : '';
|
|
583
588
|
if(code === 'ER_BAD_DB_ERROR' || code === 'ECONNREFUSED' || code === 'PROTOCOL_CONNECTION_LOST'){
|
|
584
|
-
|
|
589
|
+
const dbName = (this.db && this.db.config && this.db.config.database) ? this.db.config.database : '(unknown)';
|
|
590
|
+
if(code === 'ER_BAD_DB_ERROR'){
|
|
591
|
+
console.error(`MySQL execute skipped: database '${dbName}' does not exist`);
|
|
592
|
+
} else {
|
|
593
|
+
console.error('MySQL execute skipped: connection not defined');
|
|
594
|
+
}
|
|
585
595
|
return null;
|
|
586
596
|
}
|
|
587
597
|
console.error(err);
|
package/mySQLSyncConnect.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// version : 0.0.
|
|
1
|
+
// version : 0.0.4
|
|
2
2
|
|
|
3
3
|
var MySql = require('sync-mysql2');
|
|
4
4
|
|
|
@@ -9,6 +9,8 @@ class MySQLClient {
|
|
|
9
9
|
delete this.config.type;
|
|
10
10
|
}
|
|
11
11
|
this.connection = null;
|
|
12
|
+
this.lastErrorCode = null;
|
|
13
|
+
this.lastErrorMessage = null;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
connect() {
|
|
@@ -19,7 +21,14 @@ class MySQLClient {
|
|
|
19
21
|
}
|
|
20
22
|
} catch (err) {
|
|
21
23
|
// Swallow connection errors and leave connection undefined so callers can react gracefully
|
|
22
|
-
|
|
24
|
+
this.lastErrorCode = err && err.code ? err.code : null;
|
|
25
|
+
this.lastErrorMessage = err && err.message ? err.message : String(err);
|
|
26
|
+
if(this.lastErrorCode === 'ER_BAD_DB_ERROR'){
|
|
27
|
+
const dbName = this.config && this.config.database ? this.config.database : '(unknown)';
|
|
28
|
+
console.error(`MySQL connect error: database '${dbName}' does not exist`);
|
|
29
|
+
}else{
|
|
30
|
+
console.error('MySQL connect error:', this.lastErrorCode || this.lastErrorMessage);
|
|
31
|
+
}
|
|
23
32
|
this.connection = null;
|
|
24
33
|
return null;
|
|
25
34
|
}
|
|
@@ -35,20 +44,36 @@ class MySQLClient {
|
|
|
35
44
|
return jj;
|
|
36
45
|
} catch (err) {
|
|
37
46
|
// If the underlying driver surfaces bad DB or network errors, normalize to null
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
this.lastErrorCode = err && err.code ? err.code : null;
|
|
48
|
+
this.lastErrorMessage = err && err.message ? err.message : String(err);
|
|
49
|
+
if(this.lastErrorCode === 'ER_BAD_DB_ERROR'){
|
|
50
|
+
const dbName = this.config && this.config.database ? this.config.database : '(unknown)';
|
|
51
|
+
console.error(`MySQL error: database '${dbName}' does not exist`);
|
|
52
|
+
} else if(this.lastErrorCode === 'ECONNREFUSED' || this.lastErrorCode === 'PROTOCOL_CONNECTION_LOST'){
|
|
53
|
+
console.error('MySQL connection error:', this.lastErrorCode);
|
|
54
|
+
} else {
|
|
55
|
+
console.error(err);
|
|
42
56
|
}
|
|
43
|
-
console.error(err);
|
|
44
57
|
return null;
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
}
|
|
48
61
|
|
|
49
62
|
close() {
|
|
50
|
-
|
|
51
|
-
this.connection
|
|
63
|
+
try {
|
|
64
|
+
if (!this.connection) { return; }
|
|
65
|
+
if (typeof this.connection.finishAll === 'function') {
|
|
66
|
+
// Drain any pending RPC calls
|
|
67
|
+
try { this.connection.finishAll(); } catch(_) { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
if (typeof this.connection.end === 'function') {
|
|
70
|
+
try { this.connection.end(); } catch(_) { /* ignore */ }
|
|
71
|
+
} else if (typeof this.connection.close === 'function') {
|
|
72
|
+
try { this.connection.close(); } catch(_) { /* ignore */ }
|
|
73
|
+
} else if (typeof this.connection.dispose === 'function') {
|
|
74
|
+
try { this.connection.dispose(); } catch(_) { /* ignore */ }
|
|
75
|
+
}
|
|
76
|
+
} finally {
|
|
52
77
|
this.connection = null;
|
|
53
78
|
}
|
|
54
79
|
}
|
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.11",
|
|
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": {
|