@reventlessdev/reventless-local 3.0.0-alpha.211 → 3.0.0-alpha.213

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.
@@ -77,6 +77,14 @@ type plainState = {
77
77
  name: string,
78
78
  }
79
79
 
80
+ // No `*Id` field at all, so the key ladder has nothing to nominate — the state
81
+ // that still produces an empty capability.
82
+ @schema
83
+ type keylessState = {
84
+ name: string,
85
+ note: string,
86
+ }
87
+
80
88
  @schema
81
89
  type indexedState = {
82
90
  productId: @s.matches(Reventless.DcbTag.string) string,
@@ -582,7 +590,12 @@ describe("GraphQL_SchemaInspector", () => {
582
590
  // ── Phase 1: server capability — auto-derived Filter / OrderBy ─────────
583
591
 
584
592
  describe("server capability — auto-derived Filter / OrderBy", () => {
585
- testPromise("read model with no annotations emits the unchanged search-only Filter", async () => {
593
+ // This used to assert that a state with no annotations got a search-only
594
+ // Filter and no OrderBy at all. It no longer does: `plainState` has exactly
595
+ // one `*Id` field, so the key is knowable without `@id` and the queryable now
596
+ // gets that key's eq filter and sort. The search-only case is the one below,
597
+ // where nothing identifies the row.
598
+ testPromise("read model with no annotations gets its inferred key's Eq + OrderBy", async () => {
586
599
  let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
587
600
  ~mutationEntries=[],
588
601
  ~queryEntries=[
@@ -599,16 +612,44 @@ describe("GraphQL_SchemaInspector", () => {
599
612
  )
600
613
  let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
601
614
  let sdl = inspection.sdlPreview
602
- // Filter has only the legacy block
615
+ // The legacy block is untouched
603
616
  expect(sdl->String.includes("input PlainViewFilter"))->toBe(true)
604
617
  expect(sdl->String.includes("search: String"))->toBe(true)
605
618
  expect(sdl->String.includes("searchPrefix: String"))->toBe(true)
606
619
  expect(sdl->String.includes("ids: [ID!]"))->toBe(true)
607
- // No per-field eq inputs
620
+ // and `productId`, the sole candidate, now narrows and sorts.
621
+ expect(sdl->String.includes("productIdEq: ID"))->toBe(true)
622
+ expect(sdl->String.includes("PlainViewOrderBy"))->toBe(true)
623
+ expect(sdl->String.includes("PlainViewOrderField"))->toBe(true)
624
+ expect(sdl->String.includes("orderBy:"))->toBe(true)
625
+ // Only the key — an inferred key never drags the rest of the record in.
626
+ expect(sdl->String.includes("nameEq:"))->toBe(false)
627
+ })
628
+
629
+ // The remaining empty-capability case: no `*Id` field, so nothing says which
630
+ // field identifies a row and the ladder declines rather than guessing.
631
+ testPromise("read model whose key cannot be resolved stays search-only", async () => {
632
+ let fragment = ReventlessCore.GraphQL_FragmentGenerator.generate(
633
+ ~mutationEntries=[],
634
+ ~queryEntries=[
635
+ {
636
+ singleFieldName: "Keyless_View",
637
+ listFieldName: "Keyless_Views",
638
+ returnTypeName: "KeylessView",
639
+ stateSchema: keylessStateSchema->S.castToUnknown,
640
+ authorization: None,
641
+ includeIdParam: false,
642
+ connectionSpec: true,
643
+ },
644
+ ],
645
+ )
646
+ let inspection = ReventlessCore.GraphQL_SchemaInspector.inspectFragment(fragment)
647
+ let sdl = inspection.sdlPreview
648
+ expect(sdl->String.includes("input KeylessViewFilter"))->toBe(true)
649
+ expect(sdl->String.includes("search: String"))->toBe(true)
608
650
  expect(sdl->String.includes("Eq:"))->toBe(false)
609
- // No OrderBy / OrderField types or arg
610
- expect(sdl->String.includes("PlainViewOrderBy"))->toBe(false)
611
- expect(sdl->String.includes("PlainViewOrderField"))->toBe(false)
651
+ expect(sdl->String.includes("KeylessViewOrderBy"))->toBe(false)
652
+ expect(sdl->String.includes("KeylessViewOrderField"))->toBe(false)
612
653
  expect(sdl->String.includes("orderBy:"))->toBe(false)
613
654
  })
614
655
 
@@ -84,6 +84,11 @@ let plainStateSchema = S.schema(s => ({
84
84
  name: s.m(S.string)
85
85
  }));
86
86
 
87
+ let keylessStateSchema = S.schema(s => ({
88
+ name: s.m(S.string),
89
+ note: s.m(S.string)
90
+ }));
91
+
87
92
  let indexedStateSchema = S.schema(s => ({
88
93
  productId: s.m(DcbTag$Reventless.string),
89
94
  ownerId: s.m(S.string),
@@ -453,7 +458,7 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
453
458
  });
454
459
  });
455
460
  globalThis.describe("server capability — auto-derived Filter / OrderBy", () => {
456
- globalThis.test("read model with no annotations emits the unchanged search-only Filter", async () => {
461
+ globalThis.test("read model with no annotations gets its inferred key's Eq + OrderBy", async () => {
457
462
  let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([], [{
458
463
  singleFieldName: "Plain_View",
459
464
  listFieldName: "Plain_Views",
@@ -469,9 +474,29 @@ globalThis.describe("GraphQL_SchemaInspector", () => {
469
474
  globalThis.expect(sdl.includes("search: String")).toBe(true);
470
475
  globalThis.expect(sdl.includes("searchPrefix: String")).toBe(true);
471
476
  globalThis.expect(sdl.includes("ids: [ID!]")).toBe(true);
477
+ globalThis.expect(sdl.includes("productIdEq: ID")).toBe(true);
478
+ globalThis.expect(sdl.includes("PlainViewOrderBy")).toBe(true);
479
+ globalThis.expect(sdl.includes("PlainViewOrderField")).toBe(true);
480
+ globalThis.expect(sdl.includes("orderBy:")).toBe(true);
481
+ globalThis.expect(sdl.includes("nameEq:")).toBe(false);
482
+ });
483
+ globalThis.test("read model whose key cannot be resolved stays search-only", async () => {
484
+ let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate([], [{
485
+ singleFieldName: "Keyless_View",
486
+ listFieldName: "Keyless_Views",
487
+ returnTypeName: "KeylessView",
488
+ stateSchema: keylessStateSchema,
489
+ authorization: undefined,
490
+ includeIdParam: false,
491
+ connectionSpec: true
492
+ }]);
493
+ let inspection = GraphQL_SchemaInspector$ReventlessCore.inspectFragment(fragment);
494
+ let sdl = inspection.sdlPreview;
495
+ globalThis.expect(sdl.includes("input KeylessViewFilter")).toBe(true);
496
+ globalThis.expect(sdl.includes("search: String")).toBe(true);
472
497
  globalThis.expect(sdl.includes("Eq:")).toBe(false);
473
- globalThis.expect(sdl.includes("PlainViewOrderBy")).toBe(false);
474
- globalThis.expect(sdl.includes("PlainViewOrderField")).toBe(false);
498
+ globalThis.expect(sdl.includes("KeylessViewOrderBy")).toBe(false);
499
+ globalThis.expect(sdl.includes("KeylessViewOrderField")).toBe(false);
475
500
  globalThis.expect(sdl.includes("orderBy:")).toBe(false);
476
501
  });
477
502
  globalThis.test("read model with @id + @index emits Eq filters + OrderBy + orderBy arg", async () => {
@@ -701,6 +726,7 @@ export {
701
726
  dcbSingleCommandSchema,
702
727
  dcbMultiCommandSchema,
703
728
  plainStateSchema,
729
+ keylessStateSchema,
704
730
  indexedStateSchema,
705
731
  indexedStateSchemaWithAnnotations,
706
732
  orderedStateSchema,
@@ -21,23 +21,30 @@ let capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability = {
21
21
  sortFields: ["name", "qty", "status"],
22
22
  }
23
23
 
24
- let mk = (id, status, name, qty) => {
24
+ let mk = (id, status, name, qty, owner) => {
25
25
  let o = Dict.make()
26
26
  o->Dict.set("id", JSON.Encode.string(id))
27
27
  o->Dict.set("status", JSON.Encode.string(status))
28
28
  o->Dict.set("name", JSON.Encode.string(name))
29
29
  o->Dict.set("qty", JSON.Encode.int(qty))
30
+ o->Dict.set("owner", JSON.Encode.string(owner))
30
31
  JSON.Encode.object(o)
31
32
  }
32
33
 
33
34
  // Names differ from id-order; status has duplicates (tiebreak); qty exercises the
34
- // numeric-field-as-string comparison the push-down must match.
35
+ // numeric-field-as-string comparison the push-down must match. `owner` is split
36
+ // so that no owner holds all the rows and none holds none — a scoped read that
37
+ // returned everything and one that returned nothing would both stand out.
38
+ //
39
+ // Note `owner` is deliberately absent from `capability` below: owner scoping has
40
+ // to work on a field the client-visible filter surface does not admit, which is
41
+ // the normal case and the reason the predicate travels separately.
35
42
  let rows = [
36
- ("p-1", "active", "Charlie", 3),
37
- ("p-2", "active", "Alpha", 1),
38
- ("p-3", "inactive", "Echo", 5),
39
- ("p-4", "active", "Bravo", 2),
40
- ("p-5", "inactive", "Delta", 4),
43
+ ("p-1", "active", "Charlie", 3, "u-a"),
44
+ ("p-2", "active", "Alpha", 1, "u-b"),
45
+ ("p-3", "inactive", "Echo", 5, "u-a"),
46
+ ("p-4", "active", "Bravo", 2, "u-c"),
47
+ ("p-5", "inactive", "Delta", 4, "u-a"),
41
48
  ]
42
49
 
43
50
  type setup = {
@@ -45,6 +52,7 @@ type setup = {
45
52
  ~argsDict: dict<JSON.t>,
46
53
  ~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
47
54
  ~labelField: string,
55
+ ~ownerScope: (string, string)=?,
48
56
  ) => option<JSON.t>,
49
57
  fullScan: unit => array<JSON.t>,
50
58
  }
@@ -58,8 +66,8 @@ let build = async (): setup => {
58
66
  let s = Storage.make(~name="items", ~indexes=[], ~api=(), ~apiRole=(), ~owner=None, ~opts)
59
67
  let ops = await s.operations->TestRunner.resolve
60
68
  for i in 0 to rows->Array.length - 1 {
61
- let (id, status, name, qty) = rows->Array.getUnsafe(i)
62
- let _ = await ops.save(id, mk(id, status, name, qty), ReventlessCore.QueryDb.Any, None)
69
+ let (id, status, name, qty, owner) = rows->Array.getUnsafe(i)
70
+ let _ = await ops.save(id, mk(id, status, name, qty, owner), ReventlessCore.QueryDb.Any, None)
63
71
  }
64
72
  {
65
73
  listPage: TestBus.getQueryDbListPage("items")->Option.getOrThrow,
@@ -110,7 +118,7 @@ let orderBy = (f, dir) =>
110
118
  let cur = ReventlessCore.QueryDbListQuery.encodeCursor
111
119
 
112
120
  // Assert the push-down serves this shape AND matches the spec exactly.
113
- let checkPushed = async (~label, args) => {
121
+ let checkPushed = async (~label, ~ownerScope=?, args) => {
114
122
  let s = await build()
115
123
  let argsDict = argsOf(args)
116
124
  let expected =
@@ -120,8 +128,9 @@ let checkPushed = async (~label, args) => {
120
128
  ~capability,
121
129
  ~labelField="name",
122
130
  ~decodeLocalId=_ => None,
131
+ ~ownerScope?,
123
132
  )->norm
124
- switch s.listPage(~argsDict, ~capability, ~labelField="name") {
133
+ switch s.listPage(~argsDict, ~capability, ~labelField="name", ~ownerScope?) {
125
134
  | Some(actual) => expect(actual->norm)->toEqual(expected)
126
135
  | None => expect("push-down for " ++ label)->toBe("returned None")
127
136
  }
@@ -133,6 +142,20 @@ let checkFallback = async args => {
133
142
  expect(s.listPage(~argsDict=argsOf(args), ~capability, ~labelField="name")->Option.isSome)->toBe(false)
134
143
  }
135
144
 
145
+ // The ids a scoped read actually returns, from the push-down.
146
+ let scopedIds = async (~owner, args) => {
147
+ let s = await build()
148
+ switch s.listPage(
149
+ ~argsDict=argsOf(args),
150
+ ~capability,
151
+ ~labelField="name",
152
+ ~ownerScope=("owner", owner),
153
+ ) {
154
+ | Some(conn) => (conn->norm).edges->Array.map(e => e.id)
155
+ | None => []
156
+ }
157
+ }
158
+
136
159
  describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", () => {
137
160
  testPromise("bare page", () => checkPushed(~label="bare", []))
138
161
  testPromise("first:2", () => checkPushed(~label="first", [("first", JSON.Encode.int(2))]))
@@ -194,4 +217,62 @@ describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", ()
194
217
  testPromise("backward (last/before) → fallback", () =>
195
218
  checkFallback([("last", JSON.Encode.int(2)), ("before", JSON.Encode.string(cur("p-4")))])
196
219
  )
220
+
221
+ // ── Owner scoping ─────────────────────────────────────────────────────────
222
+ // Parity matters more here than anywhere else in this file. The push-down is
223
+ // the path a deployment actually takes; the spec is the path the tests most
224
+ // easily reach. A predicate implemented in only one of them is a hole that
225
+ // every fallback-based test would still call green.
226
+ describe("owner scoping", () => {
227
+ testPromise("bare page, scoped", () =>
228
+ checkPushed(~label="owner-bare", ~ownerScope=("owner", "u-a"), [])
229
+ )
230
+ testPromise("scoped + orderBy", () =>
231
+ checkPushed(
232
+ ~label="owner-order",
233
+ ~ownerScope=("owner", "u-a"),
234
+ [("orderBy", orderBy("name", "DESC"))],
235
+ )
236
+ )
237
+ // The client's own filter must survive alongside the scope, not be replaced
238
+ // by it — a scoped read is still a filtered read.
239
+ testPromise("scoped + client filter compose", () =>
240
+ checkPushed(
241
+ ~label="owner-and-filter",
242
+ ~ownerScope=("owner", "u-a"),
243
+ [("filter", filterOf([("statusEq", JSON.Encode.string("inactive"))]))],
244
+ )
245
+ )
246
+
247
+ testPromise("a scoped read returns exactly that owner's rows", async () =>
248
+ expect(await scopedIds(~owner="u-a", []))->toEqual(["p-1", "p-3", "p-5"])
249
+ )
250
+
251
+ // The control: a different owner sees a different, also non-empty, set. An
252
+ // implementation that scoped to nothing would pass an "is not everything"
253
+ // assertion and fail this one.
254
+ testPromise("a second owner sees their own rows, not the first's", async () =>
255
+ expect(await scopedIds(~owner="u-b", []))->toEqual(["p-2"])
256
+ )
257
+
258
+ // The case that catches a predicate applied AFTER the page rather than
259
+ // inside the SQL: u-a owns 3 of 5 rows, so a LIMIT 2 taken before scoping
260
+ // would return p-1 alone (p-2 belongs to u-b and would be dropped), not the
261
+ // two rows actually asked for.
262
+ testPromise("paging a scoped read fills each page from owned rows only", async () =>
263
+ expect(await scopedIds(~owner="u-a", [("first", JSON.Encode.int(2))]))->toEqual([
264
+ "p-1",
265
+ "p-3",
266
+ ])
267
+ )
268
+
269
+ testPromise("the second page continues the scoped sequence", async () =>
270
+ expect(
271
+ await scopedIds(
272
+ ~owner="u-a",
273
+ [("first", JSON.Encode.int(2)), ("after", JSON.Encode.string(cur("p-3")))],
274
+ ),
275
+ )->toEqual(["p-5"])
276
+ )
277
+ })
197
278
  })
@@ -36,12 +36,13 @@ let capability = {
36
36
  sortFields: capability_sortFields
37
37
  };
38
38
 
39
- function mk(id, status, name, qty) {
39
+ function mk(id, status, name, qty, owner) {
40
40
  let o = {};
41
41
  o["id"] = id;
42
42
  o["status"] = status;
43
43
  o["name"] = name;
44
44
  o["qty"] = qty;
45
+ o["owner"] = owner;
45
46
  return o;
46
47
  }
47
48
 
@@ -50,31 +51,36 @@ let rows = [
50
51
  "p-1",
51
52
  "active",
52
53
  "Charlie",
53
- 3
54
+ 3,
55
+ "u-a"
54
56
  ],
55
57
  [
56
58
  "p-2",
57
59
  "active",
58
60
  "Alpha",
59
- 1
61
+ 1,
62
+ "u-b"
60
63
  ],
61
64
  [
62
65
  "p-3",
63
66
  "inactive",
64
67
  "Echo",
65
- 5
68
+ 5,
69
+ "u-a"
66
70
  ],
67
71
  [
68
72
  "p-4",
69
73
  "active",
70
74
  "Bravo",
71
- 2
75
+ 2,
76
+ "u-c"
72
77
  ],
73
78
  [
74
79
  "p-5",
75
80
  "inactive",
76
81
  "Delta",
77
- 4
82
+ 4,
83
+ "u-a"
78
84
  ]
79
85
  ];
80
86
 
@@ -90,7 +96,7 @@ async function build() {
90
96
  for (let i = 0, i_finish = rows.length; i < i_finish; ++i) {
91
97
  let match = rows[i];
92
98
  let id = match[0];
93
- await ops.save(id, mk(id, match[1], match[2], match[3]), "Any", undefined);
99
+ await ops.save(id, mk(id, match[1], match[2], match[3], match[4]), "Any", undefined);
94
100
  }
95
101
  return {
96
102
  listPage: Stdlib_Option.getOrThrow(TestBus.getQueryDbListPage("items"), undefined),
@@ -142,11 +148,11 @@ function orderBy(f, dir) {
142
148
  ]);
143
149
  }
144
150
 
145
- async function checkPushed(label, args) {
151
+ async function checkPushed(label, ownerScope, args) {
146
152
  let s = await build();
147
153
  let argsDict = Object.fromEntries(args);
148
- let expected = norm(QueryDbListQuery$ReventlessCore.run(s.fullScan(), argsDict, capability, "name", param => {}));
149
- let actual = s.listPage(argsDict, capability, "name");
154
+ let expected = norm(QueryDbListQuery$ReventlessCore.run(s.fullScan(), argsDict, capability, "name", param => {}, ownerScope));
155
+ let actual = s.listPage(argsDict, capability, "name", ownerScope);
150
156
  if (actual !== undefined) {
151
157
  globalThis.expect(norm(actual)).toEqual(expected);
152
158
  } else {
@@ -156,16 +162,29 @@ async function checkPushed(label, args) {
156
162
 
157
163
  async function checkFallback(args) {
158
164
  let s = await build();
159
- globalThis.expect(Stdlib_Option.isSome(s.listPage(Object.fromEntries(args), capability, "name"))).toBe(false);
165
+ globalThis.expect(Stdlib_Option.isSome(s.listPage(Object.fromEntries(args), capability, "name", undefined))).toBe(false);
166
+ }
167
+
168
+ async function scopedIds(owner, args) {
169
+ let s = await build();
170
+ let conn = s.listPage(Object.fromEntries(args), capability, "name", [
171
+ "owner",
172
+ owner
173
+ ]);
174
+ if (conn !== undefined) {
175
+ return norm(conn).edges.map(e => e.id);
176
+ } else {
177
+ return [];
178
+ }
160
179
  }
161
180
 
162
181
  globalThis.describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery spec)", () => {
163
- globalThis.test("bare page", () => checkPushed("bare", []));
164
- globalThis.test("first:2", () => checkPushed("first", [[
182
+ globalThis.test("bare page", () => checkPushed("bare", undefined, []));
183
+ globalThis.test("first:2", () => checkPushed("first", undefined, [[
165
184
  "first",
166
185
  2
167
186
  ]]));
168
- globalThis.test("first:2 + after", () => checkPushed("after", [
187
+ globalThis.test("first:2 + after", () => checkPushed("after", undefined, [
169
188
  [
170
189
  "first",
171
190
  2
@@ -175,14 +194,14 @@ globalThis.describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery
175
194
  QueryDbListQuery$ReventlessCore.encodeCursor("p-2")
176
195
  ]
177
196
  ]));
178
- globalThis.test("statusEq active", () => checkPushed("eq", [[
197
+ globalThis.test("statusEq active", () => checkPushed("eq", undefined, [[
179
198
  "filter",
180
199
  Object.fromEntries([[
181
200
  "statusEq",
182
201
  "active"
183
202
  ]])
184
203
  ]]));
185
- globalThis.test("statusEq active + first:2", () => checkPushed("eq+first", [
204
+ globalThis.test("statusEq active + first:2", () => checkPushed("eq+first", undefined, [
186
205
  [
187
206
  "filter",
188
207
  Object.fromEntries([[
@@ -195,15 +214,15 @@ globalThis.describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery
195
214
  2
196
215
  ]
197
216
  ]));
198
- globalThis.test("orderBy name ASC", () => checkPushed("order-asc", [[
217
+ globalThis.test("orderBy name ASC", () => checkPushed("order-asc", undefined, [[
199
218
  "orderBy",
200
219
  orderBy("name", "ASC")
201
220
  ]]));
202
- globalThis.test("orderBy name DESC", () => checkPushed("order-desc", [[
221
+ globalThis.test("orderBy name DESC", () => checkPushed("order-desc", undefined, [[
203
222
  "orderBy",
204
223
  orderBy("name", "DESC")
205
224
  ]]));
206
- globalThis.test("orderBy name DESC + first:2 + after", () => checkPushed("order-desc-after", [
225
+ globalThis.test("orderBy name DESC + first:2 + after", () => checkPushed("order-desc-after", undefined, [
207
226
  [
208
227
  "orderBy",
209
228
  orderBy("name", "DESC")
@@ -217,11 +236,11 @@ globalThis.describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery
217
236
  QueryDbListQuery$ReventlessCore.encodeCursor("Delta")
218
237
  ]
219
238
  ]));
220
- globalThis.test("orderBy status ASC (tiebreak by id)", () => checkPushed("order-tiebreak", [[
239
+ globalThis.test("orderBy status ASC (tiebreak by id)", () => checkPushed("order-tiebreak", undefined, [[
221
240
  "orderBy",
222
241
  orderBy("status", "ASC")
223
242
  ]]));
224
- globalThis.test("qty range From/To (numeric-as-string)", () => checkPushed("range", [[
243
+ globalThis.test("qty range From/To (numeric-as-string)", () => checkPushed("range", undefined, [[
225
244
  "filter",
226
245
  Object.fromEntries([
227
246
  [
@@ -234,7 +253,7 @@ globalThis.describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery
234
253
  ]
235
254
  ])
236
255
  ]]));
237
- globalThis.test("orderBy qty ASC (numeric-as-string sort)", () => checkPushed("order-qty", [[
256
+ globalThis.test("orderBy qty ASC (numeric-as-string sort)", () => checkPushed("order-qty", undefined, [[
238
257
  "orderBy",
239
258
  orderBy("qty", "ASC")
240
259
  ]]));
@@ -269,6 +288,60 @@ globalThis.describe("QueryDb list push-down parity (SQLite ≡ QueryDbListQuery
269
288
  QueryDbListQuery$ReventlessCore.encodeCursor("p-4")
270
289
  ]
271
290
  ]));
291
+ globalThis.describe("owner scoping", () => {
292
+ globalThis.test("bare page, scoped", () => checkPushed("owner-bare", [
293
+ "owner",
294
+ "u-a"
295
+ ], []));
296
+ globalThis.test("scoped + orderBy", () => checkPushed("owner-order", [
297
+ "owner",
298
+ "u-a"
299
+ ], [[
300
+ "orderBy",
301
+ orderBy("name", "DESC")
302
+ ]]));
303
+ globalThis.test("scoped + client filter compose", () => checkPushed("owner-and-filter", [
304
+ "owner",
305
+ "u-a"
306
+ ], [[
307
+ "filter",
308
+ Object.fromEntries([[
309
+ "statusEq",
310
+ "inactive"
311
+ ]])
312
+ ]]));
313
+ globalThis.test("a scoped read returns exactly that owner's rows", async () => {
314
+ globalThis.expect(await scopedIds("u-a", [])).toEqual([
315
+ "p-1",
316
+ "p-3",
317
+ "p-5"
318
+ ]);
319
+ });
320
+ globalThis.test("a second owner sees their own rows, not the first's", async () => {
321
+ globalThis.expect(await scopedIds("u-b", [])).toEqual(["p-2"]);
322
+ });
323
+ globalThis.test("paging a scoped read fills each page from owned rows only", async () => {
324
+ globalThis.expect(await scopedIds("u-a", [[
325
+ "first",
326
+ 2
327
+ ]])).toEqual([
328
+ "p-1",
329
+ "p-3"
330
+ ]);
331
+ });
332
+ globalThis.test("the second page continues the scoped sequence", async () => {
333
+ globalThis.expect(await scopedIds("u-a", [
334
+ [
335
+ "first",
336
+ 2
337
+ ],
338
+ [
339
+ "after",
340
+ QueryDbListQuery$ReventlessCore.encodeCursor("p-3")
341
+ ]
342
+ ])).toEqual(["p-5"]);
343
+ });
344
+ });
272
345
  });
273
346
 
274
347
  let cur = QueryDbListQuery$ReventlessCore.encodeCursor;
@@ -288,5 +361,6 @@ export {
288
361
  cur,
289
362
  checkPushed,
290
363
  checkFallback,
364
+ scopedIds,
291
365
  }
292
366
  /* Not a pure module */