@reventlessdev/reventless-aws 3.0.0-alpha.172 → 3.0.0-alpha.174
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 +20 -0
- package/package.json +6 -6
- package/src/Platform.res +77 -7
- package/src/Platform.res.mjs +82 -8
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
- package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +188 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +141 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +72 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +40 -0
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
- package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
- package/src/components/Api/AppSync_Adapter.res +72 -8
- package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
- package/src/components/Plugin.res +1 -1
- package/src/components/Plugin.res.mjs +2 -2
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +10 -2
- package/tests/AppSync_AdapterTest.res +140 -0
- package/tests/AppSync_AdapterTest.res.mjs +182 -6
- package/tests/PgChangeFeedRelay_RuntimeTest.res +75 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +135 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Deploy-time DCB EventLog storage for the Postgres backend (B2.3c).
|
|
2
|
+
//
|
|
3
|
+
// Creates NO DynamoDB table and NO stream: DCB events live in Postgres (`dcb_event`)
|
|
4
|
+
// and the `PgChangeFeedRelay` drives propagation. Returns:
|
|
5
|
+
// - `resources: []` — so the EventCollector wires no DynamoDB-stream ESM for this
|
|
6
|
+
// log (the DCB EventTopic's `DynamoDbStream` publisher becomes a passive marker
|
|
7
|
+
// with nothing to subscribe to), and
|
|
8
|
+
// - `operations` — the runtime DCB op set bound to the shared `PgConnection` pool.
|
|
9
|
+
//
|
|
10
|
+
// Selected by `DcbEventLogStorage.Selectable` when `DcbBackend` holds a selection.
|
|
11
|
+
// The pool is lazy (`PgRuntime.poolFor`), so binding ops inside the deploy-time
|
|
12
|
+
// `Output.apply` opens no connection during `pulumi up` — a real query only runs
|
|
13
|
+
// inside the DcbCommandTopic Lambda at runtime. See
|
|
14
|
+
// docs/plans/aws-postgres-change-feed-bridge.md.
|
|
15
|
+
|
|
16
|
+
let make: ReventlessCore.DcbEventLog_Adapter.storageMaker = (
|
|
17
|
+
~name,
|
|
18
|
+
~indexes as _,
|
|
19
|
+
~partitionTag,
|
|
20
|
+
~crossPartitionTagKeys as _=[],
|
|
21
|
+
~opts as _,
|
|
22
|
+
) => {
|
|
23
|
+
// B2.3d: `name` is the canonical `dcb_event.log_name` (`<plugin>DcbEventLog`).
|
|
24
|
+
// Register it with its partition tag so `makePlatform` can wire the change-feed
|
|
25
|
+
// relay; the collector queue is attached later in forPluginEventCollector.
|
|
26
|
+
DcbBackend.registerRelayLog(~logName=name, ~partitionTag)
|
|
27
|
+
|
|
28
|
+
let operations = switch DcbBackend.get() {
|
|
29
|
+
| Some({connectionConfig}) =>
|
|
30
|
+
connectionConfig->Pulumi.Output.apply(config =>
|
|
31
|
+
DcbEventLogStorage_Postgres_Runtime.opsFor(config, ~logName=name)
|
|
32
|
+
)
|
|
33
|
+
| None =>
|
|
34
|
+
// Selectable only routes here when a selection is set; guard defensively.
|
|
35
|
+
JsError.throwWithMessage(
|
|
36
|
+
"DcbEventLogStorage_Postgres.make called without a DcbBackend selection",
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
{
|
|
40
|
+
ReventlessCore.DcbEventLog_Adapter.resources: [],
|
|
41
|
+
operations,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
4
|
+
import * as DcbBackend$ReventlessAws from "./DcbBackend.res.mjs";
|
|
5
|
+
import * as DcbEventLogStorage_Postgres_Runtime$ReventlessAws from "./DcbEventLogStorage_Postgres_Runtime.res.mjs";
|
|
6
|
+
|
|
7
|
+
function make(name, param, partitionTag, $staropt$star, param$1) {
|
|
8
|
+
$staropt$star !== undefined;
|
|
9
|
+
DcbBackend$ReventlessAws.registerRelayLog(name, partitionTag);
|
|
10
|
+
let match = DcbBackend$ReventlessAws.get();
|
|
11
|
+
let operations = match !== undefined ? match.connectionConfig.apply(config => DcbEventLogStorage_Postgres_Runtime$ReventlessAws.opsFor(config, name, undefined)) : Stdlib_JsError.throwWithMessage("DcbEventLogStorage_Postgres.make called without a DcbBackend selection");
|
|
12
|
+
return {
|
|
13
|
+
resources: [],
|
|
14
|
+
operations: operations
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
make,
|
|
20
|
+
}
|
|
21
|
+
/* DcbEventLogStorage_Postgres_Runtime-ReventlessAws Not a pure module */
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// AWS-side runtime DcbEventLog operations for the Postgres backend. The DCB
|
|
2
|
+
// analogue of EventLogStorage_Postgres_Runtime: turn a resolved connection config
|
|
3
|
+
// into the DCB operation set (`read`/`append`/`readStream`), bound to the
|
|
4
|
+
// container-lifetime pool from PgRuntime. Consumed by DcbCommandTopicEntryPoint.mjs
|
|
5
|
+
// (Postgres branch, selected when a PG_CONNECTION is present in HANDLER_CONFIG).
|
|
6
|
+
//
|
|
7
|
+
// The pg maker ignores `~indexes` / `~partitionTag` / `~crossPartitionTagKeys`
|
|
8
|
+
// (Postgres evaluates the real `DcbTag.query` atomically — no GSI routing), so
|
|
9
|
+
// they are passed empty here. `logName` is the `dcb_event.log_name` discriminator.
|
|
10
|
+
|
|
11
|
+
let opsFor = (
|
|
12
|
+
config: PgConnection.connectionConfig,
|
|
13
|
+
~logName: string,
|
|
14
|
+
~lockStrategy: ReventlessPostgres.DcbEventLogStorage_Postgres.lockStrategy=#AdvisoryLocks,
|
|
15
|
+
): ReventlessCore.DcbEventLog_Adapter.operations => {
|
|
16
|
+
let pool = PgRuntime.poolFor(config)
|
|
17
|
+
let (_name, ops, _storage) = ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
|
|
18
|
+
~pool,
|
|
19
|
+
~name=logName,
|
|
20
|
+
~indexes=[],
|
|
21
|
+
~partitionTag=(),
|
|
22
|
+
~opts=(),
|
|
23
|
+
~lockStrategy,
|
|
24
|
+
)
|
|
25
|
+
ops
|
|
26
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
|
|
4
|
+
import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
|
|
5
|
+
|
|
6
|
+
function opsFor(config, logName, lockStrategyOpt) {
|
|
7
|
+
let lockStrategy = lockStrategyOpt !== undefined ? lockStrategyOpt : "AdvisoryLocks";
|
|
8
|
+
let pool = PgRuntime$ReventlessAws.poolFor(config);
|
|
9
|
+
return DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, logName, [], undefined, undefined, undefined, lockStrategy, undefined)[1];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
opsFor,
|
|
14
|
+
}
|
|
15
|
+
/* PgRuntime-ReventlessAws Not a pure module */
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// AWS-side runtime EventLog operations for the Postgres backend. Mirrors the
|
|
2
|
+
// role of EventLogStorage_DynamoDb_Runtime: turn a resolved connection config
|
|
3
|
+
// into the classic EventLog operation set, bound to the container-lifetime pool
|
|
4
|
+
// from PgRuntime. Consumed by AggregateEntryPoint.mjs (Postgres branch, selected
|
|
5
|
+
// when a PG_CONNECTION is present in HANDLER_CONFIG).
|
|
6
|
+
//
|
|
7
|
+
// Unlike DynamoDB (table-per-aggregate), all aggregates share the single
|
|
8
|
+
// Postgres `event_log` table, discriminated by `log_name`. `logName` is that
|
|
9
|
+
// per-aggregate discriminator — the same stable identifier the builder would
|
|
10
|
+
// otherwise use as the DynamoDB table name.
|
|
11
|
+
|
|
12
|
+
let opsFor = (
|
|
13
|
+
config: PgConnection.connectionConfig,
|
|
14
|
+
~logName: string,
|
|
15
|
+
): ReventlessCore.EventLog_Adapter.operations => {
|
|
16
|
+
let pool = PgRuntime.poolFor(config)
|
|
17
|
+
let (_name, ops, _storage) = ReventlessPostgres.EventLogStorage_Postgres.makeStorage(
|
|
18
|
+
~pool,
|
|
19
|
+
~name=logName,
|
|
20
|
+
~opts=(),
|
|
21
|
+
)
|
|
22
|
+
ops
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as PgRuntime$ReventlessAws from "../Postgres/PgRuntime.res.mjs";
|
|
4
|
+
import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
|
|
5
|
+
|
|
6
|
+
function opsFor(config, logName) {
|
|
7
|
+
let pool = PgRuntime$ReventlessAws.poolFor(config);
|
|
8
|
+
return EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, logName, undefined, undefined)[1];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
opsFor,
|
|
13
|
+
}
|
|
14
|
+
/* PgRuntime-ReventlessAws Not a pure module */
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Deploy-time builder for the Postgres change-feed relay (B2.3b).
|
|
2
|
+
//
|
|
3
|
+
// Provisions a scheduled, in-VPC Lambda that drains each Postgres DCB log via
|
|
4
|
+
// PgChangeFeed and relays events to the plugin EventCollector SQS queue — which
|
|
5
|
+
// fans out to read-model projections, aggregate command topics, and the
|
|
6
|
+
// cross-plugin SNS EventTopic. The platform (D1, B2.3c) calls this with the set of
|
|
7
|
+
// Postgres-backed DCB logs and points the EventCollector's event source at the
|
|
8
|
+
// relay-fed queue.
|
|
9
|
+
//
|
|
10
|
+
// NOTE: EventBridge rate rules have a 1-minute floor, so v1 poll latency is
|
|
11
|
+
// >= 1 minute. Sub-minute latency needs a self-invoking loop / EventBridge
|
|
12
|
+
// Scheduler (follow-up) or the Fargate-LISTEN upgrade.
|
|
13
|
+
|
|
14
|
+
// One Postgres-backed DCB log to relay.
|
|
15
|
+
type relayLog = {
|
|
16
|
+
connectionConfig: Pulumi.Output.t<PgConnection.connectionConfig>,
|
|
17
|
+
/** dcb_event.log_name — also the DCB `event_log` discriminator. */
|
|
18
|
+
logName: string,
|
|
19
|
+
/** dcb_subscription checkpoint key for this relay. */
|
|
20
|
+
subscriber: string,
|
|
21
|
+
/** The DCB log's partition tag — sury-encoded into HANDLER_CONFIG so the relay
|
|
22
|
+
computes the same `id` (partition key) the DynamoDB-stream path would. */
|
|
23
|
+
partitionTag: Reventless.DcbTag.derivedPartitionTag,
|
|
24
|
+
/** Plugin EventCollector SQS queue this log's events are relayed to. */
|
|
25
|
+
targetQueueUrl: Pulumi.Output.t<string>,
|
|
26
|
+
targetQueueArn: Pulumi.Output.t<string>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ~securityGroupId: DB-access SG the relay Lambda attaches (PgConnection.securityGroupId).
|
|
30
|
+
// ~subnetIds: private subnets for the relay Lambda (PgConnection.subnetIds).
|
|
31
|
+
let make = (
|
|
32
|
+
~name: string,
|
|
33
|
+
~logs: array<relayLog>,
|
|
34
|
+
~securityGroupId: Pulumi.Output.t<string>,
|
|
35
|
+
~subnetIds: array<Pulumi.Input.t<string>>,
|
|
36
|
+
~intervalMinutes: int=1,
|
|
37
|
+
~opts=?,
|
|
38
|
+
): array<ReventlessInfra.Adapter.resource> => {
|
|
39
|
+
open PulumiAws
|
|
40
|
+
let customOpts =
|
|
41
|
+
opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
|
|
42
|
+
|
|
43
|
+
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
44
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs",
|
|
45
|
+
~packageDirs=Dict.make(),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// HANDLER_CONFIG.logs[], serialized from the deploy-time-resolved Outputs.
|
|
49
|
+
let logJsonOutputs = logs->Array.map(l =>
|
|
50
|
+
(l.connectionConfig, l.targetQueueUrl)
|
|
51
|
+
->Pulumi.Output.all2
|
|
52
|
+
->Pulumi.Output.apply(((cc, queueUrl)) => {
|
|
53
|
+
let pgConnectionJson =
|
|
54
|
+
[
|
|
55
|
+
("host", cc.host->JSON.Encode.string),
|
|
56
|
+
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
57
|
+
("database", cc.database->JSON.Encode.string),
|
|
58
|
+
("username", cc.username->JSON.Encode.string),
|
|
59
|
+
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
60
|
+
]
|
|
61
|
+
->Dict.fromArray
|
|
62
|
+
->JSON.Encode.object
|
|
63
|
+
[
|
|
64
|
+
("pgConnection", pgConnectionJson),
|
|
65
|
+
("logName", l.logName->JSON.Encode.string),
|
|
66
|
+
("subscriber", l.subscriber->JSON.Encode.string),
|
|
67
|
+
(
|
|
68
|
+
"partitionTag",
|
|
69
|
+
l.partitionTag->S.reverseConvertToJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema),
|
|
70
|
+
),
|
|
71
|
+
("targetQueueUrl", queueUrl->JSON.Encode.string),
|
|
72
|
+
]
|
|
73
|
+
->Dict.fromArray
|
|
74
|
+
->JSON.Encode.object
|
|
75
|
+
->JSON.stringify
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
let handlerConfigOutput =
|
|
79
|
+
Pulumi.Output.all(logJsonOutputs)->Pulumi.Output.apply(items =>
|
|
80
|
+
`{"logs":[${items->Array.join(",")}]}`
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
let envVars = Dict.fromArray([("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)])
|
|
84
|
+
|
|
85
|
+
let vpcConfig =
|
|
86
|
+
securityGroupId
|
|
87
|
+
->Pulumi.Output.apply(sgId =>
|
|
88
|
+
(
|
|
89
|
+
{
|
|
90
|
+
Lambda.Function.subnetIds: subnetIds->Pulumi.Input.make,
|
|
91
|
+
securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
|
|
92
|
+
}: Lambda.Function.vpcConfig
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
->Pulumi.Output.asInput
|
|
96
|
+
|
|
97
|
+
let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
|
|
98
|
+
~name,
|
|
99
|
+
~code,
|
|
100
|
+
~sourceCodeHash,
|
|
101
|
+
~envVars,
|
|
102
|
+
~vpcConfig,
|
|
103
|
+
~opts?,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
// IAM: GetSecretValue on each log's DB secret + SendMessage on each target queue.
|
|
107
|
+
let secretArns =
|
|
108
|
+
logs->Array.map(l => l.connectionConfig->Pulumi.Output.apply(cc => cc.secretArn))
|
|
109
|
+
let queueArns = logs->Array.map(l => l.targetQueueArn)
|
|
110
|
+
let _iam =
|
|
111
|
+
(Pulumi.Output.all(secretArns), Pulumi.Output.all(queueArns), runtime.parts.lambdaRole.id)
|
|
112
|
+
->Pulumi.Output.all3
|
|
113
|
+
->Pulumi.Output.apply(((secretArns, queueArns, roleId)) => {
|
|
114
|
+
open PulumiAws.PolicyDocument
|
|
115
|
+
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
116
|
+
~name=`${name}RelayAccess`,
|
|
117
|
+
~args={
|
|
118
|
+
policy: PulumiAws.PolicyDocument.make(
|
|
119
|
+
~id=`${name}RelayAccessPolicy`,
|
|
120
|
+
~statements=[
|
|
121
|
+
{
|
|
122
|
+
sid: "AllowGetSecret",
|
|
123
|
+
effect: Allow,
|
|
124
|
+
actions: Action("secretsmanager:GetSecretValue"),
|
|
125
|
+
resources: Resources(secretArns),
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
sid: "AllowSendSqs",
|
|
129
|
+
effect: Allow,
|
|
130
|
+
actions: Action("sqs:SendMessage"),
|
|
131
|
+
resources: Resources(queueArns),
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
)
|
|
135
|
+
->PulumiAws.PolicyDocument.toJsonString
|
|
136
|
+
->Pulumi.Input.make,
|
|
137
|
+
role: roleId->Pulumi.Input.make,
|
|
138
|
+
},
|
|
139
|
+
)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Scheduled poll: EventRule (rate) → Permission → EventTarget.
|
|
143
|
+
let rule = {
|
|
144
|
+
open PulumiAws.Cloudwatch
|
|
145
|
+
EventRule.make(
|
|
146
|
+
~name=`${Pulumi.Pulumi.getStackName()}-${name}`,
|
|
147
|
+
~args={
|
|
148
|
+
description: "Poll Postgres DCB change feed and relay to EventCollector"->Pulumi.Input.make,
|
|
149
|
+
scheduleExpression: EventRule.ScheduleExpression.every(Minutes(intervalMinutes)),
|
|
150
|
+
},
|
|
151
|
+
~opts=?customOpts,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let lambda = runtime.parts.lambda
|
|
156
|
+
let _wire =
|
|
157
|
+
(lambda->Pulumi.Output.flatMap(l => l.arn), lambda->Pulumi.Output.flatMap(l => l.name))
|
|
158
|
+
->Pulumi.Output.all2
|
|
159
|
+
->Pulumi.Output.apply(((lambdaArn, lambdaName)) => {
|
|
160
|
+
let _permission = PulumiAws.Lambda.Permission.make(
|
|
161
|
+
~name=`${name}SchedulePermission`,
|
|
162
|
+
~args={
|
|
163
|
+
action: "lambda:InvokeFunction",
|
|
164
|
+
function: lambdaName->Pulumi.Input.make,
|
|
165
|
+
principal: AWS.CloudwatchEventRule.principal,
|
|
166
|
+
},
|
|
167
|
+
~opts=?customOpts,
|
|
168
|
+
)
|
|
169
|
+
let _target = {
|
|
170
|
+
open PulumiAws.Cloudwatch
|
|
171
|
+
EventTarget.make(
|
|
172
|
+
~name,
|
|
173
|
+
~args={
|
|
174
|
+
rule: EventTarget.Rule.ofEventRule(rule),
|
|
175
|
+
arn: lambdaArn->Pulumi.Input.make,
|
|
176
|
+
},
|
|
177
|
+
~opts=?customOpts,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
[
|
|
183
|
+
lambda
|
|
184
|
+
->Pulumi.Output.apply(l => l->Util_Lambda.toResource)
|
|
185
|
+
->ReventlessCore.Adapter.outputToResource,
|
|
186
|
+
rule->Util_Cloudwatch.EventRule.toResource,
|
|
187
|
+
]
|
|
188
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Aws from "@pulumi/aws";
|
|
5
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
8
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
9
|
+
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
10
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
11
|
+
import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
|
|
12
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
13
|
+
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
14
|
+
import * as Util_Lambda$ReventlessAws from "../../util/Util_Lambda.res.mjs";
|
|
15
|
+
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
16
|
+
import * as Util_Cloudwatch$ReventlessAws from "../../util/Util_Cloudwatch.res.mjs";
|
|
17
|
+
import * as Cloudwatch_EventRule$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventRule.res.mjs";
|
|
18
|
+
import * as Cloudwatch_EventTarget$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventTarget.res.mjs";
|
|
19
|
+
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
20
|
+
|
|
21
|
+
function make(name, logs, securityGroupId, subnetIds, intervalMinutesOpt, opts) {
|
|
22
|
+
let intervalMinutes = intervalMinutesOpt !== undefined ? intervalMinutesOpt : 1;
|
|
23
|
+
let customOpts = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
|
|
24
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs", {}, undefined);
|
|
25
|
+
let logJsonOutputs = logs.map(l => Pulumi.all([
|
|
26
|
+
l.connectionConfig,
|
|
27
|
+
l.targetQueueUrl
|
|
28
|
+
]).apply(param => {
|
|
29
|
+
let cc = param[0];
|
|
30
|
+
let pgConnectionJson = Object.fromEntries([
|
|
31
|
+
[
|
|
32
|
+
"host",
|
|
33
|
+
cc.host
|
|
34
|
+
],
|
|
35
|
+
[
|
|
36
|
+
"port",
|
|
37
|
+
cc.port
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
"database",
|
|
41
|
+
cc.database
|
|
42
|
+
],
|
|
43
|
+
[
|
|
44
|
+
"username",
|
|
45
|
+
cc.username
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
"secretArn",
|
|
49
|
+
cc.secretArn
|
|
50
|
+
]
|
|
51
|
+
]);
|
|
52
|
+
return JSON.stringify(Object.fromEntries([
|
|
53
|
+
[
|
|
54
|
+
"pgConnection",
|
|
55
|
+
pgConnectionJson
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"logName",
|
|
59
|
+
l.logName
|
|
60
|
+
],
|
|
61
|
+
[
|
|
62
|
+
"subscriber",
|
|
63
|
+
l.subscriber
|
|
64
|
+
],
|
|
65
|
+
[
|
|
66
|
+
"partitionTag",
|
|
67
|
+
S.reverseConvertToJsonOrThrow(l.partitionTag, DcbTag$Reventless.derivedPartitionTagSchema)
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
"targetQueueUrl",
|
|
71
|
+
param[1]
|
|
72
|
+
]
|
|
73
|
+
]));
|
|
74
|
+
}));
|
|
75
|
+
let handlerConfigOutput = Pulumi.all(logJsonOutputs).apply(items => `{"logs":[` + items.join(",") + `]}`);
|
|
76
|
+
let envVars = Object.fromEntries([[
|
|
77
|
+
"HANDLER_CONFIG",
|
|
78
|
+
handlerConfigOutput
|
|
79
|
+
]]);
|
|
80
|
+
let vpcConfig = securityGroupId.apply(sgId => ({
|
|
81
|
+
subnetIds: subnetIds,
|
|
82
|
+
securityGroupIds: [sgId]
|
|
83
|
+
}));
|
|
84
|
+
let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, undefined, undefined, undefined, undefined, undefined, undefined, vpcConfig, opts);
|
|
85
|
+
let secretArns = logs.map(l => l.connectionConfig.apply(cc => cc.secretArn));
|
|
86
|
+
let queueArns = logs.map(l => l.targetQueueArn);
|
|
87
|
+
Pulumi.all([
|
|
88
|
+
Pulumi.all(secretArns),
|
|
89
|
+
Pulumi.all(queueArns),
|
|
90
|
+
runtime.parts.lambdaRole.id
|
|
91
|
+
]).apply(param => {
|
|
92
|
+
new (Aws.iam.RolePolicy)(name + `RelayAccess`, {
|
|
93
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `RelayAccessPolicy`, [
|
|
94
|
+
{
|
|
95
|
+
Sid: "AllowGetSecret",
|
|
96
|
+
Effect: "Allow",
|
|
97
|
+
Action: "secretsmanager:GetSecretValue",
|
|
98
|
+
Resource: param[0]
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
Sid: "AllowSendSqs",
|
|
102
|
+
Effect: "Allow",
|
|
103
|
+
Action: "sqs:SendMessage",
|
|
104
|
+
Resource: param[1]
|
|
105
|
+
}
|
|
106
|
+
])),
|
|
107
|
+
role: param[2]
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
let rule = new (Aws.cloudwatch.EventRule)(Pulumi.getStack() + `-` + name, {
|
|
111
|
+
description: "Poll Postgres DCB change feed and relay to EventCollector",
|
|
112
|
+
scheduleExpression: Primitive_option.some(Cloudwatch_EventRule$PulumiAws.ScheduleExpression.every({
|
|
113
|
+
TAG: "Minutes",
|
|
114
|
+
_0: intervalMinutes
|
|
115
|
+
}))
|
|
116
|
+
}, customOpts !== undefined ? Primitive_option.valFromOption(customOpts) : undefined);
|
|
117
|
+
let lambda = runtime.parts.lambda;
|
|
118
|
+
Pulumi.all([
|
|
119
|
+
Output$Pulumi.flatMap(lambda, l => l.arn),
|
|
120
|
+
Output$Pulumi.flatMap(lambda, l => l.name)
|
|
121
|
+
]).apply(param => {
|
|
122
|
+
new (Aws.lambda.Permission)(name + `SchedulePermission`, {
|
|
123
|
+
action: "lambda:InvokeFunction",
|
|
124
|
+
function: param[1],
|
|
125
|
+
principal: AWS$ReventlessAws.CloudwatchEventRule.principal
|
|
126
|
+
}, customOpts !== undefined ? Primitive_option.valFromOption(customOpts) : undefined);
|
|
127
|
+
new (Aws.cloudwatch.EventTarget)(name, {
|
|
128
|
+
rule: Cloudwatch_EventTarget$PulumiAws.Rule.ofEventRule(rule),
|
|
129
|
+
arn: param[0]
|
|
130
|
+
}, customOpts !== undefined ? Primitive_option.valFromOption(customOpts) : undefined);
|
|
131
|
+
});
|
|
132
|
+
return [
|
|
133
|
+
Adapter$ReventlessCore.outputToResource(lambda.apply(l => Util_Lambda$ReventlessAws.toResource(undefined, l))),
|
|
134
|
+
Util_Cloudwatch$ReventlessAws.EventRule.toResource(rule)
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
make,
|
|
140
|
+
}
|
|
141
|
+
/* S Not a pure module */
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Postgres change-feed relay runtime (B2.2).
|
|
2
|
+
//
|
|
3
|
+
// Drains a Postgres DCB log via `PgChangeFeed` and pushes each event — transformed
|
|
4
|
+
// into the `{id, meta, event}` shape the plugin EventCollector SQS handler
|
|
5
|
+
// consumes — onto that queue. The plugin EventCollector (`AdminEventCollectorEntryPoint`)
|
|
6
|
+
// then drives the full fan-out: read-model projections, aggregate command topics,
|
|
7
|
+
// and the cross-plugin SNS EventTopic. See
|
|
8
|
+
// docs/plans/aws-postgres-change-feed-bridge.md.
|
|
9
|
+
//
|
|
10
|
+
// The transform reuses the canonical DynamoDB shape producers
|
|
11
|
+
// (`derivePartitionKey` + `buildJsonEvent'`) so the emitted body is byte-identical
|
|
12
|
+
// to the DynamoDB-stream path — the SQS handler cannot tell the difference.
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Transform one feed event into the EventCollector JSON body. Rebuilds the same
|
|
16
|
+
* unmarshalled-item dict the DCB DynamoDB-stream decoder would see (id from the
|
|
17
|
+
* partition tag, position, event type, data, decomposed meta) and runs it through
|
|
18
|
+
* `buildJsonEvent'` — the exact decoder the SQS handler expects. Returns `None`
|
|
19
|
+
* for a malformed event (mirrors `buildJsonEvent'`).
|
|
20
|
+
*/
|
|
21
|
+
let toEventCollectorJson = (
|
|
22
|
+
event: ReventlessCore.DcbEventLog_Adapter.rawSequencedEvent,
|
|
23
|
+
~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>=?,
|
|
24
|
+
): option<JSON.t> => {
|
|
25
|
+
let item = Dict.make()
|
|
26
|
+
item->Dict.set(
|
|
27
|
+
"id",
|
|
28
|
+
DcbEventLogStorage_DynamoDb_Runtime.derivePartitionKey(~partitionTag?, event.tags)
|
|
29
|
+
->JSON.Encode.string,
|
|
30
|
+
)
|
|
31
|
+
item->Dict.set("position", event.position->JSON.Encode.string)
|
|
32
|
+
item->Dict.set("event", event.eventType->JSON.Encode.string)
|
|
33
|
+
item->Dict.set("data", event.data)
|
|
34
|
+
ReventlessCore.Message.decomposeMeta(event.meta)->Array.forEach(((key, value)) =>
|
|
35
|
+
item->Dict.set(key, value)
|
|
36
|
+
)
|
|
37
|
+
Util_DynamoDbStream_Runtime.buildJsonEvent'(item)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Drain a Postgres DCB log from its checkpoint and relay its events to the
|
|
42
|
+
* EventCollector via the injected `sendBatch`. `sendBatch` is injected (rather than
|
|
43
|
+
* hardcoding SQS) so the drain/transform is unit-testable; the relay entry point
|
|
44
|
+
* wires it to the actual `SendMessage`. Returns the number of events processed.
|
|
45
|
+
*
|
|
46
|
+
* At-least-once by construction: `PgChangeFeed.drain` replays the last page on a
|
|
47
|
+
* crash before checkpoint, and EventCollector projections are idempotent
|
|
48
|
+
* (event-sourced), so re-delivery is safe.
|
|
49
|
+
*/
|
|
50
|
+
let relay = async (
|
|
51
|
+
~config: PgConnection.connectionConfig,
|
|
52
|
+
~logName: string,
|
|
53
|
+
~subscriber: string,
|
|
54
|
+
// ~partitionTagJson: the DCB log's partition tag, sury-encoded into HANDLER_CONFIG
|
|
55
|
+
// by the relay builder (B2.3d). Parsed once via the shared derivedPartitionTagSchema
|
|
56
|
+
// so the `id` the relay emits matches the DynamoDB-stream path. Absent → None (a
|
|
57
|
+
// single-tag log derives the same id without it).
|
|
58
|
+
~partitionTagJson: option<JSON.t>=?,
|
|
59
|
+
~sendBatch: array<JSON.t> => promise<unit>,
|
|
60
|
+
): int => {
|
|
61
|
+
let partitionTag =
|
|
62
|
+
partitionTagJson->Option.map(json =>
|
|
63
|
+
json->S.parseJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema)
|
|
64
|
+
)
|
|
65
|
+
let pool = PgRuntime.poolFor(config)
|
|
66
|
+
await ReventlessPostgres.PgChangeFeed.drain(pool, ~logName, ~subscriber, ~handle=async events => {
|
|
67
|
+
let jsons = events->Array.filterMap(event => toEventCollectorJson(event, ~partitionTag?))
|
|
68
|
+
if jsons->Array.length > 0 {
|
|
69
|
+
await sendBatch(jsons)
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
7
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
8
|
+
import * as PgRuntime$ReventlessAws from "./PgRuntime.res.mjs";
|
|
9
|
+
import * as PgChangeFeed$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgChangeFeed.res.mjs";
|
|
10
|
+
import * as Util_DynamoDbStream_Runtime$ReventlessAws from "../../util/Util_DynamoDbStream_Runtime.res.mjs";
|
|
11
|
+
import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
12
|
+
|
|
13
|
+
function toEventCollectorJson(event, partitionTag) {
|
|
14
|
+
let item = {};
|
|
15
|
+
item["id"] = DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, event.tags);
|
|
16
|
+
item["position"] = event.position;
|
|
17
|
+
item["event"] = event.eventType;
|
|
18
|
+
item["data"] = event.data;
|
|
19
|
+
Message$ReventlessCore.decomposeMeta(event.meta).forEach(param => {
|
|
20
|
+
item[param[0]] = param[1];
|
|
21
|
+
});
|
|
22
|
+
return Util_DynamoDbStream_Runtime$ReventlessAws.buildJsonEvent$p(item);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function relay(config, logName, subscriber, partitionTagJson, sendBatch) {
|
|
26
|
+
let partitionTag = Stdlib_Option.map(partitionTagJson, json => S.parseJsonOrThrow(json, DcbTag$Reventless.derivedPartitionTagSchema));
|
|
27
|
+
let pool = PgRuntime$ReventlessAws.poolFor(config);
|
|
28
|
+
return await PgChangeFeed$ReventlessPostgres.drain(pool, logName, subscriber, undefined, async events => {
|
|
29
|
+
let jsons = Stdlib_Array.filterMap(events, event => toEventCollectorJson(event, partitionTag));
|
|
30
|
+
if (jsons.length !== 0) {
|
|
31
|
+
return await sendBatch(jsons);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
toEventCollectorJson,
|
|
38
|
+
relay,
|
|
39
|
+
}
|
|
40
|
+
/* S Not a pure module */
|
|
@@ -12,6 +12,7 @@ import { Make as commandTopicCallbackMake } from "@reventlessdev/reventless-core
|
|
|
12
12
|
import { makeGenerateCommand } from "@reventlessdev/reventless-core/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs";
|
|
13
13
|
import { commandOutcomeToJson, runInlineAndCollect } from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
|
|
14
14
|
import { append, replay, replayStream, appendStream } from "@reventlessdev/reventless-aws/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res.mjs";
|
|
15
|
+
import { opsFor as pgEventLogOpsFor } from "@reventlessdev/reventless-aws/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs";
|
|
15
16
|
import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
16
17
|
import { DynamoDBClient, ScanCommand } from "@aws-sdk/client-dynamodb";
|
|
17
18
|
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
@@ -118,15 +119,32 @@ async function checkPluginStatus(event) {
|
|
|
118
119
|
// Route 2 (SQS event source): the in-process command handler (used both as the
|
|
119
120
|
// inline dispatch target and as the SQS message handler) plus the resolved
|
|
120
121
|
// queue ref for the fire-and-forget async path.
|
|
121
|
-
|
|
122
|
+
// `pgConnection`, when present, is the resolved PgConnection.connectionConfig
|
|
123
|
+
// ({host,port,database,username,secretArn}) serialized into HANDLER_CONFIG by the
|
|
124
|
+
// runtime builder for a Postgres-backed aggregate. Its presence selects the
|
|
125
|
+
// Postgres EventLog runtime; absence keeps the DynamoDB path byte-identical.
|
|
126
|
+
// `eventLogTableName` doubles as the Postgres `event_log.log_name` discriminator
|
|
127
|
+
// (all aggregates share one Postgres table, unlike DynamoDB's table-per-aggregate).
|
|
128
|
+
function buildAggregateParts(specModule, behaviorModule, eventLogTableName, queueUrl, pgConnection) {
|
|
122
129
|
const patchedSpec = patchSpecId(specModule);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
let rawStorageOps;
|
|
131
|
+
if (pgConnection) {
|
|
132
|
+
const pgOps = pgEventLogOpsFor(pgConnection, eventLogTableName);
|
|
133
|
+
rawStorageOps = {
|
|
134
|
+
append: pgOps.append,
|
|
135
|
+
replay: pgOps.replay,
|
|
136
|
+
replayStream: pgOps.replayStream,
|
|
137
|
+
appendStream: pgOps.appendStream,
|
|
138
|
+
};
|
|
139
|
+
} else {
|
|
140
|
+
const resolvedTable = { name: eventLogTableName };
|
|
141
|
+
rawStorageOps = {
|
|
142
|
+
append: append(resolvedTable),
|
|
143
|
+
replay: replay(resolvedTable),
|
|
144
|
+
replayStream: replayStream(resolvedTable),
|
|
145
|
+
appendStream: appendStream(resolvedTable),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
130
148
|
const eventLogOps = eventLogOperationsMake(patchedSpec)({
|
|
131
149
|
Spec: patchedSpec,
|
|
132
150
|
EventTopic: { Spec: patchedSpec },
|
|
@@ -246,7 +264,7 @@ async function buildAllHandlers() {
|
|
|
246
264
|
await Promise.all(config.handlers.map(async h => {
|
|
247
265
|
const specModule = await dynamicImport(h.specModule);
|
|
248
266
|
const behaviorModule = await dynamicImport(h.behaviorModule);
|
|
249
|
-
const parts = buildAggregateParts(specModule, behaviorModule, h.eventLogTable, h.queueUrl);
|
|
267
|
+
const parts = buildAggregateParts(specModule, behaviorModule, h.eventLogTable, h.queueUrl, h.pgConnection);
|
|
250
268
|
cmdTopicHandlers[h.queueArn] = parts.sqsHandler;
|
|
251
269
|
cmdGenHandlers[specModule.name] = buildCommandGeneratorHandler(parts, DISPATCH_MODE);
|
|
252
270
|
}));
|