masterrecord 0.3.64 → 0.3.66
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/schema.js +60 -1
- package/context.js +89 -18
- package/deleteManager.js +33 -12
- package/package.json +1 -1
package/Migrations/schema.js
CHANGED
|
@@ -6,6 +6,12 @@ class schema{
|
|
|
6
6
|
this._dbEnsured = false;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
_poolKey(type, cfg) {
|
|
10
|
+
const host = cfg.host || 'localhost';
|
|
11
|
+
const port = cfg.port || (type === 'mysql' ? 3306 : 5432);
|
|
12
|
+
return `${type}:${cfg.user}@${host}:${port}/${cfg.database}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
9
15
|
/**
|
|
10
16
|
* Wait for async database initialization (MySQL/PostgreSQL) to complete.
|
|
11
17
|
* The context constructor fires off an async pool init that may not have
|
|
@@ -84,6 +90,26 @@ class schema{
|
|
|
84
90
|
if(!config){ throw new Error('No MySQL config available for retry'); }
|
|
85
91
|
const MySQLEngine = require('masterrecord/mySQLEngine');
|
|
86
92
|
const MySQLAsyncClient = require('masterrecord/mySQLConnect');
|
|
93
|
+
|
|
94
|
+
// Check global pool cache first -- another context may have already retried
|
|
95
|
+
const _pools = global.__MR_POOLS__;
|
|
96
|
+
const key = _pools ? this._poolKey('mysql', config) : null;
|
|
97
|
+
|
|
98
|
+
if (_pools && key && _pools.has(key)) {
|
|
99
|
+
const cached = _pools.get(key);
|
|
100
|
+
cached.refCount++;
|
|
101
|
+
if (cached.promise) {
|
|
102
|
+
const result = await cached.promise;
|
|
103
|
+
this.context._SQLEngine = result.engine;
|
|
104
|
+
this.context.db = result.client;
|
|
105
|
+
} else {
|
|
106
|
+
this.context._SQLEngine = cached.engine;
|
|
107
|
+
this.context.db = cached.client;
|
|
108
|
+
}
|
|
109
|
+
console.log('[MySQL] Reusing existing pool after database creation');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
87
113
|
console.log('[MySQL] Retrying connection after database creation...');
|
|
88
114
|
const client = new MySQLAsyncClient(config);
|
|
89
115
|
await client.connect();
|
|
@@ -92,6 +118,11 @@ class schema{
|
|
|
92
118
|
this.context._SQLEngine.setDB(pool);
|
|
93
119
|
this.context._SQLEngine.__name = 'mysql2';
|
|
94
120
|
this.context.db = client;
|
|
121
|
+
|
|
122
|
+
// Register in global pool cache so other contexts can reuse
|
|
123
|
+
if (_pools && key) {
|
|
124
|
+
_pools.set(key, { client, engine: this.context._SQLEngine, refCount: 1, dbType: 'mysql' });
|
|
125
|
+
}
|
|
95
126
|
console.log('[MySQL] Connection pool ready');
|
|
96
127
|
}
|
|
97
128
|
|
|
@@ -140,12 +171,40 @@ class schema{
|
|
|
140
171
|
const config = this.context._dbConfig;
|
|
141
172
|
if(!config){ throw new Error('No PostgreSQL config available for retry'); }
|
|
142
173
|
const PostgresClient = require('masterrecord/postgresSyncConnect');
|
|
174
|
+
|
|
175
|
+
// Check global pool cache first -- another context may have already retried
|
|
176
|
+
const _pools = global.__MR_POOLS__;
|
|
177
|
+
const key = _pools ? this._poolKey('postgres', config) : null;
|
|
178
|
+
|
|
179
|
+
if (_pools && key && _pools.has(key)) {
|
|
180
|
+
const cached = _pools.get(key);
|
|
181
|
+
cached.refCount++;
|
|
182
|
+
if (cached.promise) {
|
|
183
|
+
const result = await cached.promise;
|
|
184
|
+
this.context._SQLEngine = result.engine;
|
|
185
|
+
this.context._SQLEngine.__name = 'pg';
|
|
186
|
+
this.context.db = result.pool;
|
|
187
|
+
} else {
|
|
188
|
+
this.context._SQLEngine = cached.engine;
|
|
189
|
+
this.context._SQLEngine.__name = 'pg';
|
|
190
|
+
this.context.db = cached.pool;
|
|
191
|
+
}
|
|
192
|
+
console.log('[PostgreSQL] Reusing existing pool after database creation');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
143
196
|
console.log('[PostgreSQL] Retrying connection after database creation...');
|
|
144
197
|
const connection = new PostgresClient();
|
|
145
198
|
await connection.connect(config);
|
|
146
199
|
this.context._SQLEngine = connection.getEngine();
|
|
147
200
|
this.context._SQLEngine.__name = 'pg';
|
|
148
|
-
|
|
201
|
+
const pool = connection.getPool();
|
|
202
|
+
this.context.db = pool;
|
|
203
|
+
|
|
204
|
+
// Register in global pool cache so other contexts can reuse
|
|
205
|
+
if (_pools && key) {
|
|
206
|
+
_pools.set(key, { pool, engine: this.context._SQLEngine, client: connection, refCount: 1, dbType: 'postgres' });
|
|
207
|
+
}
|
|
149
208
|
console.log('[PostgreSQL] Connection pool ready');
|
|
150
209
|
}
|
|
151
210
|
|
package/context.js
CHANGED
|
@@ -376,6 +376,15 @@ class context {
|
|
|
376
376
|
if (_pools.has(key)) {
|
|
377
377
|
const cached = _pools.get(key);
|
|
378
378
|
cached.refCount++;
|
|
379
|
+
if (cached.promise) {
|
|
380
|
+
// Another caller is initializing -- await the same promise
|
|
381
|
+
const result = await cached.promise;
|
|
382
|
+
this._SQLEngine = result.engine;
|
|
383
|
+
this.isMySQL = true;
|
|
384
|
+
console.log(`[MySQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
|
|
385
|
+
return result.client;
|
|
386
|
+
}
|
|
387
|
+
// Already resolved
|
|
379
388
|
this._SQLEngine = cached.engine;
|
|
380
389
|
this.isMySQL = true;
|
|
381
390
|
console.log(`[MySQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
|
|
@@ -383,18 +392,38 @@ class context {
|
|
|
383
392
|
}
|
|
384
393
|
|
|
385
394
|
console.log('[MySQL] Initializing async connection pool...');
|
|
386
|
-
const client = new MySQLAsyncClient(env);
|
|
387
|
-
await client.connect();
|
|
388
395
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
396
|
+
// Store promise IMMEDIATELY to prevent race condition with concurrent callers
|
|
397
|
+
const initPromise = (async () => {
|
|
398
|
+
const client = new MySQLAsyncClient(env);
|
|
399
|
+
await client.connect();
|
|
400
|
+
const pool = client.getPool();
|
|
401
|
+
const engine = new MySQLEngine();
|
|
402
|
+
engine.setDB(pool);
|
|
403
|
+
engine.__name = sqlName;
|
|
404
|
+
return { client, engine };
|
|
405
|
+
})();
|
|
406
|
+
|
|
407
|
+
_pools.set(key, { promise: initPromise, refCount: 1, dbType: 'mysql' });
|
|
394
408
|
|
|
395
|
-
|
|
409
|
+
let result;
|
|
410
|
+
try {
|
|
411
|
+
result = await initPromise;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
// Remove failed entry so future callers can retry
|
|
414
|
+
_pools.delete(key);
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Replace pending entry with resolved entry, preserving refCount from concurrent joiners
|
|
419
|
+
const pending = _pools.get(key);
|
|
420
|
+
const currentRefCount = pending ? pending.refCount : 1;
|
|
421
|
+
_pools.set(key, { client: result.client, engine: result.engine, refCount: currentRefCount, dbType: 'mysql' });
|
|
422
|
+
|
|
423
|
+
this._SQLEngine = result.engine;
|
|
424
|
+
this.isMySQL = true;
|
|
396
425
|
console.log('[MySQL] Connection pool ready');
|
|
397
|
-
return client;
|
|
426
|
+
return result.client;
|
|
398
427
|
} catch (error) {
|
|
399
428
|
// Preserve original error if it's already a ContextError
|
|
400
429
|
if (error instanceof ContextError) {
|
|
@@ -459,20 +488,49 @@ class context {
|
|
|
459
488
|
if (_pools.has(key)) {
|
|
460
489
|
const cached = _pools.get(key);
|
|
461
490
|
cached.refCount++;
|
|
491
|
+
if (cached.promise) {
|
|
492
|
+
// Another caller is initializing -- await the same promise
|
|
493
|
+
const result = await cached.promise;
|
|
494
|
+
this._SQLEngine = result.engine;
|
|
495
|
+
this._SQLEngine.__name = sqlName;
|
|
496
|
+
console.log(`[PostgreSQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
|
|
497
|
+
return result.pool;
|
|
498
|
+
}
|
|
499
|
+
// Already resolved
|
|
462
500
|
this._SQLEngine = cached.engine;
|
|
463
501
|
this._SQLEngine.__name = sqlName;
|
|
464
502
|
console.log(`[PostgreSQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
|
|
465
503
|
return cached.pool;
|
|
466
504
|
}
|
|
467
505
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
506
|
+
// Store promise IMMEDIATELY to prevent race condition with concurrent callers
|
|
507
|
+
const initPromise = (async () => {
|
|
508
|
+
const connection = new PostgresClient();
|
|
509
|
+
await connection.connect(env);
|
|
510
|
+
const engine = connection.getEngine();
|
|
511
|
+
engine.__name = sqlName;
|
|
512
|
+
const pool = connection.getPool();
|
|
513
|
+
return { pool, engine, client: connection };
|
|
514
|
+
})();
|
|
515
|
+
|
|
516
|
+
_pools.set(key, { promise: initPromise, refCount: 1, dbType: 'postgres' });
|
|
472
517
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
518
|
+
let result;
|
|
519
|
+
try {
|
|
520
|
+
result = await initPromise;
|
|
521
|
+
} catch (err) {
|
|
522
|
+
// Remove failed entry so future callers can retry
|
|
523
|
+
_pools.delete(key);
|
|
524
|
+
throw err;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Replace pending entry with resolved entry, preserving refCount from concurrent joiners
|
|
528
|
+
const pending = _pools.get(key);
|
|
529
|
+
const currentRefCount = pending ? pending.refCount : 1;
|
|
530
|
+
_pools.set(key, { pool: result.pool, engine: result.engine, client: result.client, refCount: currentRefCount, dbType: 'postgres' });
|
|
531
|
+
|
|
532
|
+
this._SQLEngine = result.engine;
|
|
533
|
+
return result.pool;
|
|
476
534
|
} catch (error) {
|
|
477
535
|
// Preserve original error if it's already a ContextError
|
|
478
536
|
if (error instanceof ContextError) {
|
|
@@ -2058,6 +2116,8 @@ class context {
|
|
|
2058
2116
|
async close() {
|
|
2059
2117
|
// Find this instance's pool in the registry and decrement
|
|
2060
2118
|
for (const [key, entry] of _pools) {
|
|
2119
|
+
// Skip pending entries -- they have no engine yet
|
|
2120
|
+
if (entry.promise) continue;
|
|
2061
2121
|
if (entry.engine === this._SQLEngine) {
|
|
2062
2122
|
entry.refCount--;
|
|
2063
2123
|
if (entry.refCount <= 0) {
|
|
@@ -2096,7 +2156,17 @@ class context {
|
|
|
2096
2156
|
static async closeAll() {
|
|
2097
2157
|
for (const [key, entry] of _pools) {
|
|
2098
2158
|
try {
|
|
2099
|
-
if (entry.
|
|
2159
|
+
if (entry.promise) {
|
|
2160
|
+
// Wait for pending init to complete, then close it
|
|
2161
|
+
try {
|
|
2162
|
+
const result = await entry.promise;
|
|
2163
|
+
if (result.engine && typeof result.engine.close === 'function') {
|
|
2164
|
+
await result.engine.close();
|
|
2165
|
+
}
|
|
2166
|
+
} catch (_initErr) {
|
|
2167
|
+
// Init failed -- nothing to close
|
|
2168
|
+
}
|
|
2169
|
+
} else if (entry.engine && typeof entry.engine.close === 'function') {
|
|
2100
2170
|
await entry.engine.close();
|
|
2101
2171
|
}
|
|
2102
2172
|
} catch (err) {
|
|
@@ -2118,7 +2188,8 @@ class context {
|
|
|
2118
2188
|
*/
|
|
2119
2189
|
static getPoolStats() {
|
|
2120
2190
|
return Array.from(_pools.entries()).map(([key, entry]) => ({
|
|
2121
|
-
key, dbType: entry.dbType, refCount: entry.refCount
|
|
2191
|
+
key, dbType: entry.dbType, refCount: entry.refCount,
|
|
2192
|
+
status: entry.promise ? 'pending' : 'ready'
|
|
2122
2193
|
}));
|
|
2123
2194
|
}
|
|
2124
2195
|
|
package/deleteManager.js
CHANGED
|
@@ -77,19 +77,26 @@ class DeleteManager {
|
|
|
77
77
|
|
|
78
78
|
// Check if this is a relationship that needs cascade deletion
|
|
79
79
|
if (this._isRelationshipType(propertyConfig.type)) {
|
|
80
|
-
|
|
80
|
+
// Read the backing field directly to avoid triggering lazy-loading
|
|
81
|
+
// getters, which can return Promises or error strings
|
|
82
|
+
const relatedModel = entity.__proto__
|
|
83
|
+
? entity.__proto__["_" + property]
|
|
84
|
+
: entity["_" + property];
|
|
81
85
|
|
|
82
86
|
if (relatedModel === null || relatedModel === undefined) {
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
// Unloaded relationships are safe to skip — the database
|
|
88
|
+
// handles FK constraints; only cascade explicitly loaded data
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Only cascade into values that are actual tracked entities
|
|
93
|
+
if (Array.isArray(relatedModel)) {
|
|
94
|
+
for (const item of relatedModel) {
|
|
95
|
+
if (item && item.__entity) {
|
|
96
|
+
await this.cascadeDelete(item);
|
|
97
|
+
}
|
|
90
98
|
}
|
|
91
|
-
} else {
|
|
92
|
-
// Recursively delete related entities
|
|
99
|
+
} else if (relatedModel && relatedModel.__entity) {
|
|
93
100
|
await this.cascadeDelete(relatedModel);
|
|
94
101
|
}
|
|
95
102
|
}
|
|
@@ -129,9 +136,23 @@ class DeleteManager {
|
|
|
129
136
|
const propertyConfig = entity.__entity[property];
|
|
130
137
|
|
|
131
138
|
if (this._isRelationshipType(propertyConfig.type)) {
|
|
132
|
-
|
|
139
|
+
// Read backing field directly to avoid triggering lazy-loading getters
|
|
140
|
+
const relatedModel = entity.__proto__
|
|
141
|
+
? entity.__proto__["_" + property]
|
|
142
|
+
: entity["_" + property];
|
|
143
|
+
|
|
144
|
+
if (relatedModel === null || relatedModel === undefined) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
133
147
|
|
|
134
|
-
|
|
148
|
+
// Only cascade into actual tracked entities
|
|
149
|
+
if (Array.isArray(relatedModel)) {
|
|
150
|
+
for (const item of relatedModel) {
|
|
151
|
+
if (item && item.__entity) {
|
|
152
|
+
await this.cascadeDelete(item);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} else if (relatedModel && relatedModel.__entity) {
|
|
135
156
|
await this.cascadeDelete(relatedModel);
|
|
136
157
|
}
|
|
137
158
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.66",
|
|
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": {
|