@reventlessdev/reventless-aws 3.0.0-alpha.231 → 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 +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_Ops.res +16 -3
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +11 -6
- 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
|
@@ -16,165 +16,6 @@ import * as Util_DynamoDbStream$ReventlessAws from "../../util/Util_DynamoDbStre
|
|
|
16
16
|
|
|
17
17
|
let registry = {};
|
|
18
18
|
|
|
19
|
-
let handlerCode = `
|
|
20
|
-
import { createHmac, createHash } from "node:crypto";
|
|
21
|
-
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
22
|
-
|
|
23
|
-
const TOPIC_MAP = JSON.parse(process.env.STATE_TOPIC_MAP || "{}");
|
|
24
|
-
const APPSYNC_ENDPOINT = process.env.APPSYNC_ENDPOINT; // "https://{eventsApi.dns.http}"
|
|
25
|
-
const AWS_REGION = process.env.AWS_REGION ?? "eu-west-1";
|
|
26
|
-
|
|
27
|
-
function sha256hex(data) {
|
|
28
|
-
return createHash("sha256").update(typeof data === "string" ? data : JSON.stringify(data)).digest("hex");
|
|
29
|
-
}
|
|
30
|
-
function hmacBuf(key, data) {
|
|
31
|
-
return createHmac("sha256", key).update(data).digest();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function signedHeaders(host, path, body) {
|
|
35
|
-
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
36
|
-
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
37
|
-
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
38
|
-
const now = new Date();
|
|
39
|
-
const amzDate = now.toISOString().replace(/[:\\-]|\\..../g, "").slice(0, 15) + "Z";
|
|
40
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
41
|
-
const headers = { host, "x-amz-date": amzDate, ...(sessionToken ? { "x-amz-security-token": sessionToken } : {}) };
|
|
42
|
-
const canonH = Object.entries(headers).sort(([a],[b]) => a.localeCompare(b)).map(([k,v]) => \`\${k}:\${v}\\n\`).join("");
|
|
43
|
-
const signH = Object.keys(headers).sort().join(";");
|
|
44
|
-
const cr = ["POST", path, "", canonH, signH, sha256hex(body)].join("\\n");
|
|
45
|
-
const scope = \`\${dateStamp}/\${AWS_REGION}/appsync/aws4_request\`;
|
|
46
|
-
const sts = ["AWS4-HMAC-SHA256", amzDate, scope, sha256hex(cr)].join("\\n");
|
|
47
|
-
const kDate = hmacBuf("AWS4" + secretAccessKey, dateStamp);
|
|
48
|
-
const kSigning = hmacBuf(hmacBuf(hmacBuf(kDate, AWS_REGION), "appsync"), "aws4_request");
|
|
49
|
-
const sig = createHmac("sha256", kSigning).update(sts).digest("hex");
|
|
50
|
-
return { ...headers, Authorization: \`AWS4-HMAC-SHA256 Credential=\${accessKeyId}/\${scope}, SignedHeaders=\${signH}, Signature=\${sig}\` };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// AppSync Events channel segments allow only [A-Za-z0-9-]. Anything else
|
|
54
|
-
// (\`@\`, \`.\`, \`:\`, \`_\`, \`/\`, …) collapses to \`-\` so the segment is a
|
|
55
|
-
// legal channel-path token. The UI's AutoLive.normalizeSegment uses the same
|
|
56
|
-
// rule for Entity-scoped subscribes.
|
|
57
|
-
function pathSegment(value) {
|
|
58
|
-
return String(value).replace(/[^A-Za-z0-9-]/g, "-");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Map record.eventSourceARN → topicRoot via STATE_TOPIC_MAP.
|
|
62
|
-
// Stream ARNs look like: arn:aws:dynamodb:<region>:<acct>:table/<TableName>/stream/<ts>
|
|
63
|
-
function topicRootFromEventSourceArn(arn) {
|
|
64
|
-
const m = arn && arn.match(/:table\\/([^/]+)\\/stream\\//);
|
|
65
|
-
if (!m) return undefined;
|
|
66
|
-
const topicName = TOPIC_MAP[m[1]];
|
|
67
|
-
if (topicName === undefined) return undefined;
|
|
68
|
-
return pathSegment(topicName);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Build entityKey from record.dynamodb.Keys. Framework convention:
|
|
72
|
-
// partition-key attr is named "id"; composite tables have one extra attr (the
|
|
73
|
-
// sort key) whose name comes from the projection's subIdConfig.subIdField.
|
|
74
|
-
// Returns the ORIGINAL key string — the descriptor body uses this verbatim so
|
|
75
|
-
// AutoListView can match it against the GraphQL row.id. The channel path
|
|
76
|
-
// segment is derived later via pathSegment().
|
|
77
|
-
function entityKeyFromRecord(record) {
|
|
78
|
-
const keys = unmarshall(record.dynamodb.Keys ?? {});
|
|
79
|
-
const id = keys.id;
|
|
80
|
-
if (id === undefined) {
|
|
81
|
-
// Defensive: framework always names the partition key "id". If a future
|
|
82
|
-
// table breaks the convention, fall back to a stable sort-join.
|
|
83
|
-
const names = Object.keys(keys).sort();
|
|
84
|
-
return names.map((n) => String(keys[n])).join("-");
|
|
85
|
-
}
|
|
86
|
-
const subIdName = Object.keys(keys).find((k) => k !== "id");
|
|
87
|
-
return subIdName === undefined
|
|
88
|
-
? String(id)
|
|
89
|
-
: String(id) + "-" + String(keys[subIdName]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Map DDB stream eventName → descriptor changeKind.
|
|
93
|
-
function changeKindFor(eventName) {
|
|
94
|
-
switch (eventName) {
|
|
95
|
-
case "INSERT": return "Added";
|
|
96
|
-
case "MODIFY": return "Updated";
|
|
97
|
-
case "REMOVE": return "Removed";
|
|
98
|
-
default: return "Updated"; // defensive default — shouldn't fire
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Pick the "natural" sort timestamp from an unmarshalled image. Conventions
|
|
103
|
-
// in this codebase prefer updatedAt; createdAt is the fallback for views that
|
|
104
|
-
// never mutate after insert. Returns undefined if neither is present.
|
|
105
|
-
function pickSortKeyValue(image) {
|
|
106
|
-
if (image && typeof image.updatedAt === "string") return image.updatedAt;
|
|
107
|
-
if (image && typeof image.createdAt === "string") return image.createdAt;
|
|
108
|
-
return undefined;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export async function handler(event) {
|
|
112
|
-
const url = new URL(APPSYNC_ENDPOINT);
|
|
113
|
-
let transientErr;
|
|
114
|
-
for (const record of event.Records) {
|
|
115
|
-
const topicRoot = topicRootFromEventSourceArn(record.eventSourceARN);
|
|
116
|
-
if (topicRoot === undefined) {
|
|
117
|
-
console.warn("StateTopic: unknown table for ARN", record.eventSourceARN);
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const channelRoot = "/default/" + topicRoot;
|
|
121
|
-
const image =
|
|
122
|
-
record.eventName === "REMOVE"
|
|
123
|
-
? record.dynamodb.OldImage
|
|
124
|
-
: record.dynamodb.NewImage;
|
|
125
|
-
if (image === undefined) continue;
|
|
126
|
-
const entityKey = entityKeyFromRecord(record);
|
|
127
|
-
const channel = channelRoot + "/" + pathSegment(entityKey);
|
|
128
|
-
const unmarshalled = unmarshall(image);
|
|
129
|
-
const descriptor = {
|
|
130
|
-
changeKind: changeKindFor(record.eventName),
|
|
131
|
-
id: entityKey,
|
|
132
|
-
};
|
|
133
|
-
const sortKeyValue = pickSortKeyValue(unmarshalled);
|
|
134
|
-
if (sortKeyValue !== undefined) descriptor.sortKeyValue = sortKeyValue;
|
|
135
|
-
const body = JSON.stringify({ id: record.eventID, channel, events: [JSON.stringify(descriptor)] });
|
|
136
|
-
const auth = await signedHeaders(url.hostname, "/event", body);
|
|
137
|
-
try {
|
|
138
|
-
const res = await fetch(APPSYNC_ENDPOINT + "/event", {
|
|
139
|
-
method: "POST",
|
|
140
|
-
headers: { accept: "application/json, text/javascript", "content-encoding": "amz-1.0", "content-type": "application/json; charset=UTF-8", ...auth },
|
|
141
|
-
body,
|
|
142
|
-
});
|
|
143
|
-
if (!res.ok) {
|
|
144
|
-
const txt = await res.text();
|
|
145
|
-
// 4xx: permanent (bad request, auth, channel format) — retrying won't
|
|
146
|
-
// help. Log structurally so a CloudWatch metric filter on
|
|
147
|
-
// "STATE_TOPIC_PUBLISH_FAILED type=permanent" surfaces it; continue
|
|
148
|
-
// processing so one bad record doesn't block the rest of the batch.
|
|
149
|
-
// 5xx / network: transient — record it and throw at the end so the
|
|
150
|
-
// ESM retries (bounded by maximumRetryAttempts + bisectBatch).
|
|
151
|
-
const isTransient = res.status >= 500;
|
|
152
|
-
const tag = isTransient ? "transient" : "permanent";
|
|
153
|
-
console.error(
|
|
154
|
-
"STATE_TOPIC_PUBLISH_FAILED type=" + tag +
|
|
155
|
-
" status=" + res.status +
|
|
156
|
-
" channel=" + channel +
|
|
157
|
-
" body=" + txt.slice(0, 500)
|
|
158
|
-
);
|
|
159
|
-
if (isTransient) transientErr = new Error("StateTopic publish failed: " + res.status);
|
|
160
|
-
}
|
|
161
|
-
} catch (e) {
|
|
162
|
-
// Network-level error (DNS, connection refused, timeout) — always transient.
|
|
163
|
-
console.error(
|
|
164
|
-
"STATE_TOPIC_PUBLISH_FAILED type=transient status=network" +
|
|
165
|
-
" channel=" + channel +
|
|
166
|
-
" err=" + (e && e.message)
|
|
167
|
-
);
|
|
168
|
-
transientErr = e;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
// Throw after the loop so a single transient error doesn't stop us from
|
|
172
|
-
// emitting other records' descriptors first. The ESM will retry the batch;
|
|
173
|
-
// bisectBatchOnFunctionError isolates the bad record across retries.
|
|
174
|
-
if (transientErr) throw transientErr;
|
|
175
|
-
}
|
|
176
|
-
`;
|
|
177
|
-
|
|
178
19
|
function make(readModelName, topicName, allQueryDbs, eventsApi, param) {
|
|
179
20
|
let streamResource = Util_DynamoDbStream$ReventlessAws.findResource(Util_ReadModel$ReventlessCore.queryDbStorageResources(allQueryDbs, readModelName));
|
|
180
21
|
let streamArn = Util_DynamoDbStream$ReventlessAws.streamArnFromDynamoDbTableResource(streamResource);
|
|
@@ -241,17 +82,17 @@ function finish(eventsApi, opts) {
|
|
|
241
82
|
});
|
|
242
83
|
return JSON.stringify(dict);
|
|
243
84
|
});
|
|
244
|
-
let
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
let
|
|
85
|
+
let packageDirs = Object.fromEntries([[
|
|
86
|
+
"@reventlessdev/reventless-aws",
|
|
87
|
+
Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws")
|
|
88
|
+
]]);
|
|
89
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs", packageDirs, undefined);
|
|
249
90
|
let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
|
|
250
91
|
let appsyncEndpoint = AppSync_EventsApi$ReventlessAws.httpEndpoint(eventsApi);
|
|
251
92
|
let lambda = new (Aws.lambda.Function)(name + "StateTopicPublisher", {
|
|
252
93
|
handler: "index.handler",
|
|
253
94
|
runtime: "nodejs22.x",
|
|
254
|
-
code: code,
|
|
95
|
+
code: match.code,
|
|
255
96
|
role: lambdaRole.arn,
|
|
256
97
|
memorySize: 256,
|
|
257
98
|
timeout: 30,
|
|
@@ -281,7 +122,7 @@ function finish(eventsApi, opts) {
|
|
|
281
122
|
]
|
|
282
123
|
])
|
|
283
124
|
},
|
|
284
|
-
sourceCodeHash: sourceCodeHash
|
|
125
|
+
sourceCodeHash: match.sourceCodeHash
|
|
285
126
|
}, opts);
|
|
286
127
|
entries.forEach(entry => {
|
|
287
128
|
let esmName = entry.topicName + "Stream2" + name + "StateTopic";
|
|
@@ -299,7 +140,6 @@ function finish(eventsApi, opts) {
|
|
|
299
140
|
|
|
300
141
|
export {
|
|
301
142
|
registry,
|
|
302
|
-
handlerCode,
|
|
303
143
|
make,
|
|
304
144
|
finish,
|
|
305
145
|
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// Runtime handler for the read-model → AppSync Events state-topic relay —
|
|
2
|
+
// compiled, type-checked ReScript (replaces the inline JS `handlerCode` string
|
|
3
|
+
// in StateTopic_AppSync.res). Runtime-pure and SDK-free for signing (node:crypto
|
|
4
|
+
// via AppSyncEventsSigner_Ops; see the parity test); it uses the rescript-aws-sdk
|
|
5
|
+
// util-dynamodb unmarshaller (also Pulumi-free).
|
|
6
|
+
//
|
|
7
|
+
// Triggered by DynamoDB streams (one shared Lambda; routing is per-record via
|
|
8
|
+
// STATE_TOPIC_MAP). For each changed row it derives the entity channel and a
|
|
9
|
+
// `{changeKind, id, sortKeyValue?}` descriptor and publishes it. 4xx failures
|
|
10
|
+
// are logged and skipped; 5xx / network failures are recorded and rethrown after
|
|
11
|
+
// the batch so the EventSourceMapping retries (bisectBatchOnFunctionError).
|
|
12
|
+
|
|
13
|
+
@val @scope("process") external processEnv: dict<string> = "env"
|
|
14
|
+
|
|
15
|
+
let endpoint = processEnv->Dict.get("APPSYNC_ENDPOINT")->Option.getOr("")
|
|
16
|
+
|
|
17
|
+
// STATE_TOPIC_MAP: `{ <tableName>: <topicName> }`, injected at deploy time.
|
|
18
|
+
let topicMap: dict<string> =
|
|
19
|
+
switch processEnv->Dict.get("STATE_TOPIC_MAP")->Option.getOr("{}")->JSON.parseOrThrow->JSON.Decode.object {
|
|
20
|
+
| Some(obj) =>
|
|
21
|
+
obj
|
|
22
|
+
->Dict.toArray
|
|
23
|
+
->Array.filterMap(((k, v)) => v->JSON.Decode.string->Option.map(s => (k, s)))
|
|
24
|
+
->Dict.fromArray
|
|
25
|
+
| None => Dict.make()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── DynamoDB stream event (only the fields this handler reads) ───────────────
|
|
29
|
+
|
|
30
|
+
type attributeValue = AwsSdk.DynamoDb.Util.attributeValue
|
|
31
|
+
type streamRecord = {
|
|
32
|
+
@as("Keys") keys: dict<attributeValue>,
|
|
33
|
+
@as("NewImage") newImage?: dict<attributeValue>,
|
|
34
|
+
@as("OldImage") oldImage?: dict<attributeValue>,
|
|
35
|
+
}
|
|
36
|
+
type record = {
|
|
37
|
+
eventID: string,
|
|
38
|
+
eventName: string, // "INSERT" | "MODIFY" | "REMOVE"
|
|
39
|
+
eventSourceARN: string,
|
|
40
|
+
dynamodb?: streamRecord,
|
|
41
|
+
}
|
|
42
|
+
type event = {@as("Records") records: array<record>}
|
|
43
|
+
|
|
44
|
+
// ── Per-record derivations (ports of the former inline JS helpers) ───────────
|
|
45
|
+
|
|
46
|
+
// Map record.eventSourceARN → topicRoot via STATE_TOPIC_MAP. Stream ARNs look
|
|
47
|
+
// like arn:aws:dynamodb:<region>:<acct>:table/<TableName>/stream/<ts>; split on
|
|
48
|
+
// "/" gives [ "...:table", "<TableName>", "stream", "<ts>" ].
|
|
49
|
+
let topicRootFromEventSourceArn = (arn: string): option<string> => {
|
|
50
|
+
let parts = arn->String.split("/")
|
|
51
|
+
switch (parts->Array.get(0), parts->Array.get(1), parts->Array.get(2)) {
|
|
52
|
+
| (Some(prefix), Some(tableName), Some("stream")) if prefix->String.endsWith(":table") =>
|
|
53
|
+
topicMap->Dict.get(tableName)->Option.map(AppSyncEventsSigner_Ops.pathSegment)
|
|
54
|
+
| _ => None
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// `String(value)` parity for scalar key attributes.
|
|
59
|
+
let jsonToString = (j: JSON.t): string =>
|
|
60
|
+
switch j {
|
|
61
|
+
| String(s) => s
|
|
62
|
+
| Number(n) => n->Float.toString
|
|
63
|
+
| Boolean(b) => b ? "true" : "false"
|
|
64
|
+
| Null => "null"
|
|
65
|
+
| _ => j->JSON.stringify
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Build entityKey from the record's Keys. Framework convention: the partition
|
|
69
|
+
// key attribute is "id"; a composite table adds one sort-key attribute. Returns
|
|
70
|
+
// the ORIGINAL key string (channel-segmentised later via pathSegment).
|
|
71
|
+
let entityKeyFromRecord = (dynamodb: streamRecord): string => {
|
|
72
|
+
let keys: dict<JSON.t> = AwsSdk.DynamoDb_Util_Helpers.unmarshallDict(dynamodb.keys)
|
|
73
|
+
switch keys->Dict.get("id") {
|
|
74
|
+
| None =>
|
|
75
|
+
// Defensive: framework always names the partition key "id"; fall back to a
|
|
76
|
+
// stable sort-join if a future table breaks the convention.
|
|
77
|
+
keys
|
|
78
|
+
->Dict.keysToArray
|
|
79
|
+
->Array.toSorted(String.compare)
|
|
80
|
+
->Array.map(n => keys->Dict.get(n)->Option.mapOr("", jsonToString))
|
|
81
|
+
->Array.join("-")
|
|
82
|
+
| Some(id) =>
|
|
83
|
+
switch keys->Dict.keysToArray->Array.find(k => k != "id") {
|
|
84
|
+
| None => id->jsonToString
|
|
85
|
+
| Some(subIdName) =>
|
|
86
|
+
id->jsonToString ++ "-" ++ keys->Dict.get(subIdName)->Option.mapOr("", jsonToString)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Map DDB stream eventName → descriptor changeKind.
|
|
92
|
+
let changeKindFor = (eventName: string): string =>
|
|
93
|
+
switch eventName {
|
|
94
|
+
| "INSERT" => "Added"
|
|
95
|
+
| "MODIFY" => "Updated"
|
|
96
|
+
| "REMOVE" => "Removed"
|
|
97
|
+
| _ => "Updated" // defensive default — shouldn't fire
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Prefer updatedAt, then createdAt; None if neither is a string.
|
|
101
|
+
let pickSortKeyValue = (image: dict<JSON.t>): option<string> =>
|
|
102
|
+
switch image->Dict.get("updatedAt")->Option.flatMap(JSON.Decode.string) {
|
|
103
|
+
| Some(v) => Some(v)
|
|
104
|
+
| None => image->Dict.get("createdAt")->Option.flatMap(JSON.Decode.string)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Publish one record; returns Some(errorMessage) on a transient (5xx / network)
|
|
108
|
+
// failure so the caller can rethrow after the batch, None otherwise.
|
|
109
|
+
let processRecord = async (
|
|
110
|
+
~record: record,
|
|
111
|
+
~region: string,
|
|
112
|
+
~creds: AppSyncEventsSigner_Ops.creds,
|
|
113
|
+
): option<string> =>
|
|
114
|
+
switch topicRootFromEventSourceArn(record.eventSourceARN) {
|
|
115
|
+
| None =>
|
|
116
|
+
Console.warn2("StateTopic: unknown table for ARN", record.eventSourceARN)
|
|
117
|
+
None
|
|
118
|
+
| Some(topicRoot) =>
|
|
119
|
+
switch record.dynamodb {
|
|
120
|
+
| None => None
|
|
121
|
+
| Some(dynamodb) =>
|
|
122
|
+
let image = if record.eventName == "REMOVE" {
|
|
123
|
+
dynamodb.oldImage
|
|
124
|
+
} else {
|
|
125
|
+
dynamodb.newImage
|
|
126
|
+
}
|
|
127
|
+
switch image {
|
|
128
|
+
| None => None
|
|
129
|
+
| Some(image) =>
|
|
130
|
+
let entityKey = entityKeyFromRecord(dynamodb)
|
|
131
|
+
let channel = `/default/${topicRoot}/${AppSyncEventsSigner_Ops.pathSegment(entityKey)}`
|
|
132
|
+
let unmarshalled: dict<JSON.t> = AwsSdk.DynamoDb_Util_Helpers.unmarshallDict(image)
|
|
133
|
+
let descriptor = Dict.make()
|
|
134
|
+
descriptor->Dict.set("changeKind", JSON.Encode.string(changeKindFor(record.eventName)))
|
|
135
|
+
descriptor->Dict.set("id", JSON.Encode.string(entityKey))
|
|
136
|
+
pickSortKeyValue(unmarshalled)->Option.forEach(v =>
|
|
137
|
+
descriptor->Dict.set("sortKeyValue", JSON.Encode.string(v))
|
|
138
|
+
)
|
|
139
|
+
let body =
|
|
140
|
+
Dict.fromArray([
|
|
141
|
+
("id", JSON.Encode.string(record.eventID)),
|
|
142
|
+
("channel", JSON.Encode.string(channel)),
|
|
143
|
+
(
|
|
144
|
+
"events",
|
|
145
|
+
JSON.Encode.array([JSON.Encode.string(descriptor->JSON.Encode.object->JSON.stringify)]),
|
|
146
|
+
),
|
|
147
|
+
])
|
|
148
|
+
->JSON.Encode.object
|
|
149
|
+
->JSON.stringify
|
|
150
|
+
try {
|
|
151
|
+
let res = await AppSyncEventsSigner_Ops.postEvent(
|
|
152
|
+
~endpoint,
|
|
153
|
+
~region,
|
|
154
|
+
~isoNow=Date.make()->Date.toISOString,
|
|
155
|
+
~creds,
|
|
156
|
+
~body,
|
|
157
|
+
)
|
|
158
|
+
if res->AppSyncEventsSigner_Ops.responseOk {
|
|
159
|
+
None
|
|
160
|
+
} else {
|
|
161
|
+
let status = res->AppSyncEventsSigner_Ops.responseStatus
|
|
162
|
+
let txt = await res->AppSyncEventsSigner_Ops.responseText
|
|
163
|
+
let isTransient = status >= 500
|
|
164
|
+
let tag = isTransient ? "transient" : "permanent"
|
|
165
|
+
// 4xx: permanent (bad request, auth, channel format) — retrying won't
|
|
166
|
+
// help. 5xx: transient — recorded and rethrown so the ESM retries.
|
|
167
|
+
// Structured so a CloudWatch metric filter can surface each type.
|
|
168
|
+
Console.error(
|
|
169
|
+
`STATE_TOPIC_PUBLISH_FAILED type=${tag} status=${status->Int.toString} channel=${channel} body=${txt->String.slice(
|
|
170
|
+
~start=0,
|
|
171
|
+
~end=500,
|
|
172
|
+
)}`,
|
|
173
|
+
)
|
|
174
|
+
isTransient ? Some(`StateTopic publish failed: ${status->Int.toString}`) : None
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
| exn =>
|
|
178
|
+
// Network-level error (DNS, connection refused, timeout) — always transient.
|
|
179
|
+
let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("")
|
|
180
|
+
Console.error(
|
|
181
|
+
`STATE_TOPIC_PUBLISH_FAILED type=transient status=network channel=${channel} err=${msg}`,
|
|
182
|
+
)
|
|
183
|
+
Some(msg == "" ? "StateTopic network error" : msg)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let handler = async (event: event): unit => {
|
|
190
|
+
let region = AppSyncEventsSigner_Ops.region()
|
|
191
|
+
let creds = AppSyncEventsSigner_Ops.envCreds()
|
|
192
|
+
let transientErr = ref(None)
|
|
193
|
+
// Sequential, mirroring the former for-await loop.
|
|
194
|
+
await event.records->Array.reduce(Promise.resolve(), (acc, record) =>
|
|
195
|
+
acc->Promise.then(async _ =>
|
|
196
|
+
switch await processRecord(~record, ~region, ~creds) {
|
|
197
|
+
| Some(msg) => transientErr := Some(msg)
|
|
198
|
+
| None => ()
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
)
|
|
202
|
+
// Throw after the loop so one transient failure doesn't stop the other records'
|
|
203
|
+
// descriptors from being emitted first; the ESM retries the batch.
|
|
204
|
+
switch transientErr.contents {
|
|
205
|
+
| Some(msg) => JsError.throwWithMessage(msg)
|
|
206
|
+
| None => ()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -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 {
|