@reventlessdev/reventless-aws 3.0.0-alpha.260 → 3.0.0-alpha.261

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,13 @@
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.261 (2026-08-03)
7
+
8
+ ### Features
9
+
10
+ * **aws:** let an outbound slice read an aggregate at runtime ([24585f8](https://github.com/ReventlessDev/reventless-core/commit/24585f869c15a07910434632d76d8c8d8058042b))
11
+
12
+
6
13
  # 3.0.0-alpha.260 (2026-08-03)
7
14
 
8
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.260",
3
+ "version": "3.0.0-alpha.261",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -11,18 +11,18 @@
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": "3.0.0-alpha.3",
15
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
14
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
15
17
  "@reventlessdev/rescript-node": "2.0.0-alpha.1",
16
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
17
18
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.65",
18
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.3",
19
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
20
19
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
21
- "@reventlessdev/reventless-core": "3.0.0-alpha.206",
20
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
22
21
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
23
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.70",
24
- "@reventlessdev/reventless-spec": "3.0.0-alpha.98",
25
- "@reventlessdev/reventless-infra": "3.0.0-alpha.123"
22
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.71",
23
+ "@reventlessdev/reventless-core": "3.0.0-alpha.207",
24
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.124",
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.99"
26
26
  },
27
27
  "devDependencies": {
28
28
  "rescript": "12.3.0",
@@ -45,7 +45,19 @@ async function buildAllHandlers() {
45
45
  specModule.name,
46
46
  automationSliceCallbackMake(specModule)(bodyModule)
47
47
  );
48
- StreamOps.addToRegistry(handlers, entry.sourceUrn, registered);
48
+ // A slice can listen on several streams — its plugin's DCB log and any
49
+ // Aggregate EventTopic it named. Register the one handler under each, since
50
+ // the router dispatches by the record's own eventSourceARN. `sourceUrn` is
51
+ // the pre-multi-source spelling, kept as a fallback so an older handler
52
+ // config still routes.
53
+ const sourceUrns = entry.sourceUrns?.length
54
+ ? entry.sourceUrns
55
+ : entry.sourceUrn
56
+ ? [entry.sourceUrn]
57
+ : [];
58
+ for (const sourceUrn of sourceUrns) {
59
+ StreamOps.addToRegistry(handlers, sourceUrn, registered);
60
+ }
49
61
  }));
50
62
 
51
63
  return handlers;
@@ -24,6 +24,20 @@ type sliceInfo = {
24
24
  // Threaded to phase 1 collect/resolve on the automation path (mirrors the
25
25
  // in-process builder's `~context`); outbound slices take no context.
26
26
  context: option<Reventless.AutomationSlice.context>,
27
+ // The non-DCB topics this slice subscribes to — an Aggregate's EventTopic when
28
+ // it names one by `Spec.name`. Empty for the common DCB-only slice.
29
+ //
30
+ // Captured *synchronously* here for the same reason the whole `bundledInfos`
31
+ // path exists: `forEventCollector` runs inside a `Pulumi.Output.apply`, so
32
+ // `storedSpecs` — which carries the resolved topics on the dead `finish` path
33
+ // — is still empty when `finishWithDcbEventLog` runs. The core builders
34
+ // compute this dict before that apply, so the value is available in time; it
35
+ // just had nowhere to go until now.
36
+ sourceTopics: ReventlessCore.EventTopic.allOutputs,
37
+ // Whether this slice also reads the plugin's own DCB event log. True for every
38
+ // slice that declares no sources (the default) and for any that names the log
39
+ // explicitly, which is why it is not derivable from `sourceTopics` being empty.
40
+ consumesDcbLog: bool,
27
41
  }
28
42
 
29
43
  let bundledInfos: dict<sliceInfo> = Dict.make()
@@ -35,6 +49,9 @@ let setDcbQueueUrl = url => dcbQueueUrlRef := Some(url)
35
49
  // ApiFragmentRegistry push (2e) to dispatch RecordApiFragmentPush.
36
50
  let getDcbQueueUrl = () => dcbQueueUrlRef.contents
37
51
 
52
+ // `~sourceTopics` / `~consumesDcbLog` default to the DCB-only shape every slice
53
+ // had before aggregate sources existed, so a caller that has not been updated
54
+ // keeps exactly its previous wiring.
38
55
  let registerAutomationSlice = (
39
56
  ~name,
40
57
  ~specModulePath,
@@ -43,10 +60,21 @@ let registerAutomationSlice = (
43
60
  ~queryDbTableName,
44
61
  ~queryDbResources=[],
45
62
  ~context=?,
63
+ ~sourceTopics=Dict.make(),
64
+ ~consumesDcbLog=true,
46
65
  ) =>
47
66
  bundledInfos->Dict.set(
48
67
  name,
49
- {specModulePath, bodyModulePath, callbackType, queryDbTableName, queryDbResources, context},
68
+ {
69
+ specModulePath,
70
+ bodyModulePath,
71
+ callbackType,
72
+ queryDbTableName,
73
+ queryDbResources,
74
+ context,
75
+ sourceTopics,
76
+ consumesDcbLog,
77
+ },
50
78
  )
51
79
 
52
80
  type storedSpec = {
@@ -259,9 +287,18 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
259
287
  let opts = {Pulumi.ComponentResource.parent: parent}
260
288
  let dcbOutputs: ReventlessCore.DcbEventLog.outputs =
261
289
  dcbEventLog->ReventlessCore.Component.outputs
290
+ // The plugin's DCB log, plus every Aggregate EventTopic any slice on this
291
+ // Lambda named. One channel spans them all: `connect` turns each topic
292
+ // into its own event-source mapping, and the entry point routes an
293
+ // incoming record to the handlers registered for its stream, so a slice
294
+ // only ever sees the sources it asked for.
262
295
  let eventTopics: ReventlessCore.EventTopic.allOutputs = Dict.fromArray([
263
296
  ("DcbEventLog", dcbOutputs.eventTopic),
264
297
  ])
298
+ bundledInfos->Dict.forEach(info =>
299
+ info.sourceTopics->Dict.forEachWithKey((topic, key) => eventTopics->Dict.set(key, topic))
300
+ )
301
+
265
302
  let channel = EventCollectorChannel.make(
266
303
  ~name="AllAutomationSlices",
267
304
  ~eventTopics,
@@ -269,13 +306,29 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
269
306
  ~opts,
270
307
  )
271
308
 
272
- // One DCB stream URN, identical for every slice on this plugin log — the
273
- // dispatch key the entry point maps event-source records back to.
274
- let sourceUrn = switch dcbOutputs.eventTopic.resources->Array.get(0) {
309
+ let dcbSourceUrn = switch dcbOutputs.eventTopic.resources->Array.get(0) {
275
310
  | Some(resource) => resource.urn
276
311
  | None => Pulumi.Output.make("")
277
312
  }
278
313
 
314
+ // The stream URNs this one slice listens on — the dispatch keys the entry
315
+ // point maps event-source records back to. Per slice rather than one
316
+ // shared DCB URN, which is what lets an Aggregate-sourced slice exist at
317
+ // all: its events arrive on the Aggregate's stream and would otherwise
318
+ // reach a registry that has never heard of them.
319
+ let sourceUrnsFor = (info: sliceInfo): Pulumi.Output.t<array<string>> => {
320
+ let urns = []
321
+ if info.consumesDcbLog {
322
+ urns->Array.push(dcbSourceUrn)->ignore
323
+ }
324
+ info.sourceTopics
325
+ ->Dict.valuesToArray
326
+ ->Array.forEach(topic =>
327
+ topic.resources->Array.forEach(resource => urns->Array.push(resource.urn)->ignore)
328
+ )
329
+ urns->Pulumi.Output.all
330
+ }
331
+
279
332
  let dcbQueueUrl = switch dcbQueueUrlRef.contents {
280
333
  | Some(url) => url
281
334
  | None => Pulumi.Output.make("NOT_AVAILABLE")
@@ -307,10 +360,16 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
307
360
  }
308
361
 
309
362
  let handlerJson =
310
- Pulumi.Output.all3((info.queryDbTableName, dcbQueueUrl, sourceUrn))
311
- ->Pulumi.Output.apply(((tableName, queueUrl, urn)) =>
312
- `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","sourceUrn":"${urn}"${contextFragment}}`
313
- )
363
+ Pulumi.Output.all3((info.queryDbTableName, dcbQueueUrl, sourceUrnsFor(info)))
364
+ ->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
365
+ let urnsJson =
366
+ urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify
367
+ // `sourceUrn` stays beside `sourceUrns` so a handler config read by
368
+ // an entry point that predates multi-source still routes its first
369
+ // stream rather than none.
370
+ let firstUrn = urns->Array.get(0)->Option.getOr("")
371
+ `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
372
+ })
314
373
  let _ = handlerOutputs->Array.push(handlerJson)
315
374
  })
316
375
 
@@ -28,16 +28,20 @@ function getDcbQueueUrl() {
28
28
  return dcbQueueUrlRef.contents;
29
29
  }
30
30
 
31
- function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context) {
31
+ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt) {
32
32
  let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
33
33
  let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
34
+ let sourceTopics = sourceTopicsOpt !== undefined ? sourceTopicsOpt : ({});
35
+ let consumesDcbLog = consumesDcbLogOpt !== undefined ? consumesDcbLogOpt : true;
34
36
  bundledInfos[name] = {
35
37
  specModulePath: specModulePath,
36
38
  bodyModulePath: bodyModulePath,
37
39
  callbackType: callbackType,
38
40
  queryDbTableName: queryDbTableName,
39
41
  queryDbResources: queryDbResources,
40
- context: context
42
+ context: context,
43
+ sourceTopics: sourceTopics,
44
+ consumesDcbLog: consumesDcbLog
41
45
  };
42
46
  }
43
47
 
@@ -161,9 +165,24 @@ function finishWithDcbEventLog(dcbEventLog) {
161
165
  "DcbEventLog",
162
166
  dcbOutputs.eventTopic
163
167
  ]]);
168
+ Stdlib_Dict.forEach(bundledInfos, info => Stdlib_Dict.forEachWithKey(info.sourceTopics, (topic, key) => {
169
+ eventTopics[key] = topic;
170
+ }));
164
171
  let channel = EventCollectorChannel_DynamoDbStream$ReventlessAws.make("AllAutomationSlices", eventTopics, undefined, opts);
165
172
  let resource = dcbOutputs.eventTopic.resources[0];
166
- let sourceUrn = resource !== undefined ? resource.urn : Pulumi.output("");
173
+ let dcbSourceUrn = resource !== undefined ? resource.urn : Pulumi.output("");
174
+ let sourceUrnsFor = info => {
175
+ let urns = [];
176
+ if (info.consumesDcbLog) {
177
+ urns.push(dcbSourceUrn);
178
+ }
179
+ Object.values(info.sourceTopics).forEach(topic => {
180
+ topic.resources.forEach(resource => {
181
+ urns.push(resource.urn);
182
+ });
183
+ });
184
+ return Pulumi.all(urns);
185
+ };
167
186
  let url = dcbQueueUrlRef.contents;
168
187
  let dcbQueueUrl = url !== undefined ? url : Pulumi.output("NOT_AVAILABLE");
169
188
  let handlerOutputs = [];
@@ -185,8 +204,13 @@ function finishWithDcbEventLog(dcbEventLog) {
185
204
  let handlerJson = Pulumi.all([
186
205
  info.queryDbTableName,
187
206
  dcbQueueUrl,
188
- sourceUrn
189
- ]).apply(param => `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","sourceUrn":"` + param[2] + `"` + contextFragment + `}`);
207
+ sourceUrnsFor(info)
208
+ ]).apply(param => {
209
+ let urns = param[2];
210
+ let urnsJson = JSON.stringify(urns.map(prim => prim));
211
+ let firstUrn = Stdlib_Option.getOr(urns[0], "");
212
+ return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
213
+ });
190
214
  handlerOutputs.push(handlerJson);
191
215
  });
192
216
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
@@ -37,6 +37,17 @@ module Make = (Api: {
37
37
  let tableResource = queryDbOutputs.resources->Array.getUnsafe(0)
38
38
  let queryDbTableName = tableResource.name
39
39
 
40
+ // An automation names its sources through its Mappings rather than a flat
41
+ // list, but the runtime question is the same one the outbound builder
42
+ // answers: which streams must this Lambda listen on. Unlike outbound, an
43
+ // automation's DCB log is already a key in `allEventTopics`, so the filter
44
+ // picks it up and nothing extra has to be assumed.
45
+ let sourceTopics =
46
+ allEventTopics->ReventlessCore.EventTopic.filter(sourceNames->Belt.Set.String.fromArray)
47
+ let consumesDcbLog =
48
+ sourceNames->Array.length == 0 ||
49
+ sourceNames->Array.some(name => !(allEventTopics->Dict.has(name)))
50
+
40
51
  EventCollectorRuntimeBuilder.registerAutomationSlice(
41
52
  ~name=Spec.name,
42
53
  ~specModulePath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
@@ -45,6 +56,8 @@ module Make = (Api: {
45
56
  ~queryDbTableName,
46
57
  ~queryDbResources=queryDbOutputs.resources,
47
58
  ~context,
59
+ ~sourceTopics,
60
+ ~consumesDcbLog,
48
61
  )
49
62
 
50
63
  as_
@@ -1,6 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
3
4
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
5
+ import * as EventTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventTopic/EventTopic.res.mjs";
4
6
  import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
5
7
  import * as QueryDbStorage_DynamoDb$ReventlessAws from "../adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs";
6
8
  import * as AutomationSlice_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Builder.res.mjs";
@@ -36,19 +38,22 @@ function Make(Api) {
36
38
  })(Api);
37
39
  let Make$1 = Spec => (Automation => {
38
40
  let InnerMake = Inner.Make(Spec)(Automation);
41
+ let sourceNames = InnerMake.sourceNames;
39
42
  let make = (allEventTopics, publishJsons, context, runtime, opts) => {
40
43
  let as_ = InnerMake.make(allEventTopics, publishJsons, context, runtime, opts);
41
44
  let queryDbOutputs = Component$ReventlessCore.outputs(as_).queryDb;
42
45
  let tableResource = queryDbOutputs.resources[0];
43
46
  let queryDbTableName = tableResource.name;
44
- AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context);
47
+ let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(sourceNames));
48
+ let consumesDcbLog = sourceNames.length === 0 || sourceNames.some(name => !(name in allEventTopics));
49
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Automation.moduleUrl), "automation", queryDbTableName, queryDbOutputs.resources, context, sourceTopics, consumesDcbLog);
45
50
  return as_;
46
51
  };
47
52
  return {
48
53
  Spec: Spec,
49
54
  Automation: Automation,
50
55
  queryDbName: InnerMake.queryDbName,
51
- sourceNames: InnerMake.sourceNames,
56
+ sourceNames: sourceNames,
52
57
  make: make
53
58
  };
54
59
  });
@@ -45,6 +45,20 @@ module Make = (Api: {
45
45
  let tableResource = queryDbOutputs.resources->Array.getUnsafe(0)
46
46
  let queryDbTableName = tableResource.name
47
47
 
48
+ // Which streams this slice's Lambda must actually listen on. The core
49
+ // `make` above has already validated every declared name and thrown on a
50
+ // typo, so this only has to select — a name absent from `allEventTopics`
51
+ // is necessarily the plugin's own DCB log, the one source that dict does
52
+ // not carry.
53
+ let declaredSources = Spec.sourceNames
54
+ let sourceTopics =
55
+ allEventTopics->ReventlessCore.EventTopic.filter(
56
+ declaredSources->Belt.Set.String.fromArray,
57
+ )
58
+ let consumesDcbLog =
59
+ declaredSources->Array.length == 0 ||
60
+ declaredSources->Array.some(name => !(allEventTopics->Dict.has(name)))
61
+
48
62
  AutomationSliceRuntime_Builder_Single.registerAutomationSlice(
49
63
  ~name=Spec.name,
50
64
  ~specModulePath=Util_Bundle.getModuleSpecifier(Spec.moduleUrl),
@@ -52,6 +66,8 @@ module Make = (Api: {
52
66
  ~callbackType="outbound",
53
67
  ~queryDbTableName,
54
68
  ~queryDbResources=queryDbOutputs.resources,
69
+ ~sourceTopics,
70
+ ~consumesDcbLog,
55
71
  )
56
72
 
57
73
  ots
@@ -1,6 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
3
4
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
5
+ import * as EventTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventTopic/EventTopic.res.mjs";
4
6
  import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
5
7
  import * as QueryDbStorage_DynamoDb$ReventlessAws from "../adapter/QueryDb/QueryDbStorage_DynamoDb.res.mjs";
6
8
  import * as QueryDbResolvers_AppSync$ReventlessAws from "../adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs";
@@ -49,7 +51,10 @@ function Make(Api) {
49
51
  let queryDbOutputs = Component$ReventlessCore.outputs(ots).queryDb;
50
52
  let tableResource = queryDbOutputs.resources[0];
51
53
  let queryDbTableName = tableResource.name;
52
- AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined);
54
+ let declaredSources = Spec.sourceNames;
55
+ let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(declaredSources));
56
+ let consumesDcbLog = declaredSources.length === 0 || declaredSources.some(name => !(name in allEventTopics));
57
+ AutomationSliceRuntime_Builder_Single$ReventlessAws.registerAutomationSlice(Spec.name, Util_Bundle$ReventlessAws.getModuleSpecifier(Spec.moduleUrl), Util_Bundle$ReventlessAws.getModuleSpecifier(Translation.moduleUrl), "outbound", queryDbTableName, queryDbOutputs.resources, undefined, sourceTopics, consumesDcbLog);
53
58
  return ots;
54
59
  };
55
60
  return {