mythix-orm 1.14.0 → 1.15.0

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/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 24.13.0
@@ -824,7 +824,7 @@ class ConnectionBase extends EventEmitter {
824
824
  let modelNames = Object.keys(models);
825
825
  let newContext = new Map();
826
826
 
827
- newContext.set('connection', connection);
827
+ newContext.set(Utils.CONNECTION_KEY, connection);
828
828
 
829
829
  for (let i = 0, il = modelNames.length; i < il; i++) {
830
830
  let modelName = modelNames[i];
@@ -865,6 +865,53 @@ class ConnectionBase extends EventEmitter {
865
865
  });
866
866
  }
867
867
 
868
+ /// Capture the current AsyncLocalStorage context and return a function
869
+ /// that will execute callbacks within that captured context.
870
+ ///
871
+ /// This is useful for preserving database context across event emitters,
872
+ /// setTimeout, or other callbacks where context might otherwise be lost.
873
+ ///
874
+ /// Return: Function
875
+ /// A function that takes a callback and executes it in the captured context.
876
+ ///
877
+ /// Example:
878
+ /// const runInDbContext = connection.captureContext();
879
+ ///
880
+ /// eventEmitter.on('data', () => {
881
+ /// runInDbContext(async () => {
882
+ /// // Models work here - context is preserved
883
+ /// const user = await User.where.id.EQ('123').first();
884
+ /// });
885
+ /// });
886
+ ///
887
+ /// See: <see>Utils.AsyncStore.captureContext</see>
888
+ captureContext() {
889
+ return Utils.captureContext();
890
+ }
891
+
892
+ /// Wrap a callback function to preserve the current AsyncLocalStorage context.
893
+ ///
894
+ /// This is a convenience wrapper for use with event handlers and other callbacks
895
+ /// where you want to ensure database context is preserved.
896
+ ///
897
+ /// Arguments:
898
+ /// callback: Function
899
+ /// The callback function to wrap.
900
+ ///
901
+ /// Return: Function
902
+ /// A wrapped version of the callback that will execute in the captured context.
903
+ ///
904
+ /// Example:
905
+ /// eventEmitter.on('data', connection.bindCallback(async (data) => {
906
+ /// // Context preserved - models work here
907
+ /// const user = await User.where.id.EQ(data.userId).first();
908
+ /// }));
909
+ ///
910
+ /// See: <see>Utils.AsyncStore.bindCallback</see>
911
+ bindCallback(callback) {
912
+ return Utils.bindCallback(callback);
913
+ }
914
+
868
915
  /// Find a specific field across all registered models.
869
916
  ///
870
917
  /// This method is similar to [Array.find](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find),
package/lib/model.js CHANGED
@@ -422,7 +422,7 @@ class Model {
422
422
  if (connection)
423
423
  return connection;
424
424
 
425
- connection = AsyncStore.getContextValue('connection');
425
+ connection = AsyncStore.getContextValue(AsyncStore.CONNECTION_KEY);
426
426
  if (connection)
427
427
  return connection;
428
428
 
@@ -465,8 +465,36 @@ class Model {
465
465
 
466
466
  static getConnection(_connection) {
467
467
  let connection = this._getConnection(_connection);
468
- if (!connection)
469
- throw new Error(`${this.getModelName()}::getConnection: No connection bound to model. You need to provide a connection for this operation.`);
468
+ if (!connection) {
469
+ let modelName = this.getModelName();
470
+ let modelContext = this.getModelContext();
471
+ let globalContext = AsyncStore.getContextValue(AsyncStore.CONNECTION_KEY);
472
+ let boundConnection = this._mythixBoundConnection;
473
+ let debugInfo = AsyncStore.getContextDebugInfo();
474
+
475
+ let errorMessage = [
476
+ `${modelName}.getConnection(): No connection available.`,
477
+ '',
478
+ 'Resolution order checked:',
479
+ ` 1. Explicit argument: ${_connection ? 'provided' : 'not provided'}`,
480
+ ` 2. Model context (${modelName}): ${modelContext?.connection ? 'found' : 'not set'}`,
481
+ ` 3. Global context (CONNECTION_KEY): ${globalContext ? 'found' : 'not set'}`,
482
+ ` 4. Bound connection: ${boundConnection ? 'found' : 'not set'}`,
483
+ '',
484
+ 'Context debug info:',
485
+ ` - Inside AsyncLocalStorage context: ${debugInfo.hasContext ? 'yes' : 'no'}`,
486
+ ` - Context keys: [${debugInfo.contextKeys.map((k) => (typeof k === 'symbol' ? k.toString() : `"${k}"`)).join(', ')}]`,
487
+ ` - Parent depth: ${debugInfo.parentDepth}`,
488
+ '',
489
+ 'Possible solutions:',
490
+ ' - Wrap your code in connection.createContext(callback)',
491
+ ' - Pass a connection explicitly: Model.where(connection).field.EQ(value)',
492
+ ' - Ensure bindModels is enabled when creating the connection',
493
+ ' - For event handlers, use connection.bindCallback(callback)',
494
+ ].join('\n');
495
+
496
+ throw new Error(errorMessage);
497
+ }
470
498
 
471
499
  return connection;
472
500
  }
@@ -57,7 +57,7 @@ class CharType extends Type {
57
57
  if (value == null)
58
58
  return value;
59
59
 
60
- if (!Nife.instanceOf('string') || value.length !== 1)
60
+ if (!Nife.instanceOf(value, 'string') || value.length !== 1)
61
61
  throw new TypeError(`CharType::castToType: Value provided ("${value}") can not be cast into a char. Please provide a string that is one character wide.`);
62
62
 
63
63
  return value.charAt(0);
@@ -105,7 +105,7 @@ class NumericType extends Type {
105
105
 
106
106
  let number = parseFloat(('' + value));
107
107
  if (!isFinite(number))
108
- throw new TypeError(`FloatType::castToType: Value provided ("${value}") can not be cast into an floating point number.`);
108
+ throw new TypeError(`NumericType::castToType: Value provided ("${value}") can not be cast into a floating point number.`);
109
109
 
110
110
  return number;
111
111
  }
@@ -13,7 +13,7 @@
13
13
  ///! support... which simply means that connection instances need
14
14
  ///! to be manually passed around everywhere.
15
15
  ///!
16
- ///! **!WARNING!: Never set the `'connection'` key, or a string key
16
+ ///! **!WARNING!: Never set the <see>CONNECTION_KEY</see> Symbol, or a string key
17
17
  ///! that matches one of your model names to this `AsyncLocalStorage` context
18
18
  ///! unless you know exactly what you are doing. These keys are reserved
19
19
  ///! by Mythix ORM to pass connections and transactions through calls.** Any
@@ -26,12 +26,23 @@
26
26
 
27
27
  'use strict';
28
28
 
29
+ /// The Symbol key used to store the connection in AsyncLocalStorage context.
30
+ /// This is exported so users can access the context directly if needed.
31
+ ///
32
+ /// Example:
33
+ /// const { CONNECTION_KEY } = require('mythix-orm').Utils.AsyncStore;
34
+ /// const connection = AsyncStore.getContextValue(CONNECTION_KEY);
35
+ const CONNECTION_KEY = Symbol.for('mythix-orm:connection');
36
+
29
37
  let globalAsyncStore = global._mythixGlobalAsyncLocalStore;
38
+ let AsyncLocalStorageClass = null;
39
+ let debugEnabled = process.env.MYTHIX_ORM_DEBUG === '1' || process.env.MYTHIX_ORM_DEBUG === 'true';
30
40
 
31
41
  if (!globalAsyncStore) {
32
42
  try {
33
- const { AsyncLocalStorage } = require('async_hooks');
34
- global._mythixGlobalAsyncLocalStore = globalAsyncStore = new AsyncLocalStorage();
43
+ const asyncHooks = require('async_hooks');
44
+ AsyncLocalStorageClass = asyncHooks.AsyncLocalStorage;
45
+ global._mythixGlobalAsyncLocalStore = globalAsyncStore = new AsyncLocalStorageClass();
35
46
  } catch (error) {
36
47
  global._mythixGlobalAsyncLocalStore = globalAsyncStore = {
37
48
  getStore: () => undefined,
@@ -40,6 +51,33 @@ if (!globalAsyncStore) {
40
51
  }
41
52
  }
42
53
 
54
+ /// Enable or disable debug mode for AsyncStore operations.
55
+ /// When enabled, context operations will be logged to console.
56
+ ///
57
+ /// Debug mode can also be enabled via the `MYTHIX_ORM_DEBUG=1` environment variable.
58
+ ///
59
+ /// Arguments:
60
+ /// enabled: boolean
61
+ /// Whether to enable debug logging.
62
+ ///
63
+ /// Return: undefined
64
+ function setDebug(enabled) {
65
+ debugEnabled = !!enabled;
66
+ }
67
+
68
+ /// Check if debug mode is currently enabled.
69
+ ///
70
+ /// Return: boolean
71
+ /// True if debug mode is enabled.
72
+ function isDebugEnabled() {
73
+ return debugEnabled;
74
+ }
75
+
76
+ function debugLog(...args) {
77
+ if (debugEnabled)
78
+ console.log('[mythix-orm:AsyncStore]', ...args);
79
+ }
80
+
43
81
  /// Fetch the AsyncLocalStorage store.
44
82
  /// This calls `.getStore()` on the global
45
83
  /// `AsyncLocalStorage` instance.
@@ -51,6 +89,22 @@ function getContextStore() {
51
89
  return globalAsyncStore.getStore();
52
90
  }
53
91
 
92
+ /// Check if code is currently running inside an AsyncLocalStorage context.
93
+ ///
94
+ /// Return: boolean
95
+ /// True if a context exists, false otherwise.
96
+ function hasContext() {
97
+ return globalAsyncStore.getStore() !== undefined;
98
+ }
99
+
100
+ /// Check if a connection is available in the current AsyncLocalStorage context.
101
+ ///
102
+ /// Return: boolean
103
+ /// True if a connection is available in context, false otherwise.
104
+ function hasConnection() {
105
+ return !!getContextValue(CONNECTION_KEY);
106
+ }
107
+
54
108
  /// Get a specific value from the global `AsyncLocalStorage` context
55
109
  /// by name.
56
110
  ///
@@ -119,6 +173,8 @@ function setContextValue(key, value) {
119
173
  /// Return: any
120
174
  /// The return value from the callback.
121
175
  function runInContext(context, callback) {
176
+ debugLog('Entering context with keys:', context ? [...context.keys()] : []);
177
+
122
178
  return globalAsyncStore.run(
123
179
  {
124
180
  parent: globalAsyncStore.getStore(),
@@ -128,9 +184,115 @@ function runInContext(context, callback) {
128
184
  );
129
185
  }
130
186
 
187
+ /// Capture the current AsyncLocalStorage context and return a function
188
+ /// that will execute callbacks within that captured context.
189
+ ///
190
+ /// This is useful for preserving context across event emitters, setTimeout,
191
+ /// or other callbacks where context might otherwise be lost.
192
+ ///
193
+ /// Note:
194
+ /// This uses AsyncLocalStorage.snapshot() when available (Node.js 20+),
195
+ /// with a fallback implementation for older versions.
196
+ ///
197
+ /// Return: Function
198
+ /// A function that takes a callback and executes it in the captured context.
199
+ /// If no context exists when captured, callbacks will run without context.
200
+ ///
201
+ /// Example:
202
+ /// const runInCapturedContext = captureContext();
203
+ ///
204
+ /// eventEmitter.on('someEvent', () => {
205
+ /// runInCapturedContext(() => {
206
+ /// // Models work here - context is preserved
207
+ /// const user = await User.where.id.EQ('123').first();
208
+ /// });
209
+ /// });
210
+ function captureContext() {
211
+ // Use native snapshot() if available (Node.js 20+)
212
+ if (AsyncLocalStorageClass && typeof AsyncLocalStorageClass.snapshot === 'function') {
213
+ debugLog('Capturing context via snapshot()');
214
+ return AsyncLocalStorageClass.snapshot();
215
+ }
216
+
217
+ // Fallback: manually capture current store
218
+ const capturedStore = globalAsyncStore.getStore();
219
+ debugLog('Capturing context manually, has context:', !!capturedStore);
220
+
221
+ return function runInCapturedContext(callback) {
222
+ if (!capturedStore)
223
+ return callback();
224
+
225
+ return globalAsyncStore.run(capturedStore, callback);
226
+ };
227
+ }
228
+
229
+ /// Wrap a callback function to preserve the current AsyncLocalStorage context.
230
+ ///
231
+ /// This is a convenience wrapper around <see>captureContext</see> for use with
232
+ /// event handlers and other callbacks.
233
+ ///
234
+ /// Arguments:
235
+ /// callback: Function
236
+ /// The callback function to wrap.
237
+ ///
238
+ /// Return: Function
239
+ /// A wrapped version of the callback that will execute in the captured context.
240
+ ///
241
+ /// Example:
242
+ /// eventEmitter.on('data', bindCallback(async (data) => {
243
+ /// // Context preserved - models work here
244
+ /// await processData(data);
245
+ /// }));
246
+ function bindCallback(callback) {
247
+ const runInCapturedContext = captureContext();
248
+
249
+ return function boundCallback(...args) {
250
+ return runInCapturedContext(() => callback.apply(this, args));
251
+ };
252
+ }
253
+
254
+ /// Get debugging information about the current context state.
255
+ /// Useful for troubleshooting context-related issues.
256
+ ///
257
+ /// Return: object
258
+ /// An object containing:
259
+ /// - hasContext: boolean - whether a context exists
260
+ /// - hasConnection: boolean - whether a connection is in context
261
+ /// - contextKeys: Array - keys present in the current context
262
+ /// - parentDepth: number - how many parent contexts exist
263
+ function getContextDebugInfo() {
264
+ let store = globalAsyncStore.getStore();
265
+ let parentDepth = 0;
266
+ let contextKeys = [];
267
+
268
+ if (store && store.context)
269
+ contextKeys = [...store.context.keys()];
270
+
271
+ let tempStore = store;
272
+ while (tempStore && tempStore.parent) {
273
+ parentDepth++;
274
+ tempStore = tempStore.parent;
275
+ }
276
+
277
+ return {
278
+ hasContext: !!store,
279
+ hasConnection: hasConnection(),
280
+ contextKeys,
281
+ parentDepth,
282
+ };
283
+ }
284
+
131
285
  module.exports = {
286
+ CONNECTION_KEY,
132
287
  getContextValue,
133
288
  setContextValue,
134
289
  runInContext,
135
290
  getContextStore,
291
+ hasContext,
292
+ hasConnection,
293
+ captureContext,
294
+ bindCallback,
295
+ setDebug,
296
+ isDebugEnabled,
297
+ getContextDebugInfo,
136
298
  };
@@ -34,10 +34,18 @@ const {
34
34
  } = QueryUtils;
35
35
 
36
36
  const {
37
+ CONNECTION_KEY,
37
38
  getContextStore,
38
39
  getContextValue,
39
40
  setContextValue,
40
41
  runInContext,
42
+ hasContext,
43
+ hasConnection,
44
+ captureContext,
45
+ bindCallback,
46
+ setDebug,
47
+ isDebugEnabled,
48
+ getContextDebugInfo,
41
49
  } = AsyncStore;
42
50
 
43
51
  module.exports = {
@@ -72,8 +80,16 @@ module.exports = {
72
80
  mergeFields,
73
81
 
74
82
  // AsyncStore
83
+ CONNECTION_KEY,
75
84
  getContextStore,
76
85
  getContextValue,
77
86
  setContextValue,
78
87
  runInContext,
88
+ hasContext,
89
+ hasConnection,
90
+ captureContext,
91
+ bindCallback,
92
+ setDebug,
93
+ isDebugEnabled,
94
+ getContextDebugInfo,
79
95
  };
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "mythix-orm",
3
- "version": "1.14.0",
3
+ "version": "1.15.0",
4
4
  "description": "ORM for Mythix framework",
5
5
  "main": "lib/index",
6
6
  "type": "commonjs",
7
+ "engines": {
8
+ "node": ">=18.0.0"
9
+ },
7
10
  "scripts": {
8
11
  "coverage": "clear ; node ./node_modules/.bin/nyc ./node_modules/.bin/jasmine",
9
12
  "test": "node ./node_modules/.bin/jasmine",
@@ -34,20 +37,20 @@
34
37
  "homepage": "https://github.com/th317erd/mythix-orm#readme",
35
38
  "devDependencies": {
36
39
  "@spothero/eslint-plugin-spothero": "github:spothero/eslint-plugin-spothero",
37
- "@types/node": "^18.11.18",
38
- "better-sqlite3": "^8.0.1",
40
+ "@types/node": "^22.15.0",
41
+ "better-sqlite3": "^12.6.2",
39
42
  "colors": "^1.4.0",
40
- "diff": "^5.1.0",
41
- "eslint": "^8.31.0",
42
- "jasmine": "^4.5.0",
43
- "nyc": "^15.1.0"
43
+ "diff": "^7.0.0",
44
+ "eslint": "^8.57.1",
45
+ "jasmine": "^5.6.0",
46
+ "nyc": "^17.1.0"
44
47
  },
45
48
  "dependencies": {
46
49
  "events": "^3.3.0",
47
- "inflection": "^2.0.1",
48
- "luxon": "^3.2.1",
50
+ "inflection": "^3.0.0",
51
+ "luxon": "^3.7.2",
49
52
  "nife": "^1.12.1",
50
- "uuid": "^9.0.0",
53
+ "uuid": "^11.1.0",
51
54
  "xid-js": "^1.0.1"
52
55
  },
53
56
  "nyc": {