@reventlessdev/reventless-aws 3.0.0-alpha.316 → 3.0.0-alpha.317
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 +7 -0
- package/package.json +5 -5
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +31 -0
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +26 -1
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +1 -0
- package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +5 -1
- package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +1 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +1 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +3 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +1 -1
- package/tests/ProjectionEntryPoint_UnionStampTest.res +88 -0
- package/tests/ProjectionEntryPoint_UnionStampTest.res.mjs +137 -0
- package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +16 -0
- package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +6 -0
- package/tests/integration/SvsTestSlice.res +10 -0
- package/tests/integration/SvsTestSlice.res.mjs +17 -1
- package/tests/integration/SvsTestSlice_Projection.res +3 -1
- package/tests/integration/SvsTestSlice_Projection.res.mjs +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.317 (2026-08-20)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** stamp union member types on the deployed projection path ([140251a](https://github.com/ReventlessDev/reventless-core/commit/140251ac7c18f484be32e8683a725f6fac2b9519))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.316 (2026-08-20)
|
|
7
14
|
|
|
8
15
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
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.317",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -14,16 +14,16 @@
|
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
15
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
|
|
16
16
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
17
18
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
19
|
"@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.1",
|
|
19
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
21
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
21
22
|
"@reventlessdev/reventless-core": "3.0.0-alpha.245",
|
|
22
23
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.148",
|
|
23
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.120",
|
|
24
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.109",
|
|
25
24
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.32",
|
|
26
|
-
"@reventlessdev/
|
|
25
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.109",
|
|
26
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.120"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
|
@@ -55,6 +55,37 @@ external withLiveUpdates: (
|
|
|
55
55
|
liveConfig,
|
|
56
56
|
) => ReventlessCore.QueryDb_Adapter.operations = "withLiveUpdates"
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
Stamps every union value in a saved row with the GraphQL member type it is.
|
|
60
|
+
|
|
61
|
+
`QueryDb_Operations.Make` does this for the typed path — the local platform and
|
|
62
|
+
the storage tests — but the deployed projection Lambdas do not go through that
|
|
63
|
+
functor: they assemble JSON-level operations here. Without this the stamp never
|
|
64
|
+
ran on AWS, every union member resolved to null, and the null took its
|
|
65
|
+
non-nullable parent, so rows vanished from a view rather than erroring.
|
|
66
|
+
|
|
67
|
+
`stateSchema` is `None` for a spec module that predates it, which leaves rows
|
|
68
|
+
exactly as they were.
|
|
69
|
+
*/
|
|
70
|
+
let withUnionMemberTypes = (
|
|
71
|
+
base: ReventlessCore.QueryDb_Adapter.operations,
|
|
72
|
+
~stateSchema: option<S.t<unknown>>,
|
|
73
|
+
): ReventlessCore.QueryDb_Adapter.operations =>
|
|
74
|
+
switch stateSchema {
|
|
75
|
+
| None => base
|
|
76
|
+
| Some(schema) =>
|
|
77
|
+
let stamp = (state: JSON.t) => {
|
|
78
|
+
Reventless.TaggedUnion.stampInto(~schema, state)
|
|
79
|
+
state
|
|
80
|
+
}
|
|
81
|
+
{
|
|
82
|
+
...base,
|
|
83
|
+
save: (id, state, saveMode, ttl) => base.save(id, stamp(state), saveMode, ttl),
|
|
84
|
+
saveBatch: items =>
|
|
85
|
+
base.saveBatch(items->Array.map(((id, state, ttl)) => (id, stamp(state), ttl))),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
let makeQueryDbOps = (
|
|
59
90
|
~queryDbTableName: string,
|
|
60
91
|
~pgConnection: option<PgConnection.connectionConfig>,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
|
|
4
5
|
import * as StateTopicPublishMjs from "./StateTopicPublish.mjs";
|
|
5
6
|
import * as QueryDbEntryPoint_Ops$ReventlessAws from "./QueryDbEntryPoint_Ops.res.mjs";
|
|
6
7
|
import * as QueryDbStorage_Postgres_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs";
|
|
@@ -17,6 +18,29 @@ function withLiveUpdates(prim0, prim1) {
|
|
|
17
18
|
return StateTopicPublishMjs.withLiveUpdates(prim0, prim1);
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function withUnionMemberTypes(base, stateSchema) {
|
|
22
|
+
if (stateSchema !== undefined) {
|
|
23
|
+
return {
|
|
24
|
+
load: base.load,
|
|
25
|
+
loadStream: base.loadStream,
|
|
26
|
+
save: (id, state, saveMode, ttl) => base.save(id, (TaggedUnion$Reventless.stampInto(stateSchema, state), state), saveMode, ttl),
|
|
27
|
+
saveBatch: items => base.saveBatch(items.map(param => {
|
|
28
|
+
let state = param[1];
|
|
29
|
+
return [
|
|
30
|
+
param[0],
|
|
31
|
+
(TaggedUnion$Reventless.stampInto(stateSchema, state), state),
|
|
32
|
+
param[2]
|
|
33
|
+
];
|
|
34
|
+
})),
|
|
35
|
+
count: base.count,
|
|
36
|
+
delete: base.delete,
|
|
37
|
+
deleteBatch: base.deleteBatch
|
|
38
|
+
};
|
|
39
|
+
} else {
|
|
40
|
+
return base;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
20
44
|
function makeQueryDbOps(queryDbTableName, pgConnection, stateTopicName, indexes, subIdField) {
|
|
21
45
|
if (pgConnection !== undefined) {
|
|
22
46
|
return StateTopicPublishMjs.withLiveUpdates(QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(pgConnection, queryDbTableName, indexes, subIdField), {
|
|
@@ -34,6 +58,7 @@ export {
|
|
|
34
58
|
indexesOf,
|
|
35
59
|
subIdFieldOf,
|
|
36
60
|
withLiveUpdates,
|
|
61
|
+
withUnionMemberTypes,
|
|
37
62
|
makeQueryDbOps,
|
|
38
63
|
}
|
|
39
|
-
/*
|
|
64
|
+
/* TaggedUnion-Reventless Not a pure module */
|
|
@@ -47,6 +47,7 @@ async function buildAllHandlers() {
|
|
|
47
47
|
const operations = Ops.buildOperations(entry, {
|
|
48
48
|
config: specModule.config,
|
|
49
49
|
subIdConfig: specModule.subIdConfig,
|
|
50
|
+
stateSchema: specModule.stateSchema,
|
|
50
51
|
});
|
|
51
52
|
const callback = readModelCallbackMake(specModule)(mappingsModule)({
|
|
52
53
|
ReadModelSpec: specModule,
|
|
@@ -90,6 +90,8 @@ let withInjectedId = (
|
|
|
90
90
|
type specInfo = {
|
|
91
91
|
config: option<ProjectionEntryPoint_Ops.specConfig>,
|
|
92
92
|
subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
|
|
93
|
+
// Read off the runtime-loaded spec module; `None` on a module that predates it.
|
|
94
|
+
stateSchema: option<S.t<unknown>>,
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
let buildOperations = (
|
|
@@ -102,7 +104,9 @@ let buildOperations = (
|
|
|
102
104
|
~stateTopicName=entry.stateTopicName,
|
|
103
105
|
~indexes=ProjectionEntryPoint_Ops.indexesOf(spec.config),
|
|
104
106
|
~subIdField=ProjectionEntryPoint_Ops.subIdFieldOf(spec.subIdConfig),
|
|
105
|
-
)
|
|
107
|
+
)
|
|
108
|
+
->ProjectionEntryPoint_Ops.withUnionMemberTypes(~stateSchema=spec.stateSchema)
|
|
109
|
+
->withInjectedId
|
|
106
110
|
|
|
107
111
|
let makeRegisteredHandler = (
|
|
108
112
|
entry: handlerEntry,
|
|
@@ -60,7 +60,7 @@ function withInjectedId(base) {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function buildOperations(entry, spec) {
|
|
63
|
-
return withInjectedId(ProjectionEntryPoint_Ops$ReventlessAws.makeQueryDbOps(entry.queryDbTableName, entry.pgConnection, entry.stateTopicName, ProjectionEntryPoint_Ops$ReventlessAws.indexesOf(spec.config), ProjectionEntryPoint_Ops$ReventlessAws.subIdFieldOf(spec.subIdConfig)));
|
|
63
|
+
return withInjectedId(ProjectionEntryPoint_Ops$ReventlessAws.withUnionMemberTypes(ProjectionEntryPoint_Ops$ReventlessAws.makeQueryDbOps(entry.queryDbTableName, entry.pgConnection, entry.stateTopicName, ProjectionEntryPoint_Ops$ReventlessAws.indexesOf(spec.config), ProjectionEntryPoint_Ops$ReventlessAws.subIdFieldOf(spec.subIdConfig)), spec.stateSchema));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function makeRegisteredHandler(entry, handleJsonEvents) {
|
|
@@ -176,6 +176,8 @@ type sliceModules<'event> = {
|
|
|
176
176
|
>,
|
|
177
177
|
config: option<ProjectionEntryPoint_Ops.specConfig>,
|
|
178
178
|
subIdConfig: option<Reventless.ReadModel.subIdConfig<JSON.t>>,
|
|
179
|
+
// Same union stamp the read-model path needs; a slice's state can hold one too.
|
|
180
|
+
stateSchema: option<S.t<unknown>>,
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
let makeRegisteredHandler = (
|
|
@@ -188,7 +190,7 @@ let makeRegisteredHandler = (
|
|
|
188
190
|
~stateTopicName=entry.stateTopicName,
|
|
189
191
|
~indexes=ProjectionEntryPoint_Ops.indexesOf(modules.config),
|
|
190
192
|
~subIdField=ProjectionEntryPoint_Ops.subIdFieldOf(modules.subIdConfig),
|
|
191
|
-
)
|
|
193
|
+
)->ProjectionEntryPoint_Ops.withUnionMemberTypes(~stateSchema=modules.stateSchema)
|
|
192
194
|
{
|
|
193
195
|
handler: StreamRoutedEntryPoint_Ops.toStreamHandler(
|
|
194
196
|
makeJsonEventsHandler(
|
|
@@ -84,7 +84,7 @@ function makeJsonEventsHandler(sliceName, eventSchema, project, queryDbOps, subI
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function makeRegisteredHandler(entry, modules) {
|
|
87
|
-
let queryDbOps = ProjectionEntryPoint_Ops$ReventlessAws.makeQueryDbOps(entry.queryDbTableName, entry.pgConnection, entry.stateTopicName, ProjectionEntryPoint_Ops$ReventlessAws.indexesOf(modules.config), ProjectionEntryPoint_Ops$ReventlessAws.subIdFieldOf(modules.subIdConfig));
|
|
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);
|
|
88
88
|
return {
|
|
89
89
|
handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeJsonEventsHandler(modules.name, modules.consumedEventSchema, modules.project, queryDbOps, modules.subIdConfig)),
|
|
90
90
|
comp: `StateViewSlice(` + modules.name + `)`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The deployed projection Lambdas assemble JSON-level QueryDb operations rather
|
|
2
|
+
// than going through `QueryDb_Operations.Make`, so the union `__typename` stamp
|
|
3
|
+
// that functor applies never ran on AWS. Every union member resolved to null and
|
|
4
|
+
// took its non-nullable parent with it — rows vanished from a view instead of
|
|
5
|
+
// erroring, on every read door, with nothing reporting it.
|
|
6
|
+
//
|
|
7
|
+
// The typed path was covered from the start; this side was not, which is the
|
|
8
|
+
// whole reason it shipped.
|
|
9
|
+
|
|
10
|
+
open JestGlobals
|
|
11
|
+
|
|
12
|
+
@schema
|
|
13
|
+
type geolocation =
|
|
14
|
+
| Pending({requestedFor: string})
|
|
15
|
+
| Located({lat: float, lng: float})
|
|
16
|
+
|
|
17
|
+
let geolocationSchema = Reventless.TaggedUnion.named(~name="Geolocation", geolocationSchema)
|
|
18
|
+
|
|
19
|
+
@schema
|
|
20
|
+
type rowState = {customerId: string, geolocation: geolocation}
|
|
21
|
+
|
|
22
|
+
// A recording stand-in for the storage the runtime would wrap.
|
|
23
|
+
let capture = (): (ReventlessCore.QueryDb_Adapter.operations, array<JSON.t>) => {
|
|
24
|
+
let saved: array<JSON.t> = []
|
|
25
|
+
let base: ReventlessCore.QueryDb_Adapter.operations = {
|
|
26
|
+
load: _ => Promise.resolve(Ok([])),
|
|
27
|
+
loadStream: _ => Stream.empty,
|
|
28
|
+
save: (_id, state, _saveMode, _ttl) => {
|
|
29
|
+
saved->Array.push(state)
|
|
30
|
+
Promise.resolve(Ok())
|
|
31
|
+
},
|
|
32
|
+
saveBatch: items => {
|
|
33
|
+
items->Array.forEach(((_, state, _)) => saved->Array.push(state))
|
|
34
|
+
Promise.resolve(Ok())
|
|
35
|
+
},
|
|
36
|
+
count: (_, _, _) => Promise.resolve(Ok(0)),
|
|
37
|
+
delete: (_, _) => Promise.resolve(Ok()),
|
|
38
|
+
deleteBatch: _ => Promise.resolve(Ok()),
|
|
39
|
+
}
|
|
40
|
+
(base, saved)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let memberOf = (json: JSON.t) =>
|
|
44
|
+
json
|
|
45
|
+
->JSON.Decode.object
|
|
46
|
+
->Option.flatMap(o => o->Dict.get("geolocation"))
|
|
47
|
+
->Option.flatMap(JSON.Decode.object)
|
|
48
|
+
->Option.flatMap(o => o->Dict.get("__typename"))
|
|
49
|
+
->Option.flatMap(JSON.Decode.string)
|
|
50
|
+
|
|
51
|
+
let row = (): JSON.t =>
|
|
52
|
+
({customerId: "c1", geolocation: Located({lat: 1.0, lng: 2.0})}: rowState)
|
|
53
|
+
->ReventlessCore.Message.encode(rowStateSchema)
|
|
54
|
+
|
|
55
|
+
describe("ProjectionEntryPoint_Ops.withUnionMemberTypes", () => {
|
|
56
|
+
testPromise("stamps the member type on save", async () => {
|
|
57
|
+
let (base, saved) = capture()
|
|
58
|
+
let ops =
|
|
59
|
+
base->ProjectionEntryPoint_Ops.withUnionMemberTypes(
|
|
60
|
+
~stateSchema=Some(rowStateSchema->S.castToUnknown),
|
|
61
|
+
)
|
|
62
|
+
let _ = await ops.save("c1", row(), ReventlessCore.QueryDb.Any, None)
|
|
63
|
+
expect(saved->Array.get(0)->Option.flatMap(memberOf))->toEqual(Some("GeolocationLocated"))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// saveBatch is the path a multi-event projection takes, and it was missing the
|
|
67
|
+
// stamp for the same reason save was.
|
|
68
|
+
testPromise("stamps every row in a batch", async () => {
|
|
69
|
+
let (base, saved) = capture()
|
|
70
|
+
let ops =
|
|
71
|
+
base->ProjectionEntryPoint_Ops.withUnionMemberTypes(
|
|
72
|
+
~stateSchema=Some(rowStateSchema->S.castToUnknown),
|
|
73
|
+
)
|
|
74
|
+
let _ = await ops.saveBatch([("c1", row(), None), ("c2", row(), None)])
|
|
75
|
+
expect(saved->Array.map(memberOf))->toEqual([
|
|
76
|
+
Some("GeolocationLocated"),
|
|
77
|
+
Some("GeolocationLocated"),
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// A spec module without a stateSchema leaves rows untouched rather than throwing.
|
|
82
|
+
testPromise("passes rows through when the spec has no schema", async () => {
|
|
83
|
+
let (base, saved) = capture()
|
|
84
|
+
let ops = base->ProjectionEntryPoint_Ops.withUnionMemberTypes(~stateSchema=None)
|
|
85
|
+
let _ = await ops.save("c1", row(), ReventlessCore.QueryDb.Any, None)
|
|
86
|
+
expect(saved->Array.get(0)->Option.flatMap(memberOf))->toEqual(None)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
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 Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
8
|
+
import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
|
|
9
|
+
import * as ProjectionEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs";
|
|
10
|
+
|
|
11
|
+
let geolocationSchema = Sury.union([
|
|
12
|
+
Sury.$schema(s => ({
|
|
13
|
+
TAG: "Pending",
|
|
14
|
+
requestedFor: s.m(Sury.string)
|
|
15
|
+
})),
|
|
16
|
+
Sury.$schema(s => ({
|
|
17
|
+
TAG: "Located",
|
|
18
|
+
lat: s.m(Sury.float),
|
|
19
|
+
lng: s.m(Sury.float)
|
|
20
|
+
}))
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
let geolocationSchema$1 = TaggedUnion$Reventless.named("Geolocation", geolocationSchema);
|
|
24
|
+
|
|
25
|
+
let rowStateSchema = Sury.$schema(s => ({
|
|
26
|
+
customerId: s.m(Sury.string),
|
|
27
|
+
geolocation: s.m(geolocationSchema$1)
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
function capture() {
|
|
31
|
+
let saved = [];
|
|
32
|
+
let base_load = param => Promise.resolve({
|
|
33
|
+
TAG: "Ok",
|
|
34
|
+
_0: []
|
|
35
|
+
});
|
|
36
|
+
let base_loadStream = param => Stream.empty;
|
|
37
|
+
let base_save = (_id, state, _saveMode, _ttl) => {
|
|
38
|
+
saved.push(state);
|
|
39
|
+
return Promise.resolve({
|
|
40
|
+
TAG: "Ok",
|
|
41
|
+
_0: undefined
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
let base_saveBatch = items => {
|
|
45
|
+
items.forEach(param => {
|
|
46
|
+
saved.push(param[1]);
|
|
47
|
+
});
|
|
48
|
+
return Promise.resolve({
|
|
49
|
+
TAG: "Ok",
|
|
50
|
+
_0: undefined
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
let base_count = (param, param$1, param$2) => Promise.resolve({
|
|
54
|
+
TAG: "Ok",
|
|
55
|
+
_0: 0
|
|
56
|
+
});
|
|
57
|
+
let base_delete = (param, param$1) => Promise.resolve({
|
|
58
|
+
TAG: "Ok",
|
|
59
|
+
_0: undefined
|
|
60
|
+
});
|
|
61
|
+
let base_deleteBatch = param => Promise.resolve({
|
|
62
|
+
TAG: "Ok",
|
|
63
|
+
_0: undefined
|
|
64
|
+
});
|
|
65
|
+
let base = {
|
|
66
|
+
load: base_load,
|
|
67
|
+
loadStream: base_loadStream,
|
|
68
|
+
save: base_save,
|
|
69
|
+
saveBatch: base_saveBatch,
|
|
70
|
+
count: base_count,
|
|
71
|
+
delete: base_delete,
|
|
72
|
+
deleteBatch: base_deleteBatch
|
|
73
|
+
};
|
|
74
|
+
return [
|
|
75
|
+
base,
|
|
76
|
+
saved
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function memberOf(json) {
|
|
81
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => o["geolocation"]), Stdlib_JSON.Decode.object), o => o["__typename"]), Stdlib_JSON.Decode.string);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function row() {
|
|
85
|
+
return Message$ReventlessCore.encode({
|
|
86
|
+
customerId: "c1",
|
|
87
|
+
geolocation: {
|
|
88
|
+
TAG: "Located",
|
|
89
|
+
lat: 1.0,
|
|
90
|
+
lng: 2.0
|
|
91
|
+
}
|
|
92
|
+
}, rowStateSchema);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
globalThis.describe("ProjectionEntryPoint_Ops.withUnionMemberTypes", () => {
|
|
96
|
+
globalThis.test("stamps the member type on save", async () => {
|
|
97
|
+
let match = capture();
|
|
98
|
+
let ops = ProjectionEntryPoint_Ops$ReventlessAws.withUnionMemberTypes(match[0], rowStateSchema);
|
|
99
|
+
await ops.save("c1", row(), "Any", undefined);
|
|
100
|
+
globalThis.expect(Stdlib_Option.flatMap(match[1][0], memberOf)).toEqual("GeolocationLocated");
|
|
101
|
+
});
|
|
102
|
+
globalThis.test("stamps every row in a batch", async () => {
|
|
103
|
+
let match = capture();
|
|
104
|
+
let ops = ProjectionEntryPoint_Ops$ReventlessAws.withUnionMemberTypes(match[0], rowStateSchema);
|
|
105
|
+
await ops.saveBatch([
|
|
106
|
+
[
|
|
107
|
+
"c1",
|
|
108
|
+
row(),
|
|
109
|
+
undefined
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"c2",
|
|
113
|
+
row(),
|
|
114
|
+
undefined
|
|
115
|
+
]
|
|
116
|
+
]);
|
|
117
|
+
globalThis.expect(match[1].map(memberOf)).toEqual([
|
|
118
|
+
"GeolocationLocated",
|
|
119
|
+
"GeolocationLocated"
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("passes rows through when the spec has no schema", async () => {
|
|
123
|
+
let match = capture();
|
|
124
|
+
let ops = ProjectionEntryPoint_Ops$ReventlessAws.withUnionMemberTypes(match[0], undefined);
|
|
125
|
+
await ops.save("c1", row(), "Any", undefined);
|
|
126
|
+
globalThis.expect(Stdlib_Option.flatMap(match[1][0], memberOf)).toEqual(undefined);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
geolocationSchema$1 as geolocationSchema,
|
|
132
|
+
rowStateSchema,
|
|
133
|
+
capture,
|
|
134
|
+
memberOf,
|
|
135
|
+
row,
|
|
136
|
+
}
|
|
137
|
+
/* geolocationSchema Not a pure module */
|
|
@@ -85,6 +85,7 @@ let runOneEvent: (string, JSON.t) => promise<unit> = %raw(`
|
|
|
85
85
|
project: projectionModule.project,
|
|
86
86
|
config: specModule.config,
|
|
87
87
|
subIdConfig: specModule.subIdConfig,
|
|
88
|
+
stateSchema: specModule.stateSchema,
|
|
88
89
|
},
|
|
89
90
|
);
|
|
90
91
|
const event = {
|
|
@@ -146,6 +147,21 @@ describe("StateViewSliceEntryPoint integration", () => {
|
|
|
146
147
|
->Array.toSorted(String.compare)
|
|
147
148
|
expect(productIds)->toEqual(["prod-a", "prod-b"])
|
|
148
149
|
|
|
150
|
+
// The runtime path's union stamp, against real DynamoDB. The deployed
|
|
151
|
+
// Lambdas assemble JSON-level ops and skipped it entirely, so a member
|
|
152
|
+
// resolved to null and took its non-nullable parent with it.
|
|
153
|
+
let members =
|
|
154
|
+
rows
|
|
155
|
+
->Array.filterMap(row =>
|
|
156
|
+
row
|
|
157
|
+
->JSON.Decode.object
|
|
158
|
+
->Option.flatMap(o => o->Dict.get("fulfilment"))
|
|
159
|
+
->Option.flatMap(JSON.Decode.object)
|
|
160
|
+
->Option.flatMap(o => o->Dict.get("__typename"))
|
|
161
|
+
->Option.flatMap(JSON.Decode.string)
|
|
162
|
+
)
|
|
163
|
+
expect(members)->toEqual(["FulfilmentShipped", "FulfilmentShipped"])
|
|
164
|
+
|
|
149
165
|
await H.deleteTable(table)
|
|
150
166
|
},
|
|
151
167
|
)
|
|
@@ -94,6 +94,7 @@ let runOneEvent = (async (tableName, eventJson) => {
|
|
|
94
94
|
project: projectionModule.project,
|
|
95
95
|
config: specModule.config,
|
|
96
96
|
subIdConfig: specModule.subIdConfig,
|
|
97
|
+
stateSchema: specModule.stateSchema,
|
|
97
98
|
},
|
|
98
99
|
);
|
|
99
100
|
const event = {
|
|
@@ -138,6 +139,11 @@ globalThis.describe("StateViewSliceEntryPoint integration", () => {
|
|
|
138
139
|
"prod-a",
|
|
139
140
|
"prod-b"
|
|
140
141
|
]);
|
|
142
|
+
let members = Stdlib_Array.filterMap(rows$1, row => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(row), o => o["fulfilment"]), Stdlib_JSON.Decode.object), o => o["__typename"]), Stdlib_JSON.Decode.string));
|
|
143
|
+
globalThis.expect(members).toEqual([
|
|
144
|
+
"FulfilmentShipped",
|
|
145
|
+
"FulfilmentShipped"
|
|
146
|
+
]);
|
|
141
147
|
return await DcbIntegrationHarness$ReventlessAws.deleteTable(table);
|
|
142
148
|
});
|
|
143
149
|
});
|
|
@@ -16,11 +16,21 @@
|
|
|
16
16
|
@schema
|
|
17
17
|
type consumedEvent = ItemAddedToCart({cartId: string, productId: string, qty: int})
|
|
18
18
|
|
|
19
|
+
// A union on the state, so the integration test covers the runtime path's
|
|
20
|
+
// `__typename` stamp — the one the deployed Lambdas skipped entirely.
|
|
21
|
+
@schema
|
|
22
|
+
type fulfilment =
|
|
23
|
+
| Pending({requestedFor: string})
|
|
24
|
+
| Shipped({carrier: string})
|
|
25
|
+
|
|
26
|
+
let fulfilmentSchema = Reventless.TaggedUnion.named(~name="Fulfilment", fulfilmentSchema)
|
|
27
|
+
|
|
19
28
|
@schema
|
|
20
29
|
type state = {
|
|
21
30
|
cartId: string,
|
|
22
31
|
productId: string,
|
|
23
32
|
qty: int,
|
|
33
|
+
fulfilment: fulfilment,
|
|
24
34
|
}
|
|
25
35
|
|
|
26
36
|
// `Some({subIdField, getSubId})` — the compiled per-slice spec that a real
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
|
+
import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
|
|
4
5
|
|
|
5
6
|
let consumedEventSchema = Sury.$schema(s => ({
|
|
6
7
|
TAG: "ItemAddedToCart",
|
|
@@ -9,10 +10,24 @@ let consumedEventSchema = Sury.$schema(s => ({
|
|
|
9
10
|
qty: s.m(Sury.int)
|
|
10
11
|
}));
|
|
11
12
|
|
|
13
|
+
let fulfilmentSchema = Sury.union([
|
|
14
|
+
Sury.$schema(s => ({
|
|
15
|
+
TAG: "Pending",
|
|
16
|
+
requestedFor: s.m(Sury.string)
|
|
17
|
+
})),
|
|
18
|
+
Sury.$schema(s => ({
|
|
19
|
+
TAG: "Shipped",
|
|
20
|
+
carrier: s.m(Sury.string)
|
|
21
|
+
}))
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
let fulfilmentSchema$1 = TaggedUnion$Reventless.named("Fulfilment", fulfilmentSchema);
|
|
25
|
+
|
|
12
26
|
let stateSchema = Sury.$schema(s => ({
|
|
13
27
|
cartId: s.m(Sury.string),
|
|
14
28
|
productId: s.m(Sury.string),
|
|
15
|
-
qty: s.m(Sury.int)
|
|
29
|
+
qty: s.m(Sury.int),
|
|
30
|
+
fulfilment: s.m(fulfilmentSchema$1)
|
|
16
31
|
}));
|
|
17
32
|
|
|
18
33
|
let subIdConfig = {
|
|
@@ -22,6 +37,7 @@ let subIdConfig = {
|
|
|
22
37
|
|
|
23
38
|
export {
|
|
24
39
|
consumedEventSchema,
|
|
40
|
+
fulfilmentSchema$1 as fulfilmentSchema,
|
|
25
41
|
stateSchema,
|
|
26
42
|
subIdConfig,
|
|
27
43
|
}
|
|
@@ -17,7 +17,9 @@ let project = ({event}: Reventless.StateViewSlice.consumed<consumedEvent>) =>
|
|
|
17
17
|
cartId,
|
|
18
18
|
states => {
|
|
19
19
|
let others = states->Array.filter(s => s.productId != productId)
|
|
20
|
-
others->Array.concat([
|
|
20
|
+
others->Array.concat([
|
|
21
|
+
{cartId, productId, qty, fulfilment: Shipped({carrier: "dhl"})},
|
|
22
|
+
])
|
|
21
23
|
},
|
|
22
24
|
),
|
|
23
25
|
]
|