@reventlessdev/reventless-core 3.0.0-alpha.248 → 3.0.0-alpha.250
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 +14 -0
- package/package.json +6 -6
- package/src/components/Api/GraphQL_FragmentGenerator.res +6 -1
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +2 -1
- package/src/components/Api/QueryDbListQuery.res +26 -6
- package/src/components/Api/QueryDbListQuery.res.mjs +12 -5
- package/tests/api/GraphQL_FragmentGeneratorTest.res +36 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.250 (2026-08-23)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **querydb:** serve the previous page instead of refusing it ([48341c9](https://github.com/ReventlessDev/reventless-core/commit/48341c9f61b5155b441deeb0edc9abf461948346))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.249 (2026-08-23)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **querydb:** serve owner-scoped lists from an index instead of a filtered Scan ([ad26c28](https://github.com/ReventlessDev/reventless-core/commit/ad26c284b4b0650fa3f3b6109ed33bbfe8608f6d))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.248 (2026-08-23)
|
|
7
21
|
|
|
8
22
|
* feat(aws)!: scope the active-role store to the identity provider ([08e287d](https://github.com/ReventlessDev/reventless-core/commit/08e287db9721977b818bad76671ec4c62be6f9af))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.250",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,21 +29,21 @@
|
|
|
29
29
|
"sury": "11.0.0-rc.2",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
|
-
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
33
32
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.8",
|
|
34
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
34
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
35
35
|
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
37
|
-
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
38
37
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.8",
|
|
39
|
-
"@reventlessdev/
|
|
38
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
40
39
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.33",
|
|
41
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.123",
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.151"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
45
45
|
"sury-ppx": "11.0.0-rc.2",
|
|
46
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
46
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.73"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"rescript": "12.3.0"
|
|
@@ -903,7 +903,12 @@ let generate = (
|
|
|
903
903
|
switch entry.indexQueries {
|
|
904
904
|
| Some(indexes) =>
|
|
905
905
|
let connectionTypeName = entry.returnTypeName ++ "Connection"
|
|
906
|
-
indexes
|
|
906
|
+
// Derived indexes are skipped: they key a read the list resolver already
|
|
907
|
+
// makes on the caller's behalf, and a `<single>By<OwnerField>` door any
|
|
908
|
+
// caller may name is not the read they serve.
|
|
909
|
+
indexes
|
|
910
|
+
->Array.filter(ic => !Reventless.ReadModel.isDerivedIndex(ic))
|
|
911
|
+
->Array.forEach(indexConfig =>
|
|
907
912
|
queries->Array.push(
|
|
908
913
|
deriveIndexQueryField(
|
|
909
914
|
~singleFieldName=entry.singleFieldName,
|
|
@@ -5,6 +5,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
5
5
|
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
6
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
7
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
8
9
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
9
10
|
import * as Api_Naming$ReventlessCore from "./Api_Naming.res.mjs";
|
|
10
11
|
import * as SchemaType$ReventlessCore from "./SchemaType.res.mjs";
|
|
@@ -580,7 +581,7 @@ function generate(mutationEntries, queryEntries) {
|
|
|
580
581
|
let indexes = entry.indexQueries;
|
|
581
582
|
if (indexes !== undefined) {
|
|
582
583
|
let connectionTypeName = entry.returnTypeName + "Connection";
|
|
583
|
-
indexes.forEach(indexConfig => {
|
|
584
|
+
indexes.filter(ic => !ReadModel$Reventless.isDerivedIndex(ic)).forEach(indexConfig => {
|
|
584
585
|
queries.push(deriveIndexQueryField(entry.singleFieldName, indexConfig, connectionTypeName));
|
|
585
586
|
});
|
|
586
587
|
}
|
|
@@ -265,10 +265,22 @@ let run = (
|
|
|
265
265
|
|
|
266
266
|
let first =
|
|
267
267
|
argsDict->Dict.get("first")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
|
|
268
|
-
|
|
268
|
+
// An empty cursor is no cursor. The host shell clears the opposite argument by
|
|
269
|
+
// setting it to "" rather than dropping it, and `Some("")` read as a cursor
|
|
270
|
+
// bounds nothing while still selecting the branch that expects one.
|
|
271
|
+
let cursorArg = key =>
|
|
272
|
+
argsDict
|
|
273
|
+
->Dict.get(key)
|
|
274
|
+
->Option.flatMap(JSON.Decode.string)
|
|
275
|
+
->Option.flatMap(s => s == "" ? None : Some(s))
|
|
276
|
+
let after = cursorArg("after")
|
|
269
277
|
let last = argsDict->Dict.get("last")->Option.flatMap(JSON.Decode.float)->Option.map(Float.toInt)
|
|
270
|
-
let before =
|
|
271
|
-
|
|
278
|
+
let before = cursorArg("before")
|
|
279
|
+
// `before` decides the direction, not `last`. Relay pairs `last` with `before`,
|
|
280
|
+
// but a client that keeps its page size in `first` and only moves the cursor
|
|
281
|
+
// sends `before` alone — and keying on `last` made that a FORWARD read bounded
|
|
282
|
+
// by nothing, which answers page one and calls it the previous page.
|
|
283
|
+
let isBackward = last->Option.isSome || before->Option.isSome
|
|
272
284
|
let cursorField = orderByField->Option.getOr("id")
|
|
273
285
|
let getCursorValue = item => getFieldString(item, cursorField)->Option.getOr(getId(item))
|
|
274
286
|
|
|
@@ -288,8 +300,12 @@ let run = (
|
|
|
288
300
|
| _ => sorted
|
|
289
301
|
}
|
|
290
302
|
|
|
303
|
+
// `last` when given, else the page size the caller has been paging with.
|
|
291
304
|
let pageSize = if isBackward {
|
|
292
|
-
last
|
|
305
|
+
switch last {
|
|
306
|
+
| Some(n) => n
|
|
307
|
+
| None => first->Option.getOr(defaultListPageSize)
|
|
308
|
+
}
|
|
293
309
|
} else {
|
|
294
310
|
first->Option.getOr(defaultListPageSize)
|
|
295
311
|
}
|
|
@@ -311,10 +327,14 @@ let run = (
|
|
|
311
327
|
(arr->Array.slice(~start=0, ~end=pageSize), hasMore)
|
|
312
328
|
}
|
|
313
329
|
|
|
330
|
+
// Both directions report both flags. A backward page was cut from ahead, so a
|
|
331
|
+
// next page provably exists — the one the caller came from — and a forward page
|
|
332
|
+
// reached by a cursor has one behind it. Reporting only the flag that matches
|
|
333
|
+
// the direction made Prev vanish the moment it was used.
|
|
314
334
|
buildConnection(
|
|
315
335
|
~pageItems,
|
|
316
|
-
~hasNextPage
|
|
317
|
-
~hasPreviousPage=isBackward
|
|
336
|
+
~hasNextPage=isBackward ? true : hasMore,
|
|
337
|
+
~hasPreviousPage=isBackward ? hasMore : after->Option.isSome,
|
|
318
338
|
~cursorValueOf=getCursorValue,
|
|
319
339
|
)
|
|
320
340
|
}
|
|
@@ -154,10 +154,17 @@ function run(items, argsDict, capability, labelField, decodeLocalIdOpt, ownerSco
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
let first = Stdlib_Option.map(Stdlib_Option.flatMap(argsDict["first"], Stdlib_JSON.Decode.float), prim => prim | 0);
|
|
157
|
-
let
|
|
157
|
+
let cursorArg = key => Stdlib_Option.flatMap(Stdlib_Option.flatMap(argsDict[key], Stdlib_JSON.Decode.string), s => {
|
|
158
|
+
if (s === "") {
|
|
159
|
+
return;
|
|
160
|
+
} else {
|
|
161
|
+
return s;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
let after = cursorArg("after");
|
|
158
165
|
let last = Stdlib_Option.map(Stdlib_Option.flatMap(argsDict["last"], Stdlib_JSON.Decode.float), prim => prim | 0);
|
|
159
|
-
let before =
|
|
160
|
-
let isBackward = Stdlib_Option.isSome(last);
|
|
166
|
+
let before = cursorArg("before");
|
|
167
|
+
let isBackward = Stdlib_Option.isSome(last) || Stdlib_Option.isSome(before);
|
|
161
168
|
let cursorField = Stdlib_Option.getOr(orderByField, "id");
|
|
162
169
|
let getCursorValue = item => Stdlib_Option.getOr(getFieldString(item, cursorField), getId(item));
|
|
163
170
|
let cursorBounded;
|
|
@@ -188,7 +195,7 @@ function run(items, argsDict, capability, labelField, decodeLocalIdOpt, ownerSco
|
|
|
188
195
|
} else {
|
|
189
196
|
cursorBounded = sorted;
|
|
190
197
|
}
|
|
191
|
-
let pageSize = isBackward ?
|
|
198
|
+
let pageSize = isBackward && last !== undefined ? last : Stdlib_Option.getOr(first, 50);
|
|
192
199
|
let take = pageSize + 1 | 0;
|
|
193
200
|
let match;
|
|
194
201
|
if (isBackward) {
|
|
@@ -210,7 +217,7 @@ function run(items, argsDict, capability, labelField, decodeLocalIdOpt, ownerSco
|
|
|
210
217
|
];
|
|
211
218
|
}
|
|
212
219
|
let hasMore$2 = match[1];
|
|
213
|
-
return buildConnection(match[0],
|
|
220
|
+
return buildConnection(match[0], isBackward ? true : hasMore$2, isBackward ? hasMore$2 : Stdlib_Option.isSome(after), getCursorValue);
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
let defaultListPageSize = 50;
|
|
@@ -509,6 +509,42 @@ describe("the by-index door", () => {
|
|
|
509
509
|
GraphQL_FragmentGenerator.indexQueryFieldName(~singleFieldName="X", ~index="by"),
|
|
510
510
|
))->toEqual(("XByOwner", "XByOwnerId", "XByBy"))
|
|
511
511
|
)
|
|
512
|
+
|
|
513
|
+
// The regression that would otherwise land in silence: `@owner` provisions an
|
|
514
|
+
// index on every view that declares one, and if that index emitted a door, a
|
|
515
|
+
// `<single>By<OwnerField>` field any caller may name would appear in every
|
|
516
|
+
// published SDL — a schema change nobody asked for, on views that only wanted
|
|
517
|
+
// their own list read to stop scanning.
|
|
518
|
+
testSync("a derived index adds no query field", () => {
|
|
519
|
+
let entryWith = (indexes): ReventlessInfra.Api.querySchemaEntry => {
|
|
520
|
+
singleFieldName: "Ordering_Order",
|
|
521
|
+
listFieldName: "Ordering_Orders",
|
|
522
|
+
returnTypeName: "Ordering_Order",
|
|
523
|
+
stateSchema: S.unknown,
|
|
524
|
+
authorization: None,
|
|
525
|
+
indexQueries: indexes,
|
|
526
|
+
}
|
|
527
|
+
let derived: Reventless.ReadModel.indexConfig = {
|
|
528
|
+
index: "_owner",
|
|
529
|
+
type_: "S",
|
|
530
|
+
idField: "customerId",
|
|
531
|
+
subIdField: "id",
|
|
532
|
+
projectionType: ALL,
|
|
533
|
+
derived: true,
|
|
534
|
+
}
|
|
535
|
+
let authored: Reventless.ReadModel.indexConfig = {
|
|
536
|
+
index: "byCategory",
|
|
537
|
+
type_: "S",
|
|
538
|
+
idField: "categoryId",
|
|
539
|
+
projectionType: ALL,
|
|
540
|
+
}
|
|
541
|
+
let doorsOf = indexes =>
|
|
542
|
+
GraphQL_Stitcher.decode(
|
|
543
|
+
GraphQL_FragmentGenerator.generate(~mutationEntries=[], ~queryEntries=[entryWith(indexes)]),
|
|
544
|
+
).queries->Array.filter(q => q->String.includes("Ordering_OrderBy"))
|
|
545
|
+
expect((doorsOf([derived])->Array.length, doorsOf([authored, derived])->Array.length))
|
|
546
|
+
->toEqual((0, 1))
|
|
547
|
+
})
|
|
512
548
|
})
|
|
513
549
|
|
|
514
550
|
// ── @internal: a field that is not on the GraphQL surface ────────────────────
|
|
@@ -427,6 +427,40 @@ globalThis.describe("the by-index door", () => {
|
|
|
427
427
|
"XByBy"
|
|
428
428
|
]);
|
|
429
429
|
});
|
|
430
|
+
globalThis.test("a derived index adds no query field", () => {
|
|
431
|
+
let entryWith = indexes => ({
|
|
432
|
+
singleFieldName: "Ordering_Order",
|
|
433
|
+
listFieldName: "Ordering_Orders",
|
|
434
|
+
returnTypeName: "Ordering_Order",
|
|
435
|
+
stateSchema: Sury.unknown,
|
|
436
|
+
authorization: undefined,
|
|
437
|
+
indexQueries: indexes
|
|
438
|
+
});
|
|
439
|
+
let derived = {
|
|
440
|
+
index: "_owner",
|
|
441
|
+
type_: "S",
|
|
442
|
+
idField: "customerId",
|
|
443
|
+
subIdField: "id",
|
|
444
|
+
projectionType: "ALL",
|
|
445
|
+
derived: true
|
|
446
|
+
};
|
|
447
|
+
let doorsOf = indexes => GraphQL_Stitcher$ReventlessCore.decode(GraphQL_FragmentGenerator$ReventlessCore.generate([], [entryWith(indexes)])).queries.filter(q => q.includes("Ordering_OrderBy"));
|
|
448
|
+
globalThis.expect([
|
|
449
|
+
doorsOf([derived]).length,
|
|
450
|
+
doorsOf([
|
|
451
|
+
{
|
|
452
|
+
index: "byCategory",
|
|
453
|
+
type_: "S",
|
|
454
|
+
idField: "categoryId",
|
|
455
|
+
projectionType: "ALL"
|
|
456
|
+
},
|
|
457
|
+
derived
|
|
458
|
+
]).length
|
|
459
|
+
]).toEqual([
|
|
460
|
+
0,
|
|
461
|
+
1
|
|
462
|
+
]);
|
|
463
|
+
});
|
|
430
464
|
});
|
|
431
465
|
|
|
432
466
|
globalThis.describe("@internal keeps a field off the generated type", () => {
|