@reventlessdev/reventless-aws 3.0.0-alpha.233 → 3.0.0-alpha.235

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,21 @@
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.235 (2026-07-27)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-aws
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.234 (2026-07-27)
15
+
16
+ ### Bug Fixes
17
+
18
+ * **aws:** thread InboundTranslationSlice audit table name without option<Output> ([8b2368a](https://github.com/ReventlessDev/reventless-core/commit/8b2368a17e49746beacc0855fd5c655517bfd41a))
19
+
20
+
6
21
  # 3.0.0-alpha.233 (2026-07-27)
7
22
 
8
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.233",
3
+ "version": "3.0.0-alpha.235",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -11,17 +11,17 @@
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-aws-sdk": "2.2.0-alpha.25",
15
- "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
16
14
  "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
15
+ "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.25",
17
16
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
18
17
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.61",
19
- "@reventlessdev/reventless-core": "3.0.0-alpha.184",
20
- "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
21
- "@reventlessdev/reventless-infra": "3.0.0-alpha.105",
18
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
22
19
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
23
- "@reventlessdev/reventless-spec": "3.0.0-alpha.81",
24
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.48"
20
+ "@reventlessdev/reventless-core": "3.0.0-alpha.185",
21
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
22
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.106",
23
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.49",
24
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.81"
25
25
  },
26
26
  "devDependencies": {
27
27
  "rescript": "12.3.0",
@@ -160,7 +160,20 @@ export async function buildHandlersForConfig(config, opts = {}) {
160
160
  await Promise.all(inboundModules.map(async ({ spec, translation, auditTableName }) => {
161
161
  const specModule = await loadModule(spec);
162
162
  const translationModule = await loadModule(translation);
163
- const auditOps = (auditTableName && !config.pgConnection)
163
+ // auditTableName must be a real non-empty string. A non-string here (e.g. a
164
+ // mis-serialized deploy-time value) would build a QueryDb ops whose TableName
165
+ // is an object, and every PutCommand would crash in AWS endpoint resolution
166
+ // ("value.split is not a function") — silently, since the audit save is
167
+ // best-effort. Guard against that instead of trusting the config shape.
168
+ const hasAuditTable = typeof auditTableName === "string" && auditTableName.length > 0;
169
+ if (auditTableName != null && !hasAuditTable) {
170
+ log.warn(
171
+ "inbound audit table name is not a string; skipping audit persistence: " +
172
+ JSON.stringify(auditTableName),
173
+ { comp: "DcbCommandTopicRuntime" },
174
+ );
175
+ }
176
+ const auditOps = (hasAuditTable && !config.pgConnection)
164
177
  ? makeDynamoQueryDbOps(auditTableName)
165
178
  : undefined;
166
179
  const fieldName = config.pluginName + "_" + specModule.name;
@@ -266,14 +266,24 @@ let buildInboundReceiver = (
266
266
  let json = row->S.reverseConvertToJsonOrThrow(
267
267
  ReventlessCore.InboundTranslationSlice_Callback.auditRowSchema,
268
268
  )
269
+ // A failed audit write must not fail the mutation (the command was
270
+ // already published), but it must not vanish either. `save` RESOLVES
271
+ // with `Error(_)` on a storage failure (it does not throw), so the
272
+ // result must be inspected — a bare `let _ = await ops.save(...)` would
273
+ // swallow the failure and leave the audit view permanently, silently
274
+ // empty. `try` still guards an unexpected reject.
269
275
  try {
270
- let _ = await ops.save(id, json, ReventlessCore.QueryDb.Overwrite, None)
276
+ switch await ops.save(id, json, ReventlessCore.QueryDb.Overwrite, None) {
277
+ | Ok() => ()
278
+ | Error(err) =>
279
+ ReventlessCore.EffectLogger.logError(
280
+ ~comp="InboundTranslationSlice.audit",
281
+ `failed to persist audit row ${id}: ${err
282
+ ->ReventlessCore.QueryDb.storageErrorToString}`,
283
+ )->Effect.runSync
284
+ }
271
285
  } catch {
272
286
  | exn =>
273
- // A failed audit write must not fail the mutation (the command was
274
- // already published), but it must not vanish either: an AccessDenied
275
- // here means the command Lambda's role lacks PutItem on the audit table,
276
- // which otherwise surfaces only as a permanently empty audit view.
277
287
  let detail =
278
288
  exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown error")
279
289
  ReventlessCore.EffectLogger.logError(
@@ -11,6 +11,7 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
11
11
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
12
12
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
13
13
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
14
+ import * as QueryDb$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb.res.mjs";
14
15
  import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
15
16
  import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
16
17
  import * as InboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
@@ -112,8 +113,12 @@ function buildInboundReceiver(spec, translation, publishJsons, auditQueryDbOps)
112
113
  await prev;
113
114
  let json = S.reverseConvertToJsonOrThrow(param[1], InboundTranslationSlice_Callback$ReventlessCore.auditRowSchema);
114
115
  try {
115
- await auditQueryDbOps.save(id, json, "Overwrite", undefined);
116
- return;
116
+ let err = await auditQueryDbOps.save(id, json, "Overwrite", undefined);
117
+ if (err.TAG === "Ok") {
118
+ return;
119
+ } else {
120
+ return Effect.runSync(EffectLogger$ReventlessCore.logError("InboundTranslationSlice.audit", undefined, `failed to persist audit row ` + id + `: ` + QueryDb$ReventlessCore.storageErrorToString(err._0)));
121
+ }
117
122
  } catch (raw_exn) {
118
123
  let exn = Primitive_exceptions.internalToException(raw_exn);
119
124
  let detail = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown error");
@@ -13,9 +13,28 @@ type iamPolicy = {
13
13
  }
14
14
  let additionalIamPolicies: array<iamPolicy> = []
15
15
 
16
- // Legacy CallbackFunction path — retained for module type compatibility.
17
- // Not called at runtime; all Lambda deployments now use makeFromCodeAsset.
18
- // Can be removed once the module type no longer requires environmentMaker.
16
+ // CallbackFunction path — the last serialized-closure Lambda builder on AWS.
17
+ //
18
+ // REACHABLE, despite most deployments going through makeFromCodeAsset. Three
19
+ // component builders instantiate ReventlessCore.EventCollectorRuntime_Builder_
20
+ // PerEventCollector.Make over this module — ForeignReadModel_Builder,
21
+ // SideEffectHandler_PerSideEffectHandler and Task_Builder_PerBucket — and that
22
+ // functor's forEventCollector calls `make` below. The live route is a Task whose
23
+ // spec returns `Task.sideEffects`: Task_Builder then constructs a
24
+ // SideEffectHandler, which provisions its collector Lambda here.
25
+ //
26
+ // A Lambda built this way is a serialized closure, and Pulumi's closure walker
27
+ // fails on Effect-TS — silently, so the function is simply never created and the
28
+ // deploy still reports success (see docs/plans/done/complete-bundled-migration.md,
29
+ // where an ordering-aws deploy came up four Lambdas short). Where it does
30
+ // serialize, it then resolves @aws-sdk against whatever the layer and the runtime
31
+ // each supply, carrying the cold-start SDK-skew failure compiled entry points
32
+ // were introduced to avoid. Converting these three builders to a compiled entry
33
+ // point is the fix; until then a side-effect-bearing Task is the exposed surface.
34
+ //
35
+ // `make` cannot simply be dropped: Runtime.Environment requires environmentMaker
36
+ // and the in-memory platform implements it for real (LocalRuntimeEnvironment
37
+ // registers the handler in a Deferred).
19
38
  let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, parts> = (
20
39
  ~name,
21
40
  ~handler,
@@ -27,8 +46,8 @@ let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, part
27
46
  let opts =
28
47
  opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
29
48
 
30
- // Legacy path (see above): no component kind reaches here because nothing calls
31
- // it, so it is attributed to the Platform rather than to an invented domain kind.
49
+ // The environmentMaker signature carries no component kind, so callers cannot
50
+ // pass one through; attributed to the Platform rather than to an invented kind.
32
51
  let tags = AWS.Tags.make(~name, ~kind=ReventlessCore.ComponentType.Platform, ~role=Runtime, ~scope=Platform)
33
52
 
34
53
  let lambdaRole = IAM.Role.makeWithDefaultPolicy(
@@ -58,7 +77,8 @@ let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, part
58
77
  )
59
78
 
60
79
  // Coerce CallbackFunction.t → Function.t (structurally compatible: both have arn, id, name).
61
- // This legacy path is only retained for module type compatibility.
80
+ // The coercion goes away with the path itself, once the three PerEventCollector
81
+ // builders above move to a compiled entry point.
62
82
  let lambdaAsFunction: Pulumi.Output.t<PulumiAws.Lambda.Function.t> = lambda->Obj.magic
63
83
 
64
84
  {
@@ -21,7 +21,10 @@ let log = ReventlessCore.Logger.fromEnv()
21
21
  type inboundSlicePaths = {
22
22
  specPath: string,
23
23
  translationPath: string,
24
- auditTableName: option<Pulumi.Output.t<string>>,
24
+ // Plain Output, never `option<Pulumi.Output.t<string>>` — see the note on
25
+ // `PluginRuntime_Builder.inboundSliceReg.auditTableName`. Empty string = no
26
+ // audit table (serialized as `null`).
27
+ auditTableName: Pulumi.Output.t<string>,
25
28
  }
26
29
 
27
30
  // `slicePaths`: `(specPath, behaviorPath)` per StateChangeSlice (already merged
@@ -142,7 +145,7 @@ let forDcbCommandTopic = (
142
145
  | [] => Pulumi.Output.make("")
143
146
  | slices =>
144
147
  slices
145
- ->Array.map(s => s.auditTableName->Option.getOr(Pulumi.Output.make("")))
148
+ ->Array.map(s => s.auditTableName)
146
149
  ->Pulumi.Output.all
147
150
  ->Pulumi.Output.apply(tableNames => {
148
151
  let entries =
@@ -59,7 +59,7 @@ function forDcbCommandTopic(slicePaths, inboundSlicesOpt, dcbTableName, pluginNa
59
59
  return `{"spec":` + s + `,"behavior":` + b + `}`;
60
60
  }).join(",");
61
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 => {
62
+ let inboundFragment = inboundSlices.length !== 0 ? Pulumi.all(inboundSlices.map(s => s.auditTableName)).apply(tableNames => {
63
63
  let entries = inboundSlices.map((s, i) => {
64
64
  let spec = Stdlib_Option.getOr(JSON.stringify(s.specPath), `""`);
65
65
  let translation = Stdlib_Option.getOr(JSON.stringify(s.translationPath), `""`);
@@ -56,7 +56,13 @@ let registerStateChangeSliceSpec = (~specPath: string, ~behaviorPath: string) =>
56
56
  type inboundSliceReg = {
57
57
  specPath: string,
58
58
  translationPath: string,
59
- mutable auditTableName: option<Pulumi.Output.t<string>>,
59
+ // Plain `Pulumi.Output.t<string>`, never `option<Pulumi.Output.t<string>>`:
60
+ // any generic `Option.*` call over an Output runs `valFromOption`, whose
61
+ // `.BS_PRIVATE_NESTED_SOME_NONE` probe hits the Output proxy (every property
62
+ // access returns a truthy Output), mis-classifies it as a nested-option and
63
+ // corrupts it into `{BS_PRIVATE_NESTED_SOME_NONE:0}`. The empty-string Output
64
+ // is the "no audit table" sentinel (serialized as `null` downstream).
65
+ mutable auditTableName: Pulumi.Output.t<string>,
60
66
  }
61
67
  let registeredInboundSlices: dict<inboundSliceReg> = Dict.make()
62
68
 
@@ -68,16 +74,19 @@ let registerInboundTranslationSliceSpec = (
68
74
  switch registeredInboundSlices->Dict.get(specName) {
69
75
  | Some(reg) => registeredInboundSlices->Dict.set(specName, {...reg, specPath, translationPath})
70
76
  | None =>
71
- registeredInboundSlices->Dict.set(specName, {specPath, translationPath, auditTableName: None})
77
+ registeredInboundSlices->Dict.set(
78
+ specName,
79
+ {specPath, translationPath, auditTableName: Pulumi.Output.make("")},
80
+ )
72
81
  }
73
82
 
74
83
  let registerInboundAuditTableName = (~specName: string, tableName: Pulumi.Output.t<string>) =>
75
84
  switch registeredInboundSlices->Dict.get(specName) {
76
- | Some(reg) => reg.auditTableName = Some(tableName)
85
+ | Some(reg) => reg.auditTableName = tableName
77
86
  | None =>
78
87
  registeredInboundSlices->Dict.set(
79
88
  specName,
80
- {specPath: "", translationPath: "", auditTableName: Some(tableName)},
89
+ {specPath: "", translationPath: "", auditTableName: tableName},
81
90
  )
82
91
  }
83
92
 
@@ -65,7 +65,7 @@ function registerInboundTranslationSliceSpec(specName, specPath, translationPath
65
65
  registeredInboundSlices[specName] = {
66
66
  specPath: specPath,
67
67
  translationPath: translationPath,
68
- auditTableName: undefined
68
+ auditTableName: Pulumi.output("")
69
69
  };
70
70
  }
71
71
  }