spice-js 2.7.29 → 2.7.30
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 +98 -64
- package/package.json +1 -1
- package/src/index.js +3 -1
- package/src/models/SpiceModel.js +76 -15
- 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 +182 -0
package/build/index.js
CHANGED
|
@@ -53,7 +53,9 @@ function _asyncToGenerator(n) { return function () { var t = this, e = arguments
|
|
|
53
53
|
var path = require("path");
|
|
54
54
|
//import IO from "koa-socket-2";
|
|
55
55
|
var cors = require("kcors");
|
|
56
|
-
|
|
56
|
+
// Preserve an existing host/test configuration when the package is imported.
|
|
57
|
+
// Production boot still starts from an empty object because spice is undefined.
|
|
58
|
+
global.spice = global.spice || {};
|
|
57
59
|
require("dotenv").config({
|
|
58
60
|
path: path.resolve("./")
|
|
59
61
|
});
|
|
@@ -27,6 +27,8 @@ var SDate = require("sonover-date"),
|
|
|
27
27
|
_serializers = Symbol(),
|
|
28
28
|
_level = Symbol(),
|
|
29
29
|
_mapping_dept = Symbol(),
|
|
30
|
+
_mapping_dept_scalar = Symbol(),
|
|
31
|
+
_mapping_dept_array = Symbol(),
|
|
30
32
|
_mapping_dept_exempt = Symbol(),
|
|
31
33
|
_current_path = Symbol();
|
|
32
34
|
|
|
@@ -48,21 +50,27 @@ class SpiceModel {
|
|
|
48
50
|
args = {};
|
|
49
51
|
}
|
|
50
52
|
try {
|
|
51
|
-
var _args2,
|
|
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;
|
|
54
|
+
var modelArgs = ((_args2 = args) == null ? void 0 : _args2.args) || {};
|
|
55
|
+
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
|
+
var defaultMappingDept = process.env.MAPPING_DEPT || 3;
|
|
57
|
+
var legacyMappingDept = modelArgs.mapping_dept !== undefined && modelArgs.mapping_dept !== null ? modelArgs.mapping_dept : undefined;
|
|
52
58
|
var dbtype = spice.config.database.connections[args.connection].type || "couchbase";
|
|
53
59
|
var Database = require("spice-" + dbtype);
|
|
54
|
-
this[_mapping_dept] =
|
|
55
|
-
this[
|
|
60
|
+
this[_mapping_dept] = legacyMappingDept != null ? legacyMappingDept : defaultMappingDept;
|
|
61
|
+
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
|
+
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;
|
|
63
|
+
this[_mapping_dept_exempt] = modelArgs.mapping_dept_exempt || [];
|
|
56
64
|
this.type = "";
|
|
57
65
|
this.collection = args.connection;
|
|
58
66
|
this[_args] = args.args;
|
|
59
67
|
this[_external_modifier_loaded] = false;
|
|
60
68
|
this[_disable_lifecycle_events] = ((_args$args = args.args) == null ? void 0 : _args$args.disable_lifecycle_events) || false;
|
|
61
|
-
this[_ctx] = (
|
|
62
|
-
this[_skip_cache] = ((
|
|
63
|
-
this[_level] = ((
|
|
64
|
-
this[_current_path] = ((
|
|
65
|
-
this[_columns] = ((
|
|
69
|
+
this[_ctx] = (_args3 = args) == null ? void 0 : (_args3$args = _args3.args) == null ? void 0 : _args3$args.ctx;
|
|
70
|
+
this[_skip_cache] = ((_args4 = args) == null ? void 0 : (_args4$args = _args4.args) == null ? void 0 : _args4$args.skip_cache) || false;
|
|
71
|
+
this[_level] = ((_args5 = args) == null ? void 0 : (_args5$args = _args5.args) == null ? void 0 : _args5$args._level) || 0;
|
|
72
|
+
this[_current_path] = ((_args6 = args) == null ? void 0 : (_args6$args = _args6.args) == null ? void 0 : _args6$args._current_path) || "";
|
|
73
|
+
this[_columns] = ((_args7 = args) == null ? void 0 : (_args7$args = _args7.args) == null ? void 0 : _args7$args._columns) || "";
|
|
66
74
|
this[_hooks] = {
|
|
67
75
|
create: {
|
|
68
76
|
before: [],
|
|
@@ -209,6 +217,26 @@ class SpiceModel {
|
|
|
209
217
|
}
|
|
210
218
|
// }
|
|
211
219
|
}
|
|
220
|
+
applyMappingDepthArgs(args) {
|
|
221
|
+
if (args === void 0) {
|
|
222
|
+
args = {};
|
|
223
|
+
}
|
|
224
|
+
var hasLegacyDepth = args.mapping_dept !== undefined && args.mapping_dept !== null;
|
|
225
|
+
if (hasLegacyDepth) {
|
|
226
|
+
this[_mapping_dept] = args.mapping_dept;
|
|
227
|
+
this[_mapping_dept_scalar] = args.mapping_dept;
|
|
228
|
+
this[_mapping_dept_array] = args.mapping_dept;
|
|
229
|
+
}
|
|
230
|
+
if (args.mapping_dept_scalar !== undefined && args.mapping_dept_scalar !== null) {
|
|
231
|
+
this[_mapping_dept_scalar] = args.mapping_dept_scalar;
|
|
232
|
+
}
|
|
233
|
+
if (args.mapping_dept_array !== undefined && args.mapping_dept_array !== null) {
|
|
234
|
+
this[_mapping_dept_array] = args.mapping_dept_array;
|
|
235
|
+
}
|
|
236
|
+
if (args.mapping_dept_exempt) {
|
|
237
|
+
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
212
240
|
get database() {
|
|
213
241
|
return this[_database];
|
|
214
242
|
}
|
|
@@ -250,13 +278,13 @@ class SpiceModel {
|
|
|
250
278
|
return [];
|
|
251
279
|
})();
|
|
252
280
|
}
|
|
253
|
-
createValidationString(
|
|
281
|
+
createValidationString(_ref5) {
|
|
254
282
|
var {
|
|
255
283
|
property_name,
|
|
256
284
|
props,
|
|
257
285
|
operation,
|
|
258
286
|
complete
|
|
259
|
-
} =
|
|
287
|
+
} = _ref5;
|
|
260
288
|
var operationExempt = {
|
|
261
289
|
PUT: ["required", "unique"]
|
|
262
290
|
};
|
|
@@ -487,14 +515,13 @@ class SpiceModel {
|
|
|
487
515
|
// Profiling: use track() for proper async context forking
|
|
488
516
|
var p = (_this4$_ctx = _this4[_ctx]) == null ? void 0 : _this4$_ctx.profiler;
|
|
489
517
|
var doGet = /*#__PURE__*/function () {
|
|
490
|
-
var
|
|
491
|
-
if (args.mapping_dept) _this4[_mapping_dept] = args.mapping_dept;
|
|
492
|
-
if (args.mapping_dept_exempt) _this4[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
518
|
+
var _ref6 = _asyncToGenerator(function* () {
|
|
493
519
|
if (!args) {
|
|
494
520
|
args = {};
|
|
495
521
|
}
|
|
522
|
+
_this4.applyMappingDepthArgs(args);
|
|
496
523
|
if (_.isString(args.id)) {
|
|
497
|
-
var
|
|
524
|
+
var _args8, _results, _results2, _results3, _results4, _results5, _results6;
|
|
498
525
|
if (args.skip_hooks !== true) {
|
|
499
526
|
yield _this4.run_hook(args, "get", "before");
|
|
500
527
|
}
|
|
@@ -514,7 +541,7 @@ class SpiceModel {
|
|
|
514
541
|
var originalColumns = args.columns;
|
|
515
542
|
|
|
516
543
|
// Extract nestings from columns if specified
|
|
517
|
-
var nestings = _this4.extractNestings(((
|
|
544
|
+
var nestings = _this4.extractNestings(((_args8 = args) == null ? void 0 : _args8.columns) || "", _this4.type);
|
|
518
545
|
|
|
519
546
|
// Build join metadata and prepare columns
|
|
520
547
|
_this4.buildJoinMetadata(nestings, args);
|
|
@@ -597,14 +624,14 @@ class SpiceModel {
|
|
|
597
624
|
}
|
|
598
625
|
});
|
|
599
626
|
return function doGet() {
|
|
600
|
-
return
|
|
627
|
+
return _ref6.apply(this, arguments);
|
|
601
628
|
};
|
|
602
629
|
}();
|
|
603
630
|
try {
|
|
604
631
|
if (p) {
|
|
605
|
-
var
|
|
632
|
+
var _args9;
|
|
606
633
|
return yield p.track(_this4.type + ".get", doGet, {
|
|
607
|
-
id: (
|
|
634
|
+
id: (_args9 = args) == null ? void 0 : _args9.id
|
|
608
635
|
});
|
|
609
636
|
}
|
|
610
637
|
return yield doGet();
|
|
@@ -632,14 +659,15 @@ class SpiceModel {
|
|
|
632
659
|
// ⚡ Profiling: use track() for proper async context forking
|
|
633
660
|
var p = (_this6$_ctx = _this6[_ctx]) == null ? void 0 : _this6$_ctx.profiler;
|
|
634
661
|
var doGetMulti = /*#__PURE__*/function () {
|
|
635
|
-
var
|
|
662
|
+
var _ref9 = _asyncToGenerator(function* () {
|
|
636
663
|
if (!args) {
|
|
637
664
|
args = {};
|
|
638
665
|
}
|
|
666
|
+
_this6.applyMappingDepthArgs(args);
|
|
639
667
|
if (args.skip_hooks != true) {
|
|
640
668
|
yield _this6.run_hook(_this6, "list", "before");
|
|
641
669
|
}
|
|
642
|
-
_.
|
|
670
|
+
args.ids = _.uniq(_.filter(args.ids || [], id => _.isString(id) && id.length > 0));
|
|
643
671
|
var key = "multi-get::" + _this6.type + "::" + _.join(args.ids, "|") + ":" + args.columns;
|
|
644
672
|
var results = [];
|
|
645
673
|
if (args.ids.length > 0) {
|
|
@@ -673,14 +701,14 @@ class SpiceModel {
|
|
|
673
701
|
return results;
|
|
674
702
|
});
|
|
675
703
|
return function doGetMulti() {
|
|
676
|
-
return
|
|
704
|
+
return _ref9.apply(this, arguments);
|
|
677
705
|
};
|
|
678
706
|
}();
|
|
679
707
|
try {
|
|
680
708
|
if (p) {
|
|
681
|
-
var
|
|
709
|
+
var _args0, _args0$ids;
|
|
682
710
|
return yield p.track(_this6.type + ".getMulti", doGetMulti, {
|
|
683
|
-
ids_count: ((
|
|
711
|
+
ids_count: ((_args0 = args) == null ? void 0 : (_args0$ids = _args0.ids) == null ? void 0 : _args0$ids.length) || 0
|
|
684
712
|
});
|
|
685
713
|
}
|
|
686
714
|
return yield doGetMulti();
|
|
@@ -729,7 +757,7 @@ class SpiceModel {
|
|
|
729
757
|
// Profiling: use track() for proper async context forking
|
|
730
758
|
var p = (_this8$_ctx = _this8[_ctx]) == null ? void 0 : _this8$_ctx.profiler;
|
|
731
759
|
var doUpdate = /*#__PURE__*/function () {
|
|
732
|
-
var
|
|
760
|
+
var _ref0 = _asyncToGenerator(function* () {
|
|
733
761
|
_this8.updated_at = new SDate().now();
|
|
734
762
|
var results = yield _this8.database.get(args.id);
|
|
735
763
|
var item_exist = yield _this8.exist(results);
|
|
@@ -773,7 +801,7 @@ class SpiceModel {
|
|
|
773
801
|
});
|
|
774
802
|
});
|
|
775
803
|
return function doUpdate() {
|
|
776
|
-
return
|
|
804
|
+
return _ref0.apply(this, arguments);
|
|
777
805
|
};
|
|
778
806
|
}();
|
|
779
807
|
try {
|
|
@@ -799,7 +827,7 @@ class SpiceModel {
|
|
|
799
827
|
// Profiling: use track() for proper async context forking
|
|
800
828
|
var p = (_this9$_ctx = _this9[_ctx]) == null ? void 0 : _this9$_ctx.profiler;
|
|
801
829
|
var doCreate = /*#__PURE__*/function () {
|
|
802
|
-
var
|
|
830
|
+
var _ref10 = _asyncToGenerator(function* () {
|
|
803
831
|
var form;
|
|
804
832
|
_this9.created_at = new SDate().now();
|
|
805
833
|
args = _.defaults(args, {
|
|
@@ -845,7 +873,7 @@ class SpiceModel {
|
|
|
845
873
|
});
|
|
846
874
|
});
|
|
847
875
|
return function doCreate() {
|
|
848
|
-
return
|
|
876
|
+
return _ref10.apply(this, arguments);
|
|
849
877
|
};
|
|
850
878
|
}();
|
|
851
879
|
try {
|
|
@@ -883,7 +911,7 @@ class SpiceModel {
|
|
|
883
911
|
// Profiling: use track() for proper async context forking
|
|
884
912
|
var p = (_this1$_ctx = _this1[_ctx]) == null ? void 0 : _this1$_ctx.profiler;
|
|
885
913
|
var doDelete = /*#__PURE__*/function () {
|
|
886
|
-
var
|
|
914
|
+
var _ref12 = _asyncToGenerator(function* () {
|
|
887
915
|
var item_exist = yield _this1.exist(args.id);
|
|
888
916
|
if (!item_exist) {
|
|
889
917
|
throw new Error(_this1.type + " does not exist.");
|
|
@@ -894,7 +922,7 @@ class SpiceModel {
|
|
|
894
922
|
}
|
|
895
923
|
var delete_response = {};
|
|
896
924
|
var dbOperation = /*#__PURE__*/function () {
|
|
897
|
-
var
|
|
925
|
+
var _ref13 = _asyncToGenerator(function* () {
|
|
898
926
|
if (args.hard) {
|
|
899
927
|
delete_response = yield _this1.database.delete(args.id);
|
|
900
928
|
_this1.setMonitor();
|
|
@@ -905,7 +933,7 @@ class SpiceModel {
|
|
|
905
933
|
}
|
|
906
934
|
});
|
|
907
935
|
return function dbOperation() {
|
|
908
|
-
return
|
|
936
|
+
return _ref13.apply(this, arguments);
|
|
909
937
|
};
|
|
910
938
|
}();
|
|
911
939
|
if (p) {
|
|
@@ -921,7 +949,7 @@ class SpiceModel {
|
|
|
921
949
|
return {};
|
|
922
950
|
});
|
|
923
951
|
return function doDelete() {
|
|
924
|
-
return
|
|
952
|
+
return _ref12.apply(this, arguments);
|
|
925
953
|
};
|
|
926
954
|
}();
|
|
927
955
|
try {
|
|
@@ -1111,12 +1139,12 @@ class SpiceModel {
|
|
|
1111
1139
|
}
|
|
1112
1140
|
createJoinSection(mappedNestings) {
|
|
1113
1141
|
if (!mappedNestings || mappedNestings.length === 0) return "";
|
|
1114
|
-
return mappedNestings.map(
|
|
1142
|
+
return mappedNestings.map(_ref14 => {
|
|
1115
1143
|
var {
|
|
1116
1144
|
alias,
|
|
1117
1145
|
reference,
|
|
1118
1146
|
is_array
|
|
1119
|
-
} =
|
|
1147
|
+
} = _ref14;
|
|
1120
1148
|
var keyspace = (0, _fix.fixCollection)(reference);
|
|
1121
1149
|
if (is_array === true) {
|
|
1122
1150
|
return "LEFT NEST `" + keyspace + "` AS `" + alias + "` ON KEYS `" + this.type + "`.`" + alias + "`";
|
|
@@ -1261,10 +1289,9 @@ class SpiceModel {
|
|
|
1261
1289
|
// Profiling: use track() for proper async context forking
|
|
1262
1290
|
var p = (_this10$_ctx = _this10[_ctx]) == null ? void 0 : _this10$_ctx.profiler;
|
|
1263
1291
|
var doList = /*#__PURE__*/function () {
|
|
1264
|
-
var
|
|
1265
|
-
var
|
|
1266
|
-
|
|
1267
|
-
if (args.mapping_dept_exempt) _this10[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
1292
|
+
var _ref15 = _asyncToGenerator(function* () {
|
|
1293
|
+
var _args1, _args10;
|
|
1294
|
+
_this10.applyMappingDepthArgs(args);
|
|
1268
1295
|
|
|
1269
1296
|
// Filter out id, meta().id, and any column ending with .id (e.g., dependents.id)
|
|
1270
1297
|
if (args.columns) {
|
|
@@ -1278,9 +1305,9 @@ class SpiceModel {
|
|
|
1278
1305
|
var originalColumns = args.columns;
|
|
1279
1306
|
|
|
1280
1307
|
// Find alias tokens from query/columns/sort
|
|
1281
|
-
var nestings = [..._this10.extractNestings(((
|
|
1308
|
+
var nestings = [..._this10.extractNestings(((_args1 = args) == null ? void 0 : _args1.query) || "", _this10.type),
|
|
1282
1309
|
//...this.extractNestings(args?.columns || "", this.type),
|
|
1283
|
-
..._this10.extractNestings(((
|
|
1310
|
+
..._this10.extractNestings(((_args10 = args) == null ? void 0 : _args10.sort) || "", _this10.type)];
|
|
1284
1311
|
|
|
1285
1312
|
// Build join metadata and prepare columns
|
|
1286
1313
|
var {
|
|
@@ -1341,15 +1368,15 @@ class SpiceModel {
|
|
|
1341
1368
|
return results;
|
|
1342
1369
|
});
|
|
1343
1370
|
return function doList() {
|
|
1344
|
-
return
|
|
1371
|
+
return _ref15.apply(this, arguments);
|
|
1345
1372
|
};
|
|
1346
1373
|
}();
|
|
1347
1374
|
try {
|
|
1348
1375
|
if (p) {
|
|
1349
|
-
var
|
|
1376
|
+
var _args11, _args12;
|
|
1350
1377
|
return yield p.track(_this10.type + ".list", doList, {
|
|
1351
|
-
limit: (
|
|
1352
|
-
offset: (
|
|
1378
|
+
limit: (_args11 = args) == null ? void 0 : _args11.limit,
|
|
1379
|
+
offset: (_args12 = args) == null ? void 0 : _args12.offset
|
|
1353
1380
|
});
|
|
1354
1381
|
}
|
|
1355
1382
|
return yield doList();
|
|
@@ -1395,8 +1422,9 @@ class SpiceModel {
|
|
|
1395
1422
|
// Profiling: use track() for proper async context forking
|
|
1396
1423
|
var p = (_this11$_ctx = _this11[_ctx]) == null ? void 0 : _this11$_ctx.profiler;
|
|
1397
1424
|
var doFetch = /*#__PURE__*/function () {
|
|
1398
|
-
var
|
|
1425
|
+
var _ref16 = _asyncToGenerator(function* () {
|
|
1399
1426
|
if (args.is_custom_query === "true" && args.ids.length > 0) {
|
|
1427
|
+
console.log("Fetching results from database", _this11.type, args, query, mappedNestings);
|
|
1400
1428
|
return yield _this11.database.query(query);
|
|
1401
1429
|
} else if (args.is_full_text === "true") {
|
|
1402
1430
|
return yield _this11.database.full_text_search(_this11.type, query || "", args.limit, args.offset, args._join);
|
|
@@ -1407,7 +1435,7 @@ class SpiceModel {
|
|
|
1407
1435
|
}
|
|
1408
1436
|
});
|
|
1409
1437
|
return function doFetch() {
|
|
1410
|
-
return
|
|
1438
|
+
return _ref16.apply(this, arguments);
|
|
1411
1439
|
};
|
|
1412
1440
|
}();
|
|
1413
1441
|
if (p) {
|
|
@@ -1416,12 +1444,12 @@ class SpiceModel {
|
|
|
1416
1444
|
return yield doFetch();
|
|
1417
1445
|
})();
|
|
1418
1446
|
}
|
|
1419
|
-
addHook(
|
|
1447
|
+
addHook(_ref17) {
|
|
1420
1448
|
var {
|
|
1421
1449
|
operation,
|
|
1422
1450
|
when,
|
|
1423
1451
|
execute
|
|
1424
|
-
} =
|
|
1452
|
+
} = _ref17;
|
|
1425
1453
|
this[_hooks][operation][when].push(execute);
|
|
1426
1454
|
}
|
|
1427
1455
|
run_hook(data, op, when, old_data) {
|
|
@@ -1519,7 +1547,8 @@ class SpiceModel {
|
|
|
1519
1547
|
data = Array.of(data);
|
|
1520
1548
|
}
|
|
1521
1549
|
var isExempt = _this13.isFieldExempt(source_property);
|
|
1522
|
-
|
|
1550
|
+
var activeMappingDept = type === "read" ? _this13[_mapping_dept_scalar] : _this13[_mapping_dept];
|
|
1551
|
+
if (isExempt || _this13[_level] + 1 < activeMappingDept) {
|
|
1523
1552
|
var classes = _.compact(_.isArray(Class) ? Class : [Class]);
|
|
1524
1553
|
var ids = [];
|
|
1525
1554
|
_.each(data, result => {
|
|
@@ -1543,12 +1572,14 @@ class SpiceModel {
|
|
|
1543
1572
|
|
|
1544
1573
|
// ⚡ Wrap in profiler track() to ensure proper async context for child operations
|
|
1545
1574
|
var fetchRelated = /*#__PURE__*/function () {
|
|
1546
|
-
var
|
|
1575
|
+
var _ref18 = _asyncToGenerator(function* () {
|
|
1547
1576
|
return yield Promise.allSettled(_.map(classes, obj => {
|
|
1548
1577
|
var objInstance = new obj(_extends({}, _this13[_args], {
|
|
1549
1578
|
skip_cache: _this13[_skip_cache],
|
|
1550
1579
|
_level: _this13[_level] + 1,
|
|
1551
1580
|
mapping_dept: _this13[_mapping_dept],
|
|
1581
|
+
mapping_dept_scalar: _this13[_mapping_dept_scalar],
|
|
1582
|
+
mapping_dept_array: _this13[_mapping_dept_array],
|
|
1552
1583
|
mapping_dept_exempt: _this13[_mapping_dept_exempt],
|
|
1553
1584
|
_columns: columnArray.join(","),
|
|
1554
1585
|
_current_path: childPath
|
|
@@ -1561,7 +1592,7 @@ class SpiceModel {
|
|
|
1561
1592
|
}));
|
|
1562
1593
|
});
|
|
1563
1594
|
return function fetchRelated() {
|
|
1564
|
-
return
|
|
1595
|
+
return _ref18.apply(this, arguments);
|
|
1565
1596
|
};
|
|
1566
1597
|
}();
|
|
1567
1598
|
var returned_all;
|
|
@@ -1628,7 +1659,8 @@ class SpiceModel {
|
|
|
1628
1659
|
data = Array.of(data);
|
|
1629
1660
|
}
|
|
1630
1661
|
var isExempt = _this14.isFieldExempt(source_property);
|
|
1631
|
-
|
|
1662
|
+
var activeMappingDept = type === "read" ? _this14[_mapping_dept_array] : _this14[_mapping_dept];
|
|
1663
|
+
if (isExempt || _this14[_level] + 1 < activeMappingDept) {
|
|
1632
1664
|
var ids = [];
|
|
1633
1665
|
_.each(data, result => {
|
|
1634
1666
|
var value = result[source_property];
|
|
@@ -1661,12 +1693,14 @@ class SpiceModel {
|
|
|
1661
1693
|
|
|
1662
1694
|
// ⚡ Wrap in profiler track() to ensure proper async context for child operations
|
|
1663
1695
|
var fetchRelated = /*#__PURE__*/function () {
|
|
1664
|
-
var
|
|
1696
|
+
var _ref19 = _asyncToGenerator(function* () {
|
|
1665
1697
|
return yield Promise.allSettled(_.map(classes, obj => {
|
|
1666
1698
|
return new obj(_extends({}, _this14[_args], {
|
|
1667
1699
|
skip_cache: _this14[_skip_cache],
|
|
1668
1700
|
_level: _this14[_level] + 1,
|
|
1669
1701
|
mapping_dept: _this14[_mapping_dept],
|
|
1702
|
+
mapping_dept_scalar: _this14[_mapping_dept_scalar],
|
|
1703
|
+
mapping_dept_array: _this14[_mapping_dept_array],
|
|
1670
1704
|
mapping_dept_exempt: _this14[_mapping_dept_exempt],
|
|
1671
1705
|
_columns: columnArray.join(","),
|
|
1672
1706
|
_current_path: childPath
|
|
@@ -1678,7 +1712,7 @@ class SpiceModel {
|
|
|
1678
1712
|
}));
|
|
1679
1713
|
});
|
|
1680
1714
|
return function fetchRelated() {
|
|
1681
|
-
return
|
|
1715
|
+
return _ref19.apply(this, arguments);
|
|
1682
1716
|
};
|
|
1683
1717
|
}();
|
|
1684
1718
|
var returned_all;
|
|
@@ -1726,11 +1760,11 @@ class SpiceModel {
|
|
|
1726
1760
|
return original_is_array ? data : data[0];
|
|
1727
1761
|
})();
|
|
1728
1762
|
}
|
|
1729
|
-
addModifier(
|
|
1763
|
+
addModifier(_ref20) {
|
|
1730
1764
|
var {
|
|
1731
1765
|
when,
|
|
1732
1766
|
execute
|
|
1733
|
-
} =
|
|
1767
|
+
} = _ref20;
|
|
1734
1768
|
if (this[_serializers][when]) {
|
|
1735
1769
|
this[_serializers][when]["modifiers"].push(execute);
|
|
1736
1770
|
}
|
|
@@ -1800,14 +1834,14 @@ class SpiceModel {
|
|
|
1800
1834
|
_this15.addModifier({
|
|
1801
1835
|
when: properties[i].map.when || "read",
|
|
1802
1836
|
execute: function () {
|
|
1803
|
-
var _execute = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns) {
|
|
1837
|
+
var _execute = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
|
|
1804
1838
|
// Skip if columns are specified but this field isn't included
|
|
1805
1839
|
if (!_this15.isFieldInColumns(i, columns)) {
|
|
1806
1840
|
return data;
|
|
1807
1841
|
}
|
|
1808
|
-
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,
|
|
1842
|
+
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);
|
|
1809
1843
|
});
|
|
1810
|
-
function execute(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8) {
|
|
1844
|
+
function execute(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8, _x9) {
|
|
1811
1845
|
return _execute.apply(this, arguments);
|
|
1812
1846
|
}
|
|
1813
1847
|
return execute;
|
|
@@ -1821,14 +1855,14 @@ class SpiceModel {
|
|
|
1821
1855
|
_this15.addModifier({
|
|
1822
1856
|
when: properties[i].map.when || "read",
|
|
1823
1857
|
execute: function () {
|
|
1824
|
-
var _execute2 = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns) {
|
|
1858
|
+
var _execute2 = _asyncToGenerator(function* (data, old_data, ctx, type, args, mapping_dept, level, columns, serializationType) {
|
|
1825
1859
|
// Skip if columns are specified but this field isn't included
|
|
1826
1860
|
if (!_this15.isFieldInColumns(i, columns)) {
|
|
1827
1861
|
return data;
|
|
1828
1862
|
}
|
|
1829
|
-
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,
|
|
1863
|
+
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);
|
|
1830
1864
|
});
|
|
1831
|
-
function execute(
|
|
1865
|
+
function execute(_x0, _x1, _x10, _x11, _x12, _x13, _x14, _x15, _x16) {
|
|
1832
1866
|
return _execute2.apply(this, arguments);
|
|
1833
1867
|
}
|
|
1834
1868
|
return execute;
|
|
@@ -1860,7 +1894,7 @@ class SpiceModel {
|
|
|
1860
1894
|
// Profiling: use track() for proper async context forking
|
|
1861
1895
|
var p = (_this16$_ctx = _this16[_ctx]) == null ? void 0 : _this16$_ctx.profiler;
|
|
1862
1896
|
var doSerialize = /*#__PURE__*/function () {
|
|
1863
|
-
var
|
|
1897
|
+
var _ref21 = _asyncToGenerator(function* () {
|
|
1864
1898
|
var _this16$_serializers, _this16$_serializers$;
|
|
1865
1899
|
// Early exit if serialization should not run.
|
|
1866
1900
|
if (!_this16.shouldSerializerRun(args, type)) {
|
|
@@ -1878,7 +1912,7 @@ class SpiceModel {
|
|
|
1878
1912
|
for (var i = 0; i < modifiers.length; i++) {
|
|
1879
1913
|
var modifier = modifiers[i];
|
|
1880
1914
|
try {
|
|
1881
|
-
var result = yield modifier(data, old_data, _this16[_ctx], _this16.type, args, _this16[_mapping_dept], _this16[_level], _this16[_columns]);
|
|
1915
|
+
var result = yield modifier(data, old_data, _this16[_ctx], _this16.type, args, _this16[_mapping_dept], _this16[_level], _this16[_columns], type);
|
|
1882
1916
|
// Guard against modifiers that return undefined
|
|
1883
1917
|
if (result !== undefined) {
|
|
1884
1918
|
data = result;
|
|
@@ -1928,7 +1962,7 @@ class SpiceModel {
|
|
|
1928
1962
|
return originalIsArray ? data : data[0];
|
|
1929
1963
|
});
|
|
1930
1964
|
return function doSerialize() {
|
|
1931
|
-
return
|
|
1965
|
+
return _ref21.apply(this, arguments);
|
|
1932
1966
|
};
|
|
1933
1967
|
}();
|
|
1934
1968
|
try {
|
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,6 +22,8 @@ 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(),
|
|
25
27
|
_mapping_dept_exempt = Symbol(),
|
|
26
28
|
_current_path = Symbol();
|
|
27
29
|
|
|
@@ -47,12 +49,29 @@ if (!Promise.allSettled) {
|
|
|
47
49
|
export default class SpiceModel {
|
|
48
50
|
constructor(args = {}) {
|
|
49
51
|
try {
|
|
52
|
+
const modelArgs = args?.args || {};
|
|
53
|
+
const requestProfile = modelArgs?.ctx?.state?.read_profile || {};
|
|
54
|
+
const defaultMappingDept = process.env.MAPPING_DEPT || 3;
|
|
55
|
+
const legacyMappingDept =
|
|
56
|
+
modelArgs.mapping_dept !== undefined && modelArgs.mapping_dept !== null ?
|
|
57
|
+
modelArgs.mapping_dept
|
|
58
|
+
: undefined;
|
|
59
|
+
|
|
50
60
|
var dbtype =
|
|
51
61
|
spice.config.database.connections[args.connection].type || "couchbase";
|
|
52
62
|
let Database = require(`spice-${dbtype}`);
|
|
53
|
-
this[_mapping_dept] =
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
this[_mapping_dept] = legacyMappingDept ?? defaultMappingDept;
|
|
64
|
+
this[_mapping_dept_scalar] =
|
|
65
|
+
modelArgs.mapping_dept_scalar ??
|
|
66
|
+
legacyMappingDept ??
|
|
67
|
+
requestProfile.mapping_dept_scalar ??
|
|
68
|
+
defaultMappingDept;
|
|
69
|
+
this[_mapping_dept_array] =
|
|
70
|
+
modelArgs.mapping_dept_array ??
|
|
71
|
+
legacyMappingDept ??
|
|
72
|
+
requestProfile.mapping_dept_array ??
|
|
73
|
+
defaultMappingDept;
|
|
74
|
+
this[_mapping_dept_exempt] = modelArgs.mapping_dept_exempt || [];
|
|
56
75
|
this.type = "";
|
|
57
76
|
this.collection = args.connection;
|
|
58
77
|
this[_args] = args.args;
|
|
@@ -237,6 +256,32 @@ export default class SpiceModel {
|
|
|
237
256
|
// }
|
|
238
257
|
}
|
|
239
258
|
|
|
259
|
+
applyMappingDepthArgs(args = {}) {
|
|
260
|
+
const hasLegacyDepth =
|
|
261
|
+
args.mapping_dept !== undefined && args.mapping_dept !== null;
|
|
262
|
+
|
|
263
|
+
if (hasLegacyDepth) {
|
|
264
|
+
this[_mapping_dept] = args.mapping_dept;
|
|
265
|
+
this[_mapping_dept_scalar] = args.mapping_dept;
|
|
266
|
+
this[_mapping_dept_array] = args.mapping_dept;
|
|
267
|
+
}
|
|
268
|
+
if (
|
|
269
|
+
args.mapping_dept_scalar !== undefined &&
|
|
270
|
+
args.mapping_dept_scalar !== null
|
|
271
|
+
) {
|
|
272
|
+
this[_mapping_dept_scalar] = args.mapping_dept_scalar;
|
|
273
|
+
}
|
|
274
|
+
if (
|
|
275
|
+
args.mapping_dept_array !== undefined &&
|
|
276
|
+
args.mapping_dept_array !== null
|
|
277
|
+
) {
|
|
278
|
+
this[_mapping_dept_array] = args.mapping_dept_array;
|
|
279
|
+
}
|
|
280
|
+
if (args.mapping_dept_exempt) {
|
|
281
|
+
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
240
285
|
get database() {
|
|
241
286
|
return this[_database];
|
|
242
287
|
}
|
|
@@ -515,13 +560,10 @@ export default class SpiceModel {
|
|
|
515
560
|
const p = this[_ctx]?.profiler;
|
|
516
561
|
|
|
517
562
|
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
563
|
if (!args) {
|
|
523
564
|
args = {};
|
|
524
565
|
}
|
|
566
|
+
this.applyMappingDepthArgs(args);
|
|
525
567
|
if (_.isString(args.id)) {
|
|
526
568
|
if (args.skip_hooks !== true) {
|
|
527
569
|
await this.run_hook(args, "get", "before");
|
|
@@ -687,10 +729,13 @@ export default class SpiceModel {
|
|
|
687
729
|
if (!args) {
|
|
688
730
|
args = {};
|
|
689
731
|
}
|
|
732
|
+
this.applyMappingDepthArgs(args);
|
|
690
733
|
if (args.skip_hooks != true) {
|
|
691
734
|
await this.run_hook(this, "list", "before");
|
|
692
735
|
}
|
|
693
|
-
|
|
736
|
+
args.ids = _.uniq(
|
|
737
|
+
_.filter(args.ids || [], (id) => _.isString(id) && id.length > 0),
|
|
738
|
+
);
|
|
694
739
|
let key = `multi-get::${this.type}::${_.join(args.ids, "|")}:${args.columns}`;
|
|
695
740
|
let results = [];
|
|
696
741
|
|
|
@@ -1384,9 +1429,7 @@ export default class SpiceModel {
|
|
|
1384
1429
|
const p = this[_ctx]?.profiler;
|
|
1385
1430
|
|
|
1386
1431
|
const doList = async () => {
|
|
1387
|
-
|
|
1388
|
-
if (args.mapping_dept_exempt)
|
|
1389
|
-
this[_mapping_dept_exempt] = args.mapping_dept_exempt;
|
|
1432
|
+
this.applyMappingDepthArgs(args);
|
|
1390
1433
|
|
|
1391
1434
|
// Filter out id, meta().id, and any column ending with .id (e.g., dependents.id)
|
|
1392
1435
|
if (args.columns) {
|
|
@@ -1558,6 +1601,13 @@ export default class SpiceModel {
|
|
|
1558
1601
|
|
|
1559
1602
|
const doFetch = async () => {
|
|
1560
1603
|
if (args.is_custom_query === "true" && args.ids.length > 0) {
|
|
1604
|
+
console.log(
|
|
1605
|
+
"Fetching results from database",
|
|
1606
|
+
this.type,
|
|
1607
|
+
args,
|
|
1608
|
+
query,
|
|
1609
|
+
mappedNestings,
|
|
1610
|
+
);
|
|
1561
1611
|
return await this.database.query(query);
|
|
1562
1612
|
} else if (args.is_full_text === "true") {
|
|
1563
1613
|
return await this.database.full_text_search(
|
|
@@ -1717,7 +1767,9 @@ export default class SpiceModel {
|
|
|
1717
1767
|
data = Array.of(data);
|
|
1718
1768
|
}
|
|
1719
1769
|
const isExempt = this.isFieldExempt(source_property);
|
|
1720
|
-
|
|
1770
|
+
const activeMappingDept =
|
|
1771
|
+
type === "read" ? this[_mapping_dept_scalar] : this[_mapping_dept];
|
|
1772
|
+
if (isExempt || this[_level] + 1 < activeMappingDept) {
|
|
1721
1773
|
let classes = _.compact(_.isArray(Class) ? Class : [Class]);
|
|
1722
1774
|
|
|
1723
1775
|
let ids = [];
|
|
@@ -1761,6 +1813,8 @@ export default class SpiceModel {
|
|
|
1761
1813
|
skip_cache: this[_skip_cache],
|
|
1762
1814
|
_level: this[_level] + 1,
|
|
1763
1815
|
mapping_dept: this[_mapping_dept],
|
|
1816
|
+
mapping_dept_scalar: this[_mapping_dept_scalar],
|
|
1817
|
+
mapping_dept_array: this[_mapping_dept_array],
|
|
1764
1818
|
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1765
1819
|
_columns: columnArray.join(","),
|
|
1766
1820
|
_current_path: childPath,
|
|
@@ -1881,7 +1935,9 @@ export default class SpiceModel {
|
|
|
1881
1935
|
data = Array.of(data);
|
|
1882
1936
|
}
|
|
1883
1937
|
const isExempt = this.isFieldExempt(source_property);
|
|
1884
|
-
|
|
1938
|
+
const activeMappingDept =
|
|
1939
|
+
type === "read" ? this[_mapping_dept_array] : this[_mapping_dept];
|
|
1940
|
+
if (isExempt || this[_level] + 1 < activeMappingDept) {
|
|
1885
1941
|
let ids = [];
|
|
1886
1942
|
_.each(data, (result) => {
|
|
1887
1943
|
let value = result[source_property];
|
|
@@ -1932,6 +1988,8 @@ export default class SpiceModel {
|
|
|
1932
1988
|
skip_cache: this[_skip_cache],
|
|
1933
1989
|
_level: this[_level] + 1,
|
|
1934
1990
|
mapping_dept: this[_mapping_dept],
|
|
1991
|
+
mapping_dept_scalar: this[_mapping_dept_scalar],
|
|
1992
|
+
mapping_dept_array: this[_mapping_dept_array],
|
|
1935
1993
|
mapping_dept_exempt: this[_mapping_dept_exempt],
|
|
1936
1994
|
_columns: columnArray.join(","),
|
|
1937
1995
|
_current_path: childPath,
|
|
@@ -2096,6 +2154,7 @@ export default class SpiceModel {
|
|
|
2096
2154
|
mapping_dept,
|
|
2097
2155
|
level,
|
|
2098
2156
|
columns,
|
|
2157
|
+
serializationType,
|
|
2099
2158
|
) => {
|
|
2100
2159
|
// Skip if columns are specified but this field isn't included
|
|
2101
2160
|
if (!this.isFieldInColumns(i, columns)) {
|
|
@@ -2110,7 +2169,7 @@ export default class SpiceModel {
|
|
|
2110
2169
|
properties[i].map.destination || i,
|
|
2111
2170
|
properties[i],
|
|
2112
2171
|
args,
|
|
2113
|
-
|
|
2172
|
+
serializationType,
|
|
2114
2173
|
mapping_dept,
|
|
2115
2174
|
level,
|
|
2116
2175
|
columns,
|
|
@@ -2132,6 +2191,7 @@ export default class SpiceModel {
|
|
|
2132
2191
|
mapping_dept,
|
|
2133
2192
|
level,
|
|
2134
2193
|
columns,
|
|
2194
|
+
serializationType,
|
|
2135
2195
|
) => {
|
|
2136
2196
|
// Skip if columns are specified but this field isn't included
|
|
2137
2197
|
if (!this.isFieldInColumns(i, columns)) {
|
|
@@ -2147,7 +2207,7 @@ export default class SpiceModel {
|
|
|
2147
2207
|
properties[i].map.destination || i,
|
|
2148
2208
|
properties[i],
|
|
2149
2209
|
args,
|
|
2150
|
-
|
|
2210
|
+
serializationType,
|
|
2151
2211
|
mapping_dept,
|
|
2152
2212
|
level,
|
|
2153
2213
|
columns,
|
|
@@ -2197,6 +2257,7 @@ export default class SpiceModel {
|
|
|
2197
2257
|
this[_mapping_dept],
|
|
2198
2258
|
this[_level],
|
|
2199
2259
|
this[_columns],
|
|
2260
|
+
type,
|
|
2200
2261
|
);
|
|
2201
2262
|
// Guard against modifiers that return undefined
|
|
2202
2263
|
if (result !== undefined) {
|
|
@@ -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
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
const { createTestModel, seedDatabase } = require('../helpers/modelFactory');
|
|
2
|
+
|
|
3
|
+
describe('SpiceModel type-specific mapping depth', () => {
|
|
4
|
+
function createRelatedModel(records, capture) {
|
|
5
|
+
return class RelatedModel {
|
|
6
|
+
constructor(args) {
|
|
7
|
+
capture.constructorArgs.push(args);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async getMulti({ ids }) {
|
|
11
|
+
capture.getMultiIds.push(ids);
|
|
12
|
+
return records.filter((record) => ids.includes(record.id));
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createMappedModel(args = {}) {
|
|
18
|
+
const scalarCapture = { constructorArgs: [], getMultiIds: [] };
|
|
19
|
+
const arrayCapture = { constructorArgs: [], getMultiIds: [] };
|
|
20
|
+
const ScalarModel = createRelatedModel(
|
|
21
|
+
[{ id: 'group-1', name: 'Administrators' }],
|
|
22
|
+
scalarCapture
|
|
23
|
+
);
|
|
24
|
+
const ArrayModel = createRelatedModel(
|
|
25
|
+
[
|
|
26
|
+
{ id: 'child-1', name: 'First' },
|
|
27
|
+
{ id: 'child-2', name: 'Second' }
|
|
28
|
+
],
|
|
29
|
+
arrayCapture
|
|
30
|
+
);
|
|
31
|
+
const model = createTestModel({
|
|
32
|
+
type: 'parent',
|
|
33
|
+
args,
|
|
34
|
+
props: {
|
|
35
|
+
group: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
map: { type: 'model', reference: ScalarModel }
|
|
38
|
+
},
|
|
39
|
+
children: {
|
|
40
|
+
type: 'array',
|
|
41
|
+
map: { type: 'model', reference: ArrayModel }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
seedDatabase(model, {
|
|
46
|
+
id: 'parent-1',
|
|
47
|
+
type: 'parent',
|
|
48
|
+
group: 'group-1',
|
|
49
|
+
children: ['child-1', 'child-2']
|
|
50
|
+
});
|
|
51
|
+
return { model, scalarCapture, arrayCapture };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('uses scalar depth 1 and array depth 3 from the request profile', async () => {
|
|
55
|
+
const ctx = {
|
|
56
|
+
state: {
|
|
57
|
+
read_profile: {
|
|
58
|
+
mapping_dept_scalar: 1,
|
|
59
|
+
mapping_dept_array: 3
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const { model, scalarCapture, arrayCapture } = createMappedModel({ ctx });
|
|
64
|
+
|
|
65
|
+
const result = await model.get({ id: 'parent-1' });
|
|
66
|
+
|
|
67
|
+
expect(result.group).toBe('group-1');
|
|
68
|
+
expect(result.children).toEqual([
|
|
69
|
+
expect.objectContaining({ id: 'child-1', name: 'First' }),
|
|
70
|
+
expect.objectContaining({ id: 'child-2', name: 'Second' })
|
|
71
|
+
]);
|
|
72
|
+
expect(scalarCapture.getMultiIds).toEqual([]);
|
|
73
|
+
expect(arrayCapture.getMultiIds).toEqual([['child-1', 'child-2']]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('keeps legacy mapping_dept behavior for both relationship types', async () => {
|
|
77
|
+
const { model, scalarCapture, arrayCapture } = createMappedModel({
|
|
78
|
+
mapping_dept: 1
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const result = await model.get({ id: 'parent-1' });
|
|
82
|
+
|
|
83
|
+
expect(result.group).toBe('group-1');
|
|
84
|
+
expect(result.children).toEqual(['child-1', 'child-2']);
|
|
85
|
+
expect(scalarCapture.getMultiIds).toEqual([]);
|
|
86
|
+
expect(arrayCapture.getMultiIds).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('explicit read arguments override the request profile independently', async () => {
|
|
90
|
+
const ctx = {
|
|
91
|
+
state: {
|
|
92
|
+
read_profile: {
|
|
93
|
+
mapping_dept_scalar: 1,
|
|
94
|
+
mapping_dept_array: 1
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const { model } = createMappedModel({ ctx });
|
|
99
|
+
|
|
100
|
+
const result = await model.get({
|
|
101
|
+
id: 'parent-1',
|
|
102
|
+
mapping_dept_scalar: 3,
|
|
103
|
+
mapping_dept_array: 3
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(result.group).toEqual(
|
|
107
|
+
expect.objectContaining({ id: 'group-1', name: 'Administrators' })
|
|
108
|
+
);
|
|
109
|
+
expect(result.children).toHaveLength(2);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('mapping depth exemptions continue to expand scalar paths', async () => {
|
|
113
|
+
const { model, scalarCapture } = createMappedModel({
|
|
114
|
+
mapping_dept_scalar: 1,
|
|
115
|
+
mapping_dept_array: 3,
|
|
116
|
+
mapping_dept_exempt: ['group']
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = await model.get({ id: 'parent-1' });
|
|
120
|
+
|
|
121
|
+
expect(result.group).toEqual(
|
|
122
|
+
expect.objectContaining({ id: 'group-1', name: 'Administrators' })
|
|
123
|
+
);
|
|
124
|
+
expect(scalarCapture.getMultiIds).toEqual([['group-1']]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('propagates both depths to related model instances', async () => {
|
|
128
|
+
const { model, arrayCapture } = createMappedModel({
|
|
129
|
+
mapping_dept_scalar: 1,
|
|
130
|
+
mapping_dept_array: 3
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await model.getMulti({ ids: ['parent-1'] });
|
|
134
|
+
|
|
135
|
+
expect(arrayCapture.constructorArgs[0]).toEqual(
|
|
136
|
+
expect.objectContaining({
|
|
137
|
+
mapping_dept_scalar: 1,
|
|
138
|
+
mapping_dept_array: 3,
|
|
139
|
+
_level: 1
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('does not apply the request read profile to write serialization', async () => {
|
|
145
|
+
const capture = { constructorArgs: [], getMultiIds: [] };
|
|
146
|
+
const RelatedModel = createRelatedModel(
|
|
147
|
+
[{ id: 'group-1', name: 'Administrators' }],
|
|
148
|
+
capture
|
|
149
|
+
);
|
|
150
|
+
const model = createTestModel({
|
|
151
|
+
type: 'parent',
|
|
152
|
+
args: {
|
|
153
|
+
ctx: {
|
|
154
|
+
state: {
|
|
155
|
+
read_profile: {
|
|
156
|
+
mapping_dept_scalar: 1,
|
|
157
|
+
mapping_dept_array: 1
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
props: {
|
|
163
|
+
group: {
|
|
164
|
+
type: 'string',
|
|
165
|
+
map: { type: 'model', reference: RelatedModel, when: 'write' }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const result = await model.do_serialize(
|
|
171
|
+
{ id: 'parent-1', group: 'group-1' },
|
|
172
|
+
'write',
|
|
173
|
+
{},
|
|
174
|
+
{}
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result.group).toEqual(
|
|
178
|
+
expect.objectContaining({ id: 'group-1', name: 'Administrators' })
|
|
179
|
+
);
|
|
180
|
+
expect(capture.getMultiIds).toEqual([['group-1']]);
|
|
181
|
+
});
|
|
182
|
+
});
|