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 +1 -0
- package/lib/connection/connection-base.js +48 -1
- package/lib/model.js +31 -3
- package/lib/types/concrete/char-type.js +1 -1
- package/lib/types/concrete/numeric-type.js +1 -1
- package/lib/utils/async-store.js +165 -3
- package/lib/utils/index.js +16 -0
- package/package.json +13 -10
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(
|
|
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(
|
|
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
|
-
|
|
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(`
|
|
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
|
}
|
package/lib/utils/async-store.js
CHANGED
|
@@ -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
|
|
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
|
|
34
|
-
|
|
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
|
};
|
package/lib/utils/index.js
CHANGED
|
@@ -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.
|
|
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": "^
|
|
38
|
-
"better-sqlite3": "^
|
|
40
|
+
"@types/node": "^22.15.0",
|
|
41
|
+
"better-sqlite3": "^12.6.2",
|
|
39
42
|
"colors": "^1.4.0",
|
|
40
|
-
"diff": "^
|
|
41
|
-
"eslint": "^8.
|
|
42
|
-
"jasmine": "^
|
|
43
|
-
"nyc": "^
|
|
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": "^
|
|
48
|
-
"luxon": "^3.2
|
|
50
|
+
"inflection": "^3.0.0",
|
|
51
|
+
"luxon": "^3.7.2",
|
|
49
52
|
"nife": "^1.12.1",
|
|
50
|
-
"uuid": "^
|
|
53
|
+
"uuid": "^11.1.0",
|
|
51
54
|
"xid-js": "^1.0.1"
|
|
52
55
|
},
|
|
53
56
|
"nyc": {
|