@reventlessdev/reventless-aws 3.0.0-alpha.263 → 3.0.0-alpha.265

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +7 -7
  3. package/src/Platform.res +37 -6
  4. package/src/Platform.res.mjs +21 -6
  5. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +6 -1
  6. package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +4 -1
  7. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +5 -0
  8. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +3 -0
  9. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +6 -1
  10. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +4 -1
  11. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +6 -1
  12. package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +4 -1
  13. package/src/adapter/CommandTopic/CommandTopicRegistry.res +50 -0
  14. package/src/adapter/CommandTopic/CommandTopicRegistry.res.mjs +26 -0
  15. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -3
  16. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +230 -29
  17. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +124 -9
  18. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +188 -3
  19. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +107 -6
  20. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  21. package/src/components/OutboundTranslationSlice_Builder.res +20 -0
  22. package/src/components/OutboundTranslationSlice_Builder.res.mjs +12 -2
  23. package/src/plugin/runtime/PluginRuntime_Builder.res +26 -0
  24. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +26 -0
  25. package/tests/AutomationSliceEntryPoint_OpsTest.res +44 -3
  26. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +65 -1
@@ -38,12 +38,44 @@ type sliceInfo = {
38
38
  // slice that declares no sources (the default) and for any that names the log
39
39
  // explicitly, which is why it is not derivable from `sourceTopics` being empty.
40
40
  consumesDcbLog: bool,
41
+ // Which component's CommandTopic this slice publishes its inbound command to.
42
+ // `None` keeps the plugin's DCB queue — right for a slice targeting a DCB
43
+ // StateChangeSlice, and what the shared Lambda used unconditionally before an
44
+ // outbound slice could target an Aggregate, whose commands a different Lambda
45
+ // consumes.
46
+ //
47
+ // Resolved in the finalizer rather than here: an Aggregate's CommandTopic is
48
+ // created *after* the slices that target it, so a lookup at registration time
49
+ // finds nothing. Only the name is knowable this early.
50
+ commandTargetName: option<string>,
41
51
  }
42
52
 
43
53
  let bundledInfos: dict<sliceInfo> = Dict.make()
44
54
 
45
55
  let dcbQueueUrlRef: ref<option<Pulumi.Output.t<string>>> = ref(None)
46
- let setDcbQueueUrl = url => dcbQueueUrlRef := Some(url)
56
+ // The DCB command topic's own flavor, for the slices that publish to it. Set
57
+ // beside the URL rather than assumed: the runtime publisher used to hardcode
58
+ // FIFO, and these queues are standard, which SQS rejects.
59
+ let dcbQueueIsFifo = ref(false)
60
+ // The DCB command topic queues as deploy-time resources, so the finalizer can
61
+ // put them on the channel spec and `connectLambda`'s `sqs:SendMessage` grant
62
+ // covers the slices that publish to the DCB fallback. Accumulated rather than
63
+ // kept singular: the hook fires once per DCB command topic (sync, and async
64
+ // when async StateChangeSlices exist), and granting on both is correct.
65
+ let dcbQueueResources: array<ReventlessInfra.Adapter.resource> = []
66
+ // First call wins for the URL and flavor. The hook fires once per DCB command
67
+ // topic and a plugin with async StateChangeSlices fires it twice; the sync topic
68
+ // is created first and is the one a slice's command belongs on, so letting the
69
+ // async FIFO topic overwrite would send slice commands to a FIFO queue with no
70
+ // MessageGroupId — which SQS rejects. The resources keep accumulating: the grant
71
+ // is wanted on every DCB command topic.
72
+ let setDcbQueueUrl = (~isFifo=false, ~resource=?, url) => {
73
+ if dcbQueueUrlRef.contents->Option.isNone {
74
+ dcbQueueUrlRef := Some(url)
75
+ dcbQueueIsFifo := isFifo
76
+ }
77
+ resource->Option.forEach(r => dcbQueueResources->Array.push(r)->ignore)
78
+ }
47
79
  // The admin/plugin DCB command-topic FIFO URL captured by the
48
80
  // onDcbCommandTopicCreated hook. Read by the admin EventCollector's reactive
49
81
  // ApiFragmentRegistry push (2e) to dispatch RecordApiFragmentPush.
@@ -62,6 +94,7 @@ let registerAutomationSlice = (
62
94
  ~context=?,
63
95
  ~sourceTopics=Dict.make(),
64
96
  ~consumesDcbLog=true,
97
+ ~commandTargetName=?,
65
98
  ) =>
66
99
  bundledInfos->Dict.set(
67
100
  name,
@@ -74,6 +107,7 @@ let registerAutomationSlice = (
74
107
  context,
75
108
  sourceTopics,
76
109
  consumesDcbLog,
110
+ commandTargetName,
77
111
  },
78
112
  )
79
113
 
@@ -147,6 +181,121 @@ let forEventCollector: ReventlessCore.Runtime.forEventCollector<
147
181
  }
148
182
  }
149
183
 
184
+ // How often the shared automation Lambda is invoked with no records, to work
185
+ // its slices' TODO backlogs.
186
+ //
187
+ // Without it a backlog only moves when the *next event* arrives, so a slice
188
+ // whose traffic stops holds its failed items indefinitely — and D4's "retried to
189
+ // maxRetries, then swept" is what an outbound slice is chosen for over a
190
+ // hand-rolled Task. Five minutes is short enough that a transient outage clears
191
+ // on its own without anyone watching, and long enough that the invocations are
192
+ // noise against the event traffic (~288/day per deployment, most of them finding
193
+ // an empty backlog and exiting).
194
+ let sweepIntervalMinutes = 5
195
+
196
+ // The scheduled trigger: a rule, permission for EventBridge to invoke, and a
197
+ // target carrying the constant payload the entry point's shell branches on. The
198
+ // payload is the whole event — a scheduled invocation has no `records`, which is
199
+ // exactly how the shell tells the two apart.
200
+ let makeSweepSchedule = (~runtime: ReventlessCore.Runtime.environment<runtimeParts>, ~opts) => {
201
+ let name = "AllAutomationSlicesSweep"
202
+ let customOpts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
203
+
204
+ let rule = {
205
+ open PulumiAws.Cloudwatch
206
+ EventRule.make(
207
+ ~name=Pulumi.Pulumi.getStackName() ++ ("-" ++ name),
208
+ ~args={
209
+ description: "Work the automation/outbound slices' TODO backlogs"->Pulumi.Input.make,
210
+ scheduleExpression: EventRule.ScheduleExpression.every(sweepIntervalMinutes->Minutes),
211
+ tags: AWS.Tags.make(
212
+ ~name=Pulumi.Pulumi.getStackName() ++ ("-" ++ name),
213
+ ~kind=ReventlessCore.ComponentType.AutomationSlice,
214
+ ~role=Scheduler,
215
+ ~component=name,
216
+ ),
217
+ },
218
+ ~opts=customOpts,
219
+ )
220
+ }
221
+
222
+ // The permission and target need the Lambda's resolved arn/name, so they stay
223
+ // inside an apply — the same shape the plugin heartbeat's runner uses.
224
+ let _permissionAndTarget =
225
+ (
226
+ runtime.parts.lambda->Pulumi.Output.flatMap(l => l.arn),
227
+ runtime.parts.lambda->Pulumi.Output.flatMap(l => l.name),
228
+ )
229
+ ->Pulumi.Output.all2
230
+ ->Pulumi.Output.apply(((lambdaArn, lambdaName)) => {
231
+ let _permission = PulumiAws.Lambda.Permission.make(
232
+ ~name,
233
+ ~args={
234
+ action: "lambda:InvokeFunction",
235
+ function: lambdaName->Pulumi.Input.make,
236
+ principal: AWS.CloudwatchEventRule.principal,
237
+ },
238
+ ~opts=customOpts,
239
+ )
240
+ let _target = {
241
+ open PulumiAws.Cloudwatch
242
+ EventTarget.make(
243
+ ~name,
244
+ ~args={
245
+ rule: EventTarget.Rule.ofEventRule(rule),
246
+ arn: lambdaArn->Pulumi.Input.make,
247
+ input: `{"reventlessSweep":true}`->Pulumi.Input.make,
248
+ },
249
+ ~opts=customOpts,
250
+ )
251
+ }
252
+ })
253
+ }
254
+
255
+ // Least-privilege `geo:SearchPlaceIndexForText` on the platform's place index,
256
+ // for the slices that call the geocoder through the injected capability.
257
+ //
258
+ // Only attached when the platform actually provisioned an index — that bit is a
259
+ // plain bool and therefore known synchronously, while the index *name* is an
260
+ // Output. Without the split this would either grant on a nonsense ARN or need
261
+ // `option<Pulumi.Output.t<_>>`, which corrupts the Output proxy.
262
+ //
263
+ // The RolePolicy is created at top level with an Output-valued document, not
264
+ // inside an `apply`. A resource created in an apply callback does not reliably
265
+ // register with the engine — the same defect that intermittently left the
266
+ // heartbeat Lambda without its SQS grant.
267
+ let makeGeocoderGrant = (~runtime: ReventlessCore.Runtime.environment<runtimeParts>, ~opts) =>
268
+ if PluginRuntime_Builder.geocoderProvisioned() {
269
+ let customOpts =
270
+ opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
271
+ let policyJson =
272
+ PluginRuntime_Builder.geocoderPlaceIndex()->Pulumi.Output.apply(idx => {
273
+ open PulumiAws.PolicyDocument
274
+ PulumiAws.PolicyDocument.make(
275
+ ~id="AllAutomationSlicesGeocode",
276
+ ~statements=[
277
+ {
278
+ sid: "AllowGeocode",
279
+ effect: Allow,
280
+ actions: Action("geo:SearchPlaceIndexForText"),
281
+ // Account/region wildcarded, matching the Function URL handler's
282
+ // own policy: the index name is what identifies it, and the stack
283
+ // has no other account to reach.
284
+ resources: Resource(`arn:aws:geo:*:*:place-index/${idx}`),
285
+ },
286
+ ],
287
+ )->PulumiAws.PolicyDocument.toJsonString
288
+ })
289
+ let _ = PulumiAws.IAM.RolePolicy.make(
290
+ ~name="AllAutomationSlicesGeocode",
291
+ ~args={
292
+ policy: policyJson->Pulumi.Output.asInput,
293
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
294
+ },
295
+ ~opts=customOpts,
296
+ )
297
+ }
298
+
150
299
  let finished = ref(false)
151
300
 
152
301
  // Legacy finalizer, kept only to satisfy `EventCollectorRuntime_Builder.T`.
@@ -358,9 +507,25 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
358
507
  let handlerOutputs: array<Pulumi.Output.t<string>> = []
359
508
  let packageDirs: dict<string> = Dict.make()
360
509
  let allQueryDbResources: array<ReventlessInfra.Adapter.resource> = []
510
+ // Whether any slice publishes to the DCB fallback rather than a target
511
+ // aggregate's own topic — if so, that queue needs the SendMessage grant
512
+ // too, once, below the loop.
513
+ let anyDcbFallback = ref(false)
361
514
 
362
515
  bundledInfos->Dict.forEachWithKey((info, _name) => {
363
516
  info.queryDbResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
517
+ // The queue this slice publishes its command to. An Aggregate registers
518
+ // its CommandTopic while plugin construction is still synchronous —
519
+ // before DCB construction registers the slices targeting it — so a
520
+ // named target resolves here; anything else keeps the DCB fallback.
521
+ let target = info.commandTargetName->Option.flatMap(CommandTopicRegistry.get)
522
+ switch target {
523
+ | Some({resource}) =>
524
+ // The target CommandTopic joins the channel's resources so
525
+ // `connectLambda`'s existing `sqs:SendMessage` grant covers it.
526
+ allQueryDbResources->Array.push(resource)->ignore
527
+ | None => anyDcbFallback := true
528
+ }
364
529
  let pkg = Util_Bundle.extractPackageName(info.specModulePath)
365
530
  packageDirs->Dict.set(pkg, Util_Bundle.resolvePackageRoot(pkg))
366
531
  // Usually the same package as the spec, but bundle it explicitly —
@@ -380,8 +545,16 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
380
545
  | None => ""
381
546
  }
382
547
 
548
+ // Per slice, not plugin-wide: an outbound slice targeting an Aggregate
549
+ // publishes to that aggregate's CommandTopic. Everything else keeps the
550
+ // DCB topic, which is what the field has always meant — and with it the
551
+ // DCB queue's own flavor.
552
+ let (commandQueueUrl, commandQueueIsFifo) = switch target {
553
+ | Some({queueUrl, isFifo}) => (queueUrl, isFifo)
554
+ | None => (dcbQueueUrl, dcbQueueIsFifo.contents)
555
+ }
383
556
  let handlerJson =
384
- Pulumi.Output.all3((info.queryDbTableName, dcbQueueUrl, sourceUrnsFor(info)))
557
+ Pulumi.Output.all3((info.queryDbTableName, commandQueueUrl, sourceUrnsFor(info)))
385
558
  ->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
386
559
  let urnsJson =
387
560
  urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify
@@ -389,11 +562,20 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
389
562
  // an entry point that predates multi-source still routes its first
390
563
  // stream rather than none.
391
564
  let firstUrn = urns->Array.get(0)->Option.getOr("")
392
- `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
565
+ let fifoJson = commandQueueIsFifo ? "true" : "false"
566
+ `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","commandQueueIsFifo":${fifoJson},"sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
393
567
  })
394
568
  let _ = handlerOutputs->Array.push(handlerJson)
395
569
  })
396
570
 
571
+ // The fallback publishers' grant. Every slice that resolved no target
572
+ // publishes to the plugin's DCB command topic, whose queue was captured
573
+ // by `setDcbQueueUrl` — without this the role has no `sqs:SendMessage`
574
+ // there and every such publish is rejected.
575
+ if anyDcbFallback.contents {
576
+ dcbQueueResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
577
+ }
578
+
397
579
  let handlerConfigOutput =
398
580
  Pulumi.Output.all(handlerOutputs)
399
581
  ->Pulumi.Output.apply(handlers => `{"handlers":[${handlers->Array.join(",")}]}`)
@@ -434,6 +616,9 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
434
616
  ~runtime,
435
617
  ~opts,
436
618
  )
619
+
620
+ makeSweepSchedule(~runtime, ~opts)
621
+ makeGeocoderGrant(~runtime, ~opts)
437
622
  | None =>
438
623
  log.warn(
439
624
  ~comp="AutomationSliceRuntime_Builder_Single",
@@ -1,13 +1,23 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Aws from "@pulumi/aws";
3
4
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
5
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
8
  import * as Pulumi from "@pulumi/pulumi";
7
9
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
+ import * as AWS$ReventlessAws from "../AWS.res.mjs";
8
12
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
13
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
9
14
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
15
+ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
10
16
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
17
+ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
18
+ import * as Cloudwatch_EventRule$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventRule.res.mjs";
19
+ import * as Cloudwatch_EventTarget$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventTarget.res.mjs";
20
+ import * as CommandTopicRegistry$ReventlessAws from "../CommandTopic/CommandTopicRegistry.res.mjs";
11
21
  import * as PluginRuntime_Builder$ReventlessAws from "../../plugin/runtime/PluginRuntime_Builder.res.mjs";
12
22
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
13
23
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
@@ -20,15 +30,28 @@ let dcbQueueUrlRef = {
20
30
  contents: undefined
21
31
  };
22
32
 
23
- function setDcbQueueUrl(url) {
24
- dcbQueueUrlRef.contents = url;
33
+ let dcbQueueIsFifo = {
34
+ contents: false
35
+ };
36
+
37
+ let dcbQueueResources = [];
38
+
39
+ function setDcbQueueUrl(isFifoOpt, resource, url) {
40
+ let isFifo = isFifoOpt !== undefined ? isFifoOpt : false;
41
+ if (Stdlib_Option.isNone(dcbQueueUrlRef.contents)) {
42
+ dcbQueueUrlRef.contents = url;
43
+ dcbQueueIsFifo.contents = isFifo;
44
+ }
45
+ Stdlib_Option.forEach(resource, r => {
46
+ dcbQueueResources.push(r);
47
+ });
25
48
  }
26
49
 
27
50
  function getDcbQueueUrl() {
28
51
  return dcbQueueUrlRef.contents;
29
52
  }
30
53
 
31
- function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt) {
54
+ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName) {
32
55
  let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
33
56
  let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
34
57
  let sourceTopics = sourceTopicsOpt !== undefined ? sourceTopicsOpt : ({});
@@ -41,7 +64,8 @@ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackT
41
64
  queryDbResources: queryDbResources,
42
65
  context: context,
43
66
  sourceTopics: sourceTopics,
44
- consumesDcbLog: consumesDcbLog
67
+ consumesDcbLog: consumesDcbLog,
68
+ commandTargetName: commandTargetName
45
69
  };
46
70
  }
47
71
 
@@ -81,6 +105,51 @@ function forEventCollector(param, eventTopics, resources, memorySizeOpt, timeout
81
105
  log.info("AutomationSliceRuntime_Builder_Single", undefined, `registered ` + eventCollectorName + ` for ` + parentName);
82
106
  }
83
107
 
108
+ function makeSweepSchedule(runtime, opts) {
109
+ let name = "AllAutomationSlicesSweep";
110
+ let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
111
+ let rule = new (Aws.cloudwatch.EventRule)(Pulumi.getStack() + ("-" + name), {
112
+ description: "Work the automation/outbound slices' TODO backlogs",
113
+ scheduleExpression: Primitive_option.some(Cloudwatch_EventRule$PulumiAws.ScheduleExpression.every({
114
+ TAG: "Minutes",
115
+ _0: 5
116
+ })),
117
+ tags: AWS_Tags$ReventlessAws.make(Pulumi.getStack() + ("-" + name), "AutomationSlice", "Scheduler", undefined, name, undefined, undefined, undefined)
118
+ }, customOpts);
119
+ Pulumi.all([
120
+ Output$Pulumi.flatMap(runtime.parts.lambda, l => l.arn),
121
+ Output$Pulumi.flatMap(runtime.parts.lambda, l => l.name)
122
+ ]).apply(param => {
123
+ new (Aws.lambda.Permission)(name, {
124
+ action: "lambda:InvokeFunction",
125
+ function: param[1],
126
+ principal: AWS$ReventlessAws.CloudwatchEventRule.principal
127
+ }, customOpts);
128
+ new (Aws.cloudwatch.EventTarget)(name, {
129
+ rule: Cloudwatch_EventTarget$PulumiAws.Rule.ofEventRule(rule),
130
+ arn: param[0],
131
+ input: `{"reventlessSweep":true}`
132
+ }, customOpts);
133
+ });
134
+ }
135
+
136
+ function makeGeocoderGrant(runtime, opts) {
137
+ if (!PluginRuntime_Builder$ReventlessAws.geocoderProvisioned()) {
138
+ return;
139
+ }
140
+ let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
141
+ let policyJson = PluginRuntime_Builder$ReventlessAws.geocoderPlaceIndex().apply(idx => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllAutomationSlicesGeocode", [{
142
+ Sid: "AllowGeocode",
143
+ Effect: "Allow",
144
+ Action: "geo:SearchPlaceIndexForText",
145
+ Resource: `arn:aws:geo:*:*:place-index/` + idx
146
+ }])));
147
+ new (Aws.iam.RolePolicy)("AllAutomationSlicesGeocode", {
148
+ policy: policyJson,
149
+ role: runtime.parts.lambdaRole.id
150
+ }, customOpts);
151
+ }
152
+
84
153
  let finished = {
85
154
  contents: false
86
155
  };
@@ -195,10 +264,19 @@ function finishWithDcbEventLog(dcbEventLog) {
195
264
  let handlerOutputs = [];
196
265
  let packageDirs = {};
197
266
  let allQueryDbResources = [];
267
+ let anyDcbFallback = {
268
+ contents: false
269
+ };
198
270
  Stdlib_Dict.forEachWithKey(bundledInfos, (info, _name) => {
199
271
  info.queryDbResources.forEach(r => {
200
272
  allQueryDbResources.push(r);
201
273
  });
274
+ let target = Stdlib_Option.flatMap(info.commandTargetName, CommandTopicRegistry$ReventlessAws.get);
275
+ if (target !== undefined) {
276
+ allQueryDbResources.push(target.resource);
277
+ } else {
278
+ anyDcbFallback.contents = true;
279
+ }
202
280
  let pkg = Util_Bundle$ReventlessAws.extractPackageName(info.specModulePath);
203
281
  packageDirs[pkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, pkg);
204
282
  let bodyPkg = Util_Bundle$ReventlessAws.extractPackageName(info.bodyModulePath);
@@ -208,18 +286,32 @@ function finishWithDcbEventLog(dcbEventLog) {
208
286
  let callbackType = Stdlib_Option.getOr(JSON.stringify(info.callbackType), `""`);
209
287
  let context = info.context;
210
288
  let contextFragment = context !== undefined ? Stdlib_Option.getOr(Stdlib_Option.map(JSON.stringify(context), json => `,"context":` + json), "") : "";
289
+ let match = target !== undefined ? [
290
+ target.queueUrl,
291
+ target.isFifo
292
+ ] : [
293
+ dcbQueueUrl,
294
+ dcbQueueIsFifo.contents
295
+ ];
296
+ let commandQueueIsFifo = match[1];
211
297
  let handlerJson = Pulumi.all([
212
298
  info.queryDbTableName,
213
- dcbQueueUrl,
299
+ match[0],
214
300
  sourceUrnsFor(info)
215
301
  ]).apply(param => {
216
302
  let urns = param[2];
217
303
  let urnsJson = JSON.stringify(urns.map(prim => prim));
218
304
  let firstUrn = Stdlib_Option.getOr(urns[0], "");
219
- return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
305
+ let fifoJson = commandQueueIsFifo ? "true" : "false";
306
+ return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","commandQueueIsFifo":` + fifoJson + `,"sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
220
307
  });
221
308
  handlerOutputs.push(handlerJson);
222
309
  });
310
+ if (anyDcbFallback.contents) {
311
+ dcbQueueResources.forEach(r => {
312
+ allQueryDbResources.push(r);
313
+ });
314
+ }
223
315
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
224
316
  let envVars = {};
225
317
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
@@ -233,6 +325,8 @@ function finishWithDcbEventLog(dcbEventLog) {
233
325
  eventTopics: eventTopics,
234
326
  resources: allQueryDbResources
235
327
  }], runtime, opts);
328
+ makeSweepSchedule(runtime, opts);
329
+ makeGeocoderGrant(runtime, opts);
236
330
  } else {
237
331
  log.warn("AutomationSliceRuntime_Builder_Single", undefined, "finishWithDcbEventLog: DCB EventLog has no parent");
238
332
  }
@@ -244,18 +338,25 @@ let EventCollectorChannel;
244
338
 
245
339
  let RuntimeEnvironment;
246
340
 
341
+ let sweepIntervalMinutes = 5;
342
+
247
343
  export {
248
344
  EventCollectorChannel,
249
345
  RuntimeEnvironment,
250
346
  log,
251
347
  bundledInfos,
252
348
  dcbQueueUrlRef,
349
+ dcbQueueIsFifo,
350
+ dcbQueueResources,
253
351
  setDcbQueueUrl,
254
352
  getDcbQueueUrl,
255
353
  registerAutomationSlice,
256
354
  storedSpecs,
257
355
  grandParent,
258
356
  forEventCollector,
357
+ sweepIntervalMinutes,
358
+ makeSweepSchedule,
359
+ makeGeocoderGrant,
259
360
  finished,
260
361
  finish,
261
362
  finishWithDcbEventLog,
@@ -46,7 +46,7 @@ function Make(Api) {
46
46
  let queryDbTableName = tableResource.name;
47
47
  let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(sourceNames));
48
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);
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, undefined);
50
50
  return as_;
51
51
  };
52
52
  return {
@@ -14,6 +14,20 @@ module EventCollectorRuntimeBuilder = {
14
14
  let finish = Inner.finish
15
15
  }
16
16
 
17
+ // On AWS the core builder's in-process handler is never the thing that runs a
18
+ // translation: `forEventCollector` registers module paths, and the shared
19
+ // `AllAutomationSlices` Lambda rebuilds the callback from them and drives phase 2
20
+ // itself. So the capabilities that matter are the ones
21
+ // `AutomationSliceEntryPoint_Ops.capabilities` builds at runtime, from the
22
+ // Lambda's environment — where the place index name actually is.
23
+ //
24
+ // This deploy-time value therefore has no caller. `none` rather than a real
25
+ // geocoder because inventing one here would suggest a path that does not exist,
26
+ // and because a deploy-time module has no environment to read anyway.
27
+ module DeployTimeCapabilities = {
28
+ let capabilities = () => Reventless.Capabilities.none
29
+ }
30
+
17
31
  module Make = (Api: {
18
32
  let api: unit => Types.AppSync.api
19
33
  let apiRole: unit => Types.AppSync.role
@@ -25,6 +39,7 @@ module Make = (Api: {
25
39
  EventCollectorChannel,
26
40
  EventCollectorRuntimeBuilder,
27
41
  Api,
42
+ DeployTimeCapabilities,
28
43
  )
29
44
 
30
45
  module Make = (
@@ -65,9 +80,14 @@ module Make = (Api: {
65
80
  ~bodyModulePath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
66
81
  ~callbackType="outbound",
67
82
  ~queryDbTableName,
83
+ // The target queue rides along so `connectLambda`'s existing
84
+ // `sqs:SendMessage` grant covers it. Without it the role can publish
85
+ // nowhere, and the command is lost after the geocoder has been paid for.
68
86
  ~queryDbResources=queryDbOutputs.resources,
69
87
  ~sourceTopics,
70
88
  ~consumesDcbLog,
89
+ // Only the name: the target Aggregate's CommandTopic does not exist yet.
90
+ ~commandTargetName=?Spec.targetName,
71
91
  )
72
92
 
73
93
  ots
@@ -1,6 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
4
+ import * as Capabilities$Reventless from "@reventlessdev/reventless-spec/src/semantic/Capabilities.res.mjs";
4
5
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
5
6
  import * as EventTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventTopic/EventTopic.res.mjs";
6
7
  import * as Util_Bundle$ReventlessAws from "../util/Util_Bundle.res.mjs";
@@ -18,6 +19,14 @@ let EventCollectorRuntimeBuilder = {
18
19
  finish: AutomationSliceRuntime_Builder_Single$ReventlessAws.finish
19
20
  };
20
21
 
22
+ function capabilities() {
23
+ return Capabilities$Reventless.none;
24
+ }
25
+
26
+ let DeployTimeCapabilities = {
27
+ capabilities: capabilities
28
+ };
29
+
21
30
  function Make(Api) {
22
31
  let Inner = OutboundTranslationSlice_Builder$ReventlessCore.Make({
23
32
  make: RuntimeEnvironment_Lambda$ReventlessAws.make,
@@ -42,7 +51,7 @@ function Make(Api) {
42
51
  },
43
52
  forEventCollector: AutomationSliceRuntime_Builder_Single$ReventlessAws.forEventCollector,
44
53
  finish: AutomationSliceRuntime_Builder_Single$ReventlessAws.finish
45
- })(Api);
54
+ })(Api)(DeployTimeCapabilities);
46
55
  let Make$1 = Spec => (Translation => {
47
56
  let InnerMake = Inner.Make(Spec)(Translation);
48
57
  let make = (dcbEventLog, allEventTopicsOpt, publishJsons, runtime, opts) => {
@@ -54,7 +63,7 @@ function Make(Api) {
54
63
  let declaredSources = Spec.sourceNames;
55
64
  let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(declaredSources));
56
65
  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);
66
+ 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, Spec.targetName);
58
67
  return ots;
59
68
  };
60
69
  return {
@@ -80,6 +89,7 @@ export {
80
89
  EventCollectorChannel,
81
90
  RuntimeEnvironment,
82
91
  EventCollectorRuntimeBuilder,
92
+ DeployTimeCapabilities,
83
93
  Make,
84
94
  }
85
95
  /* Component-ReventlessCore Not a pure module */
@@ -51,6 +51,32 @@ let registerCapabilityEnv = (name: string, value: Pulumi.Output.t<string>) =>
51
51
  capability, so this returns `[]` rather than throwing. */
52
52
  let capabilityEnv = () => capabilityEnvRef->Dict.toArray
53
53
 
54
+ /**
55
+ The geocoding place index, when the platform provisioned one.
56
+
57
+ Separate from `capabilityEnv` because an env dict cannot express a *grant*: a
58
+ slice Lambda calling Amazon Location needs `geo:SearchPlaceIndexForText` on this
59
+ index, and its role is created later, in the slice runtime's finalizer. So the
60
+ name travels twice — once as a variable the runtime reads, once as the resource
61
+ its role must be allowed to touch.
62
+
63
+ A plain Output with `""` for "not provisioned", plus a separate bool for "was it
64
+ registered at all", rather than `ref<option<Pulumi.Output.t<_>>>`: that shape
65
+ compiles to `Option.getOr`, whose nested-option probe corrupts the Output proxy.
66
+ The bool is an ordinary value and is known synchronously, which is what the
67
+ finalizer needs in order to decide whether to attach a policy at all.
68
+ */
69
+ let geocoderPlaceIndexRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
70
+ let geocoderProvisionedRef = ref(false)
71
+
72
+ let registerGeocoderPlaceIndex = (indexName: Pulumi.Output.t<string>) => {
73
+ geocoderPlaceIndexRef := indexName
74
+ geocoderProvisionedRef := true
75
+ }
76
+
77
+ let geocoderPlaceIndex = () => geocoderPlaceIndexRef.contents
78
+ let geocoderProvisioned = () => geocoderProvisionedRef.contents
79
+
54
80
  type sliceModulePaths = {
55
81
  specPath: string,
56
82
  behaviorPath: string,
@@ -44,6 +44,27 @@ function capabilityEnv() {
44
44
  return Object.entries(capabilityEnvRef);
45
45
  }
46
46
 
47
+ let geocoderPlaceIndexRef = {
48
+ contents: Pulumi.output("")
49
+ };
50
+
51
+ let geocoderProvisionedRef = {
52
+ contents: false
53
+ };
54
+
55
+ function registerGeocoderPlaceIndex(indexName) {
56
+ geocoderPlaceIndexRef.contents = indexName;
57
+ geocoderProvisionedRef.contents = true;
58
+ }
59
+
60
+ function geocoderPlaceIndex() {
61
+ return geocoderPlaceIndexRef.contents;
62
+ }
63
+
64
+ function geocoderProvisioned() {
65
+ return geocoderProvisionedRef.contents;
66
+ }
67
+
47
68
  let dcbConfigRef = {
48
69
  contents: {
49
70
  pluginName: "",
@@ -506,6 +527,11 @@ export {
506
527
  capabilityEnvRef,
507
528
  registerCapabilityEnv,
508
529
  capabilityEnv,
530
+ geocoderPlaceIndexRef,
531
+ geocoderProvisionedRef,
532
+ registerGeocoderPlaceIndex,
533
+ geocoderPlaceIndex,
534
+ geocoderProvisioned,
509
535
  dcbConfigRef,
510
536
  registeredSliceModulePaths,
511
537
  registerStateChangeSliceSpec,