spice-js 2.7.30 → 2.7.32
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.
|
@@ -7,9 +7,9 @@ var _ResourceLifecycleTriggered = _interopRequireDefault(require("../events/even
|
|
|
7
7
|
var _Security = require("../utility/Security");
|
|
8
8
|
var _fix = require("../utility/fix");
|
|
9
9
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) { ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } } return n; }, _extends.apply(null, arguments); }
|
|
10
11
|
function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
|
|
11
12
|
function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
|
|
12
|
-
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) { ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } } return n; }, _extends.apply(null, arguments); }
|
|
13
13
|
var co = require("co");
|
|
14
14
|
var regeneratorRuntime = require("regenerator-runtime");
|
|
15
15
|
var SDate = require("sonover-date"),
|
|
@@ -29,11 +29,74 @@ var SDate = require("sonover-date"),
|
|
|
29
29
|
_mapping_dept = Symbol(),
|
|
30
30
|
_mapping_dept_scalar = Symbol(),
|
|
31
31
|
_mapping_dept_array = Symbol(),
|
|
32
|
+
_mapping_concurrency = Symbol(),
|
|
33
|
+
_skip_mapped_serialization = Symbol(),
|
|
32
34
|
_mapping_dept_exempt = Symbol(),
|
|
33
35
|
_current_path = Symbol();
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
var GLOBAL_MAPPING_CONCURRENCY = Math.max(1, Number(process.env.SPICE_MAPPING_GLOBAL_CONCURRENCY) || 32);
|
|
37
|
+
var activeGlobalMappings = 0;
|
|
38
|
+
var globalMappingWaiters = [];
|
|
39
|
+
function normalizeConcurrency(value, fallback) {
|
|
40
|
+
if (fallback === void 0) {
|
|
41
|
+
fallback = 1;
|
|
42
|
+
}
|
|
43
|
+
var parsed = Number(value);
|
|
44
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
|
|
45
|
+
}
|
|
46
|
+
function acquireGlobalMappingSlot() {
|
|
47
|
+
return _acquireGlobalMappingSlot.apply(this, arguments);
|
|
48
|
+
}
|
|
49
|
+
function _acquireGlobalMappingSlot() {
|
|
50
|
+
_acquireGlobalMappingSlot = _asyncToGenerator(function* () {
|
|
51
|
+
var queuedAt = process.hrtime.bigint();
|
|
52
|
+
if (activeGlobalMappings < GLOBAL_MAPPING_CONCURRENCY) {
|
|
53
|
+
activeGlobalMappings += 1;
|
|
54
|
+
} else {
|
|
55
|
+
yield new Promise(resolve => globalMappingWaiters.push(resolve));
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
active: activeGlobalMappings,
|
|
59
|
+
waitMs: Number(process.hrtime.bigint() - queuedAt) / 1e6,
|
|
60
|
+
release() {
|
|
61
|
+
var next = globalMappingWaiters.shift();
|
|
62
|
+
if (next) {
|
|
63
|
+
next();
|
|
64
|
+
} else {
|
|
65
|
+
activeGlobalMappings -= 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return _acquireGlobalMappingSlot.apply(this, arguments);
|
|
71
|
+
}
|
|
72
|
+
function runWithConcurrency(_x, _x2, _x3) {
|
|
73
|
+
return _runWithConcurrency.apply(this, arguments);
|
|
74
|
+
} //const _type = Symbol("type");
|
|
75
|
+
function _runWithConcurrency() {
|
|
76
|
+
_runWithConcurrency = _asyncToGenerator(function* (items, concurrency, worker) {
|
|
77
|
+
var results = new Array(items.length);
|
|
78
|
+
var nextIndex = 0;
|
|
79
|
+
function runWorker() {
|
|
80
|
+
return _runWorker.apply(this, arguments);
|
|
81
|
+
}
|
|
82
|
+
function _runWorker() {
|
|
83
|
+
_runWorker = _asyncToGenerator(function* () {
|
|
84
|
+
while (nextIndex < items.length) {
|
|
85
|
+
var index = nextIndex;
|
|
86
|
+
nextIndex += 1;
|
|
87
|
+
results[index] = yield worker(items[index], index);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return _runWorker.apply(this, arguments);
|
|
91
|
+
}
|
|
92
|
+
var workerCount = Math.min(concurrency, items.length);
|
|
93
|
+
yield Promise.all(Array.from({
|
|
94
|
+
length: workerCount
|
|
95
|
+
}, () => runWorker()));
|
|
96
|
+
return results;
|
|
97
|
+
});
|
|
98
|
+
return _runWithConcurrency.apply(this, arguments);
|
|
99
|
+
}
|
|
37
100
|
var that;
|
|
38
101
|
if (!Promise.allSettled) {
|
|
39
102
|
Promise.allSettled = promises => Promise.all(promises.map((promise, i) => promise.then(value => ({
|
|
@@ -50,7 +113,7 @@ class SpiceModel {
|
|
|
50
113
|
args = {};
|
|
51
114
|
}
|
|
52
115
|
try {
|
|
53
|
-
var _args2, _modelArgs$ctx, _modelArgs$ctx$state, _ref, _ref2, _modelArgs$mapping_de, _ref3, _ref4, _modelArgs$mapping_de2, _args$args, _args3, _args3$args, _args4, _args4$args, _args5, _args5$args, _args6, _args6$args, _args7, _args7$args;
|
|
116
|
+
var _args2, _modelArgs$ctx, _modelArgs$ctx$state, _ref, _ref2, _modelArgs$mapping_de, _ref3, _ref4, _modelArgs$mapping_de2, _modelArgs$mapping_co, _ref5, _modelArgs$skip_mappe, _args$args, _args3, _args3$args, _args4, _args4$args, _args5, _args5$args, _args6, _args6$args, _args7, _args7$args;
|
|
54
117
|
var modelArgs = ((_args2 = args) == null ? void 0 : _args2.args) || {};
|
|
55
118
|
var requestProfile = (modelArgs == null ? void 0 : (_modelArgs$ctx = modelArgs.ctx) == null ? void 0 : (_modelArgs$ctx$state = _modelArgs$ctx.state) == null ? void 0 : _modelArgs$ctx$state.read_profile) || {};
|
|
56
119
|
var defaultMappingDept = process.env.MAPPING_DEPT || 3;
|
|
@@ -60,6 +123,8 @@ class SpiceModel {
|
|
|
60
123
|
this[_mapping_dept] = legacyMappingDept != null ? legacyMappingDept : defaultMappingDept;
|
|
61
124
|
this[_mapping_dept_scalar] = (_ref = (_ref2 = (_modelArgs$mapping_de = modelArgs.mapping_dept_scalar) != null ? _modelArgs$mapping_de : legacyMappingDept) != null ? _ref2 : requestProfile.mapping_dept_scalar) != null ? _ref : defaultMappingDept;
|
|
62
125
|
this[_mapping_dept_array] = (_ref3 = (_ref4 = (_modelArgs$mapping_de2 = modelArgs.mapping_dept_array) != null ? _modelArgs$mapping_de2 : legacyMappingDept) != null ? _ref4 : requestProfile.mapping_dept_array) != null ? _ref3 : defaultMappingDept;
|
|
126
|
+
this[_mapping_concurrency] = normalizeConcurrency((_modelArgs$mapping_co = modelArgs.mapping_concurrency) != null ? _modelArgs$mapping_co : requestProfile.mapping_concurrency);
|
|
127
|
+
this[_skip_mapped_serialization] = (_ref5 = (_modelArgs$skip_mappe = modelArgs.skip_mapped_serialization) != null ? _modelArgs$skip_mappe : requestProfile.skip_mapped_serialization) != null ? _ref5 : false;
|
|
63
128
|
this[_mapping_dept_exempt] = modelArgs.mapping_dept_exempt || [];
|
|
64
129
|
this.type = "";
|
|
65
130
|
this.collection = args.connection;
|
|
@@ -233,6 +298,12 @@ class SpiceModel {
|
|
|
233
298
|
if (args.mapping_dept_array !== undefined && args.mapping_dept_array !== null) {
|
|
234
299
|
this[_mapping_dept_array] = args.mapping_dept_array;
|
|
235
300
|
}
|
|
301
|
+
if (args.skip_mapped_serialization !== undefined) {
|
|
302
|
+
this[_skip_mapped_serialization] = args.skip_mapped_serialization;
|
|
303
|
+
}
|
|
304
|
+
if (args.mapping_concurrency !== undefined) {
|
|
305
|
+
this[_mapping_concurrency] = normalizeConcurrency(args.mapping_concurrency, this[_mapping_concurrency]);
|
|
306
|
+
}
|
|
236
307
|
if (args.mapping_dept_exempt) {
|
|
237
308
|
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
238
309
|
}
|
|
@@ -278,13 +349,13 @@ class SpiceModel {
|
|
|
278
349
|
return [];
|
|
279
350
|
})();
|
|
280
351
|
}
|
|
281
|
-
createValidationString(
|
|
352
|
+
createValidationString(_ref6) {
|
|
282
353
|
var {
|
|
283
354
|
property_name,
|
|
284
355
|
props,
|
|
285
356
|
operation,
|
|
286
357
|
complete
|
|
287
|
-
} =
|
|
358
|
+
} = _ref6;
|
|
288
359
|
var operationExempt = {
|
|
289
360
|
PUT: ["required", "unique"]
|
|
290
361
|
};
|
|
@@ -515,7 +586,7 @@ class SpiceModel {
|
|
|
515
586
|
// Profiling: use track() for proper async context forking
|
|
516
587
|
var p = (_this4$_ctx = _this4[_ctx]) == null ? void 0 : _this4$_ctx.profiler;
|
|
517
588
|
var doGet = /*#__PURE__*/function () {
|
|
518
|
-
var
|
|
589
|
+
var _ref7 = _asyncToGenerator(function* () {
|
|
519
590
|
if (!args) {
|
|
520
591
|
args = {};
|
|
521
592
|
}
|
|
@@ -624,7 +695,7 @@ class SpiceModel {
|
|
|
624
695
|
}
|
|
625
696
|
});
|
|
626
697
|
return function doGet() {
|
|
627
|
-
return
|
|
698
|
+
return _ref7.apply(this, arguments);
|
|
628
699
|
};
|
|
629
700
|
}();
|
|
630
701
|
try {
|
|
@@ -641,17 +712,44 @@ class SpiceModel {
|
|
|
641
712
|
}
|
|
642
713
|
})();
|
|
643
714
|
}
|
|
644
|
-
query(query, scope) {
|
|
715
|
+
query(query, scope, options) {
|
|
645
716
|
var _this5 = this;
|
|
646
717
|
return _asyncToGenerator(function* () {
|
|
647
718
|
try {
|
|
648
|
-
var results = yield _this5.database.query(query, scope);
|
|
719
|
+
var results = yield _this5.database.query(query, scope, options);
|
|
649
720
|
return results;
|
|
650
721
|
} catch (error) {
|
|
651
722
|
throw error;
|
|
652
723
|
}
|
|
653
724
|
})();
|
|
654
725
|
}
|
|
726
|
+
getQueryOptions(args) {
|
|
727
|
+
if (args === void 0) {
|
|
728
|
+
args = {};
|
|
729
|
+
}
|
|
730
|
+
var scanConsistency = args.scan_consistency;
|
|
731
|
+
|
|
732
|
+
// Backward compatibility for callers that already use statement_consistent.
|
|
733
|
+
// The adapter accepts the SDK-style value as well as these snake_case keys.
|
|
734
|
+
if (scanConsistency === undefined && args.statement_consistent !== undefined) {
|
|
735
|
+
var legacyValue = args.statement_consistent;
|
|
736
|
+
var isRequestPlus = legacyValue === true || typeof legacyValue === "string" && ["true", "request_plus"].includes(legacyValue.toLowerCase());
|
|
737
|
+
scanConsistency = isRequestPlus ? "request_plus" : "not_bounded";
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
scan_consistency: scanConsistency,
|
|
741
|
+
consistent_with: args.consistent_with,
|
|
742
|
+
scan_wait: args.scan_wait
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
requiresFreshQuery(args) {
|
|
746
|
+
if (args === void 0) {
|
|
747
|
+
args = {};
|
|
748
|
+
}
|
|
749
|
+
var options = this.getQueryOptions(args);
|
|
750
|
+
var scanConsistency = options.scan_consistency;
|
|
751
|
+
return options.consistent_with !== undefined || scanConsistency === true || typeof scanConsistency === "string" && ["true", "request_plus"].includes(scanConsistency.toLowerCase());
|
|
752
|
+
}
|
|
655
753
|
getMulti(args) {
|
|
656
754
|
var _this6 = this;
|
|
657
755
|
return _asyncToGenerator(function* () {
|
|
@@ -659,7 +757,7 @@ class SpiceModel {
|
|
|
659
757
|
// ⚡ Profiling: use track() for proper async context forking
|
|
660
758
|
var p = (_this6$_ctx = _this6[_ctx]) == null ? void 0 : _this6$_ctx.profiler;
|
|
661
759
|
var doGetMulti = /*#__PURE__*/function () {
|
|
662
|
-
var
|
|
760
|
+
var _ref0 = _asyncToGenerator(function* () {
|
|
663
761
|
if (!args) {
|
|
664
762
|
args = {};
|
|
665
763
|
}
|
|
@@ -701,7 +799,7 @@ class SpiceModel {
|
|
|
701
799
|
return results;
|
|
702
800
|
});
|
|
703
801
|
return function doGetMulti() {
|
|
704
|
-
return
|
|
802
|
+
return _ref0.apply(this, arguments);
|
|
705
803
|
};
|
|
706
804
|
}();
|
|
707
805
|
try {
|
|
@@ -757,7 +855,7 @@ class SpiceModel {
|
|
|
757
855
|
// Profiling: use track() for proper async context forking
|
|
758
856
|
var p = (_this8$_ctx = _this8[_ctx]) == null ? void 0 : _this8$_ctx.profiler;
|
|
759
857
|
var doUpdate = /*#__PURE__*/function () {
|
|
760
|
-
var
|
|
858
|
+
var _ref1 = _asyncToGenerator(function* () {
|
|
761
859
|
_this8.updated_at = new SDate().now();
|
|
762
860
|
var results = yield _this8.database.get(args.id);
|
|
763
861
|
var item_exist = yield _this8.exist(results);
|
|
@@ -801,7 +899,7 @@ class SpiceModel {
|
|
|
801
899
|
});
|
|
802
900
|
});
|
|
803
901
|
return function doUpdate() {
|
|
804
|
-
return
|
|
902
|
+
return _ref1.apply(this, arguments);
|
|
805
903
|
};
|
|
806
904
|
}();
|
|
807
905
|
try {
|
|
@@ -827,7 +925,7 @@ class SpiceModel {
|
|
|
827
925
|
// Profiling: use track() for proper async context forking
|
|
828
926
|
var p = (_this9$_ctx = _this9[_ctx]) == null ? void 0 : _this9$_ctx.profiler;
|
|
829
927
|
var doCreate = /*#__PURE__*/function () {
|
|
830
|
-
var
|
|
928
|
+
var _ref11 = _asyncToGenerator(function* () {
|
|
831
929
|
var form;
|
|
832
930
|
_this9.created_at = new SDate().now();
|
|
833
931
|
args = _.defaults(args, {
|
|
@@ -873,7 +971,7 @@ class SpiceModel {
|
|
|
873
971
|
});
|
|
874
972
|
});
|
|
875
973
|
return function doCreate() {
|
|
876
|
-
return
|
|
974
|
+
return _ref11.apply(this, arguments);
|
|
877
975
|
};
|
|
878
976
|
}();
|
|
879
977
|
try {
|
|
@@ -911,7 +1009,7 @@ class SpiceModel {
|
|
|
911
1009
|
// Profiling: use track() for proper async context forking
|
|
912
1010
|
var p = (_this1$_ctx = _this1[_ctx]) == null ? void 0 : _this1$_ctx.profiler;
|
|
913
1011
|
var doDelete = /*#__PURE__*/function () {
|
|
914
|
-
var
|
|
1012
|
+
var _ref13 = _asyncToGenerator(function* () {
|
|
915
1013
|
var item_exist = yield _this1.exist(args.id);
|
|
916
1014
|
if (!item_exist) {
|
|
917
1015
|
throw new Error(_this1.type + " does not exist.");
|
|
@@ -922,7 +1020,7 @@ class SpiceModel {
|
|
|
922
1020
|
}
|
|
923
1021
|
var delete_response = {};
|
|
924
1022
|
var dbOperation = /*#__PURE__*/function () {
|
|
925
|
-
var
|
|
1023
|
+
var _ref14 = _asyncToGenerator(function* () {
|
|
926
1024
|
if (args.hard) {
|
|
927
1025
|
delete_response = yield _this1.database.delete(args.id);
|
|
928
1026
|
_this1.setMonitor();
|
|
@@ -933,7 +1031,7 @@ class SpiceModel {
|
|
|
933
1031
|
}
|
|
934
1032
|
});
|
|
935
1033
|
return function dbOperation() {
|
|
936
|
-
return
|
|
1034
|
+
return _ref14.apply(this, arguments);
|
|
937
1035
|
};
|
|
938
1036
|
}();
|
|
939
1037
|
if (p) {
|
|
@@ -949,7 +1047,7 @@ class SpiceModel {
|
|
|
949
1047
|
return {};
|
|
950
1048
|
});
|
|
951
1049
|
return function doDelete() {
|
|
952
|
-
return
|
|
1050
|
+
return _ref13.apply(this, arguments);
|
|
953
1051
|
};
|
|
954
1052
|
}();
|
|
955
1053
|
try {
|
|
@@ -1139,12 +1237,12 @@ class SpiceModel {
|
|
|
1139
1237
|
}
|
|
1140
1238
|
createJoinSection(mappedNestings) {
|
|
1141
1239
|
if (!mappedNestings || mappedNestings.length === 0) return "";
|
|
1142
|
-
return mappedNestings.map(
|
|
1240
|
+
return mappedNestings.map(_ref15 => {
|
|
1143
1241
|
var {
|
|
1144
1242
|
alias,
|
|
1145
1243
|
reference,
|
|
1146
1244
|
is_array
|
|
1147
|
-
} =
|
|
1245
|
+
} = _ref15;
|
|
1148
1246
|
var keyspace = (0, _fix.fixCollection)(reference);
|
|
1149
1247
|
if (is_array === true) {
|
|
1150
1248
|
return "LEFT NEST `" + keyspace + "` AS `" + alias + "` ON KEYS `" + this.type + "`.`" + alias + "`";
|
|
@@ -1289,7 +1387,7 @@ class SpiceModel {
|
|
|
1289
1387
|
// Profiling: use track() for proper async context forking
|
|
1290
1388
|
var p = (_this10$_ctx = _this10[_ctx]) == null ? void 0 : _this10$_ctx.profiler;
|
|
1291
1389
|
var doList = /*#__PURE__*/function () {
|
|
1292
|
-
var
|
|
1390
|
+
var _ref16 = _asyncToGenerator(function* () {
|
|
1293
1391
|
var _args1, _args10;
|
|
1294
1392
|
_this10.applyMappingDepthArgs(args);
|
|
1295
1393
|
|
|
@@ -1338,9 +1436,9 @@ class SpiceModel {
|
|
|
1338
1436
|
if (args.skip_hooks !== true) {
|
|
1339
1437
|
yield _this10.run_hook(_this10, "list", "before");
|
|
1340
1438
|
}
|
|
1341
|
-
var cacheKey = "list::" + _this10.type + "::" + args._join + "::" + query + "::" + args.limit + "::" + args.offset + "::" + args.sort + "::" + args.do_count + "::" + args.statement_consistent + "::" + args.columns + "::" + args.is_full_text + "::" + args.is_custom_query;
|
|
1439
|
+
var cacheKey = "list::" + _this10.type + "::" + args._join + "::" + query + "::" + args.limit + "::" + args.offset + "::" + args.sort + "::" + args.do_count + "::" + args.query_scope + "::" + args.scan_consistency + "::" + args.statement_consistent + "::" + args.scan_wait + "::" + args.columns + "::" + args.is_full_text + "::" + args.is_custom_query;
|
|
1342
1440
|
var results;
|
|
1343
|
-
if (_this10.shouldUseCache(_this10.type)) {
|
|
1441
|
+
if (_this10.shouldUseCache(_this10.type) && !_this10.requiresFreshQuery(args)) {
|
|
1344
1442
|
var cached = yield _this10.getCacheProviderObject(_this10.type).get(cacheKey);
|
|
1345
1443
|
results = cached == null ? void 0 : cached.value;
|
|
1346
1444
|
var isEmpty = (cached == null ? void 0 : cached.value) === undefined;
|
|
@@ -1368,7 +1466,7 @@ class SpiceModel {
|
|
|
1368
1466
|
return results;
|
|
1369
1467
|
});
|
|
1370
1468
|
return function doList() {
|
|
1371
|
-
return
|
|
1469
|
+
return _ref16.apply(this, arguments);
|
|
1372
1470
|
};
|
|
1373
1471
|
}();
|
|
1374
1472
|
try {
|
|
@@ -1422,20 +1520,21 @@ class SpiceModel {
|
|
|
1422
1520
|
// Profiling: use track() for proper async context forking
|
|
1423
1521
|
var p = (_this11$_ctx = _this11[_ctx]) == null ? void 0 : _this11$_ctx.profiler;
|
|
1424
1522
|
var doFetch = /*#__PURE__*/function () {
|
|
1425
|
-
var
|
|
1523
|
+
var _ref17 = _asyncToGenerator(function* () {
|
|
1524
|
+
var queryOptions = _this11.getQueryOptions(args);
|
|
1426
1525
|
if (args.is_custom_query === "true" && args.ids.length > 0) {
|
|
1427
1526
|
console.log("Fetching results from database", _this11.type, args, query, mappedNestings);
|
|
1428
|
-
return yield _this11.database.query(query);
|
|
1527
|
+
return yield _this11.database.query(query, args.query_scope, queryOptions);
|
|
1429
1528
|
} else if (args.is_full_text === "true") {
|
|
1430
1529
|
return yield _this11.database.full_text_search(_this11.type, query || "", args.limit, args.offset, args._join);
|
|
1431
1530
|
} else {
|
|
1432
1531
|
var correctedColumns = _this11.correctColumns(args.columns, mappedNestings);
|
|
1433
|
-
var result = yield _this11.database.search(_this11.type, correctedColumns || args.columns || "", query || "", args.limit, args.offset, args.sort, args.do_count, args.
|
|
1532
|
+
var result = yield _this11.database.search(_this11.type, correctedColumns || args.columns || "", query || "", args.limit, args.offset, args.sort, args.do_count, args.query_scope, args._join, queryOptions);
|
|
1434
1533
|
return result;
|
|
1435
1534
|
}
|
|
1436
1535
|
});
|
|
1437
1536
|
return function doFetch() {
|
|
1438
|
-
return
|
|
1537
|
+
return _ref17.apply(this, arguments);
|
|
1439
1538
|
};
|
|
1440
1539
|
}();
|
|
1441
1540
|
if (p) {
|
|
@@ -1444,12 +1543,12 @@ class SpiceModel {
|
|
|
1444
1543
|
return yield doFetch();
|
|
1445
1544
|
})();
|
|
1446
1545
|
}
|
|
1447
|
-
addHook(
|
|
1546
|
+
addHook(_ref18) {
|
|
1448
1547
|
var {
|
|
1449
1548
|
operation,
|
|
1450
1549
|
when,
|
|
1451
1550
|
execute
|
|
1452
|
-
} =
|
|
1551
|
+
} = _ref18;
|
|
1453
1552
|
this[_hooks][operation][when].push(execute);
|
|
1454
1553
|
}
|
|
1455
1554
|
run_hook(data, op, when, old_data) {
|
|
@@ -1572,7 +1671,7 @@ class SpiceModel {
|
|
|
1572
1671
|
|
|
1573
1672
|
// ⚡ Wrap in profiler track() to ensure proper async context for child operations
|
|
1574
1673
|
var fetchRelated = /*#__PURE__*/function () {
|
|
1575
|
-
var
|
|
1674
|
+
var _ref19 = _asyncToGenerator(function* () {
|
|
1576
1675
|
return yield Promise.allSettled(_.map(classes, obj => {
|
|
1577
1676
|
var objInstance = new obj(_extends({}, _this13[_args], {
|
|
1578
1677
|
skip_cache: _this13[_skip_cache],
|
|
@@ -1580,6 +1679,8 @@ class SpiceModel {
|
|
|
1580
1679
|
mapping_dept: _this13[_mapping_dept],
|
|
1581
1680
|
mapping_dept_scalar: _this13[_mapping_dept_scalar],
|
|
1582
1681
|
mapping_dept_array: _this13[_mapping_dept_array],
|
|
1682
|
+
mapping_concurrency: _this13[_mapping_concurrency],
|
|
1683
|
+
skip_mapped_serialization: _this13[_skip_mapped_serialization],
|
|
1583
1684
|
mapping_dept_exempt: _this13[_mapping_dept_exempt],
|
|
1584
1685
|
_columns: columnArray.join(","),
|
|
1585
1686
|
_current_path: childPath
|
|
@@ -1587,12 +1688,13 @@ class SpiceModel {
|
|
|
1587
1688
|
return objInstance.getMulti({
|
|
1588
1689
|
skip_hooks: true,
|
|
1589
1690
|
ids: ids,
|
|
1590
|
-
columns: columnArray
|
|
1691
|
+
columns: columnArray,
|
|
1692
|
+
skip_read_serialize: _this13[_skip_mapped_serialization]
|
|
1591
1693
|
});
|
|
1592
1694
|
}));
|
|
1593
1695
|
});
|
|
1594
1696
|
return function fetchRelated() {
|
|
1595
|
-
return
|
|
1697
|
+
return _ref19.apply(this, arguments);
|
|
1596
1698
|
};
|
|
1597
1699
|
}();
|
|
1598
1700
|
var returned_all;
|
|
@@ -1693,7 +1795,7 @@ class SpiceModel {
|
|
|
1693
1795
|
|
|
1694
1796
|
// ⚡ Wrap in profiler track() to ensure proper async context for child operations
|
|
1695
1797
|
var fetchRelated = /*#__PURE__*/function () {
|
|
1696
|
-
var
|
|
1798
|
+
var _ref20 = _asyncToGenerator(function* () {
|
|
1697
1799
|
return yield Promise.allSettled(_.map(classes, obj => {
|
|
1698
1800
|
return new obj(_extends({}, _this14[_args], {
|
|
1699
1801
|
skip_cache: _this14[_skip_cache],
|
|
@@ -1701,18 +1803,21 @@ class SpiceModel {
|
|
|
1701
1803
|
mapping_dept: _this14[_mapping_dept],
|
|
1702
1804
|
mapping_dept_scalar: _this14[_mapping_dept_scalar],
|
|
1703
1805
|
mapping_dept_array: _this14[_mapping_dept_array],
|
|
1806
|
+
mapping_concurrency: _this14[_mapping_concurrency],
|
|
1807
|
+
skip_mapped_serialization: _this14[_skip_mapped_serialization],
|
|
1704
1808
|
mapping_dept_exempt: _this14[_mapping_dept_exempt],
|
|
1705
1809
|
_columns: columnArray.join(","),
|
|
1706
1810
|
_current_path: childPath
|
|
1707
1811
|
})).getMulti({
|
|
1708
1812
|
skip_hooks: true,
|
|
1709
1813
|
ids: ids,
|
|
1710
|
-
columns: columnArray
|
|
1814
|
+
columns: columnArray,
|
|
1815
|
+
skip_read_serialize: _this14[_skip_mapped_serialization]
|
|
1711
1816
|
});
|
|
1712
1817
|
}));
|
|
1713
1818
|
});
|
|
1714
1819
|
return function fetchRelated() {
|
|
1715
|
-
return
|
|
1820
|
+
return _ref20.apply(this, arguments);
|
|
1716
1821
|
};
|
|
1717
1822
|
}();
|
|
1718
1823
|
var returned_all;
|
|
@@ -1760,12 +1865,22 @@ class SpiceModel {
|
|
|
1760
1865
|
return original_is_array ? data : data[0];
|
|
1761
1866
|
})();
|
|
1762
1867
|
}
|
|
1763
|
-
addModifier(
|
|
1868
|
+
addModifier(_ref21) {
|
|
1764
1869
|
var {
|
|
1765
1870
|
when,
|
|
1766
|
-
execute
|
|
1767
|
-
|
|
1871
|
+
execute,
|
|
1872
|
+
kind,
|
|
1873
|
+
source_property,
|
|
1874
|
+
store_property
|
|
1875
|
+
} = _ref21;
|
|
1768
1876
|
if (this[_serializers][when]) {
|
|
1877
|
+
if (kind) {
|
|
1878
|
+
execute.spiceModifier = {
|
|
1879
|
+
kind,
|
|
1880
|
+
source_property,
|
|
1881
|
+
store_property
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1769
1884
|
this[_serializers][when]["modifiers"].push(execute);
|
|
1770
1885
|
}
|
|
1771
1886
|
}
|
|
@@ -1833,6 +1948,9 @@ class SpiceModel {
|
|
|
1833
1948
|
{
|
|
1834
1949
|
_this15.addModifier({
|
|
1835
1950
|
when: properties[i].map.when || "read",
|
|
1951
|
+
kind: "relationship_map",
|
|
1952
|
+
source_property: i,
|
|
1953
|
+
store_property: properties[i].map.destination || i,
|
|
1836
1954
|
execute: function () {
|
|
1837
1955
|
var _execute = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
|
|
1838
1956
|
// Skip if columns are specified but this field isn't included
|
|
@@ -1841,7 +1959,7 @@ class SpiceModel {
|
|
|
1841
1959
|
}
|
|
1842
1960
|
return yield _this15.mapToObject(data, _.isString(properties[i].map.reference) ? spice.models[properties[i].map.reference] : properties[i].map.reference, i, properties[i].map.destination || i, properties[i], args, serializationType, mapping_dept, level, columns);
|
|
1843
1961
|
});
|
|
1844
|
-
function execute(
|
|
1962
|
+
function execute(_x4, _x5, _x6, _x7, _x8, _x9, _x0, _x1, _x10) {
|
|
1845
1963
|
return _execute.apply(this, arguments);
|
|
1846
1964
|
}
|
|
1847
1965
|
return execute;
|
|
@@ -1854,6 +1972,9 @@ class SpiceModel {
|
|
|
1854
1972
|
{
|
|
1855
1973
|
_this15.addModifier({
|
|
1856
1974
|
when: properties[i].map.when || "read",
|
|
1975
|
+
kind: "relationship_map",
|
|
1976
|
+
source_property: i,
|
|
1977
|
+
store_property: properties[i].map.destination || i,
|
|
1857
1978
|
execute: function () {
|
|
1858
1979
|
var _execute2 = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
|
|
1859
1980
|
// Skip if columns are specified but this field isn't included
|
|
@@ -1862,7 +1983,7 @@ class SpiceModel {
|
|
|
1862
1983
|
}
|
|
1863
1984
|
return yield _this15.mapToObjectArray(data, _.isString(properties[i].map.reference) ? spice.models[properties[i].map.reference] : properties[i].map.reference, i, properties[i].map.destination || i, properties[i], args, serializationType, mapping_dept, level, columns);
|
|
1864
1985
|
});
|
|
1865
|
-
function execute(
|
|
1986
|
+
function execute(_x11, _x12, _x13, _x14, _x15, _x16, _x17, _x18, _x19) {
|
|
1866
1987
|
return _execute2.apply(this, arguments);
|
|
1867
1988
|
}
|
|
1868
1989
|
return execute;
|
|
@@ -1894,7 +2015,7 @@ class SpiceModel {
|
|
|
1894
2015
|
// Profiling: use track() for proper async context forking
|
|
1895
2016
|
var p = (_this16$_ctx = _this16[_ctx]) == null ? void 0 : _this16$_ctx.profiler;
|
|
1896
2017
|
var doSerialize = /*#__PURE__*/function () {
|
|
1897
|
-
var
|
|
2018
|
+
var _ref22 = _asyncToGenerator(function* () {
|
|
1898
2019
|
var _this16$_serializers, _this16$_serializers$;
|
|
1899
2020
|
// Early exit if serialization should not run.
|
|
1900
2021
|
if (!_this16.shouldSerializerRun(args, type)) {
|
|
@@ -1909,19 +2030,117 @@ class SpiceModel {
|
|
|
1909
2030
|
|
|
1910
2031
|
// Cache the modifiers lookup for the specified type.
|
|
1911
2032
|
var modifiers = ((_this16$_serializers = _this16[_serializers]) == null ? void 0 : (_this16$_serializers$ = _this16$_serializers[type]) == null ? void 0 : _this16$_serializers$.modifiers) || [];
|
|
1912
|
-
|
|
1913
|
-
var
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
2033
|
+
var invokeModifier = /*#__PURE__*/function () {
|
|
2034
|
+
var _ref23 = _asyncToGenerator(function* (modifier, index, inputData) {
|
|
2035
|
+
try {
|
|
2036
|
+
var result = yield modifier(inputData, old_data, _this16[_ctx], _this16.type, args, _this16[_mapping_dept], _this16[_level], _this16[_columns], type);
|
|
2037
|
+
if (result !== undefined) {
|
|
2038
|
+
return {
|
|
2039
|
+
hasValue: true,
|
|
2040
|
+
value: result
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
console.warn("Modifier #" + index + " for type=" + _this16.type + " returned undefined, keeping previous data");
|
|
2044
|
+
} catch (error) {
|
|
2045
|
+
console.error("Modifier error in do_serialize (type=" + _this16.type + ", modifier #" + index + "):", error instanceof Error ? error.stack : "Non-Error thrown: " + JSON.stringify(error));
|
|
1921
2046
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
2047
|
+
return {
|
|
2048
|
+
hasValue: false
|
|
2049
|
+
};
|
|
2050
|
+
});
|
|
2051
|
+
return function invokeModifier(_x20, _x21, _x22) {
|
|
2052
|
+
return _ref23.apply(this, arguments);
|
|
2053
|
+
};
|
|
2054
|
+
}();
|
|
2055
|
+
var modifierIndex = 0;
|
|
2056
|
+
var _loop2 = function* _loop2() {
|
|
2057
|
+
var _modifier$spiceModifi;
|
|
2058
|
+
var modifier = modifiers[modifierIndex];
|
|
2059
|
+
var canRunRelationshipMapsConcurrently = type === "read" && _this16[_level] === 0 && _this16[_mapping_concurrency] > 1 && (modifier == null ? void 0 : (_modifier$spiceModifi = modifier.spiceModifier) == null ? void 0 : _modifier$spiceModifi.kind) === "relationship_map";
|
|
2060
|
+
if (!canRunRelationshipMapsConcurrently) {
|
|
2061
|
+
var result = yield invokeModifier(modifier, modifierIndex, data);
|
|
2062
|
+
if (result.hasValue) data = result.value;
|
|
2063
|
+
modifierIndex += 1;
|
|
2064
|
+
return "continue";
|
|
1924
2065
|
}
|
|
2066
|
+
var group = [];
|
|
2067
|
+
var destinations = new Set();
|
|
2068
|
+
while (modifierIndex < modifiers.length) {
|
|
2069
|
+
var candidate = modifiers[modifierIndex];
|
|
2070
|
+
var metadata = candidate == null ? void 0 : candidate.spiceModifier;
|
|
2071
|
+
if ((metadata == null ? void 0 : metadata.kind) !== "relationship_map") break;
|
|
2072
|
+
if (metadata.store_property && destinations.has(metadata.store_property)) {
|
|
2073
|
+
break;
|
|
2074
|
+
}
|
|
2075
|
+
if (metadata.store_property) {
|
|
2076
|
+
destinations.add(metadata.store_property);
|
|
2077
|
+
}
|
|
2078
|
+
group.push({
|
|
2079
|
+
modifier: candidate,
|
|
2080
|
+
index: modifierIndex,
|
|
2081
|
+
metadata
|
|
2082
|
+
});
|
|
2083
|
+
modifierIndex += 1;
|
|
2084
|
+
}
|
|
2085
|
+
var groupQueuedAt = process.hrtime.bigint();
|
|
2086
|
+
var activeRequestMappings = 0;
|
|
2087
|
+
var executeGroup = /*#__PURE__*/function () {
|
|
2088
|
+
var _ref24 = _asyncToGenerator(function* () {
|
|
2089
|
+
return yield runWithConcurrency(group, _this16[_mapping_concurrency], /*#__PURE__*/function () {
|
|
2090
|
+
var _ref25 = _asyncToGenerator(function* (entry) {
|
|
2091
|
+
var requestQueueWaitMs = Number(process.hrtime.bigint() - groupQueuedAt) / 1e6;
|
|
2092
|
+
var globalSlot = yield acquireGlobalMappingSlot();
|
|
2093
|
+
activeRequestMappings += 1;
|
|
2094
|
+
var profileMetadata = {
|
|
2095
|
+
source_property: entry.metadata.source_property,
|
|
2096
|
+
store_property: entry.metadata.store_property,
|
|
2097
|
+
request_queue_wait_ms: Math.round(requestQueueWaitMs * 100) / 100,
|
|
2098
|
+
global_queue_wait_ms: Math.round(globalSlot.waitMs * 100) / 100,
|
|
2099
|
+
active_request_mappings: activeRequestMappings,
|
|
2100
|
+
request_limit: _this16[_mapping_concurrency],
|
|
2101
|
+
active_global_mappings: globalSlot.active,
|
|
2102
|
+
global_limit: GLOBAL_MAPPING_CONCURRENCY
|
|
2103
|
+
};
|
|
2104
|
+
try {
|
|
2105
|
+
var execute = /*#__PURE__*/function () {
|
|
2106
|
+
var _ref26 = _asyncToGenerator(function* () {
|
|
2107
|
+
return yield invokeModifier(entry.modifier, entry.index, data);
|
|
2108
|
+
});
|
|
2109
|
+
return function execute() {
|
|
2110
|
+
return _ref26.apply(this, arguments);
|
|
2111
|
+
};
|
|
2112
|
+
}();
|
|
2113
|
+
if (p) {
|
|
2114
|
+
return yield p.track(_this16.type + ".map.concurrent." + entry.metadata.source_property, execute, profileMetadata);
|
|
2115
|
+
}
|
|
2116
|
+
return yield execute();
|
|
2117
|
+
} finally {
|
|
2118
|
+
activeRequestMappings -= 1;
|
|
2119
|
+
globalSlot.release();
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
return function (_x23) {
|
|
2123
|
+
return _ref25.apply(this, arguments);
|
|
2124
|
+
};
|
|
2125
|
+
}());
|
|
2126
|
+
});
|
|
2127
|
+
return function executeGroup() {
|
|
2128
|
+
return _ref24.apply(this, arguments);
|
|
2129
|
+
};
|
|
2130
|
+
}();
|
|
2131
|
+
var groupMetadata = {
|
|
2132
|
+
map_count: group.length,
|
|
2133
|
+
request_limit: _this16[_mapping_concurrency],
|
|
2134
|
+
global_limit: GLOBAL_MAPPING_CONCURRENCY
|
|
2135
|
+
};
|
|
2136
|
+
var groupResults = p ? yield p.track(_this16.type + ".map.concurrent", executeGroup, groupMetadata) : yield executeGroup();
|
|
2137
|
+
for (var _result of groupResults) {
|
|
2138
|
+
if (_result == null ? void 0 : _result.hasValue) data = _result.value;
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
while (modifierIndex < modifiers.length) {
|
|
2142
|
+
var _ret = yield* _loop2();
|
|
2143
|
+
if (_ret === "continue") continue;
|
|
1925
2144
|
}
|
|
1926
2145
|
|
|
1927
2146
|
// Ensure data is always an array for consistent processing.
|
|
@@ -1962,7 +2181,7 @@ class SpiceModel {
|
|
|
1962
2181
|
return originalIsArray ? data : data[0];
|
|
1963
2182
|
});
|
|
1964
2183
|
return function doSerialize() {
|
|
1965
|
-
return
|
|
2184
|
+
return _ref22.apply(this, arguments);
|
|
1966
2185
|
};
|
|
1967
2186
|
}();
|
|
1968
2187
|
try {
|
package/package.json
CHANGED
package/src/models/SpiceModel.js
CHANGED
|
@@ -24,9 +24,64 @@ var SDate = require("sonover-date"),
|
|
|
24
24
|
_mapping_dept = Symbol(),
|
|
25
25
|
_mapping_dept_scalar = Symbol(),
|
|
26
26
|
_mapping_dept_array = Symbol(),
|
|
27
|
+
_mapping_concurrency = Symbol(),
|
|
28
|
+
_skip_mapped_serialization = Symbol(),
|
|
27
29
|
_mapping_dept_exempt = Symbol(),
|
|
28
30
|
_current_path = Symbol();
|
|
29
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
|
+
|
|
30
85
|
//const _type = Symbol("type");
|
|
31
86
|
|
|
32
87
|
let that;
|
|
@@ -71,6 +126,13 @@ export default class SpiceModel {
|
|
|
71
126
|
legacyMappingDept ??
|
|
72
127
|
requestProfile.mapping_dept_array ??
|
|
73
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;
|
|
74
136
|
this[_mapping_dept_exempt] = modelArgs.mapping_dept_exempt || [];
|
|
75
137
|
this.type = "";
|
|
76
138
|
this.collection = args.connection;
|
|
@@ -277,6 +339,15 @@ export default class SpiceModel {
|
|
|
277
339
|
) {
|
|
278
340
|
this[_mapping_dept_array] = args.mapping_dept_array;
|
|
279
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
|
+
}
|
|
280
351
|
if (args.mapping_dept_exempt) {
|
|
281
352
|
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
282
353
|
}
|
|
@@ -712,15 +783,47 @@ export default class SpiceModel {
|
|
|
712
783
|
}
|
|
713
784
|
}
|
|
714
785
|
|
|
715
|
-
async query(query, scope) {
|
|
786
|
+
async query(query, scope, options) {
|
|
716
787
|
try {
|
|
717
|
-
let results = await this.database.query(query, scope);
|
|
788
|
+
let results = await this.database.query(query, scope, options);
|
|
718
789
|
return results;
|
|
719
790
|
} catch (error) {
|
|
720
791
|
throw error;
|
|
721
792
|
}
|
|
722
793
|
}
|
|
723
794
|
|
|
795
|
+
getQueryOptions(args = {}) {
|
|
796
|
+
let scanConsistency = args.scan_consistency;
|
|
797
|
+
|
|
798
|
+
// Backward compatibility for callers that already use statement_consistent.
|
|
799
|
+
// The adapter accepts the SDK-style value as well as these snake_case keys.
|
|
800
|
+
if (scanConsistency === undefined && args.statement_consistent !== undefined) {
|
|
801
|
+
const legacyValue = args.statement_consistent;
|
|
802
|
+
const isRequestPlus =
|
|
803
|
+
legacyValue === true ||
|
|
804
|
+
(typeof legacyValue === "string" &&
|
|
805
|
+
["true", "request_plus"].includes(legacyValue.toLowerCase()));
|
|
806
|
+
scanConsistency = isRequestPlus ? "request_plus" : "not_bounded";
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
scan_consistency: scanConsistency,
|
|
811
|
+
consistent_with: args.consistent_with,
|
|
812
|
+
scan_wait: args.scan_wait,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
requiresFreshQuery(args = {}) {
|
|
817
|
+
const options = this.getQueryOptions(args);
|
|
818
|
+
const scanConsistency = options.scan_consistency;
|
|
819
|
+
return (
|
|
820
|
+
options.consistent_with !== undefined ||
|
|
821
|
+
scanConsistency === true ||
|
|
822
|
+
(typeof scanConsistency === "string" &&
|
|
823
|
+
["true", "request_plus"].includes(scanConsistency.toLowerCase()))
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
724
827
|
async getMulti(args) {
|
|
725
828
|
// ⚡ Profiling: use track() for proper async context forking
|
|
726
829
|
const p = this[_ctx]?.profiler;
|
|
@@ -1498,10 +1601,10 @@ export default class SpiceModel {
|
|
|
1498
1601
|
await this.run_hook(this, "list", "before");
|
|
1499
1602
|
}
|
|
1500
1603
|
|
|
1501
|
-
const cacheKey = `list::${this.type}::${args._join}::${query}::${args.limit}::${args.offset}::${args.sort}::${args.do_count}::${args.statement_consistent}::${args.columns}::${args.is_full_text}::${args.is_custom_query}`;
|
|
1604
|
+
const cacheKey = `list::${this.type}::${args._join}::${query}::${args.limit}::${args.offset}::${args.sort}::${args.do_count}::${args.query_scope}::${args.scan_consistency}::${args.statement_consistent}::${args.scan_wait}::${args.columns}::${args.is_full_text}::${args.is_custom_query}`;
|
|
1502
1605
|
let results;
|
|
1503
1606
|
|
|
1504
|
-
if (this.shouldUseCache(this.type)) {
|
|
1607
|
+
if (this.shouldUseCache(this.type) && !this.requiresFreshQuery(args)) {
|
|
1505
1608
|
const cached = await this.getCacheProviderObject(this.type).get(
|
|
1506
1609
|
cacheKey,
|
|
1507
1610
|
);
|
|
@@ -1600,6 +1703,7 @@ export default class SpiceModel {
|
|
|
1600
1703
|
const p = this[_ctx]?.profiler;
|
|
1601
1704
|
|
|
1602
1705
|
const doFetch = async () => {
|
|
1706
|
+
const queryOptions = this.getQueryOptions(args);
|
|
1603
1707
|
if (args.is_custom_query === "true" && args.ids.length > 0) {
|
|
1604
1708
|
console.log(
|
|
1605
1709
|
"Fetching results from database",
|
|
@@ -1608,7 +1712,7 @@ export default class SpiceModel {
|
|
|
1608
1712
|
query,
|
|
1609
1713
|
mappedNestings,
|
|
1610
1714
|
);
|
|
1611
|
-
return await this.database.query(query);
|
|
1715
|
+
return await this.database.query(query, args.query_scope, queryOptions);
|
|
1612
1716
|
} else if (args.is_full_text === "true") {
|
|
1613
1717
|
return await this.database.full_text_search(
|
|
1614
1718
|
this.type,
|
|
@@ -1630,8 +1734,9 @@ export default class SpiceModel {
|
|
|
1630
1734
|
args.offset,
|
|
1631
1735
|
args.sort,
|
|
1632
1736
|
args.do_count,
|
|
1633
|
-
args.
|
|
1737
|
+
args.query_scope,
|
|
1634
1738
|
args._join,
|
|
1739
|
+
queryOptions,
|
|
1635
1740
|
);
|
|
1636
1741
|
return result;
|
|
1637
1742
|
}
|
|
@@ -1815,6 +1920,8 @@ export default class SpiceModel {
|
|
|
1815
1920
|
mapping_dept: this[_mapping_dept],
|
|
1816
1921
|
mapping_dept_scalar: this[_mapping_dept_scalar],
|
|
1817
1922
|
mapping_dept_array: this[_mapping_dept_array],
|
|
1923
|
+
mapping_concurrency: this[_mapping_concurrency],
|
|
1924
|
+
skip_mapped_serialization: this[_skip_mapped_serialization],
|
|
1818
1925
|
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1819
1926
|
_columns: columnArray.join(","),
|
|
1820
1927
|
_current_path: childPath,
|
|
@@ -1824,6 +1931,7 @@ export default class SpiceModel {
|
|
|
1824
1931
|
skip_hooks: true,
|
|
1825
1932
|
ids: ids,
|
|
1826
1933
|
columns: columnArray,
|
|
1934
|
+
skip_read_serialize: this[_skip_mapped_serialization],
|
|
1827
1935
|
});
|
|
1828
1936
|
}),
|
|
1829
1937
|
);
|
|
@@ -1990,6 +2098,8 @@ export default class SpiceModel {
|
|
|
1990
2098
|
mapping_dept: this[_mapping_dept],
|
|
1991
2099
|
mapping_dept_scalar: this[_mapping_dept_scalar],
|
|
1992
2100
|
mapping_dept_array: this[_mapping_dept_array],
|
|
2101
|
+
mapping_concurrency: this[_mapping_concurrency],
|
|
2102
|
+
skip_mapped_serialization: this[_skip_mapped_serialization],
|
|
1993
2103
|
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1994
2104
|
_columns: columnArray.join(","),
|
|
1995
2105
|
_current_path: childPath,
|
|
@@ -1997,6 +2107,7 @@ export default class SpiceModel {
|
|
|
1997
2107
|
skip_hooks: true,
|
|
1998
2108
|
ids: ids,
|
|
1999
2109
|
columns: columnArray,
|
|
2110
|
+
skip_read_serialize: this[_skip_mapped_serialization],
|
|
2000
2111
|
});
|
|
2001
2112
|
}),
|
|
2002
2113
|
);
|
|
@@ -2070,8 +2181,21 @@ export default class SpiceModel {
|
|
|
2070
2181
|
return original_is_array ? data : data[0];
|
|
2071
2182
|
}
|
|
2072
2183
|
|
|
2073
|
-
addModifier({
|
|
2184
|
+
addModifier({
|
|
2185
|
+
when,
|
|
2186
|
+
execute,
|
|
2187
|
+
kind,
|
|
2188
|
+
source_property,
|
|
2189
|
+
store_property,
|
|
2190
|
+
}) {
|
|
2074
2191
|
if (this[_serializers][when]) {
|
|
2192
|
+
if (kind) {
|
|
2193
|
+
execute.spiceModifier = {
|
|
2194
|
+
kind,
|
|
2195
|
+
source_property,
|
|
2196
|
+
store_property,
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2075
2199
|
this[_serializers][when]["modifiers"].push(execute);
|
|
2076
2200
|
}
|
|
2077
2201
|
}
|
|
@@ -2145,6 +2269,9 @@ export default class SpiceModel {
|
|
|
2145
2269
|
case "string": {
|
|
2146
2270
|
this.addModifier({
|
|
2147
2271
|
when: properties[i].map.when || "read",
|
|
2272
|
+
kind: "relationship_map",
|
|
2273
|
+
source_property: i,
|
|
2274
|
+
store_property: properties[i].map.destination || i,
|
|
2148
2275
|
execute: async (
|
|
2149
2276
|
data,
|
|
2150
2277
|
old_data,
|
|
@@ -2182,6 +2309,9 @@ export default class SpiceModel {
|
|
|
2182
2309
|
case "array": {
|
|
2183
2310
|
this.addModifier({
|
|
2184
2311
|
when: properties[i].map.when || "read",
|
|
2312
|
+
kind: "relationship_map",
|
|
2313
|
+
source_property: i,
|
|
2314
|
+
store_property: properties[i].map.destination || i,
|
|
2185
2315
|
execute: async (
|
|
2186
2316
|
data,
|
|
2187
2317
|
old_data,
|
|
@@ -2245,11 +2375,10 @@ export default class SpiceModel {
|
|
|
2245
2375
|
|
|
2246
2376
|
// Cache the modifiers lookup for the specified type.
|
|
2247
2377
|
const modifiers = this[_serializers]?.[type]?.modifiers || [];
|
|
2248
|
-
|
|
2249
|
-
const modifier = modifiers[i];
|
|
2378
|
+
const invokeModifier = async (modifier, index, inputData) => {
|
|
2250
2379
|
try {
|
|
2251
2380
|
const result = await modifier(
|
|
2252
|
-
|
|
2381
|
+
inputData,
|
|
2253
2382
|
old_data,
|
|
2254
2383
|
this[_ctx],
|
|
2255
2384
|
this.type,
|
|
@@ -2259,22 +2388,117 @@ export default class SpiceModel {
|
|
|
2259
2388
|
this[_columns],
|
|
2260
2389
|
type,
|
|
2261
2390
|
);
|
|
2262
|
-
// Guard against modifiers that return undefined
|
|
2263
2391
|
if (result !== undefined) {
|
|
2264
|
-
|
|
2265
|
-
} else {
|
|
2266
|
-
console.warn(
|
|
2267
|
-
`Modifier #${i} for type=${this.type} returned undefined, keeping previous data`,
|
|
2268
|
-
);
|
|
2392
|
+
return { hasValue: true, value: result };
|
|
2269
2393
|
}
|
|
2394
|
+
console.warn(
|
|
2395
|
+
`Modifier #${index} for type=${this.type} returned undefined, keeping previous data`,
|
|
2396
|
+
);
|
|
2270
2397
|
} catch (error) {
|
|
2271
2398
|
console.error(
|
|
2272
|
-
`Modifier error in do_serialize (type=${this.type}, modifier #${
|
|
2399
|
+
`Modifier error in do_serialize (type=${this.type}, modifier #${index}):`,
|
|
2273
2400
|
error instanceof Error ?
|
|
2274
2401
|
error.stack
|
|
2275
2402
|
: `Non-Error thrown: ${JSON.stringify(error)}`,
|
|
2276
2403
|
);
|
|
2277
2404
|
}
|
|
2405
|
+
return { hasValue: false };
|
|
2406
|
+
};
|
|
2407
|
+
|
|
2408
|
+
let modifierIndex = 0;
|
|
2409
|
+
while (modifierIndex < modifiers.length) {
|
|
2410
|
+
const modifier = modifiers[modifierIndex];
|
|
2411
|
+
const canRunRelationshipMapsConcurrently =
|
|
2412
|
+
type === "read" &&
|
|
2413
|
+
this[_level] === 0 &&
|
|
2414
|
+
this[_mapping_concurrency] > 1 &&
|
|
2415
|
+
modifier?.spiceModifier?.kind === "relationship_map";
|
|
2416
|
+
|
|
2417
|
+
if (!canRunRelationshipMapsConcurrently) {
|
|
2418
|
+
const result = await invokeModifier(modifier, modifierIndex, data);
|
|
2419
|
+
if (result.hasValue) data = result.value;
|
|
2420
|
+
modifierIndex += 1;
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
const group = [];
|
|
2425
|
+
const destinations = new Set();
|
|
2426
|
+
while (modifierIndex < modifiers.length) {
|
|
2427
|
+
const candidate = modifiers[modifierIndex];
|
|
2428
|
+
const metadata = candidate?.spiceModifier;
|
|
2429
|
+
if (metadata?.kind !== "relationship_map") break;
|
|
2430
|
+
if (
|
|
2431
|
+
metadata.store_property &&
|
|
2432
|
+
destinations.has(metadata.store_property)
|
|
2433
|
+
) {
|
|
2434
|
+
break;
|
|
2435
|
+
}
|
|
2436
|
+
if (metadata.store_property) {
|
|
2437
|
+
destinations.add(metadata.store_property);
|
|
2438
|
+
}
|
|
2439
|
+
group.push({ modifier: candidate, index: modifierIndex, metadata });
|
|
2440
|
+
modifierIndex += 1;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
const groupQueuedAt = process.hrtime.bigint();
|
|
2444
|
+
let activeRequestMappings = 0;
|
|
2445
|
+
const executeGroup = async () =>
|
|
2446
|
+
await runWithConcurrency(
|
|
2447
|
+
group,
|
|
2448
|
+
this[_mapping_concurrency],
|
|
2449
|
+
async (entry) => {
|
|
2450
|
+
const requestQueueWaitMs =
|
|
2451
|
+
Number(process.hrtime.bigint() - groupQueuedAt) / 1e6;
|
|
2452
|
+
const globalSlot = await acquireGlobalMappingSlot();
|
|
2453
|
+
activeRequestMappings += 1;
|
|
2454
|
+
const profileMetadata = {
|
|
2455
|
+
source_property: entry.metadata.source_property,
|
|
2456
|
+
store_property: entry.metadata.store_property,
|
|
2457
|
+
request_queue_wait_ms:
|
|
2458
|
+
Math.round(requestQueueWaitMs * 100) / 100,
|
|
2459
|
+
global_queue_wait_ms:
|
|
2460
|
+
Math.round(globalSlot.waitMs * 100) / 100,
|
|
2461
|
+
active_request_mappings: activeRequestMappings,
|
|
2462
|
+
request_limit: this[_mapping_concurrency],
|
|
2463
|
+
active_global_mappings: globalSlot.active,
|
|
2464
|
+
global_limit: GLOBAL_MAPPING_CONCURRENCY,
|
|
2465
|
+
};
|
|
2466
|
+
|
|
2467
|
+
try {
|
|
2468
|
+
const execute = async () =>
|
|
2469
|
+
await invokeModifier(entry.modifier, entry.index, data);
|
|
2470
|
+
if (p) {
|
|
2471
|
+
return await p.track(
|
|
2472
|
+
`${this.type}.map.concurrent.${entry.metadata.source_property}`,
|
|
2473
|
+
execute,
|
|
2474
|
+
profileMetadata,
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
return await execute();
|
|
2478
|
+
} finally {
|
|
2479
|
+
activeRequestMappings -= 1;
|
|
2480
|
+
globalSlot.release();
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
);
|
|
2484
|
+
|
|
2485
|
+
const groupMetadata = {
|
|
2486
|
+
map_count: group.length,
|
|
2487
|
+
request_limit: this[_mapping_concurrency],
|
|
2488
|
+
global_limit: GLOBAL_MAPPING_CONCURRENCY,
|
|
2489
|
+
};
|
|
2490
|
+
const groupResults =
|
|
2491
|
+
p ?
|
|
2492
|
+
await p.track(
|
|
2493
|
+
`${this.type}.map.concurrent`,
|
|
2494
|
+
executeGroup,
|
|
2495
|
+
groupMetadata,
|
|
2496
|
+
)
|
|
2497
|
+
: await executeGroup();
|
|
2498
|
+
|
|
2499
|
+
for (const result of groupResults) {
|
|
2500
|
+
if (result?.hasValue) data = result.value;
|
|
2501
|
+
}
|
|
2278
2502
|
}
|
|
2279
2503
|
|
|
2280
2504
|
// Ensure data is always an array for consistent processing.
|
|
@@ -7,7 +7,9 @@ describe('SpiceModel type-specific mapping depth', () => {
|
|
|
7
7
|
capture.constructorArgs.push(args);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
async getMulti(
|
|
10
|
+
async getMulti(args) {
|
|
11
|
+
const { ids } = args;
|
|
12
|
+
capture.getMultiArgs.push(args);
|
|
11
13
|
capture.getMultiIds.push(ids);
|
|
12
14
|
return records.filter((record) => ids.includes(record.id));
|
|
13
15
|
}
|
|
@@ -15,8 +17,16 @@ describe('SpiceModel type-specific mapping depth', () => {
|
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
function createMappedModel(args = {}) {
|
|
18
|
-
const scalarCapture = {
|
|
19
|
-
|
|
20
|
+
const scalarCapture = {
|
|
21
|
+
constructorArgs: [],
|
|
22
|
+
getMultiArgs: [],
|
|
23
|
+
getMultiIds: [],
|
|
24
|
+
};
|
|
25
|
+
const arrayCapture = {
|
|
26
|
+
constructorArgs: [],
|
|
27
|
+
getMultiArgs: [],
|
|
28
|
+
getMultiIds: [],
|
|
29
|
+
};
|
|
20
30
|
const ScalarModel = createRelatedModel(
|
|
21
31
|
[{ id: 'group-1', name: 'Administrators' }],
|
|
22
32
|
scalarCapture
|
|
@@ -51,6 +61,41 @@ describe('SpiceModel type-specific mapping depth', () => {
|
|
|
51
61
|
return { model, scalarCapture, arrayCapture };
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
function createConcurrencyModel(mappingConcurrency, extraArgs = {}) {
|
|
65
|
+
const stats = { active: 0, maxActive: 0 };
|
|
66
|
+
class RelatedModel {
|
|
67
|
+
async getMulti({ ids }) {
|
|
68
|
+
stats.active += 1;
|
|
69
|
+
stats.maxActive = Math.max(stats.maxActive, stats.active);
|
|
70
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
71
|
+
stats.active -= 1;
|
|
72
|
+
return ids.map((id) => ({ id, name: id }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const props = {};
|
|
77
|
+
const record = { id: 'parent-concurrent', type: 'parent' };
|
|
78
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
79
|
+
const field = `relationship_${index}`;
|
|
80
|
+
props[field] = {
|
|
81
|
+
type: 'string',
|
|
82
|
+
map: { type: 'model', reference: RelatedModel }
|
|
83
|
+
};
|
|
84
|
+
record[field] = `related-${index}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const model = createTestModel({
|
|
88
|
+
type: 'parent',
|
|
89
|
+
args: {
|
|
90
|
+
...extraArgs,
|
|
91
|
+
...(mappingConcurrency ? { mapping_concurrency: mappingConcurrency } : {})
|
|
92
|
+
},
|
|
93
|
+
props
|
|
94
|
+
});
|
|
95
|
+
seedDatabase(model, record);
|
|
96
|
+
return { model, stats };
|
|
97
|
+
}
|
|
98
|
+
|
|
54
99
|
test('uses scalar depth 1 and array depth 3 from the request profile', async () => {
|
|
55
100
|
const ctx = {
|
|
56
101
|
state: {
|
|
@@ -127,7 +172,8 @@ describe('SpiceModel type-specific mapping depth', () => {
|
|
|
127
172
|
test('propagates both depths to related model instances', async () => {
|
|
128
173
|
const { model, arrayCapture } = createMappedModel({
|
|
129
174
|
mapping_dept_scalar: 1,
|
|
130
|
-
mapping_dept_array: 3
|
|
175
|
+
mapping_dept_array: 3,
|
|
176
|
+
mapping_concurrency: 6
|
|
131
177
|
});
|
|
132
178
|
|
|
133
179
|
await model.getMulti({ ids: ['parent-1'] });
|
|
@@ -136,13 +182,105 @@ describe('SpiceModel type-specific mapping depth', () => {
|
|
|
136
182
|
expect.objectContaining({
|
|
137
183
|
mapping_dept_scalar: 1,
|
|
138
184
|
mapping_dept_array: 3,
|
|
185
|
+
mapping_concurrency: 6,
|
|
139
186
|
_level: 1
|
|
140
187
|
})
|
|
141
188
|
);
|
|
142
189
|
});
|
|
143
190
|
|
|
191
|
+
test('runs independent relationship maps with bounded concurrency', async () => {
|
|
192
|
+
const { model, stats } = createConcurrencyModel(2);
|
|
193
|
+
|
|
194
|
+
const result = await model.get({ id: 'parent-concurrent' });
|
|
195
|
+
|
|
196
|
+
expect(stats.maxActive).toBe(2);
|
|
197
|
+
expect(result.relationship_1).toEqual(
|
|
198
|
+
expect.objectContaining({ id: 'related-1' })
|
|
199
|
+
);
|
|
200
|
+
expect(result.relationship_3).toEqual(
|
|
201
|
+
expect.objectContaining({ id: 'related-3' })
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('keeps relationship maps sequential when concurrency is not enabled', async () => {
|
|
206
|
+
const { model, stats } = createConcurrencyModel();
|
|
207
|
+
|
|
208
|
+
await model.get({ id: 'parent-concurrent' });
|
|
209
|
+
|
|
210
|
+
expect(stats.maxActive).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('keeps nested relationship maps sequential to avoid recursive slot use', async () => {
|
|
214
|
+
const { model, stats } = createConcurrencyModel(2, { _level: 1 });
|
|
215
|
+
|
|
216
|
+
await model.get({ id: 'parent-concurrent' });
|
|
217
|
+
|
|
218
|
+
expect(stats.maxActive).toBe(1);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('profiles concurrent groups and mapping slot pressure', async () => {
|
|
222
|
+
const calls = [];
|
|
223
|
+
const profiler = {
|
|
224
|
+
track: jest.fn(async (name, execute, metadata) => {
|
|
225
|
+
calls.push({ name, metadata });
|
|
226
|
+
return await execute();
|
|
227
|
+
})
|
|
228
|
+
};
|
|
229
|
+
const { model } = createConcurrencyModel(2, { ctx: { profiler } });
|
|
230
|
+
|
|
231
|
+
await model.get({ id: 'parent-concurrent' });
|
|
232
|
+
|
|
233
|
+
expect(calls).toEqual(
|
|
234
|
+
expect.arrayContaining([
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
name: 'parent.map.concurrent',
|
|
237
|
+
metadata: expect.objectContaining({
|
|
238
|
+
map_count: 3,
|
|
239
|
+
request_limit: 2
|
|
240
|
+
})
|
|
241
|
+
}),
|
|
242
|
+
expect.objectContaining({
|
|
243
|
+
name: 'parent.map.concurrent.relationship_1',
|
|
244
|
+
metadata: expect.objectContaining({
|
|
245
|
+
request_limit: 2,
|
|
246
|
+
active_request_mappings: expect.any(Number),
|
|
247
|
+
active_global_mappings: expect.any(Number),
|
|
248
|
+
request_queue_wait_ms: expect.any(Number),
|
|
249
|
+
global_queue_wait_ms: expect.any(Number)
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
])
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('skips serialization for mapped child reads when requested by the profile', async () => {
|
|
257
|
+
const ctx = {
|
|
258
|
+
state: {
|
|
259
|
+
read_profile: {
|
|
260
|
+
mapping_dept_scalar: 2,
|
|
261
|
+
mapping_dept_array: 3,
|
|
262
|
+
skip_mapped_serialization: true
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
const { model, scalarCapture, arrayCapture } = createMappedModel({ ctx });
|
|
267
|
+
|
|
268
|
+
const result = await model.get({ id: 'parent-1' });
|
|
269
|
+
|
|
270
|
+
expect(result.group).toEqual(
|
|
271
|
+
expect.objectContaining({ id: 'group-1', name: 'Administrators' })
|
|
272
|
+
);
|
|
273
|
+
expect(result.children).toHaveLength(2);
|
|
274
|
+
expect(scalarCapture.getMultiArgs[0]).toEqual(
|
|
275
|
+
expect.objectContaining({ skip_hooks: true, skip_read_serialize: true })
|
|
276
|
+
);
|
|
277
|
+
expect(arrayCapture.getMultiArgs[0]).toEqual(
|
|
278
|
+
expect.objectContaining({ skip_hooks: true, skip_read_serialize: true })
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
144
282
|
test('does not apply the request read profile to write serialization', async () => {
|
|
145
|
-
const capture = { constructorArgs: [], getMultiIds: [] };
|
|
283
|
+
const capture = { constructorArgs: [], getMultiArgs: [], getMultiIds: [] };
|
|
146
284
|
const RelatedModel = createRelatedModel(
|
|
147
285
|
[{ id: 'group-1', name: 'Administrators' }],
|
|
148
286
|
capture
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const { createTestModel } = require("../helpers/modelFactory");
|
|
2
|
+
|
|
3
|
+
describe("SpiceModel query consistency", () => {
|
|
4
|
+
test("keeps query scope separate from query consistency", async () => {
|
|
5
|
+
const model = createTestModel({ type: "application" });
|
|
6
|
+
model.database.search = jest.fn(async () => ({ data: [], total: null }));
|
|
7
|
+
|
|
8
|
+
await model.fetchResults(
|
|
9
|
+
{
|
|
10
|
+
ids: [],
|
|
11
|
+
query_scope: "cluster",
|
|
12
|
+
scan_consistency: "request_plus",
|
|
13
|
+
scan_wait: 2500,
|
|
14
|
+
_join: "",
|
|
15
|
+
},
|
|
16
|
+
"",
|
|
17
|
+
[],
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
expect(model.database.search).toHaveBeenCalledWith(
|
|
21
|
+
"application",
|
|
22
|
+
"",
|
|
23
|
+
"",
|
|
24
|
+
undefined,
|
|
25
|
+
undefined,
|
|
26
|
+
undefined,
|
|
27
|
+
undefined,
|
|
28
|
+
"cluster",
|
|
29
|
+
"",
|
|
30
|
+
{
|
|
31
|
+
scan_consistency: "request_plus",
|
|
32
|
+
consistent_with: undefined,
|
|
33
|
+
scan_wait: 2500,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test.each([true, "true", "request_plus"])(
|
|
39
|
+
"maps legacy statement_consistent=%s to request_plus",
|
|
40
|
+
(legacyValue) => {
|
|
41
|
+
const model = createTestModel({ type: "application" });
|
|
42
|
+
|
|
43
|
+
expect(
|
|
44
|
+
model.getQueryOptions({ statement_consistent: legacyValue }),
|
|
45
|
+
).toEqual(
|
|
46
|
+
expect.objectContaining({ scan_consistency: "request_plus" }),
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
test.each([false, "false"])(
|
|
52
|
+
"maps legacy statement_consistent=%s to not_bounded",
|
|
53
|
+
(legacyValue) => {
|
|
54
|
+
const model = createTestModel({ type: "application" });
|
|
55
|
+
|
|
56
|
+
expect(
|
|
57
|
+
model.getQueryOptions({ statement_consistent: legacyValue }),
|
|
58
|
+
).toEqual(
|
|
59
|
+
expect.objectContaining({ scan_consistency: "not_bounded" }),
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
test("explicit scan_consistency takes precedence over the legacy flag", () => {
|
|
65
|
+
const model = createTestModel({ type: "application" });
|
|
66
|
+
|
|
67
|
+
expect(
|
|
68
|
+
model.getQueryOptions({
|
|
69
|
+
scan_consistency: "not_bounded",
|
|
70
|
+
statement_consistent: true,
|
|
71
|
+
}),
|
|
72
|
+
).toEqual(expect.objectContaining({ scan_consistency: "not_bounded" }));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("strongly consistent reads bypass the model query cache", () => {
|
|
76
|
+
const model = createTestModel({ type: "application" });
|
|
77
|
+
|
|
78
|
+
expect(model.requiresFreshQuery({ scan_consistency: "request_plus" })).toBe(
|
|
79
|
+
true,
|
|
80
|
+
);
|
|
81
|
+
expect(model.requiresFreshQuery({ statement_consistent: true })).toBe(true);
|
|
82
|
+
expect(model.requiresFreshQuery({ consistent_with: {} })).toBe(true);
|
|
83
|
+
expect(model.requiresFreshQuery({ scan_consistency: "not_bounded" })).toBe(
|
|
84
|
+
false,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("custom queries receive the same query options", async () => {
|
|
89
|
+
const model = createTestModel({ type: "application" });
|
|
90
|
+
model.database.query = jest.fn(async () => ({ rows: [] }));
|
|
91
|
+
const mutationState = { token: "test-token" };
|
|
92
|
+
|
|
93
|
+
await model.fetchResults(
|
|
94
|
+
{
|
|
95
|
+
ids: ["application-1"],
|
|
96
|
+
is_custom_query: "true",
|
|
97
|
+
query_scope: "scope",
|
|
98
|
+
consistent_with: mutationState,
|
|
99
|
+
},
|
|
100
|
+
"SELECT 1",
|
|
101
|
+
[],
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(model.database.query).toHaveBeenCalledWith(
|
|
105
|
+
"SELECT 1",
|
|
106
|
+
"scope",
|
|
107
|
+
{
|
|
108
|
+
scan_consistency: undefined,
|
|
109
|
+
consistent_with: mutationState,
|
|
110
|
+
scan_wait: undefined,
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
});
|