@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,183 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
9
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
10
|
+
import * as DynamoDb_Util_Helpers$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_Util_Helpers.res.mjs";
|
|
11
|
+
import * as AppSyncEventsSigner_Ops$ReventlessAws from "../Api/AppSyncEventsSigner_Ops.res.mjs";
|
|
12
|
+
|
|
13
|
+
let endpoint = Stdlib_Option.getOr(process.env["APPSYNC_ENDPOINT"], "");
|
|
14
|
+
|
|
15
|
+
let obj = Stdlib_JSON.Decode.object(JSON.parse(Stdlib_Option.getOr(process.env["STATE_TOPIC_MAP"], "{}")));
|
|
16
|
+
|
|
17
|
+
let topicMap = obj !== undefined ? Object.fromEntries(Stdlib_Array.filterMap(Object.entries(obj), param => {
|
|
18
|
+
let k = param[0];
|
|
19
|
+
return Stdlib_Option.map(Stdlib_JSON.Decode.string(param[1]), s => [
|
|
20
|
+
k,
|
|
21
|
+
s
|
|
22
|
+
]);
|
|
23
|
+
})) : ({});
|
|
24
|
+
|
|
25
|
+
function topicRootFromEventSourceArn(arn) {
|
|
26
|
+
let parts = arn.split("/");
|
|
27
|
+
let match = parts[0];
|
|
28
|
+
let match$1 = parts[1];
|
|
29
|
+
let match$2 = parts[2];
|
|
30
|
+
if (match !== undefined && match$1 !== undefined && match$2 !== undefined && match$2 === "stream" && match.endsWith(":table")) {
|
|
31
|
+
return Stdlib_Option.map(topicMap[match$1], AppSyncEventsSigner_Ops$ReventlessAws.pathSegment);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function jsonToString(j) {
|
|
36
|
+
if (j === null) {
|
|
37
|
+
return "null";
|
|
38
|
+
}
|
|
39
|
+
if (Array.isArray(j)) {
|
|
40
|
+
return JSON.stringify(j);
|
|
41
|
+
}
|
|
42
|
+
switch (typeof j) {
|
|
43
|
+
case "boolean" :
|
|
44
|
+
if (j) {
|
|
45
|
+
return "true";
|
|
46
|
+
} else {
|
|
47
|
+
return "false";
|
|
48
|
+
}
|
|
49
|
+
case "string" :
|
|
50
|
+
return j;
|
|
51
|
+
case "number" :
|
|
52
|
+
return j.toString();
|
|
53
|
+
case "object" :
|
|
54
|
+
return JSON.stringify(j);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function entityKeyFromRecord(dynamodb) {
|
|
59
|
+
let keys = DynamoDb_Util_Helpers$AwsSdk.unmarshallDict(undefined, dynamodb.Keys);
|
|
60
|
+
let id = keys["id"];
|
|
61
|
+
if (id === undefined) {
|
|
62
|
+
return Object.keys(keys).toSorted(Primitive_string.compare).map(n => Stdlib_Option.mapOr(keys[n], "", jsonToString)).join("-");
|
|
63
|
+
}
|
|
64
|
+
let subIdName = Object.keys(keys).find(k => k !== "id");
|
|
65
|
+
if (subIdName !== undefined) {
|
|
66
|
+
return jsonToString(id) + "-" + Stdlib_Option.mapOr(keys[subIdName], "", jsonToString);
|
|
67
|
+
} else {
|
|
68
|
+
return jsonToString(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function changeKindFor(eventName) {
|
|
73
|
+
switch (eventName) {
|
|
74
|
+
case "INSERT" :
|
|
75
|
+
return "Added";
|
|
76
|
+
case "REMOVE" :
|
|
77
|
+
return "Removed";
|
|
78
|
+
default:
|
|
79
|
+
return "Updated";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pickSortKeyValue(image) {
|
|
84
|
+
let v = Stdlib_Option.flatMap(image["updatedAt"], Stdlib_JSON.Decode.string);
|
|
85
|
+
if (v !== undefined) {
|
|
86
|
+
return v;
|
|
87
|
+
} else {
|
|
88
|
+
return Stdlib_Option.flatMap(image["createdAt"], Stdlib_JSON.Decode.string);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function processRecord(record, region, creds) {
|
|
93
|
+
let topicRoot = topicRootFromEventSourceArn(record.eventSourceARN);
|
|
94
|
+
if (topicRoot !== undefined) {
|
|
95
|
+
let dynamodb = record.dynamodb;
|
|
96
|
+
if (dynamodb === undefined) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
let image = record.eventName === "REMOVE" ? dynamodb.OldImage : dynamodb.NewImage;
|
|
100
|
+
if (image === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let entityKey = entityKeyFromRecord(dynamodb);
|
|
104
|
+
let channel = `/default/` + topicRoot + `/` + AppSyncEventsSigner_Ops$ReventlessAws.pathSegment(entityKey);
|
|
105
|
+
let unmarshalled = DynamoDb_Util_Helpers$AwsSdk.unmarshallDict(undefined, image);
|
|
106
|
+
let descriptor = {};
|
|
107
|
+
descriptor["changeKind"] = changeKindFor(record.eventName);
|
|
108
|
+
descriptor["id"] = entityKey;
|
|
109
|
+
Stdlib_Option.forEach(pickSortKeyValue(unmarshalled), v => {
|
|
110
|
+
descriptor["sortKeyValue"] = v;
|
|
111
|
+
});
|
|
112
|
+
let body = JSON.stringify(Object.fromEntries([
|
|
113
|
+
[
|
|
114
|
+
"id",
|
|
115
|
+
record.eventID
|
|
116
|
+
],
|
|
117
|
+
[
|
|
118
|
+
"channel",
|
|
119
|
+
channel
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
"events",
|
|
123
|
+
[JSON.stringify(descriptor)]
|
|
124
|
+
]
|
|
125
|
+
]));
|
|
126
|
+
try {
|
|
127
|
+
let res = await AppSyncEventsSigner_Ops$ReventlessAws.postEvent(endpoint, region, new Date().toISOString(), creds, body);
|
|
128
|
+
if (res.ok) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
let status = res.status;
|
|
132
|
+
let txt = await res.text();
|
|
133
|
+
let isTransient = status >= 500;
|
|
134
|
+
let tag = isTransient ? "transient" : "permanent";
|
|
135
|
+
console.error(`STATE_TOPIC_PUBLISH_FAILED type=` + tag + ` status=` + status.toString() + ` channel=` + channel + ` body=` + txt.slice(0, 500));
|
|
136
|
+
if (isTransient) {
|
|
137
|
+
return `StateTopic publish failed: ` + status.toString();
|
|
138
|
+
} else {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
} catch (raw_exn) {
|
|
142
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
143
|
+
let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "");
|
|
144
|
+
console.error(`STATE_TOPIC_PUBLISH_FAILED type=transient status=network channel=` + channel + ` err=` + msg);
|
|
145
|
+
return msg === "" ? "StateTopic network error" : msg;
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
console.warn("StateTopic: unknown table for ARN", record.eventSourceARN);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handler(event) {
|
|
154
|
+
let region = AppSyncEventsSigner_Ops$ReventlessAws.region();
|
|
155
|
+
let creds = AppSyncEventsSigner_Ops$ReventlessAws.envCreds();
|
|
156
|
+
let transientErr = {
|
|
157
|
+
contents: undefined
|
|
158
|
+
};
|
|
159
|
+
await Stdlib_Array.reduce(event.Records, Promise.resolve(), (acc, record) => acc.then(async () => {
|
|
160
|
+
let msg = await processRecord(record, region, creds);
|
|
161
|
+
if (msg !== undefined) {
|
|
162
|
+
transientErr.contents = msg;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}));
|
|
166
|
+
let msg = transientErr.contents;
|
|
167
|
+
if (msg !== undefined) {
|
|
168
|
+
return Stdlib_JsError.throwWithMessage(msg);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export {
|
|
173
|
+
endpoint,
|
|
174
|
+
topicMap,
|
|
175
|
+
topicRootFromEventSourceArn,
|
|
176
|
+
jsonToString,
|
|
177
|
+
entityKeyFromRecord,
|
|
178
|
+
changeKindFor,
|
|
179
|
+
pickSortKeyValue,
|
|
180
|
+
processRecord,
|
|
181
|
+
handler,
|
|
182
|
+
}
|
|
183
|
+
/* endpoint Not a pure module */
|
|
@@ -40,8 +40,10 @@ module Make = (Api: {
|
|
|
40
40
|
EventCollectorRuntimeBuilder.registerAutomationSlice(
|
|
41
41
|
~name=Spec.name,
|
|
42
42
|
~specModulePath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
|
|
43
|
+
~bodyModulePath=Util_Bundle.getModuleSpecifier(Automation.moduleUrl),
|
|
43
44
|
~callbackType="automation",
|
|
44
45
|
~queryDbTableName,
|
|
46
|
+
~context,
|
|
45
47
|
)
|
|
46
48
|
|
|
47
49
|
as_
|
|
@@ -41,7 +41,7 @@ function Make(Api) {
|
|
|
41
41
|
let queryDbOutputs = Component$ReventlessCore.outputs(as_).queryDb;
|
|
42
42
|
let tableResource = queryDbOutputs.resources[0];
|
|
43
43
|
let queryDbTableName = tableResource.name;
|
|
44
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), "automation", queryDbTableName);
|
|
44
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, context);
|
|
45
45
|
return as_;
|
|
46
46
|
};
|
|
47
47
|
return {
|
|
@@ -48,6 +48,7 @@ module Make = (Api: {
|
|
|
48
48
|
AutomationSliceRuntime_Builder_Single.registerAutomationSlice(
|
|
49
49
|
~name=Spec.name,
|
|
50
50
|
~specModulePath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
|
|
51
|
+
~bodyModulePath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
|
|
51
52
|
~callbackType="outbound",
|
|
52
53
|
~queryDbTableName,
|
|
53
54
|
)
|
|
@@ -48,7 +48,7 @@ function Make(Api) {
|
|
|
48
48
|
let queryDbOutputs = Component$ReventlessCore.outputs(ots).queryDb;
|
|
49
49
|
let tableResource = queryDbOutputs.resources[0];
|
|
50
50
|
let queryDbTableName = tableResource.name;
|
|
51
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), "outbound", queryDbTableName);
|
|
51
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, undefined);
|
|
52
52
|
return ots;
|
|
53
53
|
};
|
|
54
54
|
return {
|
|
@@ -56,7 +56,13 @@ let registerStateChangeSliceSpec = (~specPath: string, ~behaviorPath: string) =>
|
|
|
56
56
|
type inboundSliceReg = {
|
|
57
57
|
specPath: string,
|
|
58
58
|
translationPath: string,
|
|
59
|
-
|
|
59
|
+
// Plain `Pulumi.Output.t<string>`, never `option<Pulumi.Output.t<string>>`:
|
|
60
|
+
// any generic `Option.*` call over an Output runs `valFromOption`, whose
|
|
61
|
+
// `.BS_PRIVATE_NESTED_SOME_NONE` probe hits the Output proxy (every property
|
|
62
|
+
// access returns a truthy Output), mis-classifies it as a nested-option and
|
|
63
|
+
// corrupts it into `{BS_PRIVATE_NESTED_SOME_NONE:0}`. The empty-string Output
|
|
64
|
+
// is the "no audit table" sentinel (serialized as `null` downstream).
|
|
65
|
+
mutable auditTableName: Pulumi.Output.t<string>,
|
|
60
66
|
}
|
|
61
67
|
let registeredInboundSlices: dict<inboundSliceReg> = Dict.make()
|
|
62
68
|
|
|
@@ -68,16 +74,19 @@ let registerInboundTranslationSliceSpec = (
|
|
|
68
74
|
switch registeredInboundSlices->Dict.get(specName) {
|
|
69
75
|
| Some(reg) => registeredInboundSlices->Dict.set(specName, {...reg, specPath, translationPath})
|
|
70
76
|
| None =>
|
|
71
|
-
registeredInboundSlices->Dict.set(
|
|
77
|
+
registeredInboundSlices->Dict.set(
|
|
78
|
+
specName,
|
|
79
|
+
{specPath, translationPath, auditTableName: Pulumi.Output.make("")},
|
|
80
|
+
)
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
let registerInboundAuditTableName = (~specName: string, tableName: Pulumi.Output.t<string>) =>
|
|
75
84
|
switch registeredInboundSlices->Dict.get(specName) {
|
|
76
|
-
| Some(reg) => reg.auditTableName =
|
|
85
|
+
| Some(reg) => reg.auditTableName = tableName
|
|
77
86
|
| None =>
|
|
78
87
|
registeredInboundSlices->Dict.set(
|
|
79
88
|
specName,
|
|
80
|
-
{specPath: "", translationPath: "", auditTableName:
|
|
89
|
+
{specPath: "", translationPath: "", auditTableName: tableName},
|
|
81
90
|
)
|
|
82
91
|
}
|
|
83
92
|
|
|
@@ -65,7 +65,7 @@ function registerInboundTranslationSliceSpec(specName, specPath, translationPath
|
|
|
65
65
|
registeredInboundSlices[specName] = {
|
|
66
66
|
specPath: specPath,
|
|
67
67
|
translationPath: translationPath,
|
|
68
|
-
auditTableName:
|
|
68
|
+
auditTableName: Pulumi.output("")
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Byte-parity guard for the AppSync Events SigV4 signer port.
|
|
2
|
+
//
|
|
3
|
+
// AppSyncEventsSigner_Ops.signedHeaders is a ReScript port of the hand-rolled
|
|
4
|
+
// SigV4 the two stream-relay Lambdas used to carry as inline JS. A one-character
|
|
5
|
+
// drift in the canonical request or the signing-key chain silently produces a
|
|
6
|
+
// 403 from AppSync and stops live event delivery — invisible until a deploy.
|
|
7
|
+
// This asserts the port is byte-identical to the preserved original
|
|
8
|
+
// (AppSyncEventsSigner_ParityFixture.mjs) across a range of inputs.
|
|
9
|
+
|
|
10
|
+
open JestGlobals
|
|
11
|
+
|
|
12
|
+
type jsCreds = {accessKeyId: string, secretAccessKey: string, sessionToken?: string}
|
|
13
|
+
@module("./AppSyncEventsSigner_ParityFixture.mjs")
|
|
14
|
+
external signedHeadersJs: (
|
|
15
|
+
~host: string,
|
|
16
|
+
~path: string,
|
|
17
|
+
~body: string,
|
|
18
|
+
~region: string,
|
|
19
|
+
~isoNow: string,
|
|
20
|
+
~creds: jsCreds,
|
|
21
|
+
) => dict<string> = "signedHeadersJs"
|
|
22
|
+
|
|
23
|
+
let get = (d, k) => d->Dict.get(k)->Option.getOr("<missing>")
|
|
24
|
+
|
|
25
|
+
// Compare the ReScript signer to the JS reference for one input tuple.
|
|
26
|
+
let assertParity = (~host, ~path, ~body, ~region, ~isoNow, ~accessKeyId, ~secretAccessKey, ~sessionToken) => {
|
|
27
|
+
let creds: AppSyncEventsSigner_Ops.creds = {accessKeyId, secretAccessKey, sessionToken}
|
|
28
|
+
let mine = AppSyncEventsSigner_Ops.signedHeaders(~host, ~path, ~body, ~region, ~isoNow, ~creds)
|
|
29
|
+
let jsCreds: jsCreds = {accessKeyId, secretAccessKey, sessionToken: ?sessionToken}
|
|
30
|
+
let ref = signedHeadersJs(~host, ~path, ~body, ~region, ~isoNow, ~creds=jsCreds)
|
|
31
|
+
expect(mine->get("Authorization"))->toBe(ref->get("Authorization"))
|
|
32
|
+
expect(mine->get("x-amz-date"))->toBe(ref->get("x-amz-date"))
|
|
33
|
+
expect(mine->get("host"))->toBe(ref->get("host"))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("AppSyncEventsSigner_Ops.signedHeaders parity with the original JS", () => {
|
|
37
|
+
testSync("session-token present", () => {
|
|
38
|
+
assertParity(
|
|
39
|
+
~host="abc123.appsync-api.eu-west-1.amazonaws.com",
|
|
40
|
+
~path="/event",
|
|
41
|
+
~body=`{"id":"m-1","channel":"/default/Catalog","events":["{\\"position\\":\\"1\\"}"]}`,
|
|
42
|
+
~region="eu-west-1",
|
|
43
|
+
~isoNow="2026-07-27T13:45:07.123Z",
|
|
44
|
+
~accessKeyId="ASIAEXAMPLE",
|
|
45
|
+
~secretAccessKey="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
|
46
|
+
~sessionToken=Some("FQoGZXIvYXdzEXAMPLESESSIONTOKEN=="),
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
testSync("no session token (short-lived / role creds without token)", () => {
|
|
51
|
+
assertParity(
|
|
52
|
+
~host="xyz.appsync-api.us-east-1.amazonaws.com",
|
|
53
|
+
~path="/event",
|
|
54
|
+
~body=`{"id":"e-42","channel":"/default/Orders/o-9","events":["{\\"changeKind\\":\\"Added\\"}"]}`,
|
|
55
|
+
~region="us-east-1",
|
|
56
|
+
~isoNow="2026-01-02T00:00:00.000Z",
|
|
57
|
+
~accessKeyId="AKIAEXAMPLE2",
|
|
58
|
+
~secretAccessKey="anotherSecretKeyValue1234567890abcdef",
|
|
59
|
+
~sessionToken=None,
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
testSync("different region + midnight-adjacent timestamp", () => {
|
|
64
|
+
assertParity(
|
|
65
|
+
~host="host.appsync-api.ap-southeast-2.amazonaws.com",
|
|
66
|
+
~path="/event",
|
|
67
|
+
~body="{}",
|
|
68
|
+
~region="ap-southeast-2",
|
|
69
|
+
~isoNow="2026-12-31T23:59:59.999Z",
|
|
70
|
+
~accessKeyId="AKIA3",
|
|
71
|
+
~secretAccessKey="k3",
|
|
72
|
+
~sessionToken=Some("tok3"),
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as AppSyncEventsSigner_Ops$ReventlessAws from "../src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs";
|
|
5
|
+
import * as AppSyncEventsSigner_ParityFixtureMjs from "./AppSyncEventsSigner_ParityFixture.mjs";
|
|
6
|
+
|
|
7
|
+
function signedHeadersJs(prim0, prim1, prim2, prim3, prim4, prim5) {
|
|
8
|
+
return AppSyncEventsSigner_ParityFixtureMjs.signedHeadersJs(prim0, prim1, prim2, prim3, prim4, prim5);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function get(d, k) {
|
|
12
|
+
return Stdlib_Option.getOr(d[k], "<missing>");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function assertParity(host, path, body, region, isoNow, accessKeyId, secretAccessKey, sessionToken) {
|
|
16
|
+
let creds = {
|
|
17
|
+
accessKeyId: accessKeyId,
|
|
18
|
+
secretAccessKey: secretAccessKey,
|
|
19
|
+
sessionToken: sessionToken
|
|
20
|
+
};
|
|
21
|
+
let mine = AppSyncEventsSigner_Ops$ReventlessAws.signedHeaders(host, path, body, region, isoNow, creds);
|
|
22
|
+
let jsCreds = {
|
|
23
|
+
accessKeyId: accessKeyId,
|
|
24
|
+
secretAccessKey: secretAccessKey,
|
|
25
|
+
sessionToken: sessionToken
|
|
26
|
+
};
|
|
27
|
+
let ref = signedHeadersJs(host, path, body, region, isoNow, jsCreds);
|
|
28
|
+
globalThis.expect(get(mine, "Authorization")).toBe(get(ref, "Authorization"));
|
|
29
|
+
globalThis.expect(get(mine, "x-amz-date")).toBe(get(ref, "x-amz-date"));
|
|
30
|
+
globalThis.expect(get(mine, "host")).toBe(get(ref, "host"));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
globalThis.describe("AppSyncEventsSigner_Ops.signedHeaders parity with the original JS", () => {
|
|
34
|
+
globalThis.test("session-token present", () => assertParity("abc123.appsync-api.eu-west-1.amazonaws.com", "/event", `{"id":"m-1","channel":"/default/Catalog","events":["{\\"position\\":\\"1\\"}"]}`, "eu-west-1", "2026-07-27T13:45:07.123Z", "ASIAEXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "FQoGZXIvYXdzEXAMPLESESSIONTOKEN=="));
|
|
35
|
+
globalThis.test("no session token (short-lived / role creds without token)", () => assertParity("xyz.appsync-api.us-east-1.amazonaws.com", "/event", `{"id":"e-42","channel":"/default/Orders/o-9","events":["{\\"changeKind\\":\\"Added\\"}"]}`, "us-east-1", "2026-01-02T00:00:00.000Z", "AKIAEXAMPLE2", "anotherSecretKeyValue1234567890abcdef", undefined));
|
|
36
|
+
globalThis.test("different region + midnight-adjacent timestamp", () => assertParity("host.appsync-api.ap-southeast-2.amazonaws.com", "/event", "{}", "ap-southeast-2", "2026-12-31T23:59:59.999Z", "AKIA3", "k3", "tok3"));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
signedHeadersJs,
|
|
41
|
+
get,
|
|
42
|
+
assertParity,
|
|
43
|
+
}
|
|
44
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Golden reference for the AppSync Events SigV4 signer parity test.
|
|
2
|
+
//
|
|
3
|
+
// This is the EXACT hand-rolled `signedHeaders` that lived in the inline JS
|
|
4
|
+
// handler strings of EventLogSubscription_AppSync.res and StateTopic_AppSync.res
|
|
5
|
+
// before those handlers were ported to ReScript (AppSyncEventsSigner_Ops.res).
|
|
6
|
+
// Preserved verbatim except: `new Date()` becomes `new Date(isoNow)` and the
|
|
7
|
+
// credentials are passed in rather than read from process.env — so the signer is
|
|
8
|
+
// deterministic and comparable. AppSyncEventsSigner_OpsTest asserts the ReScript
|
|
9
|
+
// port produces byte-identical output to this reference, guarding against any
|
|
10
|
+
// drift in the canonical-request construction or the HMAC signing-key chain.
|
|
11
|
+
|
|
12
|
+
import { createHmac, createHash } from "node:crypto";
|
|
13
|
+
|
|
14
|
+
function sha256hex(data) {
|
|
15
|
+
return createHash("sha256").update(typeof data === "string" ? data : JSON.stringify(data)).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
function hmacBuf(key, data) {
|
|
18
|
+
return createHmac("sha256", key).update(data).digest();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function signedHeadersJs(host, path, body, region, isoNow, creds) {
|
|
22
|
+
const { accessKeyId, secretAccessKey, sessionToken } = creds;
|
|
23
|
+
const now = new Date(isoNow);
|
|
24
|
+
const amzDate = now.toISOString().replace(/[:\-]|\..../g, "").slice(0, 15) + "Z";
|
|
25
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
26
|
+
const headers = { host, "x-amz-date": amzDate, ...(sessionToken ? { "x-amz-security-token": sessionToken } : {}) };
|
|
27
|
+
const canonH = Object.entries(headers).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}:${v}\n`).join("");
|
|
28
|
+
const signH = Object.keys(headers).sort().join(";");
|
|
29
|
+
const cr = ["POST", path, "", canonH, signH, sha256hex(body)].join("\n");
|
|
30
|
+
const scope = `${dateStamp}/${region}/appsync/aws4_request`;
|
|
31
|
+
const sts = ["AWS4-HMAC-SHA256", amzDate, scope, sha256hex(cr)].join("\n");
|
|
32
|
+
const kDate = hmacBuf("AWS4" + secretAccessKey, dateStamp);
|
|
33
|
+
const kSigning = hmacBuf(hmacBuf(hmacBuf(kDate, region), "appsync"), "aws4_request");
|
|
34
|
+
const sig = createHmac("sha256", kSigning).update(sts).digest("hex");
|
|
35
|
+
return { ...headers, Authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signH}, Signature=${sig}` };
|
|
36
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Guards the typed cold-start core hoisted out of AutomationSliceEntryPoint.mjs
|
|
2
|
+
// — the entry point whose former shell had silently drifted from the reworked
|
|
3
|
+
// callback functors (single-applied curried Make, `todoItems.contents`, wrong
|
|
4
|
+
// phase1 shape) and threw on its first event:
|
|
5
|
+
// - parseHandlerConfig — incl. the new `bodyModule` + `context` fields the
|
|
6
|
+
// repaired wiring depends on.
|
|
7
|
+
// - makeAutomationJsonEventsHandler — raw envelope JSONs + context into
|
|
8
|
+
// phase 1, sync → awaited phase 2 → sync ordering.
|
|
9
|
+
// - makeOutboundJsonEventsHandler — tolerant DcbDecode of the inner `event`
|
|
10
|
+
// payload (non-consumed event types dropped silently), decoded events into
|
|
11
|
+
// phase 1.
|
|
12
|
+
|
|
13
|
+
open JestGlobals
|
|
14
|
+
|
|
15
|
+
let str = JSON.Encode.string
|
|
16
|
+
let obj = pairs => JSON.Encode.object(Dict.fromArray(pairs))
|
|
17
|
+
|
|
18
|
+
describe("AutomationSliceEntryPoint_Ops.parseHandlerConfig", () => {
|
|
19
|
+
testSync("empty raw config yields no handlers", () => {
|
|
20
|
+
expect(AutomationSliceEntryPoint_Ops.parseHandlerConfig("")->Array.length)->toBe(0)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
testSync("decodes a full entry incl. bodyModule and context", () => {
|
|
24
|
+
let config = obj([
|
|
25
|
+
(
|
|
26
|
+
"handlers",
|
|
27
|
+
JSON.Encode.array([
|
|
28
|
+
obj([
|
|
29
|
+
("specModule", str("@x/plugin/src/Order/AutomationSlice/Restock.res.mjs")),
|
|
30
|
+
("bodyModule", str("@x/plugin/src/Order/AutomationSlice/Restock_Automation.res.mjs")),
|
|
31
|
+
("callbackType", str("automation")),
|
|
32
|
+
("queryDbTableName", str("RestockTodo-abc")),
|
|
33
|
+
("dcbQueueUrl", str("https://sqs/queue.fifo")),
|
|
34
|
+
("sourceUrn", str("arn:aws:dynamodb:eu-west-1:1:table/log/stream/x")),
|
|
35
|
+
(
|
|
36
|
+
"context",
|
|
37
|
+
obj([
|
|
38
|
+
("environment", str("alpha")),
|
|
39
|
+
("platformName", str("OnlineShop")),
|
|
40
|
+
("pluginName", str("Ordering")),
|
|
41
|
+
("sliceName", str("Restock")),
|
|
42
|
+
]),
|
|
43
|
+
),
|
|
44
|
+
]),
|
|
45
|
+
]),
|
|
46
|
+
),
|
|
47
|
+
])->JSON.stringify
|
|
48
|
+
let entries = AutomationSliceEntryPoint_Ops.parseHandlerConfig(config)
|
|
49
|
+
expect(entries->Array.length)->toBe(1)
|
|
50
|
+
let e = entries->Array.getUnsafe(0)
|
|
51
|
+
expect(e.bodyModule)->toBe("@x/plugin/src/Order/AutomationSlice/Restock_Automation.res.mjs")
|
|
52
|
+
expect(e.callbackType)->toBe("automation")
|
|
53
|
+
expect(e.dcbQueueUrl)->toBe("https://sqs/queue.fifo")
|
|
54
|
+
switch e.context {
|
|
55
|
+
| Some(ctx) => {
|
|
56
|
+
expect(ctx.environment)->toBe("alpha")
|
|
57
|
+
expect(ctx.platformName)->toBe("OnlineShop")
|
|
58
|
+
expect(ctx.pluginName)->toBe("Ordering")
|
|
59
|
+
expect(ctx.sliceName)->toBe("Restock")
|
|
60
|
+
}
|
|
61
|
+
| None => JsError.throwWithMessage("expected Some(context)")
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
testSync("a stale entry without bodyModule/context decodes with defaults", () => {
|
|
66
|
+
let config = obj([
|
|
67
|
+
(
|
|
68
|
+
"handlers",
|
|
69
|
+
JSON.Encode.array([
|
|
70
|
+
obj([
|
|
71
|
+
("specModule", str("a.res.mjs")),
|
|
72
|
+
("callbackType", str("outbound")),
|
|
73
|
+
("queryDbTableName", str("t")),
|
|
74
|
+
("dcbQueueUrl", str("u")),
|
|
75
|
+
("sourceUrn", str("arn")),
|
|
76
|
+
]),
|
|
77
|
+
]),
|
|
78
|
+
),
|
|
79
|
+
])->JSON.stringify
|
|
80
|
+
let e = AutomationSliceEntryPoint_Ops.parseHandlerConfig(config)->Array.getUnsafe(0)
|
|
81
|
+
expect(e.bodyModule)->toBe("")
|
|
82
|
+
expect(e.context->Option.isNone)->toBe(true)
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Shared stubs — the pipelines only touch phase1/phase2 plus the injected
|
|
87
|
+
// sync/publish, so the callback records carry empty TODO dicts.
|
|
88
|
+
let noopPublish: ReventlessCore.CommandTopic.publishJsons = async _ => ()
|
|
89
|
+
|
|
90
|
+
describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandler", () => {
|
|
91
|
+
test("passes raw envelope JSONs + context to phase 1; sync → phase 2 → sync", async () => {
|
|
92
|
+
let steps = []
|
|
93
|
+
let receivedJsons = ref([])
|
|
94
|
+
let receivedCtx = ref(None)
|
|
95
|
+
let callback: AutomationSliceEntryPoint_Ops.automationCallback = {
|
|
96
|
+
todoItems: Dict.make(),
|
|
97
|
+
phase1: (jsons, ctx) => {
|
|
98
|
+
receivedJsons := jsons
|
|
99
|
+
receivedCtx := Some(ctx)
|
|
100
|
+
steps->Array.push("phase1")
|
|
101
|
+
},
|
|
102
|
+
phase2: async _publish => steps->Array.push("phase2"),
|
|
103
|
+
}
|
|
104
|
+
let context: Reventless.AutomationSlice.context = {
|
|
105
|
+
environment: "test",
|
|
106
|
+
platformName: "P",
|
|
107
|
+
pluginName: "Pl",
|
|
108
|
+
sliceName: "S",
|
|
109
|
+
}
|
|
110
|
+
let handler = AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandler(
|
|
111
|
+
~context,
|
|
112
|
+
~callback,
|
|
113
|
+
~publishJsons=noopPublish,
|
|
114
|
+
~syncTodoItems=async () => steps->Array.push("sync"),
|
|
115
|
+
)
|
|
116
|
+
let envelope = obj([("meta", obj([("service", str("Order"))])), ("event", str("Placed"))])
|
|
117
|
+
await Stream.fromIterable([envelope, str("bare")])->handler->Effect.runPromise
|
|
118
|
+
// Raw JSONs — the callback unwraps envelopes itself.
|
|
119
|
+
expect(receivedJsons.contents)->toEqual([envelope, str("bare")])
|
|
120
|
+
expect(receivedCtx.contents->Option.map(c => c.Reventless.AutomationSlice.sliceName))->toEqual(
|
|
121
|
+
Some("S"),
|
|
122
|
+
)
|
|
123
|
+
expect(steps)->toEqual(["phase1", "sync", "phase2", "sync"])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Consumed-event fixture for the outbound decode: one consumed variant with a
|
|
128
|
+
// payload, one payload-less, decoded from a source that also emits types this
|
|
129
|
+
// slice does not consume.
|
|
130
|
+
@schema
|
|
131
|
+
type outboundEvent =
|
|
132
|
+
| OrderPlaced({orderId: string})
|
|
133
|
+
| OrderArchived
|
|
134
|
+
|
|
135
|
+
describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler", () => {
|
|
136
|
+
test("decodes inner event payloads, drops non-consumed types silently", async () => {
|
|
137
|
+
let steps = []
|
|
138
|
+
let received = ref([])
|
|
139
|
+
let callback: AutomationSliceEntryPoint_Ops.outboundCallback<outboundEvent> = {
|
|
140
|
+
todoItems: Dict.make(),
|
|
141
|
+
phase1: events => {
|
|
142
|
+
received := events
|
|
143
|
+
steps->Array.push("phase1")
|
|
144
|
+
},
|
|
145
|
+
phase2: async _publish => steps->Array.push("phase2"),
|
|
146
|
+
}
|
|
147
|
+
let handler = AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler(
|
|
148
|
+
~consumedEventSchema=outboundEventSchema,
|
|
149
|
+
~callback,
|
|
150
|
+
~publishJsons=noopPublish,
|
|
151
|
+
~syncTodoItems=async () => steps->Array.push("sync"),
|
|
152
|
+
)
|
|
153
|
+
let placed = obj([
|
|
154
|
+
("meta", obj([("service", str("Order"))])),
|
|
155
|
+
("event", obj([("TAG", str("OrderPlaced")), ("orderId", str("ord-1"))])),
|
|
156
|
+
])
|
|
157
|
+
// Not consumed by this slice — dropped without an error.
|
|
158
|
+
let ignored = obj([("event", obj([("TAG", str("OrderShipped")), ("orderId", str("ord-1"))]))])
|
|
159
|
+
// Bare payload-less variant, no envelope.
|
|
160
|
+
let archived = str("OrderArchived")
|
|
161
|
+
await Stream.fromIterable([placed, ignored, archived])->handler->Effect.runPromise
|
|
162
|
+
expect(received.contents)->toEqual([OrderPlaced({orderId: "ord-1"}), OrderArchived])
|
|
163
|
+
expect(steps)->toEqual(["phase1", "sync", "phase2", "sync"])
|
|
164
|
+
})
|
|
165
|
+
})
|