masterrecord 0.3.50 → 0.3.52

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.
@@ -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,20 +440,23 @@ 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
- result = this.__singleEntityBuilder(entityValue[0]);
451
+ result = this.__singleEntityBuilder(entityValue);
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
- result = this.__singleEntityBuilder(entityValue[0]);
455
+ result = this.__singleEntityBuilder(entityValue);
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.');
453
460
  }
454
461
 
455
462
  // Store in cache
@@ -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
@@ -184,6 +184,9 @@ class context {
184
184
  isMySQL = false;
185
185
  isPostgres = false;
186
186
 
187
+ // Async readiness flag — set by _ensureReady() after _initPromise resolves
188
+ _ready = false;
189
+
187
190
  // Static shared cache - all context instances share the same cache
188
191
  static _sharedQueryCache = null;
189
192
 
@@ -233,6 +236,33 @@ class context {
233
236
  this._queryCache = context._sharedQueryCache;
234
237
  }
235
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
+
236
266
  /**
237
267
  * Parse integer environment variable with validation
238
268
  *
@@ -769,8 +799,10 @@ class context {
769
799
  // Note: engine is already set in __mysqlInit
770
800
  return this;
771
801
  })();
772
- // Prevent unhandled rejection crash — _ensureReady() will handle errors
773
- 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
+ });
774
806
  return this._initPromise;
775
807
  }
776
808
 
@@ -790,8 +822,10 @@ class context {
790
822
  // Note: engine is already set in __postgresInit
791
823
  return this;
792
824
  })();
793
- // Prevent unhandled rejection crash — _ensureReady() will handle errors
794
- 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
+ });
795
829
  return this._initPromise;
796
830
  }
797
831
 
@@ -879,7 +913,7 @@ class context {
879
913
  );
880
914
  }
881
915
 
882
- this.validateSQLiteOptions(options);
916
+ this.validateDatabaseOptions(options);
883
917
 
884
918
  // Resolve database path using extracted method (eliminates duplicate code)
885
919
  const dbPath = this._resolveDatabasePath(options.connection, file.rootFolder, contextName);
@@ -931,7 +965,7 @@ class context {
931
965
  * @param {object} options - Database configuration options
932
966
  * @throws {ConfigurationError} If options are invalid
933
967
  */
934
- validateSQLiteOptions(options) {
968
+ validateDatabaseOptions(options) {
935
969
  if (!options || typeof options !== 'object') {
936
970
  throw new ConfigurationError('Configuration object is missing or invalid');
937
971
  }
@@ -1005,6 +1039,11 @@ class context {
1005
1039
  );
1006
1040
  }
1007
1041
 
1042
+ /** @deprecated Use validateDatabaseOptions() */
1043
+ validateSQLiteOptions(options) {
1044
+ return this.validateDatabaseOptions(options);
1045
+ }
1046
+
1008
1047
  /**
1009
1048
  * Initialize MySQL database connection using environment file
1010
1049
  *
@@ -1039,7 +1078,7 @@ class context {
1039
1078
  );
1040
1079
  }
1041
1080
 
1042
- this.validateSQLiteOptions(options);
1081
+ this.validateDatabaseOptions(options);
1043
1082
  this.db = await this.__mysqlInit(options, 'mysql2');
1044
1083
  // Note: engine is already set in __mysqlInit
1045
1084
  return this;
@@ -1708,6 +1747,7 @@ class context {
1708
1747
  * db.saveChanges();
1709
1748
  */
1710
1749
  async saveChanges() {
1750
+ await this._ensureReady();
1711
1751
  try {
1712
1752
  const tracked = this.__trackedEntities;
1713
1753
 
@@ -1773,6 +1813,12 @@ class context {
1773
1813
  * context._execute('CREATE INDEX idx_user_email ON User(email)');
1774
1814
  */
1775
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
+ }
1776
1822
  return this._SQLEngine._execute(query);
1777
1823
  }
1778
1824
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.50",
3
+ "version": "0.3.52",
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
+ })();