@reventlessdev/reventless-aws 3.0.0-alpha.176 → 3.0.0-alpha.178
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 +23 -0
- package/docker-compose.postgres.yml +21 -0
- package/package.json +7 -6
- package/scripts/run-pg-integration-tests.sh +58 -0
- package/src/Platform.res +49 -0
- package/src/Platform.res.mjs +48 -2
- 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/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 +279 -0
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +170 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +202 -11
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +150 -9
- package/src/adapter/QueryDb/QueryDbBackend.res +8 -0
- package/src/adapter/QueryDb/QueryDbBackend.res.mjs +4 -1
- package/src/adapter/QueryDb/QueryDbResolvers.res +6 -5
- package/src/adapter/QueryDb/QueryDbResolvers.res.mjs +6 -3
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +288 -0
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +170 -0
- package/src/adapter/QueryDb/QueryDbStorage.res +6 -1
- package/src/adapter/QueryDb/QueryDbStorage.res.mjs +2 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res +5 -1
- package/src/adapter/QueryDb/QueryDbStorage_Postgres.res.mjs +3 -3
- 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/PgQueryResolverEntryPoint.mjs +88 -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 +163 -2
- package/tests/PgQueryResolver_LambdaTest.res.mjs +226 -14
- 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 */
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Deploy-time provisioner for the shared Postgres GraphQL-read Lambda (B3.2b).
|
|
2
|
+
//
|
|
3
|
+
// Postgres-backed read models have no per-table AppSync data source, so instead
|
|
4
|
+
// of the direct DynamoDB resolvers there is ONE in-VPC "PgQueryResolver" Lambda
|
|
5
|
+
// (PgQueryResolverEntryPoint.mjs → PgQueryResolver_Lambda.dispatch) registered
|
|
6
|
+
// as a single AppSync Lambda data source. Every Postgres read model's resolvers
|
|
7
|
+
// (QueryDbResolvers_Lambda) are thin APPSYNC_JS `Invoke` templates pointing at
|
|
8
|
+
// this one data source.
|
|
9
|
+
//
|
|
10
|
+
// Ordering wrinkle: `QueryDbStorage_Postgres.make` runs DURING construct and
|
|
11
|
+
// must return the data source name, but the Lambda's code bundle needs the set
|
|
12
|
+
// of Postgres read models (their spec packages), known only AFTER construct. So
|
|
13
|
+
// the data source name is a DEFERRED Output (`dataSourceName`), fulfilled by
|
|
14
|
+
// `provision` — which the platform calls in makePlatform once every plugin is
|
|
15
|
+
// built (alongside provisionPgChangeFeedRelay). The resolvers themselves are
|
|
16
|
+
// created even later (inside the schema-pushed `resourcesMaker`), so they read
|
|
17
|
+
// the name after `provision` has resolved it.
|
|
18
|
+
|
|
19
|
+
open PulumiAws
|
|
20
|
+
|
|
21
|
+
let log = ReventlessCore.Logger.fromEnv()
|
|
22
|
+
|
|
23
|
+
// -- Deferred data source name ----------------------------------------------
|
|
24
|
+
// Created once at module load. `QueryDbStorage_Postgres.make` returns this for
|
|
25
|
+
// every Postgres read model (one shared data source); `provision` resolves it.
|
|
26
|
+
let resolveDataSourceName: ref<string => unit> = ref(_ => ())
|
|
27
|
+
let dataSourceNamePromise: promise<string> = Promise.make((resolve, _reject) =>
|
|
28
|
+
resolveDataSourceName := resolve
|
|
29
|
+
)
|
|
30
|
+
let dataSourceName: Pulumi.Output.t<string> = Pulumi.Output.fromPromise(dataSourceNamePromise)
|
|
31
|
+
|
|
32
|
+
// -- Resolver-binding registry ----------------------------------------------
|
|
33
|
+
// Populated by QueryDbResolvers_Lambda.make during construct (per Postgres read
|
|
34
|
+
// model): the bits `provision` bakes into the Lambda env config that the entry
|
|
35
|
+
// point can't get from the spec module (they come from the deploy-time
|
|
36
|
+
// queryFieldNamesRegistry). specModulePath is looked up from
|
|
37
|
+
// EventCollectorRuntime_Builder_Single.readModelInfos at provision time.
|
|
38
|
+
type resolverEntry = {
|
|
39
|
+
readModelName: string,
|
|
40
|
+
labelField: string,
|
|
41
|
+
includeIdParam: bool,
|
|
42
|
+
}
|
|
43
|
+
let entries: dict<resolverEntry> = Dict.make()
|
|
44
|
+
let register = (entry: resolverEntry): unit =>
|
|
45
|
+
entries->Dict.set(entry.readModelName, entry)
|
|
46
|
+
|
|
47
|
+
// Relay node type → read-model-name, populated per RM by QueryDbResolvers_Lambda.
|
|
48
|
+
// Baked into the env config so the entry point can serve the shared node(id).
|
|
49
|
+
let nodeTypes: dict<string> = Dict.make()
|
|
50
|
+
let registerNodeType = (~typeName: string, ~readModelName: string): unit =>
|
|
51
|
+
nodeTypes->Dict.set(typeName, readModelName)
|
|
52
|
+
|
|
53
|
+
// -- Provision --------------------------------------------------------------
|
|
54
|
+
let bool = b => b ? "true" : "false"
|
|
55
|
+
|
|
56
|
+
// One `node(id: ID!)` resolver on the shared data source → Invoke {kind:"node"}.
|
|
57
|
+
let nodeResolverCode =
|
|
58
|
+
`import { util } from '@aws-appsync/utils';
|
|
59
|
+
export function request(ctx) {
|
|
60
|
+
const id = ctx.identity;
|
|
61
|
+
return {
|
|
62
|
+
operation: 'Invoke',
|
|
63
|
+
payload: {
|
|
64
|
+
readModelName: '',
|
|
65
|
+
kind: 'node',
|
|
66
|
+
arguments: ctx.args,
|
|
67
|
+
identity: id != null && id.sub != null
|
|
68
|
+
? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
|
|
69
|
+
: id != null
|
|
70
|
+
? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
|
|
71
|
+
: null
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function response(ctx) {
|
|
76
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
77
|
+
return ctx.result;
|
|
78
|
+
}
|
|
79
|
+
`->Pulumi.Input.make
|
|
80
|
+
|
|
81
|
+
// Serialize the shared connection config into the env-config JSON (same shape
|
|
82
|
+
// EventCollectorRuntime_Builder_Single bakes into HANDLER_CONFIG).
|
|
83
|
+
let pgConnectionJson = (cc: PgConnection.connectionConfig): string =>
|
|
84
|
+
cc->PgConnection.connectionConfigToJson->JSON.stringify
|
|
85
|
+
|
|
86
|
+
let provision = (
|
|
87
|
+
~api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
88
|
+
~selection: QueryDbBackend.selection,
|
|
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,
|
|
97
|
+
): unit => {
|
|
98
|
+
// Join the resolver-binding registry (labelField/includeIdParam) with the
|
|
99
|
+
// ReadModel runtime registry (specModulePath, pgBacked). Only read models
|
|
100
|
+
// that both got Lambda resolvers AND are Postgres-backed are handled here.
|
|
101
|
+
let handlers =
|
|
102
|
+
entries
|
|
103
|
+
->Dict.valuesToArray
|
|
104
|
+
->Array.filterMap(entry =>
|
|
105
|
+
switch EventCollectorRuntime_Builder_Single.readModelInfos->Dict.get(entry.readModelName) {
|
|
106
|
+
| Some(info) if info.pgBacked => Some((entry, info.specModulePath))
|
|
107
|
+
| _ => None
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
if handlers->Array.length == 0 {
|
|
111
|
+
// No Postgres read models with Lambda resolvers — nothing to provision.
|
|
112
|
+
()
|
|
113
|
+
} else {
|
|
114
|
+
// makeFromCodeAsset takes ComponentResource.options (the original); the raw
|
|
115
|
+
// IAM / DataSource resources take the CustomResourceOptions projection.
|
|
116
|
+
let customOpts =
|
|
117
|
+
opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
118
|
+
let name = "PgQueryResolver"
|
|
119
|
+
|
|
120
|
+
// Bundle the entry point plus each read model's spec package (imported at
|
|
121
|
+
// Lambda init for indexes / subIdField / schema → capability).
|
|
122
|
+
let packageDirs: dict<string> = Dict.make()
|
|
123
|
+
handlers->Array.forEach(((_entry, specModulePath)) => {
|
|
124
|
+
let pkg = Util_Bundle.extractPackageName(specModulePath)
|
|
125
|
+
packageDirs->Dict.set(pkg, Util_Bundle.resolvePackageRoot(pkg))
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// Env config: shared pgConnection + one handler per read model. Sorted for
|
|
129
|
+
// deploy-stable output. specModule/labelField/includeIdParam are baked;
|
|
130
|
+
// indexes/subIdField/authorization come from the imported spec at runtime.
|
|
131
|
+
let handlerJsons =
|
|
132
|
+
handlers
|
|
133
|
+
->Array.toSorted(((a, _), (b, _)) => String.compare(a.readModelName, b.readModelName))
|
|
134
|
+
->Array.map(((entry, specModulePath)) => {
|
|
135
|
+
let specModule = specModulePath->JSON.stringifyAny->Option.getOr(`""`)
|
|
136
|
+
`{"readModelName":"${entry.readModelName}","specModule":${specModule},"labelField":"${entry.labelField}","includeIdParam":${bool(
|
|
137
|
+
entry.includeIdParam,
|
|
138
|
+
)}}`
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Relay node type → read-model map, baked so the entry point can serve node(id).
|
|
142
|
+
let nodeTypesJson =
|
|
143
|
+
nodeTypes
|
|
144
|
+
->Dict.toArray
|
|
145
|
+
->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
|
|
146
|
+
->Array.map(((typeName, rm)) => `"${typeName}":"${rm}"`)
|
|
147
|
+
->Array.join(",")
|
|
148
|
+
|
|
149
|
+
let queryResolverConfig =
|
|
150
|
+
selection.connectionConfig->Pulumi.Output.apply(cc =>
|
|
151
|
+
`{"pgConnection":${pgConnectionJson(cc)},"handlers":[${handlerJsons->Array.join(
|
|
152
|
+
",",
|
|
153
|
+
)}],"nodeTypes":{${nodeTypesJson}}}`
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
|
|
157
|
+
envVars->Dict.set("QUERY_RESOLVER_CONFIG", queryResolverConfig->Pulumi.Output.asInput)
|
|
158
|
+
envVars->Dict.set("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make)
|
|
159
|
+
|
|
160
|
+
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
161
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs",
|
|
162
|
+
~packageDirs,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
// In-VPC on the DB-access security group + private subnets (reach RDS).
|
|
166
|
+
let vpcConfig =
|
|
167
|
+
selection.securityGroupId
|
|
168
|
+
->Pulumi.Output.apply(sgId =>
|
|
169
|
+
(
|
|
170
|
+
{
|
|
171
|
+
Lambda.Function.subnetIds: selection.subnetIds->Pulumi.Input.make,
|
|
172
|
+
securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
|
|
173
|
+
}: Lambda.Function.vpcConfig
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
->Pulumi.Output.asInput
|
|
177
|
+
|
|
178
|
+
let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
|
|
179
|
+
~name,
|
|
180
|
+
~code,
|
|
181
|
+
~sourceCodeHash,
|
|
182
|
+
~envVars,
|
|
183
|
+
~memorySize=512,
|
|
184
|
+
~timeout=30,
|
|
185
|
+
~vpcConfig,
|
|
186
|
+
~opts,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
// Read the RDS-managed secret so PgRuntime can resolve the DB password.
|
|
190
|
+
let _ = selection.connectionConfig->Pulumi.Output.apply(cc => {
|
|
191
|
+
open PolicyDocument
|
|
192
|
+
IAM.RolePolicy.make(
|
|
193
|
+
~name=name ++ "-pgSecret",
|
|
194
|
+
~args={
|
|
195
|
+
policy: PolicyDocument.make(
|
|
196
|
+
~id=name ++ "-pgSecretPolicy",
|
|
197
|
+
~statements=[
|
|
198
|
+
{
|
|
199
|
+
sid: "AllowGetPgSecret",
|
|
200
|
+
effect: Allow,
|
|
201
|
+
actions: Action("secretsmanager:GetSecretValue"),
|
|
202
|
+
resources: Resource(cc.secretArn),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
->PolicyDocument.toJsonString
|
|
207
|
+
->Pulumi.Input.make,
|
|
208
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
209
|
+
},
|
|
210
|
+
)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// AppSync → Lambda data source (one, shared by every Postgres RM's resolvers).
|
|
214
|
+
let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
|
|
215
|
+
~name=name ++ "DataSource",
|
|
216
|
+
~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
|
|
217
|
+
~opts=customOpts,
|
|
218
|
+
)
|
|
219
|
+
let lambdaArn = runtime.parts.lambda->Pulumi.Output.flatMap(l => l.arn)
|
|
220
|
+
let _ =
|
|
221
|
+
(lambdaArn, dataSourceRole.id)
|
|
222
|
+
->Pulumi.Output.all2
|
|
223
|
+
->Pulumi.Output.apply(((arn, dsRoleId)) => {
|
|
224
|
+
open PolicyDocument
|
|
225
|
+
IAM.RolePolicy.make(
|
|
226
|
+
~name=name ++ "DataSourceInvoke",
|
|
227
|
+
~args={
|
|
228
|
+
policy: PolicyDocument.make(
|
|
229
|
+
~id=name ++ "DataSourceInvokePolicy",
|
|
230
|
+
~statements=[
|
|
231
|
+
{
|
|
232
|
+
sid: "AllowDataSourceInvokeLambda",
|
|
233
|
+
effect: Allow,
|
|
234
|
+
actions: Action("lambda:InvokeFunction"),
|
|
235
|
+
resources: Resource(arn),
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
)
|
|
239
|
+
->PolicyDocument.toJsonString
|
|
240
|
+
->Pulumi.Input.make,
|
|
241
|
+
role: dsRoleId->Pulumi.Input.make,
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
let dataSource = AppSync.DataSource.make(
|
|
247
|
+
~name=name ++ "DataSource",
|
|
248
|
+
~args={
|
|
249
|
+
type_: AWS_LAMBDA,
|
|
250
|
+
apiId: api->Pulumi.Output.flatMap(a => a.id)->Pulumi.Output.asInput,
|
|
251
|
+
lambdaConfig: {
|
|
252
|
+
AppSync.DataSource.functionArn: lambdaArn->Pulumi.Output.asInput,
|
|
253
|
+
}->Pulumi.Input.make,
|
|
254
|
+
serviceRoleArn: dataSourceRole.arn->Pulumi.Output.asInput,
|
|
255
|
+
},
|
|
256
|
+
~opts=Some(customOpts),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// Shared node(id) resolver (B3.2c) — one for the whole API, dispatched by
|
|
260
|
+
// typeName in the Lambda. Only when some entity registered a node type
|
|
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 {
|
|
264
|
+
let _nodeResolver = AppSync_Resolver_Retrying.makeUnitJsResolver(
|
|
265
|
+
~name=name ++ "NodeResolver",
|
|
266
|
+
~api,
|
|
267
|
+
~dataSourceName=dataSource.name->Pulumi.Output.asInput,
|
|
268
|
+
~type_="Query"->Pulumi.Input.make,
|
|
269
|
+
~field="node"->Pulumi.Input.make,
|
|
270
|
+
~code=nodeResolverCode,
|
|
271
|
+
~opts=customOpts,
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fulfil the deferred name the Postgres storage maker handed to resolvers.
|
|
276
|
+
let _ = dataSource.name->Pulumi.Output.apply(n => resolveDataSourceName.contents(n))
|
|
277
|
+
log.info(~comp="PgQueryResolver_Builder", `provisioned for ${handlers->Array.length->Int.toString} read model(s)`)
|
|
278
|
+
}
|
|
279
|
+
}
|