@reventlessdev/reventless-aws 3.0.0-alpha.175 → 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.
- package/CHANGELOG.md +13 -0
- package/package.json +10 -10
- package/src/Platform.res +115 -31
- package/src/Platform.res.mjs +217 -44
- package/src/adapter/DcbEventLog/DcbBackend.res +2 -2
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res +4 -1
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs +1 -1
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res +14 -4
- package/src/adapter/EventCollector/EventCollectorChannel_DynamoDbStream_Runtime.res.mjs +19 -14
- package/src/adapter/EventLog/EventLogBackend.res +82 -0
- package/src/adapter/EventLog/EventLogBackend.res.mjs +53 -0
- package/src/adapter/EventLog/EventLogStorage.res +15 -0
- package/src/adapter/EventLog/EventLogStorage.res.mjs +20 -1
- package/src/adapter/EventLog/EventLogStorage_Postgres.res +37 -0
- package/src/adapter/EventLog/EventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +26 -11
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +15 -6
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +54 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +23 -0
- package/src/adapter/Postgres/PgProjectionFeed.res +100 -0
- package/src/adapter/Postgres/PgProjectionFeed.res.mjs +54 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +192 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +141 -0
- package/src/adapter/QueryDb/QueryDbBackend.res +52 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +44 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res +48 -0
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +17 -1
- package/src/adapter/QueryDb/QueryDbStorage.res +44 -0
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +34 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +43 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +21 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +19 -0
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +15 -0
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +12 -1
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs +2 -1
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +83 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +46 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +83 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +48 -3
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +130 -5
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +66 -6
- package/src/adapter/Runtime/PgChangeFeedRelayEntryPoint.mjs +15 -10
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +32 -15
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +26 -12
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +138 -8
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +76 -11
- package/src/components/Aggregate_Builder_Single.res +12 -5
- package/src/components/Aggregate_Builder_Single.res.mjs +12 -6
- package/src/components/Aggregate_Builder_Single_Async.res +12 -5
- package/src/components/Aggregate_Builder_Single_Async.res.mjs +12 -6
- package/src/components/ReadModel_Builder_NoResolver.res.mjs +1 -1
- package/src/components/ReadModel_Builder_NoResolver_Stream.res +13 -4
- package/src/components/ReadModel_Builder_NoResolver_Stream.res.mjs +14 -9
- package/src/components/ReadModel_Builder_Single.res +14 -5
- package/src/components/ReadModel_Builder_Single.res.mjs +15 -12
- package/src/components/ReadModel_Builder_Single_Stream.res +14 -5
- package/src/components/ReadModel_Builder_Single_Stream.res.mjs +15 -12
- package/src/components/StateViewSlice_Builder.res +10 -4
- package/src/components/StateViewSlice_Builder.res.mjs +8 -10
- package/src/components/StateViewSlice_Builder_Stream.res +10 -4
- package/src/components/StateViewSlice_Builder_Stream.res.mjs +8 -10
- package/src/plugin/runtime/PluginRuntime_Builder.res +12 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -0
- package/tests/EventCollectorChannelStreamRuntimeTest.res +55 -0
- package/tests/EventCollectorChannelStreamRuntimeTest.res.mjs +78 -0
- package/tests/PgChangeFeedRelay_IntegrationTest.res +115 -4
- package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +84 -0
- package/tests/PgPipeline_IntegrationTest.res +208 -0
- package/tests/PgPipeline_IntegrationTest.res.mjs +260 -0
- package/tests/PgQueryResolver_LambdaTest.res +206 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +259 -0
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
|
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
|
});
|
|
@@ -10,6 +10,10 @@ type readModelInfo = {
|
|
|
10
10
|
specModulePath: string,
|
|
11
11
|
mappingsModulePath: string,
|
|
12
12
|
queryDbTableName: Pulumi.Output.t<string>,
|
|
13
|
+
/** B3.1: this read model's QueryDb is Postgres-backed — its HANDLER_CONFIG
|
|
14
|
+
entry carries a `pgConnection` (admin-exempt read models stay DynamoDB,
|
|
15
|
+
so the flag is per read model, not per Lambda). */
|
|
16
|
+
pgBacked: bool,
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
let readModelInfos: dict<readModelInfo> = Dict.make()
|
|
@@ -19,10 +23,11 @@ let registerReadModel = (
|
|
|
19
23
|
~specModulePath,
|
|
20
24
|
~mappingsModulePath,
|
|
21
25
|
~queryDbTableName,
|
|
26
|
+
~pgBacked=false,
|
|
22
27
|
) =>
|
|
23
28
|
readModelInfos->Dict.set(
|
|
24
29
|
readModelName,
|
|
25
|
-
{specModulePath, mappingsModulePath, queryDbTableName},
|
|
30
|
+
{specModulePath, mappingsModulePath, queryDbTableName, pgBacked},
|
|
26
31
|
)
|
|
27
32
|
|
|
28
33
|
type storedSpec = {
|
|
@@ -110,6 +115,54 @@ let finish = () =>
|
|
|
110
115
|
switch grandParent.contents {
|
|
111
116
|
| Some(parent) =>
|
|
112
117
|
let opts = {Pulumi.ComponentResource.parent: parent}
|
|
118
|
+
let customOpts =
|
|
119
|
+
opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
120
|
+
|
|
121
|
+
// B3.0: Postgres-backed source logs have no DynamoDB stream — provision
|
|
122
|
+
// the SQS feed queue the change-feed relay fans events into. Its ARN
|
|
123
|
+
// doubles as the handlers' sourceUrn dispatch key (per-projection
|
|
124
|
+
// filtering makes over-delivery a no-op).
|
|
125
|
+
let feedQueue =
|
|
126
|
+
EventLogBackend.isPostgres() || DcbBackend.isPostgres()
|
|
127
|
+
? Some(
|
|
128
|
+
PgProjectionFeed.makeQueue(
|
|
129
|
+
~name="AllReadModelsFeed",
|
|
130
|
+
~scope="aws-rm-feed",
|
|
131
|
+
~includeClassic=true,
|
|
132
|
+
~includeDcb=true,
|
|
133
|
+
~opts=customOpts,
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
: None
|
|
137
|
+
let feedArnOutput = switch feedQueue {
|
|
138
|
+
| Some(queue) => queue.arn
|
|
139
|
+
| None => Pulumi.Output.make("")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// B3.1: Postgres-backed read models get a `pgConnection` in their handler
|
|
143
|
+
// entry (per read model — admin-exempt ones stay DynamoDB in the same
|
|
144
|
+
// Lambda). The Lambda itself goes in-VPC when any handler is pg-backed.
|
|
145
|
+
let qdbSelection = QueryDbBackend.get()
|
|
146
|
+
let anyPgBacked =
|
|
147
|
+
readModelInfos->Dict.valuesToArray->Array.some(info => info.pgBacked)
|
|
148
|
+
let pgConnectionFragment = switch (qdbSelection, anyPgBacked) {
|
|
149
|
+
| (Some(sel), true) =>
|
|
150
|
+
sel.connectionConfig->Pulumi.Output.apply(cc => {
|
|
151
|
+
let pgConnectionJson =
|
|
152
|
+
[
|
|
153
|
+
("host", cc.host->JSON.Encode.string),
|
|
154
|
+
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
155
|
+
("database", cc.database->JSON.Encode.string),
|
|
156
|
+
("username", cc.username->JSON.Encode.string),
|
|
157
|
+
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
158
|
+
]
|
|
159
|
+
->Dict.fromArray
|
|
160
|
+
->JSON.Encode.object
|
|
161
|
+
->JSON.stringify
|
|
162
|
+
`,"pgConnection":${pgConnectionJson}`
|
|
163
|
+
})
|
|
164
|
+
| _ => Pulumi.Output.make("")
|
|
165
|
+
}
|
|
113
166
|
|
|
114
167
|
let handlerOutputs: array<Pulumi.Output.t<string>> = []
|
|
115
168
|
let packageDirs: dict<string> = Dict.make()
|
|
@@ -136,11 +189,21 @@ let finish = () =>
|
|
|
136
189
|
let mappingsModule =
|
|
137
190
|
info.mappingsModulePath->JSON.stringifyAny->Option.getOr(`""`)
|
|
138
191
|
|
|
192
|
+
let handlerPgFragment = info.pgBacked
|
|
193
|
+
? pgConnectionFragment
|
|
194
|
+
: Pulumi.Output.make("")
|
|
139
195
|
let handlerJson =
|
|
140
|
-
Pulumi.Output.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
196
|
+
Pulumi.Output.all4((
|
|
197
|
+
info.queryDbTableName,
|
|
198
|
+
spec.sourceUrns,
|
|
199
|
+
feedArnOutput,
|
|
200
|
+
handlerPgFragment,
|
|
201
|
+
))
|
|
202
|
+
->Pulumi.Output.apply(((tableName, urns, feedArn, pgFragment)) => {
|
|
203
|
+
// No stream resource (Postgres-backed source) → dispatch on the
|
|
204
|
+
// feed queue's ARN instead of a stream URN.
|
|
205
|
+
let sourceUrn = urns->Array.get(0)->Option.getOr(feedArn)
|
|
206
|
+
`{"specModule":${specModule},"mappingsModule":${mappingsModule},"queryDbTableName":"${tableName}","sourceUrn":"${sourceUrn}"${pgFragment}}`
|
|
144
207
|
})
|
|
145
208
|
let _ = handlerOutputs->Array.push(handlerJson)
|
|
146
209
|
| None =>
|
|
@@ -165,6 +228,25 @@ let finish = () =>
|
|
|
165
228
|
~packageDirs,
|
|
166
229
|
)
|
|
167
230
|
|
|
231
|
+
// Postgres-backed read models → the projection Lambda writes to RDS, so
|
|
232
|
+
// it must land in-VPC on the DB-access security group + private subnets.
|
|
233
|
+
let vpcConfig = switch (qdbSelection, anyPgBacked) {
|
|
234
|
+
| (Some(sel), true) =>
|
|
235
|
+
Some(
|
|
236
|
+
sel.securityGroupId
|
|
237
|
+
->Pulumi.Output.apply(sgId =>
|
|
238
|
+
(
|
|
239
|
+
{
|
|
240
|
+
PulumiAws.Lambda.Function.subnetIds: sel.subnetIds->Pulumi.Input.make,
|
|
241
|
+
securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
|
|
242
|
+
}: PulumiAws.Lambda.Function.vpcConfig
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
->Pulumi.Output.asInput,
|
|
246
|
+
)
|
|
247
|
+
| _ => None
|
|
248
|
+
}
|
|
249
|
+
|
|
168
250
|
let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
|
|
169
251
|
~name="AllReadModels",
|
|
170
252
|
~code,
|
|
@@ -172,9 +254,39 @@ let finish = () =>
|
|
|
172
254
|
~envVars,
|
|
173
255
|
~memorySize=maxMemorySize,
|
|
174
256
|
~timeout=maxTimeout,
|
|
257
|
+
~vpcConfig=?vpcConfig,
|
|
175
258
|
~opts,
|
|
176
259
|
)
|
|
177
260
|
|
|
261
|
+
// Grant the Postgres-backed projection Lambda read access to the
|
|
262
|
+
// RDS-managed master secret so PgRuntime can resolve the DB password.
|
|
263
|
+
switch (qdbSelection, anyPgBacked) {
|
|
264
|
+
| (Some(sel), true) =>
|
|
265
|
+
let _ = sel.connectionConfig->Pulumi.Output.apply(cc => {
|
|
266
|
+
open PulumiAws.PolicyDocument
|
|
267
|
+
PulumiAws.IAM.RolePolicy.make(
|
|
268
|
+
~name="AllReadModels-pgSecret",
|
|
269
|
+
~args={
|
|
270
|
+
policy: PulumiAws.PolicyDocument.make(
|
|
271
|
+
~id="AllReadModels-pgSecretPolicy",
|
|
272
|
+
~statements=[
|
|
273
|
+
{
|
|
274
|
+
sid: "AllowGetPgSecret",
|
|
275
|
+
effect: Allow,
|
|
276
|
+
actions: Action("secretsmanager:GetSecretValue"),
|
|
277
|
+
resources: Resource(cc.secretArn),
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
)
|
|
281
|
+
->PulumiAws.PolicyDocument.toJsonString
|
|
282
|
+
->Pulumi.Input.make,
|
|
283
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
| _ => ()
|
|
288
|
+
}
|
|
289
|
+
|
|
178
290
|
let channelSpecs = storedSpecs->Array.map(({channelSpec}) => channelSpec)
|
|
179
291
|
let _connectResources = EventCollectorChannel.connect(
|
|
180
292
|
~name="AllReadModels",
|
|
@@ -182,6 +294,19 @@ let finish = () =>
|
|
|
182
294
|
~runtime,
|
|
183
295
|
~opts,
|
|
184
296
|
)
|
|
297
|
+
|
|
298
|
+
// B3.0: wire the Postgres feed queue into the Lambda (ESM + receive IAM).
|
|
299
|
+
switch feedQueue {
|
|
300
|
+
| Some(queue) =>
|
|
301
|
+
PgProjectionFeed.connect(
|
|
302
|
+
~name="AllReadModelsFeed",
|
|
303
|
+
~queue,
|
|
304
|
+
~lambda=runtime.parts.lambda,
|
|
305
|
+
~lambdaRole=runtime.parts.lambdaRole,
|
|
306
|
+
~opts=customOpts,
|
|
307
|
+
)
|
|
308
|
+
| None => ()
|
|
309
|
+
}
|
|
185
310
|
| None =>
|
|
186
311
|
log.warn(
|
|
187
312
|
~comp="EventCollectorRuntime_Builder_Single",
|