@reventlessdev/reventless-local 3.0.0-alpha.236 → 3.0.0-alpha.238
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 +11 -11
- package/src/Platform.res +3 -1
- package/src/Platform.res.mjs +3 -2
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +7 -2
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +4 -2
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +13 -2
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +10 -4
- package/tests/adapter/QueryDbListResolverTest.res +4 -1
- package/tests/adapter/QueryDbListResolverTest.res.mjs +1 -1
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.238 (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.237 (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.236 (2026-08-23)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/reventless-local
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-local",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.238",
|
|
4
4
|
"description": "Local platform for Reventless (in-memory or SQLite backend, for development and testing without AWS)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -35,23 +35,23 @@
|
|
|
35
35
|
"sury": "11.0.0-rc.2",
|
|
36
36
|
"ws": "^8.18.0",
|
|
37
37
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
38
|
-
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
39
38
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
40
|
-
"@reventlessdev/rescript-
|
|
39
|
+
"@reventlessdev/rescript-graphql-yoga": "1.0.0-alpha.27",
|
|
41
40
|
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
41
|
+
"@reventlessdev/rescript-mcp-sdk": "1.0.0-alpha.21",
|
|
42
42
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
43
|
-
"@reventlessdev/reventless-
|
|
44
|
-
"@reventlessdev/reventless-
|
|
45
|
-
"@reventlessdev/reventless-
|
|
46
|
-
"@reventlessdev/reventless-
|
|
47
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
48
|
-
"@reventlessdev/reventless-
|
|
49
|
-
"@reventlessdev/reventless-
|
|
43
|
+
"@reventlessdev/reventless-gwt": "1.0.0-alpha.197",
|
|
44
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.151",
|
|
45
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.114",
|
|
46
|
+
"@reventlessdev/reventless-seed": "1.0.0-alpha.17",
|
|
47
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.250",
|
|
48
|
+
"@reventlessdev/reventless-graphql-server": "1.0.0-alpha.98",
|
|
49
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.123"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"rescript": "12.3.0",
|
|
53
53
|
"sury-ppx": "11.0.0-rc.2",
|
|
54
|
-
"@reventlessdev/reventless-ppx": "1.0.0-alpha.
|
|
54
|
+
"@reventlessdev/reventless-ppx": "1.0.0-alpha.73"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"rescript": "12.3.0"
|
package/src/Platform.res
CHANGED
|
@@ -1427,7 +1427,9 @@ module MakeWithConfig = (
|
|
|
1427
1427
|
// indexed scan is fleshed out together with that first concrete index.
|
|
1428
1428
|
switch entry.indexQueries {
|
|
1429
1429
|
| Some(indexes) =>
|
|
1430
|
-
indexes
|
|
1430
|
+
indexes
|
|
1431
|
+
->Array.filter(ic => !Reventless.ReadModel.isDerivedIndex(ic))
|
|
1432
|
+
->Array.forEach((ic: Reventless.ReadModel.indexConfig) => {
|
|
1431
1433
|
let fieldName = ReventlessCore.GraphQL_FragmentGenerator.indexQueryFieldName(
|
|
1432
1434
|
~singleFieldName=entry.singleFieldName,
|
|
1433
1435
|
~index=ic.index,
|
package/src/Platform.res.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import * as Effect from "effect/Effect";
|
|
|
13
13
|
import * as Pulumi from "@pulumi/pulumi";
|
|
14
14
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
15
15
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
16
|
+
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
16
17
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
17
18
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
18
19
|
import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
|
|
@@ -1206,7 +1207,7 @@ function MakeWithConfig(Config) {
|
|
|
1206
1207
|
}
|
|
1207
1208
|
let indexes = entry.indexQueries;
|
|
1208
1209
|
if (indexes !== undefined) {
|
|
1209
|
-
indexes.forEach(ic => {
|
|
1210
|
+
indexes.filter(ic => !ReadModel$Reventless.isDerivedIndex(ic)).forEach(ic => {
|
|
1210
1211
|
let fieldName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(entry.singleFieldName, ic.index);
|
|
1211
1212
|
queryResolvers[fieldName] = async (_root, _args, _ctx) => connectionResponse([]);
|
|
1212
1213
|
});
|
|
@@ -2932,7 +2933,7 @@ function Make($star) {
|
|
|
2932
2933
|
}
|
|
2933
2934
|
let indexes = entry.indexQueries;
|
|
2934
2935
|
if (indexes !== undefined) {
|
|
2935
|
-
indexes.forEach(ic => {
|
|
2936
|
+
indexes.filter(ic => !ReadModel$Reventless.isDerivedIndex(ic)).forEach(ic => {
|
|
2936
2937
|
let fieldName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(entry.singleFieldName, ic.index);
|
|
2937
2938
|
queryResolvers[fieldName] = async (_root, _args, _ctx) => connectionResponse([]);
|
|
2938
2939
|
});
|
|
@@ -792,7 +792,12 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
792
792
|
// which is also where the AppSync SDL gets them. They used to be spelled out
|
|
793
793
|
// here independently and disagreed with that emitter on all three, so the
|
|
794
794
|
// same door had a different signature per backend and worked on neither.
|
|
795
|
-
|
|
795
|
+
//
|
|
796
|
+
// Derived indexes carry no door, on either backend — the same reasoning, and
|
|
797
|
+
// the same filter, as `GraphQL_FragmentGenerator`: a field emitted here and
|
|
798
|
+
// not there would make the local SDL disagree with the deployed one.
|
|
799
|
+
let doorIndexes = indexes->Array.filter(ic => !Reventless.ReadModel.isDerivedIndex(ic))
|
|
800
|
+
let indexSdlFields = doorIndexes->Array.map((ic: Reventless.ReadModel.indexConfig) =>
|
|
796
801
|
GraphQL_FragmentGenerator.deriveIndexQueryField(
|
|
797
802
|
~singleFieldName=singleQueryName,
|
|
798
803
|
~indexConfig=ic,
|
|
@@ -858,7 +863,7 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
858
863
|
),
|
|
859
864
|
])->JSON.Encode.object
|
|
860
865
|
}
|
|
861
|
-
let indexResolvers: array<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> =
|
|
866
|
+
let indexResolvers: array<(string, ReventlessGraphqlServer.GraphQL_ServerInstance.resolverFn)> = doorIndexes->Array.map(
|
|
862
867
|
(ic: Reventless.ReadModel.indexConfig) => {
|
|
863
868
|
let resolverName = GraphQL_FragmentGenerator.indexQueryFieldName(
|
|
864
869
|
~singleFieldName=singleQueryName,
|
|
@@ -10,6 +10,7 @@ import * as Stdlib_Nullable from "@rescript/runtime/lib/es6/Stdlib_Nullable.js";
|
|
|
10
10
|
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
11
11
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
12
12
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
13
|
+
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
13
14
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
14
15
|
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
15
16
|
import * as Authorization$Reventless from "@reventlessdev/reventless-spec/src/types/Authorization.res.mjs";
|
|
@@ -525,7 +526,8 @@ function Make(Bus) {
|
|
|
525
526
|
} else {
|
|
526
527
|
itemsResolvers = [];
|
|
527
528
|
}
|
|
528
|
-
let
|
|
529
|
+
let doorIndexes = indexes.filter(ic => !ReadModel$Reventless.isDerivedIndex(ic));
|
|
530
|
+
let indexSdlFields = doorIndexes.map(ic => GraphQL_FragmentGenerator$ReventlessCore.deriveIndexQueryField(singleQueryName, ic, returnTypeName + "Connection"));
|
|
529
531
|
let rejectBackwardPaging = args => {
|
|
530
532
|
let given = key => {
|
|
531
533
|
let match = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(args), d => d[key]);
|
|
@@ -585,7 +587,7 @@ function Make(Bus) {
|
|
|
585
587
|
]
|
|
586
588
|
]);
|
|
587
589
|
};
|
|
588
|
-
let indexResolvers =
|
|
590
|
+
let indexResolvers = doorIndexes.map(ic => {
|
|
589
591
|
let resolverName = GraphQL_FragmentGenerator$ReventlessCore.indexQueryFieldName(singleQueryName, ic.index);
|
|
590
592
|
let filterField = GraphQL_FragmentGenerator$ReventlessCore.indexKeyField(ic);
|
|
591
593
|
let resolver = async (_root, args, ctx) => {
|
|
@@ -527,7 +527,15 @@ let makeStorage = (
|
|
|
527
527
|
| Some(f) => jsonText(f)
|
|
528
528
|
| None => idExpr
|
|
529
529
|
}
|
|
530
|
-
|
|
530
|
+
// Empty is absent, as it is in the spec: the host shell clears the opposite
|
|
531
|
+
// cursor by setting it to "" rather than dropping it, and an empty cursor
|
|
532
|
+
// pushed into SQL bounds the read on a decoded empty string.
|
|
533
|
+
let afterCursor =
|
|
534
|
+
argsDict
|
|
535
|
+
->Dict.get("after")
|
|
536
|
+
->Option.flatMap(JSON.Decode.string)
|
|
537
|
+
->Option.flatMap(c => c == "" ? None : Some(c))
|
|
538
|
+
switch afterCursor {
|
|
531
539
|
| Some(c) =>
|
|
532
540
|
whereParts->Array.push(`${cursorExpr} ${isDesc ? "<" : ">"} ?`)
|
|
533
541
|
params->Array.push(JSON.Encode.string(QueryDbListQuery.decodeCursor(c)))
|
|
@@ -557,7 +565,10 @@ let makeStorage = (
|
|
|
557
565
|
QueryDbListQuery.buildConnection(
|
|
558
566
|
~pageItems,
|
|
559
567
|
~hasNextPage=hasMore,
|
|
560
|
-
|
|
568
|
+
// Same rule the spec uses, because the parity suite compares them: a
|
|
569
|
+
// forward page reached by a cursor has one behind it. Hardcoding false
|
|
570
|
+
// made Prev disappear on exactly the pages that have a previous one.
|
|
571
|
+
~hasPreviousPage=afterCursor->Option.isSome,
|
|
561
572
|
~cursorValueOf,
|
|
562
573
|
),
|
|
563
574
|
)
|
|
@@ -396,12 +396,18 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
396
396
|
let orderByField = Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["field"]), Stdlib_JSON.Decode.string);
|
|
397
397
|
let isDesc = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["direction"]), Stdlib_JSON.Decode.string), "ASC") === "DESC";
|
|
398
398
|
let cursorExpr = orderByField !== undefined ? jsonText(orderByField) : idExpr;
|
|
399
|
-
let
|
|
400
|
-
|
|
399
|
+
let afterCursor = Stdlib_Option.flatMap(Stdlib_Option.flatMap(argsDict["after"], Stdlib_JSON.Decode.string), c => {
|
|
400
|
+
if (c === "") {
|
|
401
|
+
return;
|
|
402
|
+
} else {
|
|
403
|
+
return c;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
if (afterCursor !== undefined) {
|
|
401
407
|
whereParts.push(cursorExpr + ` ` + (
|
|
402
408
|
isDesc ? "<" : ">"
|
|
403
409
|
) + ` ?`);
|
|
404
|
-
params.push(QueryDbListQuery$ReventlessCore.decodeCursor(
|
|
410
|
+
params.push(QueryDbListQuery$ReventlessCore.decodeCursor(afterCursor));
|
|
405
411
|
}
|
|
406
412
|
let orderClause = orderByField !== undefined ? jsonText(orderByField) + ` ` + (
|
|
407
413
|
isDesc ? "DESC" : "ASC"
|
|
@@ -413,7 +419,7 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
413
419
|
let pageItems = rows.slice(0, pageSize);
|
|
414
420
|
let cursorField = Stdlib_Option.getOr(orderByField, "id");
|
|
415
421
|
let cursorValueOf = item => Stdlib_Option.getOr(QueryDbListQuery$ReventlessCore.getFieldString(item, cursorField), QueryDbListQuery$ReventlessCore.getId(item));
|
|
416
|
-
return QueryDbListQuery$ReventlessCore.buildConnection(pageItems, hasMore,
|
|
422
|
+
return QueryDbListQuery$ReventlessCore.buildConnection(pageItems, hasMore, Stdlib_Option.isSome(afterCursor), cursorValueOf);
|
|
417
423
|
};
|
|
418
424
|
bus.registerQueryDbListPage(name, listPage);
|
|
419
425
|
return {
|
|
@@ -291,7 +291,10 @@ describe("QueryDb list resolver — keyset pagination", () => {
|
|
|
291
291
|
expect(edgeNodeField(backEdges->Array.getUnsafe(0), "productId"))->toBe("p-3")
|
|
292
292
|
expect(edgeNodeField(backEdges->Array.getUnsafe(1), "productId"))->toBe("p-4")
|
|
293
293
|
expect(pageInfoBool(backPage, "hasPreviousPage"))->toBe(true)
|
|
294
|
-
|
|
294
|
+
// A backward page was cut from ahead, so the page it came from is a next
|
|
295
|
+
// page. Reporting false here is what made Prev a one-way door: step back
|
|
296
|
+
// once and the connection claimed to have ended.
|
|
297
|
+
expect(pageInfoBool(backPage, "hasNextPage"))->toBe(true)
|
|
295
298
|
|
|
296
299
|
// Walk one more step back — should land on p-1, p-2 with hasPreviousPage = false.
|
|
297
300
|
let backStart = pageInfoString(backPage, "startCursor")->Option.getOr("")
|
|
@@ -334,7 +334,7 @@ globalThis.describe("QueryDb list resolver — keyset pagination", () => {
|
|
|
334
334
|
globalThis.expect(edgeNodeField(backEdges[0], "productId")).toBe("p-3");
|
|
335
335
|
globalThis.expect(edgeNodeField(backEdges[1], "productId")).toBe("p-4");
|
|
336
336
|
globalThis.expect(pageInfoBool(backPage, "hasPreviousPage")).toBe(true);
|
|
337
|
-
globalThis.expect(pageInfoBool(backPage, "hasNextPage")).toBe(
|
|
337
|
+
globalThis.expect(pageInfoBool(backPage, "hasNextPage")).toBe(true);
|
|
338
338
|
let backStart = Stdlib_Option.getOr(getString(pageInfo(backPage), "startCursor"), "");
|
|
339
339
|
let firstPage = await resolver(null, Object.fromEntries([
|
|
340
340
|
[
|