@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,198 @@
|
|
|
1
|
+
// Typed cold-start core for the TaskBucket Lambda entry point.
|
|
2
|
+
//
|
|
3
|
+
// The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
|
|
4
|
+
// TaskBucketEntryPoint.mjs keeps only the inherently-untyped seam — the
|
|
5
|
+
// dynamic `import()` of the task callback module named in HANDLER_CONFIG and
|
|
6
|
+
// the read of its `callback` export. HANDLER_CONFIG parsing, the S3-event →
|
|
7
|
+
// task-action dispatch (an exhaustive match over Reventless.Task.taskAction —
|
|
8
|
+
// the former JS `switch (action.TAG)` with its unreachable "unknown TAG" arm), the
|
|
9
|
+
// per-aggregate command publishing, and the CloudWatch Events scheduler ops
|
|
10
|
+
// live here, fully type-checked.
|
|
11
|
+
//
|
|
12
|
+
// The former `.mjs` carried a hand-maintained mirror of
|
|
13
|
+
// `toScheduleExpression` ("kept in sync manually"); typed code calls
|
|
14
|
+
// ScheduledPublisher_CloudWatchEvents_Runtime directly — the layer ships it —
|
|
15
|
+
// so the mirror is gone.
|
|
16
|
+
|
|
17
|
+
@val @scope("process") external processEnv: dict<string> = "env"
|
|
18
|
+
@module("./HandlerFactoryHelpers.mjs") @scope("log")
|
|
19
|
+
external logWarn: (string, StreamRoutedEntryPoint_Ops.dispatchOpts) => unit = "warn"
|
|
20
|
+
|
|
21
|
+
// ── HANDLER_CONFIG ──────────────────────────────────────────────────────────
|
|
22
|
+
// Written by TaskRuntime_Builder_PerBucket.
|
|
23
|
+
|
|
24
|
+
type schedulerEnvConfig = {
|
|
25
|
+
roleArnEnv: string,
|
|
26
|
+
targetArnEnv: string,
|
|
27
|
+
targetNameEnv: string,
|
|
28
|
+
}
|
|
29
|
+
type handlerConfig = {
|
|
30
|
+
callbackModule: string,
|
|
31
|
+
// aggregateName → env-var name holding the aggregate's cmd-topic SQS URL.
|
|
32
|
+
publishToAggregates: dict<string>,
|
|
33
|
+
scheduler: option<schedulerEnvConfig>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let strOf = (obj: dict<JSON.t>, key: string): option<string> =>
|
|
37
|
+
obj->Dict.get(key)->Option.flatMap(JSON.Decode.string)
|
|
38
|
+
|
|
39
|
+
let parseHandlerConfig = (rawJson: string): handlerConfig => {
|
|
40
|
+
let obj =
|
|
41
|
+
(rawJson == "" ? "{}" : rawJson)
|
|
42
|
+
->JSON.parseOrThrow
|
|
43
|
+
->JSON.Decode.object
|
|
44
|
+
->Option.getOr(Dict.make())
|
|
45
|
+
{
|
|
46
|
+
callbackModule: obj->strOf("callbackModule")->Option.getOr(""),
|
|
47
|
+
publishToAggregates: obj
|
|
48
|
+
->Dict.get("publishToAggregates")
|
|
49
|
+
->Option.flatMap(JSON.Decode.object)
|
|
50
|
+
->Option.getOr(Dict.make())
|
|
51
|
+
->Dict.toArray
|
|
52
|
+
->Array.filterMap(((k, v)) => v->JSON.Decode.string->Option.map(s => (k, s)))
|
|
53
|
+
->Dict.fromArray,
|
|
54
|
+
scheduler: obj
|
|
55
|
+
->Dict.get("scheduler")
|
|
56
|
+
->Option.flatMap(JSON.Decode.object)
|
|
57
|
+
->Option.map(s => {
|
|
58
|
+
roleArnEnv: s->strOf("roleArnEnv")->Option.getOr(""),
|
|
59
|
+
targetArnEnv: s->strOf("targetArnEnv")->Option.getOr(""),
|
|
60
|
+
targetNameEnv: s->strOf("targetNameEnv")->Option.getOr(""),
|
|
61
|
+
}),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Command publishing ──────────────────────────────────────────────────────
|
|
66
|
+
// aggregateName → publishJsons, resolving each queue URL via the env-var name
|
|
67
|
+
// carried in HANDLER_CONFIG; entries with an unset or empty URL are skipped
|
|
68
|
+
// (same as the former JS `if (queueUrl !== "")`).
|
|
69
|
+
|
|
70
|
+
let buildPublishCommands = (map: dict<string>): dict<ReventlessCore.CommandTopic.publishJsons> =>
|
|
71
|
+
map
|
|
72
|
+
->Dict.toArray
|
|
73
|
+
->Array.filterMap(((aggName, envVarName)) =>
|
|
74
|
+
processEnv
|
|
75
|
+
->Dict.get(envVarName)
|
|
76
|
+
->Option.filter(queueUrl => queueUrl != "")
|
|
77
|
+
->Option.map(queueUrl => {
|
|
78
|
+
let queue: Util_SQS_Runtime.resolvedQueue = {id: queueUrl, name: queueUrl, arn: ""}
|
|
79
|
+
(aggName, queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO))
|
|
80
|
+
})
|
|
81
|
+
)
|
|
82
|
+
->Dict.fromArray
|
|
83
|
+
|
|
84
|
+
// ── Scheduler ops (CloudWatch Events) ───────────────────────────────────────
|
|
85
|
+
// Resolved lazily on the first schedule action (tasks without schedules never
|
|
86
|
+
// touch the CloudWatch client) and cached for the container lifetime.
|
|
87
|
+
|
|
88
|
+
type schedulerOps = {
|
|
89
|
+
createSchedule: Reventless.Schedule.schedule => promise<unit>,
|
|
90
|
+
deleteSchedule: string => promise<unit>,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let schedulerOpsCache: ref<option<schedulerOps>> = ref(None)
|
|
94
|
+
|
|
95
|
+
let makeSchedulerOps = (config: option<schedulerEnvConfig>): option<schedulerOps> =>
|
|
96
|
+
switch (schedulerOpsCache.contents, config) {
|
|
97
|
+
| (Some(ops), _) => Some(ops)
|
|
98
|
+
| (None, None) => None
|
|
99
|
+
| (None, Some(cfg)) =>
|
|
100
|
+
switch (
|
|
101
|
+
processEnv->Dict.get(cfg.roleArnEnv),
|
|
102
|
+
processEnv->Dict.get(cfg.targetArnEnv),
|
|
103
|
+
processEnv->Dict.get(cfg.targetNameEnv),
|
|
104
|
+
) {
|
|
105
|
+
| (Some(roleArn), Some(targetArn), Some(targetName))
|
|
106
|
+
if roleArn != "" && targetArn != "" && targetName != "" =>
|
|
107
|
+
// The single resolved resource the shared CloudWatch Events runtime
|
|
108
|
+
// targets: `urn` is the PutTargets ARN, `name` the target id.
|
|
109
|
+
let resources: array<ReventlessInfra.Adapter.resolvedResource> = [
|
|
110
|
+
{
|
|
111
|
+
name: targetName,
|
|
112
|
+
id: targetName,
|
|
113
|
+
urn: targetArn,
|
|
114
|
+
service: "unknown",
|
|
115
|
+
resourceInfo: ReventlessInfra.Adapter.NoInfo,
|
|
116
|
+
role: "",
|
|
117
|
+
region: "",
|
|
118
|
+
resourceType: "",
|
|
119
|
+
configuration: Dict.make(),
|
|
120
|
+
tags: Dict.make(),
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
let ops = {
|
|
124
|
+
createSchedule: schedule =>
|
|
125
|
+
ScheduledPublisher_CloudWatchEvents_Runtime.createSchedule(~roleArn)(resources, schedule),
|
|
126
|
+
deleteSchedule: name =>
|
|
127
|
+
ScheduledPublisher_CloudWatchEvents_Runtime.deleteSchedule(resources, name),
|
|
128
|
+
}
|
|
129
|
+
schedulerOpsCache := Some(ops)
|
|
130
|
+
Some(ops)
|
|
131
|
+
| _ =>
|
|
132
|
+
logWarn("scheduler env vars missing — schedules will no-op", {comp: "TaskBucketRuntime"})
|
|
133
|
+
None
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Task-action dispatch ────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
let dispatchTaskActions = async (
|
|
140
|
+
~actions: array<Reventless.Task.taskAction>,
|
|
141
|
+
~publishCommands: dict<ReventlessCore.CommandTopic.publishJsons>,
|
|
142
|
+
~schedulerOps: unit => option<schedulerOps>,
|
|
143
|
+
): unit => {
|
|
144
|
+
let _ =
|
|
145
|
+
await actions
|
|
146
|
+
->Array.map(async action =>
|
|
147
|
+
switch action {
|
|
148
|
+
| Reventless.Task.PublishCommands(aggregateName, cmdJsons) =>
|
|
149
|
+
switch publishCommands->Dict.get(aggregateName) {
|
|
150
|
+
| Some(publish) => await publish(cmdJsons)
|
|
151
|
+
| None =>
|
|
152
|
+
logWarn(
|
|
153
|
+
`no publish function for aggregate "${aggregateName}"`,
|
|
154
|
+
{comp: "TaskBucketRuntime"},
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
| CreateSchedule(schedule) =>
|
|
158
|
+
switch schedulerOps() {
|
|
159
|
+
| Some(ops) => await ops.createSchedule(schedule)
|
|
160
|
+
| None =>
|
|
161
|
+
logWarn(
|
|
162
|
+
"CreateSchedule skipped — no scheduler configured for this Task (add sideEffects to setup)",
|
|
163
|
+
{comp: "TaskBucketRuntime"},
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
| DeleteSchedule(name) =>
|
|
167
|
+
switch schedulerOps() {
|
|
168
|
+
| Some(ops) => await ops.deleteSchedule(name)
|
|
169
|
+
| None =>
|
|
170
|
+
logWarn(
|
|
171
|
+
"DeleteSchedule skipped — no scheduler configured for this Task (add sideEffects to setup)",
|
|
172
|
+
{comp: "TaskBucketRuntime"},
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
->Promise.all
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Exported Lambda handler ─────────────────────────────────────────────────
|
|
181
|
+
// The shell reads `callback` off the dynamically imported task module and
|
|
182
|
+
// hands it here; from this point everything is typed
|
|
183
|
+
// (TaskBucket_S3_Runtime.handleBucketEvent decodes the S3 records and invokes
|
|
184
|
+
// the callback per object).
|
|
185
|
+
|
|
186
|
+
let makeHandler = (~callback: ReventlessCore.Task.bucketCallback, ~config: handlerConfig) => {
|
|
187
|
+
let handleEvents = TaskBucket_S3_Runtime.handleBucketEvent(callback)
|
|
188
|
+
let publishCommands = buildPublishCommands(config.publishToAggregates)
|
|
189
|
+
async (event: PulumiAws.S3.Bucket.event, context: PulumiAws.Lambda.context) => {
|
|
190
|
+
let actions = await handleEvents(event, context)
|
|
191
|
+
await dispatchTaskActions(
|
|
192
|
+
~actions,
|
|
193
|
+
~publishCommands,
|
|
194
|
+
~schedulerOps=() => makeSchedulerOps(config.scheduler),
|
|
195
|
+
)
|
|
196
|
+
""
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as HandlerFactoryHelpersMjs from "./HandlerFactoryHelpers.mjs";
|
|
7
|
+
import * as TaskBucket_S3_Runtime$ReventlessAws from "../Task/TaskBucket_S3_Runtime.res.mjs";
|
|
8
|
+
import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
9
|
+
import * as ScheduledPublisher_CloudWatchEvents_Runtime$ReventlessAws from "../ScheduledPublisher/ScheduledPublisher_CloudWatchEvents_Runtime.res.mjs";
|
|
10
|
+
|
|
11
|
+
function logWarn(prim0, prim1) {
|
|
12
|
+
HandlerFactoryHelpersMjs.log.warn(prim0, prim1);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function strOf(obj, key) {
|
|
16
|
+
return Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.string);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseHandlerConfig(rawJson) {
|
|
20
|
+
let obj = Stdlib_Option.getOr(Stdlib_JSON.Decode.object(JSON.parse(rawJson === "" ? "{}" : rawJson)), {});
|
|
21
|
+
return {
|
|
22
|
+
callbackModule: Stdlib_Option.getOr(strOf(obj, "callbackModule"), ""),
|
|
23
|
+
publishToAggregates: Object.fromEntries(Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(obj["publishToAggregates"], Stdlib_JSON.Decode.object), {})), param => {
|
|
24
|
+
let k = param[0];
|
|
25
|
+
return Stdlib_Option.map(Stdlib_JSON.Decode.string(param[1]), s => [
|
|
26
|
+
k,
|
|
27
|
+
s
|
|
28
|
+
]);
|
|
29
|
+
})),
|
|
30
|
+
scheduler: Stdlib_Option.map(Stdlib_Option.flatMap(obj["scheduler"], Stdlib_JSON.Decode.object), s => ({
|
|
31
|
+
roleArnEnv: Stdlib_Option.getOr(strOf(s, "roleArnEnv"), ""),
|
|
32
|
+
targetArnEnv: Stdlib_Option.getOr(strOf(s, "targetArnEnv"), ""),
|
|
33
|
+
targetNameEnv: Stdlib_Option.getOr(strOf(s, "targetNameEnv"), "")
|
|
34
|
+
}))
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildPublishCommands(map) {
|
|
39
|
+
return Object.fromEntries(Stdlib_Array.filterMap(Object.entries(map), param => {
|
|
40
|
+
let aggName = param[0];
|
|
41
|
+
return Stdlib_Option.map(Stdlib_Option.filter(process.env[param[1]], queueUrl => queueUrl !== ""), queueUrl => {
|
|
42
|
+
let queue = {
|
|
43
|
+
id: queueUrl,
|
|
44
|
+
name: queueUrl,
|
|
45
|
+
arn: ""
|
|
46
|
+
};
|
|
47
|
+
return [
|
|
48
|
+
aggName,
|
|
49
|
+
CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, "SQS_FIFO")
|
|
50
|
+
];
|
|
51
|
+
});
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let schedulerOpsCache = {
|
|
56
|
+
contents: undefined
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function makeSchedulerOps(config) {
|
|
60
|
+
let match = schedulerOpsCache.contents;
|
|
61
|
+
if (match !== undefined) {
|
|
62
|
+
return match;
|
|
63
|
+
}
|
|
64
|
+
if (config === undefined) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
let match$1 = process.env[config.roleArnEnv];
|
|
68
|
+
let match$2 = process.env[config.targetArnEnv];
|
|
69
|
+
let match$3 = process.env[config.targetNameEnv];
|
|
70
|
+
if (match$1 !== undefined && match$2 !== undefined && match$3 !== undefined && match$1 !== "" && match$2 !== "" && match$3 !== "") {
|
|
71
|
+
let resources = [{
|
|
72
|
+
name: match$3,
|
|
73
|
+
id: match$3,
|
|
74
|
+
urn: match$2,
|
|
75
|
+
resourceInfo: "NoInfo",
|
|
76
|
+
service: "unknown",
|
|
77
|
+
role: "",
|
|
78
|
+
region: "",
|
|
79
|
+
resourceType: "",
|
|
80
|
+
configuration: {},
|
|
81
|
+
tags: {}
|
|
82
|
+
}];
|
|
83
|
+
let ops_createSchedule = schedule => ScheduledPublisher_CloudWatchEvents_Runtime$ReventlessAws.createSchedule(match$1)(resources, schedule);
|
|
84
|
+
let ops_deleteSchedule = name => ScheduledPublisher_CloudWatchEvents_Runtime$ReventlessAws.deleteSchedule(resources, name);
|
|
85
|
+
let ops = {
|
|
86
|
+
createSchedule: ops_createSchedule,
|
|
87
|
+
deleteSchedule: ops_deleteSchedule
|
|
88
|
+
};
|
|
89
|
+
schedulerOpsCache.contents = ops;
|
|
90
|
+
return ops;
|
|
91
|
+
}
|
|
92
|
+
HandlerFactoryHelpersMjs.log.warn("scheduler env vars missing — schedules will no-op", {
|
|
93
|
+
comp: "TaskBucketRuntime"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function dispatchTaskActions(actions, publishCommands, schedulerOps) {
|
|
98
|
+
await Promise.all(actions.map(async action => {
|
|
99
|
+
switch (action.TAG) {
|
|
100
|
+
case "PublishCommands" :
|
|
101
|
+
let aggregateName = action._0;
|
|
102
|
+
let publish = publishCommands[aggregateName];
|
|
103
|
+
if (publish !== undefined) {
|
|
104
|
+
return await publish(action._1);
|
|
105
|
+
} else {
|
|
106
|
+
HandlerFactoryHelpersMjs.log.warn(`no publish function for aggregate "` + aggregateName + `"`, {
|
|
107
|
+
comp: "TaskBucketRuntime"
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
case "CreateSchedule" :
|
|
112
|
+
let ops = schedulerOps();
|
|
113
|
+
if (ops !== undefined) {
|
|
114
|
+
return await ops.createSchedule(action._0);
|
|
115
|
+
} else {
|
|
116
|
+
HandlerFactoryHelpersMjs.log.warn("CreateSchedule skipped — no scheduler configured for this Task (add sideEffects to setup)", {
|
|
117
|
+
comp: "TaskBucketRuntime"
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case "DeleteSchedule" :
|
|
122
|
+
let ops$1 = schedulerOps();
|
|
123
|
+
if (ops$1 !== undefined) {
|
|
124
|
+
return await ops$1.deleteSchedule(action._0);
|
|
125
|
+
} else {
|
|
126
|
+
HandlerFactoryHelpersMjs.log.warn("DeleteSchedule skipped — no scheduler configured for this Task (add sideEffects to setup)", {
|
|
127
|
+
comp: "TaskBucketRuntime"
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function makeHandler(callback, config) {
|
|
136
|
+
let handleEvents = TaskBucket_S3_Runtime$ReventlessAws.handleBucketEvent(callback);
|
|
137
|
+
let publishCommands = buildPublishCommands(config.publishToAggregates);
|
|
138
|
+
return async (event, context) => {
|
|
139
|
+
let actions = await handleEvents(event, context);
|
|
140
|
+
await dispatchTaskActions(actions, publishCommands, () => makeSchedulerOps(config.scheduler));
|
|
141
|
+
return "";
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
logWarn,
|
|
147
|
+
strOf,
|
|
148
|
+
parseHandlerConfig,
|
|
149
|
+
buildPublishCommands,
|
|
150
|
+
schedulerOpsCache,
|
|
151
|
+
makeSchedulerOps,
|
|
152
|
+
dispatchTaskActions,
|
|
153
|
+
makeHandler,
|
|
154
|
+
}
|
|
155
|
+
/* ./HandlerFactoryHelpers.mjs Not a pure module */
|
|
@@ -81,164 +81,6 @@ let registry: dict<array<streamEntry>> = Dict.make()
|
|
|
81
81
|
// Uses native Node.js crypto + fetch with SigV4 — no extra SDK packages needed.
|
|
82
82
|
// Credentials come from the Lambda execution role via the process.env AWS_* variables.
|
|
83
83
|
|
|
84
|
-
let handlerCode: string = `
|
|
85
|
-
import { createHmac, createHash } from "node:crypto";
|
|
86
|
-
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
87
|
-
|
|
88
|
-
const TOPIC_MAP = JSON.parse(process.env.STATE_TOPIC_MAP || "{}");
|
|
89
|
-
const APPSYNC_ENDPOINT = process.env.APPSYNC_ENDPOINT; // "https://{eventsApi.dns.http}"
|
|
90
|
-
const AWS_REGION = process.env.AWS_REGION ?? "eu-west-1";
|
|
91
|
-
|
|
92
|
-
function sha256hex(data) {
|
|
93
|
-
return createHash("sha256").update(typeof data === "string" ? data : JSON.stringify(data)).digest("hex");
|
|
94
|
-
}
|
|
95
|
-
function hmacBuf(key, data) {
|
|
96
|
-
return createHmac("sha256", key).update(data).digest();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
async function signedHeaders(host, path, body) {
|
|
100
|
-
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
101
|
-
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
102
|
-
const sessionToken = process.env.AWS_SESSION_TOKEN;
|
|
103
|
-
const now = new Date();
|
|
104
|
-
const amzDate = now.toISOString().replace(/[:\\-]|\\..../g, "").slice(0, 15) + "Z";
|
|
105
|
-
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, "");
|
|
106
|
-
const headers = { host, "x-amz-date": amzDate, ...(sessionToken ? { "x-amz-security-token": sessionToken } : {}) };
|
|
107
|
-
const canonH = Object.entries(headers).sort(([a],[b]) => a.localeCompare(b)).map(([k,v]) => \`\${k}:\${v}\\n\`).join("");
|
|
108
|
-
const signH = Object.keys(headers).sort().join(";");
|
|
109
|
-
const cr = ["POST", path, "", canonH, signH, sha256hex(body)].join("\\n");
|
|
110
|
-
const scope = \`\${dateStamp}/\${AWS_REGION}/appsync/aws4_request\`;
|
|
111
|
-
const sts = ["AWS4-HMAC-SHA256", amzDate, scope, sha256hex(cr)].join("\\n");
|
|
112
|
-
const kDate = hmacBuf("AWS4" + secretAccessKey, dateStamp);
|
|
113
|
-
const kSigning = hmacBuf(hmacBuf(hmacBuf(kDate, AWS_REGION), "appsync"), "aws4_request");
|
|
114
|
-
const sig = createHmac("sha256", kSigning).update(sts).digest("hex");
|
|
115
|
-
return { ...headers, Authorization: \`AWS4-HMAC-SHA256 Credential=\${accessKeyId}/\${scope}, SignedHeaders=\${signH}, Signature=\${sig}\` };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// AppSync Events channel segments allow only [A-Za-z0-9-]. Anything else
|
|
119
|
-
// (\`@\`, \`.\`, \`:\`, \`_\`, \`/\`, …) collapses to \`-\` so the segment is a
|
|
120
|
-
// legal channel-path token. The UI's AutoLive.normalizeSegment uses the same
|
|
121
|
-
// rule for Entity-scoped subscribes.
|
|
122
|
-
function pathSegment(value) {
|
|
123
|
-
return String(value).replace(/[^A-Za-z0-9-]/g, "-");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Map record.eventSourceARN → topicRoot via STATE_TOPIC_MAP.
|
|
127
|
-
// Stream ARNs look like: arn:aws:dynamodb:<region>:<acct>:table/<TableName>/stream/<ts>
|
|
128
|
-
function topicRootFromEventSourceArn(arn) {
|
|
129
|
-
const m = arn && arn.match(/:table\\/([^/]+)\\/stream\\//);
|
|
130
|
-
if (!m) return undefined;
|
|
131
|
-
const topicName = TOPIC_MAP[m[1]];
|
|
132
|
-
if (topicName === undefined) return undefined;
|
|
133
|
-
return pathSegment(topicName);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Build entityKey from record.dynamodb.Keys. Framework convention:
|
|
137
|
-
// partition-key attr is named "id"; composite tables have one extra attr (the
|
|
138
|
-
// sort key) whose name comes from the projection's subIdConfig.subIdField.
|
|
139
|
-
// Returns the ORIGINAL key string — the descriptor body uses this verbatim so
|
|
140
|
-
// AutoListView can match it against the GraphQL row.id. The channel path
|
|
141
|
-
// segment is derived later via pathSegment().
|
|
142
|
-
function entityKeyFromRecord(record) {
|
|
143
|
-
const keys = unmarshall(record.dynamodb.Keys ?? {});
|
|
144
|
-
const id = keys.id;
|
|
145
|
-
if (id === undefined) {
|
|
146
|
-
// Defensive: framework always names the partition key "id". If a future
|
|
147
|
-
// table breaks the convention, fall back to a stable sort-join.
|
|
148
|
-
const names = Object.keys(keys).sort();
|
|
149
|
-
return names.map((n) => String(keys[n])).join("-");
|
|
150
|
-
}
|
|
151
|
-
const subIdName = Object.keys(keys).find((k) => k !== "id");
|
|
152
|
-
return subIdName === undefined
|
|
153
|
-
? String(id)
|
|
154
|
-
: String(id) + "-" + String(keys[subIdName]);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Map DDB stream eventName → descriptor changeKind.
|
|
158
|
-
function changeKindFor(eventName) {
|
|
159
|
-
switch (eventName) {
|
|
160
|
-
case "INSERT": return "Added";
|
|
161
|
-
case "MODIFY": return "Updated";
|
|
162
|
-
case "REMOVE": return "Removed";
|
|
163
|
-
default: return "Updated"; // defensive default — shouldn't fire
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Pick the "natural" sort timestamp from an unmarshalled image. Conventions
|
|
168
|
-
// in this codebase prefer updatedAt; createdAt is the fallback for views that
|
|
169
|
-
// never mutate after insert. Returns undefined if neither is present.
|
|
170
|
-
function pickSortKeyValue(image) {
|
|
171
|
-
if (image && typeof image.updatedAt === "string") return image.updatedAt;
|
|
172
|
-
if (image && typeof image.createdAt === "string") return image.createdAt;
|
|
173
|
-
return undefined;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export async function handler(event) {
|
|
177
|
-
const url = new URL(APPSYNC_ENDPOINT);
|
|
178
|
-
let transientErr;
|
|
179
|
-
for (const record of event.Records) {
|
|
180
|
-
const topicRoot = topicRootFromEventSourceArn(record.eventSourceARN);
|
|
181
|
-
if (topicRoot === undefined) {
|
|
182
|
-
console.warn("StateTopic: unknown table for ARN", record.eventSourceARN);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
const channelRoot = "/default/" + topicRoot;
|
|
186
|
-
const image =
|
|
187
|
-
record.eventName === "REMOVE"
|
|
188
|
-
? record.dynamodb.OldImage
|
|
189
|
-
: record.dynamodb.NewImage;
|
|
190
|
-
if (image === undefined) continue;
|
|
191
|
-
const entityKey = entityKeyFromRecord(record);
|
|
192
|
-
const channel = channelRoot + "/" + pathSegment(entityKey);
|
|
193
|
-
const unmarshalled = unmarshall(image);
|
|
194
|
-
const descriptor = {
|
|
195
|
-
changeKind: changeKindFor(record.eventName),
|
|
196
|
-
id: entityKey,
|
|
197
|
-
};
|
|
198
|
-
const sortKeyValue = pickSortKeyValue(unmarshalled);
|
|
199
|
-
if (sortKeyValue !== undefined) descriptor.sortKeyValue = sortKeyValue;
|
|
200
|
-
const body = JSON.stringify({ id: record.eventID, channel, events: [JSON.stringify(descriptor)] });
|
|
201
|
-
const auth = await signedHeaders(url.hostname, "/event", body);
|
|
202
|
-
try {
|
|
203
|
-
const res = await fetch(APPSYNC_ENDPOINT + "/event", {
|
|
204
|
-
method: "POST",
|
|
205
|
-
headers: { accept: "application/json, text/javascript", "content-encoding": "amz-1.0", "content-type": "application/json; charset=UTF-8", ...auth },
|
|
206
|
-
body,
|
|
207
|
-
});
|
|
208
|
-
if (!res.ok) {
|
|
209
|
-
const txt = await res.text();
|
|
210
|
-
// 4xx: permanent (bad request, auth, channel format) — retrying won't
|
|
211
|
-
// help. Log structurally so a CloudWatch metric filter on
|
|
212
|
-
// "STATE_TOPIC_PUBLISH_FAILED type=permanent" surfaces it; continue
|
|
213
|
-
// processing so one bad record doesn't block the rest of the batch.
|
|
214
|
-
// 5xx / network: transient — record it and throw at the end so the
|
|
215
|
-
// ESM retries (bounded by maximumRetryAttempts + bisectBatch).
|
|
216
|
-
const isTransient = res.status >= 500;
|
|
217
|
-
const tag = isTransient ? "transient" : "permanent";
|
|
218
|
-
console.error(
|
|
219
|
-
"STATE_TOPIC_PUBLISH_FAILED type=" + tag +
|
|
220
|
-
" status=" + res.status +
|
|
221
|
-
" channel=" + channel +
|
|
222
|
-
" body=" + txt.slice(0, 500)
|
|
223
|
-
);
|
|
224
|
-
if (isTransient) transientErr = new Error("StateTopic publish failed: " + res.status);
|
|
225
|
-
}
|
|
226
|
-
} catch (e) {
|
|
227
|
-
// Network-level error (DNS, connection refused, timeout) — always transient.
|
|
228
|
-
console.error(
|
|
229
|
-
"STATE_TOPIC_PUBLISH_FAILED type=transient status=network" +
|
|
230
|
-
" channel=" + channel +
|
|
231
|
-
" err=" + (e && e.message)
|
|
232
|
-
);
|
|
233
|
-
transientErr = e;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
// Throw after the loop so a single transient error doesn't stop us from
|
|
237
|
-
// emitting other records' descriptors first. The ESM will retry the batch;
|
|
238
|
-
// bisectBatchOnFunctionError isolates the bad record across retries.
|
|
239
|
-
if (transientErr) throw transientErr;
|
|
240
|
-
}
|
|
241
|
-
`
|
|
242
84
|
|
|
243
85
|
// ── Registration ──────────────────────────────────────────────────────────────
|
|
244
86
|
|
|
@@ -353,19 +195,19 @@ let finish = (
|
|
|
353
195
|
dict->JSON.Encode.object->JSON.stringify
|
|
354
196
|
})
|
|
355
197
|
|
|
356
|
-
// Shared Lambda — handler
|
|
357
|
-
// routing is per-record via STATE_TOPIC_MAP env var.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
198
|
+
// Shared Lambda — the compiled `_Ops` handler is identical for every
|
|
199
|
+
// stream-enabled RM; routing is per-record via STATE_TOPIC_MAP env var.
|
|
200
|
+
// Bundle reventless-aws (the handler + its node:crypto signer + the
|
|
201
|
+
// util-dynamodb unmarshaller live inside it) and re-export its `handler`;
|
|
202
|
+
// buildCodeArchive ships the ESM resolve-hook so `@rescript/runtime` and the
|
|
203
|
+
// handler's bare @aws-sdk/util-dynamodb resolve from layer / managed runtime.
|
|
204
|
+
let packageDirs = Dict.fromArray([
|
|
205
|
+
("@reventlessdev/reventless-aws", Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws")),
|
|
206
|
+
])
|
|
207
|
+
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
208
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs",
|
|
209
|
+
~packageDirs,
|
|
362
210
|
)
|
|
363
|
-
// ESM self-containment: the handler imports @aws-sdk/util-dynamodb, provided
|
|
364
|
-
// by the nodejs22.x runtime only under /var/runtime — unreachable from
|
|
365
|
-
// /var/task ESM without the resolver hook. Ship the loader + set its env vars.
|
|
366
|
-
let loaderHash = Util_Bundle.addEsmLoaderAssets(archiveContents)
|
|
367
|
-
let code = Pulumi.Archive.assetArchive(archiveContents)
|
|
368
|
-
let sourceCodeHash = Util_Bundle.hashString(handlerCode ++ "\n---\n" ++ loaderHash)
|
|
369
211
|
|
|
370
212
|
let layers =
|
|
371
213
|
Lambda.reventlessLayerArn
|