@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res +77 -7
  4. package/src/Platform.res.mjs +82 -8
  5. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
  6. package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
  7. package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
  11. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
  12. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
  13. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
  14. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
  15. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +188 -0
  17. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +141 -0
  18. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +72 -0
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +40 -0
  20. package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
  21. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
  22. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
  23. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
  24. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
  25. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
  26. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
  27. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
  28. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
  29. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
  30. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
  31. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
  32. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
  33. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
  34. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
  35. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
  36. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
  37. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
  38. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
  39. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
  40. package/src/components/Api/AppSync_Adapter.res +72 -8
  41. package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
  42. package/src/components/Plugin.res +1 -1
  43. package/src/components/Plugin.res.mjs +2 -2
  44. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  45. package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
  46. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +10 -2
  47. package/tests/AppSync_AdapterTest.res +140 -0
  48. package/tests/AppSync_AdapterTest.res.mjs +182 -6
  49. package/tests/PgChangeFeedRelay_RuntimeTest.res +75 -0
  50. 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
- function buildAggregateParts(specModule, behaviorModule, eventLogTableName, queueUrl) {
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
- const resolvedTable = { name: eventLogTableName };
124
- const rawStorageOps = {
125
- append: append(resolvedTable),
126
- replay: replay(resolvedTable),
127
- replayStream: replayStream(resolvedTable),
128
- appendStream: appendStream(resolvedTable),
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
  }));