@reventlessdev/reventless-aws 3.0.0-alpha.338 → 3.0.0-alpha.340
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 +20 -0
- package/package.json +8 -8
- package/src/Platform.res.mjs +4 -4
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +13 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +7 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +7 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +2 -1
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +1 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +3 -1
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +39 -0
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +31 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +29 -3
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +12 -5
- package/tests/AppSync_RetirementNarrowingTest.res.mjs +1 -1
- package/tests/PgQueryResolver_LambdaTest.res +3 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +36 -31
- package/tests/ProjectionEntryPoint_DisplayNameTest.res +96 -0
- package/tests/ProjectionEntryPoint_DisplayNameTest.res.mjs +131 -0
- package/tests/StateViewSliceEntryPoint_OpsTest.res +67 -0
- package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +102 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
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.340 (2026-09-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** a deployed state view slice composes its [@display](https://github.com/display)Name ([05ba7c5](https://github.com/ReventlessDev/reventless-core/commit/05ba7c531e605c5eb8d190a1fcb4f3d99888253d))
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* a slice folder is named for its kind, without the Slice ([a400555](https://github.com/ReventlessDev/reventless-core/commit/a400555121b023823983ebe846e99cda27063c1f))
|
|
14
|
+
* **core:** a reference names a row and shows its picture ([2b40d04](https://github.com/ReventlessDev/reventless-core/commit/2b40d0418bba2bf83463f446e94e372ea0c9898c))
|
|
15
|
+
* **core:** a state view records when it reached each state ([5216f71](https://github.com/ReventlessDev/reventless-core/commit/5216f71b8dcafcb2f0c991923ebcf2438b27e347))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 3.0.0-alpha.339 (2026-09-07)
|
|
19
|
+
|
|
20
|
+
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
# 3.0.0-alpha.338 (2026-09-07)
|
|
7
27
|
|
|
8
28
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.340",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
"sury": "11.0.0-rc.2",
|
|
17
17
|
"uuid": "^13.0.0",
|
|
18
18
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.15",
|
|
19
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
20
19
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
20
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
21
21
|
"@reventlessdev/rescript-node": "2.0.0-alpha.9",
|
|
22
|
-
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.
|
|
22
|
+
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.9",
|
|
23
23
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
24
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.257",
|
|
25
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.158",
|
|
26
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.35",
|
|
27
24
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
28
|
-
"@reventlessdev/reventless-
|
|
29
|
-
"@reventlessdev/reventless-
|
|
25
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.259",
|
|
26
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.35",
|
|
27
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.123",
|
|
28
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.160",
|
|
29
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.132"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"rescript": "12.3.0",
|
package/src/Platform.res.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/s
|
|
|
30
30
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
31
31
|
import * as Util_Bundle$ReventlessAws from "./util/Util_Bundle.res.mjs";
|
|
32
32
|
import * as Plugin_Stack$ReventlessAws from "./plugin/stack/Plugin_Stack.res.mjs";
|
|
33
|
-
import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/
|
|
33
|
+
import * as UiFragments$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments.res.mjs";
|
|
34
34
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
35
35
|
import * as Platform_Casts$ReventlessAws from "./Platform_Casts.res.mjs";
|
|
36
36
|
import * as Platform_Stack$ReventlessAws from "./Platform_Stack.res.mjs";
|
|
@@ -61,7 +61,7 @@ import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-cor
|
|
|
61
61
|
import * as PluginsProjection$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsProjection.res.mjs";
|
|
62
62
|
import * as StateTopic_AppSync$ReventlessAws from "./adapter/StateTopic/StateTopic_AppSync.res.mjs";
|
|
63
63
|
import * as AppSync_SdlDecorate$ReventlessAws from "./components/Api/AppSync_SdlDecorate.res.mjs";
|
|
64
|
-
import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/
|
|
64
|
+
import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res.mjs";
|
|
65
65
|
import * as Util_ResourceNaming$ReventlessAws from "./util/Util_ResourceNaming.res.mjs";
|
|
66
66
|
import * as Auth_ActiveRoleStore$ReventlessAws from "./adapter/Auth/Auth_ActiveRoleStore.res.mjs";
|
|
67
67
|
import * as ClonerRunner_Fargate$ReventlessAws from "./plugin/cloner/ClonerRunner_Fargate.res.mjs";
|
|
@@ -77,7 +77,7 @@ import * as EventTopicPublisher_SNS$ReventlessAws from "./adapter/EventTopic/Eve
|
|
|
77
77
|
import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
78
78
|
import * as PgQueryResolver_Builder$ReventlessAws from "./adapter/QueryDb/PgQueryResolver_Builder.res.mjs";
|
|
79
79
|
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
80
|
-
import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/
|
|
80
|
+
import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateView/UiFragments_Projection.res.mjs";
|
|
81
81
|
import * as Aggregate_Builder_Single$ReventlessAws from "./components/Aggregate_Builder_Single.res.mjs";
|
|
82
82
|
import * as ReadModel_Builder_Single$ReventlessAws from "./components/ReadModel_Builder_Single.res.mjs";
|
|
83
83
|
import * as StateChangeSlice_Builder$ReventlessAws from "./components/StateChangeSlice_Builder.res.mjs";
|
|
@@ -89,7 +89,7 @@ import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/revent
|
|
|
89
89
|
import * as Platform_UIFragments_Lambda$ReventlessAws from "./adapter/Api/Platform_UIFragments_Lambda.res.mjs";
|
|
90
90
|
import * as CommandTopicChannel_SQS_Sync$ReventlessAws from "./adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs";
|
|
91
91
|
import * as EventLogSubscription_AppSync$ReventlessAws from "./adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs";
|
|
92
|
-
import * as UiFragmentRegistry_Behavior$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/
|
|
92
|
+
import * as UiFragmentRegistry_Behavior$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry_Behavior.res.mjs";
|
|
93
93
|
import * as CommandTopicChannel_SQS_Async$ReventlessAws from "./adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs";
|
|
94
94
|
import * as Geocoder_AwsLocation_Resolver$ReventlessAws from "./adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs";
|
|
95
95
|
import * as Plugin_ExtensionPoint_Builder$ReventlessAws from "./plugin/stack/Plugin_ExtensionPoint_Builder.res.mjs";
|
|
@@ -103,6 +103,10 @@ type binding = {
|
|
|
103
103
|
name — the `@namedWhenRetired` opt-in. Read only by the reference door;
|
|
104
104
|
every other door narrows as it always did. */
|
|
105
105
|
namedWhenRetired: bool,
|
|
106
|
+
/** The field a reference shows this row's picture from, derived from the same
|
|
107
|
+
schema as `capability` by `Reventless.RowImage`. `None` for a view that
|
|
108
|
+
declares no picture, which is most of them. */
|
|
109
|
+
imageSource: option<Reventless.RowImage.source>,
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
// -- arg helpers -------------------------------------------------------------
|
|
@@ -309,6 +313,15 @@ let dispatch = async (
|
|
|
309
313
|
->Option.getOr(id),
|
|
310
314
|
),
|
|
311
315
|
),
|
|
316
|
+
// The picture travels with the name, so a retired row that keeps
|
|
317
|
+
// one keeps the other.
|
|
318
|
+
(
|
|
319
|
+
"image",
|
|
320
|
+
switch (binding.imageSource, item->JSON.Decode.object) {
|
|
321
|
+
| (Some(source), Some(d)) => d->Reventless.RowImage.refFrom(source)
|
|
322
|
+
| _ => None
|
|
323
|
+
}->Option.mapOr(JSON.Encode.null, JSON.Encode.string),
|
|
324
|
+
),
|
|
312
325
|
("retired", JSON.Encode.bool(retired)),
|
|
313
326
|
("retiredState", state->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
314
327
|
])->JSON.Encode.object,
|
|
@@ -5,6 +5,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
7
|
import * as Identity$Reventless from "@reventlessdev/reventless-spec/src/types/Identity.res.mjs";
|
|
8
|
+
import * as RowImage$Reventless from "@reventlessdev/reventless-spec/src/semantic/RowImage.res.mjs";
|
|
8
9
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
10
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
10
11
|
import * as Api_Ids$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/Api_Ids.res.mjs";
|
|
@@ -218,6 +219,8 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
218
219
|
}
|
|
219
220
|
let get = key => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[key]);
|
|
220
221
|
let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(get("id"), Stdlib_JSON.Decode.string), "");
|
|
222
|
+
let match$1 = binding.imageSource;
|
|
223
|
+
let match$2 = Stdlib_JSON.Decode.object(item);
|
|
221
224
|
return Object.fromEntries([
|
|
222
225
|
[
|
|
223
226
|
"id",
|
|
@@ -227,6 +230,10 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
227
230
|
"label",
|
|
228
231
|
Stdlib_Option.getOr(Stdlib_Option.flatMap(get(binding.labelField), Stdlib_JSON.Decode.string), id)
|
|
229
232
|
],
|
|
233
|
+
[
|
|
234
|
+
"image",
|
|
235
|
+
Stdlib_Option.mapOr(match$1 !== undefined && match$2 !== undefined ? RowImage$Reventless.refFrom(match$2, match$1) : undefined, null, prim => prim)
|
|
236
|
+
],
|
|
230
237
|
[
|
|
231
238
|
"retired",
|
|
232
239
|
retired
|
|
@@ -548,6 +548,13 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
548
548
|
~retiredField,
|
|
549
549
|
~retiredValues,
|
|
550
550
|
~namedWhenRetired=retiredSpec->Option.mapOr(false, r => r.namedWhenRetired),
|
|
551
|
+
// Derived at deploy time from the same schema every other backend
|
|
552
|
+
// derives it from at runtime — this is the one resolver that is
|
|
553
|
+
// generated source rather than a function, so it takes the read as
|
|
554
|
+
// an expression instead of doing it.
|
|
555
|
+
~imageExpr=?stateSchemaOpt
|
|
556
|
+
->Option.flatMap(Reventless.RowImage.sourceFrom)
|
|
557
|
+
->Option.map(source => Reventless.RowImage.jsExpr(~row="row", source)),
|
|
551
558
|
~ownerField?,
|
|
552
559
|
~elevatedGroups,
|
|
553
560
|
),
|
|
@@ -4,6 +4,7 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
4
4
|
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 Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
7
|
+
import * as RowImage$Reventless from "@reventlessdev/reventless-spec/src/semantic/RowImage.res.mjs";
|
|
7
8
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
8
9
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
10
|
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
@@ -181,7 +182,7 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
181
182
|
if (includeIdParam && subIdField === undefined) {
|
|
182
183
|
let storage$1 = storageResource(undefined, name$1);
|
|
183
184
|
let refsField = fieldNameForAll + "Refs";
|
|
184
|
-
resolverRefs = makeQueryResolver(Stdlib_String.capitalize(refsField), refsField, generateCode(storage$1, AppSync_Resolver_Functions$PulumiAws.refsByIds(labelField, retiredField, retiredValues, Stdlib_Option.mapOr(retiredSpec, false, r => r.namedWhenRetired), ownerField, elevatedGroups)));
|
|
185
|
+
resolverRefs = makeQueryResolver(Stdlib_String.capitalize(refsField), refsField, generateCode(storage$1, AppSync_Resolver_Functions$PulumiAws.refsByIds(labelField, retiredField, retiredValues, Stdlib_Option.mapOr(retiredSpec, false, r => r.namedWhenRetired), Stdlib_Option.map(Stdlib_Option.flatMap(stateSchemaOpt, RowImage$Reventless.sourceFrom), source => RowImage$Reventless.jsExpr("row", source)), ownerField, elevatedGroups)));
|
|
185
186
|
} else {
|
|
186
187
|
resolverRefs = undefined;
|
|
187
188
|
}
|
|
@@ -169,6 +169,7 @@ let registerBinding = (
|
|
|
169
169
|
->Reventless.StateAnnotations.getSpec
|
|
170
170
|
->Option.flatMap(a => a.retired)
|
|
171
171
|
->Option.mapOr(false, r => r.namedWhenRetired),
|
|
172
|
+
imageSource: spec.stateSchema->Reventless.RowImage.sourceFrom,
|
|
172
173
|
},
|
|
173
174
|
)
|
|
174
175
|
logDebug("registered resolver binding for " ++ entry.readModelName, {comp: "PgQueryResolver"})
|
|
@@ -4,6 +4,7 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
|
|
7
|
+
import * as RowImage$Reventless from "@reventlessdev/reventless-spec/src/semantic/RowImage.res.mjs";
|
|
7
8
|
import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
|
|
8
9
|
import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
|
|
9
10
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
@@ -78,7 +79,8 @@ function registerBinding(pushdowns, pgConnection, entry, spec) {
|
|
|
78
79
|
ownerField: Owner$Reventless.fieldNames(spec.stateSchema)[0],
|
|
79
80
|
retiredField: Stdlib_Option.map(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.field),
|
|
80
81
|
retiredValues: Stdlib_Option.flatMap(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), r => r.values),
|
|
81
|
-
namedWhenRetired: Stdlib_Option.mapOr(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), false, r => r.namedWhenRetired)
|
|
82
|
+
namedWhenRetired: Stdlib_Option.mapOr(Stdlib_Option.flatMap(StateAnnotations$Reventless.getSpec(spec.stateSchema), a => a.retired), false, r => r.namedWhenRetired),
|
|
83
|
+
imageSource: RowImage$Reventless.sourceFrom(spec.stateSchema)
|
|
82
84
|
});
|
|
83
85
|
HandlerFactoryHelpersMjs.log.debug("registered resolver binding for " + entry.readModelName, {
|
|
84
86
|
comp: "PgQueryResolver"
|
|
@@ -86,6 +86,45 @@ let withUnionMemberTypes = (
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
Composes the synthetic `displayName` column a `@displayName` state declares.
|
|
91
|
+
|
|
92
|
+
`Projection.rewriteAction` does this for the typed paths — the local platform
|
|
93
|
+
and the GWT harness rewrite the projection's actions before applying them — but
|
|
94
|
+
a deployed state-view slice assembles its own JSON-level pipeline here, so the
|
|
95
|
+
overlay never ran on AWS and every surface named the row by its id.
|
|
96
|
+
|
|
97
|
+
Applied at save rather than on the action: the state reaching the table is the
|
|
98
|
+
one an `Update`'s function already produced, so every action shape is covered
|
|
99
|
+
without enumerating them. `None` — a spec with no annotation, or one that
|
|
100
|
+
predates the metadata — leaves rows exactly as they were.
|
|
101
|
+
*/
|
|
102
|
+
let withDisplayName = (
|
|
103
|
+
base: ReventlessCore.QueryDb_Adapter.operations,
|
|
104
|
+
~stateSchema: option<S.t<unknown>>,
|
|
105
|
+
): ReventlessCore.QueryDb_Adapter.operations =>
|
|
106
|
+
switch stateSchema->Option.flatMap(Reventless.DisplayName.getSpec) {
|
|
107
|
+
| None => base
|
|
108
|
+
| Some(spec) =>
|
|
109
|
+
let compose = (state: JSON.t) => {
|
|
110
|
+
switch state->JSON.Decode.object {
|
|
111
|
+
| Some(stateDict) =>
|
|
112
|
+
stateDict->Dict.set(
|
|
113
|
+
"displayName",
|
|
114
|
+
JSON.Encode.string(Reventless.DisplayName.computeLabel(spec, stateDict)),
|
|
115
|
+
)
|
|
116
|
+
| None => ()
|
|
117
|
+
}
|
|
118
|
+
state
|
|
119
|
+
}
|
|
120
|
+
{
|
|
121
|
+
...base,
|
|
122
|
+
save: (id, state, saveMode, ttl) => base.save(id, compose(state), saveMode, ttl),
|
|
123
|
+
saveBatch: items =>
|
|
124
|
+
base.saveBatch(items->Array.map(((id, state, ttl)) => (id, compose(state), ttl))),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
89
128
|
let makeQueryDbOps = (
|
|
90
129
|
~queryDbTableName: string,
|
|
91
130
|
~pgConnection: option<PgConnection.connectionConfig>,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
3
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
|
|
4
6
|
import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
|
|
5
7
|
import * as StateTopicPublishMjs from "./StateTopicPublish.mjs";
|
|
6
8
|
import * as QueryDbEntryPoint_Ops$ReventlessAws from "./QueryDbEntryPoint_Ops.res.mjs";
|
|
@@ -41,6 +43,33 @@ function withUnionMemberTypes(base, stateSchema) {
|
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
function withDisplayName(base, stateSchema) {
|
|
47
|
+
let spec = Stdlib_Option.flatMap(stateSchema, DisplayName$Reventless.getSpec);
|
|
48
|
+
if (spec === undefined) {
|
|
49
|
+
return base;
|
|
50
|
+
}
|
|
51
|
+
let compose = state => {
|
|
52
|
+
let stateDict = Stdlib_JSON.Decode.object(state);
|
|
53
|
+
if (stateDict !== undefined) {
|
|
54
|
+
stateDict["displayName"] = DisplayName$Reventless.computeLabel(spec, stateDict);
|
|
55
|
+
}
|
|
56
|
+
return state;
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
load: base.load,
|
|
60
|
+
loadStream: base.loadStream,
|
|
61
|
+
save: (id, state, saveMode, ttl) => base.save(id, compose(state), saveMode, ttl),
|
|
62
|
+
saveBatch: items => base.saveBatch(items.map(param => [
|
|
63
|
+
param[0],
|
|
64
|
+
compose(param[1]),
|
|
65
|
+
param[2]
|
|
66
|
+
])),
|
|
67
|
+
count: base.count,
|
|
68
|
+
delete: base.delete,
|
|
69
|
+
deleteBatch: base.deleteBatch
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
function makeQueryDbOps(queryDbTableName, pgConnection, stateTopicName, indexes, subIdField) {
|
|
45
74
|
if (pgConnection !== undefined) {
|
|
46
75
|
return StateTopicPublishMjs.withLiveUpdates(QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(pgConnection, queryDbTableName, indexes, subIdField), {
|
|
@@ -59,6 +88,7 @@ export {
|
|
|
59
88
|
subIdFieldOf,
|
|
60
89
|
withLiveUpdates,
|
|
61
90
|
withUnionMemberTypes,
|
|
91
|
+
withDisplayName,
|
|
62
92
|
makeQueryDbOps,
|
|
63
93
|
}
|
|
64
|
-
/*
|
|
94
|
+
/* DisplayName-Reventless Not a pure module */
|
|
@@ -97,6 +97,10 @@ type envelope<'event> = {
|
|
|
97
97
|
event: JSON.t,
|
|
98
98
|
meta: Reventless.Message.meta,
|
|
99
99
|
recordedAt: string,
|
|
100
|
+
// The producer time, read off the JSON rather than through `asMeta` — that
|
|
101
|
+
// cast admits `undefined`, so `meta.time` throws on an envelope without one.
|
|
102
|
+
// "" where the producer wrote none, which appends no trail entry.
|
|
103
|
+
producerTime: string,
|
|
100
104
|
}
|
|
101
105
|
|
|
102
106
|
let decodeEnvelope = (json: JSON.t): envelope<'event> => {
|
|
@@ -111,6 +115,12 @@ let decodeEnvelope = (json: JSON.t): envelope<'event> => {
|
|
|
111
115
|
->Option.flatMap(f => f->Dict.get("recordedAt"))
|
|
112
116
|
->Option.flatMap(JSON.Decode.string)
|
|
113
117
|
->Option.getOr(""),
|
|
118
|
+
producerTime: fields
|
|
119
|
+
->Option.flatMap(f => f->Dict.get("meta"))
|
|
120
|
+
->Option.flatMap(JSON.Decode.object)
|
|
121
|
+
->Option.flatMap(m => m->Dict.get("time"))
|
|
122
|
+
->Option.flatMap(JSON.Decode.string)
|
|
123
|
+
->Option.getOr(""),
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
|
|
@@ -129,14 +139,27 @@ let makeJsonEventsHandler = (
|
|
|
129
139
|
>,
|
|
130
140
|
~queryDbOps: ReventlessCore.QueryDb_Adapter.operations,
|
|
131
141
|
~subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
|
|
142
|
+
~stateSchema: option<S.t<unknown>>,
|
|
132
143
|
): ReventlessCore.EventCollector.jsonEventsHandler =>
|
|
133
144
|
stream =>
|
|
134
145
|
stream
|
|
135
146
|
->Stream.mapEffect(json =>
|
|
136
147
|
Effect.sync(() =>
|
|
137
148
|
try {
|
|
138
|
-
let {event, meta, recordedAt} = decodeEnvelope(json)
|
|
139
|
-
project({
|
|
149
|
+
let {event, meta, recordedAt, producerTime} = decodeEnvelope(json)
|
|
150
|
+
let actions = project({
|
|
151
|
+
event: event->Reventless.Util_Sury.fromJson(eventSchema),
|
|
152
|
+
meta,
|
|
153
|
+
recordedAt,
|
|
154
|
+
})
|
|
155
|
+
// The deployed slice assembles JSON-level operations rather than going
|
|
156
|
+
// through the typed rewrite, so the trail is appended here — with this
|
|
157
|
+
// envelope's own time, which the apply below no longer has.
|
|
158
|
+
producerTime === ""
|
|
159
|
+
? actions
|
|
160
|
+
: actions->Array.map(
|
|
161
|
+
ReventlessCore.Projection.rewriteJsonAction(_, ~at=producerTime, ~stateSchema),
|
|
162
|
+
)
|
|
140
163
|
} catch {
|
|
141
164
|
| exn =>
|
|
142
165
|
StreamRoutedEntryPoint_Ops.logError(
|
|
@@ -190,7 +213,9 @@ let makeRegisteredHandler = (
|
|
|
190
213
|
~stateTopicName=entry.stateTopicName,
|
|
191
214
|
~indexes=ProjectionEntryPoint_Ops.indexesOf(modules.config),
|
|
192
215
|
~subIdField=ProjectionEntryPoint_Ops.subIdFieldOf(modules.subIdConfig),
|
|
193
|
-
)
|
|
216
|
+
)
|
|
217
|
+
->ProjectionEntryPoint_Ops.withUnionMemberTypes(~stateSchema=modules.stateSchema)
|
|
218
|
+
->ProjectionEntryPoint_Ops.withDisplayName(~stateSchema=modules.stateSchema)
|
|
194
219
|
{
|
|
195
220
|
handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
|
|
196
221
|
makeJsonEventsHandler(
|
|
@@ -199,6 +224,7 @@ let makeRegisteredHandler = (
|
|
|
199
224
|
~project=modules.project,
|
|
200
225
|
~queryDbOps,
|
|
201
226
|
~subIdConfig=modules.subIdConfig,
|
|
227
|
+
~stateSchema=modules.stateSchema,
|
|
202
228
|
),
|
|
203
229
|
),
|
|
204
230
|
// Matches the `StateViewSlice(<name>)` shape the rest of the framework
|
|
@@ -59,19 +59,26 @@ function decodeEnvelope(json) {
|
|
|
59
59
|
return {
|
|
60
60
|
event: tmp,
|
|
61
61
|
meta: Stdlib_Option.flatMap(fields, f => f["meta"]),
|
|
62
|
-
recordedAt: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(fields, f => f["recordedAt"]), Stdlib_JSON.Decode.string), "")
|
|
62
|
+
recordedAt: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(fields, f => f["recordedAt"]), Stdlib_JSON.Decode.string), ""),
|
|
63
|
+
producerTime: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(fields, f => f["meta"]), Stdlib_JSON.Decode.object), m => m["time"]), Stdlib_JSON.Decode.string), "")
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function makeJsonEventsHandler(sliceName, eventSchema, project, queryDbOps, subIdConfig) {
|
|
67
|
+
function makeJsonEventsHandler(sliceName, eventSchema, project, queryDbOps, subIdConfig, stateSchema) {
|
|
67
68
|
return stream => Stream.runForEach(Stream.flatMap(Stream.mapEffect(stream, json => Effect.sync(() => {
|
|
68
69
|
try {
|
|
69
70
|
let match = decodeEnvelope(json);
|
|
70
|
-
|
|
71
|
+
let producerTime = match.producerTime;
|
|
72
|
+
let actions = project({
|
|
71
73
|
event: Util_Sury$Reventless.fromJson(match.event, eventSchema),
|
|
72
74
|
meta: match.meta,
|
|
73
75
|
recordedAt: match.recordedAt
|
|
74
76
|
});
|
|
77
|
+
if (producerTime === "") {
|
|
78
|
+
return actions;
|
|
79
|
+
} else {
|
|
80
|
+
return actions.map(__x => Projection$ReventlessCore.rewriteJsonAction(__x, producerTime, stateSchema));
|
|
81
|
+
}
|
|
75
82
|
} catch (raw_exn) {
|
|
76
83
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
77
84
|
StreamRoutedEntryPoint_Ops$ReventlessAws.logError("failed to decode event", {
|
|
@@ -84,9 +91,9 @@ function makeJsonEventsHandler(sliceName, eventSchema, project, queryDbOps, subI
|
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
function makeRegisteredHandler(entry, modules) {
|
|
87
|
-
let queryDbOps = ProjectionEntryPoint_Ops$ReventlessAws.withUnionMemberTypes(ProjectionEntryPoint_Ops$ReventlessAws.makeQueryDbOps(entry.queryDbTableName, entry.pgConnection, entry.stateTopicName, ProjectionEntryPoint_Ops$ReventlessAws.indexesOf(modules.config), ProjectionEntryPoint_Ops$ReventlessAws.subIdFieldOf(modules.subIdConfig)), modules.stateSchema);
|
|
94
|
+
let queryDbOps = ProjectionEntryPoint_Ops$ReventlessAws.withDisplayName(ProjectionEntryPoint_Ops$ReventlessAws.withUnionMemberTypes(ProjectionEntryPoint_Ops$ReventlessAws.makeQueryDbOps(entry.queryDbTableName, entry.pgConnection, entry.stateTopicName, ProjectionEntryPoint_Ops$ReventlessAws.indexesOf(modules.config), ProjectionEntryPoint_Ops$ReventlessAws.subIdFieldOf(modules.subIdConfig)), modules.stateSchema), modules.stateSchema);
|
|
88
95
|
return {
|
|
89
|
-
handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeJsonEventsHandler(modules.name, modules.consumedEventSchema, modules.project, queryDbOps, modules.subIdConfig)),
|
|
96
|
+
handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeJsonEventsHandler(modules.name, modules.consumedEventSchema, modules.project, queryDbOps, modules.subIdConfig, modules.stateSchema)),
|
|
90
97
|
comp: `StateViewSlice(` + modules.name + `)`
|
|
91
98
|
};
|
|
92
99
|
}
|
|
@@ -197,7 +197,7 @@ globalThis.describe("the unowned stub is callable where the door calls it", () =
|
|
|
197
197
|
]);
|
|
198
198
|
});
|
|
199
199
|
globalThis.test("declares a parameter on the reference door", () => {
|
|
200
|
-
let code = AppSync_Resolver_Functions$PulumiAws.refsByIds("name", undefined, undefined, false, undefined, undefined)("Products");
|
|
200
|
+
let code = AppSync_Resolver_Functions$PulumiAws.refsByIds("name", undefined, undefined, false, undefined, undefined, undefined)("Products");
|
|
201
201
|
globalThis.expect([
|
|
202
202
|
code.includes("const _owns = (row) => true;"),
|
|
203
203
|
code.includes("_owns(row)")
|
|
@@ -96,6 +96,7 @@ let makeBinding = (
|
|
|
96
96
|
~retiredField=None,
|
|
97
97
|
~retiredValues=None,
|
|
98
98
|
~namedWhenRetired=false,
|
|
99
|
+
~imageSource=None,
|
|
99
100
|
(),
|
|
100
101
|
): PgQueryResolver_Lambda.binding => {
|
|
101
102
|
ops,
|
|
@@ -113,6 +114,7 @@ let makeBinding = (
|
|
|
113
114
|
retiredField,
|
|
114
115
|
retiredValues,
|
|
115
116
|
namedWhenRetired,
|
|
117
|
+
imageSource,
|
|
116
118
|
}
|
|
117
119
|
|
|
118
120
|
let mkPayload = (
|
|
@@ -697,6 +699,7 @@ let geoBinding = (): PgQueryResolver_Lambda.binding => {
|
|
|
697
699
|
retiredField: None,
|
|
698
700
|
retiredValues: None,
|
|
699
701
|
namedWhenRetired: false,
|
|
702
|
+
imageSource: None,
|
|
700
703
|
}
|
|
701
704
|
|
|
702
705
|
// The member type of the first row a door answers with, whatever shape it took.
|
|
@@ -121,13 +121,14 @@ let capability = {
|
|
|
121
121
|
sortFields: capability_sortFields
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
function makeBinding(authorizationOpt, subIdFieldOpt, ownerFieldOpt, retiredFieldOpt, retiredValuesOpt, namedWhenRetiredOpt, param) {
|
|
124
|
+
function makeBinding(authorizationOpt, subIdFieldOpt, ownerFieldOpt, retiredFieldOpt, retiredValuesOpt, namedWhenRetiredOpt, imageSourceOpt, param) {
|
|
125
125
|
let authorization = authorizationOpt !== undefined ? authorizationOpt : "AllowAnonymous";
|
|
126
126
|
let subIdField = subIdFieldOpt !== undefined ? Primitive_option.valFromOption(subIdFieldOpt) : undefined;
|
|
127
127
|
let ownerField = ownerFieldOpt !== undefined ? Primitive_option.valFromOption(ownerFieldOpt) : undefined;
|
|
128
128
|
let retiredField = retiredFieldOpt !== undefined ? Primitive_option.valFromOption(retiredFieldOpt) : undefined;
|
|
129
129
|
let retiredValues = retiredValuesOpt !== undefined ? Primitive_option.valFromOption(retiredValuesOpt) : undefined;
|
|
130
130
|
let namedWhenRetired = namedWhenRetiredOpt !== undefined ? namedWhenRetiredOpt : false;
|
|
131
|
+
let imageSource = imageSourceOpt !== undefined ? Primitive_option.valFromOption(imageSourceOpt) : undefined;
|
|
131
132
|
return {
|
|
132
133
|
ops: ops,
|
|
133
134
|
pushdowns: pushdowns,
|
|
@@ -145,7 +146,8 @@ function makeBinding(authorizationOpt, subIdFieldOpt, ownerFieldOpt, retiredFiel
|
|
|
145
146
|
ownerField: ownerField,
|
|
146
147
|
retiredField: retiredField,
|
|
147
148
|
retiredValues: retiredValues,
|
|
148
|
-
namedWhenRetired: namedWhenRetired
|
|
149
|
+
namedWhenRetired: namedWhenRetired,
|
|
150
|
+
imageSource: imageSource
|
|
149
151
|
};
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -188,7 +190,7 @@ function ids(arr) {
|
|
|
188
190
|
|
|
189
191
|
globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
190
192
|
globalThis.test("getById returns the item (id-carrying)", async () => {
|
|
191
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
193
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
192
194
|
"id",
|
|
193
195
|
"p-2"
|
|
194
196
|
]]), undefined, undefined));
|
|
@@ -196,14 +198,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
196
198
|
globalThis.expect(str(r, "name")).toEqual("Bravo");
|
|
197
199
|
});
|
|
198
200
|
globalThis.test("getById miss → null", async () => {
|
|
199
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
201
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
200
202
|
"id",
|
|
201
203
|
"absent"
|
|
202
204
|
]]), undefined, undefined));
|
|
203
205
|
globalThis.expect(r).toBe(null);
|
|
204
206
|
});
|
|
205
207
|
globalThis.test("byIds returns matches, drops missing", async () => {
|
|
206
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
208
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
207
209
|
"ids",
|
|
208
210
|
[
|
|
209
211
|
"p-1",
|
|
@@ -217,7 +219,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
217
219
|
]);
|
|
218
220
|
});
|
|
219
221
|
globalThis.test("index routes to indexLookup on the idField", async () => {
|
|
220
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("index", "byStatus", Object.fromEntries([[
|
|
222
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("index", "byStatus", Object.fromEntries([[
|
|
221
223
|
"byStatus",
|
|
222
224
|
"active"
|
|
223
225
|
]]), undefined, undefined));
|
|
@@ -228,13 +230,13 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
228
230
|
});
|
|
229
231
|
globalThis.test("list with push-down → returns the connection listPage gives", async () => {
|
|
230
232
|
listPageReturn.contents = "PUSHED";
|
|
231
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
|
|
233
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
|
|
232
234
|
listPageReturn.contents = undefined;
|
|
233
235
|
globalThis.expect(r).toEqual("PUSHED");
|
|
234
236
|
});
|
|
235
237
|
globalThis.test("list without push-down → falls back to the shared spec", async () => {
|
|
236
238
|
listPageReturn.contents = undefined;
|
|
237
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
|
|
239
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("list", undefined, undefined, undefined, undefined));
|
|
238
240
|
let edgeIds = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array), []), e => Stdlib_Option.flatMap(field(e, "node"), n => str(n, "id")));
|
|
239
241
|
globalThis.expect(edgeIds).toEqual([
|
|
240
242
|
"p-1",
|
|
@@ -243,21 +245,21 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
243
245
|
]);
|
|
244
246
|
});
|
|
245
247
|
globalThis.test("items with subId routes to itemsPage (id echoed)", async () => {
|
|
246
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, "seq", undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
|
|
248
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, "seq", undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
|
|
247
249
|
"id",
|
|
248
250
|
"p-2"
|
|
249
251
|
]]), undefined, undefined));
|
|
250
252
|
globalThis.expect(str(r, "itemsFor")).toEqual("p-2");
|
|
251
253
|
});
|
|
252
254
|
globalThis.test("items without subId → empty connection", async () => {
|
|
253
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
|
|
255
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("items", undefined, Object.fromEntries([[
|
|
254
256
|
"id",
|
|
255
257
|
"p-2"
|
|
256
258
|
]]), undefined, undefined));
|
|
257
259
|
globalThis.expect(Stdlib_Option.getOr(Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array), []).length).toBe(0);
|
|
258
260
|
});
|
|
259
261
|
globalThis.test("authorization DenyAll → empty shape per kind", async () => {
|
|
260
|
-
let b = makeBinding("DenyAll", undefined, undefined, undefined, undefined, undefined, undefined);
|
|
262
|
+
let b = makeBinding("DenyAll", undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
261
263
|
let single = await PgQueryResolver_Lambda$ReventlessAws.dispatch(b, undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
262
264
|
"id",
|
|
263
265
|
"p-1"
|
|
@@ -276,7 +278,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
276
278
|
TAG: "Deny",
|
|
277
279
|
_0: "nope"
|
|
278
280
|
}));
|
|
279
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
281
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
280
282
|
"id",
|
|
281
283
|
"p-1"
|
|
282
284
|
]]), undefined, undefined));
|
|
@@ -302,7 +304,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
302
304
|
arguments: payload_arguments,
|
|
303
305
|
identity: Identity$Reventless.anonymous
|
|
304
306
|
};
|
|
305
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
|
|
307
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
|
|
306
308
|
globalThis.expect(str(r, "id")).toEqual("p-2");
|
|
307
309
|
});
|
|
308
310
|
globalThis.test("resolveOne multi via target index → array", async () => {
|
|
@@ -328,7 +330,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
328
330
|
arguments: payload_arguments,
|
|
329
331
|
identity: Identity$Reventless.anonymous
|
|
330
332
|
};
|
|
331
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
|
|
333
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
|
|
332
334
|
globalThis.expect(ids(r)).toEqual([
|
|
333
335
|
"p-1",
|
|
334
336
|
"p-3"
|
|
@@ -355,14 +357,14 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
355
357
|
arguments: payload_arguments,
|
|
356
358
|
identity: Identity$Reventless.anonymous
|
|
357
359
|
};
|
|
358
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
|
|
360
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, payload);
|
|
359
361
|
globalThis.expect(ids(r)).toEqual([
|
|
360
362
|
"p-1",
|
|
361
363
|
"p-3"
|
|
362
364
|
]);
|
|
363
365
|
});
|
|
364
366
|
globalThis.test("node decodes global id → __typename + item", async () => {
|
|
365
|
-
PgQueryResolver_Lambda$ReventlessAws.register("Things", makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined));
|
|
367
|
+
PgQueryResolver_Lambda$ReventlessAws.register("Things", makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined));
|
|
366
368
|
PgQueryResolver_Lambda$ReventlessAws.registerNodeType("Thing", "Things");
|
|
367
369
|
let payload_arguments = Object.fromEntries([[
|
|
368
370
|
"id",
|
|
@@ -412,7 +414,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
412
414
|
delete: 0,
|
|
413
415
|
deleteBatch: 0
|
|
414
416
|
};
|
|
415
|
-
let init = makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
417
|
+
let init = makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
416
418
|
let authBinding_pushdowns = init.pushdowns;
|
|
417
419
|
let authBinding_indexes = init.indexes;
|
|
418
420
|
let authBinding_subIdField = init.subIdField;
|
|
@@ -424,6 +426,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
424
426
|
let authBinding_retiredField = init.retiredField;
|
|
425
427
|
let authBinding_retiredValues = init.retiredValues;
|
|
426
428
|
let authBinding_namedWhenRetired = init.namedWhenRetired;
|
|
429
|
+
let authBinding_imageSource = init.imageSource;
|
|
427
430
|
let authBinding = {
|
|
428
431
|
ops: authOps,
|
|
429
432
|
pushdowns: authBinding_pushdowns,
|
|
@@ -436,7 +439,8 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
436
439
|
ownerField: authBinding_ownerField,
|
|
437
440
|
retiredField: authBinding_retiredField,
|
|
438
441
|
retiredValues: authBinding_retiredValues,
|
|
439
|
-
namedWhenRetired: authBinding_namedWhenRetired
|
|
442
|
+
namedWhenRetired: authBinding_namedWhenRetired,
|
|
443
|
+
imageSource: authBinding_imageSource
|
|
440
444
|
};
|
|
441
445
|
let lookupAuth = name => {
|
|
442
446
|
if (name === "AuthTable") {
|
|
@@ -462,18 +466,18 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
462
466
|
identity: ident(username, groups)
|
|
463
467
|
});
|
|
464
468
|
globalThis.test("auth index: group member who owns → results", async () => {
|
|
465
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("alice", ["Owner"]));
|
|
469
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("alice", ["Owner"]));
|
|
466
470
|
globalThis.expect(ids(r)).toEqual([
|
|
467
471
|
"p-1",
|
|
468
472
|
"p-3"
|
|
469
473
|
]);
|
|
470
474
|
});
|
|
471
475
|
globalThis.test("auth index: group member who does NOT own → empty", async () => {
|
|
472
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", ["Owner"]));
|
|
476
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", ["Owner"]));
|
|
473
477
|
globalThis.expect(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(r), []).length).toBe(0);
|
|
474
478
|
});
|
|
475
479
|
globalThis.test("auth index: non-member passes through → results", async () => {
|
|
476
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", []));
|
|
480
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), lookupAuth, authIndexPayload("bob", []));
|
|
477
481
|
globalThis.expect(ids(r)).toEqual([
|
|
478
482
|
"p-1",
|
|
479
483
|
"p-3"
|
|
@@ -482,7 +486,7 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
482
486
|
globalThis.test("unmapped kind throws", async () => {
|
|
483
487
|
let threw;
|
|
484
488
|
try {
|
|
485
|
-
await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("frobnicate", undefined, undefined, undefined, undefined));
|
|
489
|
+
await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("frobnicate", undefined, undefined, undefined, undefined));
|
|
486
490
|
threw = false;
|
|
487
491
|
} catch (exn) {
|
|
488
492
|
threw = true;
|
|
@@ -493,35 +497,35 @@ globalThis.describe("PgQueryResolver_Lambda.dispatch", () => {
|
|
|
493
497
|
|
|
494
498
|
globalThis.describe("id form — the typed doors take either", () => {
|
|
495
499
|
globalThis.test("getById resolves a Relay global id", async () => {
|
|
496
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
500
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
497
501
|
"id",
|
|
498
502
|
Api_Ids$ReventlessCore.encode("Thing", "p-2")
|
|
499
503
|
]]), undefined, undefined));
|
|
500
504
|
globalThis.expect(str(r, "name")).toEqual("Bravo");
|
|
501
505
|
});
|
|
502
506
|
globalThis.test("getById reports the storage key it resolved to", async () => {
|
|
503
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
507
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
504
508
|
"id",
|
|
505
509
|
Api_Ids$ReventlessCore.encode("Thing", "p-2")
|
|
506
510
|
]]), undefined, undefined));
|
|
507
511
|
globalThis.expect(str(r, "id")).toEqual("p-2");
|
|
508
512
|
});
|
|
509
513
|
globalThis.test("getById still resolves a plain storage key", async () => {
|
|
510
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
514
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
511
515
|
"id",
|
|
512
516
|
"p-2"
|
|
513
517
|
]]), undefined, undefined));
|
|
514
518
|
globalThis.expect(str(r, "name")).toEqual("Bravo");
|
|
515
519
|
});
|
|
516
520
|
globalThis.test("getById on a global id for a row that does not exist is still null", async () => {
|
|
517
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
521
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("getById", undefined, Object.fromEntries([[
|
|
518
522
|
"id",
|
|
519
523
|
Api_Ids$ReventlessCore.encode("Thing", "absent")
|
|
520
524
|
]]), undefined, undefined));
|
|
521
525
|
globalThis.expect(r).toBe(null);
|
|
522
526
|
});
|
|
523
527
|
globalThis.test("byIds accepts the two forms mixed in one call", async () => {
|
|
524
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
528
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
525
529
|
"ids",
|
|
526
530
|
[
|
|
527
531
|
"p-1",
|
|
@@ -534,7 +538,7 @@ globalThis.describe("id form — the typed doors take either", () => {
|
|
|
534
538
|
]);
|
|
535
539
|
});
|
|
536
540
|
globalThis.test("byIds with every id found does not double-count", async () => {
|
|
537
|
-
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
541
|
+
let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), undefined, mkPayload("byIds", undefined, Object.fromEntries([[
|
|
538
542
|
"ids",
|
|
539
543
|
[
|
|
540
544
|
"p-1",
|
|
@@ -549,7 +553,7 @@ globalThis.describe("id form — the typed doors take either", () => {
|
|
|
549
553
|
});
|
|
550
554
|
|
|
551
555
|
globalThis.describe("owner scoping", () => {
|
|
552
|
-
let ownedBinding = () => makeBinding(undefined, undefined, "owner", undefined, undefined, undefined, undefined);
|
|
556
|
+
let ownedBinding = () => makeBinding(undefined, undefined, "owner", undefined, undefined, undefined, undefined, undefined);
|
|
553
557
|
let shopper = cognito("u-a", ["User"]);
|
|
554
558
|
let listWith = async (binding, identity) => {
|
|
555
559
|
lastOwnerScope.contents = undefined;
|
|
@@ -574,7 +578,7 @@ globalThis.describe("owner scoping", () => {
|
|
|
574
578
|
globalThis.expect(match[1]).toBe(undefined);
|
|
575
579
|
});
|
|
576
580
|
globalThis.test("a view with no owner field is never scoped", async () => {
|
|
577
|
-
let match = await listWith(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined), shopper);
|
|
581
|
+
let match = await listWith(makeBinding(undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined), shopper);
|
|
578
582
|
globalThis.expect(match[1]).toBe(undefined);
|
|
579
583
|
});
|
|
580
584
|
globalThis.test("an unidentified caller is refused before the push-down runs", async () => {
|
|
@@ -709,7 +713,8 @@ function geoBinding() {
|
|
|
709
713
|
ownerField: undefined,
|
|
710
714
|
retiredField: undefined,
|
|
711
715
|
retiredValues: undefined,
|
|
712
|
-
namedWhenRetired: false
|
|
716
|
+
namedWhenRetired: false,
|
|
717
|
+
imageSource: undefined
|
|
713
718
|
};
|
|
714
719
|
}
|
|
715
720
|
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// The `@displayName` overlay reached the typed paths — the local platform's
|
|
2
|
+
// builder and the GWT harness both rewrite a projection's actions — but a
|
|
3
|
+
// deployed state-view slice assembles its own JSON-level operations, so the
|
|
4
|
+
// synthetic column stayed null on AWS and every surface named the row by its id.
|
|
5
|
+
//
|
|
6
|
+
// The same gap, and for the same reason, as the union stamp beside it.
|
|
7
|
+
|
|
8
|
+
open JestGlobals
|
|
9
|
+
|
|
10
|
+
@schema
|
|
11
|
+
type rowState = {
|
|
12
|
+
orderId: string,
|
|
13
|
+
placedAt: string,
|
|
14
|
+
shippedAt: string,
|
|
15
|
+
displayName: option<string>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// The same state with no `@displayName` on any of its fields.
|
|
19
|
+
let plainRowStateSchema = rowStateSchema
|
|
20
|
+
|
|
21
|
+
// What the ppx emits for `@displayName placedAt`.
|
|
22
|
+
let rowStateSchema =
|
|
23
|
+
rowStateSchema->S.Metadata.set(
|
|
24
|
+
~id=Reventless.DisplayName.displayNameId,
|
|
25
|
+
{Reventless.DisplayName.fields: ["placedAt"], separator: " "},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
// A recording stand-in for the storage the runtime would wrap.
|
|
29
|
+
let capture = (): (ReventlessCore.QueryDb_Adapter.operations, array<JSON.t>) => {
|
|
30
|
+
let saved: array<JSON.t> = []
|
|
31
|
+
let base: ReventlessCore.QueryDb_Adapter.operations = {
|
|
32
|
+
load: _ => Promise.resolve(Ok([])),
|
|
33
|
+
loadStream: _ => Stream.empty,
|
|
34
|
+
save: (_id, state, _saveMode, _ttl) => {
|
|
35
|
+
saved->Array.push(state)
|
|
36
|
+
Promise.resolve(Ok())
|
|
37
|
+
},
|
|
38
|
+
saveBatch: items => {
|
|
39
|
+
items->Array.forEach(((_, state, _)) => saved->Array.push(state))
|
|
40
|
+
Promise.resolve(Ok())
|
|
41
|
+
},
|
|
42
|
+
count: (_, _, _) => Promise.resolve(Ok(0)),
|
|
43
|
+
delete: (_, _) => Promise.resolve(Ok()),
|
|
44
|
+
deleteBatch: _ => Promise.resolve(Ok()),
|
|
45
|
+
}
|
|
46
|
+
(base, saved)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let labelOf = (json: JSON.t) =>
|
|
50
|
+
json
|
|
51
|
+
->JSON.Decode.object
|
|
52
|
+
->Option.flatMap(o => o->Dict.get("displayName"))
|
|
53
|
+
->Option.flatMap(JSON.Decode.string)
|
|
54
|
+
|
|
55
|
+
let row = (~placedAt="2026-09-07T13:57:46.593Z"): JSON.t =>
|
|
56
|
+
({orderId: "o1", placedAt, shippedAt: "", displayName: None}: rowState)
|
|
57
|
+
->ReventlessCore.Message.encode(rowStateSchema)
|
|
58
|
+
|
|
59
|
+
describe("ProjectionEntryPoint_Ops.withDisplayName", () => {
|
|
60
|
+
testPromise("composes the label on save", async () => {
|
|
61
|
+
let (base, saved) = capture()
|
|
62
|
+
let ops =
|
|
63
|
+
base->ProjectionEntryPoint_Ops.withDisplayName(
|
|
64
|
+
~stateSchema=Some(rowStateSchema->S.castToUnknown),
|
|
65
|
+
)
|
|
66
|
+
let _ = await ops.save("o1", row(), ReventlessCore.QueryDb.Any, None)
|
|
67
|
+
expect(saved->Array.get(0)->Option.flatMap(labelOf))->toEqual(Some("2026-09-07T13:57:46.593Z"))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// saveBatch is the path a multi-event projection takes, and it was missing the
|
|
71
|
+
// overlay for the same reason save was.
|
|
72
|
+
testPromise("composes the label for every row in a batch", async () => {
|
|
73
|
+
let (base, saved) = capture()
|
|
74
|
+
let ops =
|
|
75
|
+
base->ProjectionEntryPoint_Ops.withDisplayName(
|
|
76
|
+
~stateSchema=Some(rowStateSchema->S.castToUnknown),
|
|
77
|
+
)
|
|
78
|
+
let _ = await ops.saveBatch([("o1", row(), None), ("o2", row(~placedAt="2026-09-08T09:00:00Z"), None)])
|
|
79
|
+
expect(saved->Array.map(labelOf))->toEqual([
|
|
80
|
+
Some("2026-09-07T13:57:46.593Z"),
|
|
81
|
+
Some("2026-09-08T09:00:00Z"),
|
|
82
|
+
])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// A state that declares no `@displayName` is left exactly as it was — the
|
|
86
|
+
// wrapper is applied to every slice, annotated or not.
|
|
87
|
+
testPromise("passes rows through when the state declares no label", async () => {
|
|
88
|
+
let (base, saved) = capture()
|
|
89
|
+
let ops =
|
|
90
|
+
base->ProjectionEntryPoint_Ops.withDisplayName(
|
|
91
|
+
~stateSchema=Some(plainRowStateSchema->S.castToUnknown),
|
|
92
|
+
)
|
|
93
|
+
let _ = await ops.save("o1", row(), ReventlessCore.QueryDb.Any, None)
|
|
94
|
+
expect(saved->Array.get(0)->Option.flatMap(labelOf))->toEqual(None)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
|
|
8
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
9
|
+
import * as ProjectionEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs";
|
|
10
|
+
|
|
11
|
+
let rowStateSchema = Sury.$schema(s => ({
|
|
12
|
+
orderId: s.m(Sury.string),
|
|
13
|
+
placedAt: s.m(Sury.string),
|
|
14
|
+
shippedAt: s.m(Sury.string),
|
|
15
|
+
displayName: s.m(Sury.$option(Sury.string))
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
let rowStateSchema$1 = Sury.$Metadata_set(rowStateSchema, DisplayName$Reventless.displayNameId, {
|
|
19
|
+
fields: ["placedAt"],
|
|
20
|
+
separator: " "
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function capture() {
|
|
24
|
+
let saved = [];
|
|
25
|
+
let base_load = param => Promise.resolve({
|
|
26
|
+
TAG: "Ok",
|
|
27
|
+
_0: []
|
|
28
|
+
});
|
|
29
|
+
let base_loadStream = param => Stream.empty;
|
|
30
|
+
let base_save = (_id, state, _saveMode, _ttl) => {
|
|
31
|
+
saved.push(state);
|
|
32
|
+
return Promise.resolve({
|
|
33
|
+
TAG: "Ok",
|
|
34
|
+
_0: undefined
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
let base_saveBatch = items => {
|
|
38
|
+
items.forEach(param => {
|
|
39
|
+
saved.push(param[1]);
|
|
40
|
+
});
|
|
41
|
+
return Promise.resolve({
|
|
42
|
+
TAG: "Ok",
|
|
43
|
+
_0: undefined
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
let base_count = (param, param$1, param$2) => Promise.resolve({
|
|
47
|
+
TAG: "Ok",
|
|
48
|
+
_0: 0
|
|
49
|
+
});
|
|
50
|
+
let base_delete = (param, param$1) => Promise.resolve({
|
|
51
|
+
TAG: "Ok",
|
|
52
|
+
_0: undefined
|
|
53
|
+
});
|
|
54
|
+
let base_deleteBatch = param => Promise.resolve({
|
|
55
|
+
TAG: "Ok",
|
|
56
|
+
_0: undefined
|
|
57
|
+
});
|
|
58
|
+
let base = {
|
|
59
|
+
load: base_load,
|
|
60
|
+
loadStream: base_loadStream,
|
|
61
|
+
save: base_save,
|
|
62
|
+
saveBatch: base_saveBatch,
|
|
63
|
+
count: base_count,
|
|
64
|
+
delete: base_delete,
|
|
65
|
+
deleteBatch: base_deleteBatch
|
|
66
|
+
};
|
|
67
|
+
return [
|
|
68
|
+
base,
|
|
69
|
+
saved
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function labelOf(json) {
|
|
74
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["displayName"]), Stdlib_JSON.Decode.string);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function row(placedAtOpt) {
|
|
78
|
+
let placedAt = placedAtOpt !== undefined ? placedAtOpt : "2026-09-07T13:57:46.593Z";
|
|
79
|
+
return Message$ReventlessCore.encode({
|
|
80
|
+
orderId: "o1",
|
|
81
|
+
placedAt: placedAt,
|
|
82
|
+
shippedAt: "",
|
|
83
|
+
displayName: undefined
|
|
84
|
+
}, rowStateSchema$1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
globalThis.describe("ProjectionEntryPoint_Ops.withDisplayName", () => {
|
|
88
|
+
globalThis.test("composes the label on save", async () => {
|
|
89
|
+
let match = capture();
|
|
90
|
+
let ops = ProjectionEntryPoint_Ops$ReventlessAws.withDisplayName(match[0], rowStateSchema$1);
|
|
91
|
+
await ops.save("o1", row(undefined), "Any", undefined);
|
|
92
|
+
globalThis.expect(Stdlib_Option.flatMap(match[1][0], labelOf)).toEqual("2026-09-07T13:57:46.593Z");
|
|
93
|
+
});
|
|
94
|
+
globalThis.test("composes the label for every row in a batch", async () => {
|
|
95
|
+
let match = capture();
|
|
96
|
+
let ops = ProjectionEntryPoint_Ops$ReventlessAws.withDisplayName(match[0], rowStateSchema$1);
|
|
97
|
+
await ops.saveBatch([
|
|
98
|
+
[
|
|
99
|
+
"o1",
|
|
100
|
+
row(undefined),
|
|
101
|
+
undefined
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
"o2",
|
|
105
|
+
row("2026-09-08T09:00:00Z"),
|
|
106
|
+
undefined
|
|
107
|
+
]
|
|
108
|
+
]);
|
|
109
|
+
globalThis.expect(match[1].map(labelOf)).toEqual([
|
|
110
|
+
"2026-09-07T13:57:46.593Z",
|
|
111
|
+
"2026-09-08T09:00:00Z"
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
globalThis.test("passes rows through when the state declares no label", async () => {
|
|
115
|
+
let match = capture();
|
|
116
|
+
let ops = ProjectionEntryPoint_Ops$ReventlessAws.withDisplayName(match[0], rowStateSchema);
|
|
117
|
+
await ops.save("o1", row(undefined), "Any", undefined);
|
|
118
|
+
globalThis.expect(Stdlib_Option.flatMap(match[1][0], labelOf)).toEqual(undefined);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
let plainRowStateSchema = rowStateSchema;
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
plainRowStateSchema,
|
|
126
|
+
rowStateSchema$1 as rowStateSchema,
|
|
127
|
+
capture,
|
|
128
|
+
labelOf,
|
|
129
|
+
row,
|
|
130
|
+
}
|
|
131
|
+
/* rowStateSchema Not a pure module */
|
|
@@ -14,6 +14,19 @@ open JestGlobals
|
|
|
14
14
|
let str = JSON.Encode.string
|
|
15
15
|
let obj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
|
|
16
16
|
|
|
17
|
+
@schema
|
|
18
|
+
type trailedLifecycle =
|
|
19
|
+
| Placed
|
|
20
|
+
| Shipped
|
|
21
|
+
|
|
22
|
+
// A view declaring a trail, for the pipeline cases below. The states this path
|
|
23
|
+
// writes are JSON, so only the schema says where the trail and the lifecycle are.
|
|
24
|
+
@schema
|
|
25
|
+
type trailedState = {
|
|
26
|
+
lifecycle: trailedLifecycle,
|
|
27
|
+
trail: Reventless.Lifecycle.Trail.t<trailedLifecycle>,
|
|
28
|
+
}
|
|
29
|
+
|
|
17
30
|
describe("StateViewSliceEntryPoint_Ops.parseHandlerConfig", () => {
|
|
18
31
|
testSync("empty raw config yields no handlers", () => {
|
|
19
32
|
expect(StateViewSliceEntryPoint_Ops.parseHandlerConfig("")->Array.length)->toBe(0)
|
|
@@ -141,6 +154,7 @@ describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () => {
|
|
|
141
154
|
~project,
|
|
142
155
|
~queryDbOps=makeStub(saved),
|
|
143
156
|
~subIdConfig=None,
|
|
157
|
+
~stateSchema=None,
|
|
144
158
|
)
|
|
145
159
|
let envelope = obj([("event", str("row-1")), ("recordedAt", str("2026-07-27"))])
|
|
146
160
|
let bare = str("row-2")
|
|
@@ -156,6 +170,7 @@ describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () => {
|
|
|
156
170
|
~project,
|
|
157
171
|
~queryDbOps=makeStub(saved),
|
|
158
172
|
~subIdConfig=None,
|
|
173
|
+
~stateSchema=None,
|
|
159
174
|
)
|
|
160
175
|
// A number cannot parse as S.string → logged and skipped; the following
|
|
161
176
|
// record still projects.
|
|
@@ -164,4 +179,56 @@ describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () => {
|
|
|
164
179
|
->Effect.runPromise
|
|
165
180
|
expect(saved)->toEqual([("row-after", str(""))])
|
|
166
181
|
})
|
|
182
|
+
|
|
183
|
+
// The deployed slice assembles JSON-level operations instead of going through
|
|
184
|
+
// the typed rewrite, so the trail has to be appended on this path too — the
|
|
185
|
+
// same gap `withDisplayName` was written to close.
|
|
186
|
+
test("appends a trail entry from the envelope's producer time", async () => {
|
|
187
|
+
let saved = []
|
|
188
|
+
let handler = StateViewSliceEntryPoint_Ops.makeJsonEventsHandler(
|
|
189
|
+
~sliceName="TrailSlice",
|
|
190
|
+
~eventSchema=S.string,
|
|
191
|
+
~project=({event, _}) => [
|
|
192
|
+
Set(event, obj([("lifecycle", str("Placed")), ("trail", JSON.Encode.array([]))])),
|
|
193
|
+
],
|
|
194
|
+
~queryDbOps=makeStub(saved),
|
|
195
|
+
~subIdConfig=None,
|
|
196
|
+
~stateSchema=Some(trailedStateSchema->S.castToUnknown),
|
|
197
|
+
)
|
|
198
|
+
await Stream.fromIterable([
|
|
199
|
+
obj([("event", str("row-1")), ("meta", obj([("time", str("2026-03-02T09:00:00Z"))]))]),
|
|
200
|
+
])
|
|
201
|
+
->handler
|
|
202
|
+
->Effect.runPromise
|
|
203
|
+
expect(saved)->toEqual([
|
|
204
|
+
(
|
|
205
|
+
"row-1",
|
|
206
|
+
obj([
|
|
207
|
+
("lifecycle", str("Placed")),
|
|
208
|
+
("trail", JSON.Encode.array([obj([("state", str("Placed")), ("at", str("2026-03-02T09:00:00Z"))])])),
|
|
209
|
+
]),
|
|
210
|
+
),
|
|
211
|
+
])
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Without a producer time there is no instant to stamp, so the row is written
|
|
215
|
+
// as the projection produced it rather than with an entry the read path would
|
|
216
|
+
// refuse.
|
|
217
|
+
test("appends nothing when the envelope carries no producer time", async () => {
|
|
218
|
+
let saved = []
|
|
219
|
+
let handler = StateViewSliceEntryPoint_Ops.makeJsonEventsHandler(
|
|
220
|
+
~sliceName="TrailSlice",
|
|
221
|
+
~eventSchema=S.string,
|
|
222
|
+
~project=({event, _}) => [
|
|
223
|
+
Set(event, obj([("lifecycle", str("Placed")), ("trail", JSON.Encode.array([]))])),
|
|
224
|
+
],
|
|
225
|
+
~queryDbOps=makeStub(saved),
|
|
226
|
+
~subIdConfig=None,
|
|
227
|
+
~stateSchema=Some(trailedStateSchema->S.castToUnknown),
|
|
228
|
+
)
|
|
229
|
+
await Stream.fromIterable([obj([("event", str("row-1"))])])->handler->Effect.runPromise
|
|
230
|
+
expect(saved)->toEqual([
|
|
231
|
+
("row-1", obj([("lifecycle", str("Placed")), ("trail", JSON.Encode.array([]))])),
|
|
232
|
+
])
|
|
233
|
+
})
|
|
167
234
|
})
|
|
@@ -4,6 +4,7 @@ import * as Sury from "sury";
|
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Effect from "effect/Effect";
|
|
6
6
|
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as Lifecycle$Reventless from "@reventlessdev/reventless-spec/src/types/Lifecycle.res.mjs";
|
|
7
8
|
import * as StateViewSliceEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs";
|
|
8
9
|
|
|
9
10
|
function str(prim) {
|
|
@@ -14,6 +15,16 @@ function obj(pairs) {
|
|
|
14
15
|
return Object.fromEntries(pairs);
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
let trailedLifecycleSchema = Sury.union([
|
|
19
|
+
Sury.literal("Placed"),
|
|
20
|
+
Sury.literal("Shipped")
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
let trailedStateSchema = Sury.$schema(s => ({
|
|
24
|
+
lifecycle: s.m(trailedLifecycleSchema),
|
|
25
|
+
trail: s.m(Lifecycle$Reventless.Trail.schema(trailedLifecycleSchema))
|
|
26
|
+
}));
|
|
27
|
+
|
|
17
28
|
globalThis.describe("StateViewSliceEntryPoint_Ops.parseHandlerConfig", () => {
|
|
18
29
|
globalThis.test("empty raw config yields no handlers", () => {
|
|
19
30
|
globalThis.expect(StateViewSliceEntryPoint_Ops$ReventlessAws.parseHandlerConfig("").length).toBe(0);
|
|
@@ -249,7 +260,7 @@ globalThis.describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () =>
|
|
|
249
260
|
}];
|
|
250
261
|
globalThis.test("decodes envelopes, projects, and runs actions against the ops", async () => {
|
|
251
262
|
let saved = [];
|
|
252
|
-
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TestSlice", Sury.string, project, makeStub(saved), undefined);
|
|
263
|
+
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TestSlice", Sury.string, project, makeStub(saved), undefined, undefined);
|
|
253
264
|
let envelope = Object.fromEntries([
|
|
254
265
|
[
|
|
255
266
|
"event",
|
|
@@ -277,7 +288,7 @@ globalThis.describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () =>
|
|
|
277
288
|
});
|
|
278
289
|
globalThis.test("a decode failure yields no actions and does not wedge the batch", async () => {
|
|
279
290
|
let saved = [];
|
|
280
|
-
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TestSlice", Sury.string, project, makeStub(saved), undefined);
|
|
291
|
+
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TestSlice", Sury.string, project, makeStub(saved), undefined, undefined);
|
|
281
292
|
await Effect.runPromise(handler(Stream.fromIterable([
|
|
282
293
|
42,
|
|
283
294
|
"row-after"
|
|
@@ -287,10 +298,98 @@ globalThis.describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () =>
|
|
|
287
298
|
""
|
|
288
299
|
]]);
|
|
289
300
|
});
|
|
301
|
+
globalThis.test("appends a trail entry from the envelope's producer time", async () => {
|
|
302
|
+
let saved = [];
|
|
303
|
+
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TrailSlice", Sury.string, param => [{
|
|
304
|
+
TAG: "Set",
|
|
305
|
+
_0: param.event,
|
|
306
|
+
_1: Object.fromEntries([
|
|
307
|
+
[
|
|
308
|
+
"lifecycle",
|
|
309
|
+
"Placed"
|
|
310
|
+
],
|
|
311
|
+
[
|
|
312
|
+
"trail",
|
|
313
|
+
[]
|
|
314
|
+
]
|
|
315
|
+
])
|
|
316
|
+
}], makeStub(saved), undefined, trailedStateSchema);
|
|
317
|
+
await Effect.runPromise(handler(Stream.fromIterable([Object.fromEntries([
|
|
318
|
+
[
|
|
319
|
+
"event",
|
|
320
|
+
"row-1"
|
|
321
|
+
],
|
|
322
|
+
[
|
|
323
|
+
"meta",
|
|
324
|
+
Object.fromEntries([[
|
|
325
|
+
"time",
|
|
326
|
+
"2026-03-02T09:00:00Z"
|
|
327
|
+
]])
|
|
328
|
+
]
|
|
329
|
+
])])));
|
|
330
|
+
globalThis.expect(saved).toEqual([[
|
|
331
|
+
"row-1",
|
|
332
|
+
Object.fromEntries([
|
|
333
|
+
[
|
|
334
|
+
"lifecycle",
|
|
335
|
+
"Placed"
|
|
336
|
+
],
|
|
337
|
+
[
|
|
338
|
+
"trail",
|
|
339
|
+
[Object.fromEntries([
|
|
340
|
+
[
|
|
341
|
+
"state",
|
|
342
|
+
"Placed"
|
|
343
|
+
],
|
|
344
|
+
[
|
|
345
|
+
"at",
|
|
346
|
+
"2026-03-02T09:00:00Z"
|
|
347
|
+
]
|
|
348
|
+
])]
|
|
349
|
+
]
|
|
350
|
+
])
|
|
351
|
+
]]);
|
|
352
|
+
});
|
|
353
|
+
globalThis.test("appends nothing when the envelope carries no producer time", async () => {
|
|
354
|
+
let saved = [];
|
|
355
|
+
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TrailSlice", Sury.string, param => [{
|
|
356
|
+
TAG: "Set",
|
|
357
|
+
_0: param.event,
|
|
358
|
+
_1: Object.fromEntries([
|
|
359
|
+
[
|
|
360
|
+
"lifecycle",
|
|
361
|
+
"Placed"
|
|
362
|
+
],
|
|
363
|
+
[
|
|
364
|
+
"trail",
|
|
365
|
+
[]
|
|
366
|
+
]
|
|
367
|
+
])
|
|
368
|
+
}], makeStub(saved), undefined, trailedStateSchema);
|
|
369
|
+
await Effect.runPromise(handler(Stream.fromIterable([Object.fromEntries([[
|
|
370
|
+
"event",
|
|
371
|
+
"row-1"
|
|
372
|
+
]])])));
|
|
373
|
+
globalThis.expect(saved).toEqual([[
|
|
374
|
+
"row-1",
|
|
375
|
+
Object.fromEntries([
|
|
376
|
+
[
|
|
377
|
+
"lifecycle",
|
|
378
|
+
"Placed"
|
|
379
|
+
],
|
|
380
|
+
[
|
|
381
|
+
"trail",
|
|
382
|
+
[]
|
|
383
|
+
]
|
|
384
|
+
])
|
|
385
|
+
]]);
|
|
386
|
+
});
|
|
290
387
|
});
|
|
291
388
|
|
|
292
389
|
export {
|
|
293
390
|
str,
|
|
294
391
|
obj,
|
|
392
|
+
trailedLifecycleSchema,
|
|
393
|
+
trailedStateSchema,
|
|
295
394
|
}
|
|
296
|
-
/*
|
|
395
|
+
/* trailedLifecycleSchema Not a pure module */
|