@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.
Files changed (28) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res +77 -7
  4. package/src/Platform.res.mjs +77 -3
  5. package/src/adapter/DcbEventLog/DcbBackend.res +78 -0
  6. package/src/adapter/DcbEventLog/DcbBackend.res.mjs +53 -0
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage.res +20 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage.res.mjs +21 -1
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +43 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res.mjs +21 -0
  11. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res +7 -0
  12. package/src/adapter/Postgres/PgChangeFeedRelay_Builder.res.mjs +7 -1
  13. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +33 -8
  14. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +12 -3
  15. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +84 -6
  16. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +49 -4
  17. package/src/components/Api/AppSync_Adapter.res +3 -3
  18. package/src/components/Api/AppSync_Adapter.res.mjs +2 -2
  19. package/src/components/Plugin.res +1 -1
  20. package/src/components/Plugin.res.mjs +2 -2
  21. package/src/plugin/runtime/PluginRuntime_Builder.res +20 -0
  22. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +8 -0
  23. package/tests/AppSync_AdapterTest.res +12 -12
  24. package/tests/AppSync_AdapterTest.res.mjs +15 -15
  25. package/tests/PgChangeFeedRelay_IntegrationTest.res +148 -0
  26. package/tests/PgChangeFeedRelay_IntegrationTest.res.mjs +154 -0
  27. package/tests/PgChangeFeedRelay_RuntimeTest.res +31 -0
  28. 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
- /* @pulumi/aws Not a pure module */
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 events to the
42
- * EventCollector via the injected `sendBatch`. `sendBatch` is injected (rather than
43
- * hardcoding SQS) so the drain/transform is unit-testable; the relay entry point
44
- * wires it to the actual `SendMessage`. Returns the number of events processed.
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 relay = async (
51
- ~config: PgConnection.connectionConfig,
52
+ let relayWithPool = async (
53
+ ~pool: ReventlessPostgres.PgDriver.pool,
52
54
  ~logName: string,
53
55
  ~subscriber: string,
54
- ~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>=?,
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 pool = PgRuntime.poolFor(config)
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 relay(config, logName, subscriber, partitionTag, sendBatch) {
23
- let pool = PgRuntime$ReventlessAws.poolFor(config);
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
- /* Message-ReventlessCore Not a pure module */
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
- 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
  [],