masterrecord 0.3.49 ā 0.3.51
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/QueryLanguage/queryMethods.js +23 -13
- package/context.js +174 -8
- package/package.json +1 -1
- package/test/ensure-ready-test.js +251 -0
- package/test/pool-registry-test.js +303 -0
- 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": []
|
|
@@ -171,6 +171,8 @@ class queryMethods{
|
|
|
171
171
|
this.__queryObject.count(str, this.__entity.__name);
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
+
await this.__context._ensureReady();
|
|
175
|
+
|
|
174
176
|
if(this.__context.isSQLite){
|
|
175
177
|
// trying to match string select and relace with select Count(*);
|
|
176
178
|
var entityValue = await this.__context._SQLEngine.getCount(this.__queryObject, this.__entity, this.__context);
|
|
@@ -178,22 +180,24 @@ class queryMethods{
|
|
|
178
180
|
this.__reset();
|
|
179
181
|
return val;
|
|
180
182
|
}
|
|
181
|
-
|
|
182
|
-
if(this.__context.isMySQL){
|
|
183
|
+
else if(this.__context.isMySQL){
|
|
183
184
|
// trying to match string select and relace with select Count(*);
|
|
184
185
|
var entityValue = await this.__context._SQLEngine.getCount(this.__queryObject, this.__entity, this.__context);
|
|
185
186
|
var val = entityValue[Object.keys(entityValue)[0]];
|
|
186
187
|
this.__reset();
|
|
187
188
|
return val;
|
|
188
189
|
}
|
|
189
|
-
|
|
190
|
-
if(this.__context.isPostgres){
|
|
190
|
+
else if(this.__context.isPostgres){
|
|
191
191
|
// trying to match string select and relace with select Count(*);
|
|
192
192
|
var entityValue = await this.__context._SQLEngine.getCount(this.__queryObject, this.__entity, this.__context);
|
|
193
193
|
var val = entityValue[Object.keys(entityValue)[0]];
|
|
194
194
|
this.__reset();
|
|
195
195
|
return val;
|
|
196
196
|
}
|
|
197
|
+
else {
|
|
198
|
+
this.__reset();
|
|
199
|
+
throw new Error('No database type configured. Ensure context.env() or context.useMySql()/useSqlite() has been called and awaited.');
|
|
200
|
+
}
|
|
197
201
|
}
|
|
198
202
|
|
|
199
203
|
/**
|
|
@@ -315,7 +319,7 @@ class queryMethods{
|
|
|
315
319
|
// Get database type from context
|
|
316
320
|
const dbType = this.__context.isSQLite ? 'sqlite' :
|
|
317
321
|
this.__context.isMySQL ? 'mysql' :
|
|
318
|
-
this.__context.isPostgres ? 'postgres' : '
|
|
322
|
+
this.__context.isPostgres ? 'postgres' : 'unknown';
|
|
319
323
|
|
|
320
324
|
// Replace $$ with ? placeholders and collect parameter values
|
|
321
325
|
if(args){
|
|
@@ -436,21 +440,24 @@ class queryMethods{
|
|
|
436
440
|
}
|
|
437
441
|
|
|
438
442
|
// Cache miss - execute query
|
|
443
|
+
await this.__context._ensureReady();
|
|
439
444
|
var result = null;
|
|
440
445
|
if(this.__context.isSQLite){
|
|
441
446
|
var entityValue = await this.__context._SQLEngine.get(this.__queryObject.script, this.__entity, this.__context);
|
|
442
447
|
result = this.__singleEntityBuilder(entityValue);
|
|
443
448
|
}
|
|
444
|
-
|
|
445
|
-
if(this.__context.isMySQL){
|
|
449
|
+
else if(this.__context.isMySQL){
|
|
446
450
|
var entityValue = await this.__context._SQLEngine.get(this.__queryObject.script, this.__entity, this.__context);
|
|
447
451
|
result = this.__singleEntityBuilder(entityValue[0]);
|
|
448
452
|
}
|
|
449
|
-
|
|
450
|
-
if(this.__context.isPostgres){
|
|
453
|
+
else if(this.__context.isPostgres){
|
|
451
454
|
var entityValue = await this.__context._SQLEngine.get(this.__queryObject.script, this.__entity, this.__context);
|
|
452
455
|
result = this.__singleEntityBuilder(entityValue[0]);
|
|
453
456
|
}
|
|
457
|
+
else {
|
|
458
|
+
this.__reset();
|
|
459
|
+
throw new Error('No database type configured. Ensure context.env() or context.useMySql()/useSqlite() has been called and awaited.');
|
|
460
|
+
}
|
|
454
461
|
|
|
455
462
|
// Store in cache
|
|
456
463
|
if (this.__useCache && result) {
|
|
@@ -486,21 +493,24 @@ class queryMethods{
|
|
|
486
493
|
}
|
|
487
494
|
|
|
488
495
|
// Cache miss - execute query
|
|
496
|
+
await this.__context._ensureReady();
|
|
489
497
|
var result = [];
|
|
490
498
|
if(this.__context.isSQLite){
|
|
491
499
|
var entityValue = await this.__context._SQLEngine.all(this.__queryObject.script, this.__entity, this.__context);
|
|
492
500
|
result = this.__multipleEntityBuilder(entityValue);
|
|
493
501
|
}
|
|
494
|
-
|
|
495
|
-
if(this.__context.isMySQL){
|
|
502
|
+
else if(this.__context.isMySQL){
|
|
496
503
|
var entityValue = await this.__context._SQLEngine.all(this.__queryObject.script, this.__entity, this.__context);
|
|
497
504
|
result = this.__multipleEntityBuilder(entityValue);
|
|
498
505
|
}
|
|
499
|
-
|
|
500
|
-
if(this.__context.isPostgres){
|
|
506
|
+
else if(this.__context.isPostgres){
|
|
501
507
|
var entityValue = await this.__context._SQLEngine.all(this.__queryObject.script, this.__entity, this.__context);
|
|
502
508
|
result = this.__multipleEntityBuilder(entityValue);
|
|
503
509
|
}
|
|
510
|
+
else {
|
|
511
|
+
this.__reset();
|
|
512
|
+
throw new Error('No database type configured. Ensure context.env() or context.useMySql()/useSqlite() has been called and awaited.');
|
|
513
|
+
}
|
|
504
514
|
|
|
505
515
|
// Store in cache
|
|
506
516
|
if (this.__useCache && result) {
|
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
|
// ============================================================================
|
|
@@ -171,6 +184,9 @@ class context {
|
|
|
171
184
|
isMySQL = false;
|
|
172
185
|
isPostgres = false;
|
|
173
186
|
|
|
187
|
+
// Async readiness flag ā set by _ensureReady() after _initPromise resolves
|
|
188
|
+
_ready = false;
|
|
189
|
+
|
|
174
190
|
// Static shared cache - all context instances share the same cache
|
|
175
191
|
static _sharedQueryCache = null;
|
|
176
192
|
|
|
@@ -220,6 +236,33 @@ class context {
|
|
|
220
236
|
this._queryCache = context._sharedQueryCache;
|
|
221
237
|
}
|
|
222
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Ensure the database engine is initialized and ready for queries.
|
|
241
|
+
*
|
|
242
|
+
* If an async init is in flight (_initPromise), awaits it.
|
|
243
|
+
* If _SQLEngine is still null after that, throws a clear error.
|
|
244
|
+
* Subsequent calls are a single boolean check (no-op).
|
|
245
|
+
*
|
|
246
|
+
* @throws {DatabaseConnectionError} If the engine failed to initialize
|
|
247
|
+
*/
|
|
248
|
+
async _ensureReady() {
|
|
249
|
+
if (this._ready) return;
|
|
250
|
+
if (this._initPromise) {
|
|
251
|
+
await this._initPromise;
|
|
252
|
+
}
|
|
253
|
+
if (!this._SQLEngine) {
|
|
254
|
+
const dbType = this.isMySQL ? 'MySQL' :
|
|
255
|
+
this.isPostgres ? 'PostgreSQL' :
|
|
256
|
+
this.isSQLite ? 'SQLite' : 'unknown';
|
|
257
|
+
throw new DatabaseConnectionError(
|
|
258
|
+
'Database engine not initialized. Ensure you have awaited env() or the appropriate use*() method before querying.',
|
|
259
|
+
dbType,
|
|
260
|
+
{ hasInitPromise: !!this._initPromise, isSQLite: this.isSQLite, isMySQL: this.isMySQL, isPostgres: this.isPostgres }
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
this._ready = true;
|
|
264
|
+
}
|
|
265
|
+
|
|
223
266
|
/**
|
|
224
267
|
* Parse integer environment variable with validation
|
|
225
268
|
*
|
|
@@ -329,6 +372,16 @@ class context {
|
|
|
329
372
|
);
|
|
330
373
|
}
|
|
331
374
|
|
|
375
|
+
const key = _poolKey('mysql', env);
|
|
376
|
+
if (_pools.has(key)) {
|
|
377
|
+
const cached = _pools.get(key);
|
|
378
|
+
cached.refCount++;
|
|
379
|
+
this._SQLEngine = cached.engine;
|
|
380
|
+
this.isMySQL = true;
|
|
381
|
+
console.log(`[MySQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
|
|
382
|
+
return cached.client;
|
|
383
|
+
}
|
|
384
|
+
|
|
332
385
|
console.log('[MySQL] Initializing async connection pool...');
|
|
333
386
|
const client = new MySQLAsyncClient(env);
|
|
334
387
|
await client.connect();
|
|
@@ -339,6 +392,7 @@ class context {
|
|
|
339
392
|
this._SQLEngine.__name = sqlName;
|
|
340
393
|
this.isMySQL = true;
|
|
341
394
|
|
|
395
|
+
_pools.set(key, { client, engine: this._SQLEngine, refCount: 1, dbType: 'mysql' });
|
|
342
396
|
console.log('[MySQL] Connection pool ready');
|
|
343
397
|
return client;
|
|
344
398
|
} catch (error) {
|
|
@@ -401,12 +455,24 @@ class context {
|
|
|
401
455
|
);
|
|
402
456
|
}
|
|
403
457
|
|
|
458
|
+
const key = _poolKey('postgres', env);
|
|
459
|
+
if (_pools.has(key)) {
|
|
460
|
+
const cached = _pools.get(key);
|
|
461
|
+
cached.refCount++;
|
|
462
|
+
this._SQLEngine = cached.engine;
|
|
463
|
+
this._SQLEngine.__name = sqlName;
|
|
464
|
+
console.log(`[PostgreSQL] Reusing pool for ${env.database} (refs: ${cached.refCount})`);
|
|
465
|
+
return cached.pool;
|
|
466
|
+
}
|
|
467
|
+
|
|
404
468
|
const connection = new PostgresClient();
|
|
405
469
|
await connection.connect(env);
|
|
406
470
|
this._SQLEngine = connection.getEngine();
|
|
407
471
|
this._SQLEngine.__name = sqlName;
|
|
408
472
|
|
|
409
|
-
|
|
473
|
+
const pool = connection.getPool();
|
|
474
|
+
_pools.set(key, { pool, engine: this._SQLEngine, client: connection, refCount: 1, dbType: 'postgres' });
|
|
475
|
+
return pool;
|
|
410
476
|
} catch (error) {
|
|
411
477
|
// Preserve original error if it's already a ContextError
|
|
412
478
|
if (error instanceof ContextError) {
|
|
@@ -700,8 +766,20 @@ class context {
|
|
|
700
766
|
}
|
|
701
767
|
|
|
702
768
|
const sqliteOptions = { ...options, completeConnection: dbPath };
|
|
769
|
+
|
|
770
|
+
const sqliteKey = _poolKey('sqlite', sqliteOptions);
|
|
771
|
+
if (_pools.has(sqliteKey)) {
|
|
772
|
+
const cached = _pools.get(sqliteKey);
|
|
773
|
+
cached.refCount++;
|
|
774
|
+
this.db = cached.db;
|
|
775
|
+
this._SQLEngine = cached.engine;
|
|
776
|
+
return this;
|
|
777
|
+
}
|
|
778
|
+
|
|
703
779
|
this.db = this.__SQLiteInit(sqliteOptions, 'better-sqlite3');
|
|
704
780
|
this._SQLEngine.setDB(this.db, 'better-sqlite3');
|
|
781
|
+
|
|
782
|
+
_pools.set(sqliteKey, { db: this.db, engine: this._SQLEngine, refCount: 1, dbType: 'sqlite' });
|
|
705
783
|
return this;
|
|
706
784
|
}
|
|
707
785
|
|
|
@@ -721,8 +799,10 @@ class context {
|
|
|
721
799
|
// Note: engine is already set in __mysqlInit
|
|
722
800
|
return this;
|
|
723
801
|
})();
|
|
724
|
-
// Prevent unhandled rejection crash ā _ensureReady() will
|
|
725
|
-
this._initPromise.catch(() => {
|
|
802
|
+
// Prevent unhandled rejection crash ā _ensureReady() will re-throw on query
|
|
803
|
+
this._initPromise.catch((err) => {
|
|
804
|
+
console.error(`[MasterRecord] Database initialization failed: ${err.message || err}`);
|
|
805
|
+
});
|
|
726
806
|
return this._initPromise;
|
|
727
807
|
}
|
|
728
808
|
|
|
@@ -742,8 +822,10 @@ class context {
|
|
|
742
822
|
// Note: engine is already set in __postgresInit
|
|
743
823
|
return this;
|
|
744
824
|
})();
|
|
745
|
-
// Prevent unhandled rejection crash ā _ensureReady() will
|
|
746
|
-
this._initPromise.catch(() => {
|
|
825
|
+
// Prevent unhandled rejection crash ā _ensureReady() will re-throw on query
|
|
826
|
+
this._initPromise.catch((err) => {
|
|
827
|
+
console.error(`[MasterRecord] Database initialization failed: ${err.message || err}`);
|
|
828
|
+
});
|
|
747
829
|
return this._initPromise;
|
|
748
830
|
}
|
|
749
831
|
|
|
@@ -831,7 +913,7 @@ class context {
|
|
|
831
913
|
);
|
|
832
914
|
}
|
|
833
915
|
|
|
834
|
-
this.
|
|
916
|
+
this.validateDatabaseOptions(options);
|
|
835
917
|
|
|
836
918
|
// Resolve database path using extracted method (eliminates duplicate code)
|
|
837
919
|
const dbPath = this._resolveDatabasePath(options.connection, file.rootFolder, contextName);
|
|
@@ -843,8 +925,19 @@ class context {
|
|
|
843
925
|
fs.mkdirSync(dbDirectory, { recursive: true });
|
|
844
926
|
}
|
|
845
927
|
|
|
928
|
+
const sqliteKey = _poolKey('sqlite', options);
|
|
929
|
+
if (_pools.has(sqliteKey)) {
|
|
930
|
+
const cached = _pools.get(sqliteKey);
|
|
931
|
+
cached.refCount++;
|
|
932
|
+
this.db = cached.db;
|
|
933
|
+
this._SQLEngine = cached.engine;
|
|
934
|
+
return this;
|
|
935
|
+
}
|
|
936
|
+
|
|
846
937
|
this.db = this.__SQLiteInit(options, 'better-sqlite3');
|
|
847
938
|
this._SQLEngine.setDB(this.db, 'better-sqlite3');
|
|
939
|
+
|
|
940
|
+
_pools.set(sqliteKey, { db: this.db, engine: this._SQLEngine, refCount: 1, dbType: 'sqlite' });
|
|
848
941
|
return this;
|
|
849
942
|
} catch (error) {
|
|
850
943
|
// Preserve original error if it's already a ContextError
|
|
@@ -872,7 +965,7 @@ class context {
|
|
|
872
965
|
* @param {object} options - Database configuration options
|
|
873
966
|
* @throws {ConfigurationError} If options are invalid
|
|
874
967
|
*/
|
|
875
|
-
|
|
968
|
+
validateDatabaseOptions(options) {
|
|
876
969
|
if (!options || typeof options !== 'object') {
|
|
877
970
|
throw new ConfigurationError('Configuration object is missing or invalid');
|
|
878
971
|
}
|
|
@@ -946,6 +1039,11 @@ class context {
|
|
|
946
1039
|
);
|
|
947
1040
|
}
|
|
948
1041
|
|
|
1042
|
+
/** @deprecated Use validateDatabaseOptions() */
|
|
1043
|
+
validateSQLiteOptions(options) {
|
|
1044
|
+
return this.validateDatabaseOptions(options);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
949
1047
|
/**
|
|
950
1048
|
* Initialize MySQL database connection using environment file
|
|
951
1049
|
*
|
|
@@ -980,7 +1078,7 @@ class context {
|
|
|
980
1078
|
);
|
|
981
1079
|
}
|
|
982
1080
|
|
|
983
|
-
this.
|
|
1081
|
+
this.validateDatabaseOptions(options);
|
|
984
1082
|
this.db = await this.__mysqlInit(options, 'mysql2');
|
|
985
1083
|
// Note: engine is already set in __mysqlInit
|
|
986
1084
|
return this;
|
|
@@ -1649,6 +1747,7 @@ class context {
|
|
|
1649
1747
|
* db.saveChanges();
|
|
1650
1748
|
*/
|
|
1651
1749
|
async saveChanges() {
|
|
1750
|
+
await this._ensureReady();
|
|
1652
1751
|
try {
|
|
1653
1752
|
const tracked = this.__trackedEntities;
|
|
1654
1753
|
|
|
@@ -1714,6 +1813,12 @@ class context {
|
|
|
1714
1813
|
* context._execute('CREATE INDEX idx_user_email ON User(email)');
|
|
1715
1814
|
*/
|
|
1716
1815
|
_execute(query) {
|
|
1816
|
+
if (!this._SQLEngine) {
|
|
1817
|
+
throw new DatabaseConnectionError(
|
|
1818
|
+
'Cannot execute query: database engine not initialized. Ensure you have awaited env() before running queries.',
|
|
1819
|
+
this.isMySQL ? 'MySQL' : this.isPostgres ? 'PostgreSQL' : 'SQLite'
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1717
1822
|
return this._SQLEngine._execute(query);
|
|
1718
1823
|
}
|
|
1719
1824
|
|
|
@@ -1927,11 +2032,72 @@ class context {
|
|
|
1927
2032
|
* db.close(); // Close connections
|
|
1928
2033
|
*/
|
|
1929
2034
|
async close() {
|
|
2035
|
+
// Find this instance's pool in the registry and decrement
|
|
2036
|
+
for (const [key, entry] of _pools) {
|
|
2037
|
+
if (entry.engine === this._SQLEngine) {
|
|
2038
|
+
entry.refCount--;
|
|
2039
|
+
if (entry.refCount <= 0) {
|
|
2040
|
+
_pools.delete(key);
|
|
2041
|
+
if (this._SQLEngine && typeof this._SQLEngine.close === 'function') {
|
|
2042
|
+
await this._SQLEngine.close();
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
this._SQLEngine = null;
|
|
2046
|
+
this.db = null;
|
|
2047
|
+
return;
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
// Fallback (not in registry)
|
|
1930
2052
|
if (this._SQLEngine && typeof this._SQLEngine.close === 'function') {
|
|
1931
2053
|
return await this._SQLEngine.close();
|
|
1932
2054
|
}
|
|
1933
2055
|
}
|
|
1934
2056
|
|
|
2057
|
+
/**
|
|
2058
|
+
* Close all shared connection pools, regardless of reference count.
|
|
2059
|
+
* Useful for graceful shutdown or test cleanup.
|
|
2060
|
+
*
|
|
2061
|
+
* @static
|
|
2062
|
+
* @async
|
|
2063
|
+
* @returns {Promise<void>}
|
|
2064
|
+
*
|
|
2065
|
+
* @example
|
|
2066
|
+
* // Graceful shutdown
|
|
2067
|
+
* process.on('SIGTERM', async () => {
|
|
2068
|
+
* await context.closeAll();
|
|
2069
|
+
* process.exit(0);
|
|
2070
|
+
* });
|
|
2071
|
+
*/
|
|
2072
|
+
static async closeAll() {
|
|
2073
|
+
for (const [key, entry] of _pools) {
|
|
2074
|
+
try {
|
|
2075
|
+
if (entry.engine && typeof entry.engine.close === 'function') {
|
|
2076
|
+
await entry.engine.close();
|
|
2077
|
+
}
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
console.error('[MasterRecord] Error closing pool:', err.message);
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
_pools.clear();
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
/**
|
|
2086
|
+
* Get statistics about active connection pools.
|
|
2087
|
+
*
|
|
2088
|
+
* @static
|
|
2089
|
+
* @returns {Array<{key: string, dbType: string, refCount: number}>}
|
|
2090
|
+
*
|
|
2091
|
+
* @example
|
|
2092
|
+
* console.log(context.getPoolStats());
|
|
2093
|
+
* // [{ key: 'mysql:root@localhost:3306/mydb', dbType: 'mysql', refCount: 3 }]
|
|
2094
|
+
*/
|
|
2095
|
+
static getPoolStats() {
|
|
2096
|
+
return Array.from(_pools.entries()).map(([key, entry]) => ({
|
|
2097
|
+
key, dbType: entry.dbType, refCount: entry.refCount
|
|
2098
|
+
}));
|
|
2099
|
+
}
|
|
2100
|
+
|
|
1935
2101
|
/**
|
|
1936
2102
|
* Attach a detached entity and mark it as modified
|
|
1937
2103
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.51",
|
|
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": {
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test: _ensureReady(), validateDatabaseOptions rename, and async guards
|
|
3
|
+
*
|
|
4
|
+
* Verifies:
|
|
5
|
+
* - Query before env() resolves ā throws clear DatabaseConnectionError
|
|
6
|
+
* - Query after await env() ā works normally (no regression)
|
|
7
|
+
* - SQLite works without _initPromise (fast path)
|
|
8
|
+
* - validateDatabaseOptions works, validateSQLiteOptions alias works
|
|
9
|
+
* - _execute() throws when engine is null
|
|
10
|
+
* - saveChanges() throws when engine is null
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const context = require('../context');
|
|
14
|
+
|
|
15
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
16
|
+
console.log("ā _ensureReady + Rename + Async Guards Test ā");
|
|
17
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
|
|
18
|
+
|
|
19
|
+
let passed = 0;
|
|
20
|
+
let failed = 0;
|
|
21
|
+
|
|
22
|
+
function assert(condition, passMsg, failMsg) {
|
|
23
|
+
if (condition) {
|
|
24
|
+
console.log(` ā ${passMsg}`);
|
|
25
|
+
passed++;
|
|
26
|
+
} else {
|
|
27
|
+
console.log(` ā ${failMsg}`);
|
|
28
|
+
failed++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Test 1: _ensureReady() throws when engine is null and no init promise
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
console.log("š Test 1: _ensureReady() throws DatabaseConnectionError when engine is null");
|
|
36
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
37
|
+
|
|
38
|
+
(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const ctx = new context();
|
|
41
|
+
// No env() called ā _SQLEngine is null, no _initPromise
|
|
42
|
+
try {
|
|
43
|
+
await ctx._ensureReady();
|
|
44
|
+
assert(false, "", "_ensureReady() did not throw when engine is null");
|
|
45
|
+
} catch (err) {
|
|
46
|
+
assert(
|
|
47
|
+
err.name === 'DatabaseConnectionError',
|
|
48
|
+
`Throws DatabaseConnectionError (got: ${err.name})`,
|
|
49
|
+
`Expected DatabaseConnectionError, got: ${err.name}`
|
|
50
|
+
);
|
|
51
|
+
assert(
|
|
52
|
+
err.message.includes('Database engine not initialized'),
|
|
53
|
+
`Error message is descriptive: "${err.message.substring(0, 60)}..."`,
|
|
54
|
+
`Unexpected error message: ${err.message}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
} catch (err) {
|
|
58
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
59
|
+
failed++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Test 2: _ensureReady() is a no-op after _ready is set
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
console.log("\nš Test 2: _ensureReady() fast path ā no-op when _ready is true");
|
|
66
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const ctx = new context();
|
|
70
|
+
ctx._SQLEngine = {}; // Fake engine
|
|
71
|
+
ctx.isSQLite = true;
|
|
72
|
+
await ctx._ensureReady(); // Should set _ready = true
|
|
73
|
+
assert(ctx._ready === true, "_ready is true after first call", "_ready was not set");
|
|
74
|
+
|
|
75
|
+
// Second call should be instant (no-op)
|
|
76
|
+
await ctx._ensureReady();
|
|
77
|
+
assert(true, "Second call completes without error (fast path)", "");
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
80
|
+
failed++;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Test 3: _ensureReady() awaits _initPromise if present
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
console.log("\nš Test 3: _ensureReady() awaits _initPromise");
|
|
87
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const ctx = new context();
|
|
91
|
+
ctx.isMySQL = true;
|
|
92
|
+
// Simulate async init that sets _SQLEngine after a tick
|
|
93
|
+
ctx._initPromise = new Promise((resolve) => {
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
ctx._SQLEngine = {}; // Fake engine
|
|
96
|
+
resolve();
|
|
97
|
+
}, 10);
|
|
98
|
+
});
|
|
99
|
+
// Prevent unhandled rejection
|
|
100
|
+
ctx._initPromise.catch(() => {});
|
|
101
|
+
|
|
102
|
+
await ctx._ensureReady();
|
|
103
|
+
assert(
|
|
104
|
+
ctx._SQLEngine !== null,
|
|
105
|
+
"_SQLEngine is set after _ensureReady() awaits _initPromise",
|
|
106
|
+
"_SQLEngine is still null"
|
|
107
|
+
);
|
|
108
|
+
assert(ctx._ready === true, "_ready is true", "_ready was not set");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
111
|
+
failed++;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Test 4: _ensureReady() propagates _initPromise rejection
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
console.log("\nš Test 4: _ensureReady() propagates _initPromise rejection");
|
|
118
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const ctx = new context();
|
|
122
|
+
ctx.isMySQL = true;
|
|
123
|
+
ctx._initPromise = Promise.reject(new Error('Connection refused'));
|
|
124
|
+
// Prevent unhandled rejection warning
|
|
125
|
+
ctx._initPromise.catch(() => {});
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
await ctx._ensureReady();
|
|
129
|
+
assert(false, "", "_ensureReady() did not throw when _initPromise rejected");
|
|
130
|
+
} catch (err) {
|
|
131
|
+
assert(
|
|
132
|
+
err.message === 'Connection refused',
|
|
133
|
+
`Propagates original rejection: "${err.message}"`,
|
|
134
|
+
`Unexpected error: ${err.message}`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
139
|
+
failed++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Test 5: validateDatabaseOptions exists and works
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
console.log("\nš Test 5: validateDatabaseOptions() exists and works");
|
|
146
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const ctx = new context();
|
|
150
|
+
assert(
|
|
151
|
+
typeof ctx.validateDatabaseOptions === 'function',
|
|
152
|
+
"validateDatabaseOptions is a function",
|
|
153
|
+
`validateDatabaseOptions is ${typeof ctx.validateDatabaseOptions}`
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Test with a valid SQLite config
|
|
157
|
+
const options = { type: 'sqlite', connection: './test.db' };
|
|
158
|
+
ctx.validateDatabaseOptions(options);
|
|
159
|
+
assert(true, "validateDatabaseOptions accepts valid SQLite config", "");
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
162
|
+
failed++;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Test 6: validateSQLiteOptions deprecated alias works
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
console.log("\nš Test 6: validateSQLiteOptions() deprecated alias works");
|
|
169
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const ctx = new context();
|
|
173
|
+
assert(
|
|
174
|
+
typeof ctx.validateSQLiteOptions === 'function',
|
|
175
|
+
"validateSQLiteOptions alias exists",
|
|
176
|
+
`validateSQLiteOptions is ${typeof ctx.validateSQLiteOptions}`
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Should delegate to validateDatabaseOptions
|
|
180
|
+
const options = { type: 'sqlite', connection: './test.db' };
|
|
181
|
+
ctx.validateSQLiteOptions(options);
|
|
182
|
+
assert(true, "validateSQLiteOptions alias delegates correctly", "");
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
185
|
+
failed++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
// Test 7: _execute() throws when _SQLEngine is null
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
console.log("\nš Test 7: _execute() throws when _SQLEngine is null");
|
|
192
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const ctx = new context();
|
|
196
|
+
// _SQLEngine is null by default
|
|
197
|
+
try {
|
|
198
|
+
ctx._execute('SELECT 1');
|
|
199
|
+
assert(false, "", "_execute() did not throw when engine is null");
|
|
200
|
+
} catch (err) {
|
|
201
|
+
assert(
|
|
202
|
+
err.name === 'DatabaseConnectionError',
|
|
203
|
+
`Throws DatabaseConnectionError (got: ${err.name})`,
|
|
204
|
+
`Expected DatabaseConnectionError, got: ${err.name}`
|
|
205
|
+
);
|
|
206
|
+
assert(
|
|
207
|
+
err.message.includes('database engine not initialized'),
|
|
208
|
+
`Error message is descriptive`,
|
|
209
|
+
`Unexpected error message: ${err.message}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
214
|
+
failed++;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Test 8: saveChanges() throws when engine is null (via _ensureReady)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
console.log("\nš Test 8: saveChanges() throws when engine is null");
|
|
221
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const ctx = new context();
|
|
225
|
+
// No env() called ā should throw via _ensureReady()
|
|
226
|
+
try {
|
|
227
|
+
await ctx.saveChanges();
|
|
228
|
+
// saveChanges returns early if no tracked entities ā that's OK
|
|
229
|
+
// The _ensureReady check happens before the tracked-entities check
|
|
230
|
+
assert(false, "", "saveChanges() did not throw when engine is null");
|
|
231
|
+
} catch (err) {
|
|
232
|
+
assert(
|
|
233
|
+
err.name === 'DatabaseConnectionError',
|
|
234
|
+
`Throws DatabaseConnectionError (got: ${err.name})`,
|
|
235
|
+
`Expected DatabaseConnectionError, got: ${err.name}`
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.log(` ā Unexpected error: ${err.message}`);
|
|
240
|
+
failed++;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Summary
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
console.log("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
|
|
247
|
+
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed} assertions`);
|
|
248
|
+
console.log("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
|
|
249
|
+
|
|
250
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
251
|
+
})();
|