@reventlessdev/reventless-aws 3.0.0-alpha.230 → 3.0.0-alpha.232
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 +14 -0
- package/package.json +6 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +63 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +28 -1
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
- package/src/components/InboundTranslationSlice_Builder.res +27 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +58 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +48 -1
- 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/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.232 (2026-07-27)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** keep InboundTranslationSlice runtime path out of the Pulumi graph ([9166cff](https://github.com/ReventlessDev/reventless-core/commit/9166cff85939012d12e44835e2eb79a88fd1b66a))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.231 (2026-07-27)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** route InboundTranslationSlice mutations in the deployed DCB Lambda ([feb129c](https://github.com/ReventlessDev/reventless-core/commit/feb129caf291395e7d35bb953030a1e3efa06e1a))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.230 (2026-07-27)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.232",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
12
12
|
"sury": "11.0.0-alpha.4",
|
|
13
13
|
"uuid": "^13.0.0",
|
|
14
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
14
15
|
"@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.24",
|
|
15
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
16
16
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
17
17
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.60",
|
|
18
|
-
"@reventlessdev/rescript-
|
|
18
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
19
19
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
20
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
21
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.105",
|
|
20
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.183",
|
|
22
21
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
23
|
-
"@reventlessdev/reventless-
|
|
22
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.105",
|
|
23
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.47",
|
|
24
24
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.81"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
@@ -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,66 @@ 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_Callback.receiveResultToOutcome
|
|
279
|
+
// `CommandTopic_Helpers`, not `CommandTopic`: the latter imports `Adapter`
|
|
280
|
+
// (→ `@pulumi/pulumi`) for a deploy-time helper, which would crash this
|
|
281
|
+
// runtime Lambda's cold start. The encoder itself lives in the pure Helpers.
|
|
282
|
+
->ReventlessCore.CommandTopic_Helpers.commandOutcomeToJson
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -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,12 @@ 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_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
13
|
+
import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
|
|
10
14
|
import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
11
15
|
import * as DcbEventLogStorage_Postgres_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs";
|
|
12
16
|
import * as StateChangeSlice_CallbackResMjs from "@reventlessdev/reventless-core/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs";
|
|
17
|
+
import * as InboundTranslationSlice_CallbackResMjs from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
|
|
13
18
|
|
|
14
19
|
function deriveScope(specs) {
|
|
15
20
|
let scope = DcbTag$Reventless.deriveEffectiveScope(specs.map(s => ({
|
|
@@ -93,6 +98,27 @@ function buildSliceHandler(spec, behavior, tagKeysByEventType, crossPartitionTag
|
|
|
93
98
|
};
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
function buildInboundReceiver(spec, translation, publishJsons, auditQueryDbOps) {
|
|
102
|
+
let callback = InboundTranslationSlice_CallbackResMjs.Make(spec)(translation);
|
|
103
|
+
return async args => {
|
|
104
|
+
let result = await callback.receive(publishJsons, args);
|
|
105
|
+
if (auditQueryDbOps !== undefined) {
|
|
106
|
+
let rows = Object.entries(callback.auditLog);
|
|
107
|
+
await Stdlib_Array.reduce(rows, Promise.resolve(), async (prev, param) => {
|
|
108
|
+
await prev;
|
|
109
|
+
let json = S.reverseConvertToJsonOrThrow(param[1], InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema);
|
|
110
|
+
try {
|
|
111
|
+
await auditQueryDbOps.save(param[0], json, "Overwrite", undefined);
|
|
112
|
+
return;
|
|
113
|
+
} catch (exn) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return CommandTopic_Helpers$ReventlessCore.commandOutcomeToJson(InboundTranslationSlice_Callback$ReventlessCore.receiveResultToOutcome(result));
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
96
122
|
export {
|
|
97
123
|
deriveScope,
|
|
98
124
|
commandTypeNames,
|
|
@@ -100,5 +126,6 @@ export {
|
|
|
100
126
|
makePostgresStorageOps,
|
|
101
127
|
makeStorageOps,
|
|
102
128
|
buildSliceHandler,
|
|
129
|
+
buildInboundReceiver,
|
|
103
130
|
}
|
|
104
|
-
/*
|
|
131
|
+
/* S Not a pure module */
|
|
@@ -13,10 +13,22 @@
|
|
|
13
13
|
|
|
14
14
|
let log = ReventlessCore.Logger.fromEnv()
|
|
15
15
|
|
|
16
|
+
// `(specPath, translationPath)` module specifiers per InboundTranslationSlice,
|
|
17
|
+
// plus the resolved audit QueryDb table-name Output (None on the Postgres path or
|
|
18
|
+
// if the component hasn't reported it). The entry point imports both modules,
|
|
19
|
+
// builds `InboundTranslationSlice_Callback.Make(Spec)(Translation).receive`, and
|
|
20
|
+
// — when `auditTableName` is present — persists the audit rows to that table.
|
|
21
|
+
type inboundSlicePaths = {
|
|
22
|
+
specPath: string,
|
|
23
|
+
translationPath: string,
|
|
24
|
+
auditTableName: option<Pulumi.Output.t<string>>,
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
// `slicePaths`: `(specPath, behaviorPath)` per StateChangeSlice (already merged
|
|
17
28
|
// from auto- and manually-registered sources by the caller).
|
|
18
29
|
let forDcbCommandTopic = (
|
|
19
30
|
~slicePaths: array<(string, string)>,
|
|
31
|
+
~inboundSlices: array<inboundSlicePaths>=[],
|
|
20
32
|
~dcbTableName: option<Pulumi.Output.t<string>>,
|
|
21
33
|
~pluginName: string,
|
|
22
34
|
~syncConfig: ReventlessCore.Runtime.commandHandlerConfig,
|
|
@@ -29,7 +41,7 @@ let forDcbCommandTopic = (
|
|
|
29
41
|
~connect,
|
|
30
42
|
dcbCommandTopic,
|
|
31
43
|
) =>
|
|
32
|
-
if slicePaths->Array.length == 0 {
|
|
44
|
+
if slicePaths->Array.length == 0 && inboundSlices->Array.length == 0 {
|
|
33
45
|
log.warn(
|
|
34
46
|
~comp="StateChangeSliceRuntime_Builder_Single",
|
|
35
47
|
"forDcbCommandTopic skipped (no slice specs)",
|
|
@@ -122,11 +134,37 @@ let forDcbCommandTopic = (
|
|
|
122
134
|
| None => Pulumi.Output.make("")
|
|
123
135
|
}
|
|
124
136
|
|
|
137
|
+
// InboundTranslationSlice modules — spec + translation paths and the resolved
|
|
138
|
+
// audit table name (Pulumi-generated, so it must be resolved here). Emitted as
|
|
139
|
+
// a `,"inboundTranslationSliceModules":[…]` fragment merged into HANDLER_CONFIG
|
|
140
|
+
// (absent → the entry point has no Route 0 registry, unchanged behaviour).
|
|
141
|
+
let inboundFragment = switch inboundSlices {
|
|
142
|
+
| [] => Pulumi.Output.make("")
|
|
143
|
+
| slices =>
|
|
144
|
+
slices
|
|
145
|
+
->Array.map(s => s.auditTableName->Option.getOr(Pulumi.Output.make("")))
|
|
146
|
+
->Pulumi.Output.all
|
|
147
|
+
->Pulumi.Output.apply(tableNames => {
|
|
148
|
+
let entries =
|
|
149
|
+
slices
|
|
150
|
+
->Array.mapWithIndex((s, i) => {
|
|
151
|
+
let spec = s.specPath->JSON.stringifyAny->Option.getOr(`""`)
|
|
152
|
+
let translation = s.translationPath->JSON.stringifyAny->Option.getOr(`""`)
|
|
153
|
+
let tableName = tableNames->Array.getUnsafe(i)
|
|
154
|
+
let auditJson =
|
|
155
|
+
tableName == "" ? "null" : tableName->JSON.stringifyAny->Option.getOr("null")
|
|
156
|
+
`{"spec":${spec},"translation":${translation},"auditTableName":${auditJson}}`
|
|
157
|
+
})
|
|
158
|
+
->Array.join(",")
|
|
159
|
+
`,"inboundTranslationSliceModules":[${entries}]`
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
125
163
|
let handlerConfigJson =
|
|
126
|
-
Pulumi.Output.
|
|
127
|
-
->Pulumi.Output.apply(((table, queueUrl, pgFragment)) => {
|
|
164
|
+
Pulumi.Output.all4((dcbTableName, queue.id, pgConnectionFragment, inboundFragment))
|
|
165
|
+
->Pulumi.Output.apply(((table, queueUrl, pgFragment, inbFragment)) => {
|
|
128
166
|
let pluginNameJson = pluginName->JSON.stringifyAny->Option.getOr(`""`)
|
|
129
|
-
`{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson},"stateChangeSliceModules":[${sliceModulesJson}]${pgFragment}}`
|
|
167
|
+
`{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson},"stateChangeSliceModules":[${sliceModulesJson}]${pgFragment}${inbFragment}}`
|
|
130
168
|
})
|
|
131
169
|
envVars->Dict.set("HANDLER_CONFIG", handlerConfigJson->Pulumi.Output.asInput)
|
|
132
170
|
|
|
@@ -138,6 +176,13 @@ let forDcbCommandTopic = (
|
|
|
138
176
|
let behaviorPkg = Util_Bundle.extractPackageName(behaviorPath)
|
|
139
177
|
packageDirs->Dict.set(behaviorPkg, Util_Bundle.resolvePackageRoot(behaviorPkg))
|
|
140
178
|
})
|
|
179
|
+
// Inbound slice spec + translation packages the entry point imports for Route 0.
|
|
180
|
+
inboundSlices->Array.forEach(s => {
|
|
181
|
+
let specPkg = Util_Bundle.extractPackageName(s.specPath)
|
|
182
|
+
packageDirs->Dict.set(specPkg, Util_Bundle.resolvePackageRoot(specPkg))
|
|
183
|
+
let translationPkg = Util_Bundle.extractPackageName(s.translationPath)
|
|
184
|
+
packageDirs->Dict.set(translationPkg, Util_Bundle.resolvePackageRoot(translationPkg))
|
|
185
|
+
})
|
|
141
186
|
// Include the framework packages alongside the entry point so the deployed
|
|
142
187
|
// Lambda picks up uncommitted local changes without waiting for the Lambda
|
|
143
188
|
// Layer rebuild (the layer fetches @reventlessdev/reventless-* from GitHub
|
|
@@ -16,10 +16,11 @@ import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_L
|
|
|
16
16
|
|
|
17
17
|
let log = Logger$ReventlessCore.fromEnv();
|
|
18
18
|
|
|
19
|
-
function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, asyncConfig, sliceMemoryFloorOpt, sliceTimeoutFloorOpt, connect, dcbCommandTopic) {
|
|
19
|
+
function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginName, syncConfig, asyncConfig, sliceMemoryFloorOpt, sliceTimeoutFloorOpt, connect, dcbCommandTopic) {
|
|
20
|
+
let inboundSlices = inboundSlicesOpt !== undefined ? inboundSlicesOpt : [];
|
|
20
21
|
let sliceMemoryFloor = sliceMemoryFloorOpt !== undefined ? sliceMemoryFloorOpt : 0;
|
|
21
22
|
let sliceTimeoutFloor = sliceTimeoutFloorOpt !== undefined ? sliceTimeoutFloorOpt : 0;
|
|
22
|
-
if (slicePaths.length === 0) {
|
|
23
|
+
if (slicePaths.length === 0 && inboundSlices.length === 0) {
|
|
23
24
|
return log.warn("StateChangeSliceRuntime_Builder_Single", undefined, "forDcbCommandTopic skipped (no slice specs)");
|
|
24
25
|
}
|
|
25
26
|
let commandTopicResource = Component$ReventlessCore.toPulumiResource(dcbCommandTopic);
|
|
@@ -58,13 +59,24 @@ function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, as
|
|
|
58
59
|
return `{"spec":` + s + `,"behavior":` + b + `}`;
|
|
59
60
|
}).join(",");
|
|
60
61
|
let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => `,"pgConnection":` + JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(pgSelection.lockStrategy, cc))) : Pulumi.output("");
|
|
62
|
+
let inboundFragment = inboundSlices.length !== 0 ? Pulumi.all(inboundSlices.map(s => Stdlib_Option.getOr(s.auditTableName, Pulumi.output("")))).apply(tableNames => {
|
|
63
|
+
let entries = inboundSlices.map((s, i) => {
|
|
64
|
+
let spec = Stdlib_Option.getOr(JSON.stringify(s.specPath), `""`);
|
|
65
|
+
let translation = Stdlib_Option.getOr(JSON.stringify(s.translationPath), `""`);
|
|
66
|
+
let tableName = tableNames[i];
|
|
67
|
+
let auditJson = tableName === "" ? "null" : Stdlib_Option.getOr(JSON.stringify(tableName), "null");
|
|
68
|
+
return `{"spec":` + spec + `,"translation":` + translation + `,"auditTableName":` + auditJson + `}`;
|
|
69
|
+
}).join(",");
|
|
70
|
+
return `,"inboundTranslationSliceModules":[` + entries + `]`;
|
|
71
|
+
}) : Pulumi.output("");
|
|
61
72
|
let handlerConfigJson = Pulumi.all([
|
|
62
73
|
dcbTableName$1,
|
|
63
74
|
queue.id,
|
|
64
|
-
pgConnectionFragment
|
|
75
|
+
pgConnectionFragment,
|
|
76
|
+
inboundFragment
|
|
65
77
|
]).apply(param => {
|
|
66
78
|
let pluginNameJson = Stdlib_Option.getOr(JSON.stringify(pluginName), `""`);
|
|
67
|
-
return `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + `,"stateChangeSliceModules":[` + sliceModulesJson + `]` + param[2] + `}`;
|
|
79
|
+
return `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + `,"stateChangeSliceModules":[` + sliceModulesJson + `]` + param[2] + param[3] + `}`;
|
|
68
80
|
});
|
|
69
81
|
envVars["HANDLER_CONFIG"] = handlerConfigJson;
|
|
70
82
|
let packageDirs = {};
|
|
@@ -74,6 +86,12 @@ function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, as
|
|
|
74
86
|
let behaviorPkg = Util_Bundle$ReventlessAws.extractPackageName(param[1]);
|
|
75
87
|
packageDirs[behaviorPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(behaviorPkg);
|
|
76
88
|
});
|
|
89
|
+
inboundSlices.forEach(s => {
|
|
90
|
+
let specPkg = Util_Bundle$ReventlessAws.extractPackageName(s.specPath);
|
|
91
|
+
packageDirs[specPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(specPkg);
|
|
92
|
+
let translationPkg = Util_Bundle$ReventlessAws.extractPackageName(s.translationPath);
|
|
93
|
+
packageDirs[translationPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(translationPkg);
|
|
94
|
+
});
|
|
77
95
|
packageDirs["@reventlessdev/reventless-aws"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws");
|
|
78
96
|
packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-core");
|
|
79
97
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs", packageDirs, undefined);
|
|
@@ -15,11 +15,37 @@ module Make = (Api: {
|
|
|
15
15
|
Spec: Reventless.InboundTranslationSlice.Spec,
|
|
16
16
|
Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec,
|
|
17
17
|
): (ReventlessCore.InboundTranslationSlice.T with module Spec = Spec) => {
|
|
18
|
+
// Register spec + translation module paths so the shared DCB command Lambda's
|
|
19
|
+
// entry point can import them and route `__inboundTranslation` payloads to this
|
|
20
|
+
// slice's `receive` (mirrors StateChangeSlice_Builder's path registration).
|
|
21
|
+
PluginRuntime_Builder.registerInboundTranslationSliceSpec(
|
|
22
|
+
~specName=Spec.name,
|
|
23
|
+
~specPath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
|
|
24
|
+
~translationPath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
|
|
25
|
+
)
|
|
18
26
|
module InnerMake = Inner.Make(Spec, Translation)
|
|
19
27
|
module Spec = Spec
|
|
20
28
|
module Translation = Translation
|
|
21
29
|
type component = InnerMake.component
|
|
22
30
|
let queryDbName = InnerMake.queryDbName
|
|
23
|
-
|
|
31
|
+
|
|
32
|
+
let make = (~publishJsons, ~runtime=?, ~opts=?) => {
|
|
33
|
+
let component = InnerMake.make(~publishJsons, ~runtime?, ~opts?)
|
|
34
|
+
// The audit QueryDb's physical table name is Pulumi-generated, so it can only
|
|
35
|
+
// be read off the constructed component's outputs (resolved synchronously here,
|
|
36
|
+
// before forDcbCommandTopic builds HANDLER_CONFIG). Thread it so the entry
|
|
37
|
+
// point's Route 0 can persist the audit rows the in-process path writes inline.
|
|
38
|
+
let outputs: ReventlessInfra.InboundTranslationSlice.outputs =
|
|
39
|
+
component->ReventlessCore.Component.outputs
|
|
40
|
+
switch outputs.queryDb.resources->Array.get(0) {
|
|
41
|
+
| Some(tableResource) =>
|
|
42
|
+
PluginRuntime_Builder.registerInboundAuditTableName(
|
|
43
|
+
~specName=Spec.name,
|
|
44
|
+
tableResource.name,
|
|
45
|
+
)
|
|
46
|
+
| None => ()
|
|
47
|
+
}
|
|
48
|
+
component
|
|
49
|
+
}
|
|
24
50
|
}
|
|
25
51
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
4
|
+
import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
|
|
5
|
+
import * as PluginRuntime_Builder$ReventlessAws from "../plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
3
6
|
import * as QueryDbStorage_DynamoDb$ReventlessAws from "../adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs";
|
|
4
7
|
import * as QueryDbResolvers_AppSync$ReventlessAws from "../adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs";
|
|
5
8
|
import * as InboundTranslationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs";
|
|
@@ -11,12 +14,22 @@ function Make(Api) {
|
|
|
11
14
|
make: QueryDbResolvers_AppSync$ReventlessAws.make
|
|
12
15
|
})(Api);
|
|
13
16
|
let Make$1 = Spec => (Translation => {
|
|
17
|
+
PluginRuntime_Builder$ReventlessAws.registerInboundTranslationSliceSpec(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl));
|
|
14
18
|
let InnerMake = Inner.Make(Spec)(Translation);
|
|
19
|
+
let make = (publishJsons, runtime, opts) => {
|
|
20
|
+
let component = InnerMake.make(publishJsons, runtime, opts);
|
|
21
|
+
let outputs = Component$ReventlessCore.outputs(component);
|
|
22
|
+
let tableResource = outputs.queryDb.resources[0];
|
|
23
|
+
if (tableResource !== undefined) {
|
|
24
|
+
PluginRuntime_Builder$ReventlessAws.registerInboundAuditTableName(Spec.name, tableResource.name);
|
|
25
|
+
}
|
|
26
|
+
return component;
|
|
27
|
+
};
|
|
15
28
|
return {
|
|
16
29
|
Spec: Spec,
|
|
17
30
|
Translation: Translation,
|
|
18
31
|
queryDbName: InnerMake.queryDbName,
|
|
19
|
-
make:
|
|
32
|
+
make: make
|
|
20
33
|
};
|
|
21
34
|
});
|
|
22
35
|
return {
|
|
@@ -28,4 +41,4 @@ function Make(Api) {
|
|
|
28
41
|
export {
|
|
29
42
|
Make,
|
|
30
43
|
}
|
|
31
|
-
/*
|
|
44
|
+
/* Component-ReventlessCore Not a pure module */
|
|
@@ -43,6 +43,44 @@ let registerStateChangeSliceSpec = (~specPath: string, ~behaviorPath: string) =>
|
|
|
43
43
|
let _ = registeredSliceModulePaths->Array.push({specPath, behaviorPath})
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// InboundTranslationSlice registration for the shared DCB command Lambda. Unlike
|
|
47
|
+
// StateChangeSlices (routed by command TAG), inbound slices are invoked directly
|
|
48
|
+
// by their AppSync mutation resolver with an `__inboundTranslation` payload — the
|
|
49
|
+
// entry point routes those to a `receive` handler built from the spec + translation
|
|
50
|
+
// modules. Registration is two-phase: the AWS `InboundTranslationSlice_Builder`
|
|
51
|
+
// functor registers the module paths at instantiation (spec/translation `moduleUrl`,
|
|
52
|
+
// like StateChangeSlice), and its wrapped `make` registers the audit QueryDb's
|
|
53
|
+
// resolved table-name Output once the component is constructed (the physical name
|
|
54
|
+
// is Pulumi-generated, so it can't be reconstructed at runtime). Both fire before
|
|
55
|
+
// `forDcbCommandTopic` reads them.
|
|
56
|
+
type inboundSliceReg = {
|
|
57
|
+
specPath: string,
|
|
58
|
+
translationPath: string,
|
|
59
|
+
mutable auditTableName: option<Pulumi.Output.t<string>>,
|
|
60
|
+
}
|
|
61
|
+
let registeredInboundSlices: dict<inboundSliceReg> = Dict.make()
|
|
62
|
+
|
|
63
|
+
let registerInboundTranslationSliceSpec = (
|
|
64
|
+
~specName: string,
|
|
65
|
+
~specPath: string,
|
|
66
|
+
~translationPath: string,
|
|
67
|
+
) =>
|
|
68
|
+
switch registeredInboundSlices->Dict.get(specName) {
|
|
69
|
+
| Some(reg) => registeredInboundSlices->Dict.set(specName, {...reg, specPath, translationPath})
|
|
70
|
+
| None =>
|
|
71
|
+
registeredInboundSlices->Dict.set(specName, {specPath, translationPath, auditTableName: None})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let registerInboundAuditTableName = (~specName: string, tableName: Pulumi.Output.t<string>) =>
|
|
75
|
+
switch registeredInboundSlices->Dict.get(specName) {
|
|
76
|
+
| Some(reg) => reg.auditTableName = Some(tableName)
|
|
77
|
+
| None =>
|
|
78
|
+
registeredInboundSlices->Dict.set(
|
|
79
|
+
specName,
|
|
80
|
+
{specPath: "", translationPath: "", auditTableName: Some(tableName)},
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
46
84
|
/**
|
|
47
85
|
Redundant with the seams that populate `dcbConfigRef` automatically:
|
|
48
86
|
`pluginName` is set by `registerPluginName` (called from `Plugin_Builder.make`
|
|
@@ -768,8 +806,28 @@ module Make = (
|
|
|
768
806
|
registeredSliceModulePaths
|
|
769
807
|
->Array.concat(dcbConfig.stateChangeSliceModulePaths)
|
|
770
808
|
->Array.map(({specPath, behaviorPath}) => (specPath, behaviorPath))
|
|
809
|
+
// Drop registrations that never received their module paths (a bare
|
|
810
|
+
// audit-table registration with no matching spec functor — shouldn't happen,
|
|
811
|
+
// but keeps a half-registered slice out of HANDLER_CONFIG).
|
|
812
|
+
let inboundSlices =
|
|
813
|
+
registeredInboundSlices
|
|
814
|
+
->Dict.valuesToArray
|
|
815
|
+
->Array.filterMap(reg =>
|
|
816
|
+
reg.specPath == ""
|
|
817
|
+
? None
|
|
818
|
+
: Some(
|
|
819
|
+
(
|
|
820
|
+
{
|
|
821
|
+
StateChangeSliceRuntime_Builder_Single.specPath: reg.specPath,
|
|
822
|
+
translationPath: reg.translationPath,
|
|
823
|
+
auditTableName: reg.auditTableName,
|
|
824
|
+
}: StateChangeSliceRuntime_Builder_Single.inboundSlicePaths
|
|
825
|
+
),
|
|
826
|
+
)
|
|
827
|
+
)
|
|
771
828
|
StateChangeSliceRuntime_Builder_Single.forDcbCommandTopic(
|
|
772
829
|
~slicePaths,
|
|
830
|
+
~inboundSlices,
|
|
773
831
|
~dcbTableName=dcbConfig.dcbTableName,
|
|
774
832
|
~pluginName=dcbConfig.pluginName,
|
|
775
833
|
~syncConfig=syncStateChangesConfigRef.contents,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
7
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
@@ -50,6 +51,38 @@ function registerStateChangeSliceSpec(specPath, behaviorPath) {
|
|
|
50
51
|
});
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
let registeredInboundSlices = {};
|
|
55
|
+
|
|
56
|
+
function registerInboundTranslationSliceSpec(specName, specPath, translationPath) {
|
|
57
|
+
let reg = registeredInboundSlices[specName];
|
|
58
|
+
if (reg !== undefined) {
|
|
59
|
+
registeredInboundSlices[specName] = {
|
|
60
|
+
specPath: specPath,
|
|
61
|
+
translationPath: translationPath,
|
|
62
|
+
auditTableName: reg.auditTableName
|
|
63
|
+
};
|
|
64
|
+
} else {
|
|
65
|
+
registeredInboundSlices[specName] = {
|
|
66
|
+
specPath: specPath,
|
|
67
|
+
translationPath: translationPath,
|
|
68
|
+
auditTableName: undefined
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function registerInboundAuditTableName(specName, tableName) {
|
|
74
|
+
let reg = registeredInboundSlices[specName];
|
|
75
|
+
if (reg !== undefined) {
|
|
76
|
+
reg.auditTableName = tableName;
|
|
77
|
+
} else {
|
|
78
|
+
registeredInboundSlices[specName] = {
|
|
79
|
+
specPath: "",
|
|
80
|
+
translationPath: "",
|
|
81
|
+
auditTableName: tableName
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
53
86
|
function registerDcbConfig(pluginName, dcbTableName, stateChangeSliceModulePathsOpt, param) {
|
|
54
87
|
let stateChangeSliceModulePaths = stateChangeSliceModulePathsOpt !== undefined ? stateChangeSliceModulePathsOpt : [];
|
|
55
88
|
dcbConfigRef.contents = {
|
|
@@ -433,7 +466,18 @@ function Make(EventCollectorChannel) {
|
|
|
433
466
|
param.specPath,
|
|
434
467
|
param.behaviorPath
|
|
435
468
|
]);
|
|
436
|
-
|
|
469
|
+
let inboundSlices = Stdlib_Array.filterMap(Object.values(registeredInboundSlices), reg => {
|
|
470
|
+
if (reg.specPath === "") {
|
|
471
|
+
return;
|
|
472
|
+
} else {
|
|
473
|
+
return {
|
|
474
|
+
specPath: reg.specPath,
|
|
475
|
+
translationPath: reg.translationPath,
|
|
476
|
+
auditTableName: reg.auditTableName
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
StateChangeSliceRuntime_Builder_Single$ReventlessAws.forDcbCommandTopic(slicePaths, inboundSlices, dcbConfig.dcbTableName, dcbConfig.pluginName, syncStateChangesConfigRef.contents, asyncStateChangesConfigRef.contents, memorySize, timeout, connect, dcbCommandTopic);
|
|
437
481
|
};
|
|
438
482
|
let finish = () => {};
|
|
439
483
|
return {
|
|
@@ -452,6 +496,9 @@ export {
|
|
|
452
496
|
dcbConfigRef,
|
|
453
497
|
registeredSliceModulePaths,
|
|
454
498
|
registerStateChangeSliceSpec,
|
|
499
|
+
registeredInboundSlices,
|
|
500
|
+
registerInboundTranslationSliceSpec,
|
|
501
|
+
registerInboundAuditTableName,
|
|
455
502
|
registerDcbConfig,
|
|
456
503
|
registerDcbTableName,
|
|
457
504
|
heartbeatConfigRef,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// CI routing guard for the DCB CommandTopic Lambda's Route 0 (InboundTranslation).
|
|
2
|
+
//
|
|
3
|
+
// The AppSync resolver for an InboundTranslationSlice mutation invokes the shared
|
|
4
|
+
// DCB command Lambda with `{__inboundTranslation, fieldName, arguments}` — no
|
|
5
|
+
// `command`, no `Records`. Before Route 0 existed the payload fell through to the
|
|
6
|
+
// SQS route and crashed on `event.records` being undefined ("Cannot read
|
|
7
|
+
// properties of undefined (reading 'length')"), so every inbound mutation 500'd
|
|
8
|
+
// on a deployed stack (docs/plans/done/aws-inbound-translation-lambda-routing.md).
|
|
9
|
+
// The gap that let this ship: `__inboundTranslation` was only ever asserted on the
|
|
10
|
+
// sending side (the resolver-template test). This pins the receiving end.
|
|
11
|
+
//
|
|
12
|
+
// Drives the real `buildHandlersForConfig` with an inbound slice module and a stub
|
|
13
|
+
// loader, then dispatches the payload exactly as `handler`'s Route 0 does. The
|
|
14
|
+
// fixture translation rejects its input, so the receiver returns CommandRejected
|
|
15
|
+
// inline without touching SQS or DynamoDB — no Docker needed, runs in CI.
|
|
16
|
+
|
|
17
|
+
open JestGlobals
|
|
18
|
+
|
|
19
|
+
let routeInbound: JSON.t => promise<JSON.t> = %raw(`
|
|
20
|
+
async (event) => {
|
|
21
|
+
const { buildHandlersForConfig } = await import(
|
|
22
|
+
"@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs"
|
|
23
|
+
);
|
|
24
|
+
const loadModule = async (specifier) => {
|
|
25
|
+
if (specifier === "ep-inbound-test://spec") return await import("./EpInboundTestSlice.res.mjs");
|
|
26
|
+
if (specifier === "ep-inbound-test://translation") return await import("./EpInboundTestSliceTranslation.res.mjs");
|
|
27
|
+
throw new Error("unknown test specifier: " + specifier);
|
|
28
|
+
};
|
|
29
|
+
const config = {
|
|
30
|
+
pluginName: "EpInboundTestPlugin",
|
|
31
|
+
dcbEventLogTableName: "ep-inbound-test-table",
|
|
32
|
+
stateChangeSliceModules: [],
|
|
33
|
+
queueUrl: "https://sqs.eu-west-1.amazonaws.com/000000000000/ep-inbound-test-queue",
|
|
34
|
+
inboundTranslationSliceModules: [
|
|
35
|
+
{ spec: "ep-inbound-test://spec", translation: "ep-inbound-test://translation", auditTableName: null },
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
const [,,,, inboundReceivers] = await buildHandlersForConfig(config, { loadModule });
|
|
39
|
+
// Mirror handler's Route 0 dispatch.
|
|
40
|
+
const receiver = (inboundReceivers || {})[event.fieldName];
|
|
41
|
+
if (receiver === undefined) throw new Error("no inbound receiver for " + event.fieldName);
|
|
42
|
+
return await receiver(event.arguments);
|
|
43
|
+
}
|
|
44
|
+
`)
|
|
45
|
+
|
|
46
|
+
let inboundEvent = (~fieldName, ~currency): JSON.t => {
|
|
47
|
+
let arguments = Dict.fromArray([
|
|
48
|
+
("sku", "SKU-1"->JSON.Encode.string),
|
|
49
|
+
("currency", currency->JSON.Encode.string),
|
|
50
|
+
])
|
|
51
|
+
Dict.fromArray([
|
|
52
|
+
("__inboundTranslation", true->JSON.Encode.bool),
|
|
53
|
+
("fieldName", fieldName->JSON.Encode.string),
|
|
54
|
+
("arguments", arguments->JSON.Encode.object),
|
|
55
|
+
])->JSON.Encode.object
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe("DcbCommandTopicEntryPoint Route 0 (InboundTranslation)", () => {
|
|
59
|
+
test(
|
|
60
|
+
"routes an __inboundTranslation payload to the slice's receive and encodes the outcome",
|
|
61
|
+
async () => {
|
|
62
|
+
// fieldName is `${pluginName}_${specName}` — the same string
|
|
63
|
+
// Api_Naming.sliceMutationField produces at deploy time.
|
|
64
|
+
let event = inboundEvent(~fieldName="EpInboundTestPlugin_EpInboundTest", ~currency="EUR")
|
|
65
|
+
let outcome = await routeInbound(event)
|
|
66
|
+
let s = outcome->JSON.stringifyAny->Option.getOr("<unserializable>")
|
|
67
|
+
// A serializable outcome at all proves the payload no longer crashes on the
|
|
68
|
+
// SQS route; the rejection proves it ran the slice's translate.
|
|
69
|
+
expect(s->String.includes("CommandRejected"))->toBe(true)
|
|
70
|
+
expect(s->String.includes("Unsupported currency"))->toBe(true)
|
|
71
|
+
},
|
|
72
|
+
)
|
|
73
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
|
|
5
|
+
let routeInbound = (async (event) => {
|
|
6
|
+
const { buildHandlersForConfig } = await import(
|
|
7
|
+
"@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs"
|
|
8
|
+
);
|
|
9
|
+
const loadModule = async (specifier) => {
|
|
10
|
+
if (specifier === "ep-inbound-test://spec") return await import("./EpInboundTestSlice.res.mjs");
|
|
11
|
+
if (specifier === "ep-inbound-test://translation") return await import("./EpInboundTestSliceTranslation.res.mjs");
|
|
12
|
+
throw new Error("unknown test specifier: " + specifier);
|
|
13
|
+
};
|
|
14
|
+
const config = {
|
|
15
|
+
pluginName: "EpInboundTestPlugin",
|
|
16
|
+
dcbEventLogTableName: "ep-inbound-test-table",
|
|
17
|
+
stateChangeSliceModules: [],
|
|
18
|
+
queueUrl: "https://sqs.eu-west-1.amazonaws.com/000000000000/ep-inbound-test-queue",
|
|
19
|
+
inboundTranslationSliceModules: [
|
|
20
|
+
{ spec: "ep-inbound-test://spec", translation: "ep-inbound-test://translation", auditTableName: null },
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
const [,,,, inboundReceivers] = await buildHandlersForConfig(config, { loadModule });
|
|
24
|
+
// Mirror handler's Route 0 dispatch.
|
|
25
|
+
const receiver = (inboundReceivers || {})[event.fieldName];
|
|
26
|
+
if (receiver === undefined) throw new Error("no inbound receiver for " + event.fieldName);
|
|
27
|
+
return await receiver(event.arguments);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function inboundEvent(fieldName, currency) {
|
|
31
|
+
let $$arguments = Object.fromEntries([
|
|
32
|
+
[
|
|
33
|
+
"sku",
|
|
34
|
+
"SKU-1"
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"currency",
|
|
38
|
+
currency
|
|
39
|
+
]
|
|
40
|
+
]);
|
|
41
|
+
return Object.fromEntries([
|
|
42
|
+
[
|
|
43
|
+
"__inboundTranslation",
|
|
44
|
+
true
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"fieldName",
|
|
48
|
+
fieldName
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
"arguments",
|
|
52
|
+
$$arguments
|
|
53
|
+
]
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
globalThis.describe("DcbCommandTopicEntryPoint Route 0 (InboundTranslation)", () => {
|
|
58
|
+
globalThis.test("routes an __inboundTranslation payload to the slice's receive and encodes the outcome", async () => {
|
|
59
|
+
let event = inboundEvent("EpInboundTestPlugin_EpInboundTest", "EUR");
|
|
60
|
+
let outcome = await routeInbound(event);
|
|
61
|
+
let s = Stdlib_Option.getOr(JSON.stringify(outcome), "<unserializable>");
|
|
62
|
+
globalThis.expect(s.includes("CommandRejected")).toBe(true);
|
|
63
|
+
globalThis.expect(s.includes("Unsupported currency")).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
routeInbound,
|
|
69
|
+
inboundEvent,
|
|
70
|
+
}
|
|
71
|
+
/* routeInbound Not a pure module */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Minimal InboundTranslationSlice spec fixture for the DCB entry-point Route 0
|
|
2
|
+
// routing test (DcbInboundTranslationRoutingTest). Hand-written — not a plugin
|
|
3
|
+
// component, so no folder-based ppx; only the fields
|
|
4
|
+
// InboundTranslationSlice_Callback.Make reads at runtime are provided.
|
|
5
|
+
|
|
6
|
+
let name = "EpInboundTest"
|
|
7
|
+
let moduleUrl = "ep-inbound-test://spec"
|
|
8
|
+
|
|
9
|
+
@schema
|
|
10
|
+
type externalInput = {
|
|
11
|
+
sku: string,
|
|
12
|
+
currency: string,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@schema
|
|
16
|
+
type command = AddThing({thingId: string})
|
|
17
|
+
|
|
18
|
+
let targetName = "AddThing"
|
|
19
|
+
let externalSystem: option<string> = Some("TestFeed")
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
|
|
5
|
+
let externalInputSchema = S.schema(s => ({
|
|
6
|
+
sku: s.m(S.string),
|
|
7
|
+
currency: s.m(S.string)
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
let commandSchema = S.schema(s => ({
|
|
11
|
+
TAG: "AddThing",
|
|
12
|
+
thingId: s.m(S.string)
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
let name = "EpInboundTest";
|
|
16
|
+
|
|
17
|
+
let moduleUrl = "ep-inbound-test://spec";
|
|
18
|
+
|
|
19
|
+
let targetName = "AddThing";
|
|
20
|
+
|
|
21
|
+
let externalSystem = "TestFeed";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
name,
|
|
25
|
+
moduleUrl,
|
|
26
|
+
externalInputSchema,
|
|
27
|
+
commandSchema,
|
|
28
|
+
targetName,
|
|
29
|
+
externalSystem,
|
|
30
|
+
}
|
|
31
|
+
/* externalInputSchema Not a pure module */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Translation fixture paired with EpInboundTestSlice. Rejects non-USD input so the
|
|
2
|
+
// routing test exercises the receive reject path — no command is published, so the
|
|
3
|
+
// test needs neither SQS nor DynamoDB.
|
|
4
|
+
|
|
5
|
+
module Spec = EpInboundTestSlice
|
|
6
|
+
open Spec
|
|
7
|
+
|
|
8
|
+
let moduleUrl = "ep-inbound-test://translation"
|
|
9
|
+
|
|
10
|
+
let translate = (input: externalInput): result<array<(string, command)>, string> =>
|
|
11
|
+
input.currency !== "USD"
|
|
12
|
+
? Error("Unsupported currency: " ++ input.currency)
|
|
13
|
+
: Ok([(input.sku, AddThing({thingId: input.sku}))])
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function translate(input) {
|
|
5
|
+
if (input.currency !== "USD") {
|
|
6
|
+
return {
|
|
7
|
+
TAG: "Error",
|
|
8
|
+
_0: "Unsupported currency: " + input.currency
|
|
9
|
+
};
|
|
10
|
+
} else {
|
|
11
|
+
return {
|
|
12
|
+
TAG: "Ok",
|
|
13
|
+
_0: [[
|
|
14
|
+
input.sku,
|
|
15
|
+
{
|
|
16
|
+
TAG: "AddThing",
|
|
17
|
+
thingId: input.sku
|
|
18
|
+
}
|
|
19
|
+
]]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let Spec;
|
|
25
|
+
|
|
26
|
+
let moduleUrl = "ep-inbound-test://translation";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
Spec,
|
|
30
|
+
moduleUrl,
|
|
31
|
+
translate,
|
|
32
|
+
}
|
|
33
|
+
/* No side effect */
|