spice-js 2.7.28 → 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 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
- global.spice = {};
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, _args2$args, _args3, _args3$args, _args$args, _args4, _args4$args, _args5, _args5$args, _args6, _args6$args, _args7, _args7$args, _args8, _args8$args;
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] = ((_args2 = args) == null ? void 0 : (_args2$args = _args2.args) == null ? void 0 : _args2$args.mapping_dept) || process.env.MAPPING_DEPT || 3;
55
- this[_mapping_dept_exempt] = ((_args3 = args) == null ? void 0 : (_args3$args = _args3.args) == null ? void 0 : _args3$args.mapping_dept_exempt) || [];
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] = (_args4 = args) == null ? void 0 : (_args4$args = _args4.args) == null ? void 0 : _args4$args.ctx;
62
- this[_skip_cache] = ((_args5 = args) == null ? void 0 : (_args5$args = _args5.args) == null ? void 0 : _args5$args.skip_cache) || false;
63
- this[_level] = ((_args6 = args) == null ? void 0 : (_args6$args = _args6.args) == null ? void 0 : _args6$args._level) || 0;
64
- this[_current_path] = ((_args7 = args) == null ? void 0 : (_args7$args = _args7.args) == null ? void 0 : _args7$args._current_path) || "";
65
- this[_columns] = ((_args8 = args) == null ? void 0 : (_args8$args = _args8.args) == null ? void 0 : _args8$args._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(_ref) {
281
+ createValidationString(_ref5) {
254
282
  var {
255
283
  property_name,
256
284
  props,
257
285
  operation,
258
286
  complete
259
- } = _ref;
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 _ref2 = _asyncToGenerator(function* () {
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 _args9, _results, _results2, _results3, _results4, _results5, _results6;
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(((_args9 = args) == null ? void 0 : _args9.columns) || "", _this4.type);
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 _ref2.apply(this, arguments);
627
+ return _ref6.apply(this, arguments);
601
628
  };
602
629
  }();
603
630
  try {
604
631
  if (p) {
605
- var _args0;
632
+ var _args9;
606
633
  return yield p.track(_this4.type + ".get", doGet, {
607
- id: (_args0 = args) == null ? void 0 : _args0.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 _ref5 = _asyncToGenerator(function* () {
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
- _.remove(args.ids, o => o == undefined);
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 _ref5.apply(this, arguments);
704
+ return _ref9.apply(this, arguments);
677
705
  };
678
706
  }();
679
707
  try {
680
708
  if (p) {
681
- var _args1, _args1$ids;
709
+ var _args0, _args0$ids;
682
710
  return yield p.track(_this6.type + ".getMulti", doGetMulti, {
683
- ids_count: ((_args1 = args) == null ? void 0 : (_args1$ids = _args1.ids) == null ? void 0 : _args1$ids.length) || 0
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 _ref6 = _asyncToGenerator(function* () {
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 _ref6.apply(this, arguments);
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 _ref8 = _asyncToGenerator(function* () {
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 _ref8.apply(this, arguments);
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 _ref0 = _asyncToGenerator(function* () {
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 _ref1 = _asyncToGenerator(function* () {
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 _ref1.apply(this, arguments);
936
+ return _ref13.apply(this, arguments);
909
937
  };
910
938
  }();
911
939
  if (p) {
@@ -914,12 +942,14 @@ class SpiceModel {
914
942
  yield dbOperation();
915
943
  }
916
944
  if (args.skip_hooks != true) {
917
- yield _this1.run_hook(results, "delete", "after", results);
945
+ yield _this1.run_hook(_extends({}, results, {
946
+ id: args.id
947
+ }), "delete", "after", results);
918
948
  }
919
949
  return {};
920
950
  });
921
951
  return function doDelete() {
922
- return _ref0.apply(this, arguments);
952
+ return _ref12.apply(this, arguments);
923
953
  };
924
954
  }();
925
955
  try {
@@ -1109,12 +1139,12 @@ class SpiceModel {
1109
1139
  }
1110
1140
  createJoinSection(mappedNestings) {
1111
1141
  if (!mappedNestings || mappedNestings.length === 0) return "";
1112
- return mappedNestings.map(_ref10 => {
1142
+ return mappedNestings.map(_ref14 => {
1113
1143
  var {
1114
1144
  alias,
1115
1145
  reference,
1116
1146
  is_array
1117
- } = _ref10;
1147
+ } = _ref14;
1118
1148
  var keyspace = (0, _fix.fixCollection)(reference);
1119
1149
  if (is_array === true) {
1120
1150
  return "LEFT NEST `" + keyspace + "` AS `" + alias + "` ON KEYS `" + this.type + "`.`" + alias + "`";
@@ -1259,10 +1289,9 @@ class SpiceModel {
1259
1289
  // Profiling: use track() for proper async context forking
1260
1290
  var p = (_this10$_ctx = _this10[_ctx]) == null ? void 0 : _this10$_ctx.profiler;
1261
1291
  var doList = /*#__PURE__*/function () {
1262
- var _ref11 = _asyncToGenerator(function* () {
1263
- var _args10, _args11;
1264
- if (args.mapping_dept) _this10[_mapping_dept] = args.mapping_dept;
1265
- 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);
1266
1295
 
1267
1296
  // Filter out id, meta().id, and any column ending with .id (e.g., dependents.id)
1268
1297
  if (args.columns) {
@@ -1276,9 +1305,9 @@ class SpiceModel {
1276
1305
  var originalColumns = args.columns;
1277
1306
 
1278
1307
  // Find alias tokens from query/columns/sort
1279
- var nestings = [..._this10.extractNestings(((_args10 = args) == null ? void 0 : _args10.query) || "", _this10.type),
1308
+ var nestings = [..._this10.extractNestings(((_args1 = args) == null ? void 0 : _args1.query) || "", _this10.type),
1280
1309
  //...this.extractNestings(args?.columns || "", this.type),
1281
- ..._this10.extractNestings(((_args11 = args) == null ? void 0 : _args11.sort) || "", _this10.type)];
1310
+ ..._this10.extractNestings(((_args10 = args) == null ? void 0 : _args10.sort) || "", _this10.type)];
1282
1311
 
1283
1312
  // Build join metadata and prepare columns
1284
1313
  var {
@@ -1339,15 +1368,15 @@ class SpiceModel {
1339
1368
  return results;
1340
1369
  });
1341
1370
  return function doList() {
1342
- return _ref11.apply(this, arguments);
1371
+ return _ref15.apply(this, arguments);
1343
1372
  };
1344
1373
  }();
1345
1374
  try {
1346
1375
  if (p) {
1347
- var _args12, _args13;
1376
+ var _args11, _args12;
1348
1377
  return yield p.track(_this10.type + ".list", doList, {
1349
- limit: (_args12 = args) == null ? void 0 : _args12.limit,
1350
- offset: (_args13 = args) == null ? void 0 : _args13.offset
1378
+ limit: (_args11 = args) == null ? void 0 : _args11.limit,
1379
+ offset: (_args12 = args) == null ? void 0 : _args12.offset
1351
1380
  });
1352
1381
  }
1353
1382
  return yield doList();
@@ -1393,8 +1422,9 @@ class SpiceModel {
1393
1422
  // Profiling: use track() for proper async context forking
1394
1423
  var p = (_this11$_ctx = _this11[_ctx]) == null ? void 0 : _this11$_ctx.profiler;
1395
1424
  var doFetch = /*#__PURE__*/function () {
1396
- var _ref12 = _asyncToGenerator(function* () {
1425
+ var _ref16 = _asyncToGenerator(function* () {
1397
1426
  if (args.is_custom_query === "true" && args.ids.length > 0) {
1427
+ console.log("Fetching results from database", _this11.type, args, query, mappedNestings);
1398
1428
  return yield _this11.database.query(query);
1399
1429
  } else if (args.is_full_text === "true") {
1400
1430
  return yield _this11.database.full_text_search(_this11.type, query || "", args.limit, args.offset, args._join);
@@ -1405,7 +1435,7 @@ class SpiceModel {
1405
1435
  }
1406
1436
  });
1407
1437
  return function doFetch() {
1408
- return _ref12.apply(this, arguments);
1438
+ return _ref16.apply(this, arguments);
1409
1439
  };
1410
1440
  }();
1411
1441
  if (p) {
@@ -1414,12 +1444,12 @@ class SpiceModel {
1414
1444
  return yield doFetch();
1415
1445
  })();
1416
1446
  }
1417
- addHook(_ref13) {
1447
+ addHook(_ref17) {
1418
1448
  var {
1419
1449
  operation,
1420
1450
  when,
1421
1451
  execute
1422
- } = _ref13;
1452
+ } = _ref17;
1423
1453
  this[_hooks][operation][when].push(execute);
1424
1454
  }
1425
1455
  run_hook(data, op, when, old_data) {
@@ -1517,7 +1547,8 @@ class SpiceModel {
1517
1547
  data = Array.of(data);
1518
1548
  }
1519
1549
  var isExempt = _this13.isFieldExempt(source_property);
1520
- if (isExempt || _this13[_level] + 1 < _this13[_mapping_dept]) {
1550
+ var activeMappingDept = type === "read" ? _this13[_mapping_dept_scalar] : _this13[_mapping_dept];
1551
+ if (isExempt || _this13[_level] + 1 < activeMappingDept) {
1521
1552
  var classes = _.compact(_.isArray(Class) ? Class : [Class]);
1522
1553
  var ids = [];
1523
1554
  _.each(data, result => {
@@ -1541,12 +1572,14 @@ class SpiceModel {
1541
1572
 
1542
1573
  // ⚡ Wrap in profiler track() to ensure proper async context for child operations
1543
1574
  var fetchRelated = /*#__PURE__*/function () {
1544
- var _ref14 = _asyncToGenerator(function* () {
1575
+ var _ref18 = _asyncToGenerator(function* () {
1545
1576
  return yield Promise.allSettled(_.map(classes, obj => {
1546
1577
  var objInstance = new obj(_extends({}, _this13[_args], {
1547
1578
  skip_cache: _this13[_skip_cache],
1548
1579
  _level: _this13[_level] + 1,
1549
1580
  mapping_dept: _this13[_mapping_dept],
1581
+ mapping_dept_scalar: _this13[_mapping_dept_scalar],
1582
+ mapping_dept_array: _this13[_mapping_dept_array],
1550
1583
  mapping_dept_exempt: _this13[_mapping_dept_exempt],
1551
1584
  _columns: columnArray.join(","),
1552
1585
  _current_path: childPath
@@ -1559,7 +1592,7 @@ class SpiceModel {
1559
1592
  }));
1560
1593
  });
1561
1594
  return function fetchRelated() {
1562
- return _ref14.apply(this, arguments);
1595
+ return _ref18.apply(this, arguments);
1563
1596
  };
1564
1597
  }();
1565
1598
  var returned_all;
@@ -1626,7 +1659,8 @@ class SpiceModel {
1626
1659
  data = Array.of(data);
1627
1660
  }
1628
1661
  var isExempt = _this14.isFieldExempt(source_property);
1629
- if (isExempt || _this14[_level] + 1 < _this14[_mapping_dept]) {
1662
+ var activeMappingDept = type === "read" ? _this14[_mapping_dept_array] : _this14[_mapping_dept];
1663
+ if (isExempt || _this14[_level] + 1 < activeMappingDept) {
1630
1664
  var ids = [];
1631
1665
  _.each(data, result => {
1632
1666
  var value = result[source_property];
@@ -1659,12 +1693,14 @@ class SpiceModel {
1659
1693
 
1660
1694
  // ⚡ Wrap in profiler track() to ensure proper async context for child operations
1661
1695
  var fetchRelated = /*#__PURE__*/function () {
1662
- var _ref15 = _asyncToGenerator(function* () {
1696
+ var _ref19 = _asyncToGenerator(function* () {
1663
1697
  return yield Promise.allSettled(_.map(classes, obj => {
1664
1698
  return new obj(_extends({}, _this14[_args], {
1665
1699
  skip_cache: _this14[_skip_cache],
1666
1700
  _level: _this14[_level] + 1,
1667
1701
  mapping_dept: _this14[_mapping_dept],
1702
+ mapping_dept_scalar: _this14[_mapping_dept_scalar],
1703
+ mapping_dept_array: _this14[_mapping_dept_array],
1668
1704
  mapping_dept_exempt: _this14[_mapping_dept_exempt],
1669
1705
  _columns: columnArray.join(","),
1670
1706
  _current_path: childPath
@@ -1676,7 +1712,7 @@ class SpiceModel {
1676
1712
  }));
1677
1713
  });
1678
1714
  return function fetchRelated() {
1679
- return _ref15.apply(this, arguments);
1715
+ return _ref19.apply(this, arguments);
1680
1716
  };
1681
1717
  }();
1682
1718
  var returned_all;
@@ -1724,11 +1760,11 @@ class SpiceModel {
1724
1760
  return original_is_array ? data : data[0];
1725
1761
  })();
1726
1762
  }
1727
- addModifier(_ref16) {
1763
+ addModifier(_ref20) {
1728
1764
  var {
1729
1765
  when,
1730
1766
  execute
1731
- } = _ref16;
1767
+ } = _ref20;
1732
1768
  if (this[_serializers][when]) {
1733
1769
  this[_serializers][when]["modifiers"].push(execute);
1734
1770
  }
@@ -1798,14 +1834,14 @@ class SpiceModel {
1798
1834
  _this15.addModifier({
1799
1835
  when: properties[i].map.when || "read",
1800
1836
  execute: function () {
1801
- 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) {
1802
1838
  // Skip if columns are specified but this field isn't included
1803
1839
  if (!_this15.isFieldInColumns(i, columns)) {
1804
1840
  return data;
1805
1841
  }
1806
- 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, type, mapping_dept, level, columns);
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);
1807
1843
  });
1808
- function execute(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8) {
1844
+ function execute(_x, _x2, _x3, _x4, _x5, _x6, _x7, _x8, _x9) {
1809
1845
  return _execute.apply(this, arguments);
1810
1846
  }
1811
1847
  return execute;
@@ -1819,14 +1855,14 @@ class SpiceModel {
1819
1855
  _this15.addModifier({
1820
1856
  when: properties[i].map.when || "read",
1821
1857
  execute: function () {
1822
- 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) {
1823
1859
  // Skip if columns are specified but this field isn't included
1824
1860
  if (!_this15.isFieldInColumns(i, columns)) {
1825
1861
  return data;
1826
1862
  }
1827
- 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, type, mapping_dept, level, columns);
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);
1828
1864
  });
1829
- function execute(_x9, _x0, _x1, _x10, _x11, _x12, _x13, _x14) {
1865
+ function execute(_x0, _x1, _x10, _x11, _x12, _x13, _x14, _x15, _x16) {
1830
1866
  return _execute2.apply(this, arguments);
1831
1867
  }
1832
1868
  return execute;
@@ -1858,7 +1894,7 @@ class SpiceModel {
1858
1894
  // Profiling: use track() for proper async context forking
1859
1895
  var p = (_this16$_ctx = _this16[_ctx]) == null ? void 0 : _this16$_ctx.profiler;
1860
1896
  var doSerialize = /*#__PURE__*/function () {
1861
- var _ref17 = _asyncToGenerator(function* () {
1897
+ var _ref21 = _asyncToGenerator(function* () {
1862
1898
  var _this16$_serializers, _this16$_serializers$;
1863
1899
  // Early exit if serialization should not run.
1864
1900
  if (!_this16.shouldSerializerRun(args, type)) {
@@ -1876,7 +1912,7 @@ class SpiceModel {
1876
1912
  for (var i = 0; i < modifiers.length; i++) {
1877
1913
  var modifier = modifiers[i];
1878
1914
  try {
1879
- 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);
1880
1916
  // Guard against modifiers that return undefined
1881
1917
  if (result !== undefined) {
1882
1918
  data = result;
@@ -1926,7 +1962,7 @@ class SpiceModel {
1926
1962
  return originalIsArray ? data : data[0];
1927
1963
  });
1928
1964
  return function doSerialize() {
1929
- return _ref17.apply(this, arguments);
1965
+ return _ref21.apply(this, arguments);
1930
1966
  };
1931
1967
  }();
1932
1968
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.28",
3
+ "version": "2.7.30",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
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
- global.spice = {};
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
 
@@ -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
- args?.args?.mapping_dept || process.env.MAPPING_DEPT || 3;
55
- this[_mapping_dept_exempt] = args?.args?.mapping_dept_exempt || [];
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
- _.remove(args.ids, (o) => o == undefined);
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
 
@@ -995,7 +1040,12 @@ export default class SpiceModel {
995
1040
  }
996
1041
 
997
1042
  if (args.skip_hooks != true) {
998
- await this.run_hook(results, "delete", "after", results);
1043
+ await this.run_hook(
1044
+ { ...results, id: args.id },
1045
+ "delete",
1046
+ "after",
1047
+ results,
1048
+ );
999
1049
  }
1000
1050
  return {};
1001
1051
  };
@@ -1379,9 +1429,7 @@ export default class SpiceModel {
1379
1429
  const p = this[_ctx]?.profiler;
1380
1430
 
1381
1431
  const doList = async () => {
1382
- if (args.mapping_dept) this[_mapping_dept] = args.mapping_dept;
1383
- if (args.mapping_dept_exempt)
1384
- this[_mapping_dept_exempt] = args.mapping_dept_exempt;
1432
+ this.applyMappingDepthArgs(args);
1385
1433
 
1386
1434
  // Filter out id, meta().id, and any column ending with .id (e.g., dependents.id)
1387
1435
  if (args.columns) {
@@ -1553,6 +1601,13 @@ export default class SpiceModel {
1553
1601
 
1554
1602
  const doFetch = async () => {
1555
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
+ );
1556
1611
  return await this.database.query(query);
1557
1612
  } else if (args.is_full_text === "true") {
1558
1613
  return await this.database.full_text_search(
@@ -1712,7 +1767,9 @@ export default class SpiceModel {
1712
1767
  data = Array.of(data);
1713
1768
  }
1714
1769
  const isExempt = this.isFieldExempt(source_property);
1715
- if (isExempt || this[_level] + 1 < this[_mapping_dept]) {
1770
+ const activeMappingDept =
1771
+ type === "read" ? this[_mapping_dept_scalar] : this[_mapping_dept];
1772
+ if (isExempt || this[_level] + 1 < activeMappingDept) {
1716
1773
  let classes = _.compact(_.isArray(Class) ? Class : [Class]);
1717
1774
 
1718
1775
  let ids = [];
@@ -1756,6 +1813,8 @@ export default class SpiceModel {
1756
1813
  skip_cache: this[_skip_cache],
1757
1814
  _level: this[_level] + 1,
1758
1815
  mapping_dept: this[_mapping_dept],
1816
+ mapping_dept_scalar: this[_mapping_dept_scalar],
1817
+ mapping_dept_array: this[_mapping_dept_array],
1759
1818
  mapping_dept_exempt: this[_mapping_dept_exempt],
1760
1819
  _columns: columnArray.join(","),
1761
1820
  _current_path: childPath,
@@ -1876,7 +1935,9 @@ export default class SpiceModel {
1876
1935
  data = Array.of(data);
1877
1936
  }
1878
1937
  const isExempt = this.isFieldExempt(source_property);
1879
- if (isExempt || this[_level] + 1 < this[_mapping_dept]) {
1938
+ const activeMappingDept =
1939
+ type === "read" ? this[_mapping_dept_array] : this[_mapping_dept];
1940
+ if (isExempt || this[_level] + 1 < activeMappingDept) {
1880
1941
  let ids = [];
1881
1942
  _.each(data, (result) => {
1882
1943
  let value = result[source_property];
@@ -1927,6 +1988,8 @@ export default class SpiceModel {
1927
1988
  skip_cache: this[_skip_cache],
1928
1989
  _level: this[_level] + 1,
1929
1990
  mapping_dept: this[_mapping_dept],
1991
+ mapping_dept_scalar: this[_mapping_dept_scalar],
1992
+ mapping_dept_array: this[_mapping_dept_array],
1930
1993
  mapping_dept_exempt: this[_mapping_dept_exempt],
1931
1994
  _columns: columnArray.join(","),
1932
1995
  _current_path: childPath,
@@ -2091,6 +2154,7 @@ export default class SpiceModel {
2091
2154
  mapping_dept,
2092
2155
  level,
2093
2156
  columns,
2157
+ serializationType,
2094
2158
  ) => {
2095
2159
  // Skip if columns are specified but this field isn't included
2096
2160
  if (!this.isFieldInColumns(i, columns)) {
@@ -2105,7 +2169,7 @@ export default class SpiceModel {
2105
2169
  properties[i].map.destination || i,
2106
2170
  properties[i],
2107
2171
  args,
2108
- type,
2172
+ serializationType,
2109
2173
  mapping_dept,
2110
2174
  level,
2111
2175
  columns,
@@ -2127,6 +2191,7 @@ export default class SpiceModel {
2127
2191
  mapping_dept,
2128
2192
  level,
2129
2193
  columns,
2194
+ serializationType,
2130
2195
  ) => {
2131
2196
  // Skip if columns are specified but this field isn't included
2132
2197
  if (!this.isFieldInColumns(i, columns)) {
@@ -2142,7 +2207,7 @@ export default class SpiceModel {
2142
2207
  properties[i].map.destination || i,
2143
2208
  properties[i],
2144
2209
  args,
2145
- type,
2210
+ serializationType,
2146
2211
  mapping_dept,
2147
2212
  level,
2148
2213
  columns,
@@ -2192,6 +2257,7 @@ export default class SpiceModel {
2192
2257
  this[_mapping_dept],
2193
2258
  this[_level],
2194
2259
  this[_columns],
2260
+ type,
2195
2261
  );
2196
2262
  // Guard against modifiers that return undefined
2197
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: 'item',
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
+ });