spice-js 2.7.29 → 2.7.31
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/build/index.js +3 -1
- package/build/models/SpiceModel.js +306 -81
- package/package.json +1 -1
- package/src/index.js +3 -1
- package/src/models/SpiceModel.js +277 -26
- package/tests/__mocks__/globalMocks.js +3 -1
- package/tests/fixtures/testData.js +3 -1
- package/tests/helpers/modelFactory.js +7 -1
- package/tests/jest-env.js +3 -1
- package/tests/models/SpiceModel.mapping-depth.test.js +320 -0
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
var path = require("path");
|
|
3
3
|
//import IO from "koa-socket-2";
|
|
4
4
|
const cors = require("kcors");
|
|
5
|
-
|
|
5
|
+
// Preserve an existing host/test configuration when the package is imported.
|
|
6
|
+
// Production boot still starts from an empty object because spice is undefined.
|
|
7
|
+
global.spice = global.spice || {};
|
|
6
8
|
|
|
7
9
|
require("dotenv").config({ path: path.resolve("./") });
|
|
8
10
|
|
package/src/models/SpiceModel.js
CHANGED
|
@@ -22,9 +22,66 @@ var SDate = require("sonover-date"),
|
|
|
22
22
|
_serializers = Symbol(),
|
|
23
23
|
_level = Symbol(),
|
|
24
24
|
_mapping_dept = Symbol(),
|
|
25
|
+
_mapping_dept_scalar = Symbol(),
|
|
26
|
+
_mapping_dept_array = Symbol(),
|
|
27
|
+
_mapping_concurrency = Symbol(),
|
|
28
|
+
_skip_mapped_serialization = Symbol(),
|
|
25
29
|
_mapping_dept_exempt = Symbol(),
|
|
26
30
|
_current_path = Symbol();
|
|
27
31
|
|
|
32
|
+
const GLOBAL_MAPPING_CONCURRENCY = Math.max(
|
|
33
|
+
1,
|
|
34
|
+
Number(process.env.SPICE_MAPPING_GLOBAL_CONCURRENCY) || 32,
|
|
35
|
+
);
|
|
36
|
+
let activeGlobalMappings = 0;
|
|
37
|
+
const globalMappingWaiters = [];
|
|
38
|
+
|
|
39
|
+
function normalizeConcurrency(value, fallback = 1) {
|
|
40
|
+
const parsed = Number(value);
|
|
41
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function acquireGlobalMappingSlot() {
|
|
45
|
+
const queuedAt = process.hrtime.bigint();
|
|
46
|
+
if (activeGlobalMappings < GLOBAL_MAPPING_CONCURRENCY) {
|
|
47
|
+
activeGlobalMappings += 1;
|
|
48
|
+
} else {
|
|
49
|
+
await new Promise((resolve) => globalMappingWaiters.push(resolve));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
active: activeGlobalMappings,
|
|
54
|
+
waitMs: Number(process.hrtime.bigint() - queuedAt) / 1e6,
|
|
55
|
+
release() {
|
|
56
|
+
const next = globalMappingWaiters.shift();
|
|
57
|
+
if (next) {
|
|
58
|
+
next();
|
|
59
|
+
} else {
|
|
60
|
+
activeGlobalMappings -= 1;
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runWithConcurrency(items, concurrency, worker) {
|
|
67
|
+
const results = new Array(items.length);
|
|
68
|
+
let nextIndex = 0;
|
|
69
|
+
|
|
70
|
+
async function runWorker() {
|
|
71
|
+
while (nextIndex < items.length) {
|
|
72
|
+
const index = nextIndex;
|
|
73
|
+
nextIndex += 1;
|
|
74
|
+
results[index] = await worker(items[index], index);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const workerCount = Math.min(concurrency, items.length);
|
|
79
|
+
await Promise.all(
|
|
80
|
+
Array.from({ length: workerCount }, () => runWorker()),
|
|
81
|
+
);
|
|
82
|
+
return results;
|
|
83
|
+
}
|
|
84
|
+
|
|
28
85
|
//const _type = Symbol("type");
|
|
29
86
|
|
|
30
87
|
let that;
|
|
@@ -47,12 +104,36 @@ if (!Promise.allSettled) {
|
|
|
47
104
|
export default class SpiceModel {
|
|
48
105
|
constructor(args = {}) {
|
|
49
106
|
try {
|
|
107
|
+
const modelArgs = args?.args || {};
|
|
108
|
+
const requestProfile = modelArgs?.ctx?.state?.read_profile || {};
|
|
109
|
+
const defaultMappingDept = process.env.MAPPING_DEPT || 3;
|
|
110
|
+
const legacyMappingDept =
|
|
111
|
+
modelArgs.mapping_dept !== undefined && modelArgs.mapping_dept !== null ?
|
|
112
|
+
modelArgs.mapping_dept
|
|
113
|
+
: undefined;
|
|
114
|
+
|
|
50
115
|
var dbtype =
|
|
51
116
|
spice.config.database.connections[args.connection].type || "couchbase";
|
|
52
117
|
let Database = require(`spice-${dbtype}`);
|
|
53
|
-
this[_mapping_dept] =
|
|
54
|
-
|
|
55
|
-
|
|
118
|
+
this[_mapping_dept] = legacyMappingDept ?? defaultMappingDept;
|
|
119
|
+
this[_mapping_dept_scalar] =
|
|
120
|
+
modelArgs.mapping_dept_scalar ??
|
|
121
|
+
legacyMappingDept ??
|
|
122
|
+
requestProfile.mapping_dept_scalar ??
|
|
123
|
+
defaultMappingDept;
|
|
124
|
+
this[_mapping_dept_array] =
|
|
125
|
+
modelArgs.mapping_dept_array ??
|
|
126
|
+
legacyMappingDept ??
|
|
127
|
+
requestProfile.mapping_dept_array ??
|
|
128
|
+
defaultMappingDept;
|
|
129
|
+
this[_mapping_concurrency] = normalizeConcurrency(
|
|
130
|
+
modelArgs.mapping_concurrency ?? requestProfile.mapping_concurrency,
|
|
131
|
+
);
|
|
132
|
+
this[_skip_mapped_serialization] =
|
|
133
|
+
modelArgs.skip_mapped_serialization ??
|
|
134
|
+
requestProfile.skip_mapped_serialization ??
|
|
135
|
+
false;
|
|
136
|
+
this[_mapping_dept_exempt] = modelArgs.mapping_dept_exempt || [];
|
|
56
137
|
this.type = "";
|
|
57
138
|
this.collection = args.connection;
|
|
58
139
|
this[_args] = args.args;
|
|
@@ -237,6 +318,41 @@ export default class SpiceModel {
|
|
|
237
318
|
// }
|
|
238
319
|
}
|
|
239
320
|
|
|
321
|
+
applyMappingDepthArgs(args = {}) {
|
|
322
|
+
const hasLegacyDepth =
|
|
323
|
+
args.mapping_dept !== undefined && args.mapping_dept !== null;
|
|
324
|
+
|
|
325
|
+
if (hasLegacyDepth) {
|
|
326
|
+
this[_mapping_dept] = args.mapping_dept;
|
|
327
|
+
this[_mapping_dept_scalar] = args.mapping_dept;
|
|
328
|
+
this[_mapping_dept_array] = args.mapping_dept;
|
|
329
|
+
}
|
|
330
|
+
if (
|
|
331
|
+
args.mapping_dept_scalar !== undefined &&
|
|
332
|
+
args.mapping_dept_scalar !== null
|
|
333
|
+
) {
|
|
334
|
+
this[_mapping_dept_scalar] = args.mapping_dept_scalar;
|
|
335
|
+
}
|
|
336
|
+
if (
|
|
337
|
+
args.mapping_dept_array !== undefined &&
|
|
338
|
+
args.mapping_dept_array !== null
|
|
339
|
+
) {
|
|
340
|
+
this[_mapping_dept_array] = args.mapping_dept_array;
|
|
341
|
+
}
|
|
342
|
+
if (args.skip_mapped_serialization !== undefined) {
|
|
343
|
+
this[_skip_mapped_serialization] = args.skip_mapped_serialization;
|
|
344
|
+
}
|
|
345
|
+
if (args.mapping_concurrency !== undefined) {
|
|
346
|
+
this[_mapping_concurrency] = normalizeConcurrency(
|
|
347
|
+
args.mapping_concurrency,
|
|
348
|
+
this[_mapping_concurrency],
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (args.mapping_dept_exempt) {
|
|
352
|
+
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
240
356
|
get database() {
|
|
241
357
|
return this[_database];
|
|
242
358
|
}
|
|
@@ -515,13 +631,10 @@ export default class SpiceModel {
|
|
|
515
631
|
const p = this[_ctx]?.profiler;
|
|
516
632
|
|
|
517
633
|
const doGet = async () => {
|
|
518
|
-
if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
|
|
519
|
-
if (args.mapping_dept_exempt)
|
|
520
|
-
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
521
|
-
|
|
522
634
|
if (!args) {
|
|
523
635
|
args = {};
|
|
524
636
|
}
|
|
637
|
+
this.applyMappingDepthArgs(args);
|
|
525
638
|
if (_.isString(args.id)) {
|
|
526
639
|
if (args.skip_hooks !== true) {
|
|
527
640
|
await this.run_hook(args, "get", "before");
|
|
@@ -687,10 +800,13 @@ export default class SpiceModel {
|
|
|
687
800
|
if (!args) {
|
|
688
801
|
args = {};
|
|
689
802
|
}
|
|
803
|
+
this.applyMappingDepthArgs(args);
|
|
690
804
|
if (args.skip_hooks != true) {
|
|
691
805
|
await this.run_hook(this, "list", "before");
|
|
692
806
|
}
|
|
693
|
-
|
|
807
|
+
args.ids = _.uniq(
|
|
808
|
+
_.filter(args.ids || [], (id) => _.isString(id) && id.length > 0),
|
|
809
|
+
);
|
|
694
810
|
let key = `multi-get::${this.type}::${_.join(args.ids, "|")}:${args.columns}`;
|
|
695
811
|
let results = [];
|
|
696
812
|
|
|
@@ -1384,9 +1500,7 @@ export default class SpiceModel {
|
|
|
1384
1500
|
const p = this[_ctx]?.profiler;
|
|
1385
1501
|
|
|
1386
1502
|
const doList = async () => {
|
|
1387
|
-
|
|
1388
|
-
if (args.mapping_dept_exempt)
|
|
1389
|
-
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
1503
|
+
this.applyMappingDepthArgs(args);
|
|
1390
1504
|
|
|
1391
1505
|
// Filter out id, meta().id, and any column ending with .id (e.g., dependents.id)
|
|
1392
1506
|
if (args.columns) {
|
|
@@ -1558,6 +1672,13 @@ export default class SpiceModel {
|
|
|
1558
1672
|
|
|
1559
1673
|
const doFetch = async () => {
|
|
1560
1674
|
if (args.is_custom_query === "true" && args.ids.length > 0) {
|
|
1675
|
+
console.log(
|
|
1676
|
+
"Fetching results from database",
|
|
1677
|
+
this.type,
|
|
1678
|
+
args,
|
|
1679
|
+
query,
|
|
1680
|
+
mappedNestings,
|
|
1681
|
+
);
|
|
1561
1682
|
return await this.database.query(query);
|
|
1562
1683
|
} else if (args.is_full_text === "true") {
|
|
1563
1684
|
return await this.database.full_text_search(
|
|
@@ -1717,7 +1838,9 @@ export default class SpiceModel {
|
|
|
1717
1838
|
data = Array.of(data);
|
|
1718
1839
|
}
|
|
1719
1840
|
const isExempt = this.isFieldExempt(source_property);
|
|
1720
|
-
|
|
1841
|
+
const activeMappingDept =
|
|
1842
|
+
type === "read" ? this[_mapping_dept_scalar] : this[_mapping_dept];
|
|
1843
|
+
if (isExempt || this[_level] + 1 < activeMappingDept) {
|
|
1721
1844
|
let classes = _.compact(_.isArray(Class) ? Class : [Class]);
|
|
1722
1845
|
|
|
1723
1846
|
let ids = [];
|
|
@@ -1761,6 +1884,10 @@ export default class SpiceModel {
|
|
|
1761
1884
|
skip_cache: this[_skip_cache],
|
|
1762
1885
|
_level: this[_level] + 1,
|
|
1763
1886
|
mapping_dept: this[_mapping_dept],
|
|
1887
|
+
mapping_dept_scalar: this[_mapping_dept_scalar],
|
|
1888
|
+
mapping_dept_array: this[_mapping_dept_array],
|
|
1889
|
+
mapping_concurrency: this[_mapping_concurrency],
|
|
1890
|
+
skip_mapped_serialization: this[_skip_mapped_serialization],
|
|
1764
1891
|
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1765
1892
|
_columns: columnArray.join(","),
|
|
1766
1893
|
_current_path: childPath,
|
|
@@ -1770,6 +1897,7 @@ export default class SpiceModel {
|
|
|
1770
1897
|
skip_hooks: true,
|
|
1771
1898
|
ids: ids,
|
|
1772
1899
|
columns: columnArray,
|
|
1900
|
+
skip_read_serialize: this[_skip_mapped_serialization],
|
|
1773
1901
|
});
|
|
1774
1902
|
}),
|
|
1775
1903
|
);
|
|
@@ -1881,7 +2009,9 @@ export default class SpiceModel {
|
|
|
1881
2009
|
data = Array.of(data);
|
|
1882
2010
|
}
|
|
1883
2011
|
const isExempt = this.isFieldExempt(source_property);
|
|
1884
|
-
|
|
2012
|
+
const activeMappingDept =
|
|
2013
|
+
type === "read" ? this[_mapping_dept_array] : this[_mapping_dept];
|
|
2014
|
+
if (isExempt || this[_level] + 1 < activeMappingDept) {
|
|
1885
2015
|
let ids = [];
|
|
1886
2016
|
_.each(data, (result) => {
|
|
1887
2017
|
let value = result[source_property];
|
|
@@ -1932,6 +2062,10 @@ export default class SpiceModel {
|
|
|
1932
2062
|
skip_cache: this[_skip_cache],
|
|
1933
2063
|
_level: this[_level] + 1,
|
|
1934
2064
|
mapping_dept: this[_mapping_dept],
|
|
2065
|
+
mapping_dept_scalar: this[_mapping_dept_scalar],
|
|
2066
|
+
mapping_dept_array: this[_mapping_dept_array],
|
|
2067
|
+
mapping_concurrency: this[_mapping_concurrency],
|
|
2068
|
+
skip_mapped_serialization: this[_skip_mapped_serialization],
|
|
1935
2069
|
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1936
2070
|
_columns: columnArray.join(","),
|
|
1937
2071
|
_current_path: childPath,
|
|
@@ -1939,6 +2073,7 @@ export default class SpiceModel {
|
|
|
1939
2073
|
skip_hooks: true,
|
|
1940
2074
|
ids: ids,
|
|
1941
2075
|
columns: columnArray,
|
|
2076
|
+
skip_read_serialize: this[_skip_mapped_serialization],
|
|
1942
2077
|
});
|
|
1943
2078
|
}),
|
|
1944
2079
|
);
|
|
@@ -2012,8 +2147,21 @@ export default class SpiceModel {
|
|
|
2012
2147
|
return original_is_array ? data : data[0];
|
|
2013
2148
|
}
|
|
2014
2149
|
|
|
2015
|
-
addModifier({
|
|
2150
|
+
addModifier({
|
|
2151
|
+
when,
|
|
2152
|
+
execute,
|
|
2153
|
+
kind,
|
|
2154
|
+
source_property,
|
|
2155
|
+
store_property,
|
|
2156
|
+
}) {
|
|
2016
2157
|
if (this[_serializers][when]) {
|
|
2158
|
+
if (kind) {
|
|
2159
|
+
execute.spiceModifier = {
|
|
2160
|
+
kind,
|
|
2161
|
+
source_property,
|
|
2162
|
+
store_property,
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2017
2165
|
this[_serializers][when]["modifiers"].push(execute);
|
|
2018
2166
|
}
|
|
2019
2167
|
}
|
|
@@ -2087,6 +2235,9 @@ export default class SpiceModel {
|
|
|
2087
2235
|
case "string": {
|
|
2088
2236
|
this.addModifier({
|
|
2089
2237
|
when: properties[i].map.when || "read",
|
|
2238
|
+
kind: "relationship_map",
|
|
2239
|
+
source_property: i,
|
|
2240
|
+
store_property: properties[i].map.destination || i,
|
|
2090
2241
|
execute: async (
|
|
2091
2242
|
data,
|
|
2092
2243
|
old_data,
|
|
@@ -2096,6 +2247,7 @@ export default class SpiceModel {
|
|
|
2096
2247
|
mapping_dept,
|
|
2097
2248
|
level,
|
|
2098
2249
|
columns,
|
|
2250
|
+
serializationType,
|
|
2099
2251
|
) => {
|
|
2100
2252
|
// Skip if columns are specified but this field isn't included
|
|
2101
2253
|
if (!this.isFieldInColumns(i, columns)) {
|
|
@@ -2110,7 +2262,7 @@ export default class SpiceModel {
|
|
|
2110
2262
|
properties[i].map.destination || i,
|
|
2111
2263
|
properties[i],
|
|
2112
2264
|
args,
|
|
2113
|
-
|
|
2265
|
+
serializationType,
|
|
2114
2266
|
mapping_dept,
|
|
2115
2267
|
level,
|
|
2116
2268
|
columns,
|
|
@@ -2123,6 +2275,9 @@ export default class SpiceModel {
|
|
|
2123
2275
|
case "array": {
|
|
2124
2276
|
this.addModifier({
|
|
2125
2277
|
when: properties[i].map.when || "read",
|
|
2278
|
+
kind: "relationship_map",
|
|
2279
|
+
source_property: i,
|
|
2280
|
+
store_property: properties[i].map.destination || i,
|
|
2126
2281
|
execute: async (
|
|
2127
2282
|
data,
|
|
2128
2283
|
old_data,
|
|
@@ -2132,6 +2287,7 @@ export default class SpiceModel {
|
|
|
2132
2287
|
mapping_dept,
|
|
2133
2288
|
level,
|
|
2134
2289
|
columns,
|
|
2290
|
+
serializationType,
|
|
2135
2291
|
) => {
|
|
2136
2292
|
// Skip if columns are specified but this field isn't included
|
|
2137
2293
|
if (!this.isFieldInColumns(i, columns)) {
|
|
@@ -2147,7 +2303,7 @@ export default class SpiceModel {
|
|
|
2147
2303
|
properties[i].map.destination || i,
|
|
2148
2304
|
properties[i],
|
|
2149
2305
|
args,
|
|
2150
|
-
|
|
2306
|
+
serializationType,
|
|
2151
2307
|
mapping_dept,
|
|
2152
2308
|
level,
|
|
2153
2309
|
columns,
|
|
@@ -2185,11 +2341,10 @@ export default class SpiceModel {
|
|
|
2185
2341
|
|
|
2186
2342
|
// Cache the modifiers lookup for the specified type.
|
|
2187
2343
|
const modifiers = this[_serializers]?.[type]?.modifiers || [];
|
|
2188
|
-
|
|
2189
|
-
const modifier = modifiers[i];
|
|
2344
|
+
const invokeModifier = async (modifier, index, inputData) => {
|
|
2190
2345
|
try {
|
|
2191
2346
|
const result = await modifier(
|
|
2192
|
-
|
|
2347
|
+
inputData,
|
|
2193
2348
|
old_data,
|
|
2194
2349
|
this[_ctx],
|
|
2195
2350
|
this.type,
|
|
@@ -2197,23 +2352,119 @@ export default class SpiceModel {
|
|
|
2197
2352
|
this[_mapping_dept],
|
|
2198
2353
|
this[_level],
|
|
2199
2354
|
this[_columns],
|
|
2355
|
+
type,
|
|
2200
2356
|
);
|
|
2201
|
-
// Guard against modifiers that return undefined
|
|
2202
2357
|
if (result !== undefined) {
|
|
2203
|
-
|
|
2204
|
-
} else {
|
|
2205
|
-
console.warn(
|
|
2206
|
-
`Modifier #${i} for type=${this.type} returned undefined, keeping previous data`,
|
|
2207
|
-
);
|
|
2358
|
+
return { hasValue: true, value: result };
|
|
2208
2359
|
}
|
|
2360
|
+
console.warn(
|
|
2361
|
+
`Modifier #${index} for type=${this.type} returned undefined, keeping previous data`,
|
|
2362
|
+
);
|
|
2209
2363
|
} catch (error) {
|
|
2210
2364
|
console.error(
|
|
2211
|
-
`Modifier error in do_serialize (type=${this.type}, modifier #${
|
|
2365
|
+
`Modifier error in do_serialize (type=${this.type}, modifier #${index}):`,
|
|
2212
2366
|
error instanceof Error ?
|
|
2213
2367
|
error.stack
|
|
2214
2368
|
: `Non-Error thrown: ${JSON.stringify(error)}`,
|
|
2215
2369
|
);
|
|
2216
2370
|
}
|
|
2371
|
+
return { hasValue: false };
|
|
2372
|
+
};
|
|
2373
|
+
|
|
2374
|
+
let modifierIndex = 0;
|
|
2375
|
+
while (modifierIndex < modifiers.length) {
|
|
2376
|
+
const modifier = modifiers[modifierIndex];
|
|
2377
|
+
const canRunRelationshipMapsConcurrently =
|
|
2378
|
+
type === "read" &&
|
|
2379
|
+
this[_level] === 0 &&
|
|
2380
|
+
this[_mapping_concurrency] > 1 &&
|
|
2381
|
+
modifier?.spiceModifier?.kind === "relationship_map";
|
|
2382
|
+
|
|
2383
|
+
if (!canRunRelationshipMapsConcurrently) {
|
|
2384
|
+
const result = await invokeModifier(modifier, modifierIndex, data);
|
|
2385
|
+
if (result.hasValue) data = result.value;
|
|
2386
|
+
modifierIndex += 1;
|
|
2387
|
+
continue;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
const group = [];
|
|
2391
|
+
const destinations = new Set();
|
|
2392
|
+
while (modifierIndex < modifiers.length) {
|
|
2393
|
+
const candidate = modifiers[modifierIndex];
|
|
2394
|
+
const metadata = candidate?.spiceModifier;
|
|
2395
|
+
if (metadata?.kind !== "relationship_map") break;
|
|
2396
|
+
if (
|
|
2397
|
+
metadata.store_property &&
|
|
2398
|
+
destinations.has(metadata.store_property)
|
|
2399
|
+
) {
|
|
2400
|
+
break;
|
|
2401
|
+
}
|
|
2402
|
+
if (metadata.store_property) {
|
|
2403
|
+
destinations.add(metadata.store_property);
|
|
2404
|
+
}
|
|
2405
|
+
group.push({ modifier: candidate, index: modifierIndex, metadata });
|
|
2406
|
+
modifierIndex += 1;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
const groupQueuedAt = process.hrtime.bigint();
|
|
2410
|
+
let activeRequestMappings = 0;
|
|
2411
|
+
const executeGroup = async () =>
|
|
2412
|
+
await runWithConcurrency(
|
|
2413
|
+
group,
|
|
2414
|
+
this[_mapping_concurrency],
|
|
2415
|
+
async (entry) => {
|
|
2416
|
+
const requestQueueWaitMs =
|
|
2417
|
+
Number(process.hrtime.bigint() - groupQueuedAt) / 1e6;
|
|
2418
|
+
const globalSlot = await acquireGlobalMappingSlot();
|
|
2419
|
+
activeRequestMappings += 1;
|
|
2420
|
+
const profileMetadata = {
|
|
2421
|
+
source_property: entry.metadata.source_property,
|
|
2422
|
+
store_property: entry.metadata.store_property,
|
|
2423
|
+
request_queue_wait_ms:
|
|
2424
|
+
Math.round(requestQueueWaitMs * 100) / 100,
|
|
2425
|
+
global_queue_wait_ms:
|
|
2426
|
+
Math.round(globalSlot.waitMs * 100) / 100,
|
|
2427
|
+
active_request_mappings: activeRequestMappings,
|
|
2428
|
+
request_limit: this[_mapping_concurrency],
|
|
2429
|
+
active_global_mappings: globalSlot.active,
|
|
2430
|
+
global_limit: GLOBAL_MAPPING_CONCURRENCY,
|
|
2431
|
+
};
|
|
2432
|
+
|
|
2433
|
+
try {
|
|
2434
|
+
const execute = async () =>
|
|
2435
|
+
await invokeModifier(entry.modifier, entry.index, data);
|
|
2436
|
+
if (p) {
|
|
2437
|
+
return await p.track(
|
|
2438
|
+
`${this.type}.map.concurrent.${entry.metadata.source_property}`,
|
|
2439
|
+
execute,
|
|
2440
|
+
profileMetadata,
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
return await execute();
|
|
2444
|
+
} finally {
|
|
2445
|
+
activeRequestMappings -= 1;
|
|
2446
|
+
globalSlot.release();
|
|
2447
|
+
}
|
|
2448
|
+
},
|
|
2449
|
+
);
|
|
2450
|
+
|
|
2451
|
+
const groupMetadata = {
|
|
2452
|
+
map_count: group.length,
|
|
2453
|
+
request_limit: this[_mapping_concurrency],
|
|
2454
|
+
global_limit: GLOBAL_MAPPING_CONCURRENCY,
|
|
2455
|
+
};
|
|
2456
|
+
const groupResults =
|
|
2457
|
+
p ?
|
|
2458
|
+
await p.track(
|
|
2459
|
+
`${this.type}.map.concurrent`,
|
|
2460
|
+
executeGroup,
|
|
2461
|
+
groupMetadata,
|
|
2462
|
+
)
|
|
2463
|
+
: await executeGroup();
|
|
2464
|
+
|
|
2465
|
+
for (const result of groupResults) {
|
|
2466
|
+
if (result?.hasValue) data = result.value;
|
|
2467
|
+
}
|
|
2217
2468
|
}
|
|
2218
2469
|
|
|
2219
2470
|
// Ensure data is always an array for consistent processing.
|
|
@@ -22,8 +22,10 @@ const spiceConfig = {
|
|
|
22
22
|
cache: {},
|
|
23
23
|
cache_providers: {},
|
|
24
24
|
schemas: {},
|
|
25
|
+
models: {},
|
|
25
26
|
modifiers: {},
|
|
26
|
-
schema_extenders: []
|
|
27
|
+
schema_extenders: [],
|
|
28
|
+
getModifiers: jest.fn(() => [])
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
// Set on multiple global scopes to ensure it's available
|
|
@@ -29,6 +29,8 @@ const columnTestCases = {
|
|
|
29
29
|
*/
|
|
30
30
|
const sampleDbResults = {
|
|
31
31
|
users: [
|
|
32
|
+
{ id: 'valid-1', type: 'user', name: 'Valid One', email: 'valid1@example.com' },
|
|
33
|
+
{ id: 'valid-2', type: 'user', name: 'Valid Two', email: 'valid2@example.com' },
|
|
32
34
|
{ id: 'user-1', type: 'user', name: 'Alice', email: 'alice@example.com' },
|
|
33
35
|
{ id: 'user-2', type: 'user', name: 'Bob', email: 'bob@example.com' },
|
|
34
36
|
{ id: 'user-3', type: 'user', name: 'Charlie', email: 'charlie@example.com' }
|
|
@@ -71,7 +73,7 @@ function generateLargeDataset(size = 100) {
|
|
|
71
73
|
for (let i = 0; i < size; i++) {
|
|
72
74
|
data.push({
|
|
73
75
|
id: `item-${i}`,
|
|
74
|
-
type: '
|
|
76
|
+
type: 'user',
|
|
75
77
|
name: `Item ${i}`,
|
|
76
78
|
value: Math.random()
|
|
77
79
|
});
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
const SpiceModel = require('../../src/models/SpiceModel').default;
|
|
6
6
|
const MockDatabase = require('../__mocks__/spice-couchbase');
|
|
7
|
+
const spiceConfig = require('../__mocks__/globalMocks');
|
|
8
|
+
|
|
9
|
+
// Importing SpiceModel loads the package index, which initializes global.spice.
|
|
10
|
+
// Restore the complete test configuration after that package initialization.
|
|
11
|
+
global.spice = spiceConfig;
|
|
12
|
+
globalThis.spice = spiceConfig;
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* Create a test model instance with mocked database
|
|
@@ -32,7 +38,7 @@ function createTestModel(config = {}) {
|
|
|
32
38
|
connection,
|
|
33
39
|
collection: type,
|
|
34
40
|
props,
|
|
35
|
-
args: { ...args, skip_cache: true }
|
|
41
|
+
args: { ...args, skip_cache: true, disable_lifecycle_events: true }
|
|
36
42
|
});
|
|
37
43
|
|
|
38
44
|
// Inject mock database
|
package/tests/jest-env.js
CHANGED
|
@@ -21,8 +21,10 @@ class CustomEnvironment extends NodeEnvironment {
|
|
|
21
21
|
cache: {},
|
|
22
22
|
cache_providers: {},
|
|
23
23
|
schemas: {},
|
|
24
|
+
models: {},
|
|
24
25
|
modifiers: {},
|
|
25
|
-
schema_extenders: []
|
|
26
|
+
schema_extenders: [],
|
|
27
|
+
getModifiers: () => []
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
// Set on the global object for the test environment
|