masterrecord 0.3.48 → 0.3.50

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.
@@ -63,7 +63,8 @@
63
63
  "Bash(npx mocha:*)",
64
64
  "Bash(masterrecord add-migration:*)",
65
65
  "Bash(masterrecord:*)",
66
- "Bash(git -C /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training log --oneline --all -- *MASTERRECORD_ISSUE*)"
66
+ "Bash(git -C /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training log --oneline --all -- *MASTERRECORD_ISSUE*)",
67
+ "Bash(npm ls -g masterrecord 2>&1 | head -5)"
67
68
  ],
68
69
  "deny": [],
69
70
  "ask": []
@@ -201,6 +201,9 @@ class Migrations{
201
201
 
202
202
  #findNewIndexes(tables){
203
203
  tables.forEach(function (item, index) {
204
+ // Skip new tables — createTable() in schema.js already creates their indexes
205
+ if(item.newTables && item.newTables.length > 0) return;
206
+
204
207
  if(item.new && item.old){
205
208
  Object.keys(item.new).forEach(function (key) {
206
209
  if(typeof item.new[key] === "object" && item.new[key].indexes){
@@ -289,6 +292,9 @@ class Migrations{
289
292
 
290
293
  #findNewCompositeIndexes(tables) {
291
294
  tables.forEach(function (item, index) {
295
+ // Skip new tables — createTable() in schema.js already creates their composite indexes
296
+ if(item.newTables && item.newTables.length > 0) return;
297
+
292
298
  if (item.new && item.old) {
293
299
  const newComposite = item.new.__compositeIndexes || [];
294
300
  const oldComposite = item.old.__compositeIndexes || [];
@@ -307,17 +313,6 @@ class Migrations{
307
313
  });
308
314
  }
309
315
  });
310
- } else if (item.new && !item.old) {
311
- // New table - all composite indexes are new
312
- const composites = item.new.__compositeIndexes || [];
313
- composites.forEach(function(idx) {
314
- item.newCompositeIndexes.push({
315
- tableName: item.name,
316
- columns: idx.columns,
317
- indexName: idx.name,
318
- unique: idx.unique
319
- });
320
- });
321
316
  }
322
317
  });
323
318
  return tables;
package/context.js CHANGED
@@ -32,6 +32,19 @@ const MySQLAsyncClient = require('masterrecord/mySQLConnect');
32
32
  const PostgresClient = require('masterrecord/postgresSyncConnect');
33
33
  const QueryCache = require('./Cache/QueryCache');
34
34
 
35
+ // ============================================================================
36
+ // GLOBAL POOL REGISTRY - One pool per database, shared across all contexts
37
+ // ============================================================================
38
+
39
+ const _pools = global.__MR_POOLS__ || (global.__MR_POOLS__ = new Map());
40
+
41
+ function _poolKey(type, cfg) {
42
+ if (type === 'sqlite') return `sqlite:${cfg.completeConnection || cfg.connection}`;
43
+ const host = cfg.host || 'localhost';
44
+ const port = cfg.port || (type === 'mysql' ? 3306 : 5432);
45
+ return `${type}:${cfg.user}@${host}:${port}/${cfg.database}`;
46
+ }
47
+
35
48
  // ============================================================================
36
49
  // CONSTANTS - Extract all magic numbers for maintainability
37
50
  // ============================================================================
@@ -329,6 +342,16 @@ class context {
329
342
  );
330
343
  }
331
344
 
345
+ const key = _poolKey('mysql', env);
346
+ if (_pools.has(key)) {
347
+ const cached = _pools.get(key);
348
+ cached.refCount++;
349
+ this._SQLEngine = cached.engine;
350
+ this.isMySQL = true;
351
+ console.log(`[MySQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
352
+ return cached.client;
353
+ }
354
+
332
355
  console.log('[MySQL] Initializing async connection pool...');
333
356
  const client = new MySQLAsyncClient(env);
334
357
  await client.connect();
@@ -339,6 +362,7 @@ class context {
339
362
  this._SQLEngine.__name = sqlName;
340
363
  this.isMySQL = true;
341
364
 
365
+ _pools.set(key, { client, engine: this._SQLEngine, refCount: 1, dbType: 'mysql' });
342
366
  console.log('[MySQL] Connection pool ready');
343
367
  return client;
344
368
  } catch (error) {
@@ -401,12 +425,24 @@ class context {
401
425
  );
402
426
  }
403
427
 
428
+ const key = _poolKey('postgres', env);
429
+ if (_pools.has(key)) {
430
+ const cached = _pools.get(key);
431
+ cached.refCount++;
432
+ this._SQLEngine = cached.engine;
433
+ this._SQLEngine.__name = sqlName;
434
+ console.log(`[PostgreSQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
435
+ return cached.pool;
436
+ }
437
+
404
438
  const connection = new PostgresClient();
405
439
  await connection.connect(env);
406
440
  this._SQLEngine = connection.getEngine();
407
441
  this._SQLEngine.__name = sqlName;
408
442
 
409
- return connection.getPool();
443
+ const pool = connection.getPool();
444
+ _pools.set(key, { pool, engine: this._SQLEngine, client: connection, refCount: 1, dbType: 'postgres' });
445
+ return pool;
410
446
  } catch (error) {
411
447
  // Preserve original error if it's already a ContextError
412
448
  if (error instanceof ContextError) {
@@ -700,8 +736,20 @@ class context {
700
736
  }
701
737
 
702
738
  const sqliteOptions = { ...options, completeConnection: dbPath };
739
+
740
+ const sqliteKey = _poolKey('sqlite', sqliteOptions);
741
+ if (_pools.has(sqliteKey)) {
742
+ const cached = _pools.get(sqliteKey);
743
+ cached.refCount++;
744
+ this.db = cached.db;
745
+ this._SQLEngine = cached.engine;
746
+ return this;
747
+ }
748
+
703
749
  this.db = this.__SQLiteInit(sqliteOptions, 'better-sqlite3');
704
750
  this._SQLEngine.setDB(this.db, 'better-sqlite3');
751
+
752
+ _pools.set(sqliteKey, { db: this.db, engine: this._SQLEngine, refCount: 1, dbType: 'sqlite' });
705
753
  return this;
706
754
  }
707
755
 
@@ -843,8 +891,19 @@ class context {
843
891
  fs.mkdirSync(dbDirectory, { recursive: true });
844
892
  }
845
893
 
894
+ const sqliteKey = _poolKey('sqlite', options);
895
+ if (_pools.has(sqliteKey)) {
896
+ const cached = _pools.get(sqliteKey);
897
+ cached.refCount++;
898
+ this.db = cached.db;
899
+ this._SQLEngine = cached.engine;
900
+ return this;
901
+ }
902
+
846
903
  this.db = this.__SQLiteInit(options, 'better-sqlite3');
847
904
  this._SQLEngine.setDB(this.db, 'better-sqlite3');
905
+
906
+ _pools.set(sqliteKey, { db: this.db, engine: this._SQLEngine, refCount: 1, dbType: 'sqlite' });
848
907
  return this;
849
908
  } catch (error) {
850
909
  // Preserve original error if it's already a ContextError
@@ -1927,11 +1986,72 @@ class context {
1927
1986
  * db.close(); // Close connections
1928
1987
  */
1929
1988
  async close() {
1989
+ // Find this instance's pool in the registry and decrement
1990
+ for (const [key, entry] of _pools) {
1991
+ if (entry.engine === this._SQLEngine) {
1992
+ entry.refCount--;
1993
+ if (entry.refCount <= 0) {
1994
+ _pools.delete(key);
1995
+ if (this._SQLEngine && typeof this._SQLEngine.close === 'function') {
1996
+ await this._SQLEngine.close();
1997
+ }
1998
+ }
1999
+ this._SQLEngine = null;
2000
+ this.db = null;
2001
+ return;
2002
+ }
2003
+ }
2004
+
2005
+ // Fallback (not in registry)
1930
2006
  if (this._SQLEngine && typeof this._SQLEngine.close === 'function') {
1931
2007
  return await this._SQLEngine.close();
1932
2008
  }
1933
2009
  }
1934
2010
 
2011
+ /**
2012
+ * Close all shared connection pools, regardless of reference count.
2013
+ * Useful for graceful shutdown or test cleanup.
2014
+ *
2015
+ * @static
2016
+ * @async
2017
+ * @returns {Promise<void>}
2018
+ *
2019
+ * @example
2020
+ * // Graceful shutdown
2021
+ * process.on('SIGTERM', async () => {
2022
+ * await context.closeAll();
2023
+ * process.exit(0);
2024
+ * });
2025
+ */
2026
+ static async closeAll() {
2027
+ for (const [key, entry] of _pools) {
2028
+ try {
2029
+ if (entry.engine && typeof entry.engine.close === 'function') {
2030
+ await entry.engine.close();
2031
+ }
2032
+ } catch (err) {
2033
+ console.error('[MasterRecord] Error closing pool:', err.message);
2034
+ }
2035
+ }
2036
+ _pools.clear();
2037
+ }
2038
+
2039
+ /**
2040
+ * Get statistics about active connection pools.
2041
+ *
2042
+ * @static
2043
+ * @returns {Array<{key: string, dbType: string, refCount: number}>}
2044
+ *
2045
+ * @example
2046
+ * console.log(context.getPoolStats());
2047
+ * // [{ key: 'mysql:root@localhost:3306/mydb', dbType: 'mysql', refCount: 3 }]
2048
+ */
2049
+ static getPoolStats() {
2050
+ return Array.from(_pools.entries()).map(([key, entry]) => ({
2051
+ key, dbType: entry.dbType, refCount: entry.refCount
2052
+ }));
2053
+ }
2054
+
1935
2055
  /**
1936
2056
  * Attach a detached entity and mark it as modified
1937
2057
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
4
4
  "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 ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {