@reventlessdev/reventless-aws 3.0.0-alpha.171 → 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 (52) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +6 -5
  3. package/rescript.json +2 -1
  4. package/src/Platform.res.mjs +5 -5
  5. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +1 -1
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +12 -6
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +17 -5
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +26 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +15 -0
  10. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +23 -0
  11. package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +14 -0
  12. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +181 -0
  13. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +135 -0
  14. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +64 -0
  15. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +36 -0
  16. package/src/adapter/Postgres/PgConnection.res +169 -0
  17. package/src/adapter/Postgres/PgConnection.res.mjs +112 -0
  18. package/src/adapter/Postgres/PgRuntime.res +71 -0
  19. package/src/adapter/Postgres/PgRuntime.res.mjs +71 -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.mjs +1 -1
  37. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +1 -1
  38. package/src/adapter/Runtime/TaskRuntime_Builder_PerBucket.res.mjs +1 -1
  39. package/src/components/Api/AppSync_Adapter.res +72 -8
  40. package/src/components/Api/AppSync_Adapter.res.mjs +45 -6
  41. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  42. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +2 -2
  43. package/src/util/Util_AppSync_Caller.res +24 -4
  44. package/src/util/Util_AppSync_Caller.res.mjs +7 -4
  45. package/tests/AppSync_AdapterTest.res +140 -0
  46. package/tests/AppSync_AdapterTest.res.mjs +182 -6
  47. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +7 -3
  48. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +18 -3
  49. package/tests/PgChangeFeedRelay_RuntimeTest.res +44 -0
  50. package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +70 -0
  51. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +2 -1
  52. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +2 -2
@@ -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 */
@@ -0,0 +1,169 @@
1
+ /** Deploy-time component that provisions a managed Postgres instance (RDS) and
2
+ resolves the connection details the `reventless-postgres` runtime needs inside
3
+ a Lambda.
4
+
5
+ One `PgConnection` hosts **all three** storage surfaces (classic `event_log`,
6
+ DCB `dcb_event`, and the QueryDb `qdb_*` tables) in a single database — the
7
+ per-surface adapters take a reference to it, they do not each provision a DB.
8
+
9
+ Networking is supplied by the platform (an existing VPC + private subnets); a
10
+ `PgConnection` does not provision a VPC. It creates:
11
+ - a **shared security group** with a self-referencing `5432` ingress rule, so
12
+ every Lambda that attaches this same SG (see C1 / `securityGroupId`) can
13
+ reach the DB without a separate client SG;
14
+ - a **DB subnet group** over the given private subnets;
15
+ - the **RDS instance** with `manageMasterUserPassword` — RDS mints and rotates
16
+ the master credentials in Secrets Manager, so no plaintext password ever
17
+ lands in Pulumi state. The resulting secret ARN surfaces on
18
+ `connectionConfig.secretArn`; the runtime resolves it at cold start.
19
+
20
+ Aurora / Aurora Serverless v2 is a planned second engine behind the same
21
+ `connectionConfig` output shape (tracked in the AWS-Postgres plan). */
22
+
23
+ /** Resolved at deploy time, serialized into the handler Lambda env, and consumed
24
+ at cold start to build a `PgDriver` pool. `secretArn` points at the
25
+ RDS-managed `{username, password}` secret; `host`/`port`/`database` come from
26
+ the instance itself. */
27
+ @schema
28
+ type connectionConfig = {
29
+ host: string,
30
+ port: int,
31
+ database: string,
32
+ /** RDS master username — set at deploy time, not secret. The pool's `user`;
33
+ the Secrets Manager secret supplies only the matching password. */
34
+ username: string,
35
+ secretArn: string,
36
+ }
37
+
38
+ type t = {
39
+ /** Deploy-time resources (currently the RDS instance) for dependency wiring —
40
+ e.g. the A3 migration Lambda and the B-phase adapters order after these. */
41
+ resources: array<ReventlessInfra.Adapter.resource>,
42
+ connectionConfig: Pulumi.Output.t<connectionConfig>,
43
+ /** Attach to every Lambda that talks to Postgres: the SG's self-referencing
44
+ rule is what lets those Lambdas reach the DB on 5432. */
45
+ securityGroupId: Pulumi.Output.t<string>,
46
+ /** The DB's private subnets, echoed back for the Lambda `vpcConfig` so
47
+ handlers land in the same subnets as the database. */
48
+ subnetIds: array<Pulumi.Input.t<string>>,
49
+ }
50
+
51
+ /** Postgres wire port. */
52
+ let port = 5432
53
+
54
+ let make = (
55
+ ~name,
56
+ ~vpcId: Pulumi.Input.t<string>,
57
+ ~subnetIds: array<Pulumi.Input.t<string>>,
58
+ ~databaseName="reventless",
59
+ ~username="reventless_admin",
60
+ ~engineVersion="16",
61
+ ~instanceClass="db.t3.micro",
62
+ ~allocatedStorage=20,
63
+ ~multiAz=false,
64
+ ~storageEncrypted=true,
65
+ ~backupRetentionPeriod=7,
66
+ ~deletionProtection=true,
67
+ ~skipFinalSnapshot=false,
68
+ ~opts: option<Pulumi.ComponentResource.options>=?,
69
+ ): t => {
70
+ let opts =
71
+ opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
72
+
73
+ let tags =
74
+ [
75
+ ("Name", name),
76
+ ("Environment", Pulumi.Pulumi.getStackName()),
77
+ ("reventless:role", "postgres-connection"),
78
+ ]->Dict.fromArray
79
+
80
+ // Shared DB-access security group. Self-referencing 5432 ingress lets member
81
+ // Lambdas reach the DB; open egress so both the DB and those Lambdas can make
82
+ // outbound calls (e.g. Secrets Manager, other AWS APIs).
83
+ let sg = PulumiAws.EC2.SecurityGroup.make(
84
+ ~name=`${name}-pg-sg`,
85
+ ~args={
86
+ name: `${name}-pg-sg`,
87
+ vpcId,
88
+ ingress: [
89
+ {
90
+ fromPort: port,
91
+ protocol: "tcp",
92
+ toPort: port,
93
+ cidrBlocks: [],
94
+ self: true,
95
+ },
96
+ ],
97
+ egress: [PulumiAws.EC2.SecurityGroup.Egress.allowAll],
98
+ tags,
99
+ },
100
+ ~opts?,
101
+ )
102
+
103
+ let subnetGroup = PulumiAws.Rds.SubnetGroup.make(
104
+ ~name=`${name}-pg-subnets`,
105
+ ~args={
106
+ name: `${name}-pg-subnets`->Pulumi.Input.make,
107
+ subnetIds: subnetIds->Pulumi.Input.make,
108
+ tags: tags->Pulumi.Input.make,
109
+ },
110
+ ~opts?,
111
+ )
112
+
113
+ let instance = PulumiAws.Rds.Instance.make(
114
+ ~name=`${name}-pg`,
115
+ ~args={
116
+ engine: "postgres"->Pulumi.Input.make,
117
+ engineVersion: engineVersion->Pulumi.Input.make,
118
+ instanceClass: instanceClass->Pulumi.Input.make,
119
+ allocatedStorage: allocatedStorage->Pulumi.Input.make,
120
+ dbName: databaseName->Pulumi.Input.make,
121
+ username: username->Pulumi.Input.make,
122
+ manageMasterUserPassword: true->Pulumi.Input.make,
123
+ dbSubnetGroupName: subnetGroup.name->Pulumi.Output.asInput,
124
+ vpcSecurityGroupIds: [sg.id->Pulumi.Output.asInput]->Pulumi.Input.make,
125
+ port: port->Pulumi.Input.make,
126
+ multiAz: multiAz->Pulumi.Input.make,
127
+ publiclyAccessible: false->Pulumi.Input.make,
128
+ storageEncrypted: storageEncrypted->Pulumi.Input.make,
129
+ backupRetentionPeriod: backupRetentionPeriod->Pulumi.Input.make,
130
+ deletionProtection: deletionProtection->Pulumi.Input.make,
131
+ skipFinalSnapshot: skipFinalSnapshot->Pulumi.Input.make,
132
+ tags: tags->Pulumi.Input.make,
133
+ },
134
+ ~opts?,
135
+ )
136
+
137
+ let connectionConfig =
138
+ (instance.address, instance.masterUserSecrets)
139
+ ->Pulumi.Output.all2
140
+ ->Pulumi.Output.apply(((host, secrets)) => {
141
+ host,
142
+ port,
143
+ database: databaseName,
144
+ username,
145
+ secretArn: switch secrets->Array.get(0) {
146
+ | Some(secret) => secret.secretArn
147
+ | None =>
148
+ JsError.throwWithMessage(
149
+ "RDS instance exposes no master user secret — manageMasterUserPassword must be enabled",
150
+ )
151
+ },
152
+ })
153
+
154
+ {
155
+ resources: [
156
+ ReventlessInfra.Adapter.make(
157
+ ~name=name->Pulumi.Output.make,
158
+ ~id=instance.id,
159
+ ~urn=instance.arn,
160
+ ~service="aws:rds"->Pulumi.Output.make,
161
+ ~role="postgres"->Pulumi.Output.make,
162
+ ~resourceType="aws:rds:Instance"->Pulumi.Output.make,
163
+ ),
164
+ ],
165
+ connectionConfig,
166
+ securityGroupId: sg.id,
167
+ subnetIds,
168
+ }
169
+ }
@@ -0,0 +1,112 @@
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 Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Pulumi from "@pulumi/pulumi";
7
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
9
+ import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
10
+ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
11
+ import * as EC2_SecurityGroup$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/EC2/EC2_SecurityGroup.res.mjs";
12
+
13
+ let connectionConfigSchema = S.schema(s => ({
14
+ host: s.m(S.string),
15
+ port: s.m(S.int),
16
+ database: s.m(S.string),
17
+ username: s.m(S.string),
18
+ secretArn: s.m(S.string)
19
+ }));
20
+
21
+ function make(name, vpcId, subnetIds, databaseNameOpt, usernameOpt, engineVersionOpt, instanceClassOpt, allocatedStorageOpt, multiAzOpt, storageEncryptedOpt, backupRetentionPeriodOpt, deletionProtectionOpt, skipFinalSnapshotOpt, opts) {
22
+ let databaseName = databaseNameOpt !== undefined ? databaseNameOpt : "reventless";
23
+ let username = usernameOpt !== undefined ? usernameOpt : "reventless_admin";
24
+ let engineVersion = engineVersionOpt !== undefined ? engineVersionOpt : "16";
25
+ let instanceClass = instanceClassOpt !== undefined ? instanceClassOpt : "db.t3.micro";
26
+ let allocatedStorage = allocatedStorageOpt !== undefined ? allocatedStorageOpt : 20;
27
+ let multiAz = multiAzOpt !== undefined ? multiAzOpt : false;
28
+ let storageEncrypted = storageEncryptedOpt !== undefined ? storageEncryptedOpt : true;
29
+ let backupRetentionPeriod = backupRetentionPeriodOpt !== undefined ? backupRetentionPeriodOpt : 7;
30
+ let deletionProtection = deletionProtectionOpt !== undefined ? deletionProtectionOpt : true;
31
+ let skipFinalSnapshot = skipFinalSnapshotOpt !== undefined ? skipFinalSnapshotOpt : false;
32
+ let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
33
+ let tags = Object.fromEntries([
34
+ [
35
+ "Name",
36
+ name
37
+ ],
38
+ [
39
+ "Environment",
40
+ Pulumi.getStack()
41
+ ],
42
+ [
43
+ "reventless:role",
44
+ "postgres-connection"
45
+ ]
46
+ ]);
47
+ let sg = EC2_SecurityGroup$PulumiAws.make(name + `-pg-sg`, {
48
+ name: name + `-pg-sg`,
49
+ vpcId: vpcId,
50
+ ingress: [{
51
+ fromPort: 5432,
52
+ protocol: "tcp",
53
+ toPort: 5432,
54
+ cidrBlocks: [],
55
+ self: true
56
+ }],
57
+ egress: [EC2_SecurityGroup$PulumiAws.Egress.allowAll],
58
+ tags: tags
59
+ }, opts$1);
60
+ let subnetGroup = new (Aws.rds.SubnetGroup)(name + `-pg-subnets`, {
61
+ name: name + `-pg-subnets`,
62
+ subnetIds: subnetIds,
63
+ tags: tags
64
+ }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
65
+ let instance = new (Aws.rds.Instance)(name + `-pg`, {
66
+ allocatedStorage: allocatedStorage,
67
+ engine: "postgres",
68
+ engineVersion: engineVersion,
69
+ instanceClass: instanceClass,
70
+ dbName: databaseName,
71
+ username: username,
72
+ manageMasterUserPassword: true,
73
+ dbSubnetGroupName: subnetGroup.name,
74
+ vpcSecurityGroupIds: [sg.id],
75
+ port: 5432,
76
+ multiAz: multiAz,
77
+ publiclyAccessible: false,
78
+ storageEncrypted: storageEncrypted,
79
+ backupRetentionPeriod: backupRetentionPeriod,
80
+ deletionProtection: deletionProtection,
81
+ skipFinalSnapshot: skipFinalSnapshot,
82
+ tags: tags
83
+ }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
84
+ let connectionConfig = Pulumi.all([
85
+ instance.address,
86
+ instance.masterUserSecrets
87
+ ]).apply(param => {
88
+ let secret = param[1][0];
89
+ return {
90
+ host: param[0],
91
+ port: 5432,
92
+ database: databaseName,
93
+ username: username,
94
+ secretArn: secret !== undefined ? secret.secretArn : Stdlib_JsError.throwWithMessage("RDS instance exposes no master user secret — manageMasterUserPassword must be enabled")
95
+ };
96
+ });
97
+ return {
98
+ resources: [Adapter$ReventlessInfra.make(Pulumi.output(name), instance.id, instance.arn, Pulumi.output("aws:rds"), undefined, Pulumi.output("postgres"), undefined, Pulumi.output("aws:rds:Instance"), undefined, undefined)],
99
+ connectionConfig: connectionConfig,
100
+ securityGroupId: sg.id,
101
+ subnetIds: subnetIds
102
+ };
103
+ }
104
+
105
+ let port = 5432;
106
+
107
+ export {
108
+ connectionConfigSchema,
109
+ port,
110
+ make,
111
+ }
112
+ /* connectionConfigSchema Not a pure module */
@@ -0,0 +1,71 @@
1
+ /** Cold-start Postgres connectivity for deployed Lambdas.
2
+
3
+ Given a resolved `PgConnection.connectionConfig` (injected into the handler env
4
+ at deploy time), this resolves the RDS-managed master password from Secrets
5
+ Manager and builds **one pg pool per container**, memoized by secret ARN.
6
+
7
+ Rotation-safe by construction: the pool is created with a *password provider*,
8
+ which pg invokes each time it opens a physical connection — so a rotated secret
9
+ is picked up on the next new connection. The fetched secret is cached per ARN
10
+ (as a promise) to avoid a Secrets Manager round trip on every connection; a
11
+ password rotation surfaces as auth failures that recycle the container, after
12
+ which the next cold start re-fetches. */
13
+
14
+ /** RDS-managed master secret payload (`{username, password}` JSON). We only need
15
+ the password here — the username is carried on `connectionConfig` (deploy-time
16
+ known) so the pool can be constructed without first awaiting the secret. */
17
+ let passwordFromSecret = async (secretArn: string): string => {
18
+ let out =
19
+ await {AwsSdk.SecretsManager.GetSecretValueCommand.secretId: secretArn}
20
+ ->AwsSdk.SecretsManager.GetSecretValueCommand.make
21
+ ->AwsSdk.SecretsManager.GetSecretValueCommand.send
22
+ switch out.secretString {
23
+ | Some(str) =>
24
+ switch str->JSON.parseOrThrow->JSON.Decode.object {
25
+ | Some(obj) =>
26
+ switch obj->Dict.get("password")->Option.flatMap(JSON.Decode.string) {
27
+ | Some(password) => password
28
+ | None => JsError.throwWithMessage("Secrets Manager secret has no `password` field")
29
+ }
30
+ | None => JsError.throwWithMessage("Secrets Manager secret is not a JSON object")
31
+ }
32
+ | None =>
33
+ JsError.throwWithMessage(`Secrets Manager secret ${secretArn} has no SecretString`)
34
+ }
35
+ }
36
+
37
+ // Cache the in-flight/settled password fetch per ARN so pg's per-connection
38
+ // provider doesn't hit Secrets Manager on every physical connection.
39
+ let passwordCache: dict<promise<string>> = Dict.make()
40
+
41
+ let cachedPassword = (secretArn: string): promise<string> =>
42
+ switch passwordCache->Dict.get(secretArn) {
43
+ | Some(p) => p
44
+ | None =>
45
+ let p = passwordFromSecret(secretArn)
46
+ passwordCache->Dict.set(secretArn, p)
47
+ p
48
+ }
49
+
50
+ // One pool per secret ARN for the life of the container.
51
+ let poolCache: dict<ReventlessPostgres.PgDriver.pool> = Dict.make()
52
+
53
+ /** The container-lifetime pool for the given connection config, built on first
54
+ use. pg pools connect lazily, so this is cheap until the first query. */
55
+ let poolFor = (config: PgConnection.connectionConfig): ReventlessPostgres.PgDriver.pool =>
56
+ switch poolCache->Dict.get(config.secretArn) {
57
+ | Some(pool) => pool
58
+ | None =>
59
+ let pool = ReventlessPostgres.PgDriver.makePool({
60
+ host: config.host,
61
+ port: config.port,
62
+ database: config.database,
63
+ user: config.username,
64
+ password: () => cachedPassword(config.secretArn),
65
+ // RDS PG15+ defaults to rds.force_ssl=1. Encrypt without CA verification
66
+ // for v1; pin the RDS CA bundle as a hardening follow-up.
67
+ ssl: {rejectUnauthorized: false},
68
+ })
69
+ poolCache->Dict.set(config.secretArn, pool)
70
+ pool
71
+ }
@@ -0,0 +1,71 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
+ import * as SecretsManager$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/SecretsManager.res.mjs";
8
+ import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
9
+ import * as ClientSecretsManager from "@aws-sdk/client-secrets-manager";
10
+
11
+ async function passwordFromSecret(secretArn) {
12
+ let out = await SecretsManager$AwsSdk.GetSecretValueCommand.send(new ClientSecretsManager.GetSecretValueCommand({
13
+ SecretId: secretArn
14
+ }));
15
+ let str = out.SecretString;
16
+ if (str === undefined) {
17
+ return Stdlib_JsError.throwWithMessage(`Secrets Manager secret ` + secretArn + ` has no SecretString`);
18
+ }
19
+ let obj = Stdlib_JSON.Decode.object(JSON.parse(str));
20
+ if (obj === undefined) {
21
+ return Stdlib_JsError.throwWithMessage("Secrets Manager secret is not a JSON object");
22
+ }
23
+ let password = Stdlib_Option.flatMap(obj["password"], Stdlib_JSON.Decode.string);
24
+ if (password !== undefined) {
25
+ return password;
26
+ } else {
27
+ return Stdlib_JsError.throwWithMessage("Secrets Manager secret has no `password` field");
28
+ }
29
+ }
30
+
31
+ let passwordCache = {};
32
+
33
+ function cachedPassword(secretArn) {
34
+ let p = passwordCache[secretArn];
35
+ if (p !== undefined) {
36
+ return p;
37
+ }
38
+ let p$1 = passwordFromSecret(secretArn);
39
+ passwordCache[secretArn] = p$1;
40
+ return p$1;
41
+ }
42
+
43
+ let poolCache = {};
44
+
45
+ function poolFor(config) {
46
+ let pool = poolCache[config.secretArn];
47
+ if (pool !== undefined) {
48
+ return Primitive_option.valFromOption(pool);
49
+ }
50
+ let pool$1 = PgDriver$ReventlessPostgres.makePool({
51
+ host: config.host,
52
+ port: config.port,
53
+ database: config.database,
54
+ user: config.username,
55
+ password: () => cachedPassword(config.secretArn),
56
+ ssl: {
57
+ rejectUnauthorized: false
58
+ }
59
+ });
60
+ poolCache[config.secretArn] = pool$1;
61
+ return pool$1;
62
+ }
63
+
64
+ export {
65
+ passwordFromSecret,
66
+ passwordCache,
67
+ cachedPassword,
68
+ poolCache,
69
+ poolFor,
70
+ }
71
+ /* SecretsManager-AwsSdk 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
  }));
@@ -173,7 +173,7 @@ function finish() {
173
173
  cmdTopicEnvVars["HANDLER_CONFIG"] = cmdTopicHandlerConfigOutput;
174
174
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
175
175
  let cmdTopicName = baseName + "CmdHandler";
176
- let cmdTopicRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdTopicName, match.code, match.sourceCodeHash, cmdTopicEnvVars, Math.max(spec.commandTopicMemorySize, 1024), Math.max(spec.commandTopicTimeout, 30), undefined, undefined, undefined, undefined, aggregateOpts);
176
+ let cmdTopicRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdTopicName, match.code, match.sourceCodeHash, cmdTopicEnvVars, Math.max(spec.commandTopicMemorySize, 1024), Math.max(spec.commandTopicTimeout, 30), undefined, undefined, undefined, undefined, undefined, aggregateOpts);
177
177
  spec.commandTopicConnects.forEach(connect => connect(cmdTopicRuntime));
178
178
  if (spec.commandGeneratorConnects.length !== 0) {
179
179
  let cmdGenHandlerConfigOutput = Pulumi.all([
@@ -185,7 +185,7 @@ function finish() {
185
185
  cmdGenEnvVars["HANDLER_CONFIG"] = cmdGenHandlerConfigOutput;
186
186
  let match$1 = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
187
187
  let cmdGenName = baseName + "CmdGen";
188
- let cmdGenRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdGenName, match$1.code, match$1.sourceCodeHash, cmdGenEnvVars, Math.max(spec.commandGeneratorMemorySize, 1024), Math.max(spec.commandGeneratorTimeout, 30), undefined, undefined, undefined, undefined, aggregateOpts);
188
+ let cmdGenRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdGenName, match$1.code, match$1.sourceCodeHash, cmdGenEnvVars, Math.max(spec.commandGeneratorMemorySize, 1024), Math.max(spec.commandGeneratorTimeout, 30), undefined, undefined, undefined, undefined, undefined, aggregateOpts);
189
189
  spec.commandGeneratorConnects.forEach(connect => connect(cmdGenRuntime));
190
190
  }
191
191
  let match$2 = spec.eventCollectorChannelSpec;
@@ -208,7 +208,7 @@ function finish() {
208
208
  evtMapperPackageDirs[mappingsPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(mappingsPkg);
209
209
  let match$4 = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/EventMapperEntryPoint.mjs", evtMapperPackageDirs, undefined);
210
210
  let evtMapperName = baseName + "EventMapper";
211
- let evtMapperRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(evtMapperName, match$4.code, match$4.sourceCodeHash, evtMapperEnvVars, Math.max(spec.eventCollectorMemorySize, 2048), Math.max(spec.eventCollectorTimeout, 180), undefined, undefined, undefined, undefined, aggregateOpts);
211
+ let evtMapperRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(evtMapperName, match$4.code, match$4.sourceCodeHash, evtMapperEnvVars, Math.max(spec.eventCollectorMemorySize, 2048), Math.max(spec.eventCollectorTimeout, 180), undefined, undefined, undefined, undefined, undefined, aggregateOpts);
212
212
  EventCollectorChannel_DynamoDbStream$ReventlessAws.connect(evtMapperName, [match$2], evtMapperRuntime, aggregateOpts);
213
213
  });
214
214
  }
@@ -173,7 +173,7 @@ function finish() {
173
173
  cmdTopicEnvVars["HANDLER_CONFIG"] = cmdTopicHandlerConfigOutput;
174
174
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
175
175
  let cmdTopicName = baseName + "CmdHandler";
176
- let cmdTopicRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdTopicName, match.code, match.sourceCodeHash, cmdTopicEnvVars, Math.max(spec.commandTopicMemorySize, 1024), Math.max(spec.commandTopicTimeout, 30), undefined, undefined, undefined, undefined, aggregateOpts);
176
+ let cmdTopicRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdTopicName, match.code, match.sourceCodeHash, cmdTopicEnvVars, Math.max(spec.commandTopicMemorySize, 1024), Math.max(spec.commandTopicTimeout, 30), undefined, undefined, undefined, undefined, undefined, aggregateOpts);
177
177
  spec.commandTopicConnects.forEach(connect => connect(cmdTopicRuntime));
178
178
  if (spec.commandGeneratorConnects.length !== 0) {
179
179
  let cmdGenHandlerConfigOutput = Pulumi.all([
@@ -186,7 +186,7 @@ function finish() {
186
186
  cmdGenEnvVars["DISPATCH_MODE"] = "async";
187
187
  let match$1 = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
188
188
  let cmdGenName = baseName + "CmdGen";
189
- let cmdGenRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdGenName, match$1.code, match$1.sourceCodeHash, cmdGenEnvVars, Math.max(spec.commandGeneratorMemorySize, 1024), Math.max(spec.commandGeneratorTimeout, 30), undefined, undefined, undefined, undefined, aggregateOpts);
189
+ let cmdGenRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(cmdGenName, match$1.code, match$1.sourceCodeHash, cmdGenEnvVars, Math.max(spec.commandGeneratorMemorySize, 1024), Math.max(spec.commandGeneratorTimeout, 30), undefined, undefined, undefined, undefined, undefined, aggregateOpts);
190
190
  spec.commandGeneratorConnects.forEach(connect => connect(cmdGenRuntime));
191
191
  }
192
192
  let match$2 = spec.eventCollectorChannelSpec;
@@ -209,7 +209,7 @@ function finish() {
209
209
  evtMapperPackageDirs[mappingsPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(mappingsPkg);
210
210
  let match$4 = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/EventMapperEntryPoint.mjs", evtMapperPackageDirs, undefined);
211
211
  let evtMapperName = baseName + "EventMapper";
212
- let evtMapperRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(evtMapperName, match$4.code, match$4.sourceCodeHash, evtMapperEnvVars, Math.max(spec.eventCollectorMemorySize, 2048), Math.max(spec.eventCollectorTimeout, 180), undefined, undefined, undefined, undefined, aggregateOpts);
212
+ let evtMapperRuntime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(evtMapperName, match$4.code, match$4.sourceCodeHash, evtMapperEnvVars, Math.max(spec.eventCollectorMemorySize, 2048), Math.max(spec.eventCollectorTimeout, 180), undefined, undefined, undefined, undefined, undefined, aggregateOpts);
213
213
  EventCollectorChannel_DynamoDbStream$ReventlessAws.connect(evtMapperName, [match$2], evtMapperRuntime, aggregateOpts);
214
214
  });
215
215
  }
@@ -152,7 +152,7 @@ function finish() {
152
152
  packageDirs[specPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(specPkg);
153
153
  packageDirs[behaviorPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(behaviorPkg);
154
154
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
155
- let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(lambdaName, match.code, match.sourceCodeHash, envVars, spec.memorySize, spec.timeout, undefined, undefined, undefined, undefined, aggregateOpts);
155
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(lambdaName, match.code, match.sourceCodeHash, envVars, spec.memorySize, spec.timeout, undefined, undefined, undefined, undefined, undefined, aggregateOpts);
156
156
  spec.connects.forEach(connect => connect(runtime));
157
157
  let channelSpecs = Stdlib_Option.mapOr(spec.eventCollectorChannelSpec, [], cs => [cs]);
158
158
  EventCollectorChannel_DynamoDbStream$ReventlessAws.connect(name, channelSpecs, runtime, aggregateOpts);
@@ -152,7 +152,7 @@ function finish() {
152
152
  packageDirs[specPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(specPkg);
153
153
  packageDirs[behaviorPkg] = Util_Bundle$ReventlessAws.resolvePackageRoot(behaviorPkg);
154
154
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
155
- let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, spec.memorySize, spec.timeout, undefined, undefined, undefined, undefined, aggregateOpts);
155
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, spec.memorySize, spec.timeout, undefined, undefined, undefined, undefined, undefined, aggregateOpts);
156
156
  spec.connects.forEach(connect => connect(runtime));
157
157
  let channelSpecs = Stdlib_Option.mapOr(spec.eventCollectorChannelSpec, [], cs => [cs]);
158
158
  EventCollectorChannel_DynamoDbStream$ReventlessAws.connect(name, channelSpecs, runtime, aggregateOpts);