@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.
- package/CHANGELOG.md +18 -0
- package/package.json +11 -11
- package/src/BakedManifest.res +72 -0
- package/src/BakedManifest.res.mjs +52 -0
- package/src/Platform.res +66 -20
- package/src/Platform.res.mjs +55 -2
- package/src/adapter/DomainGraphQL_Server.res +6 -18
- package/src/adapter/DomainGraphQL_Server.res.mjs +5 -22
- package/src/adapter/LocalBus.res +3 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +167 -50
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +85 -30
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +11 -0
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +5 -1
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +47 -6
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +29 -3
- package/tests/adapter/QueryDbListPushdownParityTest.res +92 -11
- package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +96 -22
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
610
|
-
expect(sdl->String.includes("
|
|
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
|
|
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("
|
|
474
|
-
globalThis.expect(sdl.includes("
|
|
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 */
|