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.
@@ -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' : 'sqlite';
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
- return connection.getPool();
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 handle errors
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 handle errors
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.validateSQLiteOptions(options);
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
- validateSQLiteOptions(options) {
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.validateSQLiteOptions(options);
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.49",
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
+ })();