@reventlessdev/reventless-aws 3.0.0-alpha.223 → 3.0.0-alpha.225

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 (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +6 -6
  3. package/src/adapter/AWS_Tags.res +36 -0
  4. package/src/adapter/Adapter_Helpers.res +6 -0
  5. package/src/adapter/Adapter_Helpers.res.mjs +6 -0
  6. package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res +77 -48
  7. package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res.mjs +55 -40
  8. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +6 -0
  9. package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +3 -1
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +29 -6
  11. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +13 -1
  12. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +83 -60
  13. package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +20 -18
  14. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +44 -30
  15. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +22 -23
  16. package/src/adapter/Postgres/PgProjectionFeed.res +20 -1
  17. package/src/adapter/Postgres/PgProjectionFeed.res.mjs +3 -1
  18. package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +6 -2
  19. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +9 -2
  20. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +10 -4
  21. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
  22. package/src/adapter/StateTopic/StateTopic_AppSync.res +8 -1
  23. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +4 -2
  24. package/src/plugin/cloner/ClonerRunner_Fargate.res +6 -0
  25. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +2 -1
  26. package/src/util/Util_DeadLetterQueue.res +39 -4
  27. package/src/util/Util_DeadLetterQueue.res.mjs +16 -4
  28. package/src/util/Util_EventSourceMapping.res +8 -0
  29. package/src/util/Util_EventSourceMapping.res.mjs +6 -4
  30. package/tests/CommandTopicQueuePolicyTest.res +65 -0
  31. package/tests/CommandTopicQueuePolicyTest.res.mjs +53 -0
  32. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +50 -0
  33. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +47 -0
  34. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +58 -0
  35. package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +78 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.225 (2026-07-22)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** keep an empty DCB tag value from breaking the append ([1c0ab40](https://github.com/ReventlessDev/reventless-core/commit/1c0ab40f25b596702a81f42686d77c5e423e9b44))
11
+ * **aws:** key the disconnect schedule's rule name to the stack, not the Lambda ([05aebcf](https://github.com/ReventlessDev/reventless-core/commit/05aebcfacac34fd9c7cb42f353ee91e20066067e))
12
+ * **aws:** tag every taggable resource the framework still left bare ([d4f7a90](https://github.com/ReventlessDev/reventless-core/commit/d4f7a908bcaf686ff59a85e300efac4e5e6199df))
13
+
14
+
15
+ # 3.0.0-alpha.224 (2026-07-22)
16
+
17
+ ### Bug Fixes
18
+
19
+ * **aws:** register the QueuePolicy -> Queue dependency so destroys order correctly ([81cf2b3](https://github.com/ReventlessDev/reventless-core/commit/81cf2b3198fea7ad280817d2414102089ba0f13d))
20
+ * **aws:** scope the CommandTopic queue policy's EventBridge send grant ([2f5f148](https://github.com/ReventlessDev/reventless-core/commit/2f5f148f4d73b2997633207f071c8d055960cc75))
21
+
22
+
6
23
  # 3.0.0-alpha.223 (2026-07-22)
7
24
 
8
25
  * refactor!: retire the stale "Admin" vocabulary for platform-owned resources ([2770bd9](https://github.com/ReventlessDev/reventless-core/commit/2770bd96f309142bdfc65b9f36bd1a3ef60641bd))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.223",
3
+ "version": "3.0.0-alpha.225",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -12,13 +12,13 @@
12
12
  "@reventlessdev/rescript-effect": "0.1.0-alpha.30",
13
13
  "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
14
14
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
15
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.57",
16
15
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
17
- "@reventlessdev/reventless-core": "3.0.0-alpha.177",
18
- "@reventlessdev/reventless-spec": "3.0.0-alpha.80",
16
+ "@reventlessdev/reventless-core": "3.0.0-alpha.178",
19
17
  "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
20
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.41",
21
- "@reventlessdev/reventless-infra": "3.0.0-alpha.104"
18
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.104",
19
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.42",
20
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.58",
21
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.80"
22
22
  },
23
23
  "devDependencies": {
24
24
  "rescript": "12.3.0",
@@ -103,6 +103,42 @@ let makeDict = (
103
103
  ]->Dict.fromArray
104
104
  }
105
105
 
106
+ /***
107
+ Framework-created AWS resource types that carry NO attribution tags, and why.
108
+ Keep this list here rather than in a plan: it is what tells a coverage audit
109
+ whether a bare resource is a gap to close or a fact to work around.
110
+
111
+ **Cannot be tagged** — the provider type exposes no `tags` property at all
112
+ (verified against `@pulumi/aws` 7.19.0; re-verify on a major provider bump, since
113
+ AWS does add tag support over time — `lambda/eventSourceMapping` gained it after
114
+ the first sweep and is now tagged):
115
+
116
+ aws:appsync/dataSource aws:appsync/resolver (incl. aws-native)
117
+ aws:appsync/function aws:appsync/sourceApiAssociation
118
+ aws:iam/rolePolicy aws:iam/rolePolicyAttachment
119
+ aws:sqs/queuePolicy aws:sns/topicSubscription
120
+ aws:lambda/permission aws:lambda/invocation
121
+ aws:cloudwatch/logMetricFilter aws:cloudwatch/eventTarget
122
+ aws:s3/bucketPolicy aws:s3/bucketPublicAccessBlock
123
+ aws:cloudfront/originAccessControl
124
+ aws:cognito/userPoolClient aws:cognito/userGroup
125
+ aws:route53/record aws:acm/certificateValidation
126
+ aws:ses/emailIdentity aws:ses/identityPolicy
127
+
128
+ Each is attributable only through its parent (function, log group, API, role,
129
+ bucket, pool, certificate) — i.e. structurally, by URN ancestry. A consumer that
130
+ treats "carries `reventless:*` tags" as universal will be wrong about every row
131
+ above, and no amount of coverage work changes that. Note the ReScript binding for
132
+ `IAM.RolePolicy` carries a `tags?` field that AWS ignores; do not read its
133
+ presence as tag support.
134
+
135
+ **Taggable, deliberately untagged** — `aws:s3/bucketObject`. Static-bundle assets
136
+ are bucket *contents*, not infrastructure: they are fully attributed by the
137
+ bucket that holds them, and S3 bills object tags per object per month, so tagging
138
+ a few hundred bundle files would cost money to restate a fact the parent already
139
+ carries.
140
+ */
141
+
106
142
  /**
107
143
  The same schema as `makeDict`, wrapped as a Pulumi input — what nearly every
108
144
  resource's `tags` field expects. Use `makeDict` only for the bindings that take
@@ -1,3 +1,9 @@
1
+ // SQS queue ARN is `arn:aws:sqs:<region>:<accountId>:<name>` — segment index 4
2
+ // (0-based). Used to scope queue-policy service principals to this account via
3
+ // the aws:SourceAccount condition key.
4
+ let accountIdOfQueueArn = (queueArn: string) =>
5
+ queueArn->String.split(":")->Array.get(4)->Option.getOr("")
6
+
1
7
  let dynamoDbStreamResources = eventTopicResources =>
2
8
  eventTopicResources->ReventlessCore.Util.Adapter.filterSupportedResolvedResources([
3
9
  AWS.DynamoDbStream.service,
@@ -1,8 +1,13 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
4
  import * as AWS$ReventlessAws from "./AWS.res.mjs";
4
5
  import * as Util_Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Adapter.res.mjs";
5
6
 
7
+ function accountIdOfQueueArn(queueArn) {
8
+ return Stdlib_Option.getOr(queueArn.split(":")[4], "");
9
+ }
10
+
6
11
  function dynamoDbStreamResources(eventTopicResources) {
7
12
  return Util_Adapter$ReventlessCore.filterSupportedResolvedResources(eventTopicResources, [AWS$ReventlessAws.DynamoDbStream.service]);
8
13
  }
@@ -29,6 +34,7 @@ function dynamoDbResources(resources) {
29
34
  }
30
35
 
31
36
  export {
37
+ accountIdOfQueueArn,
32
38
  dynamoDbStreamResources,
33
39
  snsResources,
34
40
  sqsResources,
@@ -1,55 +1,73 @@
1
1
  open PulumiAws.PolicyDocument
2
2
  open Adapter_Helpers
3
3
 
4
+ let createQueuePolicyDocument = (~name, ~queueArn: string, ~lambdaArn: string) => {
5
+ let accountId = queueArn->accountIdOfQueueArn
6
+ PulumiAws.PolicyDocument.make(
7
+ ~id=name ++ "QueuePolicy",
8
+ ~statements=[
9
+ {
10
+ sid: "AllowLambdaToAccessQueue",
11
+ effect: Allow,
12
+ principal: Principals({
13
+ service: PrincipalId(AWS.Lambda.principal),
14
+ }),
15
+ actions: Actions(["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]),
16
+ resources: Resource(queueArn),
17
+ conditions: {
18
+ arnEquals: [("AWS:SourceArn", ConditionValue(lambdaArn))]->Dict.fromArray,
19
+ },
20
+ },
21
+ {
22
+ // ScheduledPublisher creates EventBridge rules at runtime
23
+ // (PutRule/PutTargets) targeting this queue directly, and EventBridge
24
+ // authorises SQS targets against the queue's resource policy — so the
25
+ // grant cannot name a single rule ARN. Scope it to rules in this
26
+ // account instead: SourceAccount closes the cross-account hole,
27
+ // ArnLike narrows the in-account surface to Events rules.
28
+ sid: "AllowCloudWatchEventsToSendToQueue",
29
+ effect: Allow,
30
+ principal: Principals({
31
+ service: PrincipalId(AWS.CloudwatchEventRule.principal),
32
+ }),
33
+ actions: Actions(["sqs:SendMessage"]),
34
+ resources: Resource(queueArn),
35
+ conditions: {
36
+ stringEquals: [("aws:SourceAccount", ConditionValue(accountId))]->Dict.fromArray,
37
+ arnLike: [
38
+ ("aws:SourceArn", ConditionValue(`arn:aws:events:*:${accountId}:rule/*`)),
39
+ ]->Dict.fromArray,
40
+ },
41
+ },
42
+ ],
43
+ )->toJsonString
44
+ }
45
+
4
46
  let createQueuePolicy = (
5
47
  queue: PulumiAws.SQS.Queue.t,
6
48
  name,
7
49
  lambda: Pulumi.Output.t<PulumiAws.Lambda.Function.t>,
8
50
  opts,
9
51
  ) => {
10
- let _ =
11
- (queue.arn, queue.id, lambda->Pulumi.Output.flatMap(lambda => lambda.arn))
12
- ->Pulumi.Output.all3
13
- ->Pulumi.Output.apply(((queueArn, queueId, lambdaArn)) => {
14
- let queuePolicyDocument =
15
- PulumiAws.PolicyDocument.make(
16
- ~id=name ++ "QueuePolicy",
17
- ~statements=[
18
- {
19
- sid: "AllowLambdaToAccessQueue",
20
- effect: Allow,
21
- principal: Principals({
22
- service: PrincipalId("lambda.amazonaws.com"),
23
- }),
24
- actions: Actions([
25
- "sqs:ReceiveMessage",
26
- "sqs:DeleteMessage",
27
- "sqs:GetQueueAttributes",
28
- ]),
29
- resources: Resource(queueArn),
30
- conditions: {
31
- arnEquals: [("AWS:SourceArn", ConditionValue(lambdaArn))]->Dict.fromArray,
32
- },
33
- },
34
- {
35
- sid: "AllowCloudWatchEventsToSendToQueue",
36
- effect: Allow,
37
- principal: Principals({
38
- service: PrincipalId("events.amazonaws.com"),
39
- }),
40
- actions: Actions(["sqs:SendMessage"]),
41
- resources: Resource(queueArn),
42
- },
43
- ],
44
- )
45
- ->toJsonString
46
- ->Pulumi.Input.make
47
- let _queuePolicy = PulumiAws.SQS.QueuePolicy.make(
48
- ~name,
49
- ~args={queueUrl: queueId->Pulumi.Input.make, policy: queuePolicyDocument},
50
- ~opts=Some(opts),
51
- )
52
- })
52
+ // The document is a *value* derived from resolved ARNs, so it is built in an
53
+ // apply. The QueuePolicy itself stays outside: passing `queue.id` as an Output
54
+ // is what registers the policy -> queue dependency, without which Pulumi has
55
+ // no ordering constraint and can delete the queue first on a replacement,
56
+ // leaving the policy's delete polling a queue that no longer exists.
57
+ let queuePolicyDocument =
58
+ (queue.arn, lambda->Pulumi.Output.flatMap(lambda => lambda.arn))
59
+ ->Pulumi.Output.all2
60
+ ->Pulumi.Output.apply(((queueArn, lambdaArn)) =>
61
+ createQueuePolicyDocument(~name, ~queueArn, ~lambdaArn)
62
+ )
63
+ let _queuePolicy = PulumiAws.SQS.QueuePolicy.make(
64
+ ~name,
65
+ ~args={
66
+ queueUrl: queue.id->Pulumi.Output.asInput,
67
+ policy: queuePolicyDocument->Pulumi.Output.asInput,
68
+ },
69
+ ~opts=Some(opts),
70
+ )
53
71
  }
54
72
 
55
73
  let createLambdaPolicy = (
@@ -100,10 +118,10 @@ let createLambdaPolicy = (
100
118
  "dynamodb:UpdateItem",
101
119
  "dynamodb:DeleteItem",
102
120
  "dynamodb:BatchWriteItem",
103
- // Phase 7 cross-partition fences emit read-only
104
- // `ConditionCheck` entries inside `TransactWriteItems`
105
- // when a write doesn't touch the cross-partition tag's
106
- // partition itself; this needs its own IAM action.
121
+ // Cross-partition fences emit read-only `ConditionCheck`
122
+ // entries inside `TransactWriteItems` when a write
123
+ // doesn't touch the cross-partition tag's partition
124
+ // itself; this needs its own IAM action.
107
125
  "dynamodb:ConditionCheckItem",
108
126
  ]),
109
127
  resources: Resources(
@@ -193,4 +211,15 @@ let createLambdaPolicy = (
193
211
  }
194
212
 
195
213
  let subscribeLambda2SqsTopic = (~batchSize=?, lambda, name, queue, opts) =>
196
- Util_EventSourceMapping.subscribeSqs(~lambda, ~name, ~queue, ~batchSize?, ~opts)
214
+ Util_EventSourceMapping.subscribeSqs(
215
+ ~lambda,
216
+ ~name,
217
+ ~queue,
218
+ ~batchSize?,
219
+ ~tags=AWS.Tags.make(
220
+ ~name,
221
+ ~kind=ReventlessCore.CommandTopic.componentType,
222
+ ~role=EventSourceMapping,
223
+ ),
224
+ ~opts,
225
+ )
@@ -5,53 +5,67 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
8
+ import * as AWS$ReventlessAws from "../AWS.res.mjs";
9
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
8
10
  import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
9
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
+ import * as CommandTopic$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic.res.mjs";
10
13
  import * as Adapter_Helpers$ReventlessAws from "../Adapter_Helpers.res.mjs";
11
14
  import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
12
15
 
16
+ function createQueuePolicyDocument(name, queueArn, lambdaArn) {
17
+ let accountId = Adapter_Helpers$ReventlessAws.accountIdOfQueueArn(queueArn);
18
+ return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "QueuePolicy", [
19
+ {
20
+ Sid: "AllowLambdaToAccessQueue",
21
+ Principal: {
22
+ Service: AWS$ReventlessAws.Lambda.principal
23
+ },
24
+ Effect: "Allow",
25
+ Action: [
26
+ "sqs:ReceiveMessage",
27
+ "sqs:DeleteMessage",
28
+ "sqs:GetQueueAttributes"
29
+ ],
30
+ Resource: queueArn,
31
+ Condition: {
32
+ ArnEquals: Object.fromEntries([[
33
+ "AWS:SourceArn",
34
+ lambdaArn
35
+ ]])
36
+ }
37
+ },
38
+ {
39
+ Sid: "AllowCloudWatchEventsToSendToQueue",
40
+ Principal: {
41
+ Service: AWS$ReventlessAws.CloudwatchEventRule.principal
42
+ },
43
+ Effect: "Allow",
44
+ Action: ["sqs:SendMessage"],
45
+ Resource: queueArn,
46
+ Condition: {
47
+ StringEquals: Object.fromEntries([[
48
+ "aws:SourceAccount",
49
+ accountId
50
+ ]]),
51
+ ArnLike: Object.fromEntries([[
52
+ "aws:SourceArn",
53
+ `arn:aws:events:*:` + accountId + `:rule/*`
54
+ ]])
55
+ }
56
+ }
57
+ ]));
58
+ }
59
+
13
60
  function createQueuePolicy(queue, name, lambda, opts) {
14
- Pulumi.all([
61
+ let queuePolicyDocument = Pulumi.all([
15
62
  queue.arn,
16
- queue.id,
17
63
  Output$Pulumi.flatMap(lambda, lambda => lambda.arn)
18
- ]).apply(param => {
19
- let queueArn = param[0];
20
- let queuePolicyDocument = PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "QueuePolicy", [
21
- {
22
- Sid: "AllowLambdaToAccessQueue",
23
- Principal: {
24
- Service: "lambda.amazonaws.com"
25
- },
26
- Effect: "Allow",
27
- Action: [
28
- "sqs:ReceiveMessage",
29
- "sqs:DeleteMessage",
30
- "sqs:GetQueueAttributes"
31
- ],
32
- Resource: queueArn,
33
- Condition: {
34
- ArnEquals: Object.fromEntries([[
35
- "AWS:SourceArn",
36
- param[2]
37
- ]])
38
- }
39
- },
40
- {
41
- Sid: "AllowCloudWatchEventsToSendToQueue",
42
- Principal: {
43
- Service: "events.amazonaws.com"
44
- },
45
- Effect: "Allow",
46
- Action: ["sqs:SendMessage"],
47
- Resource: queueArn
48
- }
49
- ]));
50
- new (Aws.sqs.QueuePolicy)(name, {
51
- policy: queuePolicyDocument,
52
- queueUrl: param[1]
53
- }, opts);
54
- });
64
+ ]).apply(param => createQueuePolicyDocument(name, param[0], param[1]));
65
+ new (Aws.sqs.QueuePolicy)(name, {
66
+ policy: queuePolicyDocument,
67
+ queueUrl: queue.id
68
+ }, opts);
55
69
  }
56
70
 
57
71
  function createLambdaPolicy(lambdaRole, name, queue, resources, opts) {
@@ -134,10 +148,11 @@ function createLambdaPolicy(lambdaRole, name, queue, resources, opts) {
134
148
  }
135
149
 
136
150
  function subscribeLambda2SqsTopic(batchSize, lambda, name, queue, opts) {
137
- return Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, name, queue, batchSize, opts);
151
+ return Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, name, queue, batchSize, AWS_Tags$ReventlessAws.make(name, CommandTopic$ReventlessCore.componentType, "EventSourceMapping", undefined, undefined, undefined, undefined, undefined), opts);
138
152
  }
139
153
 
140
154
  export {
155
+ createQueuePolicyDocument,
141
156
  createQueuePolicy,
142
157
  createLambdaPolicy,
143
158
  subscribeLambda2SqsTopic,
@@ -77,6 +77,12 @@ let make: ReventlessCore.Counter_Adapter.handlerMaker = (
77
77
  ~targetName=name,
78
78
  ~sourceName,
79
79
  ~source,
80
+ ~tags=AWS.Tags.make(
81
+ ~name=sourceName->ReventlessCore.Util.baseName ++ ("2" ++ name),
82
+ ~kind=ReventlessCore.ComponentType.Counter,
83
+ ~role=EventSourceMapping,
84
+ ~component=name,
85
+ ),
80
86
  ~opts,
81
87
  )
82
88
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
4
  import * as Pulumi from "@pulumi/pulumi";
5
+ import * as Util$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util.res.mjs";
6
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
5
7
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
6
8
  import * as Util_DynamoDbStream$ReventlessAws from "../../util/Util_DynamoDbStream.res.mjs";
7
9
  import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
@@ -42,7 +44,7 @@ function make(name, referencesName, referencesDb, countsName, countsDb, param, s
42
44
  _0: "Counter"
43
45
  }, "Counter", match.code, match.sourceCodeHash, envVars, 1024, 30, undefined, undefined, undefined, undefined, undefined, componentOpts);
44
46
  let lambda = runtime.parts.lambda;
45
- let subscribe = (sourceName, source) => Util_EventSourceMapping$ReventlessAws.subscribe(undefined, lambda, name, sourceName, source, opts);
47
+ let subscribe = (sourceName, source) => Util_EventSourceMapping$ReventlessAws.subscribe(undefined, lambda, name, sourceName, source, AWS_Tags$ReventlessAws.make(Util$ReventlessCore.baseName(sourceName) + ("2" + name), "Counter", "EventSourceMapping", undefined, name, undefined, undefined, undefined), opts);
46
48
  subscribe(referencesName, referencesStream);
47
49
  subscribe(countsName, countsStream);
48
50
  return {
@@ -118,11 +118,19 @@ let toItem = (
118
118
  ->ReventlessCore.Message.decomposeMeta
119
119
  ->Array.forEach(((k, v)) => item->Dict.set(k, v))
120
120
 
121
- // Add individual tag attributes for GSI queries
122
- event.tags->Array.forEach(tag => {
123
- let attrName = tagToAttributeName(tag.key)
124
- item->Dict.set(attrName, tag.value->JSON.Encode.string)
125
- })
121
+ // Add individual tag attributes for GSI queries. An empty tag value is a
122
+ // legitimate model state (a composite-partition member that is absent at this
123
+ // level of the hierarchy), but `tag_<key>` is a GSI hash key and DynamoDB
124
+ // rejects an empty string for a key attribute — the whole append fails. Skip
125
+ // the attribute instead: the index goes sparse (its intended mechanism), the
126
+ // event still records the tag in its `tags` payload, and `tag_composite`, the
127
+ // partition key and the OCC fences are all unaffected.
128
+ event.tags->Array.forEach(tag =>
129
+ if tag.value != "" {
130
+ let attrName = tagToAttributeName(tag.key)
131
+ item->Dict.set(attrName, tag.value->JSON.Encode.string)
132
+ }
133
+ )
126
134
 
127
135
  // Add composite tag attribute if multiple tags. The tag set here is exactly the
128
136
  // event's *entity* tags (no framework provenance tag is smuggled in — see
@@ -370,7 +378,7 @@ let resolveBaseItem = (table: resolvedTable, keyItem: JSON.t): Effect.t<
370
378
  }
371
379
  }
372
380
 
373
- let queryBySingleTagCrossPartitionStream = (
381
+ let queryBySingleTagCrossPartitionStreamIndexed = (
374
382
  table: resolvedTable,
375
383
  tag: Reventless.DcbTag.tag,
376
384
  ~after: option<string>=?,
@@ -419,6 +427,21 @@ let queryBySingleTagCrossPartitionStream = (
419
427
  )
420
428
  }
421
429
 
430
+ let queryBySingleTagCrossPartitionStream = (
431
+ table: resolvedTable,
432
+ tag: Reventless.DcbTag.tag,
433
+ ~after: option<string>=?,
434
+ ) =>
435
+ if tag.value == "" {
436
+ // `toItem` does not index an empty tag value (a GSI hash key cannot hold an
437
+ // empty string), so nothing carries it in this index. Querying for it would
438
+ // be rejected outright too — a key condition value cannot be empty either.
439
+ // The empty result is the honest answer; an exception is not.
440
+ Stream.empty
441
+ } else {
442
+ queryBySingleTagCrossPartitionStreamIndexed(table, tag, ~after?)
443
+ }
444
+
422
445
  // --- Query Item Execution ---
423
446
 
424
447
  let executeQueryItem = async (
@@ -103,6 +103,9 @@ function toItem(position, event, partitionTag, recordedAt) {
103
103
  item[param[0]] = param[1];
104
104
  });
105
105
  event.tags.forEach(tag => {
106
+ if (tag.value === "") {
107
+ return;
108
+ }
106
109
  let attrName = tagToAttributeName(tag.key);
107
110
  item[attrName] = tag.value;
108
111
  });
@@ -281,7 +284,7 @@ function resolveBaseItem(table, keyItem) {
281
284
  }))), DynamoDb_Error$ReventlessAws.retrySchedule), err => Effect$1.fail(DynamoDb_Error$ReventlessAws.message(err))), result => result.Item);
282
285
  }
283
286
 
284
- function queryBySingleTagCrossPartitionStream(table, tag, after) {
287
+ function queryBySingleTagCrossPartitionStreamIndexed(table, tag, after) {
285
288
  let indexName = tagToAttributeName(tag.key);
286
289
  let expressionAttributeValues = Object.fromEntries([[
287
290
  ":val",
@@ -326,6 +329,14 @@ function queryBySingleTagCrossPartitionStream(table, tag, after) {
326
329
  });
327
330
  }
328
331
 
332
+ function queryBySingleTagCrossPartitionStream(table, tag, after) {
333
+ if (tag.value === "") {
334
+ return Stream$1.empty;
335
+ } else {
336
+ return queryBySingleTagCrossPartitionStreamIndexed(table, tag, after);
337
+ }
338
+ }
339
+
329
340
  async function executeQueryItem(table, queryItem, after, crossPartitionTagKeysOpt) {
330
341
  let crossPartitionTagKeys = crossPartitionTagKeysOpt !== undefined ? crossPartitionTagKeysOpt : [];
331
342
  let tags = queryItem.tags;
@@ -1164,6 +1175,7 @@ export {
1164
1175
  buildQueryByPartitionKeyInput,
1165
1176
  queryByPartitionKey,
1166
1177
  resolveBaseItem,
1178
+ queryBySingleTagCrossPartitionStreamIndexed,
1167
1179
  queryBySingleTagCrossPartitionStream,
1168
1180
  executeQueryItem,
1169
1181
  deduplicateByPosition,