@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 +15 -0
- package/package.json +8 -8
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +14 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +15 -5
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +7 -2
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +26 -6
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +5 -2
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +13 -4
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +1 -1
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.
|
|
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/
|
|
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-
|
|
24
|
-
"@reventlessdev/reventless-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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
|
-
//
|
|
31
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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 =>
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
85
|
+
| Some(reg) => reg.auditTableName = tableName
|
|
77
86
|
| None =>
|
|
78
87
|
registeredInboundSlices->Dict.set(
|
|
79
88
|
specName,
|
|
80
|
-
{specPath: "", translationPath: "", auditTableName:
|
|
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:
|
|
68
|
+
auditTableName: Pulumi.output("")
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
}
|