@reventlessdev/reventless-aws 3.0.0-alpha.229 → 3.0.0-alpha.231

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 (33) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +5 -5
  3. package/src/adapter/Geocoder/Geocoder_AwsLocation.res +60 -141
  4. package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +52 -118
  5. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +137 -0
  6. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +124 -0
  7. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
  8. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +60 -0
  9. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +29 -1
  10. package/src/adapter/Runtime/HeartbeatEntryPoint.res +44 -0
  11. package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +47 -0
  12. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +234 -0
  13. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs +226 -0
  14. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
  15. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
  16. package/src/adapter/Upload/Upload_Presign_S3.res +65 -164
  17. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +56 -119
  18. package/src/adapter/Upload/Upload_Presign_S3_Ops.res +157 -0
  19. package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +120 -0
  20. package/src/components/InboundTranslationSlice_Builder.res +27 -1
  21. package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
  22. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -1
  23. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  24. package/src/plugin/runtime/PluginRuntime_Builder.res +59 -1
  25. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +49 -2
  26. package/tests/DcbInboundTranslationRoutingTest.res +73 -0
  27. package/tests/DcbInboundTranslationRoutingTest.res.mjs +71 -0
  28. package/tests/EpInboundTestSlice.res +19 -0
  29. package/tests/EpInboundTestSlice.res.mjs +31 -0
  30. package/tests/EpInboundTestSliceTranslation.res +13 -0
  31. package/tests/EpInboundTestSliceTranslation.res.mjs +33 -0
  32. package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +0 -38
  33. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +0 -127
@@ -0,0 +1,124 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
6
+ import * as ClientLocation from "@aws-sdk/client-location";
7
+
8
+ function getEnv(k) {
9
+ let v = process.env[k];
10
+ if (v !== undefined && v !== "") {
11
+ return v;
12
+ }
13
+ }
14
+
15
+ function corsHeaders() {
16
+ return Object.fromEntries([
17
+ [
18
+ "content-type",
19
+ "application/json"
20
+ ],
21
+ [
22
+ "access-control-allow-origin",
23
+ "*"
24
+ ],
25
+ [
26
+ "access-control-allow-methods",
27
+ "GET,OPTIONS"
28
+ ],
29
+ [
30
+ "access-control-allow-headers",
31
+ "*"
32
+ ]
33
+ ]);
34
+ }
35
+
36
+ function readQueryParam(event) {
37
+ let q = Stdlib_Option.flatMap(event.queryStringParameters, p => p["q"]);
38
+ if (q !== undefined) {
39
+ return q;
40
+ } else {
41
+ return Stdlib_Option.flatMap(event.rawQueryString, raw => Stdlib_Array.findMap(raw.split("&"), pair => {
42
+ let match = pair.split("=");
43
+ if (match.length !== 2) {
44
+ return;
45
+ }
46
+ let k = match[0];
47
+ let v = match[1];
48
+ if (k === "q") {
49
+ return decodeURIComponent(v);
50
+ }
51
+ }));
52
+ }
53
+ }
54
+
55
+ async function handler(event) {
56
+ try {
57
+ let indexName = Stdlib_Option.getOr(getEnv("PLACE_INDEX_NAME"), "");
58
+ let q = Stdlib_Option.getOr(readQueryParam(event), "");
59
+ if (indexName === "" || q === "") {
60
+ return {
61
+ statusCode: 200,
62
+ headers: corsHeaders(),
63
+ body: "[]"
64
+ };
65
+ }
66
+ let client = new ClientLocation.LocationClient();
67
+ let resp = await client.send(new ClientLocation.SearchPlaceIndexForTextCommand({
68
+ IndexName: indexName,
69
+ Text: q,
70
+ MaxResults: 5
71
+ }));
72
+ let results = Stdlib_Array.filterMap(Stdlib_Option.getOr(resp.Results, []), r => {
73
+ let place = r.Place;
74
+ if (place === undefined) {
75
+ return;
76
+ }
77
+ let label = Stdlib_Option.getOr(place.Label, "");
78
+ let pt = Stdlib_Option.flatMap(place.Geometry, g => g.Point);
79
+ if (pt === undefined) {
80
+ return;
81
+ }
82
+ if (pt.length < 2) {
83
+ return;
84
+ }
85
+ let lng = pt[0];
86
+ let lat = pt[1];
87
+ return Object.fromEntries([
88
+ [
89
+ "label",
90
+ label
91
+ ],
92
+ [
93
+ "lat",
94
+ lat
95
+ ],
96
+ [
97
+ "lng",
98
+ lng
99
+ ]
100
+ ]);
101
+ });
102
+ return {
103
+ statusCode: 200,
104
+ headers: corsHeaders(),
105
+ body: JSON.stringify(results)
106
+ };
107
+ } catch (raw_exn) {
108
+ let exn = Primitive_exceptions.internalToException(raw_exn);
109
+ console.error("Geocoder: search failed", exn);
110
+ return {
111
+ statusCode: 200,
112
+ headers: corsHeaders(),
113
+ body: "[]"
114
+ };
115
+ }
116
+ }
117
+
118
+ export {
119
+ getEnv,
120
+ corsHeaders,
121
+ readQueryParam,
122
+ handler,
123
+ }
124
+ /* @aws-sdk/client-location Not a pure module */
@@ -25,7 +25,8 @@ import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessde
25
25
  // per-slice handler building (functor + decode + handleCommands), all
26
26
  // compiler-checked against the framework signatures (see the module header and
27
27
  // docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
28
- import { deriveScope, commandTypeNames, makeStorageOps, buildSliceHandler } from "./DcbCommandTopicEntryPoint_Ops.res.mjs";
28
+ import { deriveScope, commandTypeNames, makeStorageOps, buildSliceHandler, buildInboundReceiver } from "./DcbCommandTopicEntryPoint_Ops.res.mjs";
29
+ import { makeDynamoQueryDbOps } from "./QueryDbEntryPoint_Ops.res.mjs";
29
30
 
30
31
  const dynamicImport = (specifier) => import('/var/task/node_modules/' + specifier);
31
32
 
@@ -145,6 +146,32 @@ export async function buildHandlersForConfig(config, opts = {}) {
145
146
  const sqsHandler = handleQueueEvent(resolvedQueue, compositeJsonCommandsHandler);
146
147
  const publishJsons = sqsPublishJsons(resolvedQueue, "SQS_FIFO");
147
148
 
149
+ // Route 0 registry: InboundTranslationSlice receive handlers keyed by their
150
+ // AppSync mutation field name. The resolver invokes this Lambda with
151
+ // {__inboundTranslation, fieldName, arguments}; the field name is
152
+ // `<pluginName>_<sliceName>` (Api_Naming.sliceMutationField — the deploy side
153
+ // uses the same `name` that HANDLER_CONFIG.pluginName carries). The spec +
154
+ // translation modules are the one untyped seam (dynamic import); the typed
155
+ // `buildInboundReceiver` owns the functor call + audit persistence. Audit ops
156
+ // are DynamoDB-only for now (makeDynamoQueryDbOps); Postgres audit is a
157
+ // follow-up, so on that backend the receiver runs without persistence.
158
+ const inboundModules = config.inboundTranslationSliceModules || [];
159
+ const inboundReceiversByField = {};
160
+ await Promise.all(inboundModules.map(async ({ spec, translation, auditTableName }) => {
161
+ const specModule = await loadModule(spec);
162
+ const translationModule = await loadModule(translation);
163
+ const auditOps = (auditTableName && !config.pgConnection)
164
+ ? makeDynamoQueryDbOps(auditTableName)
165
+ : undefined;
166
+ const fieldName = config.pluginName + "_" + specModule.name;
167
+ inboundReceiversByField[fieldName] = buildInboundReceiver(
168
+ specModule,
169
+ translationModule,
170
+ publishJsons,
171
+ auditOps,
172
+ );
173
+ }));
174
+
148
175
  // Sync (default): inline-dispatch the command via the same composite handler
149
176
  // that Route 2 uses, so the AppSync resolver gets a typed Accepted/Rejected
150
177
  // outcome. Async: undefined → makeCommandGenerator falls back to publishJsons
@@ -185,10 +212,11 @@ export async function buildHandlersForConfig(config, opts = {}) {
185
212
  return generateCommand({ ...event, meta, identity });
186
213
  };
187
214
 
188
- // Third element is additive — existing callers (the integration tests) keep
189
- // destructuring the first two. No deploy-time `plugin` fragment needed here:
190
- // this Lambda serves exactly one plugin and HANDLER_CONFIG already names it.
191
- return [sqsHandler, cmdGenHandler, dcbComp(config.pluginName), config.pluginName];
215
+ // Elements past the first two are additive — existing callers (the integration
216
+ // tests) keep destructuring the first two. No deploy-time `plugin` fragment
217
+ // needed here: this Lambda serves exactly one plugin and HANDLER_CONFIG already
218
+ // names it. The 5th element is the Route 0 inbound-translation registry.
219
+ return [sqsHandler, cmdGenHandler, dcbComp(config.pluginName), config.pluginName, inboundReceiversByField];
192
220
  }
193
221
 
194
222
  async function buildHandler() {
@@ -205,7 +233,24 @@ const initPromise = buildHandler();
205
233
 
206
234
  export async function handler(event, context) {
207
235
  setRequestId(context?.awsRequestId);
208
- const [sqsHandler, cmdGenHandler, comp, plugin] = await initPromise;
236
+ const [sqsHandler, cmdGenHandler, comp, plugin, inboundReceivers] = await initPromise;
237
+
238
+ // Route 0: InboundTranslationSlice mutation — the AppSync resolver invokes this
239
+ // Lambda with `{__inboundTranslation: true, fieldName, arguments}` (no `command`,
240
+ // no `Records`). Dispatch to the field's receive handler, which translates +
241
+ // publishes and returns a commandOutcome JSON byte-compatible with Route 1's
242
+ // `commandOutcomeToJson`. Without this branch the payload fell through to Route 2
243
+ // and crashed on `event.records` being undefined.
244
+ if (event.__inboundTranslation === true) {
245
+ const fieldName = event.fieldName;
246
+ const receiver = (inboundReceivers || {})[fieldName];
247
+ if (receiver === undefined) {
248
+ log.warn("no inbound translation receiver for field: " + fieldName, { comp: "DcbCommandTopicRuntime" });
249
+ throw new Error("no inbound translation receiver for field: " + fieldName);
250
+ }
251
+ log.debug("InboundTranslation invocation (" + fieldName + ")", { comp: "DcbCommandTopicRuntime" });
252
+ return await receiver(event.arguments);
253
+ }
209
254
 
210
255
  // Route 1: AppSync direct invocation — payload carries the CommandGenerator.payload
211
256
  // shape (`{command, arguments, meta, identity?}`).
@@ -219,3 +219,63 @@ let buildSliceHandler = (
219
219
  callback.handleCommands(~tagKeysByEventType, ~crossPartitionTagKeys, dcbEventLog, decodedStream)
220
220
  }
221
221
  }
222
+
223
+ // ── Inbound translation receiver wiring (Route 0) ───────────────────────────
224
+ // The DCB command Lambda is also the target of every InboundTranslationSlice
225
+ // mutation on the plugin's API — its AppSync resolver invokes this Lambda with an
226
+ // `{__inboundTranslation, fieldName, arguments}` payload. Building the per-field
227
+ // `receive` here keeps the curried `InboundTranslationSlice_Callback.Make` functor
228
+ // call compiler-checked (same rationale as `buildSliceHandler`) and mirrors the
229
+ // in-process composite handler in `Dcb_Builder.res`, so the deployed surface and
230
+ // the local surface encode the same `commandOutcome`.
231
+
232
+ // The dynamically-imported Translation module — opaque; a clean pass-through to
233
+ // the functor (only `translate` is read, inside the compiled callback).
234
+ type translationModule
235
+
236
+ type inboundCallback = {
237
+ receive: (
238
+ ReventlessInfra.CommandTopic.publishJsons,
239
+ JSON.t,
240
+ ) => promise<ReventlessInfra.InboundTranslationSlice.receiveResult>,
241
+ auditLog: dict<ReventlessCore.InboundTranslationSlice_Callback.auditRow>,
242
+ }
243
+ @module("@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs")
244
+ external makeInboundCallback: specModule => translationModule => inboundCallback = "Make"
245
+
246
+ // Builds the inbound receive handler: run the slice's `receive` (which validates
247
+ // the external input against the spec schema, translates, and publishes the mapped
248
+ // commands via `publishJsons`), then — when an audit table name was threaded —
249
+ // drain the in-memory audit log to that table (best-effort, matching
250
+ // `InboundTranslationSlice_Builder`'s inline `syncToQueryDb`). Returns the
251
+ // `commandOutcome` JSON, byte-compatible with the AppSync direct-invocation route.
252
+ let buildInboundReceiver = (
253
+ spec: specModule,
254
+ translation: translationModule,
255
+ ~publishJsons: ReventlessInfra.CommandTopic.publishJsons,
256
+ ~auditQueryDbOps: option<ReventlessCore.QueryDb_Adapter.operations>,
257
+ ) => {
258
+ let callback = makeInboundCallback(spec)(translation)
259
+ async (args: JSON.t): JSON.t => {
260
+ let result = await callback.receive(publishJsons, args)
261
+ switch auditQueryDbOps {
262
+ | Some(ops) =>
263
+ let rows = callback.auditLog->Dict.toArray
264
+ let _ = await rows->Array.reduce(Promise.resolve(), async (prev, (id, row)) => {
265
+ let _ = await prev
266
+ let json = row->S.reverseConvertToJsonOrThrow(
267
+ ReventlessCore.InboundTranslationSlice_Callback.auditRowSchema,
268
+ )
269
+ try {
270
+ let _ = await ops.save(id, json, ReventlessCore.QueryDb.Overwrite, None)
271
+ } catch {
272
+ | _ => ()
273
+ }
274
+ })
275
+ | None => ()
276
+ }
277
+ result
278
+ ->ReventlessCore.InboundTranslationSlice.receiveResultToOutcome
279
+ ->ReventlessCore.CommandTopic.commandOutcomeToJson
280
+ }
281
+ }
@@ -1,5 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
3
5
  import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
4
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
7
  import * as Effect from "effect/Effect";
@@ -7,9 +9,13 @@ import * as Stream from "effect/Stream";
7
9
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
10
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
9
11
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
12
+ import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
13
+ import * as InboundTranslationSlice$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice.res.mjs";
14
+ import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
10
15
  import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
11
16
  import * as DcbEventLogStorage_Postgres_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs";
12
17
  import * as StateChangeSlice_CallbackResMjs from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
18
+ import * as InboundTranslationSlice_CallbackResMjs from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
13
19
 
14
20
  function deriveScope(specs) {
15
21
  let scope = DcbTag$Reventless.deriveEffectiveScope(specs.map(s => ({
@@ -93,6 +99,27 @@ function buildSliceHandler(spec, behavior, tagKeysByEventType, crossPartitionTag
93
99
  };
94
100
  }
95
101
 
102
+ function buildInboundReceiver(spec, translation, publishJsons, auditQueryDbOps) {
103
+ let callback = InboundTranslationSlice_CallbackResMjs.Make(spec)(translation);
104
+ return async args => {
105
+ let result = await callback.receive(publishJsons, args);
106
+ if (auditQueryDbOps !== undefined) {
107
+ let rows = Object.entries(callback.auditLog);
108
+ await Stdlib_Array.reduce(rows, Promise.resolve(), async (prev, param) => {
109
+ await prev;
110
+ let json = S.reverseConvertToJsonOrThrow(param[1], InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema);
111
+ try {
112
+ await auditQueryDbOps.save(param[0], json, "Overwrite", undefined);
113
+ return;
114
+ } catch (exn) {
115
+ return;
116
+ }
117
+ });
118
+ }
119
+ return CommandTopic$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice$ReventlessCore.receiveResultToOutcome(result));
120
+ };
121
+ }
122
+
96
123
  export {
97
124
  deriveScope,
98
125
  commandTypeNames,
@@ -100,5 +127,6 @@ export {
100
127
  makePostgresStorageOps,
101
128
  makeStorageOps,
102
129
  buildSliceHandler,
130
+ buildInboundReceiver,
103
131
  }
104
- /* Id-Reventless Not a pure module */
132
+ /* S Not a pure module */
@@ -0,0 +1,44 @@
1
+ // Heartbeat Lambda entry point — compiled, type-checked ReScript (replaces the
2
+ // hand-written HeartbeatEntryPoint.mjs shell; no dynamic user-module import, so
3
+ // no untyped seam is needed).
4
+ //
5
+ // Publishes a Heartbeat(timeout) command to the PluginExtensionPoint
6
+ // CommandTopic. Triggered by CloudWatch Events on a schedule. Runtime-pure: no
7
+ // Pulumi value reaches this module's import graph (deploy-time wiring lives in
8
+ // PluginRuntime_Builder.forPluginHeartbeat).
9
+
10
+ @val @scope("process") external processEnv: dict<string> = "env"
11
+
12
+ // === Initialize eagerly at module load (Lambda cold start) ===
13
+
14
+ let epQueueUrl = processEnv->Dict.get("EP_QUEUE_URL")->Option.getOr("")
15
+ let pluginId = processEnv->Dict.get("PLUGIN_ID")->Option.getOr("")
16
+ // Mirrors the former shell's `parseInt(…) || 10`: unset, unparsable, and 0 all
17
+ // fall back to the 10-minute default.
18
+ let timeout =
19
+ processEnv
20
+ ->Dict.get("HEARTBEAT_TIMEOUT")
21
+ ->Option.flatMap(s => Int.fromString(s))
22
+ ->Option.filter(n => n != 0)
23
+ ->Option.getOr(10)
24
+
25
+ let queue: Util_SQS_Runtime.resolvedQueue = {id: epQueueUrl, name: epQueueUrl, arn: ""}
26
+ let publishJsons = queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO)
27
+
28
+ // === Exported handler ===
29
+
30
+ let handler = async (_event: JSON.t, _context: PulumiAws.Lambda.context) => {
31
+ let message: ReventlessCore.Message.commandJson = {
32
+ id: pluginId,
33
+ meta: ReventlessCore.Message.generateMeta(
34
+ ~service=ReventlessInfra.PluginExtensionPointSpec.name,
35
+ ~ip="",
36
+ ~user="Heartbeat",
37
+ ),
38
+ commandJson: ReventlessInfra.PluginExtensionPointSpec.Heartbeat(
39
+ timeout,
40
+ )->S.reverseConvertToJsonOrThrow(ReventlessInfra.PluginExtensionPointSpec.commandSchema),
41
+ }
42
+ await publishJsons([message])
43
+ ""
44
+ }
@@ -0,0 +1,47 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
7
+ import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
8
+ import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
9
+
10
+ let epQueueUrl = Stdlib_Option.getOr(process.env["EP_QUEUE_URL"], "");
11
+
12
+ let pluginId = Stdlib_Option.getOr(process.env["PLUGIN_ID"], "");
13
+
14
+ let timeout = Stdlib_Option.getOr(Stdlib_Option.filter(Stdlib_Option.flatMap(process.env["HEARTBEAT_TIMEOUT"], s => Stdlib_Int.fromString(s, undefined)), n => n !== 0), 10);
15
+
16
+ let queue = {
17
+ id: epQueueUrl,
18
+ name: epQueueUrl,
19
+ arn: ""
20
+ };
21
+
22
+ let publishJsons = CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, "SQS_FIFO");
23
+
24
+ async function handler(_event, _context) {
25
+ let message_meta = Message$ReventlessCore.generateMeta(PluginExtensionPointSpec$ReventlessInfra.name, "", "Heartbeat", undefined, undefined, undefined, undefined, undefined);
26
+ let message_commandJson = S.reverseConvertToJsonOrThrow({
27
+ TAG: "Heartbeat",
28
+ _0: timeout
29
+ }, PluginExtensionPointSpec$ReventlessInfra.commandSchema);
30
+ let message = {
31
+ id: pluginId,
32
+ meta: message_meta,
33
+ commandJson: message_commandJson
34
+ };
35
+ await publishJsons([message]);
36
+ return "";
37
+ }
38
+
39
+ export {
40
+ epQueueUrl,
41
+ pluginId,
42
+ timeout,
43
+ queue,
44
+ publishJsons,
45
+ handler,
46
+ }
47
+ /* epQueueUrl Not a pure module */
@@ -0,0 +1,234 @@
1
+ // PluginExtensionPoint Lambda entry point — compiled, type-checked ReScript
2
+ // (replaces the hand-written PluginExtensionPointEntryPoint.mjs shell).
3
+ //
4
+ // All wiring is framework modules — no user modules, hence no dynamic-import
5
+ // seam and no reason to stay untyped. Handles Heartbeat, Cloner, and other
6
+ // plugin-level extension point commands.
7
+ //
8
+ // The former shell's `patchSpecId` hack (re-adding the `Id` module that the
9
+ // ESM export of a `module Id = …` alias drops) disappears: `SpecWithId` below
10
+ // binds the Id module at the ReScript level, mirroring ExtensionPoint_Builder.
11
+ // Runtime-pure: no Pulumi value reaches this module's import graph.
12
+
13
+ @val @scope("process") external processEnv: dict<string> = "env"
14
+
15
+ // ── Shim bindings (HandlerFactoryHelpers.mjs) ───────────────────────────────
16
+ // The structured-log + Effect dispatch boundary shared by every deployed entry
17
+ // point (single place where invocations get their log annotations and
18
+ // RequestContext), and the DynamoDB scan backing the read-model query engine.
19
+
20
+ // Extra fields threaded onto the dispatch boundary / log lines. The shim reads
21
+ // them structurally, so one record serves both `runEffect` and `log.debug`.
22
+ type dispatchOpts = {
23
+ correlationId?: string,
24
+ causationId?: string,
25
+ comp?: string,
26
+ timestamp?: float,
27
+ retryCount?: int,
28
+ }
29
+
30
+ @module("./HandlerFactoryHelpers.mjs")
31
+ external setRequestId: string => unit = "setRequestId"
32
+ @module("./HandlerFactoryHelpers.mjs")
33
+ external runEffect: (Effect.t<'a, 'e, 'r>, dispatchOpts) => promise<unit> = "runEffect"
34
+ @module("./HandlerFactoryHelpers.mjs")
35
+ external extractMetaField: (array<PulumiAws.SQS.Queue.record>, string) => option<string> =
36
+ "extractMetaField"
37
+ @module("./HandlerFactoryHelpers.mjs")
38
+ external extractSentTimestamp: array<PulumiAws.SQS.Queue.record> => option<float> =
39
+ "extractSentTimestamp"
40
+ @module("./HandlerFactoryHelpers.mjs")
41
+ external extractRetryCount: array<PulumiAws.SQS.Queue.record> => int = "extractRetryCount"
42
+ @module("./HandlerFactoryHelpers.mjs") @scope("log")
43
+ external logDebug: (string, dispatchOpts) => unit = "debug"
44
+ @module("./HandlerFactoryHelpers.mjs")
45
+ external scanByTableName: (
46
+ string,
47
+ array<Reventless.QueryEngine.Filter.config>,
48
+ int,
49
+ ) => promise<array<JSON.t>> = "scanByTableName"
50
+
51
+ // ── HANDLER_CONFIG ──────────────────────────────────────────────────────────
52
+ // Written by PluginExtensionPointRuntime_Builder.forCommandTopic; all fields
53
+ // optional so a partial config degrades like the former shell's `|| ""` reads.
54
+
55
+ type handlerConfig = {
56
+ queueUrl?: string,
57
+ pluginReadModelTableName?: string,
58
+ schedulerRoleArn?: string,
59
+ schedulerQueueArn?: string,
60
+ schedulerQueueName?: string,
61
+ publishToAggregates?: dict<string>,
62
+ }
63
+ @val @scope("JSON") external parseHandlerConfig: string => handlerConfig = "parse"
64
+
65
+ // === Initialize eagerly at module load (Lambda cold start) ===
66
+
67
+ let config = processEnv->Dict.get("HANDLER_CONFIG")->Option.getOr("{}")->parseHandlerConfig
68
+
69
+ // Instantiate the Plugin EP mapping. updateApiSchema and manageSubscriptions
70
+ // are admin-only hooks; this Lambda only handles incoming commands (Heartbeat,
71
+ // ForwardCommand) so they stay None here. Only sendMessageToChannel is used by
72
+ // the incoming-command path (ForwardCommand) — cross-plugin subscribe /
73
+ // unsubscribe directives were retired in Phase 3 Step 3.
74
+ //
75
+ // `environment` prefixes the disconnect schedule's EventBridge rule name, so it
76
+ // must be stable across deploys and unique per stack. The stack name is both;
77
+ // AWS_LAMBDA_FUNCTION_NAME is neither — it carries a content hash, so replacing
78
+ // this Lambda would orphan every outstanding rule the previous generation
79
+ // created. EventCollectorEntryPoint instantiates the same EP module and must
80
+ // agree, or one Lambda creates rules the other cannot delete.
81
+ module EpSpec = {
82
+ let runtimeOps: ReventlessCore.PluginRuntimeOperations.operations = {
83
+ messagePublish: {sendMessageToChannel: Util_PluginMessage_Runtime.sendMessage},
84
+ }
85
+ let environment = processEnv->Dict.get("Environment")->Option.getOr("unknown")
86
+ let updateApiSchema: option<Reventless.QueryEngine.operations => promise<unit>> = None
87
+ let manageSubscriptions: option<
88
+ (Reventless.Plugin.pluginDefinition, [#connect | #disconnect]) => promise<unit>,
89
+ > = None
90
+ }
91
+ module PluginMappingInstance = ReventlessCore.PluginExtensionPoint_Plugin.Make(EpSpec)
92
+
93
+ module Mappings = {
94
+ module type Mapping = ReventlessInfra.ExtensionPointMapping.T
95
+ with module ExtensionPoint := ReventlessInfra.PluginExtensionPointSpec
96
+ // Parity with the former shell: only the Plugin lifecycle mapping. The
97
+ // in-process wiring (PluginExtensionPoint_Builder) additionally registers
98
+ // PluginExtensionPoint_UiFragment.Mapping — divergence tracked in
99
+ // docs/plans/entry-point-rescript-conversion.md.
100
+ let mappings: array<module(Mapping)> = [module(PluginMappingInstance.Mapping)]
101
+ }
102
+
103
+ // Reconstruct publishToAggregates. The deploy-side builder writes
104
+ // HANDLER_CONFIG.publishToAggregates as { aggregateName: envVarName }
105
+ // (see PluginExtensionPointRuntime_Builder.res), so iterate accordingly.
106
+ let publishToAggregates: dict<ReventlessCore.CommandTopic.publishJsons> =
107
+ config.publishToAggregates
108
+ ->Option.getOr(Dict.make())
109
+ ->Dict.toArray
110
+ ->Array.map(((aggName, envVarName)) => {
111
+ let queueUrl = processEnv->Dict.get(envVarName)->Option.getOr("")
112
+ let queue: Util_SQS_Runtime.resolvedQueue = {id: queueUrl, name: queueUrl, arn: ""}
113
+ (aggName, queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO))
114
+ })
115
+ ->Dict.fromArray
116
+
117
+ // Reconstruct queryEngine: scans go to the plugin read-model table; query is
118
+ // not available in this bundled handler (same restriction as the former shell).
119
+ let pluginReadModelTableName = config.pluginReadModelTableName->Option.getOr("")
120
+ let queryEngine: Reventless.QueryEngine.operations = {
121
+ scan: (~readModelName as _, ~filterConfigs, ~limit) =>
122
+ scanByTableName(pluginReadModelTableName, filterConfigs, limit),
123
+ query: async (
124
+ ~readModelName as _,
125
+ ~key as _=?,
126
+ ~id as _,
127
+ ~subIdConfig as _=?,
128
+ ~filterConfigs as _=?,
129
+ ~ascending as _=?,
130
+ ~limit as _=?,
131
+ ) => JsError.throwWithMessage("QueryEngine.query not available in bundled Plugin EP handler"),
132
+ }
133
+
134
+ // Reconstruct scheduler
135
+ let scheduler: ReventlessCore.Scheduler.operations = {
136
+ createSchedule: ScheduledPublisher_CloudWatchEvents_Runtime.createSchedule(
137
+ ~roleArn=config.schedulerRoleArn->Option.getOr(""),
138
+ ),
139
+ deleteSchedule: ScheduledPublisher_CloudWatchEvents_Runtime.deleteSchedule,
140
+ }
141
+
142
+ // CommandTopic resources for scheduler targets
143
+ let commandTopicResources: array<ReventlessInfra.Adapter.resolvedResource> = {
144
+ let schedulerQueueArn = config.schedulerQueueArn->Option.getOr("")
145
+ let schedulerQueueName = config.schedulerQueueName->Option.getOr("")
146
+ schedulerQueueArn == ""
147
+ ? []
148
+ : [
149
+ {
150
+ name: schedulerQueueName,
151
+ id: schedulerQueueName,
152
+ urn: schedulerQueueArn,
153
+ service: "unknown",
154
+ resourceInfo: ReventlessInfra.Adapter.NoInfo,
155
+ role: "",
156
+ region: "",
157
+ resourceType: "",
158
+ configuration: Dict.make(),
159
+ tags: Dict.make(),
160
+ },
161
+ ]
162
+ }
163
+
164
+ let invalidNameChars = %re("/[^.\-_a-zA-Z0-9]/g")
165
+ let resourceNaming: ReventlessInfra.ResourceNaming.operations = {
166
+ validateName: n => n->String.replaceRegExp(invalidNameChars, "_"),
167
+ urnName: arn =>
168
+ arn
169
+ ->String.split(":")
170
+ ->Array.get(5)
171
+ ->Option.filter(s => s != "")
172
+ ->Option.getOr("unknown"),
173
+ }
174
+
175
+ module Callback = ReventlessCore.ExtensionPoint_Callback.Make(
176
+ {
177
+ let publishToAggregates = publishToAggregates
178
+ let commandTopicResources = commandTopicResources
179
+ let scheduler = scheduler
180
+ let queryEngine = queryEngine
181
+ let resourceNaming = resourceNaming
182
+ },
183
+ ReventlessInfra.PluginExtensionPointSpec,
184
+ Mappings,
185
+ )
186
+
187
+ module SpecWithId = {
188
+ module Id = Reventless.Id.String
189
+ let name = ReventlessInfra.PluginExtensionPointSpec.name
190
+ type command = ReventlessInfra.PluginExtensionPointSpec.command
191
+ let commandSchema = ReventlessInfra.PluginExtensionPointSpec.commandSchema
192
+ }
193
+
194
+ module CommandTopicCallback = ReventlessCore.CommandTopic_Callback.Make(
195
+ SpecWithId,
196
+ {
197
+ module Spec = SpecWithId
198
+ let commandsHandler = Callback.handleIncomingCommands
199
+ },
200
+ )
201
+
202
+ let queue: Util_SQS_Runtime.resolvedQueue = {
203
+ id: config.queueUrl->Option.getOr(""),
204
+ name: config.queueUrl->Option.getOr(""),
205
+ arn: "",
206
+ }
207
+ let sqsHandler = CommandTopicChannel_SQS_Runtime.handleQueueEvent(
208
+ queue,
209
+ CommandTopicCallback.handleJsonCommands,
210
+ )
211
+ let comp = `ExtensionPoint(${ReventlessInfra.PluginExtensionPointSpec.name})`
212
+
213
+ // === Exported handler ===
214
+
215
+ let handler = async (event: PulumiAws.SQS.Queue.event, context: PulumiAws.Lambda.context) => {
216
+ setRequestId(context.awsRequestId)
217
+ let records = event.records
218
+
219
+ logDebug(
220
+ `processing ${records->Array.length->Int.toString} record(s)`,
221
+ {comp: "PluginExtensionPointRuntime"},
222
+ )
223
+ await runEffect(
224
+ sqsHandler(event, context),
225
+ {
226
+ correlationId: ?extractMetaField(records, "correlationId"),
227
+ causationId: ?extractMetaField(records, "causationId"),
228
+ comp,
229
+ timestamp: ?extractSentTimestamp(records),
230
+ retryCount: extractRetryCount(records),
231
+ },
232
+ )
233
+ ""
234
+ }