@reventlessdev/reventless-aws 3.0.0-alpha.173 → 3.0.0-alpha.175
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 +17 -0
- package/package.json +6 -6
- package/src/Platform.res +77 -7
- package/src/Platform.res.mjs +77 -3
- package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
- package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +7 -0
- package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +7 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +33 -8
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +12 -3
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
- package/src/components/Api/AppSync_Adapter.res +3 -3
- package/src/components/Api/AppSync_Adapter.res.mjs +2 -2
- package/src/components/Plugin.res +1 -1
- package/src/components/Plugin.res.mjs +2 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +8 -0
- package/tests/AppSync_AdapterTest.res +12 -12
- package/tests/AppSync_AdapterTest.res.mjs +15 -15
- package/tests/PgChangeFeedRelay_IntegrationTest.res +148 -0
- package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +154 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res +31 -0
- package/tests/PgChangeFeedRelay_RuntimeTest.res.mjs +65 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
3
4
|
import * as Aws from "@pulumi/aws";
|
|
4
5
|
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
5
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
7
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
8
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
8
9
|
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
10
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
9
11
|
import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
|
|
10
12
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
11
13
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
@@ -60,6 +62,10 @@ function make(name, logs, securityGroupId, subnetIds, intervalMinutesOpt, opts)
|
|
|
60
62
|
"subscriber",
|
|
61
63
|
l.subscriber
|
|
62
64
|
],
|
|
65
|
+
[
|
|
66
|
+
"partitionTag",
|
|
67
|
+
S.reverseConvertToJsonOrThrow(l.partitionTag, DcbTag$Reventless.derivedPartitionTagSchema)
|
|
68
|
+
],
|
|
63
69
|
[
|
|
64
70
|
"targetQueueUrl",
|
|
65
71
|
param[1]
|
|
@@ -132,4 +138,4 @@ function make(name, logs, securityGroupId, subnetIds, intervalMinutesOpt, opts)
|
|
|
132
138
|
export {
|
|
133
139
|
make,
|
|
134
140
|
}
|
|
135
|
-
/*
|
|
141
|
+
/* S Not a pure module */
|
|
@@ -38,23 +38,32 @@ let toEventCollectorJson = (
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Drain a Postgres DCB log from its checkpoint and relay its
|
|
42
|
-
* EventCollector via the injected `sendBatch`. `
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Drain a Postgres DCB log from its checkpoint on the given `pool` and relay its
|
|
42
|
+
* events to the EventCollector via the injected `sendBatch`. Both `pool` and
|
|
43
|
+
* `sendBatch` are injected (rather than hardcoding Secrets Manager / SQS) so the
|
|
44
|
+
* whole drain→transform→send path is integration-testable against a plain local
|
|
45
|
+
* Postgres (B2.4); the `relay` wrapper below builds the container pool for the
|
|
46
|
+
* deployed Lambda. Returns the number of events processed.
|
|
45
47
|
*
|
|
46
48
|
* At-least-once by construction: `PgChangeFeed.drain` replays the last page on a
|
|
47
49
|
* crash before checkpoint, and EventCollector projections are idempotent
|
|
48
50
|
* (event-sourced), so re-delivery is safe.
|
|
49
51
|
*/
|
|
50
|
-
let
|
|
51
|
-
~
|
|
52
|
+
let relayWithPool = async (
|
|
53
|
+
~pool: ReventlessPostgres.PgDriver.pool,
|
|
52
54
|
~logName: string,
|
|
53
55
|
~subscriber: string,
|
|
54
|
-
~
|
|
56
|
+
// ~partitionTagJson: the DCB log's partition tag, sury-encoded into HANDLER_CONFIG
|
|
57
|
+
// by the relay builder (B2.3d). Parsed once via the shared derivedPartitionTagSchema
|
|
58
|
+
// so the `id` the relay emits matches the DynamoDB-stream path. Absent → None (a
|
|
59
|
+
// single-tag log derives the same id without it).
|
|
60
|
+
~partitionTagJson: option<JSON.t>=?,
|
|
55
61
|
~sendBatch: array<JSON.t> => promise<unit>,
|
|
56
62
|
): int => {
|
|
57
|
-
let
|
|
63
|
+
let partitionTag =
|
|
64
|
+
partitionTagJson->Option.map(json =>
|
|
65
|
+
json->S.parseJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema)
|
|
66
|
+
)
|
|
58
67
|
await ReventlessPostgres.PgChangeFeed.drain(pool, ~logName, ~subscriber, ~handle=async events => {
|
|
59
68
|
let jsons = events->Array.filterMap(event => toEventCollectorJson(event, ~partitionTag?))
|
|
60
69
|
if jsons->Array.length > 0 {
|
|
@@ -62,3 +71,19 @@ let relay = async (
|
|
|
62
71
|
}
|
|
63
72
|
})
|
|
64
73
|
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Deployed-Lambda entry: resolve the container-lifetime pool from the injected
|
|
77
|
+
* `PgConnection.connectionConfig` (via Secrets Manager) and relay. Thin wrapper over
|
|
78
|
+
* `relayWithPool`.
|
|
79
|
+
*/
|
|
80
|
+
let relay = async (
|
|
81
|
+
~config: PgConnection.connectionConfig,
|
|
82
|
+
~logName: string,
|
|
83
|
+
~subscriber: string,
|
|
84
|
+
~partitionTagJson: option<JSON.t>=?,
|
|
85
|
+
~sendBatch: array<JSON.t> => promise<unit>,
|
|
86
|
+
): int => {
|
|
87
|
+
let pool = PgRuntime.poolFor(config)
|
|
88
|
+
await relayWithPool(~pool, ~logName, ~subscriber, ~partitionTagJson?, ~sendBatch)
|
|
89
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
3
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
4
7
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
5
8
|
import * as PgRuntime$ReventlessAws from "./PgRuntime.res.mjs";
|
|
6
9
|
import * as PgChangeFeed$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgChangeFeed.res.mjs";
|
|
@@ -19,8 +22,8 @@ function toEventCollectorJson(event, partitionTag) {
|
|
|
19
22
|
return Util_DynamoDbStream_Runtime$ReventlessAws.buildJsonEvent$p(item);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
async function
|
|
23
|
-
let
|
|
25
|
+
async function relayWithPool(pool, logName, subscriber, partitionTagJson, sendBatch) {
|
|
26
|
+
let partitionTag = Stdlib_Option.map(partitionTagJson, json => S.parseJsonOrThrow(json, DcbTag$Reventless.derivedPartitionTagSchema));
|
|
24
27
|
return await PgChangeFeed$ReventlessPostgres.drain(pool, logName, subscriber, undefined, async events => {
|
|
25
28
|
let jsons = Stdlib_Array.filterMap(events, event => toEventCollectorJson(event, partitionTag));
|
|
26
29
|
if (jsons.length !== 0) {
|
|
@@ -29,8 +32,14 @@ async function relay(config, logName, subscriber, partitionTag, sendBatch) {
|
|
|
29
32
|
});
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
async function relay(config, logName, subscriber, partitionTagJson, sendBatch) {
|
|
36
|
+
let pool = PgRuntime$ReventlessAws.poolFor(config);
|
|
37
|
+
return await relayWithPool(pool, logName, subscriber, partitionTagJson, sendBatch);
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
export {
|
|
33
41
|
toEventCollectorJson,
|
|
42
|
+
relayWithPool,
|
|
34
43
|
relay,
|
|
35
44
|
}
|
|
36
|
-
/*
|
|
45
|
+
/* S Not a pure module */
|
|
@@ -43,9 +43,16 @@ let forDcbCommandTopic = (
|
|
|
43
43
|
let channelParts: Util.SQS.channelParts = Obj.magic(channel.parts)
|
|
44
44
|
let queue = channelParts.queue
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
// B2.3c: on a Postgres-backed platform there is no DynamoDB table — DCB events
|
|
47
|
+
// live in `dcb_event` under the canonical log_name `<plugin>DcbEventLog`
|
|
48
|
+
// (= ComponentType.name(_, DcbEventLog), the same string the deploy-time
|
|
49
|
+
// storage + the change-feed relay use). The DcbCommandTopicEntryPoint Postgres
|
|
50
|
+
// branch reads `dcbEventLogTableName` as that log_name.
|
|
51
|
+
let pgSelection = DcbBackend.get()
|
|
52
|
+
let dcbTableName = switch (pgSelection, dcbTableName) {
|
|
53
|
+
| (Some(_), _) => Pulumi.Output.make(pluginName ++ "DcbEventLog")
|
|
54
|
+
| (None, Some(tableName)) => tableName
|
|
55
|
+
| (None, None) => Pulumi.Output.make("NOT_AVAILABLE")
|
|
49
56
|
}
|
|
50
57
|
|
|
51
58
|
// The async DCB CommandTopic is named `<Plugin>DcbAsync` by Dcb_Builder, so the
|
|
@@ -83,11 +90,33 @@ let forDcbCommandTopic = (
|
|
|
83
90
|
})
|
|
84
91
|
->Array.join(",")
|
|
85
92
|
|
|
93
|
+
// B2.3c: on Postgres, add a `pgConnection` object so the entry point selects the
|
|
94
|
+
// Postgres DCB runtime (absent → DynamoDB path, unchanged). Built as a JSON
|
|
95
|
+
// fragment merged into HANDLER_CONFIG below.
|
|
96
|
+
let pgConnectionFragment = switch pgSelection {
|
|
97
|
+
| Some(sel) =>
|
|
98
|
+
sel.connectionConfig->Pulumi.Output.apply(cc => {
|
|
99
|
+
let pgConnectionJson =
|
|
100
|
+
[
|
|
101
|
+
("host", cc.host->JSON.Encode.string),
|
|
102
|
+
("port", cc.port->Int.toFloat->JSON.Encode.float),
|
|
103
|
+
("database", cc.database->JSON.Encode.string),
|
|
104
|
+
("username", cc.username->JSON.Encode.string),
|
|
105
|
+
("secretArn", cc.secretArn->JSON.Encode.string),
|
|
106
|
+
]
|
|
107
|
+
->Dict.fromArray
|
|
108
|
+
->JSON.Encode.object
|
|
109
|
+
->JSON.stringify
|
|
110
|
+
`,"pgConnection":${pgConnectionJson}`
|
|
111
|
+
})
|
|
112
|
+
| None => Pulumi.Output.make("")
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
let handlerConfigJson =
|
|
87
|
-
Pulumi.Output.
|
|
88
|
-
->Pulumi.Output.apply(((table, queueUrl)) => {
|
|
116
|
+
Pulumi.Output.all3((dcbTableName, queue.id, pgConnectionFragment))
|
|
117
|
+
->Pulumi.Output.apply(((table, queueUrl, pgFragment)) => {
|
|
89
118
|
let pluginNameJson = pluginName->JSON.stringifyAny->Option.getOr(`""`)
|
|
90
|
-
`{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson},"stateChangeSliceModules":[${sliceModulesJson}]}`
|
|
119
|
+
`{"dcbEventLogTableName":"${table}","queueUrl":"${queueUrl}","pluginName":${pluginNameJson},"stateChangeSliceModules":[${sliceModulesJson}]${pgFragment}}`
|
|
91
120
|
})
|
|
92
121
|
envVars->Dict.set("HANDLER_CONFIG", handlerConfigJson->Pulumi.Output.asInput)
|
|
93
122
|
|
|
@@ -120,6 +149,26 @@ let forDcbCommandTopic = (
|
|
|
120
149
|
|
|
121
150
|
cfg.sqsBatchSize->Option.forEach(CommandTopicChannel.SQS.setBatchSize)
|
|
122
151
|
|
|
152
|
+
// B2.3c: Postgres-backed DCB → the command Lambda talks to RDS, so it must land
|
|
153
|
+
// in-VPC on the DB-access security group + private subnets. makeFromCodeAsset
|
|
154
|
+
// adds the EC2-ENI IAM automatically when vpcConfig is present (C1).
|
|
155
|
+
let vpcConfig = switch pgSelection {
|
|
156
|
+
| Some(sel) =>
|
|
157
|
+
Some(
|
|
158
|
+
sel.securityGroupId
|
|
159
|
+
->Pulumi.Output.apply(sgId =>
|
|
160
|
+
(
|
|
161
|
+
{
|
|
162
|
+
PulumiAws.Lambda.Function.subnetIds: sel.subnetIds->Pulumi.Input.make,
|
|
163
|
+
securityGroupIds: [sgId->Pulumi.Input.make]->Pulumi.Input.make,
|
|
164
|
+
}: PulumiAws.Lambda.Function.vpcConfig
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
->Pulumi.Output.asInput,
|
|
168
|
+
)
|
|
169
|
+
| None => None
|
|
170
|
+
}
|
|
171
|
+
|
|
123
172
|
let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
|
|
124
173
|
~name,
|
|
125
174
|
~code,
|
|
@@ -130,12 +179,41 @@ let forDcbCommandTopic = (
|
|
|
130
179
|
~reservedConcurrency=?cfg.reservedConcurrency,
|
|
131
180
|
~ephemeralStorageMb=?cfg.ephemeralStorageMb,
|
|
132
181
|
~logRetentionDays=?cfg.logRetentionDays,
|
|
182
|
+
~vpcConfig=?vpcConfig,
|
|
133
183
|
// This is the StateChangeSlice command handler — provision the DCB
|
|
134
184
|
// retry/conflict metric filters (takes effect when logRetentionDays is set).
|
|
135
185
|
~dcbMetrics=true,
|
|
136
186
|
~opts,
|
|
137
187
|
)
|
|
138
188
|
|
|
189
|
+
// B2.3c: grant the Postgres command Lambda read access to the RDS-managed
|
|
190
|
+
// master secret so PgRuntime can resolve the DB password at cold start.
|
|
191
|
+
switch pgSelection {
|
|
192
|
+
| Some(sel) =>
|
|
193
|
+
let _ = sel.connectionConfig->Pulumi.Output.apply(cc => {
|
|
194
|
+
PulumiAws.IAM.RolePolicy.make(
|
|
195
|
+
~name=`${name}-pgSecret`,
|
|
196
|
+
~args={
|
|
197
|
+
policy: PulumiAws.PolicyDocument.make(
|
|
198
|
+
~id=`${name}-pgSecretPolicy`,
|
|
199
|
+
~statements=[
|
|
200
|
+
{
|
|
201
|
+
sid: "AllowGetPgSecret",
|
|
202
|
+
effect: Allow,
|
|
203
|
+
actions: Action("secretsmanager:GetSecretValue"),
|
|
204
|
+
resources: Resource(cc.secretArn),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
)
|
|
208
|
+
->PulumiAws.PolicyDocument.toJsonString
|
|
209
|
+
->Pulumi.Input.make,
|
|
210
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
211
|
+
},
|
|
212
|
+
)
|
|
213
|
+
})
|
|
214
|
+
| None => ()
|
|
215
|
+
}
|
|
216
|
+
|
|
139
217
|
// The DCB command topic Lambda is invoked directly by AppSync (Route 1) and
|
|
140
218
|
// needs sqs:SendMessage to publish commands to the FIFO queue for processing.
|
|
141
219
|
// makeWithDefaultPolicy only grants sqs:Receive* (for SQS trigger Route 2).
|
|
@@ -6,6 +6,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
6
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
7
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
8
8
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
9
|
+
import * as DcbBackend$ReventlessAws from "../DcbEventLog/DcbBackend.res.mjs";
|
|
9
10
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
10
11
|
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
11
12
|
import * as CommandTopicChannel_SQS$ReventlessAws from "../CommandTopic/CommandTopicChannel_SQS.res.mjs";
|
|
@@ -26,7 +27,10 @@ function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, as
|
|
|
26
27
|
let channel = dcbCommandTopic.channel;
|
|
27
28
|
let channelParts = channel.parts;
|
|
28
29
|
let queue = channelParts.queue;
|
|
29
|
-
let
|
|
30
|
+
let pgSelection = DcbBackend$ReventlessAws.get();
|
|
31
|
+
let dcbTableName$1 = pgSelection !== undefined ? Pulumi.output(pluginName + "DcbEventLog") : (
|
|
32
|
+
dcbTableName !== undefined ? dcbTableName : Pulumi.output("NOT_AVAILABLE")
|
|
33
|
+
);
|
|
30
34
|
let isAsync = baseName.endsWith("Async");
|
|
31
35
|
let name = baseName + "CmdHandler";
|
|
32
36
|
let cfg = isAsync ? asyncConfig : syncConfig;
|
|
@@ -47,12 +51,38 @@ function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, as
|
|
|
47
51
|
let b = Stdlib_Option.getOr(JSON.stringify(param[1]), `""`);
|
|
48
52
|
return `{"spec":` + s + `,"behavior":` + b + `}`;
|
|
49
53
|
}).join(",");
|
|
54
|
+
let pgConnectionFragment = pgSelection !== undefined ? pgSelection.connectionConfig.apply(cc => {
|
|
55
|
+
let pgConnectionJson = JSON.stringify(Object.fromEntries([
|
|
56
|
+
[
|
|
57
|
+
"host",
|
|
58
|
+
cc.host
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
"port",
|
|
62
|
+
cc.port
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
"database",
|
|
66
|
+
cc.database
|
|
67
|
+
],
|
|
68
|
+
[
|
|
69
|
+
"username",
|
|
70
|
+
cc.username
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"secretArn",
|
|
74
|
+
cc.secretArn
|
|
75
|
+
]
|
|
76
|
+
]));
|
|
77
|
+
return `,"pgConnection":` + pgConnectionJson;
|
|
78
|
+
}) : Pulumi.output("");
|
|
50
79
|
let handlerConfigJson = Pulumi.all([
|
|
51
80
|
dcbTableName$1,
|
|
52
|
-
queue.id
|
|
81
|
+
queue.id,
|
|
82
|
+
pgConnectionFragment
|
|
53
83
|
]).apply(param => {
|
|
54
84
|
let pluginNameJson = Stdlib_Option.getOr(JSON.stringify(pluginName), `""`);
|
|
55
|
-
return `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + `,"stateChangeSliceModules":[` + sliceModulesJson + `]}`;
|
|
85
|
+
return `{"dcbEventLogTableName":"` + param[0] + `","queueUrl":"` + param[1] + `","pluginName":` + pluginNameJson + `,"stateChangeSliceModules":[` + sliceModulesJson + `]` + param[2] + `}`;
|
|
56
86
|
});
|
|
57
87
|
envVars["HANDLER_CONFIG"] = handlerConfigJson;
|
|
58
88
|
let packageDirs = {};
|
|
@@ -66,7 +96,22 @@ function forDcbCommandTopic(slicePaths, dcbTableName, pluginName, syncConfig, as
|
|
|
66
96
|
packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-core");
|
|
67
97
|
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs", packageDirs, undefined);
|
|
68
98
|
Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS$ReventlessAws.setBatchSize);
|
|
69
|
-
let
|
|
99
|
+
let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({
|
|
100
|
+
subnetIds: pgSelection.subnetIds,
|
|
101
|
+
securityGroupIds: [sgId]
|
|
102
|
+
})) : undefined;
|
|
103
|
+
let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, cfg.memorySize, cfg.timeout, cfg.reservedConcurrency, cfg.ephemeralStorageMb, cfg.logRetentionDays, true, vpcConfig, opts);
|
|
104
|
+
if (pgSelection !== undefined) {
|
|
105
|
+
pgSelection.connectionConfig.apply(cc => new (Aws.iam.RolePolicy)(name + `-pgSecret`, {
|
|
106
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `-pgSecretPolicy`, [{
|
|
107
|
+
Sid: "AllowGetPgSecret",
|
|
108
|
+
Effect: "Allow",
|
|
109
|
+
Action: "secretsmanager:GetSecretValue",
|
|
110
|
+
Resource: cc.secretArn
|
|
111
|
+
}])),
|
|
112
|
+
role: runtime.parts.lambdaRole.id
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
70
115
|
queue.arn.apply(queueArn => new (Aws.iam.RolePolicy)(name + `-sqsSend`, {
|
|
71
116
|
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `-sqsSendPolicy`, [{
|
|
72
117
|
Sid: "AllowSqsSend",
|
|
@@ -268,7 +268,7 @@ let getClient = () =>
|
|
|
268
268
|
// belong to — effectively blocking the field at the API layer.
|
|
269
269
|
//
|
|
270
270
|
// ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
|
|
271
|
-
// A field flagged `
|
|
271
|
+
// A field flagged `systemCallable` (via `mutationSchemaEntry`/`querySchemaEntry`)
|
|
272
272
|
// must be reachable by BOTH the console UI (Cognito) and a deploy-time system
|
|
273
273
|
// caller signing SigV4 with ambient AWS credentials (`Util_AppSync_Caller`,
|
|
274
274
|
// the AWS_IAM additional auth provider). `@aws_auth(...)` is the single-mode
|
|
@@ -328,7 +328,7 @@ let injectAwsAuth = (
|
|
|
328
328
|
|
|
329
329
|
let mutationAuthMap: Dict.t<array<string>> = Dict.make()
|
|
330
330
|
mutationEntries->Array.forEach(entry => {
|
|
331
|
-
if entry.
|
|
331
|
+
if entry.systemCallable->Option.getOr(false) {
|
|
332
332
|
entry.fieldNames->Array.forEach(fieldName => iamFields->Dict.set(fieldName, true))
|
|
333
333
|
}
|
|
334
334
|
switch entry.authorization {
|
|
@@ -356,7 +356,7 @@ let injectAwsAuth = (
|
|
|
356
356
|
// Spec-level `permission` takes precedence over the legacy authorization.
|
|
357
357
|
let queryAuthMap: Dict.t<array<string>> = Dict.make()
|
|
358
358
|
queryEntries->Array.forEach(entry => {
|
|
359
|
-
if entry.
|
|
359
|
+
if entry.systemCallable->Option.getOr(false) {
|
|
360
360
|
iamFields->Dict.set(entry.singleFieldName, true)
|
|
361
361
|
iamFields->Dict.set(entry.listFieldName, true)
|
|
362
362
|
}
|
|
@@ -220,7 +220,7 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
220
220
|
let iamFields = {};
|
|
221
221
|
let mutationAuthMap = {};
|
|
222
222
|
mutationEntries.forEach(entry => {
|
|
223
|
-
if (Stdlib_Option.getOr(entry.
|
|
223
|
+
if (Stdlib_Option.getOr(entry.systemCallable, false)) {
|
|
224
224
|
entry.fieldNames.forEach(fieldName => {
|
|
225
225
|
iamFields[fieldName] = true;
|
|
226
226
|
});
|
|
@@ -249,7 +249,7 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
|
|
|
249
249
|
});
|
|
250
250
|
let queryAuthMap = {};
|
|
251
251
|
queryEntries.forEach(entry => {
|
|
252
|
-
if (Stdlib_Option.getOr(entry.
|
|
252
|
+
if (Stdlib_Option.getOr(entry.systemCallable, false)) {
|
|
253
253
|
iamFields[entry.singleFieldName] = true;
|
|
254
254
|
iamFields[entry.listFieldName] = true;
|
|
255
255
|
}
|
|
@@ -21,7 +21,7 @@ module Make = (HooksConfig: ReventlessCore.Plugin_Helpers.HooksConfig) => {
|
|
|
21
21
|
CommandTopicRemoteChannel.SQS,
|
|
22
22
|
HeartbeatRunner.CloudwatchEvents,
|
|
23
23
|
PluginRuntime_Builder.Make(EventCollectorChannel),
|
|
24
|
-
DcbEventLogStorage.
|
|
24
|
+
DcbEventLogStorage.Selectable,
|
|
25
25
|
EventTopicPublisher.DynamoDbStream,
|
|
26
26
|
CommandTopicChannel.SQS_Sync,
|
|
27
27
|
CommandTopicChannel.SQS_Async,
|
|
@@ -4,13 +4,13 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
4
4
|
import * as Pulumi from "@pulumi/pulumi";
|
|
5
5
|
import * as AppSync_Adapter$ReventlessAws from "./Api/AppSync_Adapter.res.mjs";
|
|
6
6
|
import * as Plugin_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Builder.res.mjs";
|
|
7
|
+
import * as DcbEventLogStorage$ReventlessAws from "../adapter/DcbEventLog/DcbEventLogStorage.res.mjs";
|
|
7
8
|
import * as Util_ResourceNaming$ReventlessAws from "../util/Util_ResourceNaming.res.mjs";
|
|
8
9
|
import * as QueryEngine_DynamoDb$ReventlessAws from "../adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs";
|
|
9
10
|
import * as PluginRuntime_Builder$ReventlessAws from "../plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
10
11
|
import * as PluginRuntimeOperations$ReventlessAws from "../plugin/runtime/PluginRuntimeOperations.res.mjs";
|
|
11
12
|
import * as EventCollectorChannel_SQS$ReventlessAws from "../adapter/EventCollector/EventCollectorChannel_SQS.res.mjs";
|
|
12
13
|
import * as RuntimeEnvironment_Lambda$ReventlessAws from "../adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs";
|
|
13
|
-
import * as DcbEventLogStorage_DynamoDb$ReventlessAws from "../adapter/DcbEventLog/DcbEventLogStorage_DynamoDb.res.mjs";
|
|
14
14
|
import * as CommandTopicChannel_SQS_Sync$ReventlessAws from "../adapter/CommandTopic/CommandTopicChannel_SQS_Sync.res.mjs";
|
|
15
15
|
import * as CommandTopicChannel_SQS_Async$ReventlessAws from "../adapter/CommandTopic/CommandTopicChannel_SQS_Async.res.mjs";
|
|
16
16
|
import * as CommandTopicRemoteChannel_SQS$ReventlessAws from "../adapter/CommandTopic/CommandTopicRemoteChannel_SQS.res.mjs";
|
|
@@ -44,7 +44,7 @@ function Make(HooksConfig) {
|
|
|
44
44
|
})(CommandTopicRemoteChannel_SQS$ReventlessAws)(HeartbeatRunner_CloudWatchEvents$ReventlessAws)(PluginRuntime_Builder$ReventlessAws.Make({
|
|
45
45
|
make: EventCollectorChannel_SQS$ReventlessAws.make,
|
|
46
46
|
connect: EventCollectorChannel_SQS$ReventlessAws.connect
|
|
47
|
-
}))(
|
|
47
|
+
}))(DcbEventLogStorage$ReventlessAws.Selectable)(EventTopicPublisher_DynamoDbStream$ReventlessAws)({
|
|
48
48
|
make: CommandTopicChannel_SQS_Sync$ReventlessAws.make
|
|
49
49
|
})({
|
|
50
50
|
make: CommandTopicChannel_SQS_Async$ReventlessAws.make
|
|
@@ -196,6 +196,26 @@ module Make = (
|
|
|
196
196
|
let channelParts: Util.SQS.channelParts = Obj.magic(channel.parts)
|
|
197
197
|
let queue = channelParts.queue
|
|
198
198
|
|
|
199
|
+
// B2.3d: if this plugin's DCB log is Postgres-backed, hand its EventCollector
|
|
200
|
+
// SQS queue to the change-feed relay registry. `resource.name` is the plugin
|
|
201
|
+
// component name (`<plugin>Plugin`), so the canonical log_name is
|
|
202
|
+
// `<plugin>DcbEventLog` — the key DcbEventLogStorage_Postgres registered under.
|
|
203
|
+
if DcbBackend.isPostgres() {
|
|
204
|
+
switch resource.name {
|
|
205
|
+
| Some(compName) =>
|
|
206
|
+
let pluginName =
|
|
207
|
+
compName->String.endsWith("Plugin")
|
|
208
|
+
? compName->String.slice(~start=0, ~end=compName->String.length - 6)
|
|
209
|
+
: compName
|
|
210
|
+
DcbBackend.attachCollectorQueue(
|
|
211
|
+
~logName=pluginName ++ "DcbEventLog",
|
|
212
|
+
~url=queue.id,
|
|
213
|
+
~arn=queue.arn,
|
|
214
|
+
)
|
|
215
|
+
| None => ()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
199
219
|
// Pull the per-EventCollector context registered by Plugin_Helpers.connect
|
|
200
220
|
// (plugins) or synthesise an admin one if none registered.
|
|
201
221
|
let context: ReventlessCore.Plugin_Helpers.eventCollectorContext =
|
|
@@ -7,6 +7,7 @@ import * as Pulumi from "@pulumi/pulumi";
|
|
|
7
7
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
8
8
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
9
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
10
|
+
import * as DcbBackend$ReventlessAws from "../../adapter/DcbEventLog/DcbBackend.res.mjs";
|
|
10
11
|
import * as Heartbeat$ReventlessCore from "@reventlessdev/reventless-core/src/components/Heartbeat/Heartbeat.res.mjs";
|
|
11
12
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
13
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
@@ -166,6 +167,13 @@ function Make(EventCollectorChannel) {
|
|
|
166
167
|
let channel = eventCollector.channel;
|
|
167
168
|
let channelParts = channel.parts;
|
|
168
169
|
let queue = channelParts.queue;
|
|
170
|
+
if (DcbBackend$ReventlessAws.isPostgres()) {
|
|
171
|
+
let compName = resource.__name;
|
|
172
|
+
if (compName !== undefined) {
|
|
173
|
+
let pluginName = compName.endsWith("Plugin") ? compName.slice(0, compName.length - 6 | 0) : compName;
|
|
174
|
+
DcbBackend$ReventlessAws.attachCollectorQueue(pluginName + "DcbEventLog", queue.id, queue.arn);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
169
177
|
let ctx = Plugin_Helpers$ReventlessCore.eventCollectorContextRef.contents[name];
|
|
170
178
|
let context = ctx !== undefined ? ctx : synthesizeAdminContext();
|
|
171
179
|
let envVars = {};
|
|
@@ -349,12 +349,12 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
349
349
|
|
|
350
350
|
// ── Dual-auth (Cognito + IAM) for deploy-time system callers ────────────────
|
|
351
351
|
//
|
|
352
|
-
// A field flagged `
|
|
352
|
+
// A field flagged `systemCallable` must be reachable by BOTH the console UI
|
|
353
353
|
// (Cognito) and the deploy-time SigV4 system caller (AWS_IAM). It switches from
|
|
354
354
|
// the single-mode `@aws_auth(...)` form to the multi-auth
|
|
355
355
|
// `@aws_cognito_user_pools(...) @aws_iam` form.
|
|
356
356
|
|
|
357
|
-
describe("AppSync_Adapter.injectAwsAuth —
|
|
357
|
+
describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
358
358
|
let makeFragment = (mutationFields, queryFields) =>
|
|
359
359
|
ReventlessCore.GraphQL_Stitcher.encode({
|
|
360
360
|
types: [],
|
|
@@ -363,12 +363,12 @@ describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
|
363
363
|
subscriptions: [],
|
|
364
364
|
})
|
|
365
365
|
|
|
366
|
-
testSync("
|
|
366
|
+
testSync("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
367
367
|
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
368
368
|
fieldNames: ["p_Sync"],
|
|
369
369
|
commandSchema: S.unknown,
|
|
370
370
|
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
|
|
371
|
-
|
|
371
|
+
systemCallable: true,
|
|
372
372
|
}
|
|
373
373
|
let frag = makeFragment(["p_Sync(id: ID!): String"], [])
|
|
374
374
|
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
|
|
@@ -379,12 +379,12 @@ describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
|
379
379
|
expect(m)->not_->toContain("@aws_auth")
|
|
380
380
|
})
|
|
381
381
|
|
|
382
|
-
testSync("
|
|
382
|
+
testSync("systemCallable mutation without a group restriction stays open to Cognito + IAM", () => {
|
|
383
383
|
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
384
384
|
fieldNames: ["p_Sync"],
|
|
385
385
|
commandSchema: S.unknown,
|
|
386
386
|
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowAuthenticated)]),
|
|
387
|
-
|
|
387
|
+
systemCallable: true,
|
|
388
388
|
}
|
|
389
389
|
let frag = makeFragment(["p_Sync(id: ID!): String"], [])
|
|
390
390
|
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry], ~queryEntries=[])
|
|
@@ -395,7 +395,7 @@ describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
|
395
395
|
expect(m)->not_->toContain("@aws_auth")
|
|
396
396
|
})
|
|
397
397
|
|
|
398
|
-
testSync("a non-
|
|
398
|
+
testSync("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
|
|
399
399
|
let entry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
400
400
|
fieldNames: ["p_Sync", "p_Other"],
|
|
401
401
|
commandSchema: S.unknown,
|
|
@@ -403,15 +403,15 @@ describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
|
403
403
|
("p_Sync", Reventless.Authorization.AllowGroups(["Admin"])),
|
|
404
404
|
("p_Other", Reventless.Authorization.AllowGroups(["Admin"])),
|
|
405
405
|
]),
|
|
406
|
-
//
|
|
406
|
+
// systemCallable applies to every field in the entry; split the fields so only
|
|
407
407
|
// p_Sync opts in.
|
|
408
|
-
|
|
408
|
+
systemCallable: false,
|
|
409
409
|
}
|
|
410
410
|
let iamEntry: ReventlessInfra.Api.mutationSchemaEntry = {
|
|
411
411
|
fieldNames: ["p_Sync"],
|
|
412
412
|
commandSchema: S.unknown,
|
|
413
413
|
fieldPermissions: Dict.fromArray([("p_Sync", Reventless.Authorization.AllowGroups(["Admin"]))]),
|
|
414
|
-
|
|
414
|
+
systemCallable: true,
|
|
415
415
|
}
|
|
416
416
|
let frag = makeFragment(["p_Sync(id: ID!): String", "p_Other(id: ID!): String"], [])
|
|
417
417
|
let aug = AppSync_Adapter.injectAwsAuth(frag, ~mutationEntries=[entry, iamEntry], ~queryEntries=[])
|
|
@@ -424,7 +424,7 @@ describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
|
424
424
|
}
|
|
425
425
|
})
|
|
426
426
|
|
|
427
|
-
testSync("
|
|
427
|
+
testSync("systemCallable query applies dual-auth to BOTH single and list fields", () => {
|
|
428
428
|
let entry: ReventlessInfra.Api.querySchemaEntry = {
|
|
429
429
|
singleFieldName: "p_Item",
|
|
430
430
|
listFieldName: "p_Items",
|
|
@@ -432,7 +432,7 @@ describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", () => {
|
|
|
432
432
|
stateSchema: S.unknown,
|
|
433
433
|
authorization: None,
|
|
434
434
|
permission: Reventless.Authorization.AllowGroups(["Admin"]),
|
|
435
|
-
|
|
435
|
+
systemCallable: true,
|
|
436
436
|
}
|
|
437
437
|
let frag = makeFragment(
|
|
438
438
|
[],
|