spice-js 2.7.31 → 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.
@@ -712,17 +712,44 @@ class SpiceModel {
712
712
  }
713
713
  })();
714
714
  }
715
- query(query, scope) {
715
+ query(query, scope, options) {
716
716
  var _this5 = this;
717
717
  return _asyncToGenerator(function* () {
718
718
  try {
719
- var results = yield _this5.database.query(query, scope);
719
+ var results = yield _this5.database.query(query, scope, options);
720
720
  return results;
721
721
  } catch (error) {
722
722
  throw error;
723
723
  }
724
724
  })();
725
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
+ }
726
753
  getMulti(args) {
727
754
  var _this6 = this;
728
755
  return _asyncToGenerator(function* () {
@@ -1409,9 +1436,9 @@ class SpiceModel {
1409
1436
  if (args.skip_hooks !== true) {
1410
1437
  yield _this10.run_hook(_this10, "list", "before");
1411
1438
  }
1412
- 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;
1413
1440
  var results;
1414
- if (_this10.shouldUseCache(_this10.type)) {
1441
+ if (_this10.shouldUseCache(_this10.type) && !_this10.requiresFreshQuery(args)) {
1415
1442
  var cached = yield _this10.getCacheProviderObject(_this10.type).get(cacheKey);
1416
1443
  results = cached == null ? void 0 : cached.value;
1417
1444
  var isEmpty = (cached == null ? void 0 : cached.value) === undefined;
@@ -1494,14 +1521,15 @@ class SpiceModel {
1494
1521
  var p = (_this11$_ctx = _this11[_ctx]) == null ? void 0 : _this11$_ctx.profiler;
1495
1522
  var doFetch = /*#__PURE__*/function () {
1496
1523
  var _ref17 = _asyncToGenerator(function* () {
1524
+ var queryOptions = _this11.getQueryOptions(args);
1497
1525
  if (args.is_custom_query === "true" && args.ids.length > 0) {
1498
1526
  console.log("Fetching results from database", _this11.type, args, query, mappedNestings);
1499
- return yield _this11.database.query(query);
1527
+ return yield _this11.database.query(query, args.query_scope, queryOptions);
1500
1528
  } else if (args.is_full_text === "true") {
1501
1529
  return yield _this11.database.full_text_search(_this11.type, query || "", args.limit, args.offset, args._join);
1502
1530
  } else {
1503
1531
  var correctedColumns = _this11.correctColumns(args.columns, mappedNestings);
1504
- var result = yield _this11.database.search(_this11.type, correctedColumns || args.columns || "", query || "", args.limit, args.offset, args.sort, args.do_count, args.statement_consistent, args._join);
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);
1505
1533
  return result;
1506
1534
  }
1507
1535
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.31",
3
+ "version": "2.7.32",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
@@ -783,15 +783,47 @@ export default class SpiceModel {
783
783
  }
784
784
  }
785
785
 
786
- async query(query, scope) {
786
+ async query(query, scope, options) {
787
787
  try {
788
- let results = await this.database.query(query, scope);
788
+ let results = await this.database.query(query, scope, options);
789
789
  return results;
790
790
  } catch (error) {
791
791
  throw error;
792
792
  }
793
793
  }
794
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
+
795
827
  async getMulti(args) {
796
828
  // ⚡ Profiling: use track() for proper async context forking
797
829
  const p = this[_ctx]?.profiler;
@@ -1569,10 +1601,10 @@ export default class SpiceModel {
1569
1601
  await this.run_hook(this, "list", "before");
1570
1602
  }
1571
1603
 
1572
- 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}`;
1573
1605
  let results;
1574
1606
 
1575
- if (this.shouldUseCache(this.type)) {
1607
+ if (this.shouldUseCache(this.type) && !this.requiresFreshQuery(args)) {
1576
1608
  const cached = await this.getCacheProviderObject(this.type).get(
1577
1609
  cacheKey,
1578
1610
  );
@@ -1671,6 +1703,7 @@ export default class SpiceModel {
1671
1703
  const p = this[_ctx]?.profiler;
1672
1704
 
1673
1705
  const doFetch = async () => {
1706
+ const queryOptions = this.getQueryOptions(args);
1674
1707
  if (args.is_custom_query === "true" && args.ids.length > 0) {
1675
1708
  console.log(
1676
1709
  "Fetching results from database",
@@ -1679,7 +1712,7 @@ export default class SpiceModel {
1679
1712
  query,
1680
1713
  mappedNestings,
1681
1714
  );
1682
- return await this.database.query(query);
1715
+ return await this.database.query(query, args.query_scope, queryOptions);
1683
1716
  } else if (args.is_full_text === "true") {
1684
1717
  return await this.database.full_text_search(
1685
1718
  this.type,
@@ -1701,8 +1734,9 @@ export default class SpiceModel {
1701
1734
  args.offset,
1702
1735
  args.sort,
1703
1736
  args.do_count,
1704
- args.statement_consistent,
1737
+ args.query_scope,
1705
1738
  args._join,
1739
+ queryOptions,
1706
1740
  );
1707
1741
  return result;
1708
1742
  }
@@ -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
+ });