@reventlessdev/reventless-aws 3.0.0-alpha.232 → 3.0.0-alpha.233
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 +16 -0
- package/package.json +8 -8
- 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_Ops.res +11 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +9 -3
- 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/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/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/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,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
|
+
})
|
|
@@ -0,0 +1,280 @@
|
|
|
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 Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
8
|
+
import * as AutomationSliceEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs";
|
|
9
|
+
|
|
10
|
+
function str(prim) {
|
|
11
|
+
return prim;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function obj(pairs) {
|
|
15
|
+
return Object.fromEntries(pairs);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
globalThis.describe("AutomationSliceEntryPoint_Ops.parseHandlerConfig", () => {
|
|
19
|
+
globalThis.test("empty raw config yields no handlers", () => {
|
|
20
|
+
globalThis.expect(AutomationSliceEntryPoint_Ops$ReventlessAws.parseHandlerConfig("").length).toBe(0);
|
|
21
|
+
});
|
|
22
|
+
globalThis.test("decodes a full entry incl. bodyModule and context", () => {
|
|
23
|
+
let config = JSON.stringify(Object.fromEntries([[
|
|
24
|
+
"handlers",
|
|
25
|
+
[Object.fromEntries([
|
|
26
|
+
[
|
|
27
|
+
"specModule",
|
|
28
|
+
"@x/plugin/src/Order/AutomationSlice/Restock.res.mjs"
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
"bodyModule",
|
|
32
|
+
"@x/plugin/src/Order/AutomationSlice/Restock_Automation.res.mjs"
|
|
33
|
+
],
|
|
34
|
+
[
|
|
35
|
+
"callbackType",
|
|
36
|
+
"automation"
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"queryDbTableName",
|
|
40
|
+
"RestockTodo-abc"
|
|
41
|
+
],
|
|
42
|
+
[
|
|
43
|
+
"dcbQueueUrl",
|
|
44
|
+
"https://sqs/queue.fifo"
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"sourceUrn",
|
|
48
|
+
"arn:aws:dynamodb:eu-west-1:1:table/log/stream/x"
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"context",
|
|
52
|
+
Object.fromEntries([
|
|
53
|
+
[
|
|
54
|
+
"environment",
|
|
55
|
+
"alpha"
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"platformName",
|
|
59
|
+
"OnlineShop"
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"pluginName",
|
|
63
|
+
"Ordering"
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"sliceName",
|
|
67
|
+
"Restock"
|
|
68
|
+
]
|
|
69
|
+
])
|
|
70
|
+
]
|
|
71
|
+
])]
|
|
72
|
+
]]));
|
|
73
|
+
let entries = AutomationSliceEntryPoint_Ops$ReventlessAws.parseHandlerConfig(config);
|
|
74
|
+
globalThis.expect(entries.length).toBe(1);
|
|
75
|
+
let e = entries[0];
|
|
76
|
+
globalThis.expect(e.bodyModule).toBe("@x/plugin/src/Order/AutomationSlice/Restock_Automation.res.mjs");
|
|
77
|
+
globalThis.expect(e.callbackType).toBe("automation");
|
|
78
|
+
globalThis.expect(e.dcbQueueUrl).toBe("https://sqs/queue.fifo");
|
|
79
|
+
let ctx = e.context;
|
|
80
|
+
if (ctx !== undefined) {
|
|
81
|
+
globalThis.expect(ctx.environment).toBe("alpha");
|
|
82
|
+
globalThis.expect(ctx.platformName).toBe("OnlineShop");
|
|
83
|
+
globalThis.expect(ctx.pluginName).toBe("Ordering");
|
|
84
|
+
globalThis.expect(ctx.sliceName).toBe("Restock");
|
|
85
|
+
return;
|
|
86
|
+
} else {
|
|
87
|
+
return Stdlib_JsError.throwWithMessage("expected Some(context)");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
globalThis.test("a stale entry without bodyModule/context decodes with defaults", () => {
|
|
91
|
+
let config = JSON.stringify(Object.fromEntries([[
|
|
92
|
+
"handlers",
|
|
93
|
+
[Object.fromEntries([
|
|
94
|
+
[
|
|
95
|
+
"specModule",
|
|
96
|
+
"a.res.mjs"
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
"callbackType",
|
|
100
|
+
"outbound"
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
"queryDbTableName",
|
|
104
|
+
"t"
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
"dcbQueueUrl",
|
|
108
|
+
"u"
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
"sourceUrn",
|
|
112
|
+
"arn"
|
|
113
|
+
]
|
|
114
|
+
])]
|
|
115
|
+
]]));
|
|
116
|
+
let e = AutomationSliceEntryPoint_Ops$ReventlessAws.parseHandlerConfig(config)[0];
|
|
117
|
+
globalThis.expect(e.bodyModule).toBe("");
|
|
118
|
+
globalThis.expect(Stdlib_Option.isNone(e.context)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
async function noopPublish(param) {
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
globalThis.describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandler", () => {
|
|
127
|
+
globalThis.test("passes raw envelope JSONs + context to phase 1; sync → phase 2 → sync", async () => {
|
|
128
|
+
let steps = [];
|
|
129
|
+
let receivedJsons = {
|
|
130
|
+
contents: []
|
|
131
|
+
};
|
|
132
|
+
let receivedCtx = {
|
|
133
|
+
contents: undefined
|
|
134
|
+
};
|
|
135
|
+
let callback_todoItems = {};
|
|
136
|
+
let callback_phase1 = (jsons, ctx) => {
|
|
137
|
+
receivedJsons.contents = jsons;
|
|
138
|
+
receivedCtx.contents = ctx;
|
|
139
|
+
steps.push("phase1");
|
|
140
|
+
};
|
|
141
|
+
let callback_phase2 = async _publish => {
|
|
142
|
+
steps.push("phase2");
|
|
143
|
+
};
|
|
144
|
+
let callback = {
|
|
145
|
+
todoItems: callback_todoItems,
|
|
146
|
+
phase1: callback_phase1,
|
|
147
|
+
phase2: callback_phase2
|
|
148
|
+
};
|
|
149
|
+
let handler = AutomationSliceEntryPoint_Ops$ReventlessAws.makeAutomationJsonEventsHandler({
|
|
150
|
+
environment: "test",
|
|
151
|
+
platformName: "P",
|
|
152
|
+
pluginName: "Pl",
|
|
153
|
+
sliceName: "S"
|
|
154
|
+
}, callback, noopPublish, async () => {
|
|
155
|
+
steps.push("sync");
|
|
156
|
+
});
|
|
157
|
+
let envelope = Object.fromEntries([
|
|
158
|
+
[
|
|
159
|
+
"meta",
|
|
160
|
+
Object.fromEntries([[
|
|
161
|
+
"service",
|
|
162
|
+
"Order"
|
|
163
|
+
]])
|
|
164
|
+
],
|
|
165
|
+
[
|
|
166
|
+
"event",
|
|
167
|
+
"Placed"
|
|
168
|
+
]
|
|
169
|
+
]);
|
|
170
|
+
await Effect.runPromise(handler(Stream.fromIterable([
|
|
171
|
+
envelope,
|
|
172
|
+
"bare"
|
|
173
|
+
])));
|
|
174
|
+
globalThis.expect(receivedJsons.contents).toEqual([
|
|
175
|
+
envelope,
|
|
176
|
+
"bare"
|
|
177
|
+
]);
|
|
178
|
+
globalThis.expect(Stdlib_Option.map(receivedCtx.contents, c => c.sliceName)).toEqual("S");
|
|
179
|
+
globalThis.expect(steps).toEqual([
|
|
180
|
+
"phase1",
|
|
181
|
+
"sync",
|
|
182
|
+
"phase2",
|
|
183
|
+
"sync"
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
let outboundEventSchema = S.union([
|
|
189
|
+
S.schema(s => ({
|
|
190
|
+
TAG: "OrderPlaced",
|
|
191
|
+
orderId: s.m(S.string)
|
|
192
|
+
})),
|
|
193
|
+
S.literal("OrderArchived")
|
|
194
|
+
]);
|
|
195
|
+
|
|
196
|
+
globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler", () => {
|
|
197
|
+
globalThis.test("decodes inner event payloads, drops non-consumed types silently", async () => {
|
|
198
|
+
let steps = [];
|
|
199
|
+
let received = {
|
|
200
|
+
contents: []
|
|
201
|
+
};
|
|
202
|
+
let callback_todoItems = {};
|
|
203
|
+
let callback_phase1 = events => {
|
|
204
|
+
received.contents = events;
|
|
205
|
+
steps.push("phase1");
|
|
206
|
+
};
|
|
207
|
+
let callback_phase2 = async _publish => {
|
|
208
|
+
steps.push("phase2");
|
|
209
|
+
};
|
|
210
|
+
let callback = {
|
|
211
|
+
todoItems: callback_todoItems,
|
|
212
|
+
phase1: callback_phase1,
|
|
213
|
+
phase2: callback_phase2
|
|
214
|
+
};
|
|
215
|
+
let handler = AutomationSliceEntryPoint_Ops$ReventlessAws.makeOutboundJsonEventsHandler(outboundEventSchema, callback, noopPublish, async () => {
|
|
216
|
+
steps.push("sync");
|
|
217
|
+
});
|
|
218
|
+
let placed = Object.fromEntries([
|
|
219
|
+
[
|
|
220
|
+
"meta",
|
|
221
|
+
Object.fromEntries([[
|
|
222
|
+
"service",
|
|
223
|
+
"Order"
|
|
224
|
+
]])
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"event",
|
|
228
|
+
Object.fromEntries([
|
|
229
|
+
[
|
|
230
|
+
"TAG",
|
|
231
|
+
"OrderPlaced"
|
|
232
|
+
],
|
|
233
|
+
[
|
|
234
|
+
"orderId",
|
|
235
|
+
"ord-1"
|
|
236
|
+
]
|
|
237
|
+
])
|
|
238
|
+
]
|
|
239
|
+
]);
|
|
240
|
+
let ignored = Object.fromEntries([[
|
|
241
|
+
"event",
|
|
242
|
+
Object.fromEntries([
|
|
243
|
+
[
|
|
244
|
+
"TAG",
|
|
245
|
+
"OrderShipped"
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"orderId",
|
|
249
|
+
"ord-1"
|
|
250
|
+
]
|
|
251
|
+
])
|
|
252
|
+
]]);
|
|
253
|
+
await Effect.runPromise(handler(Stream.fromIterable([
|
|
254
|
+
placed,
|
|
255
|
+
ignored,
|
|
256
|
+
"OrderArchived"
|
|
257
|
+
])));
|
|
258
|
+
globalThis.expect(received.contents).toEqual([
|
|
259
|
+
{
|
|
260
|
+
TAG: "OrderPlaced",
|
|
261
|
+
orderId: "ord-1"
|
|
262
|
+
},
|
|
263
|
+
"OrderArchived"
|
|
264
|
+
]);
|
|
265
|
+
globalThis.expect(steps).toEqual([
|
|
266
|
+
"phase1",
|
|
267
|
+
"sync",
|
|
268
|
+
"phase2",
|
|
269
|
+
"sync"
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
export {
|
|
275
|
+
str,
|
|
276
|
+
obj,
|
|
277
|
+
noopPublish,
|
|
278
|
+
outboundEventSchema,
|
|
279
|
+
}
|
|
280
|
+
/* Not a pure module */
|