@reventlessdev/reventless-aws 3.0.0-alpha.172 → 3.0.0-alpha.173

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 (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res.mjs +5 -5
  4. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
  5. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
  7. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
  8. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
  9. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +181 -0
  10. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +135 -0
  11. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +64 -0
  12. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +36 -0
  13. package/src/adapter/Runtime/AggregateEntryPoint.mjs +27 -9
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro.res.mjs +3 -3
  15. package/src/adapter/Runtime/AggregateRuntime_Builder_Micro_Async.res.mjs +3 -3
  16. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate.res.mjs +1 -1
  17. package/src/adapter/Runtime/AggregateRuntime_Builder_PerAggregate_Async.res.mjs +1 -1
  18. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +1 -1
  19. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +1 -1
  20. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +1 -1
  21. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +20 -6
  22. package/src/adapter/Runtime/EventCollectorRuntime_Builder_PerEventCollector.res.mjs +1 -1
  23. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +1 -1
  24. package/src/adapter/Runtime/ExtensionPointRuntime_Builder_PerExtensionPoint.res.mjs +1 -1
  25. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +52 -0
  26. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +38 -0
  27. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +20 -2
  28. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +1 -1
  29. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +1 -1
  30. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
  31. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
  32. package/src/components/Api/AppSync_Adapter.res +72 -8
  33. package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
  34. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  35. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
  36. package/tests/AppSync_AdapterTest.res +140 -0
  37. package/tests/AppSync_AdapterTest.res.mjs +182 -6
  38. package/tests/PgChangeFeedRelay_RuntimeTest.res +44 -0
  39. package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +70 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
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.173 (2026-07-05)
7
+
8
+ ### Features
9
+
10
+ * **reventless-aws:** aggregate EventLog Postgres runtime + entry-point branch ([fd81ece](https://github.com/ReventlessDev/reventless-core/commit/fd81ece65c2bf6aebf227d74ec2d0f002833d351))
11
+ * **reventless-aws:** DCB EventLog Postgres runtime + entry-point branch (B2.1) ([a6b4f8d](https://github.com/ReventlessDev/reventless-core/commit/a6b4f8d34509ff7213273a1f71f31705b625ff40)), closes [#4](https://github.com/ReventlessDev/reventless-core/issues/4)
12
+ * **reventless-aws:** opt-in [@aws](https://github.com/aws)_iam dual-auth for deploy-time AppSync callers ([94037f1](https://github.com/ReventlessDev/reventless-core/commit/94037f17ae48a33415b02e8e7b178906be8b59a4))
13
+ * **reventless-aws:** Postgres change-feed relay deploy builder + Lambda VPC (B2.3a/b, C1) ([64d67f3](https://github.com/ReventlessDev/reventless-core/commit/64d67f3ecd2c42d59ba814d6d821683ae8c81612))
14
+ * **reventless-aws:** Postgres change-feed relay runtime (B2.2) ([9ccb601](https://github.com/ReventlessDev/reventless-core/commit/9ccb60192457e9da21869e0d1c98fe8dbfb42178))
15
+
16
+
6
17
  # 3.0.0-alpha.172 (2026-07-05)
7
18
 
8
19
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.172",
3
+ "version": "3.0.0-alpha.173",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -9,15 +9,15 @@
9
9
  "sury": "11.0.0-alpha.4",
10
10
  "uuid": "^13.0.0",
11
11
  "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.20",
12
- "@reventlessdev/rescript-effect": "0.1.0-alpha.24",
13
12
  "@reventlessdev/rescript-jest": "1.0.0-alpha.5",
13
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.24",
14
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.45",
14
15
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
15
16
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.13",
16
- "@reventlessdev/reventless-core": "3.0.0-alpha.139",
17
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.45",
18
- "@reventlessdev/reventless-infra": "3.0.0-alpha.85",
19
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.1",
17
+ "@reventlessdev/reventless-core": "3.0.0-alpha.140",
18
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.86",
20
19
  "@reventlessdev/reventless-interop": "3.0.0-alpha.23",
20
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.2",
21
21
  "@reventlessdev/reventless-spec": "3.0.0-alpha.64"
22
22
  },
23
23
  "devDependencies": {
@@ -598,7 +598,7 @@ function MakeWithConfig(Config) {
598
598
  return Output$Pulumi.flatMap(targetApi, api => Output$Pulumi.flatMap(api.id, apiId => {
599
599
  let runSchemaPush = () => writeAndScanFragments().then(async allPluginFragments => {
600
600
  let baseFragment;
601
- baseFragment = capturedDeployTarget === "Domain" && Config.splitApi ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin");
601
+ baseFragment = capturedDeployTarget === "Domain" && Config.splitApi ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
602
602
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, allPluginFragments);
603
603
  let currentHash = AppSync_Adapter$ReventlessAws.sha256Hex(sdl);
604
604
  let storedHash = tableNameOpt !== undefined ? await readSchemaHash(tableNameOpt, apiId) : undefined;
@@ -653,7 +653,7 @@ function MakeWithConfig(Config) {
653
653
  let hooks_preAdminResolversSchemaHook = adminBarrier => {
654
654
  let match = splitApiOutputsRef.contents;
655
655
  let targetApi = match !== undefined ? match.platformApi : domainApi;
656
- let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin");
656
+ let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
657
657
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []);
658
658
  return Output$Pulumi.flatMap(Pulumi.all([
659
659
  targetApi,
@@ -920,7 +920,7 @@ function MakeWithConfig(Config) {
920
920
  return;
921
921
  }
922
922
  });
923
- let baseFragment = Config.splitApi ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin");
923
+ let baseFragment = Config.splitApi ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
924
924
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, fragments);
925
925
  return await AppSync_Adapter$ReventlessAws.startSchemaCreationRetrying(AppSync_Adapter$ReventlessAws.getClient(), {
926
926
  apiId: apiId,
@@ -1686,7 +1686,7 @@ function Make($star) {
1686
1686
  return Output$Pulumi.flatMap(targetApi, api => Output$Pulumi.flatMap(api.id, apiId => {
1687
1687
  let runSchemaPush = () => writeAndScanFragments().then(async allPluginFragments => {
1688
1688
  let baseFragment;
1689
- baseFragment = capturedDeployTarget === "Domain" ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin");
1689
+ baseFragment = capturedDeployTarget === "Domain" ? emptyBaseFragment : AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
1690
1690
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, allPluginFragments);
1691
1691
  let currentHash = AppSync_Adapter$ReventlessAws.sha256Hex(sdl);
1692
1692
  let storedHash = tableNameOpt !== undefined ? await readSchemaHash(tableNameOpt, apiId) : undefined;
@@ -1741,7 +1741,7 @@ function Make($star) {
1741
1741
  let hooks_preAdminResolversSchemaHook = adminBarrier => {
1742
1742
  let match = splitApiOutputsRef.contents;
1743
1743
  let targetApi = match !== undefined ? match.platformApi : domainApi;
1744
- let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin");
1744
+ let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
1745
1745
  let sdl = GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []);
1746
1746
  return Output$Pulumi.flatMap(Pulumi.all([
1747
1747
  targetApi,
@@ -52,7 +52,7 @@ function make(name, referencesName, referencesDb, countsName, countsDb, param, s
52
52
  let componentOpts = {
53
53
  parent: componentOpts_parent
54
54
  };
55
- let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, code, sourceCodeHash, envVars, 1024, 30, undefined, undefined, undefined, undefined, componentOpts);
55
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, code, sourceCodeHash, envVars, 1024, 30, undefined, undefined, undefined, undefined, undefined, componentOpts);
56
56
  let lambda = runtime.parts.lambda;
57
57
  let subscribe = (sourceName, source) => Util_EventSourceMapping$ReventlessAws.subscribe(undefined, lambda, name, sourceName, source, opts);
58
58
  subscribe(referencesName, referencesStream);
@@ -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,181 @@
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
+ /** Plugin EventCollector SQS queue this log's events are relayed to. */
22
+ targetQueueUrl: Pulumi.Output.t<string>,
23
+ targetQueueArn: Pulumi.Output.t<string>,
24
+ }
25
+
26
+ // ~securityGroupId: DB-access SG the relay Lambda attaches (PgConnection.securityGroupId).
27
+ // ~subnetIds: private subnets for the relay Lambda (PgConnection.subnetIds).
28
+ let make = (
29
+ ~name: string,
30
+ ~logs: array<relayLog>,
31
+ ~securityGroupId: Pulumi.Output.t<string>,
32
+ ~subnetIds: array<Pulumi.Input.t<string>>,
33
+ ~intervalMinutes: int=1,
34
+ ~opts=?,
35
+ ): array<ReventlessInfra.Adapter.resource> => {
36
+ open PulumiAws
37
+ let customOpts =
38
+ opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
39
+
40
+ let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
41
+ ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs",
42
+ ~packageDirs=Dict.make(),
43
+ )
44
+
45
+ // HANDLER_CONFIG.logs[], serialized from the deploy-time-resolved Outputs.
46
+ let logJsonOutputs = logs->Array.map(l =>
47
+ (l.connectionConfig, l.targetQueueUrl)
48
+ ->Pulumi.Output.all2
49
+ ->Pulumi.Output.apply(((cc, queueUrl)) => {
50
+ let pgConnectionJson =
51
+ [
52
+ ("host", cc.host->JSON.Encode.string),
53
+ ("port", cc.port->Int.toFloat->JSON.Encode.float),
54
+ ("database", cc.database->JSON.Encode.string),
55
+ ("username", cc.username->JSON.Encode.string),
56
+ ("secretArn", cc.secretArn->JSON.Encode.string),
57
+ ]
58
+ ->Dict.fromArray
59
+ ->JSON.Encode.object
60
+ [
61
+ ("pgConnection", pgConnectionJson),
62
+ ("logName", l.logName->JSON.Encode.string),
63
+ ("subscriber", l.subscriber->JSON.Encode.string),
64
+ ("targetQueueUrl", queueUrl->JSON.Encode.string),
65
+ ]
66
+ ->Dict.fromArray
67
+ ->JSON.Encode.object
68
+ ->JSON.stringify
69
+ })
70
+ )
71
+ let handlerConfigOutput =
72
+ Pulumi.Output.all(logJsonOutputs)->Pulumi.Output.apply(items =>
73
+ `{"logs":[${items->Array.join(",")}]}`
74
+ )
75
+
76
+ let envVars = Dict.fromArray([("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)])
77
+
78
+ let vpcConfig =
79
+ securityGroupId
80
+ ->Pulumi.Output.apply(sgId =>
81
+ (
82
+ {
83
+ Lambda.Function.subnetIds: subnetIds->Pulumi.Input.make,
84
+ securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
85
+ }: Lambda.Function.vpcConfig
86
+ )
87
+ )
88
+ ->Pulumi.Output.asInput
89
+
90
+ let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
91
+ ~name,
92
+ ~code,
93
+ ~sourceCodeHash,
94
+ ~envVars,
95
+ ~vpcConfig,
96
+ ~opts?,
97
+ )
98
+
99
+ // IAM: GetSecretValue on each log's DB secret + SendMessage on each target queue.
100
+ let secretArns =
101
+ logs->Array.map(l => l.connectionConfig->Pulumi.Output.apply(cc => cc.secretArn))
102
+ let queueArns = logs->Array.map(l => l.targetQueueArn)
103
+ let _iam =
104
+ (Pulumi.Output.all(secretArns), Pulumi.Output.all(queueArns), runtime.parts.lambdaRole.id)
105
+ ->Pulumi.Output.all3
106
+ ->Pulumi.Output.apply(((secretArns, queueArns, roleId)) => {
107
+ open PulumiAws.PolicyDocument
108
+ let _ = PulumiAws.IAM.RolePolicy.make(
109
+ ~name=`${name}RelayAccess`,
110
+ ~args={
111
+ policy: PulumiAws.PolicyDocument.make(
112
+ ~id=`${name}RelayAccessPolicy`,
113
+ ~statements=[
114
+ {
115
+ sid: "AllowGetSecret",
116
+ effect: Allow,
117
+ actions: Action("secretsmanager:GetSecretValue"),
118
+ resources: Resources(secretArns),
119
+ },
120
+ {
121
+ sid: "AllowSendSqs",
122
+ effect: Allow,
123
+ actions: Action("sqs:SendMessage"),
124
+ resources: Resources(queueArns),
125
+ },
126
+ ],
127
+ )
128
+ ->PulumiAws.PolicyDocument.toJsonString
129
+ ->Pulumi.Input.make,
130
+ role: roleId->Pulumi.Input.make,
131
+ },
132
+ )
133
+ })
134
+
135
+ // Scheduled poll: EventRule (rate) → Permission → EventTarget.
136
+ let rule = {
137
+ open PulumiAws.Cloudwatch
138
+ EventRule.make(
139
+ ~name=`${Pulumi.Pulumi.getStackName()}-${name}`,
140
+ ~args={
141
+ description: "Poll Postgres DCB change feed and relay to EventCollector"->Pulumi.Input.make,
142
+ scheduleExpression: EventRule.ScheduleExpression.every(Minutes(intervalMinutes)),
143
+ },
144
+ ~opts=?customOpts,
145
+ )
146
+ }
147
+
148
+ let lambda = runtime.parts.lambda
149
+ let _wire =
150
+ (lambda->Pulumi.Output.flatMap(l => l.arn), lambda->Pulumi.Output.flatMap(l => l.name))
151
+ ->Pulumi.Output.all2
152
+ ->Pulumi.Output.apply(((lambdaArn, lambdaName)) => {
153
+ let _permission = PulumiAws.Lambda.Permission.make(
154
+ ~name=`${name}SchedulePermission`,
155
+ ~args={
156
+ action: "lambda:InvokeFunction",
157
+ function: lambdaName->Pulumi.Input.make,
158
+ principal: AWS.CloudwatchEventRule.principal,
159
+ },
160
+ ~opts=?customOpts,
161
+ )
162
+ let _target = {
163
+ open PulumiAws.Cloudwatch
164
+ EventTarget.make(
165
+ ~name,
166
+ ~args={
167
+ rule: EventTarget.Rule.ofEventRule(rule),
168
+ arn: lambdaArn->Pulumi.Input.make,
169
+ },
170
+ ~opts=?customOpts,
171
+ )
172
+ }
173
+ })
174
+
175
+ [
176
+ lambda
177
+ ->Pulumi.Output.apply(l => l->Util_Lambda.toResource)
178
+ ->ReventlessCore.Adapter.outputToResource,
179
+ rule->Util_Cloudwatch.EventRule.toResource,
180
+ ]
181
+ }
@@ -0,0 +1,135 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Aws from "@pulumi/aws";
4
+ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Pulumi from "@pulumi/pulumi";
7
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
+ import * as AWS$ReventlessAws from "../AWS.res.mjs";
9
+ import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
10
+ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
11
+ import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
12
+ import * as Util_Lambda$ReventlessAws from "../../util/Util_Lambda.res.mjs";
13
+ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
+ import * as Util_Cloudwatch$ReventlessAws from "../../util/Util_Cloudwatch.res.mjs";
15
+ import * as Cloudwatch_EventRule$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventRule.res.mjs";
16
+ import * as Cloudwatch_EventTarget$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventTarget.res.mjs";
17
+ import * as RuntimeEnvironment_Lambda$ReventlessAws from "../Runtime/RuntimeEnvironment_Lambda.res.mjs";
18
+
19
+ function make(name, logs, securityGroupId, subnetIds, intervalMinutesOpt, opts) {
20
+ let intervalMinutes = intervalMinutesOpt !== undefined ? intervalMinutesOpt : 1;
21
+ let customOpts = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
22
+ let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs", {}, undefined);
23
+ let logJsonOutputs = logs.map(l => Pulumi.all([
24
+ l.connectionConfig,
25
+ l.targetQueueUrl
26
+ ]).apply(param => {
27
+ let cc = param[0];
28
+ let pgConnectionJson = Object.fromEntries([
29
+ [
30
+ "host",
31
+ cc.host
32
+ ],
33
+ [
34
+ "port",
35
+ cc.port
36
+ ],
37
+ [
38
+ "database",
39
+ cc.database
40
+ ],
41
+ [
42
+ "username",
43
+ cc.username
44
+ ],
45
+ [
46
+ "secretArn",
47
+ cc.secretArn
48
+ ]
49
+ ]);
50
+ return JSON.stringify(Object.fromEntries([
51
+ [
52
+ "pgConnection",
53
+ pgConnectionJson
54
+ ],
55
+ [
56
+ "logName",
57
+ l.logName
58
+ ],
59
+ [
60
+ "subscriber",
61
+ l.subscriber
62
+ ],
63
+ [
64
+ "targetQueueUrl",
65
+ param[1]
66
+ ]
67
+ ]));
68
+ }));
69
+ let handlerConfigOutput = Pulumi.all(logJsonOutputs).apply(items => `{"logs":[` + items.join(",") + `]}`);
70
+ let envVars = Object.fromEntries([[
71
+ "HANDLER_CONFIG",
72
+ handlerConfigOutput
73
+ ]]);
74
+ let vpcConfig = securityGroupId.apply(sgId => ({
75
+ subnetIds: subnetIds,
76
+ securityGroupIds: [sgId]
77
+ }));
78
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, undefined, undefined, undefined, undefined, undefined, undefined, vpcConfig, opts);
79
+ let secretArns = logs.map(l => l.connectionConfig.apply(cc => cc.secretArn));
80
+ let queueArns = logs.map(l => l.targetQueueArn);
81
+ Pulumi.all([
82
+ Pulumi.all(secretArns),
83
+ Pulumi.all(queueArns),
84
+ runtime.parts.lambdaRole.id
85
+ ]).apply(param => {
86
+ new (Aws.iam.RolePolicy)(name + `RelayAccess`, {
87
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `RelayAccessPolicy`, [
88
+ {
89
+ Sid: "AllowGetSecret",
90
+ Effect: "Allow",
91
+ Action: "secretsmanager:GetSecretValue",
92
+ Resource: param[0]
93
+ },
94
+ {
95
+ Sid: "AllowSendSqs",
96
+ Effect: "Allow",
97
+ Action: "sqs:SendMessage",
98
+ Resource: param[1]
99
+ }
100
+ ])),
101
+ role: param[2]
102
+ });
103
+ });
104
+ let rule = new (Aws.cloudwatch.EventRule)(Pulumi.getStack() + `-` + name, {
105
+ description: "Poll Postgres DCB change feed and relay to EventCollector",
106
+ scheduleExpression: Primitive_option.some(Cloudwatch_EventRule$PulumiAws.ScheduleExpression.every({
107
+ TAG: "Minutes",
108
+ _0: intervalMinutes
109
+ }))
110
+ }, customOpts !== undefined ? Primitive_option.valFromOption(customOpts) : undefined);
111
+ let lambda = runtime.parts.lambda;
112
+ Pulumi.all([
113
+ Output$Pulumi.flatMap(lambda, l => l.arn),
114
+ Output$Pulumi.flatMap(lambda, l => l.name)
115
+ ]).apply(param => {
116
+ new (Aws.lambda.Permission)(name + `SchedulePermission`, {
117
+ action: "lambda:InvokeFunction",
118
+ function: param[1],
119
+ principal: AWS$ReventlessAws.CloudwatchEventRule.principal
120
+ }, customOpts !== undefined ? Primitive_option.valFromOption(customOpts) : undefined);
121
+ new (Aws.cloudwatch.EventTarget)(name, {
122
+ rule: Cloudwatch_EventTarget$PulumiAws.Rule.ofEventRule(rule),
123
+ arn: param[0]
124
+ }, customOpts !== undefined ? Primitive_option.valFromOption(customOpts) : undefined);
125
+ });
126
+ return [
127
+ Adapter$ReventlessCore.outputToResource(lambda.apply(l => Util_Lambda$ReventlessAws.toResource(undefined, l))),
128
+ Util_Cloudwatch$ReventlessAws.EventRule.toResource(rule)
129
+ ];
130
+ }
131
+
132
+ export {
133
+ make,
134
+ }
135
+ /* @pulumi/aws Not a pure module */
@@ -0,0 +1,64 @@
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
+ ~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>=?,
55
+ ~sendBatch: array<JSON.t> => promise<unit>,
56
+ ): int => {
57
+ let pool = PgRuntime.poolFor(config)
58
+ await ReventlessPostgres.PgChangeFeed.drain(pool, ~logName, ~subscriber, ~handle=async events => {
59
+ let jsons = events->Array.filterMap(event => toEventCollectorJson(event, ~partitionTag?))
60
+ if jsons->Array.length > 0 {
61
+ await sendBatch(jsons)
62
+ }
63
+ })
64
+ }
@@ -0,0 +1,36 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
5
+ import * as PgRuntime$ReventlessAws from "./PgRuntime.res.mjs";
6
+ import * as PgChangeFeed$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgChangeFeed.res.mjs";
7
+ import * as Util_DynamoDbStream_Runtime$ReventlessAws from "../../util/Util_DynamoDbStream_Runtime.res.mjs";
8
+ import * as DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws from "../DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs";
9
+
10
+ function toEventCollectorJson(event, partitionTag) {
11
+ let item = {};
12
+ item["id"] = DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.derivePartitionKey(partitionTag, event.tags);
13
+ item["position"] = event.position;
14
+ item["event"] = event.eventType;
15
+ item["data"] = event.data;
16
+ Message$ReventlessCore.decomposeMeta(event.meta).forEach(param => {
17
+ item[param[0]] = param[1];
18
+ });
19
+ return Util_DynamoDbStream_Runtime$ReventlessAws.buildJsonEvent$p(item);
20
+ }
21
+
22
+ async function relay(config, logName, subscriber, partitionTag, sendBatch) {
23
+ let pool = PgRuntime$ReventlessAws.poolFor(config);
24
+ return await PgChangeFeed$ReventlessPostgres.drain(pool, logName, subscriber, undefined, async events => {
25
+ let jsons = Stdlib_Array.filterMap(events, event => toEventCollectorJson(event, partitionTag));
26
+ if (jsons.length !== 0) {
27
+ return await sendBatch(jsons);
28
+ }
29
+ });
30
+ }
31
+
32
+ export {
33
+ toEventCollectorJson,
34
+ relay,
35
+ }
36
+ /* Message-ReventlessCore 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
  }));