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.
- package/.claude/settings.local.json +2 -1
- package/Migrations/migrations.js +6 -11
- package/context.js +121 -1
- package/package.json +1 -1
- package/readme.md +9 -749
- package/test/pool-registry-test.js +303 -0
- package/test/v0.3.34-bug-fixes-test.js +50 -8
- package/test/multiContextCacheSimple.test.js +0 -185
- package/test/simple-id-test.js +0 -61
- package/test/single-user-id-test.js +0 -70
- package/test/verifyFindById.js +0 -169
- package/test/verifyNewMethod.js +0 -191
|
@@ -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": []
|
package/Migrations/migrations.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": {
|