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 CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // version 0.0.6
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
@@ -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
- const sql = `INSERT INTO ${tableName} (${colList}) VALUES (${vals})`;
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
- console.error('MySQL execute skipped: connection not defined');
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
- console.error('MySQL execute skipped: connection not defined');
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);
@@ -1,4 +1,4 @@
1
- // version : 0.0.3
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
- console.error('MySQL connect error:', err && err.code ? err.code : err);
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
- const code = err && err.code ? err.code : '';
39
- if(code === 'ER_BAD_DB_ERROR' || code === 'ECONNREFUSED' || code === 'PROTOCOL_CONNECTION_LOST'){
40
- console.error('MySQL query skipped due to connection not defined');
41
- return null;
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
- if (this.connection) {
51
- this.connection.end();
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.9",
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": {