@reventlessdev/reventless-aws 3.0.0-alpha.262 → 3.0.0-alpha.264
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 +14 -0
- package/package.json +7 -7
- package/src/Platform.res +7 -2
- package/src/Platform.res.mjs +2 -2
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res +5 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs +3 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_FIFO.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res +6 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs +4 -1
- package/src/adapter/CommandTopic/CommandTopicRegistry.res +50 -0
- package/src/adapter/CommandTopic/CommandTopicRegistry.res.mjs +26 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +40 -20
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +12 -6
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +70 -3
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +46 -6
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res +5 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +1 -1
- package/tests/AutomationSliceEntryPoint_OpsTest.res +12 -2
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +15 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.264 (2026-08-03)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** give slice command publishes a queue they can actually reach ([42fdb55](https://github.com/ReventlessDev/reventless-core/commit/42fdb55b751a197d586c49e3615ce12ffea42046))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.263 (2026-08-03)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** make the slice callback protocol one declaration, not three ([d49e543](https://github.com/ReventlessDev/reventless-core/commit/d49e543f4715c955c75cec8bd691ee0927b7e2f4))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.262 (2026-08-03)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.264",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,17 +12,17 @@
|
|
|
12
12
|
"sury": "11.0.0-alpha.4",
|
|
13
13
|
"uuid": "^13.0.0",
|
|
14
14
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.3",
|
|
15
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
15
16
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
16
17
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.65",
|
|
17
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.1",
|
|
18
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
19
19
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
20
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.207",
|
|
21
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.124",
|
|
22
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
23
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.71",
|
|
24
20
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
25
|
-
"@reventlessdev/reventless-
|
|
21
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.209",
|
|
22
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.124",
|
|
23
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.99",
|
|
24
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.73",
|
|
25
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -794,12 +794,17 @@ module MakeWithConfig = (
|
|
|
794
794
|
let tableResource = outputs.resources->Array.getUnsafe(0)
|
|
795
795
|
PluginRuntime_Builder.registerDcbTableName(tableResource.name)
|
|
796
796
|
},
|
|
797
|
-
// DCB CommandTopic created hook — extracts SQS queue URL for slice builders
|
|
797
|
+
// DCB CommandTopic created hook — extracts SQS queue URL for slice builders,
|
|
798
|
+
// and the queue resource so the slice Lambda's role is granted
|
|
799
|
+
// `sqs:SendMessage` on it.
|
|
798
800
|
onDcbCommandTopicCreated: dcbCommandTopicUnknown => {
|
|
799
801
|
let commandTopic = dcbCommandTopicUnknown->asDcbCommandTopicComponent
|
|
800
802
|
let channel = commandTopic->ReventlessCore.CommandTopic_Adapter.channel
|
|
801
803
|
let channelParts = channel.parts->asSqsChannelParts
|
|
802
|
-
AutomationSliceRuntime_Builder_Single.setDcbQueueUrl(
|
|
804
|
+
AutomationSliceRuntime_Builder_Single.setDcbQueueUrl(
|
|
805
|
+
~resource=?channel.resources->Array.get(0),
|
|
806
|
+
channelParts.queue.id,
|
|
807
|
+
)
|
|
803
808
|
},
|
|
804
809
|
// DCB slices created hook — finalize slice Lambdas.
|
|
805
810
|
onDcbSlicesCreated: dcbEventLogUnknown => {
|
package/src/Platform.res.mjs
CHANGED
|
@@ -542,7 +542,7 @@ function MakeWithConfig(Config) {
|
|
|
542
542
|
let hooks_onDcbCommandTopicCreated = dcbCommandTopicUnknown => {
|
|
543
543
|
let channel = dcbCommandTopicUnknown.channel;
|
|
544
544
|
let channelParts = channel.parts;
|
|
545
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(channelParts.queue.id);
|
|
545
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(undefined, channel.resources[0], channelParts.queue.id);
|
|
546
546
|
};
|
|
547
547
|
let hooks_onDcbSlicesCreated = dcbEventLogUnknown => {
|
|
548
548
|
StateViewSliceRuntime_Builder_Single$ReventlessAws.finishWithDcbEventLog(dcbEventLogUnknown);
|
|
@@ -1796,7 +1796,7 @@ function Make($star) {
|
|
|
1796
1796
|
let hooks_onDcbCommandTopicCreated = dcbCommandTopicUnknown => {
|
|
1797
1797
|
let channel = dcbCommandTopicUnknown.channel;
|
|
1798
1798
|
let channelParts = channel.parts;
|
|
1799
|
-
AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(channelParts.queue.id);
|
|
1799
|
+
AutomationSliceRuntime_Builder_Single$ReventlessAws.setDcbQueueUrl(undefined, channel.resources[0], channelParts.queue.id);
|
|
1800
1800
|
};
|
|
1801
1801
|
let hooks_onDcbSlicesCreated = dcbEventLogUnknown => {
|
|
1802
1802
|
StateViewSliceRuntime_Builder_Single$ReventlessAws.finishWithDcbEventLog(dcbEventLogUnknown);
|
|
@@ -65,10 +65,15 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
|
|
68
|
+
let queueResource = queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)
|
|
69
|
+
|
|
70
|
+
// Recorded while the queue is still concrete, so a component publishing to
|
|
71
|
+
// this owner from a Lambda can be given the URL and flavor as configuration.
|
|
72
|
+
CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=false)
|
|
68
73
|
|
|
69
74
|
{
|
|
70
75
|
ReventlessCore.CommandTopic_Adapter.parts: {queue: queue},
|
|
71
|
-
resources: [
|
|
76
|
+
resources: [queueResource],
|
|
72
77
|
publishJsons: resolvedQueueOutput->Pulumi.Output.apply(resolvedQueue =>
|
|
73
78
|
resolvedQueue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS, ...)
|
|
74
79
|
),
|
|
@@ -12,6 +12,7 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
|
12
12
|
import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
|
|
13
13
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
14
14
|
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
15
|
+
import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
|
|
15
16
|
import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
|
|
16
17
|
import * as CommandTopicChannel_Helpers$ReventlessAws from "./CommandTopicChannel_Helpers.res.mjs";
|
|
17
18
|
import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
@@ -48,11 +49,13 @@ function make(name, owner, opts) {
|
|
|
48
49
|
sqsManagedSseEnabled: false
|
|
49
50
|
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
50
51
|
let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
|
|
52
|
+
let queueResource = Util_SQS$ReventlessAws.toResource(tags, queue);
|
|
53
|
+
CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, false);
|
|
51
54
|
return {
|
|
52
55
|
parts: {
|
|
53
56
|
queue: queue
|
|
54
57
|
},
|
|
55
|
-
resources: [
|
|
58
|
+
resources: [queueResource],
|
|
56
59
|
publishJsons: resolvedQueueOutput.apply(resolvedQueue => CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS")),
|
|
57
60
|
publishJsonsStream: resolvedQueueOutput.apply(resolvedQueue => {
|
|
58
61
|
let publishJsons = CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS");
|
|
@@ -44,6 +44,11 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
|
|
|
44
44
|
)
|
|
45
45
|
|
|
46
46
|
let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
|
|
47
|
+
let queueResource = queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)
|
|
48
|
+
|
|
49
|
+
// Async aggregates and StateChangeSlices land here; same capture so a slice
|
|
50
|
+
// targeting one is routed to it rather than to the plugin's DCB topic.
|
|
51
|
+
CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=false)
|
|
47
52
|
|
|
48
53
|
{
|
|
49
54
|
parts: {queue: queue},
|
|
@@ -13,6 +13,7 @@ import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
|
|
|
13
13
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
14
14
|
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
15
15
|
import * as Util_SQS_FIFO$ReventlessAws from "../../util/Util_SQS_FIFO.res.mjs";
|
|
16
|
+
import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
|
|
16
17
|
import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
|
|
17
18
|
import * as CommandTopicChannel_SQS$ReventlessAws from "./CommandTopicChannel_SQS.res.mjs";
|
|
18
19
|
import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
@@ -31,6 +32,8 @@ function make(name, owner, opts) {
|
|
|
31
32
|
sqsManagedSseEnabled: false
|
|
32
33
|
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
33
34
|
let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
|
|
35
|
+
let queueResource = Util_SQS$ReventlessAws.toResource(tags, queue);
|
|
36
|
+
CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, false);
|
|
34
37
|
return {
|
|
35
38
|
parts: {
|
|
36
39
|
queue: queue
|
|
@@ -33,10 +33,15 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
|
|
|
33
33
|
)
|
|
34
34
|
|
|
35
35
|
let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
|
|
36
|
+
let queueResource = queue->Util_SQS_FIFO.toResource(~tags=tags->Pulumi.Output.fromInput)
|
|
37
|
+
|
|
38
|
+
// Same capture as the standard variant, and the reason the flag exists: only
|
|
39
|
+
// the creating module knows this queue needs a MessageGroupId.
|
|
40
|
+
CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=true)
|
|
36
41
|
|
|
37
42
|
{
|
|
38
43
|
parts: {queue: queue},
|
|
39
|
-
resources: [
|
|
44
|
+
resources: [queueResource],
|
|
40
45
|
publishJsons: resolvedQueueOutput->Pulumi.Output.apply(resolvedQueue =>
|
|
41
46
|
resolvedQueue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO, ...)
|
|
42
47
|
),
|
|
@@ -13,6 +13,7 @@ import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
|
|
|
13
13
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
14
14
|
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
15
15
|
import * as Util_SQS_FIFO$ReventlessAws from "../../util/Util_SQS_FIFO.res.mjs";
|
|
16
|
+
import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
|
|
16
17
|
import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
|
|
17
18
|
import * as CommandTopicChannel_SQS$ReventlessAws from "./CommandTopicChannel_SQS.res.mjs";
|
|
18
19
|
import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
@@ -31,11 +32,13 @@ function make(name, owner, opts) {
|
|
|
31
32
|
sqsManagedSseEnabled: false
|
|
32
33
|
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
33
34
|
let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
|
|
35
|
+
let queueResource = Util_SQS_FIFO$ReventlessAws.toResource(tags, queue);
|
|
36
|
+
CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, true);
|
|
34
37
|
return {
|
|
35
38
|
parts: {
|
|
36
39
|
queue: queue
|
|
37
40
|
},
|
|
38
|
-
resources: [
|
|
41
|
+
resources: [queueResource],
|
|
39
42
|
publishJsons: resolvedQueueOutput.apply(resolvedQueue => CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS_FIFO")),
|
|
40
43
|
publishJsonsStream: resolvedQueueOutput.apply(resolvedQueue => {
|
|
41
44
|
let publishJsons = CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS_FIFO");
|
|
@@ -45,10 +45,15 @@ let make: ReventlessCore.CommandTopic_Adapter.channelMaker<
|
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
let resolvedQueueOutput = queue->Util_SQS.toResolvedQueueOutput
|
|
48
|
+
let queueResource = queue->Util_SQS.toResource(~tags=tags->Pulumi.Output.fromInput)
|
|
49
|
+
|
|
50
|
+
// This is the maker Aggregates use, so it is the one an OutboundTranslationSlice
|
|
51
|
+
// naming an Aggregate in `targetName` needs to find.
|
|
52
|
+
CommandTopicRegistry.register(~owner, ~queueUrl=queue.id, ~resource=queueResource, ~isFifo=false)
|
|
48
53
|
|
|
49
54
|
{
|
|
50
55
|
ReventlessCore.CommandTopic_Adapter.parts: {queue: queue},
|
|
51
|
-
resources: [
|
|
56
|
+
resources: [queueResource],
|
|
52
57
|
publishJsons: resolvedQueueOutput->Pulumi.Output.apply(resolvedQueue =>
|
|
53
58
|
resolvedQueue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS, ...)
|
|
54
59
|
),
|
|
@@ -11,6 +11,7 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
|
11
11
|
import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
|
|
12
12
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
13
13
|
import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
|
|
14
|
+
import * as CommandTopicRegistry$ReventlessAws from "./CommandTopicRegistry.res.mjs";
|
|
14
15
|
import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
|
|
15
16
|
import * as CommandTopicChannel_SQS$ReventlessAws from "./CommandTopicChannel_SQS.res.mjs";
|
|
16
17
|
import * as CommandTopicChannel_SQS_Runtime$ReventlessAws from "./CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
@@ -28,11 +29,13 @@ function make(name, owner, opts) {
|
|
|
28
29
|
sqsManagedSseEnabled: false
|
|
29
30
|
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
30
31
|
let resolvedQueueOutput = Util_SQS$ReventlessAws.toResolvedQueueOutput(queue);
|
|
32
|
+
let queueResource = Util_SQS$ReventlessAws.toResource(tags, queue);
|
|
33
|
+
CommandTopicRegistry$ReventlessAws.register(owner, queue.id, queueResource, false);
|
|
31
34
|
return {
|
|
32
35
|
parts: {
|
|
33
36
|
queue: queue
|
|
34
37
|
},
|
|
35
|
-
resources: [
|
|
38
|
+
resources: [queueResource],
|
|
36
39
|
publishJsons: resolvedQueueOutput.apply(resolvedQueue => CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS")),
|
|
37
40
|
publishJsonsStream: resolvedQueueOutput.apply(resolvedQueue => {
|
|
38
41
|
let publishJsons = CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(resolvedQueue, "SQS");
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Where each component's CommandTopic queue lives, recorded as the queue is
|
|
2
|
+
// created.
|
|
3
|
+
//
|
|
4
|
+
// Runtime code cannot be handed a publisher. A slice Lambda is a compiled
|
|
5
|
+
// EntryPoint configured through `HANDLER_CONFIG`, not a serialized closure —
|
|
6
|
+
// that separation is what fixed the cold-start crashes from closures mixing
|
|
7
|
+
// layer and runtime SDK versions — so a `publishJsons` function cannot cross
|
|
8
|
+
// into it. The same choice has to travel as *data*: which queue, and whether it
|
|
9
|
+
// is FIFO.
|
|
10
|
+
//
|
|
11
|
+
// Captured here rather than recovered later, because recovering it does not
|
|
12
|
+
// work. A component's `resources` array reaches callers inside a
|
|
13
|
+
// `Pulumi.Output.t`, and although every `resource` field is typed
|
|
14
|
+
// `Pulumi.Output.t<string>`, Pulumi flattens nested Outputs at resolution — so
|
|
15
|
+
// within the enclosing apply those fields are already plain strings and calling
|
|
16
|
+
// `.apply` on one throws. At creation the queue is still concrete: `.id` is a
|
|
17
|
+
// flat Output, and the creating module's identity already settles the flavor.
|
|
18
|
+
//
|
|
19
|
+
// Keyed by owner *name* — an Aggregate's `Spec.name` — which is the same key an
|
|
20
|
+
// OutboundTranslationSlice names in `targetName`.
|
|
21
|
+
|
|
22
|
+
type entry = {
|
|
23
|
+
/** The queue URL, for a runtime publisher rebuilt from configuration. */
|
|
24
|
+
queueUrl: Pulumi.Output.t<string>,
|
|
25
|
+
/** The queue as a deploy-time resource, so a caller can add it to a channel
|
|
26
|
+
spec and pick up `connectLambda`'s existing `sqs:SendMessage` grant. */
|
|
27
|
+
resource: ReventlessInfra.Adapter.resource,
|
|
28
|
+
/** FIFO queues need a `MessageGroupId` and standard queues reject one, so this
|
|
29
|
+
cannot be assumed by the caller — it is a property of the queue that only
|
|
30
|
+
the module which created it knows. */
|
|
31
|
+
isFifo: bool,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let byOwner: dict<entry> = Dict.make()
|
|
35
|
+
|
|
36
|
+
let register = (
|
|
37
|
+
~owner: option<ReventlessCore.ResourceAttribution.owner>,
|
|
38
|
+
~queueUrl: Pulumi.Output.t<string>,
|
|
39
|
+
~resource: ReventlessInfra.Adapter.resource,
|
|
40
|
+
~isFifo: bool,
|
|
41
|
+
) =>
|
|
42
|
+
owner->Option.forEach(({name}) =>
|
|
43
|
+
byOwner->Dict.set(name, {queueUrl, resource, isFifo})
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
/** The CommandTopic of the component with this name, if one has been created.
|
|
47
|
+
`None` for a target that is not an Aggregate in this plugin — a DCB
|
|
48
|
+
StateChangeSlice, or a name that matches nothing — where the caller keeps
|
|
49
|
+
the plugin-wide DCB command topic. */
|
|
50
|
+
let get = (name: string) => byOwner->Dict.get(name)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
|
|
5
|
+
let byOwner = {};
|
|
6
|
+
|
|
7
|
+
function register(owner, queueUrl, resource, isFifo) {
|
|
8
|
+
Stdlib_Option.forEach(owner, param => {
|
|
9
|
+
byOwner[param.name] = {
|
|
10
|
+
queueUrl: queueUrl,
|
|
11
|
+
resource: resource,
|
|
12
|
+
isFifo: isFifo
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function get(name) {
|
|
18
|
+
return byOwner[name];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
byOwner,
|
|
23
|
+
register,
|
|
24
|
+
get,
|
|
25
|
+
}
|
|
26
|
+
/* No side effect */
|
|
@@ -27,6 +27,10 @@ type handlerEntry = {
|
|
|
27
27
|
callbackType: string,
|
|
28
28
|
queryDbTableName: string,
|
|
29
29
|
dcbQueueUrl: string,
|
|
30
|
+
/** Whether `dcbQueueUrl` is a FIFO queue. Absent in a config written before
|
|
31
|
+
the flavor travelled with the URL, where `false` — a standard queue — is
|
|
32
|
+
what these command topics actually are. */
|
|
33
|
+
commandQueueIsFifo: option<bool>,
|
|
30
34
|
sourceUrn: string,
|
|
31
35
|
context: option<Reventless.AutomationSlice.context>,
|
|
32
36
|
}
|
|
@@ -53,6 +57,7 @@ let decodeEntry = (json: JSON.t): option<handlerEntry> =>
|
|
|
53
57
|
callbackType: h->strOf("callbackType")->Option.getOr("automation"),
|
|
54
58
|
queryDbTableName: h->strOf("queryDbTableName")->Option.getOr(""),
|
|
55
59
|
dcbQueueUrl: h->strOf("dcbQueueUrl")->Option.getOr(""),
|
|
60
|
+
commandQueueIsFifo: h->Dict.get("commandQueueIsFifo")->Option.flatMap(JSON.Decode.bool),
|
|
56
61
|
sourceUrn: h->strOf("sourceUrn")->Option.getOr(""),
|
|
57
62
|
context: h->Dict.get("context")->Option.flatMap(decodeContext),
|
|
58
63
|
})
|
|
@@ -88,9 +93,13 @@ let defaultContext = (sliceName: string): Reventless.AutomationSlice.context =>
|
|
|
88
93
|
|
|
89
94
|
// ── Shared plumbing ─────────────────────────────────────────────────────────
|
|
90
95
|
|
|
91
|
-
|
|
96
|
+
// The flavor comes from the handler config rather than being assumed. It used to
|
|
97
|
+
// be hardcoded FIFO while the command topics are standard queues, and SQS rejects
|
|
98
|
+
// the MessageGroupId a FIFO publisher attaches — a failure nothing had reached,
|
|
99
|
+
// because the role had no sqs:SendMessage to get that far.
|
|
100
|
+
let makePublishJsons = (queueUrl: string, ~isFifo: bool): ReventlessCore.CommandTopic.publishJsons => {
|
|
92
101
|
let queue: Util_SQS_Runtime.resolvedQueue = {id: queueUrl, name: queueUrl, arn: ""}
|
|
93
|
-
queue->CommandTopicChannel_SQS_Runtime.publishJsons(AWS.SQS_FIFO)
|
|
102
|
+
queue->CommandTopicChannel_SQS_Runtime.publishJsons(isFifo ? AWS.SQS_FIFO : AWS.SQS)
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
// Overwrite-sync the in-memory TODO list into the slice's view table. Rows are
|
|
@@ -129,16 +138,21 @@ let makeSyncTodoItems = (
|
|
|
129
138
|
// The callback shapes the shell's functor applications produce
|
|
130
139
|
// (AutomationSlice_Callback.T / OutboundTranslationSlice_Callback.T), typed at
|
|
131
140
|
// the seam.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
// The entry point builds callbacks dynamically per HANDLER_CONFIG entry, so it
|
|
142
|
+
// cannot name `AutomationSlice_Callback.T` / `OutboundTranslationSlice_Callback.T`
|
|
143
|
+
// — the Spec is not known statically — and has to ascribe a structural type where
|
|
144
|
+
// the value crosses from the dynamically-imported JS module into ReScript.
|
|
145
|
+
//
|
|
146
|
+
// These are aliases of the protocol types each callback module owns, never
|
|
147
|
+
// restatements of them. That distinction is the whole point: a hand-written copy
|
|
148
|
+
// is a second source of truth the compiler cannot reconcile with the first, and
|
|
149
|
+
// it drifts silently. It did — when `phase1` gained its `sourceId` the copy here
|
|
150
|
+
// kept saying `array<'event>`, stayed internally consistent, compiled, and then
|
|
151
|
+
// destructured a bare event as a tuple at runtime, so every outbound slice on AWS
|
|
152
|
+
// died reading `.TAG` of `undefined` on its first record. As aliases, the same
|
|
153
|
+
// change is a compile error here instead.
|
|
154
|
+
type automationCallback = ReventlessCore.AutomationSlice_Callback.runtime
|
|
155
|
+
type outboundCallback<'event> = ReventlessCore.OutboundTranslationSlice_Callback.runtime<'event>
|
|
142
156
|
|
|
143
157
|
// Automation: phase 1 takes the RAW envelope JSONs — the callback routes by
|
|
144
158
|
// `meta.service` and unwraps `{id, meta, event}` itself — plus the context.
|
|
@@ -175,14 +189,20 @@ let makeOutboundJsonEventsHandler = (
|
|
|
175
189
|
stream
|
|
176
190
|
->Stream.mapEffect(json =>
|
|
177
191
|
Effect.sync(() => {
|
|
178
|
-
let
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
192
|
+
let envelope = json->JSON.Decode.object
|
|
193
|
+
let rawEvent = envelope->Option.flatMap(d => d->Dict.get("event"))->Option.getOr(json)
|
|
194
|
+
// The entity the event was published for, mirroring the in-process
|
|
195
|
+
// builder. An Aggregate's event payload does not repeat its own id, so
|
|
196
|
+
// without lifting this out of the envelope `collect` cannot name the
|
|
197
|
+
// subject of the outbound item it creates.
|
|
198
|
+
let sourceId =
|
|
199
|
+
envelope
|
|
200
|
+
->Option.flatMap(d => d->Dict.get("id"))
|
|
201
|
+
->Option.flatMap(JSON.Decode.string)
|
|
202
|
+
->Option.getOr("")
|
|
183
203
|
let (eventType, dataDict) = rawEvent->ReventlessCore.Message.splitMessage
|
|
184
204
|
switch decoder.decode(~eventType, ~data=dataDict) {
|
|
185
|
-
| Some(event) => [event]
|
|
205
|
+
| Some(event) => [(sourceId, event)]
|
|
186
206
|
| None => []
|
|
187
207
|
}
|
|
188
208
|
})
|
|
@@ -212,7 +232,7 @@ let makeAutomationRegisteredHandler = (
|
|
|
212
232
|
makeAutomationJsonEventsHandler(
|
|
213
233
|
~context=entry.context->Option.getOr(defaultContext(sliceName)),
|
|
214
234
|
~callback,
|
|
215
|
-
~publishJsons=makePublishJsons(entry.dcbQueueUrl),
|
|
235
|
+
~publishJsons=makePublishJsons(entry.dcbQueueUrl, ~isFifo=entry.commandQueueIsFifo->Option.getOr(false)),
|
|
216
236
|
~syncTodoItems=makeSyncTodoItems(
|
|
217
237
|
~queryDbTableName=entry.queryDbTableName,
|
|
218
238
|
~todoItems=callback.todoItems,
|
|
@@ -233,7 +253,7 @@ let makeOutboundRegisteredHandler = (
|
|
|
233
253
|
makeOutboundJsonEventsHandler(
|
|
234
254
|
~consumedEventSchema,
|
|
235
255
|
~callback,
|
|
236
|
-
~publishJsons=makePublishJsons(entry.dcbQueueUrl),
|
|
256
|
+
~publishJsons=makePublishJsons(entry.dcbQueueUrl, ~isFifo=entry.commandQueueIsFifo->Option.getOr(false)),
|
|
237
257
|
~syncTodoItems=makeSyncTodoItems(
|
|
238
258
|
~queryDbTableName=entry.queryDbTableName,
|
|
239
259
|
~todoItems=callback.todoItems,
|
|
@@ -36,6 +36,7 @@ function decodeEntry(json) {
|
|
|
36
36
|
callbackType: Stdlib_Option.getOr(strOf(h, "callbackType"), "automation"),
|
|
37
37
|
queryDbTableName: Stdlib_Option.getOr(strOf(h, "queryDbTableName"), ""),
|
|
38
38
|
dcbQueueUrl: Stdlib_Option.getOr(strOf(h, "dcbQueueUrl"), ""),
|
|
39
|
+
commandQueueIsFifo: Stdlib_Option.flatMap(h["commandQueueIsFifo"], Stdlib_JSON.Decode.bool),
|
|
39
40
|
sourceUrn: Stdlib_Option.getOr(strOf(h, "sourceUrn"), ""),
|
|
40
41
|
context: Stdlib_Option.flatMap(h["context"], decodeContext)
|
|
41
42
|
}));
|
|
@@ -64,13 +65,13 @@ function defaultContext(sliceName) {
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function makePublishJsons(queueUrl) {
|
|
68
|
+
function makePublishJsons(queueUrl, isFifo) {
|
|
68
69
|
let queue = {
|
|
69
70
|
id: queueUrl,
|
|
70
71
|
name: queueUrl,
|
|
71
72
|
arn: ""
|
|
72
73
|
};
|
|
73
|
-
return CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, "SQS_FIFO");
|
|
74
|
+
return CommandTopicChannel_SQS_Runtime$ReventlessAws.publishJsons(queue, isFifo ? "SQS_FIFO" : "SQS");
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
function makeSyncTodoItems(queryDbTableName, todoItems, rowSchema) {
|
|
@@ -98,11 +99,16 @@ function makeAutomationJsonEventsHandler(context, callback, publishJsons, syncTo
|
|
|
98
99
|
function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJsons, syncTodoItems) {
|
|
99
100
|
let decoder = DcbDecode$Reventless.makeDecoder(consumedEventSchema);
|
|
100
101
|
return stream => Effect.flatMap(Stream.runCollect(Stream$1.flatMap(Stream$1.mapEffect(stream, json => Effect.sync(() => {
|
|
101
|
-
let
|
|
102
|
+
let envelope = Stdlib_JSON.Decode.object(json);
|
|
103
|
+
let rawEvent = Stdlib_Option.getOr(Stdlib_Option.flatMap(envelope, d => d["event"]), json);
|
|
104
|
+
let sourceId = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(envelope, d => d["id"]), Stdlib_JSON.Decode.string), "");
|
|
102
105
|
let match = Message$ReventlessCore.splitMessage(rawEvent);
|
|
103
106
|
let event = decoder.decode(match[0], match[1]);
|
|
104
107
|
if (event !== undefined) {
|
|
105
|
-
return [
|
|
108
|
+
return [[
|
|
109
|
+
sourceId,
|
|
110
|
+
Primitive_option.valFromOption(event)
|
|
111
|
+
]];
|
|
106
112
|
} else {
|
|
107
113
|
return [];
|
|
108
114
|
}
|
|
@@ -116,14 +122,14 @@ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJso
|
|
|
116
122
|
|
|
117
123
|
function makeAutomationRegisteredHandler(entry, sliceName, callback) {
|
|
118
124
|
return {
|
|
119
|
-
handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(Stdlib_Option.getOr(entry.context, defaultContext(sliceName)), callback, makePublishJsons(entry.dcbQueueUrl), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema))),
|
|
125
|
+
handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeAutomationJsonEventsHandler(Stdlib_Option.getOr(entry.context, defaultContext(sliceName)), callback, makePublishJsons(entry.dcbQueueUrl, Stdlib_Option.getOr(entry.commandQueueIsFifo, false)), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, AutomationSlice_Callback$ReventlessCore.todoRowSchema))),
|
|
120
126
|
comp: `AutomationSlice(` + sliceName + `)`
|
|
121
127
|
};
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
function makeOutboundRegisteredHandler(entry, sliceName, consumedEventSchema, callback) {
|
|
125
131
|
return {
|
|
126
|
-
handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeOutboundJsonEventsHandler(consumedEventSchema, callback, makePublishJsons(entry.dcbQueueUrl), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema))),
|
|
132
|
+
handler: StreamRoutedEntryPoint_Ops$ReventlessAws.toStreamHandler(makeOutboundJsonEventsHandler(consumedEventSchema, callback, makePublishJsons(entry.dcbQueueUrl, Stdlib_Option.getOr(entry.commandQueueIsFifo, false)), makeSyncTodoItems(entry.queryDbTableName, callback.todoItems, OutboundTranslationSlice_Callback$ReventlessCore.todoRowSchema))),
|
|
127
133
|
comp: `OutboundTranslationSlice(` + sliceName + `)`
|
|
128
134
|
};
|
|
129
135
|
}
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -358,9 +392,25 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
358
392
|
let handlerOutputs: array<Pulumi.Output.t<string>> = []
|
|
359
393
|
let packageDirs: dict<string> = Dict.make()
|
|
360
394
|
let allQueryDbResources: array<ReventlessInfra.Adapter.resource> = []
|
|
395
|
+
// Whether any slice publishes to the DCB fallback rather than a target
|
|
396
|
+
// aggregate's own topic — if so, that queue needs the SendMessage grant
|
|
397
|
+
// too, once, below the loop.
|
|
398
|
+
let anyDcbFallback = ref(false)
|
|
361
399
|
|
|
362
400
|
bundledInfos->Dict.forEachWithKey((info, _name) => {
|
|
363
401
|
info.queryDbResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
|
|
402
|
+
// The queue this slice publishes its command to. An Aggregate registers
|
|
403
|
+
// its CommandTopic while plugin construction is still synchronous —
|
|
404
|
+
// before DCB construction registers the slices targeting it — so a
|
|
405
|
+
// named target resolves here; anything else keeps the DCB fallback.
|
|
406
|
+
let target = info.commandTargetName->Option.flatMap(CommandTopicRegistry.get)
|
|
407
|
+
switch target {
|
|
408
|
+
| Some({resource}) =>
|
|
409
|
+
// The target CommandTopic joins the channel's resources so
|
|
410
|
+
// `connectLambda`'s existing `sqs:SendMessage` grant covers it.
|
|
411
|
+
allQueryDbResources->Array.push(resource)->ignore
|
|
412
|
+
| None => anyDcbFallback := true
|
|
413
|
+
}
|
|
364
414
|
let pkg = Util_Bundle.extractPackageName(info.specModulePath)
|
|
365
415
|
packageDirs->Dict.set(pkg, Util_Bundle.resolvePackageRoot(pkg))
|
|
366
416
|
// Usually the same package as the spec, but bundle it explicitly —
|
|
@@ -380,8 +430,16 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
380
430
|
| None => ""
|
|
381
431
|
}
|
|
382
432
|
|
|
433
|
+
// Per slice, not plugin-wide: an outbound slice targeting an Aggregate
|
|
434
|
+
// publishes to that aggregate's CommandTopic. Everything else keeps the
|
|
435
|
+
// DCB topic, which is what the field has always meant — and with it the
|
|
436
|
+
// DCB queue's own flavor.
|
|
437
|
+
let (commandQueueUrl, commandQueueIsFifo) = switch target {
|
|
438
|
+
| Some({queueUrl, isFifo}) => (queueUrl, isFifo)
|
|
439
|
+
| None => (dcbQueueUrl, dcbQueueIsFifo.contents)
|
|
440
|
+
}
|
|
383
441
|
let handlerJson =
|
|
384
|
-
Pulumi.Output.all3((info.queryDbTableName,
|
|
442
|
+
Pulumi.Output.all3((info.queryDbTableName, commandQueueUrl, sourceUrnsFor(info)))
|
|
385
443
|
->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
|
|
386
444
|
let urnsJson =
|
|
387
445
|
urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify
|
|
@@ -389,11 +447,20 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
389
447
|
// an entry point that predates multi-source still routes its first
|
|
390
448
|
// stream rather than none.
|
|
391
449
|
let firstUrn = urns->Array.get(0)->Option.getOr("")
|
|
392
|
-
|
|
450
|
+
let fifoJson = commandQueueIsFifo ? "true" : "false"
|
|
451
|
+
`{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","commandQueueIsFifo":${fifoJson},"sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
|
|
393
452
|
})
|
|
394
453
|
let _ = handlerOutputs->Array.push(handlerJson)
|
|
395
454
|
})
|
|
396
455
|
|
|
456
|
+
// The fallback publishers' grant. Every slice that resolved no target
|
|
457
|
+
// publishes to the plugin's DCB command topic, whose queue was captured
|
|
458
|
+
// by `setDcbQueueUrl` — without this the role has no `sqs:SendMessage`
|
|
459
|
+
// there and every such publish is rejected.
|
|
460
|
+
if anyDcbFallback.contents {
|
|
461
|
+
dcbQueueResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
|
|
462
|
+
}
|
|
463
|
+
|
|
397
464
|
let handlerConfigOutput =
|
|
398
465
|
Pulumi.Output.all(handlerOutputs)
|
|
399
466
|
->Pulumi.Output.apply(handlers => `{"handlers":[${handlers->Array.join(",")}]}`)
|
|
@@ -8,6 +8,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
|
8
8
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
9
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
10
10
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
11
|
+
import * as CommandTopicRegistry$ReventlessAws from "../CommandTopic/CommandTopicRegistry.res.mjs";
|
|
11
12
|
import * as PluginRuntime_Builder$ReventlessAws from "../../plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
12
13
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
|
|
13
14
|
import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
|
|
@@ -20,15 +21,28 @@ let dcbQueueUrlRef = {
|
|
|
20
21
|
contents: undefined
|
|
21
22
|
};
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
let dcbQueueIsFifo = {
|
|
25
|
+
contents: false
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let dcbQueueResources = [];
|
|
29
|
+
|
|
30
|
+
function setDcbQueueUrl(isFifoOpt, resource, url) {
|
|
31
|
+
let isFifo = isFifoOpt !== undefined ? isFifoOpt : false;
|
|
32
|
+
if (Stdlib_Option.isNone(dcbQueueUrlRef.contents)) {
|
|
33
|
+
dcbQueueUrlRef.contents = url;
|
|
34
|
+
dcbQueueIsFifo.contents = isFifo;
|
|
35
|
+
}
|
|
36
|
+
Stdlib_Option.forEach(resource, r => {
|
|
37
|
+
dcbQueueResources.push(r);
|
|
38
|
+
});
|
|
25
39
|
}
|
|
26
40
|
|
|
27
41
|
function getDcbQueueUrl() {
|
|
28
42
|
return dcbQueueUrlRef.contents;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt) {
|
|
45
|
+
function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackTypeOpt, queryDbTableName, queryDbResourcesOpt, context, sourceTopicsOpt, consumesDcbLogOpt, commandTargetName) {
|
|
32
46
|
let callbackType = callbackTypeOpt !== undefined ? callbackTypeOpt : "automation";
|
|
33
47
|
let queryDbResources = queryDbResourcesOpt !== undefined ? queryDbResourcesOpt : [];
|
|
34
48
|
let sourceTopics = sourceTopicsOpt !== undefined ? sourceTopicsOpt : ({});
|
|
@@ -41,7 +55,8 @@ function registerAutomationSlice(name, specModulePath, bodyModulePath, callbackT
|
|
|
41
55
|
queryDbResources: queryDbResources,
|
|
42
56
|
context: context,
|
|
43
57
|
sourceTopics: sourceTopics,
|
|
44
|
-
consumesDcbLog: consumesDcbLog
|
|
58
|
+
consumesDcbLog: consumesDcbLog,
|
|
59
|
+
commandTargetName: commandTargetName
|
|
45
60
|
};
|
|
46
61
|
}
|
|
47
62
|
|
|
@@ -195,10 +210,19 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
195
210
|
let handlerOutputs = [];
|
|
196
211
|
let packageDirs = {};
|
|
197
212
|
let allQueryDbResources = [];
|
|
213
|
+
let anyDcbFallback = {
|
|
214
|
+
contents: false
|
|
215
|
+
};
|
|
198
216
|
Stdlib_Dict.forEachWithKey(bundledInfos, (info, _name) => {
|
|
199
217
|
info.queryDbResources.forEach(r => {
|
|
200
218
|
allQueryDbResources.push(r);
|
|
201
219
|
});
|
|
220
|
+
let target = Stdlib_Option.flatMap(info.commandTargetName, CommandTopicRegistry$ReventlessAws.get);
|
|
221
|
+
if (target !== undefined) {
|
|
222
|
+
allQueryDbResources.push(target.resource);
|
|
223
|
+
} else {
|
|
224
|
+
anyDcbFallback.contents = true;
|
|
225
|
+
}
|
|
202
226
|
let pkg = Util_Bundle$ReventlessAws.extractPackageName(info.specModulePath);
|
|
203
227
|
packageDirs[pkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, pkg);
|
|
204
228
|
let bodyPkg = Util_Bundle$ReventlessAws.extractPackageName(info.bodyModulePath);
|
|
@@ -208,18 +232,32 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
208
232
|
let callbackType = Stdlib_Option.getOr(JSON.stringify(info.callbackType), `""`);
|
|
209
233
|
let context = info.context;
|
|
210
234
|
let contextFragment = context !== undefined ? Stdlib_Option.getOr(Stdlib_Option.map(JSON.stringify(context), json => `,"context":` + json), "") : "";
|
|
235
|
+
let match = target !== undefined ? [
|
|
236
|
+
target.queueUrl,
|
|
237
|
+
target.isFifo
|
|
238
|
+
] : [
|
|
239
|
+
dcbQueueUrl,
|
|
240
|
+
dcbQueueIsFifo.contents
|
|
241
|
+
];
|
|
242
|
+
let commandQueueIsFifo = match[1];
|
|
211
243
|
let handlerJson = Pulumi.all([
|
|
212
244
|
info.queryDbTableName,
|
|
213
|
-
|
|
245
|
+
match[0],
|
|
214
246
|
sourceUrnsFor(info)
|
|
215
247
|
]).apply(param => {
|
|
216
248
|
let urns = param[2];
|
|
217
249
|
let urnsJson = JSON.stringify(urns.map(prim => prim));
|
|
218
250
|
let firstUrn = Stdlib_Option.getOr(urns[0], "");
|
|
219
|
-
|
|
251
|
+
let fifoJson = commandQueueIsFifo ? "true" : "false";
|
|
252
|
+
return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","commandQueueIsFifo":` + fifoJson + `,"sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
|
|
220
253
|
});
|
|
221
254
|
handlerOutputs.push(handlerJson);
|
|
222
255
|
});
|
|
256
|
+
if (anyDcbFallback.contents) {
|
|
257
|
+
dcbQueueResources.forEach(r => {
|
|
258
|
+
allQueryDbResources.push(r);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
223
261
|
let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
|
|
224
262
|
let envVars = {};
|
|
225
263
|
envVars["HANDLER_CONFIG"] = handlerConfigOutput;
|
|
@@ -250,6 +288,8 @@ export {
|
|
|
250
288
|
log,
|
|
251
289
|
bundledInfos,
|
|
252
290
|
dcbQueueUrlRef,
|
|
291
|
+
dcbQueueIsFifo,
|
|
292
|
+
dcbQueueResources,
|
|
253
293
|
setDcbQueueUrl,
|
|
254
294
|
getDcbQueueUrl,
|
|
255
295
|
registerAutomationSlice,
|
|
@@ -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 {
|
|
@@ -65,9 +65,14 @@ module Make = (Api: {
|
|
|
65
65
|
~bodyModulePath=Util_Bundle.getModuleSpecifier(Translation.moduleUrl),
|
|
66
66
|
~callbackType="outbound",
|
|
67
67
|
~queryDbTableName,
|
|
68
|
+
// The target queue rides along so `connectLambda`'s existing
|
|
69
|
+
// `sqs:SendMessage` grant covers it. Without it the role can publish
|
|
70
|
+
// nowhere, and the command is lost after the geocoder has been paid for.
|
|
68
71
|
~queryDbResources=queryDbOutputs.resources,
|
|
69
72
|
~sourceTopics,
|
|
70
73
|
~consumesDcbLog,
|
|
74
|
+
// Only the name: the target Aggregate's CommandTopic does not exist yet.
|
|
75
|
+
~commandTargetName=?Spec.targetName,
|
|
71
76
|
)
|
|
72
77
|
|
|
73
78
|
ots
|
|
@@ -54,7 +54,7 @@ function Make(Api) {
|
|
|
54
54
|
let declaredSources = Spec.sourceNames;
|
|
55
55
|
let sourceTopics = EventTopic$ReventlessCore.filter(allEventTopics, Belt_SetString.fromArray(declaredSources));
|
|
56
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);
|
|
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, Spec.targetName);
|
|
58
58
|
return ots;
|
|
59
59
|
};
|
|
60
60
|
return {
|
|
@@ -150,16 +150,26 @@ describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler", () => {
|
|
|
150
150
|
~publishJsons=noopPublish,
|
|
151
151
|
~syncTodoItems=async () => steps->Array.push("sync"),
|
|
152
152
|
)
|
|
153
|
+
// `id` is the envelope's subject — the entity the event was published for.
|
|
154
|
+
// An Aggregate's event payload does not repeat it, so this is the only place
|
|
155
|
+
// `collect` can learn which customer/order an event belongs to.
|
|
153
156
|
let placed = obj([
|
|
157
|
+
("id", str("ord-1")),
|
|
154
158
|
("meta", obj([("service", str("Order"))])),
|
|
155
159
|
("event", obj([("TAG", str("OrderPlaced")), ("orderId", str("ord-1"))])),
|
|
156
160
|
])
|
|
157
161
|
// Not consumed by this slice — dropped without an error.
|
|
158
162
|
let ignored = obj([("event", obj([("TAG", str("OrderShipped")), ("orderId", str("ord-1"))]))])
|
|
159
|
-
// Bare payload-less variant, no envelope.
|
|
163
|
+
// Bare payload-less variant, no envelope and therefore no id.
|
|
160
164
|
let archived = str("OrderArchived")
|
|
161
165
|
await Stream.fromIterable([placed, ignored, archived])->handler->Effect.runPromise
|
|
162
|
-
|
|
166
|
+
// Pairs, not bare events. Asserting the bare shape is what let the AWS entry
|
|
167
|
+
// point ship a phase1 that destructured `undefined` out of a non-tuple: the
|
|
168
|
+
// test agreed with the drifted local type instead of with the callback.
|
|
169
|
+
expect(received.contents)->toEqual([
|
|
170
|
+
("ord-1", OrderPlaced({orderId: "ord-1"})),
|
|
171
|
+
("", OrderArchived),
|
|
172
|
+
])
|
|
163
173
|
expect(steps)->toEqual(["phase1", "sync", "phase2", "sync"])
|
|
164
174
|
})
|
|
165
175
|
})
|
|
@@ -216,6 +216,10 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
|
|
|
216
216
|
steps.push("sync");
|
|
217
217
|
});
|
|
218
218
|
let placed = Object.fromEntries([
|
|
219
|
+
[
|
|
220
|
+
"id",
|
|
221
|
+
"ord-1"
|
|
222
|
+
],
|
|
219
223
|
[
|
|
220
224
|
"meta",
|
|
221
225
|
Object.fromEntries([[
|
|
@@ -256,11 +260,17 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
|
|
|
256
260
|
"OrderArchived"
|
|
257
261
|
])));
|
|
258
262
|
globalThis.expect(received.contents).toEqual([
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
[
|
|
264
|
+
"ord-1",
|
|
265
|
+
{
|
|
266
|
+
TAG: "OrderPlaced",
|
|
267
|
+
orderId: "ord-1"
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
[
|
|
271
|
+
"",
|
|
272
|
+
"OrderArchived"
|
|
273
|
+
]
|
|
264
274
|
]);
|
|
265
275
|
globalThis.expect(steps).toEqual([
|
|
266
276
|
"phase1",
|