@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.
- package/CHANGELOG.md +15 -0
- package/package.json +5 -5
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +60 -141
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +52 -118
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +137 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +124 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +60 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +29 -1
- package/src/adapter/Runtime/HeartbeatEntryPoint.res +44 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +47 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +234 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs +226 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
- package/src/adapter/Upload/Upload_Presign_S3.res +65 -164
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +56 -119
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res +157 -0
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +120 -0
- package/src/components/InboundTranslationSlice_Builder.res +27 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -1
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +59 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +49 -2
- package/tests/DcbInboundTranslationRoutingTest.res +73 -0
- package/tests/DcbInboundTranslationRoutingTest.res.mjs +71 -0
- package/tests/EpInboundTestSlice.res +19 -0
- package/tests/EpInboundTestSlice.res.mjs +31 -0
- package/tests/EpInboundTestSliceTranslation.res +13 -0
- package/tests/EpInboundTestSliceTranslation.res.mjs +33 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +0 -38
- 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
|
-
//
|
|
189
|
-
// destructuring the first two. No deploy-time `plugin` fragment
|
|
190
|
-
// this Lambda serves exactly one plugin and HANDLER_CONFIG already
|
|
191
|
-
|
|
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
|
-
/*
|
|
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
|
+
}
|