@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.
@@ -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
- /* @pulumi/aws Not a pure module */
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
- ~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>=?,
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, partitionTag, sendBatch) {
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
- /* Message-ReventlessCore Not a pure module */
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
- let dcbTableName = switch dcbTableName {
47
- | Some(tableName) => tableName
48
- | None => Pulumi.Output.make("NOT_AVAILABLE")
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.all2((dcbTableName, queue.id))
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 dcbTableName$1 = dcbTableName !== undefined ? dcbTableName : Pulumi.output("NOT_AVAILABLE");
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 runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset(name, match.code, match.sourceCodeHash, envVars, cfg.memorySize, cfg.timeout, cfg.reservedConcurrency, cfg.ephemeralStorageMb, cfg.logRetentionDays, true, undefined, opts);
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 `iamCallable` (via `mutationSchemaEntry`/`querySchemaEntry`)
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.iamCallable->Option.getOr(false) {
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.iamCallable->Option.getOr(false) {
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.iamCallable, false)) {
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.iamCallable, false)) {
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.DynamoDb,
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
- }))(DcbEventLogStorage_DynamoDb$ReventlessAws)(EventTopicPublisher_DynamoDbStream$ReventlessAws)({
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 `iamCallable` must be reachable by BOTH the console UI
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 — iamCallable dual-auth", () => {
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("iamCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
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
- iamCallable: true,
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("iamCallable mutation without a group restriction stays open to Cognito + IAM", () => {
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
- iamCallable: true,
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-iamCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
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
- // iamCallable applies to every field in the entry; split the fields so only
406
+ // systemCallable applies to every field in the entry; split the fields so only
407
407
  // p_Sync opts in.
408
- iamCallable: false,
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
- iamCallable: true,
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("iamCallable query applies dual-auth to BOTH single and list fields", () => {
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
- iamCallable: true,
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 — iamCallable dual-auth", () => {
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("iamCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
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 entry_iamCallable = true;
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
- iamCallable: entry_iamCallable
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("iamCallable mutation without a group restriction stays open to Cognito + IAM", () => {
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 entry_iamCallable = true;
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
- iamCallable: entry_iamCallable
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-iamCallable sibling keeps single-mode @aws_auth (no @aws_iam)", () => {
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 entry_iamCallable = false;
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
- iamCallable: entry_iamCallable
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 iamEntry_iamCallable = true;
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
- iamCallable: iamEntry_iamCallable
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("iamCallable query applies dual-auth to BOTH single and list fields", () => {
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 entry_iamCallable = true;
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
- iamCallable: entry_iamCallable
409
+ systemCallable: entry_systemCallable
410
410
  };
411
411
  let frag = makeFragment([], [
412
412
  "p_Item(id: ID!): Item",