@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 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.230",
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-effect": "0.1.0-alpha.31",
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.182",
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-postgres": "3.0.0-alpha.46",
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
- // 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,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
- /* Id-Reventless Not a pure module */
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.all3((dcbTableName, queue.id, pgConnectionFragment))
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
- let make = InnerMake.make
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: InnerMake.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
- /* QueryDbStorage_DynamoDb-ReventlessAws Not a pure module */
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
- StateChangeSliceRuntime_Builder_Single$ReventlessAws.forDcbCommandTopic(slicePaths, dcbConfig.dcbTableName, dcbConfig.pluginName, syncStateChangesConfigRef.contents, asyncStateChangesConfigRef.contents, memorySize, timeout, connect, dcbCommandTopic);
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 */