@reventlessdev/reventless-aws 3.0.0-alpha.232 → 3.0.0-alpha.234
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 +23 -0
- package/package.json +7 -7
- package/src/adapter/Api/AppSyncEventsSigner_Ops.res +137 -0
- package/src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs +121 -0
- package/src/adapter/Api/Platform_AdminScan_Ops.res +136 -0
- package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +98 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +12 -90
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -84
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +86 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +79 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +9 -79
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +7 -143
- package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +48 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs +63 -0
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +21 -80
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +23 -68
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +70 -0
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res.mjs +68 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +42 -126
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +246 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +145 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +27 -2
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +10 -3
- package/src/adapter/Runtime/CounterEntryPoint.mjs +19 -73
- package/src/adapter/Runtime/CounterEntryPoint_Ops.res +145 -0
- package/src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs +129 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +14 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +22 -2
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +15 -4
- package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
- package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
- package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
- package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
- package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
- package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
- package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
- package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
- package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
- package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
- package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
- package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +26 -6
- package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
- package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
- package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +5 -2
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
- package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
- package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
- package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
- package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
- package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
- package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
- package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
- package/src/components/AutomationSlice_Builder.res +2 -0
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res +1 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +13 -4
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +1 -1
- package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
- package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
- package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
- package/tests/CounterEntryPoint_OpsTest.res +115 -0
- package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
- package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
- package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
- package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
- package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
- package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
- package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
- package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
- package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
- package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
- package/tests/Platform_AdminScan_OpsTest.res +80 -0
- package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
- package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
- package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
- package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
- package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
- package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
- package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
- package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
- package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
- package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
- package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
- package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +0 -40
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Guards the typed cold-start core hoisted out of StateViewSliceEntryPoint.mjs:
|
|
2
|
+
// - parseHandlerConfig — the compact-v2 HANDLER_CONFIG expansion (shared
|
|
3
|
+
// base / sourceUrn / pgConnection, shortened s/p/q/u/t keys) and the legacy
|
|
4
|
+
// full-key pass-through; a drift here mis-routes or mis-backends every
|
|
5
|
+
// view slice of a plugin at cold start.
|
|
6
|
+
// - decodeEnvelope — the `{event, meta, recordedAt}` envelope rule incl. the
|
|
7
|
+
// bare-event fallback.
|
|
8
|
+
// - makeJsonEventsHandler — the projection stream pipeline: schema parse →
|
|
9
|
+
// project → handleAction, with decode failures logged and skipped instead
|
|
10
|
+
// of wedging the batch.
|
|
11
|
+
|
|
12
|
+
open JestGlobals
|
|
13
|
+
|
|
14
|
+
let str = JSON.Encode.string
|
|
15
|
+
let obj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
|
|
16
|
+
|
|
17
|
+
describe("StateViewSliceEntryPoint_Ops.parseHandlerConfig", () => {
|
|
18
|
+
testSync("empty raw config yields no handlers", () => {
|
|
19
|
+
expect(StateViewSliceEntryPoint_Ops.parseHandlerConfig("")->Array.length)->toBe(0)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
testSync("expands compact-v2 entries against shared base/sourceUrn/pgConnection", () => {
|
|
23
|
+
let config = obj([
|
|
24
|
+
("v", JSON.Encode.int(2)),
|
|
25
|
+
("base", str("@x/plugin/src/StateViewSlice/")),
|
|
26
|
+
("sourceUrn", str("arn:shared")),
|
|
27
|
+
(
|
|
28
|
+
"pgConnection",
|
|
29
|
+
obj([
|
|
30
|
+
("host", str("db.local")),
|
|
31
|
+
("port", JSON.Encode.int(5432)),
|
|
32
|
+
("database", str("app")),
|
|
33
|
+
("username", str("master")),
|
|
34
|
+
("secretArn", str("arn:secret")),
|
|
35
|
+
]),
|
|
36
|
+
),
|
|
37
|
+
(
|
|
38
|
+
"handlers",
|
|
39
|
+
JSON.Encode.array([
|
|
40
|
+
obj([("s", str("Carts.res.mjs")), ("p", str("Carts_Projection.res.mjs")), ("q", str("qdb_Carts"))]),
|
|
41
|
+
obj([
|
|
42
|
+
("s", str("Totals.res.mjs")),
|
|
43
|
+
("p", str("Totals_Projection.res.mjs")),
|
|
44
|
+
("q", str("qdb_Totals")),
|
|
45
|
+
("u", str("arn:override")),
|
|
46
|
+
("t", str("cartTotals")),
|
|
47
|
+
]),
|
|
48
|
+
]),
|
|
49
|
+
),
|
|
50
|
+
])->JSON.stringify
|
|
51
|
+
let entries = StateViewSliceEntryPoint_Ops.parseHandlerConfig(config)
|
|
52
|
+
expect(entries->Array.length)->toBe(2)
|
|
53
|
+
let first = entries->Array.getUnsafe(0)
|
|
54
|
+
expect(first.specModule)->toBe("@x/plugin/src/StateViewSlice/Carts.res.mjs")
|
|
55
|
+
expect(first.projectionModule)->toBe("@x/plugin/src/StateViewSlice/Carts_Projection.res.mjs")
|
|
56
|
+
expect(first.queryDbTableName)->toBe("qdb_Carts")
|
|
57
|
+
expect(first.sourceUrn)->toBe("arn:shared")
|
|
58
|
+
expect(first.stateTopicName->Option.isNone)->toBe(true)
|
|
59
|
+
expect(first.pgConnection->Option.map(cc => cc.host))->toEqual(Some("db.local"))
|
|
60
|
+
let second = entries->Array.getUnsafe(1)
|
|
61
|
+
expect(second.sourceUrn)->toBe("arn:override")
|
|
62
|
+
expect(second.stateTopicName)->toEqual(Some("cartTotals"))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
testSync("passes legacy full-key entries through with per-entry fields", () => {
|
|
66
|
+
let config = obj([
|
|
67
|
+
("base", str("ignored/")),
|
|
68
|
+
("sourceUrn", str("arn:shared")),
|
|
69
|
+
(
|
|
70
|
+
"handlers",
|
|
71
|
+
JSON.Encode.array([
|
|
72
|
+
obj([
|
|
73
|
+
("specModule", str("full/Carts.res.mjs")),
|
|
74
|
+
("projectionModule", str("full/Carts_Projection.res.mjs")),
|
|
75
|
+
("queryDbTableName", str("qdb_Carts")),
|
|
76
|
+
("sourceUrn", str("arn:own")),
|
|
77
|
+
]),
|
|
78
|
+
]),
|
|
79
|
+
),
|
|
80
|
+
])->JSON.stringify
|
|
81
|
+
let e = StateViewSliceEntryPoint_Ops.parseHandlerConfig(config)->Array.getUnsafe(0)
|
|
82
|
+
// Legacy entries are NOT base-prefixed and keep their own sourceUrn.
|
|
83
|
+
expect(e.specModule)->toBe("full/Carts.res.mjs")
|
|
84
|
+
expect(e.projectionModule)->toBe("full/Carts_Projection.res.mjs")
|
|
85
|
+
expect(e.sourceUrn)->toBe("arn:own")
|
|
86
|
+
expect(e.pgConnection->Option.isNone)->toBe(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
describe("StateViewSliceEntryPoint_Ops.decodeEnvelope", () => {
|
|
91
|
+
testSync("surfaces event/recordedAt from the envelope", () => {
|
|
92
|
+
let envelope = obj([
|
|
93
|
+
("id", str("evt-1")),
|
|
94
|
+
("event", str("payload")),
|
|
95
|
+
("meta", obj([("service", str("Carts"))])),
|
|
96
|
+
("recordedAt", str("2026-07-27T00:00:00Z")),
|
|
97
|
+
])
|
|
98
|
+
let decoded = StateViewSliceEntryPoint_Ops.decodeEnvelope(envelope)
|
|
99
|
+
expect(decoded.event)->toEqual(str("payload"))
|
|
100
|
+
expect(decoded.recordedAt)->toBe("2026-07-27T00:00:00Z")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
testSync("falls back to the whole json for bare / null-event records", () => {
|
|
104
|
+
let bare = obj([("kind", str("CartCreated"))])
|
|
105
|
+
expect(StateViewSliceEntryPoint_Ops.decodeEnvelope(bare).event)->toEqual(bare)
|
|
106
|
+
let nullEvent = obj([("event", JSON.Null), ("recordedAt", str("t"))])
|
|
107
|
+
expect(StateViewSliceEntryPoint_Ops.decodeEnvelope(nullEvent).event)->toEqual(nullEvent)
|
|
108
|
+
expect(StateViewSliceEntryPoint_Ops.decodeEnvelope(bare).recordedAt)->toBe("")
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () => {
|
|
113
|
+
// Capturing stub operation set — Set actions land as saves.
|
|
114
|
+
let makeStub = (saved: array<(string, JSON.t)>): ReventlessCore.QueryDb_Adapter.operations => {
|
|
115
|
+
load: async _ => Ok([]),
|
|
116
|
+
loadStream: _ => Stream.fromIterable([]),
|
|
117
|
+
save: async (id, state, _mode, _ttl) => {
|
|
118
|
+
saved->Array.push((id, state))
|
|
119
|
+
Ok()
|
|
120
|
+
},
|
|
121
|
+
saveBatch: async items => {
|
|
122
|
+
items->Array.forEach(((id, state, _ttl)) => saved->Array.push((id, state)))
|
|
123
|
+
Ok()
|
|
124
|
+
},
|
|
125
|
+
count: async (_, _, _) => Ok(0),
|
|
126
|
+
delete: async (_, _) => Ok(),
|
|
127
|
+
deleteBatch: async _ => Ok(),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// String-typed consumed events keep the pipeline test independent of any
|
|
131
|
+
// real slice: each event value becomes the row id, `recordedAt` the state.
|
|
132
|
+
let project = (
|
|
133
|
+
{event, recordedAt, _}: Reventless.StateViewSlice.consumed<string>,
|
|
134
|
+
): array<Reventless.Projection.action<string, JSON.t>> => [Set(event, str(recordedAt))]
|
|
135
|
+
|
|
136
|
+
test("decodes envelopes, projects, and runs actions against the ops", async () => {
|
|
137
|
+
let saved = []
|
|
138
|
+
let handler = StateViewSliceEntryPoint_Ops.makeJsonEventsHandler(
|
|
139
|
+
~sliceName="TestSlice",
|
|
140
|
+
~eventSchema=S.string,
|
|
141
|
+
~project,
|
|
142
|
+
~queryDbOps=makeStub(saved),
|
|
143
|
+
~subIdConfig=None,
|
|
144
|
+
)
|
|
145
|
+
let envelope = obj([("event", str("row-1")), ("recordedAt", str("2026-07-27"))])
|
|
146
|
+
let bare = str("row-2")
|
|
147
|
+
await Stream.fromIterable([envelope, bare])->handler->Effect.runPromise
|
|
148
|
+
expect(saved)->toEqual([("row-1", str("2026-07-27")), ("row-2", str(""))])
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test("a decode failure yields no actions and does not wedge the batch", async () => {
|
|
152
|
+
let saved = []
|
|
153
|
+
let handler = StateViewSliceEntryPoint_Ops.makeJsonEventsHandler(
|
|
154
|
+
~sliceName="TestSlice",
|
|
155
|
+
~eventSchema=S.string,
|
|
156
|
+
~project,
|
|
157
|
+
~queryDbOps=makeStub(saved),
|
|
158
|
+
~subIdConfig=None,
|
|
159
|
+
)
|
|
160
|
+
// A number cannot parse as S.string → logged and skipped; the following
|
|
161
|
+
// record still projects.
|
|
162
|
+
await Stream.fromIterable([JSON.Encode.int(42), str("row-after")])
|
|
163
|
+
->handler
|
|
164
|
+
->Effect.runPromise
|
|
165
|
+
expect(saved)->toEqual([("row-after", str(""))])
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Effect from "effect/Effect";
|
|
6
|
+
import * as Stream from "effect/Stream";
|
|
7
|
+
import * as StateViewSliceEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs";
|
|
8
|
+
|
|
9
|
+
function str(prim) {
|
|
10
|
+
return prim;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function obj(pairs) {
|
|
14
|
+
return Object.fromEntries(pairs);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
globalThis.describe("StateViewSliceEntryPoint_Ops.parseHandlerConfig", () => {
|
|
18
|
+
globalThis.test("empty raw config yields no handlers", () => {
|
|
19
|
+
globalThis.expect(StateViewSliceEntryPoint_Ops$ReventlessAws.parseHandlerConfig("").length).toBe(0);
|
|
20
|
+
});
|
|
21
|
+
globalThis.test("expands compact-v2 entries against shared base/sourceUrn/pgConnection", () => {
|
|
22
|
+
let config = JSON.stringify(Object.fromEntries([
|
|
23
|
+
[
|
|
24
|
+
"v",
|
|
25
|
+
2
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
"base",
|
|
29
|
+
"@x/plugin/src/StateViewSlice/"
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"sourceUrn",
|
|
33
|
+
"arn:shared"
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"pgConnection",
|
|
37
|
+
Object.fromEntries([
|
|
38
|
+
[
|
|
39
|
+
"host",
|
|
40
|
+
"db.local"
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"port",
|
|
44
|
+
5432
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"database",
|
|
48
|
+
"app"
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"username",
|
|
52
|
+
"master"
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
"secretArn",
|
|
56
|
+
"arn:secret"
|
|
57
|
+
]
|
|
58
|
+
])
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"handlers",
|
|
62
|
+
[
|
|
63
|
+
Object.fromEntries([
|
|
64
|
+
[
|
|
65
|
+
"s",
|
|
66
|
+
"Carts.res.mjs"
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"p",
|
|
70
|
+
"Carts_Projection.res.mjs"
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"q",
|
|
74
|
+
"qdb_Carts"
|
|
75
|
+
]
|
|
76
|
+
]),
|
|
77
|
+
Object.fromEntries([
|
|
78
|
+
[
|
|
79
|
+
"s",
|
|
80
|
+
"Totals.res.mjs"
|
|
81
|
+
],
|
|
82
|
+
[
|
|
83
|
+
"p",
|
|
84
|
+
"Totals_Projection.res.mjs"
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"q",
|
|
88
|
+
"qdb_Totals"
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
"u",
|
|
92
|
+
"arn:override"
|
|
93
|
+
],
|
|
94
|
+
[
|
|
95
|
+
"t",
|
|
96
|
+
"cartTotals"
|
|
97
|
+
]
|
|
98
|
+
])
|
|
99
|
+
]
|
|
100
|
+
]
|
|
101
|
+
]));
|
|
102
|
+
let entries = StateViewSliceEntryPoint_Ops$ReventlessAws.parseHandlerConfig(config);
|
|
103
|
+
globalThis.expect(entries.length).toBe(2);
|
|
104
|
+
let first = entries[0];
|
|
105
|
+
globalThis.expect(first.specModule).toBe("@x/plugin/src/StateViewSlice/Carts.res.mjs");
|
|
106
|
+
globalThis.expect(first.projectionModule).toBe("@x/plugin/src/StateViewSlice/Carts_Projection.res.mjs");
|
|
107
|
+
globalThis.expect(first.queryDbTableName).toBe("qdb_Carts");
|
|
108
|
+
globalThis.expect(first.sourceUrn).toBe("arn:shared");
|
|
109
|
+
globalThis.expect(Stdlib_Option.isNone(first.stateTopicName)).toBe(true);
|
|
110
|
+
globalThis.expect(Stdlib_Option.map(first.pgConnection, cc => cc.host)).toEqual("db.local");
|
|
111
|
+
let second = entries[1];
|
|
112
|
+
globalThis.expect(second.sourceUrn).toBe("arn:override");
|
|
113
|
+
globalThis.expect(second.stateTopicName).toEqual("cartTotals");
|
|
114
|
+
});
|
|
115
|
+
globalThis.test("passes legacy full-key entries through with per-entry fields", () => {
|
|
116
|
+
let config = JSON.stringify(Object.fromEntries([
|
|
117
|
+
[
|
|
118
|
+
"base",
|
|
119
|
+
"ignored/"
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
"sourceUrn",
|
|
123
|
+
"arn:shared"
|
|
124
|
+
],
|
|
125
|
+
[
|
|
126
|
+
"handlers",
|
|
127
|
+
[Object.fromEntries([
|
|
128
|
+
[
|
|
129
|
+
"specModule",
|
|
130
|
+
"full/Carts.res.mjs"
|
|
131
|
+
],
|
|
132
|
+
[
|
|
133
|
+
"projectionModule",
|
|
134
|
+
"full/Carts_Projection.res.mjs"
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
"queryDbTableName",
|
|
138
|
+
"qdb_Carts"
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
"sourceUrn",
|
|
142
|
+
"arn:own"
|
|
143
|
+
]
|
|
144
|
+
])]
|
|
145
|
+
]
|
|
146
|
+
]));
|
|
147
|
+
let e = StateViewSliceEntryPoint_Ops$ReventlessAws.parseHandlerConfig(config)[0];
|
|
148
|
+
globalThis.expect(e.specModule).toBe("full/Carts.res.mjs");
|
|
149
|
+
globalThis.expect(e.projectionModule).toBe("full/Carts_Projection.res.mjs");
|
|
150
|
+
globalThis.expect(e.sourceUrn).toBe("arn:own");
|
|
151
|
+
globalThis.expect(Stdlib_Option.isNone(e.pgConnection)).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
globalThis.describe("StateViewSliceEntryPoint_Ops.decodeEnvelope", () => {
|
|
156
|
+
globalThis.test("surfaces event/recordedAt from the envelope", () => {
|
|
157
|
+
let envelope = Object.fromEntries([
|
|
158
|
+
[
|
|
159
|
+
"id",
|
|
160
|
+
"evt-1"
|
|
161
|
+
],
|
|
162
|
+
[
|
|
163
|
+
"event",
|
|
164
|
+
"payload"
|
|
165
|
+
],
|
|
166
|
+
[
|
|
167
|
+
"meta",
|
|
168
|
+
Object.fromEntries([[
|
|
169
|
+
"service",
|
|
170
|
+
"Carts"
|
|
171
|
+
]])
|
|
172
|
+
],
|
|
173
|
+
[
|
|
174
|
+
"recordedAt",
|
|
175
|
+
"2026-07-27T00:00:00Z"
|
|
176
|
+
]
|
|
177
|
+
]);
|
|
178
|
+
let decoded = StateViewSliceEntryPoint_Ops$ReventlessAws.decodeEnvelope(envelope);
|
|
179
|
+
globalThis.expect(decoded.event).toEqual("payload");
|
|
180
|
+
globalThis.expect(decoded.recordedAt).toBe("2026-07-27T00:00:00Z");
|
|
181
|
+
});
|
|
182
|
+
globalThis.test("falls back to the whole json for bare / null-event records", () => {
|
|
183
|
+
let bare = Object.fromEntries([[
|
|
184
|
+
"kind",
|
|
185
|
+
"CartCreated"
|
|
186
|
+
]]);
|
|
187
|
+
globalThis.expect(StateViewSliceEntryPoint_Ops$ReventlessAws.decodeEnvelope(bare).event).toEqual(bare);
|
|
188
|
+
let nullEvent = Object.fromEntries([
|
|
189
|
+
[
|
|
190
|
+
"event",
|
|
191
|
+
null
|
|
192
|
+
],
|
|
193
|
+
[
|
|
194
|
+
"recordedAt",
|
|
195
|
+
"t"
|
|
196
|
+
]
|
|
197
|
+
]);
|
|
198
|
+
globalThis.expect(StateViewSliceEntryPoint_Ops$ReventlessAws.decodeEnvelope(nullEvent).event).toEqual(nullEvent);
|
|
199
|
+
globalThis.expect(StateViewSliceEntryPoint_Ops$ReventlessAws.decodeEnvelope(bare).recordedAt).toBe("");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
globalThis.describe("StateViewSliceEntryPoint_Ops.makeJsonEventsHandler", () => {
|
|
204
|
+
let makeStub = saved => ({
|
|
205
|
+
load: async param => ({
|
|
206
|
+
TAG: "Ok",
|
|
207
|
+
_0: []
|
|
208
|
+
}),
|
|
209
|
+
loadStream: param => Stream.fromIterable([]),
|
|
210
|
+
save: async (id, state, _mode, _ttl) => {
|
|
211
|
+
saved.push([
|
|
212
|
+
id,
|
|
213
|
+
state
|
|
214
|
+
]);
|
|
215
|
+
return {
|
|
216
|
+
TAG: "Ok",
|
|
217
|
+
_0: undefined
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
saveBatch: async items => {
|
|
221
|
+
items.forEach(param => {
|
|
222
|
+
saved.push([
|
|
223
|
+
param[0],
|
|
224
|
+
param[1]
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
TAG: "Ok",
|
|
229
|
+
_0: undefined
|
|
230
|
+
};
|
|
231
|
+
},
|
|
232
|
+
count: async (param, param$1, param$2) => ({
|
|
233
|
+
TAG: "Ok",
|
|
234
|
+
_0: 0
|
|
235
|
+
}),
|
|
236
|
+
delete: async (param, param$1) => ({
|
|
237
|
+
TAG: "Ok",
|
|
238
|
+
_0: undefined
|
|
239
|
+
}),
|
|
240
|
+
deleteBatch: async param => ({
|
|
241
|
+
TAG: "Ok",
|
|
242
|
+
_0: undefined
|
|
243
|
+
})
|
|
244
|
+
});
|
|
245
|
+
let project = param => [{
|
|
246
|
+
TAG: "Set",
|
|
247
|
+
_0: param.event,
|
|
248
|
+
_1: param.recordedAt
|
|
249
|
+
}];
|
|
250
|
+
globalThis.test("decodes envelopes, projects, and runs actions against the ops", async () => {
|
|
251
|
+
let saved = [];
|
|
252
|
+
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TestSlice", S.string, project, makeStub(saved), undefined);
|
|
253
|
+
let envelope = Object.fromEntries([
|
|
254
|
+
[
|
|
255
|
+
"event",
|
|
256
|
+
"row-1"
|
|
257
|
+
],
|
|
258
|
+
[
|
|
259
|
+
"recordedAt",
|
|
260
|
+
"2026-07-27"
|
|
261
|
+
]
|
|
262
|
+
]);
|
|
263
|
+
await Effect.runPromise(handler(Stream.fromIterable([
|
|
264
|
+
envelope,
|
|
265
|
+
"row-2"
|
|
266
|
+
])));
|
|
267
|
+
globalThis.expect(saved).toEqual([
|
|
268
|
+
[
|
|
269
|
+
"row-1",
|
|
270
|
+
"2026-07-27"
|
|
271
|
+
],
|
|
272
|
+
[
|
|
273
|
+
"row-2",
|
|
274
|
+
""
|
|
275
|
+
]
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
globalThis.test("a decode failure yields no actions and does not wedge the batch", async () => {
|
|
279
|
+
let saved = [];
|
|
280
|
+
let handler = StateViewSliceEntryPoint_Ops$ReventlessAws.makeJsonEventsHandler("TestSlice", S.string, project, makeStub(saved), undefined);
|
|
281
|
+
await Effect.runPromise(handler(Stream.fromIterable([
|
|
282
|
+
42,
|
|
283
|
+
"row-after"
|
|
284
|
+
])));
|
|
285
|
+
globalThis.expect(saved).toEqual([[
|
|
286
|
+
"row-after",
|
|
287
|
+
""
|
|
288
|
+
]]);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
export {
|
|
293
|
+
str,
|
|
294
|
+
obj,
|
|
295
|
+
}
|
|
296
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Guards the typed cold-start core hoisted out of TaskBucketEntryPoint.mjs:
|
|
2
|
+
// - parseHandlerConfig — the callbackModule / publishToAggregates /
|
|
3
|
+
// scheduler env-indirection contract.
|
|
4
|
+
// - buildPublishCommands — env-var queue resolution incl. the skip on
|
|
5
|
+
// unset/empty URLs.
|
|
6
|
+
// - dispatchTaskActions — the exhaustive task-action dispatch (publish
|
|
7
|
+
// routing, schedule create/delete, and the no-scheduler skip paths).
|
|
8
|
+
|
|
9
|
+
open JestGlobals
|
|
10
|
+
|
|
11
|
+
let str = JSON.Encode.string
|
|
12
|
+
let obj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
|
|
13
|
+
|
|
14
|
+
@val @scope("process") external processEnv: dict<string> = "env"
|
|
15
|
+
|
|
16
|
+
describe("TaskBucketEntryPoint_Ops.parseHandlerConfig", () => {
|
|
17
|
+
testSync("empty raw config decodes to empty defaults", () => {
|
|
18
|
+
let c = TaskBucketEntryPoint_Ops.parseHandlerConfig("")
|
|
19
|
+
expect(c.callbackModule)->toBe("")
|
|
20
|
+
expect(c.publishToAggregates->Dict.toArray->Array.length)->toBe(0)
|
|
21
|
+
expect(c.scheduler->Option.isNone)->toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
testSync("decodes callbackModule, publishToAggregates, and scheduler env names", () => {
|
|
25
|
+
let config = obj([
|
|
26
|
+
("callbackModule", str("@x/plugin/src/Task/ImportCatalog.res.mjs")),
|
|
27
|
+
("publishToAggregates", obj([("Product", str("PTA_Product_QUEUE_URL"))])),
|
|
28
|
+
(
|
|
29
|
+
"scheduler",
|
|
30
|
+
obj([
|
|
31
|
+
("roleArnEnv", str("SCHEDULER_ROLE_ARN")),
|
|
32
|
+
("targetArnEnv", str("SCHEDULER_TARGET_ARN")),
|
|
33
|
+
("targetNameEnv", str("SCHEDULER_TARGET_NAME")),
|
|
34
|
+
]),
|
|
35
|
+
),
|
|
36
|
+
])->JSON.stringify
|
|
37
|
+
let c = TaskBucketEntryPoint_Ops.parseHandlerConfig(config)
|
|
38
|
+
expect(c.callbackModule)->toBe("@x/plugin/src/Task/ImportCatalog.res.mjs")
|
|
39
|
+
expect(c.publishToAggregates->Dict.get("Product"))->toEqual(Some("PTA_Product_QUEUE_URL"))
|
|
40
|
+
switch c.scheduler {
|
|
41
|
+
| Some(s) => expect(s.roleArnEnv)->toBe("SCHEDULER_ROLE_ARN")
|
|
42
|
+
| None => JsError.throwWithMessage("expected Some(scheduler)")
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe("TaskBucketEntryPoint_Ops.buildPublishCommands", () => {
|
|
48
|
+
testSync("resolves queue URLs via env vars, skipping unset or empty ones", () => {
|
|
49
|
+
processEnv->Dict.set("TB_TEST_QUEUE_A", "https://sqs/a.fifo")
|
|
50
|
+
processEnv->Dict.set("TB_TEST_QUEUE_EMPTY", "")
|
|
51
|
+
let map = Dict.fromArray([
|
|
52
|
+
("AggA", "TB_TEST_QUEUE_A"),
|
|
53
|
+
("AggEmpty", "TB_TEST_QUEUE_EMPTY"),
|
|
54
|
+
("AggUnset", "TB_TEST_QUEUE_UNSET"),
|
|
55
|
+
])
|
|
56
|
+
let publish = TaskBucketEntryPoint_Ops.buildPublishCommands(map)
|
|
57
|
+
expect(publish->Dict.get("AggA")->Option.isSome)->toBe(true)
|
|
58
|
+
expect(publish->Dict.get("AggEmpty")->Option.isNone)->toBe(true)
|
|
59
|
+
expect(publish->Dict.get("AggUnset")->Option.isNone)->toBe(true)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("TaskBucketEntryPoint_Ops.dispatchTaskActions", () => {
|
|
64
|
+
let schedule: Reventless.Schedule.schedule = {
|
|
65
|
+
name: "sweep",
|
|
66
|
+
rate: Reventless.Schedule.Minutes(5),
|
|
67
|
+
payload: "{}",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
test("routes PublishCommands to the aggregate's publish fn", async () => {
|
|
71
|
+
let published = []
|
|
72
|
+
let publishCommands = Dict.fromArray([
|
|
73
|
+
(
|
|
74
|
+
"Product",
|
|
75
|
+
(
|
|
76
|
+
async cmds => {
|
|
77
|
+
published->Array.push(cmds)
|
|
78
|
+
}: ReventlessCore.CommandTopic.publishJsons
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
])
|
|
82
|
+
await TaskBucketEntryPoint_Ops.dispatchTaskActions(
|
|
83
|
+
~actions=[
|
|
84
|
+
Reventless.Task.PublishCommands("Product", []),
|
|
85
|
+
// Unknown aggregate — logged, not thrown.
|
|
86
|
+
Reventless.Task.PublishCommands("Nope", []),
|
|
87
|
+
],
|
|
88
|
+
~publishCommands,
|
|
89
|
+
~schedulerOps=() => None,
|
|
90
|
+
)
|
|
91
|
+
expect(published->Array.length)->toBe(1)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("routes schedule actions to the scheduler ops; skips when unconfigured", async () => {
|
|
95
|
+
let created = []
|
|
96
|
+
let deleted = []
|
|
97
|
+
let ops: TaskBucketEntryPoint_Ops.schedulerOps = {
|
|
98
|
+
createSchedule: async s => created->Array.push(s.name),
|
|
99
|
+
deleteSchedule: async name => deleted->Array.push(name),
|
|
100
|
+
}
|
|
101
|
+
await TaskBucketEntryPoint_Ops.dispatchTaskActions(
|
|
102
|
+
~actions=[Reventless.Task.CreateSchedule(schedule), Reventless.Task.DeleteSchedule("sweep")],
|
|
103
|
+
~publishCommands=Dict.make(),
|
|
104
|
+
~schedulerOps=() => Some(ops),
|
|
105
|
+
)
|
|
106
|
+
expect(created)->toEqual(["sweep"])
|
|
107
|
+
expect(deleted)->toEqual(["sweep"])
|
|
108
|
+
|
|
109
|
+
// No scheduler configured — both actions skip without throwing.
|
|
110
|
+
await TaskBucketEntryPoint_Ops.dispatchTaskActions(
|
|
111
|
+
~actions=[Reventless.Task.CreateSchedule(schedule), Reventless.Task.DeleteSchedule("sweep")],
|
|
112
|
+
~publishCommands=Dict.make(),
|
|
113
|
+
~schedulerOps=() => None,
|
|
114
|
+
)
|
|
115
|
+
expect(created->Array.length)->toBe(1)
|
|
116
|
+
})
|
|
117
|
+
})
|