@reventlessdev/reventless-aws 3.0.0-alpha.382 → 3.0.0-alpha.384
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 +27 -0
- package/package.json +10 -10
- package/src/Platform.res +9 -2
- package/src/Platform.res.mjs +16 -16
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +13 -9
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +227 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +185 -7
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +150 -4
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +92 -6
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res +104 -15
- package/src/adapter/ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs +122 -34
- package/src/components/Api/AppSync_Adapter.res +6 -9
- package/src/components/Api/AppSync_Adapter.res.mjs +9 -6
- package/src/components/AutomationSlice_Builder.res +24 -0
- package/src/components/AutomationSlice_Builder.res.mjs +20 -2
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
- package/tests/AppSync_AdapterTest.res +52 -0
- package/tests/AppSync_AdapterTest.res.mjs +35 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res +91 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +171 -35
- package/tests/ScheduledPublisher_EventBridgeSchedulerTest.res +25 -0
- package/tests/ScheduledPublisher_EventBridgeSchedulerTest.res.mjs +27 -0
|
@@ -8,7 +8,10 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
8
8
|
import * as Pulumi from "@pulumi/pulumi";
|
|
9
9
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
10
10
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
11
12
|
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
13
|
+
import * as Schedule$Reventless from "@reventlessdev/reventless-spec/src/types/Schedule.res.mjs";
|
|
14
|
+
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
12
15
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
13
16
|
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
14
17
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
@@ -22,6 +25,7 @@ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBu
|
|
|
22
25
|
import * as PluginRuntime_Builder$ReventlessAws from "../../plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
23
26
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
|
|
24
27
|
import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
|
|
28
|
+
import * as ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws from "../ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs";
|
|
25
29
|
|
|
26
30
|
let log = Logger$ReventlessCore.fromEnv();
|
|
27
31
|
|
|
@@ -52,11 +56,12 @@ function getDcbQueueUrl() {
|
|
|
52
56
|
return dcbQueueUrlRef.contents;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
|
-
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName) {
|
|
59
|
+
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName, schedule, schedulesResourcesOpt) {
|
|
56
60
|
let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
|
|
57
61
|
let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
|
|
58
62
|
let sourceTopics = sourceTopicsOpt !== undefined ? sourceTopicsOpt : ({});
|
|
59
63
|
let consumesDcbLog = consumesDcbLogOpt !== undefined ? consumesDcbLogOpt : true;
|
|
64
|
+
let schedulesResources = schedulesResourcesOpt !== undefined ? schedulesResourcesOpt : [];
|
|
60
65
|
bundledInfos[name] = {
|
|
61
66
|
specModulePath: specModulePath,
|
|
62
67
|
bodyModulePath: bodyModulePath,
|
|
@@ -66,7 +71,9 @@ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackT
|
|
|
66
71
|
context: context,
|
|
67
72
|
sourceTopics: sourceTopics,
|
|
68
73
|
consumesDcbLog: consumesDcbLog,
|
|
69
|
-
commandTargetName: commandTargetName
|
|
74
|
+
commandTargetName: commandTargetName,
|
|
75
|
+
schedule: schedule,
|
|
76
|
+
schedulesResources: schedulesResources
|
|
70
77
|
};
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -168,6 +175,73 @@ function makeMessagingGrant(runtime, opts) {
|
|
|
168
175
|
}, customOpts);
|
|
169
176
|
}
|
|
170
177
|
|
|
178
|
+
function makeAutomationSchedules(runtime, schedulerRoleArn, sourceCodeHash, opts) {
|
|
179
|
+
let name = "AllAutomationSlices";
|
|
180
|
+
let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
181
|
+
let lambdaArn = Output$Pulumi.flatMap(runtime.parts.lambda, l => l.arn);
|
|
182
|
+
let manage = new (Aws.iam.RolePolicy)(name + `SchedulerPolicy`, {
|
|
183
|
+
policy: schedulerRoleArn.apply(arn => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `SchedulerPolicy`, [
|
|
184
|
+
{
|
|
185
|
+
Sid: "AllowManageSchedules",
|
|
186
|
+
Effect: "Allow",
|
|
187
|
+
Action: ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.manageActions,
|
|
188
|
+
Resource: ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.schedulesOfRoleArn(arn)
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
Sid: "AllowPassSchedulerRole",
|
|
192
|
+
Effect: "Allow",
|
|
193
|
+
Action: "iam:PassRole",
|
|
194
|
+
Resource: arn
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
Sid: "AllowListSchedules",
|
|
198
|
+
Effect: "Allow",
|
|
199
|
+
Action: ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.listActions,
|
|
200
|
+
Resource: "*"
|
|
201
|
+
}
|
|
202
|
+
]))),
|
|
203
|
+
role: runtime.parts.lambdaRole.id
|
|
204
|
+
}, customOpts);
|
|
205
|
+
let invoke = new (Aws.iam.RolePolicy)(name + `SchedulerInvokePolicy`, {
|
|
206
|
+
policy: lambdaArn.apply(arn => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `SchedulerInvokePolicy`, [{
|
|
207
|
+
Sid: "AllowSchedulerToInvokeAutomations",
|
|
208
|
+
Effect: "Allow",
|
|
209
|
+
Action: "lambda:InvokeFunction",
|
|
210
|
+
Resource: arn
|
|
211
|
+
}]))),
|
|
212
|
+
role: schedulerRoleArn.apply(ScheduledPublisher_EventBridgeScheduler_Runtime$ReventlessAws.groupOfRoleArn)
|
|
213
|
+
}, customOpts);
|
|
214
|
+
let declared = Object.fromEntries(Stdlib_Array.filterMap(Object.entries(bundledInfos), param => {
|
|
215
|
+
let slice = param[0];
|
|
216
|
+
return Stdlib_Option.map(param[1].schedule, time => [
|
|
217
|
+
slice,
|
|
218
|
+
Util_Sury$Reventless.toJson(time, Schedule$Reventless.timeSchema)
|
|
219
|
+
]);
|
|
220
|
+
}).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0])));
|
|
221
|
+
let input = JSON.stringify(Object.fromEntries([
|
|
222
|
+
[
|
|
223
|
+
"reventlessReconcile",
|
|
224
|
+
true
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"declared",
|
|
228
|
+
declared
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
"code",
|
|
232
|
+
sourceCodeHash
|
|
233
|
+
]
|
|
234
|
+
]));
|
|
235
|
+
let newrecord = {...customOpts};
|
|
236
|
+
new (Aws.lambda.Invocation)(name + `ReconcileSchedules`, {
|
|
237
|
+
functionName: Output$Pulumi.flatMap(runtime.parts.lambda, l => l.name),
|
|
238
|
+
input: input
|
|
239
|
+
}, (newrecord.dependsOn = [
|
|
240
|
+
manage,
|
|
241
|
+
invoke
|
|
242
|
+
], newrecord));
|
|
243
|
+
}
|
|
244
|
+
|
|
171
245
|
let finished = {
|
|
172
246
|
contents: false
|
|
173
247
|
};
|
|
@@ -235,7 +309,7 @@ function finish() {
|
|
|
235
309
|
finished.contents = true;
|
|
236
310
|
}
|
|
237
311
|
|
|
238
|
-
function finishWithDcbEventLog(dcbEventLog) {
|
|
312
|
+
function finishWithDcbEventLog(schedulerRoleArn, dcbEventLog) {
|
|
239
313
|
if (finished.contents) {
|
|
240
314
|
return;
|
|
241
315
|
}
|
|
@@ -290,6 +364,9 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
290
364
|
info.queryDbResources.forEach(r => {
|
|
291
365
|
allQueryDbResources.push(r);
|
|
292
366
|
});
|
|
367
|
+
info.schedulesResources.forEach(r => {
|
|
368
|
+
allQueryDbResources.push(r);
|
|
369
|
+
});
|
|
293
370
|
let target = Stdlib_Option.flatMap(info.commandTargetName, CommandTopicRegistry$ReventlessAws.get);
|
|
294
371
|
if (target !== undefined) {
|
|
295
372
|
allQueryDbResources.push(target.resource);
|
|
@@ -316,16 +393,21 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
316
393
|
];
|
|
317
394
|
let commandQueueIsFifo = match[1];
|
|
318
395
|
let hasOwnUrns = !info.consumesDcbLog || Object.keys(info.sourceTopics).length !== 0;
|
|
396
|
+
let resource = info.schedulesResources[0];
|
|
397
|
+
let schedulesTableName = resource !== undefined ? resource.name : Pulumi.output("");
|
|
319
398
|
let handlerJson = Pulumi.all([
|
|
320
399
|
info.queryDbTableName,
|
|
321
400
|
match[0],
|
|
322
|
-
sourceUrnsFor(info)
|
|
401
|
+
sourceUrnsFor(info),
|
|
402
|
+
schedulesTableName
|
|
323
403
|
]).apply(param => {
|
|
404
|
+
let schedulesTable = param[3];
|
|
324
405
|
let urnsFragment = hasOwnUrns ? `,"u":` + JSON.stringify(param[2].map(prim => prim)) : "";
|
|
325
406
|
let queueFragment = target !== undefined ? `,"k":"` + param[1] + `","f":` + (
|
|
326
407
|
commandQueueIsFifo ? "true" : "false"
|
|
327
408
|
) : "";
|
|
328
|
-
|
|
409
|
+
let schedulesFragment = schedulesTable === "" ? "" : `,"s":"` + schedulesTable + `"`;
|
|
410
|
+
return `{"n":` + nameJson + `,"q":"` + param[0] + `"` + urnsFragment + queueFragment + schedulesFragment + `}`;
|
|
329
411
|
});
|
|
330
412
|
handlerOutputs.push(handlerJson);
|
|
331
413
|
});
|
|
@@ -355,16 +437,19 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
355
437
|
PluginRuntime_Builder$ReventlessAws.capabilityEnv().forEach(param => {
|
|
356
438
|
envVars[param[0]] = param[1];
|
|
357
439
|
});
|
|
440
|
+
envVars["SCHEDULER_ROLE_ARN"] = schedulerRoleArn;
|
|
358
441
|
let extraStringAssets = {};
|
|
359
442
|
extraStringAssets["automationSliceModules.json"] = `{` + sliceModuleEntries.join(",") + `}`;
|
|
360
443
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs", packageDirs, extraStringAssets, undefined);
|
|
361
|
-
let
|
|
444
|
+
let sourceCodeHash = match.sourceCodeHash;
|
|
445
|
+
let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAutomationSlices", "Reactor", "AutomationSlice", match.code, sourceCodeHash, envVars, 1024, 30, undefined, undefined, undefined, undefined, undefined, opts);
|
|
362
446
|
EventCollectorChannel_DynamoDbStream$ReventlessAws.connect("AllAutomationSlices", [{
|
|
363
447
|
channel: channel,
|
|
364
448
|
eventTopics: eventTopics,
|
|
365
449
|
resources: allQueryDbResources
|
|
366
450
|
}], runtime, opts);
|
|
367
451
|
makeSweepSchedule(runtime, opts);
|
|
452
|
+
makeAutomationSchedules(runtime, schedulerRoleArn, sourceCodeHash, opts);
|
|
368
453
|
makeGeocoderGrant(runtime, opts);
|
|
369
454
|
makeMessagingGrant(runtime, opts);
|
|
370
455
|
} else {
|
|
@@ -398,6 +483,7 @@ export {
|
|
|
398
483
|
makeSweepSchedule,
|
|
399
484
|
makeGeocoderGrant,
|
|
400
485
|
makeMessagingGrant,
|
|
486
|
+
makeAutomationSchedules,
|
|
401
487
|
finished,
|
|
402
488
|
finish,
|
|
403
489
|
finishWithDcbEventLog,
|
|
@@ -57,20 +57,33 @@ let failWithoutQueue = action => {
|
|
|
57
57
|
->Effect.runPromise
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
let putSchedule = async (~roleArn, ~targetArn, schedule: Reventless.Schedule.schedule) => {
|
|
61
|
+
ReventlessCore.ScheduleTime.validate(schedule.time)
|
|
62
|
+
let input = schedule->scheduleInput(~roleArn, ~targetArn, ~setAt=Date.now())
|
|
63
|
+
switch await CreateScheduleCommand.make(input)->CreateScheduleCommand.send {
|
|
64
|
+
| _ => ()
|
|
65
|
+
| exception JsExn(err) if err->ErrorName.is(ErrorName.conflict) =>
|
|
66
|
+
let _ = await UpdateScheduleCommand.make(input)->UpdateScheduleCommand.send
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let removeSchedule = async (~roleArn, name) =>
|
|
71
|
+
switch await DeleteScheduleCommand.make({
|
|
72
|
+
name,
|
|
73
|
+
groupName: roleArn->groupOfRoleArn,
|
|
74
|
+
})->DeleteScheduleCommand.send {
|
|
75
|
+
| _ => ()
|
|
76
|
+
| exception JsExn(err) if err->ErrorName.is(ErrorName.resourceNotFound) => ()
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
/** Creates the schedule, or replaces the one of that name, as a classic rule's `PutRule` did. */
|
|
61
80
|
let createSchedule = (~roleArn: string): ReventlessCore.Scheduler.createSchedule =>
|
|
62
81
|
async (queueResources, schedule) =>
|
|
63
82
|
switch queueResources {
|
|
64
83
|
| [] => await failWithoutQueue("createSchedule")
|
|
65
84
|
| resources =>
|
|
66
|
-
ReventlessCore.ScheduleTime.validate(schedule.time)
|
|
67
85
|
let resource = resources->Array.getUnsafe(0) // FIXME
|
|
68
|
-
|
|
69
|
-
switch await CreateScheduleCommand.make(input)->CreateScheduleCommand.send {
|
|
70
|
-
| _ => ()
|
|
71
|
-
| exception JsExn(err) if err->ErrorName.is(ErrorName.conflict) =>
|
|
72
|
-
let _ = await UpdateScheduleCommand.make(input)->UpdateScheduleCommand.send
|
|
73
|
-
}
|
|
86
|
+
await putSchedule(~roleArn, ~targetArn=resource.urn, schedule)
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
/** Deletes the schedule; one already gone, such as a once-only time that fired, is no error. */
|
|
@@ -78,12 +91,88 @@ let deleteSchedule = (~roleArn: string): ReventlessCore.Scheduler.deleteSchedule
|
|
|
78
91
|
async (queueResources, name) =>
|
|
79
92
|
switch queueResources {
|
|
80
93
|
| [] => await failWithoutQueue("deleteSchedule")
|
|
81
|
-
| _ =>
|
|
82
|
-
switch await DeleteScheduleCommand.make({
|
|
83
|
-
name,
|
|
84
|
-
groupName: roleArn->groupOfRoleArn,
|
|
85
|
-
})->DeleteScheduleCommand.send {
|
|
86
|
-
| _ => ()
|
|
87
|
-
| exception JsExn(err) if err->ErrorName.is(ErrorName.resourceNotFound) => ()
|
|
88
|
-
}
|
|
94
|
+
| _ => await removeSchedule(~roleArn, name)
|
|
89
95
|
}
|
|
96
|
+
|
|
97
|
+
// ── An automation's schedules ───────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
let validName = /^[0-9a-zA-Z\-_.]{1,64}$/
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
The platform name of an automation's schedule: `<slice>.<key>`, or, when that is too
|
|
103
|
+
long or holds a character EventBridge refuses, the slice's name cut to 40 characters
|
|
104
|
+
and a hash of the key.
|
|
105
|
+
*/
|
|
106
|
+
let tickScheduleName = (~slice, ~key) => {
|
|
107
|
+
let name = `${slice}.${key}`
|
|
108
|
+
validName->RegExp.test(name)
|
|
109
|
+
? name
|
|
110
|
+
: {
|
|
111
|
+
let hash =
|
|
112
|
+
NodeCrypto.createHash("sha256")
|
|
113
|
+
->NodeCrypto.hashUpdate(key)
|
|
114
|
+
->NodeCrypto.hashDigest("hex")
|
|
115
|
+
->String.slice(~start=0, ~end=16)
|
|
116
|
+
`${slice->String.slice(~start=0, ~end=40)}.${hash}`
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** What the Scheduler sends the automation Lambda: the slice, the key and the scheduled time. */
|
|
121
|
+
let tickInput = (~slice, ~key) =>
|
|
122
|
+
JSON.Encode.object(
|
|
123
|
+
Dict.fromArray([
|
|
124
|
+
(
|
|
125
|
+
"reventlessTick",
|
|
126
|
+
JSON.Encode.object(
|
|
127
|
+
Dict.fromArray([
|
|
128
|
+
("slice", slice->JSON.Encode.string),
|
|
129
|
+
("key", key->JSON.Encode.string),
|
|
130
|
+
("at", "<aws.scheduler.scheduled-time>"->JSON.Encode.string),
|
|
131
|
+
]),
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
]),
|
|
135
|
+
)->JSON.stringify
|
|
136
|
+
|
|
137
|
+
/** Sets the slice's schedule of that key, invoking `lambdaArn` with `tickInput` at each firing. */
|
|
138
|
+
let putTickSchedule = (~roleArn, ~lambdaArn, ~slice, ~key, time) =>
|
|
139
|
+
putSchedule(
|
|
140
|
+
~roleArn,
|
|
141
|
+
~targetArn=lambdaArn,
|
|
142
|
+
{name: tickScheduleName(~slice, ~key), time, payload: tickInput(~slice, ~key)},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
/** The actions a Lambda that reconciles an automation's schedules is granted on every
|
|
146
|
+
resource: listing schedules has no resource-level permission. */
|
|
147
|
+
let listActions = ["scheduler:ListSchedules"]
|
|
148
|
+
|
|
149
|
+
// Hashed names start with the slice's name cut to 40 characters.
|
|
150
|
+
let sliceNamePrefix = slice => slice->String.slice(~start=0, ~end=40)
|
|
151
|
+
|
|
152
|
+
let isSliceSchedule = (name, ~slice) => {
|
|
153
|
+
let cut = slice->sliceNamePrefix
|
|
154
|
+
name->String.startsWith(`${slice}.`) ||
|
|
155
|
+
(cut != slice &&
|
|
156
|
+
name->String.startsWith(`${cut}.`) &&
|
|
157
|
+
name->String.length == cut->String.length + 17)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The slice's schedules in the scheduler group of `roleArn`, invoking `lambdaArn`. */
|
|
161
|
+
let platformSchedules = (
|
|
162
|
+
~roleArn,
|
|
163
|
+
~lambdaArn,
|
|
164
|
+
~slice,
|
|
165
|
+
): ReventlessInfra.AutomationSlice.platformSchedules => {
|
|
166
|
+
put: (~key, time) => putTickSchedule(~roleArn, ~lambdaArn, ~slice, ~key, time),
|
|
167
|
+
removeName: name => removeSchedule(~roleArn, name),
|
|
168
|
+
names: async () =>
|
|
169
|
+
(
|
|
170
|
+
await ListSchedulesCommand.all({
|
|
171
|
+
groupName: roleArn->groupOfRoleArn,
|
|
172
|
+
namePrefix: slice->sliceNamePrefix,
|
|
173
|
+
})
|
|
174
|
+
)
|
|
175
|
+
->Array.map(s => s.name)
|
|
176
|
+
->Array.filter(isSliceSchedule(_, ~slice)),
|
|
177
|
+
nameOf: key => tickScheduleName(~slice, ~key),
|
|
178
|
+
}
|
package/src/adapter/ScheduledPublisher/ScheduledPublisher_EventBridgeScheduler_Runtime.res.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Nodecrypto from "node:crypto";
|
|
3
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Effect from "effect/Effect";
|
|
5
6
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
@@ -62,52 +63,129 @@ function failWithoutQueue(action) {
|
|
|
62
63
|
return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logError("ScheduledPublisher_EventBridgeScheduler_Runtime-ReventlessAws", undefined, err), () => Effect.sync(() => Stdlib_JsError.throwWithMessage(err))));
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
async function putSchedule(roleArn, targetArn, schedule) {
|
|
67
|
+
ScheduleTime$ReventlessCore.validate(schedule.time);
|
|
68
|
+
let input = scheduleInput(roleArn, targetArn, Date.now(), schedule);
|
|
69
|
+
try {
|
|
70
|
+
await EventBridgeScheduler$AwsSdk.CreateScheduleCommand.send(new ClientScheduler.CreateScheduleCommand(input));
|
|
71
|
+
return;
|
|
72
|
+
} catch (raw_err) {
|
|
73
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
74
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
75
|
+
if (EventBridgeScheduler$AwsSdk.ErrorName.is(err._1, EventBridgeScheduler$AwsSdk.ErrorName.conflict)) {
|
|
76
|
+
await EventBridgeScheduler$AwsSdk.UpdateScheduleCommand.send(new ClientScheduler.UpdateScheduleCommand(input));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function removeSchedule(roleArn, name) {
|
|
86
|
+
try {
|
|
87
|
+
await EventBridgeScheduler$AwsSdk.DeleteScheduleCommand.send(new ClientScheduler.DeleteScheduleCommand({
|
|
88
|
+
Name: name,
|
|
89
|
+
GroupName: groupOfRoleArn(roleArn)
|
|
90
|
+
}));
|
|
91
|
+
return;
|
|
92
|
+
} catch (raw_err) {
|
|
93
|
+
let err = Primitive_exceptions.internalToException(raw_err);
|
|
94
|
+
if (err.RE_EXN_ID === "JsExn") {
|
|
95
|
+
if (EventBridgeScheduler$AwsSdk.ErrorName.is(err._1, EventBridgeScheduler$AwsSdk.ErrorName.resourceNotFound)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
65
104
|
function createSchedule(roleArn) {
|
|
66
105
|
return async (queueResources, schedule) => {
|
|
67
106
|
if (queueResources.length === 0) {
|
|
68
107
|
return await failWithoutQueue("createSchedule");
|
|
69
108
|
}
|
|
70
|
-
ScheduleTime$ReventlessCore.validate(schedule.time);
|
|
71
109
|
let resource = queueResources[0];
|
|
72
|
-
|
|
73
|
-
try {
|
|
74
|
-
await EventBridgeScheduler$AwsSdk.CreateScheduleCommand.send(new ClientScheduler.CreateScheduleCommand(input));
|
|
75
|
-
return;
|
|
76
|
-
} catch (raw_err) {
|
|
77
|
-
let err = Primitive_exceptions.internalToException(raw_err);
|
|
78
|
-
if (err.RE_EXN_ID === "JsExn") {
|
|
79
|
-
if (EventBridgeScheduler$AwsSdk.ErrorName.is(err._1, EventBridgeScheduler$AwsSdk.ErrorName.conflict)) {
|
|
80
|
-
await EventBridgeScheduler$AwsSdk.UpdateScheduleCommand.send(new ClientScheduler.UpdateScheduleCommand(input));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
85
|
-
throw err;
|
|
86
|
-
}
|
|
110
|
+
return await putSchedule(roleArn, resource.urn, schedule);
|
|
87
111
|
};
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
function deleteSchedule(roleArn) {
|
|
91
115
|
return async (queueResources, name) => {
|
|
92
|
-
if (queueResources.length
|
|
116
|
+
if (queueResources.length !== 0) {
|
|
117
|
+
return await removeSchedule(roleArn, name);
|
|
118
|
+
} else {
|
|
93
119
|
return await failWithoutQueue("deleteSchedule");
|
|
94
120
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let validName = /^[0-9a-zA-Z\-_.]{1,64}$/;
|
|
125
|
+
|
|
126
|
+
function tickScheduleName(slice, key) {
|
|
127
|
+
let name = slice + `.` + key;
|
|
128
|
+
if (validName.test(name)) {
|
|
129
|
+
return name;
|
|
130
|
+
}
|
|
131
|
+
let hash = Nodecrypto.createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
132
|
+
return slice.slice(0, 40) + `.` + hash;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function tickInput(slice, key) {
|
|
136
|
+
return JSON.stringify(Object.fromEntries([[
|
|
137
|
+
"reventlessTick",
|
|
138
|
+
Object.fromEntries([
|
|
139
|
+
[
|
|
140
|
+
"slice",
|
|
141
|
+
slice
|
|
142
|
+
],
|
|
143
|
+
[
|
|
144
|
+
"key",
|
|
145
|
+
key
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"at",
|
|
149
|
+
"<aws.scheduler.scheduled-time>"
|
|
150
|
+
]
|
|
151
|
+
])
|
|
152
|
+
]]));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function putTickSchedule(roleArn, lambdaArn, slice, key, time) {
|
|
156
|
+
return putSchedule(roleArn, lambdaArn, {
|
|
157
|
+
name: tickScheduleName(slice, key),
|
|
158
|
+
time: time,
|
|
159
|
+
payload: tickInput(slice, key)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let listActions = ["scheduler:ListSchedules"];
|
|
164
|
+
|
|
165
|
+
function sliceNamePrefix(slice) {
|
|
166
|
+
return slice.slice(0, 40);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isSliceSchedule(name, slice) {
|
|
170
|
+
let cut = slice.slice(0, 40);
|
|
171
|
+
if (name.startsWith(slice + `.`)) {
|
|
172
|
+
return true;
|
|
173
|
+
} else if (cut !== slice && name.startsWith(cut + `.`)) {
|
|
174
|
+
return name.length === (cut.length + 17 | 0);
|
|
175
|
+
} else {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function platformSchedules(roleArn, lambdaArn, slice) {
|
|
181
|
+
return {
|
|
182
|
+
put: (key, time) => putTickSchedule(roleArn, lambdaArn, slice, key, time),
|
|
183
|
+
removeName: name => removeSchedule(roleArn, name),
|
|
184
|
+
names: async () => (await EventBridgeScheduler$AwsSdk.ListSchedulesCommand.all({
|
|
185
|
+
GroupName: groupOfRoleArn(roleArn),
|
|
186
|
+
NamePrefix: slice.slice(0, 40)
|
|
187
|
+
})).map(s => s.Name).filter(__x => isSliceSchedule(__x, slice)),
|
|
188
|
+
nameOf: key => tickScheduleName(slice, key)
|
|
111
189
|
};
|
|
112
190
|
}
|
|
113
191
|
|
|
@@ -120,7 +198,17 @@ export {
|
|
|
120
198
|
isFifoQueue,
|
|
121
199
|
scheduleInput,
|
|
122
200
|
failWithoutQueue,
|
|
201
|
+
putSchedule,
|
|
202
|
+
removeSchedule,
|
|
123
203
|
createSchedule,
|
|
124
204
|
deleteSchedule,
|
|
205
|
+
validName,
|
|
206
|
+
tickScheduleName,
|
|
207
|
+
tickInput,
|
|
208
|
+
putTickSchedule,
|
|
209
|
+
listActions,
|
|
210
|
+
sliceNamePrefix,
|
|
211
|
+
isSliceSchedule,
|
|
212
|
+
platformSchedules,
|
|
125
213
|
}
|
|
126
|
-
/*
|
|
214
|
+
/* node:crypto Not a pure module */
|
|
@@ -309,21 +309,18 @@ let injectAwsAuth = (
|
|
|
309
309
|
iamQueryFieldPrefixes->Array.push(entry.listFieldName)
|
|
310
310
|
iamTypePrefixes->Array.push(entry.returnTypeName)
|
|
311
311
|
}
|
|
312
|
+
// Every door the entry emits: the resolvers here do not check the rule, so a
|
|
313
|
+
// by-ids or by-index field left to the open directive reads the gated rows.
|
|
314
|
+
let fieldNames = ReventlessCore.GraphQL_FragmentGenerator.queryFieldNamesOf(entry)
|
|
312
315
|
switch entry.authorization {
|
|
313
|
-
| Some({group}) =>
|
|
314
|
-
queryAuthMap->Dict.set(entry.singleFieldName, [group])
|
|
315
|
-
queryAuthMap->Dict.set(entry.listFieldName, [group])
|
|
316
|
+
| Some({group}) => fieldNames->Array.forEach(f => queryAuthMap->Dict.set(f, [group]))
|
|
316
317
|
| None => ()
|
|
317
318
|
}
|
|
318
319
|
switch entry.permission {
|
|
319
320
|
| Some(permission) =>
|
|
320
321
|
switch AppSync_SdlDecorate.permissionToGate(permission) {
|
|
321
|
-
| Groups(groups) =>
|
|
322
|
-
|
|
323
|
-
queryAuthMap->Dict.set(entry.listFieldName, groups)
|
|
324
|
-
| AnyAuthenticated =>
|
|
325
|
-
queryAuthMap->Dict.delete(entry.singleFieldName)
|
|
326
|
-
queryAuthMap->Dict.delete(entry.listFieldName)
|
|
322
|
+
| Groups(groups) => fieldNames->Array.forEach(f => queryAuthMap->Dict.set(f, groups))
|
|
323
|
+
| AnyAuthenticated => fieldNames->Array.forEach(f => queryAuthMap->Dict.delete(f))
|
|
327
324
|
| Anonymous =>
|
|
328
325
|
// Both derived field names, because the spec declares one permission and
|
|
329
326
|
// the generator emits two fields from it; naming only one would send the
|
|
@@ -154,11 +154,13 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
154
154
|
iamQueryFieldPrefixes.push(entry.listFieldName);
|
|
155
155
|
iamTypePrefixes.push(entry.returnTypeName);
|
|
156
156
|
}
|
|
157
|
+
let fieldNames = GraphQL_FragmentGenerator$ReventlessCore.queryFieldNamesOf(entry);
|
|
157
158
|
let match = entry.authorization;
|
|
158
159
|
if (match !== undefined) {
|
|
159
160
|
let group = match.group;
|
|
160
|
-
|
|
161
|
-
|
|
161
|
+
fieldNames.forEach(f => {
|
|
162
|
+
queryAuthMap[f] = [group];
|
|
163
|
+
});
|
|
162
164
|
}
|
|
163
165
|
let permission = entry.permission;
|
|
164
166
|
if (permission === undefined) {
|
|
@@ -167,16 +169,17 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
167
169
|
let groups = AppSync_SdlDecorate$ReventlessAws.permissionToGate(permission);
|
|
168
170
|
if (typeof groups !== "object") {
|
|
169
171
|
if (groups === "AnyAuthenticated") {
|
|
170
|
-
Stdlib_Dict.$$delete(queryAuthMap,
|
|
171
|
-
return
|
|
172
|
+
fieldNames.forEach(f => Stdlib_Dict.$$delete(queryAuthMap, f));
|
|
173
|
+
return;
|
|
172
174
|
}
|
|
173
175
|
anonymousFields.push(entry.singleFieldName);
|
|
174
176
|
anonymousFields.push(entry.listFieldName);
|
|
175
177
|
return;
|
|
176
178
|
} else {
|
|
177
179
|
let groups$1 = groups._0;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
fieldNames.forEach(f => {
|
|
181
|
+
queryAuthMap[f] = groups$1;
|
|
182
|
+
});
|
|
180
183
|
return;
|
|
181
184
|
}
|
|
182
185
|
});
|
|
@@ -5,6 +5,23 @@ module EventCollectorChannel = EventCollectorChannel.DynamoDbStream
|
|
|
5
5
|
module RuntimeEnvironment = RuntimeEnvironment.Lambda
|
|
6
6
|
module EventCollectorRuntimeBuilder = AutomationSliceRuntime_Builder_Single
|
|
7
7
|
|
|
8
|
+
// The core builder's operations never run on AWS: the automation Lambda drives the
|
|
9
|
+
// callback itself (AutomationSliceEntryPoint_Ops).
|
|
10
|
+
module DeployTimeSchedules = {
|
|
11
|
+
let platformFor = (~slice): ReventlessInfra.AutomationSlice.platformSchedules => {
|
|
12
|
+
let refuse = () =>
|
|
13
|
+
JsError.throwWithMessage(
|
|
14
|
+
`AutomationSlice(${slice}): schedules are set by its Lambda, not at deploy`,
|
|
15
|
+
)
|
|
16
|
+
{
|
|
17
|
+
put: async (~key as _, _) => refuse(),
|
|
18
|
+
removeName: async _ => refuse(),
|
|
19
|
+
names: async () => refuse(),
|
|
20
|
+
nameOf: key => key,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
module Make = (
|
|
9
26
|
Api: {
|
|
10
27
|
let api: unit => Types.AppSync.api
|
|
@@ -18,6 +35,7 @@ module Make = (
|
|
|
18
35
|
EventCollectorChannel,
|
|
19
36
|
EventCollectorRuntimeBuilder,
|
|
20
37
|
Api,
|
|
38
|
+
DeployTimeSchedules,
|
|
21
39
|
)
|
|
22
40
|
|
|
23
41
|
module Make = (
|
|
@@ -30,6 +48,7 @@ module Make = (
|
|
|
30
48
|
module Automation = Automation
|
|
31
49
|
type component = InnerMake.component
|
|
32
50
|
let queryDbName = InnerMake.queryDbName
|
|
51
|
+
let schedulesDbName = InnerMake.schedulesDbName
|
|
33
52
|
let sourceNames = InnerMake.sourceNames
|
|
34
53
|
|
|
35
54
|
let make = (~allEventTopics, ~publishJsons, ~context, ~runtime=?, ~opts=?): component => {
|
|
@@ -60,6 +79,11 @@ module Make = (
|
|
|
60
79
|
~context,
|
|
61
80
|
~sourceTopics,
|
|
62
81
|
~consumesDcbLog,
|
|
82
|
+
~schedule=?Spec.schedule,
|
|
83
|
+
~schedulesResources=(as_->ReventlessCore.Component.outputs).schedulesDb->Option.mapOr(
|
|
84
|
+
[],
|
|
85
|
+
db => db.resources,
|
|
86
|
+
),
|
|
63
87
|
)
|
|
64
88
|
|
|
65
89
|
as_
|