@reventlessdev/reventless-aws 3.0.0-alpha.173 → 3.0.0-alpha.174
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 +9 -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 +9 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +6 -2
- 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_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 */
|
|
@@ -51,9 +51,17 @@ let relay = async (
|
|
|
51
51
|
~config: PgConnection.connectionConfig,
|
|
52
52
|
~logName: string,
|
|
53
53
|
~subscriber: string,
|
|
54
|
-
~
|
|
54
|
+
// ~partitionTagJson: the DCB log's partition tag, sury-encoded into HANDLER_CONFIG
|
|
55
|
+
// by the relay builder (B2.3d). Parsed once via the shared derivedPartitionTagSchema
|
|
56
|
+
// so the `id` the relay emits matches the DynamoDB-stream path. Absent → None (a
|
|
57
|
+
// single-tag log derives the same id without it).
|
|
58
|
+
~partitionTagJson: option<JSON.t>=?,
|
|
55
59
|
~sendBatch: array<JSON.t> => promise<unit>,
|
|
56
60
|
): int => {
|
|
61
|
+
let partitionTag =
|
|
62
|
+
partitionTagJson->Option.map(json =>
|
|
63
|
+
json->S.parseJsonOrThrow(Reventless.DcbTag.derivedPartitionTagSchema)
|
|
64
|
+
)
|
|
57
65
|
let pool = PgRuntime.poolFor(config)
|
|
58
66
|
await ReventlessPostgres.PgChangeFeed.drain(pool, ~logName, ~subscriber, ~handle=async events => {
|
|
59
67
|
let jsons = events->Array.filterMap(event => toEventCollectorJson(event, ~partitionTag?))
|
|
@@ -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,7 +22,8 @@ function toEventCollectorJson(event, partitionTag) {
|
|
|
19
22
|
return Util_DynamoDbStream_Runtime$ReventlessAws.buildJsonEvent$p(item);
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
async function relay(config, logName, subscriber,
|
|
25
|
+
async function relay(config, logName, subscriber, partitionTagJson, sendBatch) {
|
|
26
|
+
let partitionTag = Stdlib_Option.map(partitionTagJson, json => S.parseJsonOrThrow(json, DcbTag$Reventless.derivedPartitionTagSchema));
|
|
23
27
|
let pool = PgRuntime$ReventlessAws.poolFor(config);
|
|
24
28
|
return await PgChangeFeed$ReventlessPostgres.drain(pool, logName, subscriber, undefined, async events => {
|
|
25
29
|
let jsons = Stdlib_Array.filterMap(events, event => toEventCollectorJson(event, partitionTag));
|
|
@@ -33,4 +37,4 @@ export {
|
|
|
33
37
|
toEventCollectorJson,
|
|
34
38
|
relay,
|
|
35
39
|
}
|
|
36
|
-
/*
|
|
40
|
+
/* 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
|
[],
|
|
@@ -282,14 +282,14 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
globalThis.describe("AppSync_Adapter.injectAwsAuth —
|
|
285
|
+
globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
286
286
|
let makeFragment = (mutationFields, queryFields) => GraphQL_Stitcher$ReventlessCore.encode({
|
|
287
287
|
types: [],
|
|
288
288
|
mutations: mutationFields,
|
|
289
289
|
queries: queryFields,
|
|
290
290
|
subscriptions: []
|
|
291
291
|
});
|
|
292
|
-
globalThis.test("
|
|
292
|
+
globalThis.test("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
293
293
|
let entry_fieldNames = ["p_Sync"];
|
|
294
294
|
let entry_fieldPermissions = Object.fromEntries([[
|
|
295
295
|
"p_Sync",
|
|
@@ -298,12 +298,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
298
298
|
_0: ["Admin"]
|
|
299
299
|
}
|
|
300
300
|
]]);
|
|
301
|
-
let
|
|
301
|
+
let entry_systemCallable = true;
|
|
302
302
|
let entry = {
|
|
303
303
|
fieldNames: entry_fieldNames,
|
|
304
304
|
commandSchema: S.unknown,
|
|
305
305
|
fieldPermissions: entry_fieldPermissions,
|
|
306
|
-
|
|
306
|
+
systemCallable: entry_systemCallable
|
|
307
307
|
};
|
|
308
308
|
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
309
309
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
@@ -312,18 +312,18 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
312
312
|
globalThis.expect(m).toContain("@aws_iam");
|
|
313
313
|
globalThis.expect(m).not.toContain("@aws_auth");
|
|
314
314
|
});
|
|
315
|
-
globalThis.test("
|
|
315
|
+
globalThis.test("systemCallable mutation without a group restriction stays open to Cognito + IAM", () => {
|
|
316
316
|
let entry_fieldNames = ["p_Sync"];
|
|
317
317
|
let entry_fieldPermissions = Object.fromEntries([[
|
|
318
318
|
"p_Sync",
|
|
319
319
|
"AllowAuthenticated"
|
|
320
320
|
]]);
|
|
321
|
-
let
|
|
321
|
+
let entry_systemCallable = true;
|
|
322
322
|
let entry = {
|
|
323
323
|
fieldNames: entry_fieldNames,
|
|
324
324
|
commandSchema: S.unknown,
|
|
325
325
|
fieldPermissions: entry_fieldPermissions,
|
|
326
|
-
|
|
326
|
+
systemCallable: entry_systemCallable
|
|
327
327
|
};
|
|
328
328
|
let frag = makeFragment(["p_Sync(id: ID!): String"], []);
|
|
329
329
|
let aug = AppSync_Adapter$ReventlessAws.injectAwsAuth(frag, [entry], []);
|
|
@@ -332,7 +332,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
332
332
|
globalThis.expect(m).not.toContain("cognito_groups");
|
|
333
333
|
globalThis.expect(m).not.toContain("@aws_auth");
|
|
334
334
|
});
|
|
335
|
-
globalThis.test("a non-
|
|
335
|
+
globalThis.test("a non-systemCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
|
|
336
336
|
let entry_fieldNames = [
|
|
337
337
|
"p_Sync",
|
|
338
338
|
"p_Other"
|
|
@@ -353,12 +353,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
353
353
|
}
|
|
354
354
|
]
|
|
355
355
|
]);
|
|
356
|
-
let
|
|
356
|
+
let entry_systemCallable = false;
|
|
357
357
|
let entry = {
|
|
358
358
|
fieldNames: entry_fieldNames,
|
|
359
359
|
commandSchema: S.unknown,
|
|
360
360
|
fieldPermissions: entry_fieldPermissions,
|
|
361
|
-
|
|
361
|
+
systemCallable: entry_systemCallable
|
|
362
362
|
};
|
|
363
363
|
let iamEntry_fieldNames = ["p_Sync"];
|
|
364
364
|
let iamEntry_fieldPermissions = Object.fromEntries([[
|
|
@@ -368,12 +368,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
368
368
|
_0: ["Admin"]
|
|
369
369
|
}
|
|
370
370
|
]]);
|
|
371
|
-
let
|
|
371
|
+
let iamEntry_systemCallable = true;
|
|
372
372
|
let iamEntry = {
|
|
373
373
|
fieldNames: iamEntry_fieldNames,
|
|
374
374
|
commandSchema: S.unknown,
|
|
375
375
|
fieldPermissions: iamEntry_fieldPermissions,
|
|
376
|
-
|
|
376
|
+
systemCallable: iamEntry_systemCallable
|
|
377
377
|
};
|
|
378
378
|
let frag = makeFragment([
|
|
379
379
|
"p_Sync(id: ID!): String",
|
|
@@ -393,12 +393,12 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
393
393
|
return Stdlib_JsError.throwWithMessage("missing p_Other field");
|
|
394
394
|
}
|
|
395
395
|
});
|
|
396
|
-
globalThis.test("
|
|
396
|
+
globalThis.test("systemCallable query applies dual-auth to BOTH single and list fields", () => {
|
|
397
397
|
let entry_permission = {
|
|
398
398
|
TAG: "AllowGroups",
|
|
399
399
|
_0: ["Admin"]
|
|
400
400
|
};
|
|
401
|
-
let
|
|
401
|
+
let entry_systemCallable = true;
|
|
402
402
|
let entry = {
|
|
403
403
|
singleFieldName: "p_Item",
|
|
404
404
|
listFieldName: "p_Items",
|
|
@@ -406,7 +406,7 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — iamCallable dual-auth", (
|
|
|
406
406
|
stateSchema: S.unknown,
|
|
407
407
|
authorization: undefined,
|
|
408
408
|
permission: entry_permission,
|
|
409
|
-
|
|
409
|
+
systemCallable: entry_systemCallable
|
|
410
410
|
};
|
|
411
411
|
let frag = makeFragment([], [
|
|
412
412
|
"p_Item(id: ID!): Item",
|