@reventlessdev/reventless-aws 3.0.0-alpha.381 → 3.0.0-alpha.383

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.
@@ -7,6 +7,7 @@ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
8
  import * as Effect from "effect/Effect";
9
9
  import * as Stream$1 from "effect/Stream";
10
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
10
11
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
12
  import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
12
13
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
@@ -20,11 +21,13 @@ import * as Secrets_Node_Backend$ReventlessCore from "@reventlessdev/reventless-
20
21
  import * as Util_DynamoDb_Runtime$ReventlessAws from "../../util/Util_DynamoDb_Runtime.res.mjs";
21
22
  import * as Messaging_Log_Backend$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Messaging/Messaging_Log_Backend.res.mjs";
22
23
  import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
24
+ import * as AutomationSlice_Schedules$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Schedules.res.mjs";
23
25
  import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
24
26
  import * as Geocoder_AwsLocation_Backend$ReventlessAws from "../Geocoder/Geocoder_AwsLocation_Backend.res.mjs";
25
27
  import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
26
28
  import * as QueryDbStorage_DynamoDb_Runtime$ReventlessAws from "../QueryDb/QueryDbStorage_DynamoDb_Runtime.res.mjs";
27
29
  import * as OutboundTranslationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs";
30
+ import * as ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws from "../ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs";
28
31
 
29
32
  function strOf(obj, key) {
30
33
  return Stdlib_Option.flatMap(obj[key], Stdlib_JSON.Decode.string);
@@ -48,7 +51,8 @@ function decodeEntry(json) {
48
51
  dcbQueueUrl: Stdlib_Option.getOr(strOf(h, "dcbQueueUrl"), ""),
49
52
  commandQueueIsFifo: Stdlib_Option.flatMap(h["commandQueueIsFifo"], Stdlib_JSON.Decode.bool),
50
53
  sourceUrn: Stdlib_Option.getOr(strOf(h, "sourceUrn"), ""),
51
- context: Stdlib_Option.flatMap(h["context"], decodeContext)
54
+ context: Stdlib_Option.flatMap(h["context"], decodeContext),
55
+ schedulesTableName: strOf(h, "schedulesTableName")
52
56
  }));
53
57
  }
54
58
 
@@ -64,7 +68,8 @@ function decodeCompactEntry(modules, sharedQueueUrl, sharedIsFifo, sharedUrns, h
64
68
  dcbQueueUrl: Stdlib_Option.getOr(strOf(h, "k"), sharedQueueUrl),
65
69
  commandQueueIsFifo: f !== undefined ? f : sharedIsFifo,
66
70
  sourceUrn: Stdlib_Option.getOr(urns[0], ""),
67
- context: Stdlib_Option.flatMap(m["context"], decodeContext)
71
+ context: Stdlib_Option.flatMap(m["context"], decodeContext),
72
+ schedulesTableName: strOf(h, "s")
68
73
  };
69
74
  }
70
75
 
@@ -188,6 +193,74 @@ function makeLoadTodoItems(queryDbTableName, todoItems, rowSchema, comp) {
188
193
  };
189
194
  }
190
195
 
196
+ function scheduleTable(tableName) {
197
+ return {
198
+ id: "",
199
+ name: tableName,
200
+ arn: "",
201
+ hashKey: "id"
202
+ };
203
+ }
204
+
205
+ function makeScheduleStore(tableName, rows, comp) {
206
+ let table = scheduleTable(tableName);
207
+ let save = QueryDbStorage_DynamoDb_Runtime$ReventlessAws.save(table);
208
+ let $$delete = QueryDbStorage_DynamoDb_Runtime$ReventlessAws.$$delete(table);
209
+ let stored = {
210
+ contents: []
211
+ };
212
+ let loaded = {
213
+ contents: false
214
+ };
215
+ let load = async () => {
216
+ if (loaded.contents) {
217
+ return;
218
+ }
219
+ loaded.contents = true;
220
+ let params_ConsistentRead = true;
221
+ let params = {
222
+ TableName: tableName,
223
+ ConsistentRead: params_ConsistentRead
224
+ };
225
+ await Effect.runPromise(Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.scanStream(params)), items => {
226
+ items.forEach(item => {
227
+ let json = JSON.parse(Stdlib_Option.getOr(JSON.stringify(item), ""));
228
+ let row;
229
+ try {
230
+ row = Util_Sury$Reventless.fromJson(json, AutomationSlice_Schedules$ReventlessCore.rowSchema);
231
+ } catch (exn) {
232
+ return;
233
+ }
234
+ if (Stdlib_Option.isNone(rows[row.key])) {
235
+ rows[row.key] = row;
236
+ return;
237
+ }
238
+ });
239
+ }), err => EffectLogger$ReventlessCore.logError(comp, undefined, `restore: couldn't read the schedules from ` + tableName + `: ` + DynamoDb_Error$ReventlessAws.message(err))));
240
+ stored.contents = Object.keys(rows);
241
+ };
242
+ let sync = async () => {
243
+ let keys = Object.keys(rows);
244
+ await Promise.all(Object.entries(rows).map(param => save(param[0], Util_Sury$Reventless.toJson(param[1], AutomationSlice_Schedules$ReventlessCore.rowSchema), "Overwrite", undefined)));
245
+ await Promise.all(stored.contents.filter(key => !keys.includes(key)).map(key => $$delete(key, undefined)));
246
+ stored.contents = keys;
247
+ };
248
+ return [
249
+ load,
250
+ sync
251
+ ];
252
+ }
253
+
254
+ function ownArn() {
255
+ let env = key => Stdlib_Option.getOr(process.env[key], "");
256
+ let account = Stdlib_Option.getOr(env("SCHEDULER_ROLE_ARN").split(":")[4], "");
257
+ return `arn:aws:lambda:` + env("AWS_REGION") + `:` + account + `:function:` + env("AWS_LAMBDA_FUNCTION_NAME");
258
+ }
259
+
260
+ function platformSchedulesOf(slice) {
261
+ return ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.platformSchedules(Stdlib_Option.getOr(process.env["SCHEDULER_ROLE_ARN"], ""), ownArn(), slice);
262
+ }
263
+
191
264
  function messagingEmailProvider() {
192
265
  let sender = Stdlib_Option.getOr(process.env["MESSAGING_EMAIL_SENDER"], "");
193
266
  let match = Stdlib_Option.getOr(process.env["MESSAGING_EMAIL_PROVIDER"], "");
@@ -207,11 +280,31 @@ function capabilities() {
207
280
  };
208
281
  }
209
282
 
210
- function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTodoItems, loadTodoItems) {
283
+ async function loadSchedules(step) {
284
+ if (step !== undefined) {
285
+ return await step.load();
286
+ }
287
+ }
288
+
289
+ async function followSchedules(step, callback) {
290
+ if (step === undefined) {
291
+ return;
292
+ }
293
+ let changes = callback.takeScheduleChanges();
294
+ if (changes.length !== 0) {
295
+ await step.sync();
296
+ return await AutomationSlice_Schedules$ReventlessCore.tell(step.platform, changes);
297
+ }
298
+ }
299
+
300
+ function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTodoItems, loadTodoItems, schedulesOpt) {
301
+ let schedules = schedulesOpt !== undefined ? schedulesOpt : () => {};
211
302
  return stream => Effect.flatMap(Stream.runCollect(stream), jsons => Effect.promise(async () => {
212
303
  await loadTodoItems();
304
+ await loadSchedules(schedules());
213
305
  callback.phase1(jsons, context);
214
306
  await syncTodoItems();
307
+ await followSchedules(schedules(), callback);
215
308
  await callback.phase2(publishJsons);
216
309
  return await syncTodoItems();
217
310
  }));
@@ -242,14 +335,39 @@ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJso
242
335
  }));
243
336
  }
244
337
 
245
- function makeAutomationRegisteredHandler(entry, sliceName, callback) {
338
+ function makeAutomationRegisteredHandler(entry, sliceName, callback, schedule) {
246
339
  let comp = `AutomationSlice(` + sliceName + `)`;
247
340
  let publishJsons = makePublishJsons(entry.dcbQueueUrl, Stdlib_Option.getOr(entry.commandQueueIsFifo, false));
248
341
  let syncTodoItems = makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema);
249
342
  let loadTodoItems = makeLoadTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema, comp);
343
+ let context = Stdlib_Option.getOr(entry.context, defaultContext(sliceName));
344
+ let step = {
345
+ contents: undefined
346
+ };
347
+ let schedules = () => {
348
+ let match = step.contents;
349
+ let match$1 = entry.schedulesTableName;
350
+ if (match !== undefined) {
351
+ return match;
352
+ }
353
+ if (match$1 === undefined) {
354
+ return;
355
+ }
356
+ let match$2 = makeScheduleStore(match$1, callback.schedules, comp);
357
+ let s_load = match$2[0];
358
+ let s_sync = match$2[1];
359
+ let s_platform = platformSchedulesOf(sliceName);
360
+ let s = {
361
+ load: s_load,
362
+ sync: s_sync,
363
+ platform: s_platform
364
+ };
365
+ step.contents = s;
366
+ return s;
367
+ };
250
368
  return {
251
369
  registered: {
252
- handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(Stdlib_Option.getOr(entry.context, defaultContext(sliceName)), callback, publishJsons, syncTodoItems, loadTodoItems)),
370
+ handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTodoItems, loadTodoItems, schedules)),
253
371
  comp: comp
254
372
  },
255
373
  sweep: async () => {
@@ -257,7 +375,33 @@ function makeAutomationRegisteredHandler(entry, sliceName, callback) {
257
375
  await callback.phase2(publishJsons);
258
376
  return await syncTodoItems();
259
377
  },
260
- comp: comp
378
+ comp: comp,
379
+ name: sliceName,
380
+ tick: async (key, at) => {
381
+ await loadTodoItems();
382
+ await loadSchedules(schedules());
383
+ callback.phase1([AutomationSlice_Callback$ReventlessCore.tickEvent(key, at)], context);
384
+ await syncTodoItems();
385
+ let match = schedules();
386
+ if (match !== undefined) {
387
+ await match.sync();
388
+ }
389
+ await callback.phase2(publishJsons);
390
+ return await syncTodoItems();
391
+ },
392
+ reconcile: async () => {
393
+ let match = schedules();
394
+ if (match !== undefined) {
395
+ await match.load();
396
+ await AutomationSlice_Schedules$ReventlessCore.reconcile(callback.schedules, match.platform, schedule, Date.now());
397
+ return await match.sync();
398
+ }
399
+ let platform = platformSchedulesOf(sliceName);
400
+ let names = await platform.names();
401
+ for (let i = 0, i_finish = names.length; i < i_finish; ++i) {
402
+ await platform.removeName(names[i]);
403
+ }
404
+ }
261
405
  };
262
406
  }
263
407
 
@@ -276,7 +420,10 @@ function makeOutboundRegisteredHandler(entry, sliceName, consumedEventSchema, ca
276
420
  await callback.phase2(publishJsons, capabilities());
277
421
  return await syncTodoItems();
278
422
  },
279
- comp: comp
423
+ comp: comp,
424
+ name: sliceName,
425
+ tick: undefined,
426
+ reconcile: undefined
280
427
  };
281
428
  }
282
429
 
@@ -293,6 +440,29 @@ async function runSweeps(slices) {
293
440
  }
294
441
  }
295
442
 
443
+ async function runTick(slices, invocation) {
444
+ let tick = Stdlib_Option.flatMap(slices.find(s => s.name === invocation.slice), s => s.tick);
445
+ if (tick === undefined) {
446
+ return Stdlib_JsError.throwWithMessage(`tick for ` + invocation.slice + `: no automation of that name here`);
447
+ }
448
+ let at = new Date(invocation.at).getTime();
449
+ if (Number.isNaN(at)) {
450
+ Stdlib_JsError.throwWithMessage(`tick for ` + invocation.slice + `: "` + invocation.at + `" is not a time`);
451
+ }
452
+ return await tick(invocation.key, at);
453
+ }
454
+
455
+ async function reconcileSchedules(slices) {
456
+ if (Stdlib_Option.getOr(process.env["SCHEDULER_ROLE_ARN"], "") === "") {
457
+ Stdlib_JsError.throwWithMessage(`reconcile: SCHEDULER_ROLE_ARN is not set`);
458
+ }
459
+ let reconciles = Stdlib_Array.filterMap(slices, s => s.reconcile);
460
+ for (let i = 0, i_finish = reconciles.length; i < i_finish; ++i) {
461
+ await reconciles[i]();
462
+ }
463
+ return Effect.runSync(EffectLogger$ReventlessCore.logInfo("AutomationSliceRuntime", undefined, `reconcile: the schedules of ` + reconciles.length.toString() + ` automation(s) are in line`));
464
+ }
465
+
296
466
  export {
297
467
  strOf,
298
468
  decodeContext,
@@ -305,12 +475,20 @@ export {
305
475
  makePublishJsons,
306
476
  makeSyncTodoItems,
307
477
  makeLoadTodoItems,
478
+ scheduleTable,
479
+ makeScheduleStore,
480
+ ownArn,
481
+ platformSchedulesOf,
308
482
  messagingEmailProvider,
309
483
  capabilities,
484
+ loadSchedules,
485
+ followSchedules,
310
486
  makeAutomationJsonEventsHandler,
311
487
  makeOutboundJsonEventsHandler,
312
488
  makeAutomationRegisteredHandler,
313
489
  makeOutboundRegisteredHandler,
314
490
  runSweeps,
491
+ runTick,
492
+ reconcileSchedules,
315
493
  }
316
494
  /* Stream Not a pure module */
@@ -48,6 +48,12 @@ type sliceInfo = {
48
48
  // created *after* the slices that target it, so a lookup at registration time
49
49
  // finds nothing. Only the name is knowable this early.
50
50
  commandTargetName: option<string>,
51
+ // The automation's declared time; what the deploy's reconcile puts on the
52
+ // platform, and what re-runs it when it changes.
53
+ schedule: option<Reventless.Schedule.time>,
54
+ // The schedules store's table, empty for a slice that maps no `Clock`. Granted to
55
+ // the Lambda with the to-do tables, and named in the slice's handler config.
56
+ schedulesResources: array<ReventlessInfra.Adapter.resource>,
51
57
  }
52
58
 
53
59
  let bundledInfos: dict<sliceInfo> = Dict.make()
@@ -95,6 +101,8 @@ let registerAutomationSlice = (
95
101
  ~sourceTopics=Dict.make(),
96
102
  ~consumesDcbLog=true,
97
103
  ~commandTargetName=?,
104
+ ~schedule=?,
105
+ ~schedulesResources=[],
98
106
  ) =>
99
107
  bundledInfos->Dict.set(
100
108
  name,
@@ -108,6 +116,8 @@ let registerAutomationSlice = (
108
116
  sourceTopics,
109
117
  consumesDcbLog,
110
118
  commandTargetName,
119
+ schedule,
120
+ schedulesResources,
111
121
  },
112
122
  )
113
123
 
@@ -319,6 +329,128 @@ let makeMessagingGrant = (~runtime: ReventlessCore.Runtime.environment<runtimePa
319
329
  )
320
330
  }
321
331
 
332
+ // The automations' schedules. The Lambda may manage the schedules in the stack's
333
+ // scheduler group and pass the scheduler role to them; the scheduler role may invoke
334
+ // the Lambda; and each deploy invokes the Lambda to reconcile the declared schedules
335
+ // (`<slice>.default`), so a schedule the platform refuses fails the deploy. The
336
+ // declared times and the code's hash are the invocation's input, so a changed time
337
+ // or a new build re-runs it.
338
+ let makeAutomationSchedules = (
339
+ ~runtime: ReventlessCore.Runtime.environment<runtimeParts>,
340
+ ~schedulerRoleArn: Pulumi.Output.t<string>,
341
+ ~sourceCodeHash: string,
342
+ ~opts,
343
+ ) => {
344
+ let name = "AllAutomationSlices"
345
+ let customOpts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
346
+ let lambdaArn = runtime.parts.lambda->Pulumi.Output.flatMap(l => l.arn)
347
+
348
+ let manage = {
349
+ open PulumiAws.PolicyDocument
350
+ PulumiAws.IAM.RolePolicy.make(
351
+ ~name=`${name}SchedulerPolicy`,
352
+ ~args={
353
+ policy: schedulerRoleArn
354
+ ->Pulumi.Output.apply(arn =>
355
+ PulumiAws.PolicyDocument.make(
356
+ ~id=`${name}SchedulerPolicy`,
357
+ ~statements=[
358
+ {
359
+ sid: "AllowManageSchedules",
360
+ effect: Allow,
361
+ actions: Actions(ScheduledPublisher_EventBridgeScheduler_Runtime.manageActions),
362
+ resources: Resource(
363
+ arn->ScheduledPublisher_EventBridgeScheduler_Runtime.schedulesOfRoleArn,
364
+ ),
365
+ },
366
+ {
367
+ sid: "AllowPassSchedulerRole",
368
+ effect: Allow,
369
+ actions: Action("iam:PassRole"),
370
+ resources: Resource(arn),
371
+ },
372
+ {
373
+ sid: "AllowListSchedules",
374
+ effect: Allow,
375
+ actions: Actions(ScheduledPublisher_EventBridgeScheduler_Runtime.listActions),
376
+ resources: AllResources,
377
+ },
378
+ ],
379
+ )->PulumiAws.PolicyDocument.toJsonString
380
+ )
381
+ ->Pulumi.Output.asInput,
382
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
383
+ },
384
+ ~opts=customOpts,
385
+ )
386
+ }
387
+
388
+ // The group is named after the scheduler role, so its last ARN segment is the role's name.
389
+ let invoke = {
390
+ open PulumiAws.PolicyDocument
391
+ PulumiAws.IAM.RolePolicy.make(
392
+ ~name=`${name}SchedulerInvokePolicy`,
393
+ ~args={
394
+ policy: lambdaArn
395
+ ->Pulumi.Output.apply(arn =>
396
+ PulumiAws.PolicyDocument.make(
397
+ ~id=`${name}SchedulerInvokePolicy`,
398
+ ~statements=[
399
+ {
400
+ sid: "AllowSchedulerToInvokeAutomations",
401
+ effect: Allow,
402
+ actions: Action("lambda:InvokeFunction"),
403
+ resources: Resource(arn),
404
+ },
405
+ ],
406
+ )->PulumiAws.PolicyDocument.toJsonString
407
+ )
408
+ ->Pulumi.Output.asInput,
409
+ role: schedulerRoleArn
410
+ ->Pulumi.Output.apply(ScheduledPublisher_EventBridgeScheduler_Runtime.groupOfRoleArn)
411
+ ->Pulumi.Output.asInput,
412
+ },
413
+ ~opts=customOpts,
414
+ )
415
+ }
416
+
417
+ let declared =
418
+ bundledInfos
419
+ ->Dict.toArray
420
+ ->Array.filterMap(((slice, info)) =>
421
+ info.schedule->Option.map(time => (
422
+ slice,
423
+ time->Reventless.Util_Sury.toJson(Reventless.Schedule.timeSchema),
424
+ ))
425
+ )
426
+ ->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
427
+ ->Dict.fromArray
428
+ ->JSON.Encode.object
429
+ let input =
430
+ Dict.fromArray([
431
+ ("reventlessReconcile", JSON.Encode.bool(true)),
432
+ ("declared", declared),
433
+ ("code", sourceCodeHash->JSON.Encode.string),
434
+ ])
435
+ ->JSON.Encode.object
436
+ ->JSON.stringify
437
+
438
+ let _reconcile = PulumiAws.Lambda.Invocation.make(
439
+ ~name=`${name}ReconcileSchedules`,
440
+ ~args={
441
+ functionName: runtime.parts.lambda->Pulumi.Output.flatMap(l => l.name)->Pulumi.Output.asInput,
442
+ input: input->Pulumi.Input.make,
443
+ },
444
+ ~opts={
445
+ ...customOpts,
446
+ dependsOn: [
447
+ manage->Pulumi.Resource.makeFromJs,
448
+ invoke->Pulumi.Resource.makeFromJs,
449
+ ]->Pulumi.Input.make,
450
+ },
451
+ )
452
+ }
453
+
322
454
  let finished = ref(false)
323
455
 
324
456
  // Legacy finalizer, kept only to satisfy `EventCollectorRuntime_Builder.T`.
@@ -445,7 +577,10 @@ let finish = () =>
445
577
  // stream — the sole source for automation/outbound slices, which dispatch by
446
578
  // `meta.service` inside the callback. `dcbEventLog` is passed so the parent and
447
579
  // the stream URN are available synchronously here.
448
- let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component) =>
580
+ let finishWithDcbEventLog = (
581
+ ~schedulerRoleArn: Pulumi.Output.t<string>,
582
+ dcbEventLog: ReventlessCore.DcbEventLog.component,
583
+ ) =>
449
584
  if !finished.contents {
450
585
  if bundledInfos->Dict.keysToArray->Array.length > 0 {
451
586
  let dcbResource = dcbEventLog->ReventlessCore.Component.toPulumiResource
@@ -538,6 +673,7 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
538
673
 
539
674
  bundledInfos->Dict.forEachWithKey((info, name) => {
540
675
  info.queryDbResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
676
+ info.schedulesResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
541
677
  // The queue this slice publishes its command to. An Aggregate registers
542
678
  // its CommandTopic while plugin construction is still synchronous —
543
679
  // before DCB construction registers the slices targeting it — so a
@@ -589,11 +725,16 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
589
725
  // exactly — so it emits neither and inherits both.
590
726
  let hasOwnUrns =
591
727
  !info.consumesDcbLog || info.sourceTopics->Dict.keysToArray->Array.length > 0
592
- let handlerJson = Pulumi.Output.all3((
728
+ let schedulesTableName = switch info.schedulesResources->Array.get(0) {
729
+ | Some(resource) => resource.name
730
+ | None => Pulumi.Output.make("")
731
+ }
732
+ let handlerJson = Pulumi.Output.all4((
593
733
  info.queryDbTableName,
594
734
  commandQueueUrl,
595
735
  sourceUrnsFor(info),
596
- ))->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
736
+ schedulesTableName,
737
+ ))->Pulumi.Output.apply(((tableName, queueUrl, urns, schedulesTable)) => {
597
738
  let urnsFragment = hasOwnUrns
598
739
  ? `,"u":${urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify}`
599
740
  : ""
@@ -601,7 +742,8 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
601
742
  | Some(_) => `,"k":"${queueUrl}","f":${commandQueueIsFifo ? "true" : "false"}`
602
743
  | None => ""
603
744
  }
604
- `{"n":${nameJson},"q":"${tableName}"${urnsFragment}${queueFragment}}`
745
+ let schedulesFragment = schedulesTable == "" ? "" : `,"s":"${schedulesTable}"`
746
+ `{"n":${nameJson},"q":"${tableName}"${urnsFragment}${queueFragment}${schedulesFragment}}`
605
747
  })
606
748
  let _ = handlerOutputs->Array.push(handlerJson)
607
749
  })
@@ -645,6 +787,9 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
645
787
  PluginRuntime_Builder.capabilityEnv()->Array.forEach(((name, value)) =>
646
788
  envVars->Dict.set(name, value->Pulumi.Output.asInput)
647
789
  )
790
+ // The stack's scheduler role, which also names its group: where the
791
+ // automations' schedules go.
792
+ envVars->Dict.set("SCHEDULER_ROLE_ARN", schedulerRoleArn->Pulumi.Output.asInput)
648
793
 
649
794
  // Carries no Pulumi Output, so the archive is still built synchronously —
650
795
  // the table names and urns that do are what stays in HANDLER_CONFIG.
@@ -680,6 +825,7 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
680
825
  )
681
826
 
682
827
  makeSweepSchedule(~runtime, ~opts)
828
+ makeAutomationSchedules(~runtime, ~schedulerRoleArn, ~sourceCodeHash, ~opts)
683
829
  makeGeocoderGrant(~runtime, ~opts)
684
830
  makeMessagingGrant(~runtime, ~opts)
685
831
  | None =>