@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +7 -7
  3. package/src/adapter/Api/AppSyncEventsSigner_Ops.res +137 -0
  4. package/src/adapter/Api/AppSyncEventsSigner_Ops.res.mjs +121 -0
  5. package/src/adapter/Api/Platform_AdminScan_Ops.res +136 -0
  6. package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +98 -0
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +12 -90
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -84
  9. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +86 -0
  10. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +79 -0
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res +9 -79
  12. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +7 -143
  13. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res +48 -0
  14. package/src/adapter/Api/Platform_UIFragments_Lambda_Ops.res.mjs +63 -0
  15. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +21 -80
  16. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +23 -68
  17. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res +70 -0
  18. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync_Ops.res.mjs +68 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -1
  20. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +1 -1
  21. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  22. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -1
  23. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +42 -126
  24. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +246 -0
  25. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +145 -0
  26. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +27 -2
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +10 -3
  28. package/src/adapter/Runtime/CounterEntryPoint.mjs +19 -73
  29. package/src/adapter/Runtime/CounterEntryPoint_Ops.res +145 -0
  30. package/src/adapter/Runtime/CounterEntryPoint_Ops.res.mjs +129 -0
  31. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +16 -3
  32. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +11 -6
  33. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +52 -510
  34. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +816 -0
  35. package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +651 -0
  36. package/src/adapter/Runtime/EventMapperEntryPoint.mjs +23 -67
  37. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +126 -0
  38. package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res.mjs +83 -0
  39. package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +16 -63
  40. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +150 -0
  41. package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res.mjs +111 -0
  42. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res +96 -0
  43. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.res.mjs +70 -0
  44. package/src/adapter/Runtime/PgMigrationEntryPoint.res +54 -0
  45. package/src/adapter/Runtime/PgMigrationEntryPoint.res.mjs +37 -0
  46. package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +20 -55
  47. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +155 -0
  48. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +94 -0
  49. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +80 -0
  50. package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res.mjs +39 -0
  51. package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +4 -4
  52. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +34 -153
  53. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +114 -0
  54. package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res.mjs +83 -0
  55. package/src/adapter/Runtime/SideEffectEntryPoint.mjs +23 -64
  56. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +71 -0
  57. package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res.mjs +57 -0
  58. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +34 -165
  59. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +206 -0
  60. package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res.mjs +105 -0
  61. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +156 -0
  62. package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res.mjs +123 -0
  63. package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +18 -138
  64. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +198 -0
  65. package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res.mjs +155 -0
  66. package/src/adapter/StateTopic/StateTopic_AppSync.res +12 -170
  67. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +7 -167
  68. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +208 -0
  69. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +183 -0
  70. package/src/components/AutomationSlice_Builder.res +2 -0
  71. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  72. package/src/components/OutboundTranslationSlice_Builder.res +1 -0
  73. package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
  74. package/tests/AppSyncEventsSigner_OpsTest.res +75 -0
  75. package/tests/AppSyncEventsSigner_OpsTest.res.mjs +44 -0
  76. package/tests/AppSyncEventsSigner_ParityFixture.mjs +36 -0
  77. package/tests/AutomationSliceEntryPoint_OpsTest.res +165 -0
  78. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +280 -0
  79. package/tests/CounterEntryPoint_OpsTest.res +115 -0
  80. package/tests/CounterEntryPoint_OpsTest.res.mjs +150 -0
  81. package/tests/EventCollectorEntryPoint_OpsTest.res +216 -0
  82. package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +339 -0
  83. package/tests/EventMapperEntryPoint_OpsTest.res +103 -0
  84. package/tests/EventMapperEntryPoint_OpsTest.res.mjs +120 -0
  85. package/tests/ExtensionPointEntryPoint_OpsTest.res +57 -0
  86. package/tests/ExtensionPointEntryPoint_OpsTest.res.mjs +54 -0
  87. package/tests/PgMigrationEntryPoint_IntegrationTest.res +22 -31
  88. package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +20 -23
  89. package/tests/PgQueryResolverEntryPoint_OpsTest.res +38 -0
  90. package/tests/PgQueryResolverEntryPoint_OpsTest.res.mjs +31 -0
  91. package/tests/Platform_AdminScan_OpsTest.res +80 -0
  92. package/tests/Platform_AdminScan_OpsTest.res.mjs +127 -0
  93. package/tests/ReadModelEntryPoint_OpsTest.res +141 -0
  94. package/tests/ReadModelEntryPoint_OpsTest.res.mjs +267 -0
  95. package/tests/SideEffectEntryPoint_OpsTest.res +79 -0
  96. package/tests/SideEffectEntryPoint_OpsTest.res.mjs +105 -0
  97. package/tests/StateViewSliceEntryPoint_OpsTest.res +167 -0
  98. package/tests/StateViewSliceEntryPoint_OpsTest.res.mjs +296 -0
  99. package/tests/TaskBucketEntryPoint_OpsTest.res +117 -0
  100. package/tests/TaskBucketEntryPoint_OpsTest.res.mjs +165 -0
  101. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res +42 -13
  102. package/tests/integration/StateViewSliceEntryPoint_IntegrationTest.res.mjs +29 -6
  103. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +0 -57
  104. 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 code is identical for every stream-enabled RM;
357
- // routing is per-record via STATE_TOPIC_MAP env var.
358
- let archiveContents: dict<Pulumi.Archive.assetOrArchive> = Dict.make()
359
- archiveContents->Dict.set(
360
- "index.mjs",
361
- Pulumi.Asset.stringAsset(handlerCode)->Pulumi.Archive.assetToAssetOrArchive,
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