@reventlessdev/reventless-aws 3.0.0-alpha.177 → 3.0.0-alpha.179
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 +21 -0
- package/docker-compose.postgres.yml +21 -0
- package/package.json +6 -5
- package/scripts/run-pg-integration-tests.sh +58 -0
- package/src/Platform.res +37 -0
- package/src/Platform.res.mjs +41 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +7 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +10 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +7 -1
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +10 -1
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +6 -21
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +2 -17
- package/src/adapter/DcbEventLog/DcbBackend.res +4 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +2 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +7 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res +4 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres_Runtime.res.mjs +2 -2
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res +3 -1
- package/src/adapter/EventLog/EventLogStorage_Postgres_Runtime.res.mjs +2 -2
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +1 -11
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +2 -24
- package/src/adapter/Postgres/PgConnection.res +70 -10
- package/src/adapter/Postgres/PgConnection.res.mjs +48 -3
- package/src/adapter/Postgres/PgMigration_Builder.res +127 -0
- package/src/adapter/Postgres/PgMigration_Builder.res.mjs +51 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res +11 -13
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +5 -24
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +51 -3
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +48 -18
- package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +31 -7
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +16 -10
- package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +1 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res +3 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres_Runtime.res.mjs +2 -2
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +3 -14
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +2 -25
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +3 -14
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +2 -25
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +7 -1
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +78 -15
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +43 -26
- package/src/adapter/Runtime/PgMigrationEntryPoint.mjs +40 -0
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +17 -5
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +10 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +3 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +9 -14
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +2 -25
- package/src/adapter/Runtime/StateTopicPublish.mjs +204 -0
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +17 -3
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +86 -17
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +50 -33
- package/src/adapter/StateTopic/StateTopic_AppSync.res +7 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +10 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res +7 -1
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +10 -1
- package/src/util/Util_Bundle.res +71 -1
- package/src/util/Util_Bundle.res.mjs +49 -1
- package/tests/PgChangeFeedRelay_IntegrationTest.res +3 -3
- package/tests/PgConnectionJsonTest.res +84 -0
- package/tests/PgConnectionJsonTest.res.mjs +68 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res +82 -0
- package/tests/PgMigrationEntryPoint_IntegrationTest.res.mjs +63 -0
- package/tests/PgPipeline_IntegrationTest.res +3 -1
- package/tests/PgQueryResolver_LambdaTest.res +59 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +95 -16
- package/tests/StateTopicPublishTest.res +162 -0
- package/tests/StateTopicPublishTest.res.mjs +158 -0
- package/tests/Util_BundleEsmLoaderTest.res +57 -0
- package/tests/Util_BundleEsmLoaderTest.res.mjs +44 -0
|
@@ -35,6 +35,32 @@ type connectionConfig = {
|
|
|
35
35
|
secretArn: string,
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Serialize a resolved `connectionConfig` into the `pgConnection` JSON object
|
|
39
|
+
the runtime entry points read from `HANDLER_CONFIG` (and `PgRuntime.poolFor`
|
|
40
|
+
reconstructs). One shared producer so all seven deploy-time builders emit a
|
|
41
|
+
byte-identical shape — the field set is a cold-start contract with the entry
|
|
42
|
+
points, so a drift here silently breaks a Lambda. `~lockStrategy` is included
|
|
43
|
+
only on the DCB command path (the sole Postgres append that takes a lock);
|
|
44
|
+
omit it everywhere else. Kept pure (no Pulumi) so it is directly unit-testable. */
|
|
45
|
+
let connectionConfigToJson = (
|
|
46
|
+
~lockStrategy: option<ReventlessPostgres.DcbEventLogStorage_Postgres.lockStrategy>=?,
|
|
47
|
+
cc: connectionConfig,
|
|
48
|
+
): JSON.t => {
|
|
49
|
+
let fields = [
|
|
50
|
+
("host", cc.host->JSON.Encode.string),
|
|
51
|
+
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
52
|
+
("database", cc.database->JSON.Encode.string),
|
|
53
|
+
("username", cc.username->JSON.Encode.string),
|
|
54
|
+
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
55
|
+
]
|
|
56
|
+
switch lockStrategy {
|
|
57
|
+
| Some(#AdvisoryLocks) => fields->Array.push(("lockStrategy", "AdvisoryLocks"->JSON.Encode.string))
|
|
58
|
+
| Some(#RowLocks) => fields->Array.push(("lockStrategy", "RowLocks"->JSON.Encode.string))
|
|
59
|
+
| None => ()
|
|
60
|
+
}
|
|
61
|
+
fields->Dict.fromArray->JSON.Encode.object
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
type t = {
|
|
39
65
|
/** Deploy-time resources (currently the RDS instance) for dependency wiring —
|
|
40
66
|
e.g. the A3 migration Lambda and the B-phase adapters order after these. */
|
|
@@ -46,6 +72,14 @@ type t = {
|
|
|
46
72
|
/** The DB's private subnets, echoed back for the Lambda `vpcConfig` so
|
|
47
73
|
handlers land in the same subnets as the database. */
|
|
48
74
|
subnetIds: array<Pulumi.Input.t<string>>,
|
|
75
|
+
/** DCB append lock strategy (C2). `#AdvisoryLocks` (default) is
|
|
76
|
+
transaction-scoped and lowest-overhead when Lambdas hold a few long-lived
|
|
77
|
+
pools **directly** against RDS, but it **pins connections on RDS Proxy**
|
|
78
|
+
(advisory locks are session state Proxy cannot multiplex). Choose
|
|
79
|
+
`#RowLocks` when fronting the DB with RDS Proxy so the Proxy can share
|
|
80
|
+
connections across invocations. Threaded to the DCB command Lambda via
|
|
81
|
+
`DcbBackend`; classic EventLog and QueryDb appends take no such lock. */
|
|
82
|
+
lockStrategy: ReventlessPostgres.DcbEventLogStorage_Postgres.lockStrategy,
|
|
49
83
|
}
|
|
50
84
|
|
|
51
85
|
/** Postgres wire port. */
|
|
@@ -65,8 +99,13 @@ let make = (
|
|
|
65
99
|
~backupRetentionPeriod=7,
|
|
66
100
|
~deletionProtection=true,
|
|
67
101
|
~skipFinalSnapshot=false,
|
|
102
|
+
~lockStrategy: ReventlessPostgres.DcbEventLogStorage_Postgres.lockStrategy=#AdvisoryLocks,
|
|
68
103
|
~opts: option<Pulumi.ComponentResource.options>=?,
|
|
69
104
|
): t => {
|
|
105
|
+
// Keep the caller's ComponentResource.options for sub-builders that take it
|
|
106
|
+
// directly (PgMigration_Builder → RuntimeEnvironment_Lambda); the RDS/SG/subnet
|
|
107
|
+
// resources below want the CustomResourceOptions form.
|
|
108
|
+
let componentOpts = opts
|
|
70
109
|
let opts =
|
|
71
110
|
opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
|
|
72
111
|
|
|
@@ -151,19 +190,40 @@ let make = (
|
|
|
151
190
|
},
|
|
152
191
|
})
|
|
153
192
|
|
|
193
|
+
// A3: run PgSchema.ensureSchema against the fresh instance via a one-shot
|
|
194
|
+
// in-VPC migration Lambda invoked during `pulumi up`. Its resources join the
|
|
195
|
+
// instance's so the B-phase storage adapters can order after the schema exists.
|
|
196
|
+
// The connection is passed as pre-serialized pieces to avoid a module cycle
|
|
197
|
+
// (PgMigration_Builder must not reference this module's `connectionConfig`).
|
|
198
|
+
let migrationHandlerConfig = connectionConfig->Pulumi.Output.apply(cc =>
|
|
199
|
+
`{"pgConnection":${cc->connectionConfigToJson->JSON.stringify}}`
|
|
200
|
+
)
|
|
201
|
+
let migrationResources = PgMigration_Builder.make(
|
|
202
|
+
~name=`${name}-pg-migrate`,
|
|
203
|
+
~handlerConfig=migrationHandlerConfig,
|
|
204
|
+
~secretArn=connectionConfig->Pulumi.Output.apply(cc => cc.secretArn),
|
|
205
|
+
~securityGroupId=sg.id,
|
|
206
|
+
~subnetIds,
|
|
207
|
+
~opts=?componentOpts,
|
|
208
|
+
)
|
|
209
|
+
|
|
154
210
|
{
|
|
155
|
-
resources:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
211
|
+
resources: Array.concat(
|
|
212
|
+
[
|
|
213
|
+
ReventlessInfra.Adapter.make(
|
|
214
|
+
~name=name->Pulumi.Output.make,
|
|
215
|
+
~id=instance.id,
|
|
216
|
+
~urn=instance.arn,
|
|
217
|
+
~service="aws:rds"->Pulumi.Output.make,
|
|
218
|
+
~role="postgres"->Pulumi.Output.make,
|
|
219
|
+
~resourceType="aws:rds:Instance"->Pulumi.Output.make,
|
|
220
|
+
),
|
|
221
|
+
],
|
|
222
|
+
migrationResources,
|
|
223
|
+
),
|
|
165
224
|
connectionConfig,
|
|
166
225
|
securityGroupId: sg.id,
|
|
167
226
|
subnetIds,
|
|
227
|
+
lockStrategy,
|
|
168
228
|
}
|
|
169
229
|
}
|
|
@@ -9,6 +9,7 @@ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js
|
|
|
9
9
|
import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
|
|
10
10
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
11
11
|
import * as EC2_SecurityGroup$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/EC2/EC2_SecurityGroup.res.mjs";
|
|
12
|
+
import * as PgMigration_Builder$ReventlessAws from "./PgMigration_Builder.res.mjs";
|
|
12
13
|
|
|
13
14
|
let connectionConfigSchema = S.schema(s => ({
|
|
14
15
|
host: s.m(S.string),
|
|
@@ -18,7 +19,46 @@ let connectionConfigSchema = S.schema(s => ({
|
|
|
18
19
|
secretArn: s.m(S.string)
|
|
19
20
|
}));
|
|
20
21
|
|
|
21
|
-
function
|
|
22
|
+
function connectionConfigToJson(lockStrategy, cc) {
|
|
23
|
+
let fields = [
|
|
24
|
+
[
|
|
25
|
+
"host",
|
|
26
|
+
cc.host
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
"port",
|
|
30
|
+
cc.port
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
"database",
|
|
34
|
+
cc.database
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
"username",
|
|
38
|
+
cc.username
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
"secretArn",
|
|
42
|
+
cc.secretArn
|
|
43
|
+
]
|
|
44
|
+
];
|
|
45
|
+
if (lockStrategy !== undefined) {
|
|
46
|
+
if (lockStrategy === "AdvisoryLocks") {
|
|
47
|
+
fields.push([
|
|
48
|
+
"lockStrategy",
|
|
49
|
+
"AdvisoryLocks"
|
|
50
|
+
]);
|
|
51
|
+
} else {
|
|
52
|
+
fields.push([
|
|
53
|
+
"lockStrategy",
|
|
54
|
+
"RowLocks"
|
|
55
|
+
]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Object.fromEntries(fields);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function make(name, vpcId, subnetIds, databaseNameOpt, usernameOpt, engineVersionOpt, instanceClassOpt, allocatedStorageOpt, multiAzOpt, storageEncryptedOpt, backupRetentionPeriodOpt, deletionProtectionOpt, skipFinalSnapshotOpt, lockStrategyOpt, opts) {
|
|
22
62
|
let databaseName = databaseNameOpt !== undefined ? databaseNameOpt : "reventless";
|
|
23
63
|
let username = usernameOpt !== undefined ? usernameOpt : "reventless_admin";
|
|
24
64
|
let engineVersion = engineVersionOpt !== undefined ? engineVersionOpt : "16";
|
|
@@ -29,6 +69,7 @@ function make(name, vpcId, subnetIds, databaseNameOpt, usernameOpt, engineVersio
|
|
|
29
69
|
let backupRetentionPeriod = backupRetentionPeriodOpt !== undefined ? backupRetentionPeriodOpt : 7;
|
|
30
70
|
let deletionProtection = deletionProtectionOpt !== undefined ? deletionProtectionOpt : true;
|
|
31
71
|
let skipFinalSnapshot = skipFinalSnapshotOpt !== undefined ? skipFinalSnapshotOpt : false;
|
|
72
|
+
let lockStrategy = lockStrategyOpt !== undefined ? lockStrategyOpt : "AdvisoryLocks";
|
|
32
73
|
let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
|
|
33
74
|
let tags = Object.fromEntries([
|
|
34
75
|
[
|
|
@@ -94,11 +135,14 @@ function make(name, vpcId, subnetIds, databaseNameOpt, usernameOpt, engineVersio
|
|
|
94
135
|
secretArn: secret !== undefined ? secret.secretArn : Stdlib_JsError.throwWithMessage("RDS instance exposes no master user secret — manageMasterUserPassword must be enabled")
|
|
95
136
|
};
|
|
96
137
|
});
|
|
138
|
+
let migrationHandlerConfig = connectionConfig.apply(cc => `{"pgConnection":` + JSON.stringify(connectionConfigToJson(undefined, cc)) + `}`);
|
|
139
|
+
let migrationResources = PgMigration_Builder$ReventlessAws.make(name + `-pg-migrate`, migrationHandlerConfig, connectionConfig.apply(cc => cc.secretArn), sg.id, subnetIds, opts);
|
|
97
140
|
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)],
|
|
141
|
+
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)].concat(migrationResources),
|
|
99
142
|
connectionConfig: connectionConfig,
|
|
100
143
|
securityGroupId: sg.id,
|
|
101
|
-
subnetIds: subnetIds
|
|
144
|
+
subnetIds: subnetIds,
|
|
145
|
+
lockStrategy: lockStrategy
|
|
102
146
|
};
|
|
103
147
|
}
|
|
104
148
|
|
|
@@ -106,6 +150,7 @@ let port = 5432;
|
|
|
106
150
|
|
|
107
151
|
export {
|
|
108
152
|
connectionConfigSchema,
|
|
153
|
+
connectionConfigToJson,
|
|
109
154
|
port,
|
|
110
155
|
make,
|
|
111
156
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Deploy-time builder for the one-shot Postgres schema migration (A3).
|
|
2
|
+
//
|
|
3
|
+
// Provisions an in-VPC Lambda whose only job is to run `PgSchema.ensureSchema`
|
|
4
|
+
// against the freshly-provisioned RDS instance, and an `aws.lambda.Invocation`
|
|
5
|
+
// resource that invokes it once during `pulumi up`. Because the invocation runs
|
|
6
|
+
// the Lambda *inside* the DB's VPC, the deploy runner itself needs no network
|
|
7
|
+
// path to the private instance — the reason the plan (A3) chose a migration
|
|
8
|
+
// Lambda over a Pulumi `command`/dynamic resource that would have to connect
|
|
9
|
+
// from the runner.
|
|
10
|
+
//
|
|
11
|
+
// `PgConnection.make` calls this after creating the instance so every
|
|
12
|
+
// `PgConnection` provisions its own schema; the returned resources are appended
|
|
13
|
+
// to `PgConnection.t.resources` so the B-phase storage adapters order after the
|
|
14
|
+
// migration. `ensureSchema` is idempotent (all `IF NOT EXISTS`), so the
|
|
15
|
+
// invocation re-runs safely whenever the migration code or the Reventless Lambda
|
|
16
|
+
// layer (which ships the DDL) changes.
|
|
17
|
+
|
|
18
|
+
// Takes the connection as already-serialized pieces rather than the
|
|
19
|
+
// `PgConnection.connectionConfig` record: `PgConnection` calls this builder, so
|
|
20
|
+
// referencing its type here would form a module cycle. The caller passes:
|
|
21
|
+
// ~handlerConfig — the full HANDLER_CONFIG JSON (`{"pgConnection":{…}}`),
|
|
22
|
+
// ~secretArn — the DB's RDS-managed master secret ARN (for the IAM grant).
|
|
23
|
+
// ~securityGroupId: DB-access SG the migration Lambda attaches (PgConnection.securityGroupId).
|
|
24
|
+
// ~subnetIds: private subnets for the in-VPC migration Lambda (PgConnection.subnetIds).
|
|
25
|
+
let make = (
|
|
26
|
+
~name: string,
|
|
27
|
+
~handlerConfig: Pulumi.Output.t<string>,
|
|
28
|
+
~secretArn: Pulumi.Output.t<string>,
|
|
29
|
+
~securityGroupId: Pulumi.Output.t<string>,
|
|
30
|
+
~subnetIds: array<Pulumi.Input.t<string>>,
|
|
31
|
+
~opts: option<Pulumi.ComponentResource.options>=?,
|
|
32
|
+
): array<ReventlessInfra.Adapter.resource> => {
|
|
33
|
+
open PulumiAws
|
|
34
|
+
|
|
35
|
+
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
36
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/PgMigrationEntryPoint.mjs",
|
|
37
|
+
~packageDirs=Dict.make(),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
let envVars = Dict.fromArray([("HANDLER_CONFIG", handlerConfig->Pulumi.Output.asInput)])
|
|
41
|
+
|
|
42
|
+
let vpcConfig =
|
|
43
|
+
securityGroupId
|
|
44
|
+
->Pulumi.Output.apply(sgId =>
|
|
45
|
+
(
|
|
46
|
+
{
|
|
47
|
+
Lambda.Function.subnetIds: subnetIds->Pulumi.Input.make,
|
|
48
|
+
securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
|
|
49
|
+
}: Lambda.Function.vpcConfig
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
->Pulumi.Output.asInput
|
|
53
|
+
|
|
54
|
+
let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
|
|
55
|
+
~name,
|
|
56
|
+
~code,
|
|
57
|
+
~sourceCodeHash,
|
|
58
|
+
~envVars,
|
|
59
|
+
~vpcConfig,
|
|
60
|
+
~opts?,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// IAM: GetSecretValue on the DB's RDS-managed master secret. Built outside an
|
|
64
|
+
// Output.apply so the RolePolicy is a top-level resource the invocation can
|
|
65
|
+
// `dependsOn` (invoke only after the Lambda can read the secret).
|
|
66
|
+
let rolePolicy = {
|
|
67
|
+
open PulumiAws.PolicyDocument
|
|
68
|
+
IAM.RolePolicy.make(
|
|
69
|
+
~name=`${name}MigrationAccess`,
|
|
70
|
+
~args={
|
|
71
|
+
policy: secretArn
|
|
72
|
+
->Pulumi.Output.apply(arn =>
|
|
73
|
+
PulumiAws.PolicyDocument.make(
|
|
74
|
+
~id=`${name}MigrationAccessPolicy`,
|
|
75
|
+
~statements=[
|
|
76
|
+
{
|
|
77
|
+
sid: "AllowGetSecret",
|
|
78
|
+
effect: Allow,
|
|
79
|
+
actions: Action("secretsmanager:GetSecretValue"),
|
|
80
|
+
resources: Resource(arn),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
)->PulumiAws.PolicyDocument.toJsonString
|
|
84
|
+
)
|
|
85
|
+
->Pulumi.Output.asInput,
|
|
86
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Re-invoke whenever the migration code or the Reventless layer (which ships
|
|
92
|
+
// PgSchema's DDL) changes; idempotent DDL makes an unchanged re-run a no-op.
|
|
93
|
+
let layerArn = PulumiAws.Lambda.reventlessLayerArn->Option.getOr("no-layer")
|
|
94
|
+
let inputJson =
|
|
95
|
+
[("trigger", `${sourceCodeHash}:${layerArn}`->JSON.Encode.string)]
|
|
96
|
+
->Dict.fromArray
|
|
97
|
+
->JSON.Encode.object
|
|
98
|
+
->JSON.stringify
|
|
99
|
+
|
|
100
|
+
let invocation = Lambda.Invocation.make(
|
|
101
|
+
~name=`${name}Migrate`,
|
|
102
|
+
~args={
|
|
103
|
+
// Ordering: functionName ties to the Lambda, whose env depends on
|
|
104
|
+
// connectionConfig (the instance's resolved outputs), so the invocation
|
|
105
|
+
// waits for the DB to be available. dependsOn adds the secret-access policy.
|
|
106
|
+
functionName: runtime.parts.lambda->Pulumi.Output.flatMap(l => l.name)->Pulumi.Output.asInput,
|
|
107
|
+
input: inputJson->Pulumi.Input.make,
|
|
108
|
+
},
|
|
109
|
+
~opts={
|
|
110
|
+
dependsOn: [rolePolicy->Pulumi.Resource.makeFromJs]->Pulumi.Input.make,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
Array.concat(
|
|
115
|
+
runtime.resources,
|
|
116
|
+
[
|
|
117
|
+
ReventlessInfra.Adapter.make(
|
|
118
|
+
~name=`${name}Migration`->Pulumi.Output.make,
|
|
119
|
+
~id=invocation.id,
|
|
120
|
+
~urn=invocation.id,
|
|
121
|
+
~service="aws:lambda"->Pulumi.Output.make,
|
|
122
|
+
~role="postgres-migration"->Pulumi.Output.make,
|
|
123
|
+
~resourceType="aws:lambda:Invocation"->Pulumi.Output.make,
|
|
124
|
+
),
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
7
|
+
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
8
|
+
import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
|
|
9
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
10
|
+
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
11
|
+
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
12
|
+
|
|
13
|
+
function make(name, handlerConfig, secretArn, securityGroupId, subnetIds, opts) {
|
|
14
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/PgMigrationEntryPoint.mjs", {}, undefined);
|
|
15
|
+
let sourceCodeHash = match.sourceCodeHash;
|
|
16
|
+
let envVars = Object.fromEntries([[
|
|
17
|
+
"HANDLER_CONFIG",
|
|
18
|
+
handlerConfig
|
|
19
|
+
]]);
|
|
20
|
+
let vpcConfig = securityGroupId.apply(sgId => ({
|
|
21
|
+
subnetIds: subnetIds,
|
|
22
|
+
securityGroupIds: [sgId]
|
|
23
|
+
}));
|
|
24
|
+
let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, sourceCodeHash, envVars, undefined, undefined, undefined, undefined, undefined, undefined, vpcConfig, opts);
|
|
25
|
+
let rolePolicy = new (Aws.iam.RolePolicy)(name + `MigrationAccess`, {
|
|
26
|
+
policy: secretArn.apply(arn => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `MigrationAccessPolicy`, [{
|
|
27
|
+
Sid: "AllowGetSecret",
|
|
28
|
+
Effect: "Allow",
|
|
29
|
+
Action: "secretsmanager:GetSecretValue",
|
|
30
|
+
Resource: arn
|
|
31
|
+
}]))),
|
|
32
|
+
role: runtime.parts.lambdaRole.id
|
|
33
|
+
});
|
|
34
|
+
let layerArn = Stdlib_Option.getOr(Lambda$PulumiAws.reventlessLayerArn, "no-layer");
|
|
35
|
+
let inputJson = JSON.stringify(Object.fromEntries([[
|
|
36
|
+
"trigger",
|
|
37
|
+
sourceCodeHash + `:` + layerArn
|
|
38
|
+
]]));
|
|
39
|
+
let invocation = new (Aws.lambda.Invocation)(name + `Migrate`, {
|
|
40
|
+
functionName: Output$Pulumi.flatMap(runtime.parts.lambda, l => l.name),
|
|
41
|
+
input: inputJson
|
|
42
|
+
}, {
|
|
43
|
+
dependsOn: [rolePolicy]
|
|
44
|
+
});
|
|
45
|
+
return runtime.resources.concat([Adapter$ReventlessInfra.make(Pulumi.output(name + `Migration`), invocation.id, invocation.id, Pulumi.output("aws:lambda"), undefined, Pulumi.output("postgres-migration"), undefined, Pulumi.output("aws:lambda:Invocation"), undefined, undefined)]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
make,
|
|
50
|
+
}
|
|
51
|
+
/* @pulumi/aws Not a pure module */
|
|
@@ -81,21 +81,19 @@ export function response(ctx) {
|
|
|
81
81
|
// Serialize the shared connection config into the env-config JSON (same shape
|
|
82
82
|
// EventCollectorRuntime_Builder_Single bakes into HANDLER_CONFIG).
|
|
83
83
|
let pgConnectionJson = (cc: PgConnection.connectionConfig): string =>
|
|
84
|
-
|
|
85
|
-
("host", cc.host->JSON.Encode.string),
|
|
86
|
-
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
87
|
-
("database", cc.database->JSON.Encode.string),
|
|
88
|
-
("username", cc.username->JSON.Encode.string),
|
|
89
|
-
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
90
|
-
]
|
|
91
|
-
->Dict.fromArray
|
|
92
|
-
->JSON.Encode.object
|
|
93
|
-
->JSON.stringify
|
|
84
|
+
cc->PgConnection.connectionConfigToJson->JSON.stringify
|
|
94
85
|
|
|
95
86
|
let provision = (
|
|
96
87
|
~api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
97
88
|
~selection: QueryDbBackend.selection,
|
|
98
89
|
~opts: Pulumi.ComponentResource.options,
|
|
90
|
+
// The shared node(id) resolver is a single resolver on the API's Query.node
|
|
91
|
+
// field. In plugin-stack mode each plugin stack deploys independently onto one
|
|
92
|
+
// shared API, so only ONE stack may own that field — and a per-plugin node
|
|
93
|
+
// resolver only knows its own plugin's types anyway. So node is provisioned in
|
|
94
|
+
// monolithic mode only; plugin stacks pass false. (Node is not wired on the
|
|
95
|
+
// DynamoDB path either — it stays a monolithic-only capability for now.)
|
|
96
|
+
~createNodeResolver: bool=true,
|
|
99
97
|
): unit => {
|
|
100
98
|
// Join the resolver-binding registry (labelField/includeIdParam) with the
|
|
101
99
|
// ReadModel runtime registry (specModulePath, pgBacked). Only read models
|
|
@@ -260,9 +258,9 @@ let provision = (
|
|
|
260
258
|
|
|
261
259
|
// Shared node(id) resolver (B3.2c) — one for the whole API, dispatched by
|
|
262
260
|
// typeName in the Lambda. Only when some entity registered a node type
|
|
263
|
-
// (else the `node` field isn't in the schema)
|
|
264
|
-
//
|
|
265
|
-
if nodeTypes->Dict.toArray->Array.length > 0 {
|
|
261
|
+
// (else the `node` field isn't in the schema) AND in monolithic mode
|
|
262
|
+
// (plugin stacks would collide on the single Query.node field).
|
|
263
|
+
if createNodeResolver && nodeTypes->Dict.toArray->Array.length > 0 {
|
|
266
264
|
let _nodeResolver = AppSync_Resolver_Retrying.makeUnitJsResolver(
|
|
267
265
|
~name=name ++ "NodeResolver",
|
|
268
266
|
~api,
|
|
@@ -11,6 +11,7 @@ import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
|
11
11
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
12
12
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
13
13
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
14
|
+
import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
|
|
14
15
|
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
15
16
|
import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
|
|
16
17
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
@@ -70,31 +71,11 @@ export function response(ctx) {
|
|
|
70
71
|
`;
|
|
71
72
|
|
|
72
73
|
function pgConnectionJson(cc) {
|
|
73
|
-
return JSON.stringify(
|
|
74
|
-
[
|
|
75
|
-
"host",
|
|
76
|
-
cc.host
|
|
77
|
-
],
|
|
78
|
-
[
|
|
79
|
-
"port",
|
|
80
|
-
cc.port
|
|
81
|
-
],
|
|
82
|
-
[
|
|
83
|
-
"database",
|
|
84
|
-
cc.database
|
|
85
|
-
],
|
|
86
|
-
[
|
|
87
|
-
"username",
|
|
88
|
-
cc.username
|
|
89
|
-
],
|
|
90
|
-
[
|
|
91
|
-
"secretArn",
|
|
92
|
-
cc.secretArn
|
|
93
|
-
]
|
|
94
|
-
]));
|
|
74
|
+
return JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(undefined, cc));
|
|
95
75
|
}
|
|
96
76
|
|
|
97
|
-
function provision(api, selection, opts) {
|
|
77
|
+
function provision(api, selection, opts, createNodeResolverOpt) {
|
|
78
|
+
let createNodeResolver = createNodeResolverOpt !== undefined ? createNodeResolverOpt : true;
|
|
98
79
|
let handlers = Stdlib_Array.filterMap(Object.values(entries), entry => {
|
|
99
80
|
let info = EventCollectorRuntime_Builder_Single$ReventlessAws.readModelInfos[entry.readModelName];
|
|
100
81
|
if (info !== undefined && info.pgBacked) {
|
|
@@ -163,7 +144,7 @@ function provision(api, selection, opts) {
|
|
|
163
144
|
},
|
|
164
145
|
serviceRoleArn: dataSourceRole.arn
|
|
165
146
|
}, customOpts);
|
|
166
|
-
if (Object.entries(nodeTypes).length !== 0) {
|
|
147
|
+
if (createNodeResolver && Object.entries(nodeTypes).length !== 0) {
|
|
167
148
|
AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name + "NodeResolver", api, dataSource.name, "Query", "node", nodeResolverCode, customOpts);
|
|
168
149
|
}
|
|
169
150
|
dataSource.name.apply(n => resolveDataSourceName.contents(n));
|
|
@@ -41,10 +41,22 @@ type payload = {
|
|
|
41
41
|
targetIndex?: string,
|
|
42
42
|
targetIndexIdField?: string,
|
|
43
43
|
multi?: bool,
|
|
44
|
+
// Auth-table pipeline (B3.2c): a group-restricted index. Group members must
|
|
45
|
+
// own the resource (the auth table maps index value → {<group>Id: username});
|
|
46
|
+
// non-members pass through (mirrors the DynamoDB pipeline's earlyReturn).
|
|
47
|
+
authTable?: string,
|
|
48
|
+
authGroup?: string,
|
|
44
49
|
arguments: JSON.t,
|
|
45
50
|
identity: Reventless.Identity.t,
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
// "Owner" → "ownerId" (auth-table owner field name, matching authorizeIndexedAccess).
|
|
54
|
+
let authIdField = (group: string): string =>
|
|
55
|
+
switch group->String.get(0) {
|
|
56
|
+
| Some(c) => c->String.toLowerCase ++ group->String.slice(~start=1, ~end=group->String.length) ++ "Id"
|
|
57
|
+
| None => "Id"
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
// The Postgres query push-downs a binding needs (QueryEnginePostgres.Make
|
|
49
61
|
// provides these; the mock in tests provides in-memory equivalents).
|
|
50
62
|
type pushdowns = {
|
|
@@ -130,7 +142,14 @@ let runInterceptor = async (~binding, ~payload): ReventlessCore.QueryDb_Callback
|
|
|
130
142
|
}
|
|
131
143
|
}
|
|
132
144
|
|
|
133
|
-
|
|
145
|
+
// `lookupBinding` resolves another read model's binding by key — needed only for
|
|
146
|
+
// the auth-table pipeline (the auth table is another read model, loaded by the
|
|
147
|
+
// index value). The handler passes the module-level registry; tests pass a mock.
|
|
148
|
+
let dispatch = async (
|
|
149
|
+
~binding: binding,
|
|
150
|
+
~lookupBinding: string => option<binding>=_ => None,
|
|
151
|
+
~payload: payload,
|
|
152
|
+
): JSON.t => {
|
|
134
153
|
let rm = payload.readModelName
|
|
135
154
|
switch await runInterceptor(~binding, ~payload) {
|
|
136
155
|
| Deny(_) =>
|
|
@@ -183,7 +202,35 @@ let dispatch = async (~binding: binding, ~payload: payload): JSON.t => {
|
|
|
183
202
|
->Option.flatMap(ic => ic.idField)
|
|
184
203
|
->Option.getOr(indexName)
|
|
185
204
|
let value = payload.arguments->argStr(indexName)->Option.getOr("")
|
|
186
|
-
|
|
205
|
+
// Auth-table pipeline: a group-restricted index. Non-members pass through;
|
|
206
|
+
// members must own the resource (auth table maps index value →
|
|
207
|
+
// {<group>Id: username}). Auth table is another read model, loaded via the
|
|
208
|
+
// registry (unsupported → deny if it isn't Postgres-registered).
|
|
209
|
+
let authorized = switch (payload.authGroup, payload.authTable) {
|
|
210
|
+
| (Some(group), Some(authTable)) =>
|
|
211
|
+
if !Reventless.Identity.hasGroup(payload.identity, group) {
|
|
212
|
+
true
|
|
213
|
+
} else {
|
|
214
|
+
switch lookupBinding(authTable) {
|
|
215
|
+
| Some(authBinding) =>
|
|
216
|
+
switch await authBinding.ops.load(value) {
|
|
217
|
+
| Ok(items) =>
|
|
218
|
+
items
|
|
219
|
+
->Array.get(0)
|
|
220
|
+
->Option.flatMap(row => row->argStr(authIdField(group)))
|
|
221
|
+
->Option.mapOr(false, owner => owner == payload.identity.username)
|
|
222
|
+
| Error(_) => false
|
|
223
|
+
}
|
|
224
|
+
| None => false
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
| _ => true
|
|
228
|
+
}
|
|
229
|
+
if authorized {
|
|
230
|
+
JSON.Encode.array(await binding.pushdowns.indexLookup(~readModelName=rm, field, value))
|
|
231
|
+
} else {
|
|
232
|
+
JSON.Encode.array([])
|
|
233
|
+
}
|
|
187
234
|
|
|
188
235
|
| "list" =>
|
|
189
236
|
let argsDict = payload.arguments->argObj
|
|
@@ -327,7 +374,8 @@ let handler = async (payload: payload, _context) => {
|
|
|
327
374
|
| "node" => await handleNode(~payload)
|
|
328
375
|
| _ =>
|
|
329
376
|
switch bindings->Dict.get(bindingKey) {
|
|
330
|
-
| Some(binding) =>
|
|
377
|
+
| Some(binding) =>
|
|
378
|
+
await dispatch(~binding, ~lookupBinding=name => bindings->Dict.get(name), ~payload)
|
|
331
379
|
| None =>
|
|
332
380
|
JsError.throwWithMessage("PgQueryResolver: no binding registered for read model " ++ bindingKey)
|
|
333
381
|
}
|