@reventlessdev/reventless-aws 3.0.0-alpha.174 → 3.0.0-alpha.176

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 (71) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +10 -10
  3. package/src/Platform.res +115 -31
  4. package/src/Platform.res.mjs +217 -44
  5. package/src/adapter/DcbEventLog/DcbBackend.res +2 -2
  6. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +4 -1
  7. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
  8. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +14 -4
  9. package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +19 -14
  10. package/src/adapter/EventLog/EventLogBackend.res +82 -0
  11. package/src/adapter/EventLog/EventLogBackend.res.mjs +53 -0
  12. package/src/adapter/EventLog/EventLogStorage.res +15 -0
  13. package/src/adapter/EventLog/EventLogStorage.res.mjs +20 -1
  14. package/src/adapter/EventLog/EventLogStorage_Postgres.res +37 -0
  15. package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +21 -0
  16. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +26 -11
  17. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +15 -6
  18. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +78 -7
  19. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +30 -2
  20. package/src/adapter/Postgres/PgProjectionFeed.res +100 -0
  21. package/src/adapter/Postgres/PgProjectionFeed.res.mjs +54 -0
  22. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +192 -0
  23. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +141 -0
  24. package/src/adapter/QueryDb/QueryDbBackend.res +52 -0
  25. package/src/adapter/QueryDb/QueryDbBackend.res.mjs +44 -0
  26. package/src/adapter/QueryDb/QueryDbResolvers.res +48 -0
  27. package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +17 -1
  28. package/src/adapter/QueryDb/QueryDbStorage.res +44 -0
  29. package/src/adapter/QueryDb/QueryDbStorage.res.mjs +34 -1
  30. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +43 -0
  31. package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +21 -0
  32. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +19 -0
  33. package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +15 -0
  34. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +12 -1
  35. package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +2 -1
  36. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +83 -3
  37. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +46 -3
  38. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +83 -3
  39. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +48 -3
  40. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +130 -5
  41. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +66 -6
  42. package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +15 -10
  43. package/src/adapter/Runtime/ReadModelEntryPoint.mjs +32 -15
  44. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -12
  45. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +138 -8
  46. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +76 -11
  47. package/src/components/Aggregate_Builder_Single.res +12 -5
  48. package/src/components/Aggregate_Builder_Single.res.mjs +12 -6
  49. package/src/components/Aggregate_Builder_Single_Async.res +12 -5
  50. package/src/components/Aggregate_Builder_Single_Async.res.mjs +12 -6
  51. package/src/components/ReadModel_Builder_NoResolver.res.mjs +1 -1
  52. package/src/components/ReadModel_Builder_NoResolver_Stream.res +13 -4
  53. package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +14 -9
  54. package/src/components/ReadModel_Builder_Single.res +14 -5
  55. package/src/components/ReadModel_Builder_Single.res.mjs +15 -12
  56. package/src/components/ReadModel_Builder_Single_Stream.res +14 -5
  57. package/src/components/ReadModel_Builder_Single_Stream.res.mjs +15 -12
  58. package/src/components/StateViewSlice_Builder.res +10 -4
  59. package/src/components/StateViewSlice_Builder.res.mjs +8 -10
  60. package/src/components/StateViewSlice_Builder_Stream.res +10 -4
  61. package/src/components/StateViewSlice_Builder_Stream.res.mjs +8 -10
  62. package/src/plugin/runtime/PluginRuntime_Builder.res +12 -0
  63. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -0
  64. package/tests/EventCollectorChannelStreamRuntimeTest.res +55 -0
  65. package/tests/EventCollectorChannelStreamRuntimeTest.res.mjs +78 -0
  66. package/tests/PgChangeFeedRelay_IntegrationTest.res +259 -0
  67. package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +238 -0
  68. package/tests/PgPipeline_IntegrationTest.res +208 -0
  69. package/tests/PgPipeline_IntegrationTest.res.mjs +260 -0
  70. package/tests/PgQueryResolver_LambdaTest.res +206 -0
  71. package/tests/PgQueryResolver_LambdaTest.res.mjs +259 -0
@@ -0,0 +1,43 @@
1
+ // Deploy-time QueryDb storage for the Postgres backend (B3.1) — the read-model
2
+ // analogue of `EventLogStorage_Postgres` / `DcbEventLogStorage_Postgres`.
3
+ //
4
+ // Creates NO DynamoDB table and NO AppSync data source: read-model state lives in
5
+ // Postgres (`qdb_<name>`, JSONB) and is written by the projection Lambdas via the
6
+ // runtime ops. Returns:
7
+ // - `resources: []` — no stream, no direct AppSync resolvers (suppressed by
8
+ // `QueryDbResolvers.Selectable` until B3.2's Lambda data source);
9
+ // - `dataSourceName: ""` — nothing to attach resolvers to;
10
+ // - `operations` — the Postgres op set bound to the shared `PgConnection` pool.
11
+ // Schema setup is lazy (first operation), so nothing connects during
12
+ // `pulumi up` — a real query only runs inside a Lambda at runtime.
13
+ //
14
+ // Selected by `QueryDbStorage.Selectable(Stream)` when `QueryDbBackend` holds a
15
+ // selection and the read model is not admin-exempt.
16
+
17
+ type api = Types.AppSync.api
18
+ type role = Types.AppSync.role
19
+
20
+ let make: ReventlessCore.QueryDb_Adapter.storageMaker<api, role> = (
21
+ ~name,
22
+ ~indexes,
23
+ ~subIdField=?,
24
+ ~ttl as _=?,
25
+ ~api as _,
26
+ ~apiRole as _,
27
+ ~opts as _,
28
+ ) => {
29
+ let operations = switch QueryDbBackend.get() {
30
+ | Some({connectionConfig}) =>
31
+ connectionConfig->Pulumi.Output.apply(config =>
32
+ QueryDbStorage_Postgres_Runtime.opsFor(config, ~name, ~indexes, ~subIdField?)
33
+ )
34
+ | None =>
35
+ // Selectable only routes here when a selection is set; guard defensively.
36
+ JsError.throwWithMessage("QueryDbStorage_Postgres.make called without a QueryDbBackend selection")
37
+ }
38
+ {
39
+ ReventlessCore.QueryDb_Adapter.resources: [],
40
+ dataSourceName: ""->Pulumi.Output.make,
41
+ operations,
42
+ }
43
+ }
@@ -0,0 +1,21 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Pulumi from "@pulumi/pulumi";
4
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
5
+ import * as QueryDbBackend$ReventlessAws from "./QueryDbBackend.res.mjs";
6
+ import * as QueryDbStorage_Postgres_Runtime$ReventlessAws from "./QueryDbStorage_Postgres_Runtime.res.mjs";
7
+
8
+ function make(name, indexes, subIdField, param, param$1, param$2, param$3) {
9
+ let match = QueryDbBackend$ReventlessAws.get();
10
+ let operations = match !== undefined ? match.connectionConfig.apply(config => QueryDbStorage_Postgres_Runtime$ReventlessAws.opsFor(config, name, indexes, subIdField)) : Stdlib_JsError.throwWithMessage("QueryDbStorage_Postgres.make called without a QueryDbBackend selection");
11
+ return {
12
+ resources: [],
13
+ dataSourceName: Pulumi.output(""),
14
+ operations: operations
15
+ };
16
+ }
17
+
18
+ export {
19
+ make,
20
+ }
21
+ /* @pulumi/pulumi Not a pure module */
@@ -0,0 +1,19 @@
1
+ // AWS-side runtime QueryDb operations for the Postgres backend. Mirrors the role
2
+ // of QueryDbStorage_DynamoDb_Runtime: turn a resolved connection config into the
3
+ // read-model operation set, bound to the container-lifetime pool from PgRuntime.
4
+ // Consumed by ReadModelEntryPoint.mjs / StateViewSliceEntryPoint.mjs (Postgres
5
+ // branch, selected when the handler's HANDLER_CONFIG entry carries a
6
+ // `pgConnection`).
7
+ //
8
+ // `name` is the read-model spec name — the same discriminator the deploy-time
9
+ // maker used, so runtime and deploy-time ops target the same `qdb_<name>` table.
10
+
11
+ let opsFor = (
12
+ config: PgConnection.connectionConfig,
13
+ ~name: string,
14
+ ~indexes: array<Reventless.ReadModel.indexConfig>=[],
15
+ ~subIdField: option<string>=?,
16
+ ): ReventlessCore.QueryDb_Adapter.operations => {
17
+ let pool = PgRuntime.poolFor(config)
18
+ ReventlessPostgres.QueryDbStorage_Postgres.makeOperations(~pool, ~name, ~indexes, ~subIdField)
19
+ }
@@ -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 QueryDbStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/QueryDbStorage_Postgres.res.mjs";
5
+
6
+ function opsFor(config, name, indexesOpt, subIdField) {
7
+ let indexes = indexesOpt !== undefined ? indexesOpt : [];
8
+ let pool = PgRuntime$ReventlessAws.poolFor(config);
9
+ return QueryDbStorage_Postgres$ReventlessPostgres.makeOperations(pool, name, indexes, subIdField);
10
+ }
11
+
12
+ export {
13
+ opsFor,
14
+ }
15
+ /* PgRuntime-ReventlessAws Not a pure module */
@@ -144,7 +144,18 @@ let scanByTableName = (~tableName, ~filterConfigs, ~limit) => {
144
144
  }
145
145
 
146
146
  let make: ReventlessCore.QueryDb_Adapter.queryEngineMaker = allQueryDbs => {
147
- let allRuntimeQueryDbsOutputs = Dict.mapValues(allQueryDbs, (
147
+ // B3.1: Postgres-backed read models have NO DynamoDB resource — exclude them
148
+ // instead of failing findResource at deploy time. They are not reachable via
149
+ // this engine (getRuntimeResource raises its explicit not-found error at
150
+ // query time); see docs/plans/aws-postgres-querydb-adapter.md § B3.1b.
151
+ let dynamoDbQueryDbs =
152
+ allQueryDbs
153
+ ->Dict.toArray
154
+ ->Array.filter(((_, queryDb: ReventlessCore.QueryDb.outputs)) =>
155
+ queryDb.resources->Array.length > 0
156
+ )
157
+ ->Dict.fromArray
158
+ let allRuntimeQueryDbsOutputs = Dict.mapValues(dynamoDbQueryDbs, (
148
159
  queryDb: ReventlessCore.QueryDb.outputs,
149
160
  ) =>
150
161
  queryDb.resources
@@ -203,7 +203,8 @@ function scanByTableName(tableName, filterConfigs, limit) {
203
203
  }
204
204
 
205
205
  function make(allQueryDbs) {
206
- let allRuntimeQueryDbsOutputs = Pulumi.all(Stdlib_Dict.mapValues(allQueryDbs, queryDb => Adapter$ReventlessCore.resourceToResolvedOutput(Util_DynamoDb$ReventlessAws.findResource(queryDb.resources))));
206
+ let dynamoDbQueryDbs = Object.fromEntries(Object.entries(allQueryDbs).filter(param => param[1].resources.length !== 0));
207
+ let allRuntimeQueryDbsOutputs = Pulumi.all(Stdlib_Dict.mapValues(dynamoDbQueryDbs, queryDb => Adapter$ReventlessCore.resourceToResolvedOutput(Util_DynamoDb$ReventlessAws.findResource(queryDb.resources))));
207
208
  return allRuntimeQueryDbsOutputs.apply(allRuntimeQueryDbs => ({
208
209
  scan: (readModelName, filterConfigs, limit) => scanByTableName(Util_QueryDbRuntime$ReventlessCore.getRuntimeResource(allRuntimeQueryDbs, readModelName).name, filterConfigs, limit),
209
210
  query: (readModelName, key, id, subIdConfig, filterConfigs, ascending, limit) => queryByTableName(Util_QueryDbRuntime$ReventlessCore.getRuntimeResource(allRuntimeQueryDbs, readModelName).name, key, id, subIdConfig, filterConfigs, ascending, limit)
@@ -207,6 +207,30 @@ let finish = () =>
207
207
  let handlerOutputs: array<Pulumi.Output.t<string>> = []
208
208
  let packageDirs: dict<string> = Dict.make()
209
209
 
210
+ // Classic Postgres backend: add a `pgConnection` object to every handler
211
+ // entry so the AggregateEntryPoint Postgres branch activates (absent →
212
+ // DynamoDB path, unchanged). Whole-Lambda toggle — all aggregates follow
213
+ // the platform selection, matching EventLogStorage.Selectable.
214
+ let pgSelection = EventLogBackend.get()
215
+ let pgConnectionFragment = switch pgSelection {
216
+ | Some(sel) =>
217
+ sel.connectionConfig->Pulumi.Output.apply(cc => {
218
+ let pgConnectionJson =
219
+ [
220
+ ("host", cc.host->JSON.Encode.string),
221
+ ("port", cc.port->Int.toFloat->JSON.Encode.float),
222
+ ("database", cc.database->JSON.Encode.string),
223
+ ("username", cc.username->JSON.Encode.string),
224
+ ("secretArn", cc.secretArn->JSON.Encode.string),
225
+ ]
226
+ ->Dict.fromArray
227
+ ->JSON.Encode.object
228
+ ->JSON.stringify
229
+ `,"pgConnection":${pgConnectionJson}`
230
+ })
231
+ | None => Pulumi.Output.make("")
232
+ }
233
+
210
234
  specs->Array.forEach(spec => {
211
235
  switch aggregateInfos->Dict.get(spec.aggregateName) {
212
236
  | Some(info) =>
@@ -223,9 +247,14 @@ let finish = () =>
223
247
  info.behaviorModulePath->JSON.stringifyAny->Option.getOr(`""`)
224
248
 
225
249
  let handlerJson =
226
- Pulumi.Output.all3((info.eventLogTableName, spec.queueUrl, spec.queueArn))
227
- ->Pulumi.Output.apply(((table, queueUrl, queueArn)) =>
228
- `{"specModule":${specModule},"behaviorModule":${behaviorModule},"eventLogTable":"${table}","queueUrl":"${queueUrl}","queueArn":"${queueArn}"}`
250
+ Pulumi.Output.all4((
251
+ info.eventLogTableName,
252
+ spec.queueUrl,
253
+ spec.queueArn,
254
+ pgConnectionFragment,
255
+ ))
256
+ ->Pulumi.Output.apply(((table, queueUrl, queueArn, pgFragment)) =>
257
+ `{"specModule":${specModule},"behaviorModule":${behaviorModule},"eventLogTable":"${table}","queueUrl":"${queueUrl}","queueArn":"${queueArn}"${pgFragment}}`
229
258
  )
230
259
  let _ = handlerOutputs->Array.push(handlerJson)
231
260
  | None =>
@@ -270,6 +299,27 @@ let finish = () =>
270
299
  // so the next flavor's `finish()` starts from a clean slate.
271
300
  cfg.sqsBatchSize->Option.forEach(CommandTopicChannel.setBatchSize)
272
301
 
302
+ // Postgres-backed classic EventLog → the command Lambda talks to RDS, so it
303
+ // must land in-VPC on the DB-access security group + private subnets.
304
+ // makeFromCodeAsset adds the EC2-ENI IAM automatically when vpcConfig is
305
+ // present (C1).
306
+ let vpcConfig = switch pgSelection {
307
+ | Some(sel) =>
308
+ Some(
309
+ sel.securityGroupId
310
+ ->Pulumi.Output.apply(sgId =>
311
+ (
312
+ {
313
+ PulumiAws.Lambda.Function.subnetIds: sel.subnetIds->Pulumi.Input.make,
314
+ securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
315
+ }: PulumiAws.Lambda.Function.vpcConfig
316
+ )
317
+ )
318
+ ->Pulumi.Output.asInput,
319
+ )
320
+ | None => None
321
+ }
322
+
273
323
  let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
274
324
  ~name="AllAggregatesCmdHandler",
275
325
  ~code,
@@ -280,9 +330,39 @@ let finish = () =>
280
330
  ~reservedConcurrency=?cfg.reservedConcurrency,
281
331
  ~ephemeralStorageMb=?cfg.ephemeralStorageMb,
282
332
  ~logRetentionDays=?cfg.logRetentionDays,
333
+ ~vpcConfig=?vpcConfig,
283
334
  ~opts,
284
335
  )
285
336
 
337
+ // Grant the Postgres-backed command Lambda read access to the RDS-managed
338
+ // master secret so PgRuntime can resolve the DB password at cold start.
339
+ switch pgSelection {
340
+ | Some(sel) =>
341
+ let _ = sel.connectionConfig->Pulumi.Output.apply(cc => {
342
+ open PulumiAws.PolicyDocument
343
+ PulumiAws.IAM.RolePolicy.make(
344
+ ~name="AllAggregatesCmdHandler-pgSecret",
345
+ ~args={
346
+ policy: PulumiAws.PolicyDocument.make(
347
+ ~id="AllAggregatesCmdHandler-pgSecretPolicy",
348
+ ~statements=[
349
+ {
350
+ sid: "AllowGetPgSecret",
351
+ effect: Allow,
352
+ actions: Action("secretsmanager:GetSecretValue"),
353
+ resources: Resource(cc.secretArn),
354
+ },
355
+ ],
356
+ )
357
+ ->PulumiAws.PolicyDocument.toJsonString
358
+ ->Pulumi.Input.make,
359
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
360
+ },
361
+ )
362
+ })
363
+ | None => ()
364
+ }
365
+
286
366
  // Grant the AllAggregates Lambda dynamodb:Scan on the Plugin RM table so
287
367
  // the in-Lambda plugin status gate (AggregateEntryPoint.mjs::checkPluginStatus)
288
368
  // can read plugin status at command-dispatch time. Skipped if the platform
@@ -10,6 +10,7 @@ import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/
10
10
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
+ import * as EventLogBackend$ReventlessAws from "../EventLog/EventLogBackend.res.mjs";
13
14
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
14
15
  import * as CommandTopicChannel_SQS_Sync$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs";
15
16
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
@@ -154,6 +155,32 @@ function finish() {
154
155
  };
155
156
  let handlerOutputs = [];
156
157
  let packageDirs = {};
158
+ let pgSelection = EventLogBackend$ReventlessAws.get();
159
+ let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => {
160
+ let pgConnectionJson = JSON.stringify(Object.fromEntries([
161
+ [
162
+ "host",
163
+ cc.host
164
+ ],
165
+ [
166
+ "port",
167
+ cc.port
168
+ ],
169
+ [
170
+ "database",
171
+ cc.database
172
+ ],
173
+ [
174
+ "username",
175
+ cc.username
176
+ ],
177
+ [
178
+ "secretArn",
179
+ cc.secretArn
180
+ ]
181
+ ]));
182
+ return `,"pgConnection":` + pgConnectionJson;
183
+ }) : Pulumi.output("");
157
184
  specs.forEach(spec => {
158
185
  let info = aggregateInfos[spec.aggregateName];
159
186
  if (info === undefined) {
@@ -168,8 +195,9 @@ function finish() {
168
195
  let handlerJson = Pulumi.all([
169
196
  info.eventLogTableName,
170
197
  spec.queueUrl,
171
- spec.queueArn
172
- ]).apply(param => `{"specModule":` + specModule + `,"behaviorModule":` + behaviorModule + `,"eventLogTable":"` + param[0] + `","queueUrl":"` + param[1] + `","queueArn":"` + param[2] + `"}`);
198
+ spec.queueArn,
199
+ pgConnectionFragment
200
+ ]).apply(param => `{"specModule":` + specModule + `,"behaviorModule":` + behaviorModule + `,"eventLogTable":"` + param[0] + `","queueUrl":"` + param[1] + `","queueArn":"` + param[2] + `"` + param[3] + `}`);
173
201
  handlerOutputs.push(handlerJson);
174
202
  });
175
203
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
@@ -187,7 +215,22 @@ function finish() {
187
215
  }));
188
216
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
189
217
  Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS_Sync$ReventlessAws.setBatchSize);
190
- let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAggregatesCmdHandler", match.code, match.sourceCodeHash, envVars, cfg.memorySize, cfg.timeout, cfg.reservedConcurrency, cfg.ephemeralStorageMb, cfg.logRetentionDays, undefined, undefined, opts);
218
+ let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({
219
+ subnetIds: pgSelection.subnetIds,
220
+ securityGroupIds: [sgId]
221
+ })) : undefined;
222
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAggregatesCmdHandler", match.code, match.sourceCodeHash, envVars, cfg.memorySize, cfg.timeout, cfg.reservedConcurrency, cfg.ephemeralStorageMb, cfg.logRetentionDays, undefined, vpcConfig, opts);
223
+ if (pgSelection !== undefined) {
224
+ pgSelection.connectionConfig.apply(cc => new (Aws.iam.RolePolicy)("AllAggregatesCmdHandler-pgSecret", {
225
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllAggregatesCmdHandler-pgSecretPolicy", [{
226
+ Sid: "AllowGetPgSecret",
227
+ Effect: "Allow",
228
+ Action: "secretsmanager:GetSecretValue",
229
+ Resource: cc.secretArn
230
+ }])),
231
+ role: runtime.parts.lambdaRole.id
232
+ }));
233
+ }
191
234
  let tableName$1 = pluginRmTableName.contents;
192
235
  if (tableName$1 !== undefined) {
193
236
  Pulumi.all([
@@ -194,6 +194,30 @@ let finish = () =>
194
194
  let handlerOutputs: array<Pulumi.Output.t<string>> = []
195
195
  let packageDirs: dict<string> = Dict.make()
196
196
 
197
+ // Classic Postgres backend: add a `pgConnection` object to every handler
198
+ // entry so the AggregateEntryPoint Postgres branch activates (absent →
199
+ // DynamoDB path, unchanged). Whole-Lambda toggle — all aggregates follow
200
+ // the platform selection, matching EventLogStorage.Selectable.
201
+ let pgSelection = EventLogBackend.get()
202
+ let pgConnectionFragment = switch pgSelection {
203
+ | Some(sel) =>
204
+ sel.connectionConfig->Pulumi.Output.apply(cc => {
205
+ let pgConnectionJson =
206
+ [
207
+ ("host", cc.host->JSON.Encode.string),
208
+ ("port", cc.port->Int.toFloat->JSON.Encode.float),
209
+ ("database", cc.database->JSON.Encode.string),
210
+ ("username", cc.username->JSON.Encode.string),
211
+ ("secretArn", cc.secretArn->JSON.Encode.string),
212
+ ]
213
+ ->Dict.fromArray
214
+ ->JSON.Encode.object
215
+ ->JSON.stringify
216
+ `,"pgConnection":${pgConnectionJson}`
217
+ })
218
+ | None => Pulumi.Output.make("")
219
+ }
220
+
197
221
  specs->Array.forEach(spec => {
198
222
  switch aggregateInfos->Dict.get(spec.aggregateName) {
199
223
  | Some(info) =>
@@ -208,9 +232,14 @@ let finish = () =>
208
232
  info.behaviorModulePath->JSON.stringifyAny->Option.getOr(`""`)
209
233
 
210
234
  let handlerJson =
211
- Pulumi.Output.all3((info.eventLogTableName, spec.queueUrl, spec.queueArn))
212
- ->Pulumi.Output.apply(((table, queueUrl, queueArn)) =>
213
- `{"specModule":${specModule},"behaviorModule":${behaviorModule},"eventLogTable":"${table}","queueUrl":"${queueUrl}","queueArn":"${queueArn}"}`
235
+ Pulumi.Output.all4((
236
+ info.eventLogTableName,
237
+ spec.queueUrl,
238
+ spec.queueArn,
239
+ pgConnectionFragment,
240
+ ))
241
+ ->Pulumi.Output.apply(((table, queueUrl, queueArn, pgFragment)) =>
242
+ `{"specModule":${specModule},"behaviorModule":${behaviorModule},"eventLogTable":"${table}","queueUrl":"${queueUrl}","queueArn":"${queueArn}"${pgFragment}}`
214
243
  )
215
244
  let _ = handlerOutputs->Array.push(handlerJson)
216
245
  | None =>
@@ -249,6 +278,27 @@ let finish = () =>
249
278
 
250
279
  cfg.sqsBatchSize->Option.forEach(CommandTopicChannel.setBatchSize)
251
280
 
281
+ // Postgres-backed classic EventLog → the command Lambda talks to RDS, so it
282
+ // must land in-VPC on the DB-access security group + private subnets.
283
+ // makeFromCodeAsset adds the EC2-ENI IAM automatically when vpcConfig is
284
+ // present (C1).
285
+ let vpcConfig = switch pgSelection {
286
+ | Some(sel) =>
287
+ Some(
288
+ sel.securityGroupId
289
+ ->Pulumi.Output.apply(sgId =>
290
+ (
291
+ {
292
+ PulumiAws.Lambda.Function.subnetIds: sel.subnetIds->Pulumi.Input.make,
293
+ securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
294
+ }: PulumiAws.Lambda.Function.vpcConfig
295
+ )
296
+ )
297
+ ->Pulumi.Output.asInput,
298
+ )
299
+ | None => None
300
+ }
301
+
252
302
  let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
253
303
  ~name="AllAggregatesAsyncCmdHandler",
254
304
  ~code,
@@ -259,9 +309,39 @@ let finish = () =>
259
309
  ~reservedConcurrency=?cfg.reservedConcurrency,
260
310
  ~ephemeralStorageMb=?cfg.ephemeralStorageMb,
261
311
  ~logRetentionDays=?cfg.logRetentionDays,
312
+ ~vpcConfig=?vpcConfig,
262
313
  ~opts,
263
314
  )
264
315
 
316
+ // Grant the Postgres-backed command Lambda read access to the RDS-managed
317
+ // master secret so PgRuntime can resolve the DB password at cold start.
318
+ switch pgSelection {
319
+ | Some(sel) =>
320
+ let _ = sel.connectionConfig->Pulumi.Output.apply(cc => {
321
+ open PulumiAws.PolicyDocument
322
+ PulumiAws.IAM.RolePolicy.make(
323
+ ~name="AllAggregatesAsyncCmdHandler-pgSecret",
324
+ ~args={
325
+ policy: PulumiAws.PolicyDocument.make(
326
+ ~id="AllAggregatesAsyncCmdHandler-pgSecretPolicy",
327
+ ~statements=[
328
+ {
329
+ sid: "AllowGetPgSecret",
330
+ effect: Allow,
331
+ actions: Action("secretsmanager:GetSecretValue"),
332
+ resources: Resource(cc.secretArn),
333
+ },
334
+ ],
335
+ )
336
+ ->PulumiAws.PolicyDocument.toJsonString
337
+ ->Pulumi.Input.make,
338
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
339
+ },
340
+ )
341
+ })
342
+ | None => ()
343
+ }
344
+
265
345
  specs->Array.forEach(({connects}) => {
266
346
  connects->Array.forEach(connect => connect(~runtime))
267
347
  })
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Aws from "@pulumi/aws";
3
4
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
@@ -7,7 +8,9 @@ import * as Pulumi from "@pulumi/pulumi";
7
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
9
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
10
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
11
+ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
10
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
+ import * as EventLogBackend$ReventlessAws from "../EventLog/EventLogBackend.res.mjs";
11
14
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
12
15
  import * as CommandTopicChannel_SQS_Async$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS_Async.res.mjs";
13
16
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
@@ -144,6 +147,32 @@ function finish() {
144
147
  };
145
148
  let handlerOutputs = [];
146
149
  let packageDirs = {};
150
+ let pgSelection = EventLogBackend$ReventlessAws.get();
151
+ let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => {
152
+ let pgConnectionJson = JSON.stringify(Object.fromEntries([
153
+ [
154
+ "host",
155
+ cc.host
156
+ ],
157
+ [
158
+ "port",
159
+ cc.port
160
+ ],
161
+ [
162
+ "database",
163
+ cc.database
164
+ ],
165
+ [
166
+ "username",
167
+ cc.username
168
+ ],
169
+ [
170
+ "secretArn",
171
+ cc.secretArn
172
+ ]
173
+ ]));
174
+ return `,"pgConnection":` + pgConnectionJson;
175
+ }) : Pulumi.output("");
147
176
  specs.forEach(spec => {
148
177
  let info = aggregateInfos[spec.aggregateName];
149
178
  if (info === undefined) {
@@ -158,8 +187,9 @@ function finish() {
158
187
  let handlerJson = Pulumi.all([
159
188
  info.eventLogTableName,
160
189
  spec.queueUrl,
161
- spec.queueArn
162
- ]).apply(param => `{"specModule":` + specModule + `,"behaviorModule":` + behaviorModule + `,"eventLogTable":"` + param[0] + `","queueUrl":"` + param[1] + `","queueArn":"` + param[2] + `"}`);
190
+ spec.queueArn,
191
+ pgConnectionFragment
192
+ ]).apply(param => `{"specModule":` + specModule + `,"behaviorModule":` + behaviorModule + `,"eventLogTable":"` + param[0] + `","queueUrl":"` + param[1] + `","queueArn":"` + param[2] + `"` + param[3] + `}`);
163
193
  handlerOutputs.push(handlerJson);
164
194
  });
165
195
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
@@ -174,7 +204,22 @@ function finish() {
174
204
  }));
175
205
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
176
206
  Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS_Async$ReventlessAws.setBatchSize);
177
- let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAggregatesAsyncCmdHandler", match.code, match.sourceCodeHash, envVars, cfg.memorySize, cfg.timeout, cfg.reservedConcurrency, cfg.ephemeralStorageMb, cfg.logRetentionDays, undefined, undefined, opts);
207
+ let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({
208
+ subnetIds: pgSelection.subnetIds,
209
+ securityGroupIds: [sgId]
210
+ })) : undefined;
211
+ let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAggregatesAsyncCmdHandler", match.code, match.sourceCodeHash, envVars, cfg.memorySize, cfg.timeout, cfg.reservedConcurrency, cfg.ephemeralStorageMb, cfg.logRetentionDays, undefined, vpcConfig, opts);
212
+ if (pgSelection !== undefined) {
213
+ pgSelection.connectionConfig.apply(cc => new (Aws.iam.RolePolicy)("AllAggregatesAsyncCmdHandler-pgSecret", {
214
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllAggregatesAsyncCmdHandler-pgSecretPolicy", [{
215
+ Sid: "AllowGetPgSecret",
216
+ Effect: "Allow",
217
+ Action: "secretsmanager:GetSecretValue",
218
+ Resource: cc.secretArn
219
+ }])),
220
+ role: runtime.parts.lambdaRole.id
221
+ }));
222
+ }
178
223
  specs.forEach(param => {
179
224
  param.connects.forEach(connect => connect(runtime));
180
225
  });