@reventlessdev/reventless-aws 3.0.0-alpha.224 → 3.0.0-alpha.226
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 +16 -0
- package/package.json +5 -5
- package/src/adapter/AWS_Tags.res +36 -0
- package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res +12 -1
- package/src/adapter/CommandTopic/CommandTopicChannel_Helpers.res.mjs +3 -1
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res +6 -0
- package/src/adapter/Counter/CounterHandler_DynamoDbStream.res.mjs +3 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +29 -6
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +13 -1
- package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res +22 -1
- package/src/adapter/EventCollector/EventCollectorChannel_Helpers.res.mjs +10 -2
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +12 -0
- package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +2 -1
- package/src/adapter/Postgres/PgProjectionFeed.res +20 -1
- package/src/adapter/Postgres/PgProjectionFeed.res.mjs +3 -1
- package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +6 -2
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +9 -2
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +10 -4
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res +8 -1
- package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +4 -2
- package/src/plugin/cloner/ClonerRunner_Fargate.res +6 -0
- package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +2 -1
- package/src/util/Util_DeadLetterQueue.res +26 -1
- package/src/util/Util_DeadLetterQueue.res.mjs +14 -2
- package/src/util/Util_EventSourceMapping.res +8 -0
- package/src/util/Util_EventSourceMapping.res.mjs +6 -4
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +50 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +47 -0
- package/tests/QueryDbResolvers_AppSyncTest.res +41 -0
- package/tests/QueryDbResolvers_AppSyncTest.res.mjs +24 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +58 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +78 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.226 (2026-07-23)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** round-trip the DynamoDB Scan cursor in full-list connections ([0e21008](https://github.com/ReventlessDev/reventless-core/commit/0e2100860767a153a5c2f50a88fd6b501787c7f4))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.225 (2026-07-22)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** keep an empty DCB tag value from breaking the append ([1c0ab40](https://github.com/ReventlessDev/reventless-core/commit/1c0ab40f25b596702a81f42686d77c5e423e9b44))
|
|
18
|
+
* **aws:** key the disconnect schedule's rule name to the stack, not the Lambda ([05aebcf](https://github.com/ReventlessDev/reventless-core/commit/05aebcfacac34fd9c7cb42f353ee91e20066067e))
|
|
19
|
+
* **aws:** tag every taggable resource the framework still left bare ([d4f7a90](https://github.com/ReventlessDev/reventless-core/commit/d4f7a908bcaf686ff59a85e300efac4e5e6199df))
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.224 (2026-07-22)
|
|
7
23
|
|
|
8
24
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.226",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
"@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.24",
|
|
12
12
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.30",
|
|
13
13
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
14
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.57",
|
|
15
14
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
16
|
-
"@reventlessdev/rescript-
|
|
15
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.59",
|
|
17
16
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.104",
|
|
18
|
-
"@reventlessdev/reventless-
|
|
17
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.43",
|
|
19
18
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
19
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.179",
|
|
20
20
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.80",
|
|
21
|
-
"@reventlessdev/
|
|
21
|
+
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"rescript": "12.3.0",
|
package/src/adapter/AWS_Tags.res
CHANGED
|
@@ -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
|
|
@@ -211,4 +211,15 @@ let createLambdaPolicy = (
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
let subscribeLambda2SqsTopic = (~batchSize=?, lambda, name, queue, opts) =>
|
|
214
|
-
Util_EventSourceMapping.subscribeSqs(
|
|
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
|
+
)
|
|
@@ -6,8 +6,10 @@ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output
|
|
|
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
8
|
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
9
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
9
10
|
import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
|
|
10
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";
|
|
11
13
|
import * as Adapter_Helpers$ReventlessAws from "../Adapter_Helpers.res.mjs";
|
|
12
14
|
import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
|
|
13
15
|
|
|
@@ -146,7 +148,7 @@ function createLambdaPolicy(lambdaRole, name, queue, resources, opts) {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
function subscribeLambda2SqsTopic(batchSize, lambda, name, queue, opts) {
|
|
149
|
-
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);
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
export {
|
|
@@ -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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
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,
|
|
@@ -98,6 +98,17 @@ let connectSqsQueue2SnsTopics = (queue: PulumiAws.SQS.Queue.t, name, eventTopics
|
|
|
98
98
|
})
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// The EventCollector's transport wiring: one mapping per stream source plus one
|
|
102
|
+
// per buffer queue. `~component=name` keeps them attributed to the collector they
|
|
103
|
+
// feed, not to the mapping's own generated name.
|
|
104
|
+
let esmTags = (name, esmName) =>
|
|
105
|
+
AWS.Tags.make(
|
|
106
|
+
~name=esmName,
|
|
107
|
+
~kind=ReventlessCore.EventCollector.componentType,
|
|
108
|
+
~role=EventSourceMapping,
|
|
109
|
+
~component=name,
|
|
110
|
+
)
|
|
111
|
+
|
|
101
112
|
let connectLambda = (
|
|
102
113
|
lambda: Pulumi.Output.t<PulumiAws.Lambda.Function.t>,
|
|
103
114
|
name: string,
|
|
@@ -254,6 +265,10 @@ let connectLambda = (
|
|
|
254
265
|
~targetName=name,
|
|
255
266
|
~sourceName=dynamoDbStreamResource.name,
|
|
256
267
|
~source=dynamoDbStreamResource->ReventlessCore.AdapterDeploytime.resolvedToResource,
|
|
268
|
+
~tags=esmTags(
|
|
269
|
+
name,
|
|
270
|
+
dynamoDbStreamResource.name->ReventlessCore.Util.baseName ++ ("2" ++ name),
|
|
271
|
+
),
|
|
257
272
|
~opts,
|
|
258
273
|
)
|
|
259
274
|
)
|
|
@@ -261,7 +276,13 @@ let connectLambda = (
|
|
|
261
276
|
|
|
262
277
|
let subscriptionResources = queues->Array.mapWithIndex((queue, idx) => {
|
|
263
278
|
let esmName = queues->Array.length > 1 ? `${name}Sqs${Int.toString(idx)}` : name
|
|
264
|
-
Util_EventSourceMapping.subscribeSqs(
|
|
279
|
+
Util_EventSourceMapping.subscribeSqs(
|
|
280
|
+
~lambda,
|
|
281
|
+
~name=esmName,
|
|
282
|
+
~queue,
|
|
283
|
+
~tags=esmTags(name, esmName),
|
|
284
|
+
~opts,
|
|
285
|
+
)
|
|
265
286
|
})
|
|
266
287
|
|
|
267
288
|
subscriptionResources
|
|
@@ -5,11 +5,14 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
|
5
5
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
6
|
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
7
7
|
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
8
|
+
import * as Util$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util.res.mjs";
|
|
8
9
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
10
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
9
11
|
import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
|
|
10
12
|
import * as Util_SQS$ReventlessAws from "../../util/Util_SQS.res.mjs";
|
|
11
13
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
14
|
import * as Adapter_Helpers$ReventlessAws from "../Adapter_Helpers.res.mjs";
|
|
15
|
+
import * as EventCollector$ReventlessCore from "@reventlessdev/reventless-core/src/components/EventCollector/EventCollector.res.mjs";
|
|
13
16
|
import * as AdapterDeploytime$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/AdapterDeploytime.res.mjs";
|
|
14
17
|
import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
|
|
15
18
|
|
|
@@ -68,6 +71,10 @@ function connectSqsQueue2SnsTopics(queue, name, eventTopics, opts) {
|
|
|
68
71
|
});
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
function esmTags(name, esmName) {
|
|
75
|
+
return AWS_Tags$ReventlessAws.make(esmName, EventCollector$ReventlessCore.componentType, "EventSourceMapping", undefined, name, undefined, undefined, undefined);
|
|
76
|
+
}
|
|
77
|
+
|
|
71
78
|
function connectLambda(lambda, name, lambdaRole, queues, eventTopics, resources, opts) {
|
|
72
79
|
Pulumi.all([
|
|
73
80
|
toResources(eventTopics),
|
|
@@ -141,11 +148,11 @@ function connectLambda(lambda, name, lambdaRole, queues, eventTopics, resources,
|
|
|
141
148
|
])),
|
|
142
149
|
role: lambdaRole.id
|
|
143
150
|
}, opts);
|
|
144
|
-
dynamoDbStreamResources.map(dynamoDbStreamResource => Util_EventSourceMapping$ReventlessAws.subscribe(undefined, lambda, name, dynamoDbStreamResource.name, AdapterDeploytime$ReventlessCore.resolvedToResource(dynamoDbStreamResource), opts));
|
|
151
|
+
dynamoDbStreamResources.map(dynamoDbStreamResource => Util_EventSourceMapping$ReventlessAws.subscribe(undefined, lambda, name, dynamoDbStreamResource.name, AdapterDeploytime$ReventlessCore.resolvedToResource(dynamoDbStreamResource), esmTags(name, Util$ReventlessCore.baseName(dynamoDbStreamResource.name) + ("2" + name)), opts));
|
|
145
152
|
});
|
|
146
153
|
return queues.map((queue, idx) => {
|
|
147
154
|
let esmName = queues.length > 1 ? name + `Sqs` + idx.toString() : name;
|
|
148
|
-
return Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, esmName, queue, undefined, opts);
|
|
155
|
+
return Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, esmName, queue, undefined, esmTags(name, esmName), opts);
|
|
149
156
|
});
|
|
150
157
|
}
|
|
151
158
|
|
|
@@ -155,6 +162,7 @@ export {
|
|
|
155
162
|
createQueuePolicy,
|
|
156
163
|
subscribeQueue2SnsTopic,
|
|
157
164
|
connectSqsQueue2SnsTopics,
|
|
165
|
+
esmTags,
|
|
158
166
|
connectLambda,
|
|
159
167
|
}
|
|
160
168
|
/* log Not a pure module */
|
|
@@ -108,6 +108,12 @@ let make = (
|
|
|
108
108
|
)
|
|
109
109
|
->Pulumi.Output.asInput,
|
|
110
110
|
sqsManagedSseEnabled: false->Pulumi.Input.make,
|
|
111
|
+
tags: AWS.Tags.make(
|
|
112
|
+
~name=name ++ "EventLogSubQueue",
|
|
113
|
+
~kind=ReventlessCore.EventTopic.componentType,
|
|
114
|
+
~role=EventLogSubscription,
|
|
115
|
+
~component=name,
|
|
116
|
+
),
|
|
111
117
|
},
|
|
112
118
|
~opts,
|
|
113
119
|
)
|
|
@@ -266,6 +272,12 @@ let make = (
|
|
|
266
272
|
~lambda=lambdaOutput,
|
|
267
273
|
~name=name ++ "EventLogSubEventSourceMapping",
|
|
268
274
|
~queue,
|
|
275
|
+
~tags=AWS.Tags.make(
|
|
276
|
+
~name=name ++ "EventLogSubEventSourceMapping",
|
|
277
|
+
~kind=ReventlessCore.EventTopic.componentType,
|
|
278
|
+
~role=EventSourceMapping,
|
|
279
|
+
~component=name,
|
|
280
|
+
),
|
|
269
281
|
~opts,
|
|
270
282
|
)
|
|
271
283
|
}
|
|
@@ -81,6 +81,7 @@ export async function handler(event) {
|
|
|
81
81
|
function make(name, topicName, eventTopicOutputs, eventsApi, opts) {
|
|
82
82
|
let queue = new (Aws.sqs.Queue)(name + "EventLogSubQueue", {
|
|
83
83
|
redrivePolicy: Util_DeadLetterQueue$ReventlessAws.queue.arn.apply(dlqArn => SQS_Queue$PulumiAws.RedrivePolicy.make(dlqArn, 5)),
|
|
84
|
+
tags: AWS_Tags$ReventlessAws.make(name + "EventLogSubQueue", EventTopic$ReventlessCore.componentType, "EventLogSubscription", undefined, name, undefined, undefined, undefined),
|
|
84
85
|
visibilityTimeoutSeconds: 60,
|
|
85
86
|
sqsManagedSseEnabled: false
|
|
86
87
|
}, opts);
|
|
@@ -172,7 +173,7 @@ function make(name, topicName, eventTopicOutputs, eventsApi, opts) {
|
|
|
172
173
|
sourceCodeHash: sourceCodeHash
|
|
173
174
|
}, opts);
|
|
174
175
|
let lambdaOutput = Pulumi.output(lambda);
|
|
175
|
-
Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambdaOutput, name + "EventLogSubEventSourceMapping", queue, undefined, opts);
|
|
176
|
+
Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambdaOutput, name + "EventLogSubEventSourceMapping", queue, undefined, AWS_Tags$ReventlessAws.make(name + "EventLogSubEventSourceMapping", EventTopic$ReventlessCore.componentType, "EventSourceMapping", undefined, name, undefined, undefined, undefined), opts);
|
|
176
177
|
}
|
|
177
178
|
|
|
178
179
|
export {
|
|
@@ -51,6 +51,14 @@ let makeQueue = (
|
|
|
51
51
|
)
|
|
52
52
|
->Pulumi.Output.asInput,
|
|
53
53
|
sqsManagedSseEnabled: false->Pulumi.Input.make,
|
|
54
|
+
// Plugin substrate: one shared feed per plugin ("AllReadModelsFeed"), not a
|
|
55
|
+
// resource any single component owns.
|
|
56
|
+
tags: AWS.Tags.make(
|
|
57
|
+
~name,
|
|
58
|
+
~kind=ReventlessCore.ComponentType.Plugin,
|
|
59
|
+
~role=EventCollector,
|
|
60
|
+
~scope=Plugin,
|
|
61
|
+
),
|
|
54
62
|
},
|
|
55
63
|
~opts,
|
|
56
64
|
)
|
|
@@ -69,7 +77,18 @@ let connect = (
|
|
|
69
77
|
~lambdaRole: PulumiAws.IAM.Role.t,
|
|
70
78
|
~opts: Pulumi.CustomResourceOptions.t,
|
|
71
79
|
) => {
|
|
72
|
-
let _esm = Util_EventSourceMapping.subscribeSqs(
|
|
80
|
+
let _esm = Util_EventSourceMapping.subscribeSqs(
|
|
81
|
+
~lambda,
|
|
82
|
+
~name,
|
|
83
|
+
~queue,
|
|
84
|
+
~tags=AWS.Tags.make(
|
|
85
|
+
~name,
|
|
86
|
+
~kind=ReventlessCore.ComponentType.Plugin,
|
|
87
|
+
~role=EventSourceMapping,
|
|
88
|
+
~scope=Plugin,
|
|
89
|
+
),
|
|
90
|
+
~opts,
|
|
91
|
+
)
|
|
73
92
|
let _policy = queue.arn->Pulumi.Output.apply(queueArn => {
|
|
74
93
|
open PulumiAws.PolicyDocument
|
|
75
94
|
PulumiAws.IAM.RolePolicy.make(
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Aws from "@pulumi/aws";
|
|
4
4
|
import * as SQS_Queue$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/SQS/SQS_Queue.res.mjs";
|
|
5
|
+
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
5
6
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
6
7
|
import * as Util_DeadLetterQueue$ReventlessAws from "../../util/Util_DeadLetterQueue.res.mjs";
|
|
7
8
|
import * as Util_EventSourceMapping$ReventlessAws from "../../util/Util_EventSourceMapping.res.mjs";
|
|
@@ -15,6 +16,7 @@ function getFeedQueues() {
|
|
|
15
16
|
function makeQueue(name, scope, includeClassic, includeDcb, opts) {
|
|
16
17
|
let queue = new (Aws.sqs.Queue)(name, {
|
|
17
18
|
redrivePolicy: Util_DeadLetterQueue$ReventlessAws.queue.arn.apply(dlqArn => SQS_Queue$PulumiAws.RedrivePolicy.make(dlqArn, 5)),
|
|
19
|
+
tags: AWS_Tags$ReventlessAws.make(name, "Plugin", "EventCollector", "Plugin", undefined, undefined, undefined, undefined),
|
|
18
20
|
visibilityTimeoutSeconds: 120,
|
|
19
21
|
sqsManagedSseEnabled: false
|
|
20
22
|
}, opts);
|
|
@@ -29,7 +31,7 @@ function makeQueue(name, scope, includeClassic, includeDcb, opts) {
|
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function connect(name, queue, lambda, lambdaRole, opts) {
|
|
32
|
-
Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, name, queue, undefined, opts);
|
|
34
|
+
Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, name, queue, undefined, AWS_Tags$ReventlessAws.make(name, "Plugin", "EventSourceMapping", "Plugin", undefined, undefined, undefined, undefined), opts);
|
|
33
35
|
queue.arn.apply(queueArn => new (Aws.iam.RolePolicy)(name + `Receive`, {
|
|
34
36
|
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `ReceivePolicy`, [{
|
|
35
37
|
Sid: "AllowReceiveFeedQueue",
|
|
@@ -430,7 +430,11 @@ const importFromAsset = (() => {
|
|
|
430
430
|
async function buildHandler() {
|
|
431
431
|
const config = parseHandlerConfig(process.env["HANDLER_CONFIG"]);
|
|
432
432
|
const pluginDefinition = loadPluginDefinition();
|
|
433
|
-
|
|
433
|
+
// Stack name, not the function name — see PluginExtensionPointEntryPoint for
|
|
434
|
+
// why the disconnect schedule's rule prefix must not carry a content hash.
|
|
435
|
+
// Both entry points instantiate the same admin Plugin EP module, so they have
|
|
436
|
+
// to derive this identically.
|
|
437
|
+
const environment = process.env["Environment"] || "unknown";
|
|
434
438
|
|
|
435
439
|
// runtimeOps carries only what the admin EP mapping's callHandler actually
|
|
436
440
|
// needs: sendMessageToChannel for ForwardCommand. Cross-plugin SNS
|
|
@@ -496,7 +500,7 @@ async function buildHandler() {
|
|
|
496
500
|
// Admin's Plugin EP: invoke the Make functor with runtime ops.
|
|
497
501
|
const epModule = mappingsMod.Make({
|
|
498
502
|
runtimeOps,
|
|
499
|
-
environment
|
|
503
|
+
environment,
|
|
500
504
|
// Connect-driven schema self-heal retired (Phase 4b) — the ApiSchemaPush
|
|
501
505
|
// SideEffect on ApiFragmentRegistry events is the single schema writer.
|
|
502
506
|
updateApiSchema: undefined,
|
|
@@ -37,10 +37,17 @@ function buildHandler() {
|
|
|
37
37
|
// Instantiate Plugin EP mapping. updateApiSchema and manageSubscriptions are
|
|
38
38
|
// admin-only hooks; this Lambda only handles incoming commands (Heartbeat,
|
|
39
39
|
// ForwardCommand) so they stay undefined here.
|
|
40
|
-
|
|
40
|
+
//
|
|
41
|
+
// `environment` prefixes the disconnect schedule's EventBridge rule name, so it
|
|
42
|
+
// must be stable across deploys and unique per stack. The stack name is both;
|
|
43
|
+
// AWS_LAMBDA_FUNCTION_NAME is neither — it carries a content hash, so replacing
|
|
44
|
+
// this Lambda would orphan every outstanding rule the previous generation
|
|
45
|
+
// created. EventCollectorEntryPoint instantiates the same EP module and must
|
|
46
|
+
// agree, or one Lambda creates rules the other cannot delete.
|
|
47
|
+
const environment = process.env["Environment"] || "unknown";
|
|
41
48
|
const pluginModule = pluginEPPluginMake({
|
|
42
49
|
runtimeOps,
|
|
43
|
-
environment
|
|
50
|
+
environment,
|
|
44
51
|
updateApiSchema: undefined,
|
|
45
52
|
manageSubscriptions: undefined,
|
|
46
53
|
});
|
|
@@ -27,15 +27,21 @@ let make: ReventlessCore.Runtime.environmentMaker<'event, context, 'result, part
|
|
|
27
27
|
let opts =
|
|
28
28
|
opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
|
|
29
29
|
|
|
30
|
+
// Legacy path (see above): no component kind reaches here because nothing calls
|
|
31
|
+
// it, so it is attributed to the Platform rather than to an invented domain kind.
|
|
32
|
+
let tags = AWS.Tags.make(~name, ~kind=ReventlessCore.ComponentType.Platform, ~role=Runtime, ~scope=Platform)
|
|
33
|
+
|
|
30
34
|
let lambdaRole = IAM.Role.makeWithDefaultPolicy(
|
|
31
35
|
~name,
|
|
32
36
|
~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
|
|
37
|
+
~tags=AWS.Tags.make(
|
|
38
|
+
~name,
|
|
39
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
40
|
+
~role=Identity,
|
|
41
|
+
~scope=Platform,
|
|
42
|
+
),
|
|
33
43
|
~opts?,
|
|
34
44
|
)
|
|
35
|
-
|
|
36
|
-
// Legacy path (see above): no component kind reaches here because nothing calls
|
|
37
|
-
// it, so it is attributed to the Platform rather than to an invented domain kind.
|
|
38
|
-
let tags = AWS.Tags.make(~name, ~kind=ReventlessCore.ComponentType.Platform, ~role=Runtime, ~scope=Platform)
|
|
39
45
|
let lambda =
|
|
40
46
|
handler->Pulumi.Output.apply(handler =>
|
|
41
47
|
Lambda.CallbackFunction.make(
|
|
@@ -29,8 +29,8 @@ function make(name, handler, memorySizeOpt, timeoutOpt, opts) {
|
|
|
29
29
|
let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
|
|
30
30
|
let timeout = timeoutOpt !== undefined ? timeoutOpt : 30;
|
|
31
31
|
let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
|
|
32
|
-
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), undefined, opts$1);
|
|
33
32
|
let tags = AWS_Tags$ReventlessAws.make(name, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined);
|
|
33
|
+
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name, "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
34
34
|
let lambda = handler.apply(handler => new (Aws.lambda.CallbackFunction)(name, Lambda$PulumiAws.CallbackFunction.Args.make(handler, lambdaRole, undefined, undefined, undefined, memorySize, timeout, undefined, undefined, undefined, tags, undefined), opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined));
|
|
35
35
|
return {
|
|
36
36
|
parts: {
|
|
@@ -410,14 +410,21 @@ let finish = (
|
|
|
410
410
|
// isolates the bad record across those retries instead of redelivering
|
|
411
411
|
// the whole batch each time.
|
|
412
412
|
entries->Array.forEach(entry => {
|
|
413
|
+
let esmName = entry.topicName ++ "Stream2" ++ name ++ "StateTopic"
|
|
413
414
|
let _esm = EventSourceMapping.make(
|
|
414
|
-
~name=
|
|
415
|
+
~name=esmName,
|
|
415
416
|
~args={
|
|
416
417
|
EventSourceMapping.functionName: lambda.arn->Pulumi.Output.asInput,
|
|
417
418
|
eventSourceArn: entry.streamArn->Pulumi.Output.asInput,
|
|
418
419
|
startingPosition: LATEST,
|
|
419
420
|
maximumRetryAttempts: 3,
|
|
420
421
|
bisectBatchOnFunctionError: true,
|
|
422
|
+
tags: AWS.Tags.make(
|
|
423
|
+
~name=esmName,
|
|
424
|
+
~kind=ReventlessCore.QueryDb.componentType,
|
|
425
|
+
~role=EventSourceMapping,
|
|
426
|
+
~component=name,
|
|
427
|
+
),
|
|
421
428
|
},
|
|
422
429
|
~opts=Some(opts),
|
|
423
430
|
)
|
|
@@ -284,12 +284,14 @@ function finish(eventsApi, opts) {
|
|
|
284
284
|
sourceCodeHash: sourceCodeHash
|
|
285
285
|
}, opts);
|
|
286
286
|
entries.forEach(entry => {
|
|
287
|
-
|
|
287
|
+
let esmName = entry.topicName + "Stream2" + name + "StateTopic";
|
|
288
|
+
new (Aws.lambda.EventSourceMapping)(esmName, {
|
|
288
289
|
functionName: lambda.arn,
|
|
289
290
|
bisectBatchOnFunctionError: true,
|
|
290
291
|
eventSourceArn: entry.streamArn,
|
|
291
292
|
maximumRetryAttempts: 3,
|
|
292
|
-
startingPosition: "LATEST"
|
|
293
|
+
startingPosition: "LATEST",
|
|
294
|
+
tags: AWS_Tags$ReventlessAws.make(esmName, QueryDb$ReventlessCore.componentType, "EventSourceMapping", undefined, name, undefined, undefined, undefined)
|
|
293
295
|
}, opts);
|
|
294
296
|
});
|
|
295
297
|
registry[key] = [];
|
|
@@ -209,6 +209,12 @@ export const handler = async (event) => {
|
|
|
209
209
|
cpu: "1024"->Pulumi.Input.make,
|
|
210
210
|
requiresCompatibilities: ["FARGATE"],
|
|
211
211
|
networkMode: #awsvpc,
|
|
212
|
+
tags: AWS.Tags.make(
|
|
213
|
+
~name,
|
|
214
|
+
~kind=ReventlessCore.Cloner.componentType,
|
|
215
|
+
~role=DataTransfer,
|
|
216
|
+
~component=name,
|
|
217
|
+
),
|
|
212
218
|
},
|
|
213
219
|
~opts?,
|
|
214
220
|
)
|
|
@@ -131,7 +131,8 @@ export const handler = async (event) => {
|
|
|
131
131
|
memory: "4096",
|
|
132
132
|
networkMode: "awsvpc",
|
|
133
133
|
requiresCompatibilities: ["FARGATE"],
|
|
134
|
-
executionRoleArn: taskExecutionRole.arn
|
|
134
|
+
executionRoleArn: taskExecutionRole.arn,
|
|
135
|
+
tags: AWS_Tags$ReventlessAws.make(name, Cloner$ReventlessCore.componentType, "DataTransfer", undefined, name, undefined, undefined, undefined)
|
|
135
136
|
}, opts !== undefined ? Primitive_option.valFromOption(opts) : undefined);
|
|
136
137
|
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name, Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name, Cloner$ReventlessCore.componentType, "Identity", undefined, undefined, undefined, undefined, undefined), opts);
|
|
137
138
|
let lambda = new (Aws.lambda.Function)(name, {
|
|
@@ -4,11 +4,20 @@ open PulumiAws
|
|
|
4
4
|
let name = "DeadLetterQueue"
|
|
5
5
|
let nameFifo = "FIFO" ++ name
|
|
6
6
|
|
|
7
|
+
let queueTags = queueName =>
|
|
8
|
+
AWS.Tags.make(
|
|
9
|
+
~name=queueName,
|
|
10
|
+
~kind=ReventlessCore.ComponentType.Plugin,
|
|
11
|
+
~role=DeadLetter,
|
|
12
|
+
~scope=Plugin,
|
|
13
|
+
)
|
|
14
|
+
|
|
7
15
|
let queue = SQS.Queue.make(
|
|
8
16
|
~name,
|
|
9
17
|
~args={
|
|
10
18
|
SQS.Queue.visibilityTimeoutSeconds: 180->Pulumi.Input.make,
|
|
11
19
|
sqsManagedSseEnabled: false->Pulumi.Input.make,
|
|
20
|
+
tags: queueTags(name),
|
|
12
21
|
},
|
|
13
22
|
)
|
|
14
23
|
|
|
@@ -19,6 +28,7 @@ let fifoQueue = SQS.Queue.make(
|
|
|
19
28
|
contentBasedDeduplication: true->Pulumi.Input.make,
|
|
20
29
|
visibilityTimeoutSeconds: 180->Pulumi.Input.make,
|
|
21
30
|
sqsManagedSseEnabled: false->Pulumi.Input.make,
|
|
31
|
+
tags: queueTags(nameFifo),
|
|
22
32
|
},
|
|
23
33
|
)
|
|
24
34
|
|
|
@@ -89,11 +99,26 @@ ReventlessCore.Monitoring.notify(
|
|
|
89
99
|
|
|
90
100
|
let lambda = handler->Pulumi.Output.make
|
|
91
101
|
|
|
92
|
-
let
|
|
102
|
+
let esmTags = esmName =>
|
|
103
|
+
AWS.Tags.make(
|
|
104
|
+
~name=esmName,
|
|
105
|
+
~kind=ReventlessCore.ComponentType.Plugin,
|
|
106
|
+
~role=EventSourceMapping,
|
|
107
|
+
~scope=Plugin,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
let _subscription = Util_EventSourceMapping.subscribeSqs(
|
|
111
|
+
~lambda,
|
|
112
|
+
~name,
|
|
113
|
+
~queue,
|
|
114
|
+
~tags=esmTags(name),
|
|
115
|
+
~opts,
|
|
116
|
+
)
|
|
93
117
|
let _fifoSubscription = Util_EventSourceMapping.subscribeSqs(
|
|
94
118
|
~lambda,
|
|
95
119
|
~name=nameFifo,
|
|
96
120
|
~queue=fifoQueue,
|
|
121
|
+
~tags=esmTags(nameFifo),
|
|
97
122
|
~opts,
|
|
98
123
|
)
|
|
99
124
|
let createQueuePolicyDocument = (name, queueArn: string, handlerArn: string) => {
|
|
@@ -17,7 +17,12 @@ let name = "DeadLetterQueue";
|
|
|
17
17
|
|
|
18
18
|
let nameFifo = "FIFO" + name;
|
|
19
19
|
|
|
20
|
+
function queueTags(queueName) {
|
|
21
|
+
return AWS_Tags$ReventlessAws.make(queueName, "Plugin", "DeadLetter", "Plugin", undefined, undefined, undefined, undefined);
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
let queue = new (Aws.sqs.Queue)(name, {
|
|
25
|
+
tags: queueTags(name),
|
|
21
26
|
visibilityTimeoutSeconds: 180,
|
|
22
27
|
sqsManagedSseEnabled: false
|
|
23
28
|
});
|
|
@@ -25,6 +30,7 @@ let queue = new (Aws.sqs.Queue)(name, {
|
|
|
25
30
|
let fifoQueue = new (Aws.sqs.Queue)(nameFifo, {
|
|
26
31
|
contentBasedDeduplication: true,
|
|
27
32
|
fifoQueue: true,
|
|
33
|
+
tags: queueTags(nameFifo),
|
|
28
34
|
visibilityTimeoutSeconds: 180,
|
|
29
35
|
sqsManagedSseEnabled: false
|
|
30
36
|
});
|
|
@@ -73,9 +79,13 @@ Monitoring$ReventlessCore.notify("DeadLetterSink", name, Util_Lambda$ReventlessA
|
|
|
73
79
|
|
|
74
80
|
let lambda = Pulumi.output(handler);
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
function esmTags(esmName) {
|
|
83
|
+
return AWS_Tags$ReventlessAws.make(esmName, "Plugin", "EventSourceMapping", "Plugin", undefined, undefined, undefined, undefined);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let _subscription = Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, name, queue, undefined, esmTags(name), opts);
|
|
77
87
|
|
|
78
|
-
let _fifoSubscription = Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, nameFifo, fifoQueue, undefined, opts);
|
|
88
|
+
let _fifoSubscription = Util_EventSourceMapping$ReventlessAws.subscribeSqs(lambda, nameFifo, fifoQueue, undefined, esmTags(nameFifo), opts);
|
|
79
89
|
|
|
80
90
|
function createQueuePolicyDocument(name, queueArn, handlerArn) {
|
|
81
91
|
return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "QueuePolicy", [{
|
|
@@ -143,6 +153,7 @@ Pulumi.all([
|
|
|
143
153
|
export {
|
|
144
154
|
name,
|
|
145
155
|
nameFifo,
|
|
156
|
+
queueTags,
|
|
146
157
|
queue,
|
|
147
158
|
fifoQueue,
|
|
148
159
|
opts,
|
|
@@ -154,6 +165,7 @@ export {
|
|
|
154
165
|
layers,
|
|
155
166
|
handler,
|
|
156
167
|
lambda,
|
|
168
|
+
esmTags,
|
|
157
169
|
_subscription,
|
|
158
170
|
_fifoSubscription,
|
|
159
171
|
createQueuePolicyDocument,
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
open PulumiAws
|
|
2
2
|
|
|
3
|
+
// `~tags` is required rather than optional: an EventSourceMapping is created by
|
|
4
|
+
// several unrelated adapters, and an optional argument is exactly how the last
|
|
5
|
+
// coverage sweep left these bare. Making the caller name the owning piece is the
|
|
6
|
+
// regression guard — the same reasoning as `makeFromCodeAsset`'s `~componentKind`.
|
|
3
7
|
let subscribe = (
|
|
4
8
|
~batchSize=?,
|
|
5
9
|
~lambda: Pulumi.Output.t<PulumiAws.Lambda.Function.t>,
|
|
6
10
|
~targetName,
|
|
7
11
|
~sourceName,
|
|
8
12
|
~source: ReventlessInfra.Adapter.resource,
|
|
13
|
+
~tags: Pulumi.Input.t<Aws.tags>,
|
|
9
14
|
~opts,
|
|
10
15
|
) =>
|
|
11
16
|
EventSourceMapping.make(
|
|
@@ -17,6 +22,7 @@ let subscribe = (
|
|
|
17
22
|
eventSourceArn: source.urn->Pulumi.Output.asInput,
|
|
18
23
|
startingPosition: LATEST,
|
|
19
24
|
?batchSize,
|
|
25
|
+
tags,
|
|
20
26
|
},
|
|
21
27
|
~opts=Some(opts),
|
|
22
28
|
)
|
|
@@ -26,6 +32,7 @@ let subscribeSqs = (
|
|
|
26
32
|
~name,
|
|
27
33
|
~queue: PulumiAws.SQS.Queue.t,
|
|
28
34
|
~batchSize=?,
|
|
35
|
+
~tags: Pulumi.Input.t<Aws.tags>,
|
|
29
36
|
~opts,
|
|
30
37
|
) => {
|
|
31
38
|
let esm = EventSourceMapping.make(
|
|
@@ -36,6 +43,7 @@ let subscribeSqs = (
|
|
|
36
43
|
->Pulumi.Output.asInput,
|
|
37
44
|
eventSourceArn: queue.arn->Pulumi.Output.asInput,
|
|
38
45
|
?batchSize,
|
|
46
|
+
tags,
|
|
39
47
|
},
|
|
40
48
|
~opts=Some(opts),
|
|
41
49
|
)
|
|
@@ -7,20 +7,22 @@ import * as AWS$ReventlessAws from "../adapter/AWS.res.mjs";
|
|
|
7
7
|
import * as Util$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util.res.mjs";
|
|
8
8
|
import * as Adapter$ReventlessInfra from "@reventlessdev/reventless-infra/src/adapter/Adapter.res.mjs";
|
|
9
9
|
|
|
10
|
-
function subscribe(batchSize, lambda, targetName, sourceName, source, opts) {
|
|
10
|
+
function subscribe(batchSize, lambda, targetName, sourceName, source, tags, opts) {
|
|
11
11
|
return new (Aws.lambda.EventSourceMapping)(Util$ReventlessCore.baseName(sourceName) + ("2" + targetName), {
|
|
12
12
|
functionName: Output$Pulumi.flatMap(lambda, lambda => lambda.arn),
|
|
13
13
|
batchSize: batchSize,
|
|
14
14
|
eventSourceArn: source.urn,
|
|
15
|
-
startingPosition: "LATEST"
|
|
15
|
+
startingPosition: "LATEST",
|
|
16
|
+
tags: tags
|
|
16
17
|
}, opts);
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function subscribeSqs(lambda, name, queue, batchSize, opts) {
|
|
20
|
+
function subscribeSqs(lambda, name, queue, batchSize, tags, opts) {
|
|
20
21
|
let esm = new (Aws.lambda.EventSourceMapping)(name, {
|
|
21
22
|
functionName: Output$Pulumi.flatMap(lambda, lambda => lambda.arn),
|
|
22
23
|
batchSize: batchSize,
|
|
23
|
-
eventSourceArn: queue.arn
|
|
24
|
+
eventSourceArn: queue.arn,
|
|
25
|
+
tags: tags
|
|
24
26
|
}, opts);
|
|
25
27
|
return Adapter$ReventlessInfra.make(esm.id, esm.id, esm.arn, esm.id.apply(param => AWS$ReventlessAws.SQS.service), undefined, undefined, undefined, Pulumi.output("aws:lambda:EventSourceMapping"), undefined, undefined);
|
|
26
28
|
}
|
|
@@ -559,6 +559,56 @@ describe("Runtime.toItem — tag_composite is keyed on the event's entity tags",
|
|
|
559
559
|
})
|
|
560
560
|
})
|
|
561
561
|
|
|
562
|
+
describe("Runtime.toItem — an empty tag value skips its GSI attribute", () => {
|
|
563
|
+
// `tag_<key>` is a GSI hash key, and DynamoDB rejects an empty string for a key
|
|
564
|
+
// attribute — writing one fails the whole append. An empty value is legitimate
|
|
565
|
+
// (an absent composite-partition member), so the attribute is skipped and that
|
|
566
|
+
// index goes sparse. See docs/plans/dcb-empty-tag-values-break-append.md.
|
|
567
|
+
let tagOf = (key, value): Reventless.DcbTag.tag => {key, value}
|
|
568
|
+
let tags = [tagOf("environment", "prod"), tagOf("componentName", "")]
|
|
569
|
+
let storedEvent: ReventlessCore.DcbEventLog_Adapter.rawStoredEvent = {
|
|
570
|
+
eventType: "ResourceAdded",
|
|
571
|
+
data: JSON.Object(Dict.make()),
|
|
572
|
+
tags,
|
|
573
|
+
meta: testMeta(),
|
|
574
|
+
}
|
|
575
|
+
let item =
|
|
576
|
+
Runtime.toItem("100", storedEvent, ~recordedAt="2026-07-08T00:00:00Z")
|
|
577
|
+
->JSON.Decode.object
|
|
578
|
+
->Option.getOrThrow
|
|
579
|
+
|
|
580
|
+
testSync("the non-empty tag keeps its attribute", () => {
|
|
581
|
+
expect(item->Dict.get("tag_environment"))->toEqual(Some("prod"->JSON.Encode.string))
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
testSync("the empty tag has no attribute at all", () => {
|
|
585
|
+
expect(item->Dict.get("tag_componentName"))->toEqual(None)
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
testSync("tag_composite still carries both tags", () => {
|
|
589
|
+
expect(item->Dict.get("tag_composite"))->toEqual(
|
|
590
|
+
Some(Runtime.compositeTagKey(tags)->JSON.Encode.string),
|
|
591
|
+
)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
testSync("the tags payload records the empty value verbatim", () => {
|
|
595
|
+
let payloadTags =
|
|
596
|
+
item
|
|
597
|
+
->Dict.get("tags")
|
|
598
|
+
->Option.flatMap(JSON.Decode.array)
|
|
599
|
+
->Option.getOrThrow
|
|
600
|
+
->Array.filterMap(t =>
|
|
601
|
+
t
|
|
602
|
+
->JSON.Decode.object
|
|
603
|
+
->Option.map(o => (
|
|
604
|
+
o->Dict.get("key")->Option.flatMap(JSON.Decode.string)->Option.getOr(""),
|
|
605
|
+
o->Dict.get("value")->Option.flatMap(JSON.Decode.string)->Option.getOr("!missing"),
|
|
606
|
+
))
|
|
607
|
+
)
|
|
608
|
+
expect(payloadTags)->toEqual([("environment", "prod"), ("componentName", "")])
|
|
609
|
+
})
|
|
610
|
+
})
|
|
611
|
+
|
|
562
612
|
describe("Runtime.buildConditionalTransactItems — folded create guard (after=None)", () => {
|
|
563
613
|
let event = (eventType, tags): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
|
|
564
614
|
eventType,
|
|
@@ -690,6 +690,53 @@ globalThis.describe("Runtime.toItem — tag_composite is keyed on the event's en
|
|
|
690
690
|
});
|
|
691
691
|
});
|
|
692
692
|
|
|
693
|
+
globalThis.describe("Runtime.toItem — an empty tag value skips its GSI attribute", () => {
|
|
694
|
+
let tags = [
|
|
695
|
+
{
|
|
696
|
+
key: "environment",
|
|
697
|
+
value: "prod"
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
key: "componentName",
|
|
701
|
+
value: ""
|
|
702
|
+
}
|
|
703
|
+
];
|
|
704
|
+
let storedEvent_data = {};
|
|
705
|
+
let storedEvent_meta = testMeta();
|
|
706
|
+
let storedEvent = {
|
|
707
|
+
eventType: "ResourceAdded",
|
|
708
|
+
data: storedEvent_data,
|
|
709
|
+
tags: tags,
|
|
710
|
+
meta: storedEvent_meta
|
|
711
|
+
};
|
|
712
|
+
let item = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.toItem("100", storedEvent, undefined, "2026-07-08T00:00:00Z")), undefined);
|
|
713
|
+
globalThis.test("the non-empty tag keeps its attribute", () => {
|
|
714
|
+
globalThis.expect(item["tag_environment"]).toEqual("prod");
|
|
715
|
+
});
|
|
716
|
+
globalThis.test("the empty tag has no attribute at all", () => {
|
|
717
|
+
globalThis.expect(item["tag_componentName"]).toEqual(undefined);
|
|
718
|
+
});
|
|
719
|
+
globalThis.test("tag_composite still carries both tags", () => {
|
|
720
|
+
globalThis.expect(item["tag_composite"]).toEqual(DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.compositeTagKey(tags));
|
|
721
|
+
});
|
|
722
|
+
globalThis.test("the tags payload records the empty value verbatim", () => {
|
|
723
|
+
let payloadTags = Stdlib_Array.filterMap(Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(item["tags"], Stdlib_JSON.Decode.array), undefined), t => Stdlib_Option.map(Stdlib_JSON.Decode.object(t), o => [
|
|
724
|
+
Stdlib_Option.getOr(Stdlib_Option.flatMap(o["key"], Stdlib_JSON.Decode.string), ""),
|
|
725
|
+
Stdlib_Option.getOr(Stdlib_Option.flatMap(o["value"], Stdlib_JSON.Decode.string), "!missing")
|
|
726
|
+
]));
|
|
727
|
+
globalThis.expect(payloadTags).toEqual([
|
|
728
|
+
[
|
|
729
|
+
"environment",
|
|
730
|
+
"prod"
|
|
731
|
+
],
|
|
732
|
+
[
|
|
733
|
+
"componentName",
|
|
734
|
+
""
|
|
735
|
+
]
|
|
736
|
+
]);
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
693
740
|
globalThis.describe("Runtime.buildConditionalTransactItems — folded create guard (after=None)", () => {
|
|
694
741
|
let event = (eventType, tags) => ({
|
|
695
742
|
eventType: eventType,
|
|
@@ -38,3 +38,44 @@ describe("AppSync_Resolver_Retrying.Functions.listAllItemsConnection", () => {
|
|
|
38
38
|
expect(code->String.includes("attribute_exists"))->toBe(false)
|
|
39
39
|
})
|
|
40
40
|
})
|
|
41
|
+
|
|
42
|
+
// Tripwires for docs/plans/aws-scan-connection-cursor-roundtrip.md. The Scan
|
|
43
|
+
// resolver's cursor must round-trip DynamoDB's own continuation token, not a
|
|
44
|
+
// synthetic index — every list past page 1 was unreachable before this.
|
|
45
|
+
describe("listAllItemsConnection — Scan cursor round-trip (paging fixes 1–3)", () => {
|
|
46
|
+
let code = AppSync_Resolver_Retrying.Functions.listAllItemsConnection(~labelField="name")->codeOf
|
|
47
|
+
|
|
48
|
+
testSync("Fix 1: response encodes the real nextToken as the cursor", () => {
|
|
49
|
+
expect(code->String.includes("const next = ctx.result?.nextToken ?? null;"))->toBe(true)
|
|
50
|
+
expect(
|
|
51
|
+
code->String.includes("util.base64Encode(JSON.stringify({ token: next, index: i }))"),
|
|
52
|
+
)->toBe(true)
|
|
53
|
+
expect(code->String.includes("hasNextPage: !!next,"))->toBe(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
testSync("Fix 1: request decodes `after` back to the DynamoDB token", () => {
|
|
57
|
+
expect(code->String.includes("JSON.parse(util.base64Decode(ctx.args.after))"))->toBe(true)
|
|
58
|
+
expect(code->String.includes("nextToken: after,"))->toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
testSync("Fix 1: the old synthetic-index cursor is gone", () => {
|
|
62
|
+
// The regression: `cursor: ctx.args.after ? ctx.args.after + '_' + i : '' + i`.
|
|
63
|
+
expect(code->String.includes("ctx.args.after + '_' + i"))->toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
testSync("Fix 2: backward paging (last/before) is rejected, not silently mishandled", () => {
|
|
67
|
+
expect(code->String.includes("ctx.args.before != null || ctx.args.last != null"))->toBe(true)
|
|
68
|
+
expect(code->String.includes("UnsupportedPagination"))->toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
testSync("Fix 3: an empty/short filtered page still yields a resumable boundary cursor", () => {
|
|
72
|
+
expect(
|
|
73
|
+
code->String.includes(
|
|
74
|
+
"const boundary = next ? util.base64Encode(JSON.stringify({ token: next, index: -1 })) : null;",
|
|
75
|
+
),
|
|
76
|
+
)->toBe(true)
|
|
77
|
+
expect(
|
|
78
|
+
code->String.includes("edges.length > 0 ? edges[edges.length - 1].cursor : boundary"),
|
|
79
|
+
)->toBe(true)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -29,6 +29,30 @@ globalThis.describe("AppSync_Resolver_Retrying.Functions.listAllItemsConnection"
|
|
|
29
29
|
});
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
globalThis.describe("listAllItemsConnection — Scan cursor round-trip (paging fixes 1–3)", () => {
|
|
33
|
+
let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, undefined);
|
|
34
|
+
globalThis.test("Fix 1: response encodes the real nextToken as the cursor", () => {
|
|
35
|
+
globalThis.expect(code.includes("const next = ctx.result?.nextToken ?? null;")).toBe(true);
|
|
36
|
+
globalThis.expect(code.includes("util.base64Encode(JSON.stringify({ token: next, index: i }))")).toBe(true);
|
|
37
|
+
globalThis.expect(code.includes("hasNextPage: !!next,")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
globalThis.test("Fix 1: request decodes `after` back to the DynamoDB token", () => {
|
|
40
|
+
globalThis.expect(code.includes("JSON.parse(util.base64Decode(ctx.args.after))")).toBe(true);
|
|
41
|
+
globalThis.expect(code.includes("nextToken: after,")).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
globalThis.test("Fix 1: the old synthetic-index cursor is gone", () => {
|
|
44
|
+
globalThis.expect(code.includes("ctx.args.after + '_' + i")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
globalThis.test("Fix 2: backward paging (last/before) is rejected, not silently mishandled", () => {
|
|
47
|
+
globalThis.expect(code.includes("ctx.args.before != null || ctx.args.last != null")).toBe(true);
|
|
48
|
+
globalThis.expect(code.includes("UnsupportedPagination")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
globalThis.test("Fix 3: an empty/short filtered page still yields a resumable boundary cursor", () => {
|
|
51
|
+
globalThis.expect(code.includes("const boundary = next ? util.base64Encode(JSON.stringify({ token: next, index: -1 })) : null;")).toBe(true);
|
|
52
|
+
globalThis.expect(code.includes("edges.length > 0 ? edges[edges.length - 1].cursor : boundary")).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
32
56
|
export {
|
|
33
57
|
codeOf,
|
|
34
58
|
}
|
|
@@ -458,3 +458,61 @@ describe("DCB DynamoDb integration — per-type fence granularity", () => {
|
|
|
458
458
|
expect(conflicts >= 1)->toBe(true)
|
|
459
459
|
})
|
|
460
460
|
})
|
|
461
|
+
|
|
462
|
+
// An empty tag value is a legitimate model state (an absent member of a composite
|
|
463
|
+
// partition key), and the in-memory and SQLite backends have always accepted one.
|
|
464
|
+
// DynamoDB used to reject the whole append: `tag_<key>` is a GSI hash key and a key
|
|
465
|
+
// attribute cannot hold an empty string. The adapter now skips that one attribute,
|
|
466
|
+
// leaving the index sparse — everything else about the event is unchanged.
|
|
467
|
+
// See docs/plans/dcb-empty-tag-values-break-append.md.
|
|
468
|
+
describe("DCB DynamoDb integration — an empty tag value does not break the append", () => {
|
|
469
|
+
let tags = [tag("orderId", "O-empty"), tag("customerId", "")]
|
|
470
|
+
|
|
471
|
+
testAsync("appends, and reads back through its partition with the tag intact", async () => {
|
|
472
|
+
let table = await H.freshTable()
|
|
473
|
+
let result = await Runtime.appendConditional(
|
|
474
|
+
table,
|
|
475
|
+
[event("OrderPlaced", tags)],
|
|
476
|
+
{query: [{tags: tags}], after: ?None},
|
|
477
|
+
~partitionTag=simple("orderId"),
|
|
478
|
+
)
|
|
479
|
+
expect(isOk(result))->toBe(true)
|
|
480
|
+
|
|
481
|
+
let byPartition = await Runtime.read(table)(~query=[{tags: [tag("orderId", "O-empty")]}])
|
|
482
|
+
expect(byPartition.events->Array.length)->toBe(1)
|
|
483
|
+
let stored = byPartition.events->Array.getUnsafe(0)
|
|
484
|
+
expect(stored.eventType)->toBe("OrderPlaced")
|
|
485
|
+
// The event records the empty value verbatim — only the index skips it.
|
|
486
|
+
expect(stored.tags->Array.map(t => (t.key, t.value)))->toEqual([
|
|
487
|
+
("orderId", "O-empty"),
|
|
488
|
+
("customerId", ""),
|
|
489
|
+
])
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
testAsync("is found by a composite (multi-tag) read", async () => {
|
|
493
|
+
let table = await H.freshTable()
|
|
494
|
+
let _ = await Runtime.appendUnconditional(
|
|
495
|
+
table,
|
|
496
|
+
[event("OrderPlaced", tags)],
|
|
497
|
+
~partitionTag=simple("orderId"),
|
|
498
|
+
)
|
|
499
|
+
let composite = await Runtime.read(table)(~query=[{tags: tags}])
|
|
500
|
+
expect(composite.events->Array.length)->toBe(1)
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
testAsync("is absent from that tag's single-tag index rather than erroring", async () => {
|
|
504
|
+
let table = await H.freshTable()
|
|
505
|
+
let _ = await Runtime.appendUnconditional(
|
|
506
|
+
table,
|
|
507
|
+
[event("OrderPlaced", tags)],
|
|
508
|
+
~partitionTag=simple("orderId"),
|
|
509
|
+
)
|
|
510
|
+
// A cross-partition read goes through the `tag_customerId` GSI, which carries
|
|
511
|
+
// no empty-valued key. DynamoDB would reject the key condition outright, so the
|
|
512
|
+
// adapter short-circuits: empty result, no exception.
|
|
513
|
+
let crossPartition = await Runtime.read(table, ~crossPartitionTagKeys=["customerId"])(
|
|
514
|
+
~query=[{tags: [tag("customerId", "")]}],
|
|
515
|
+
)
|
|
516
|
+
expect(crossPartition.events->Array.length)->toBe(0)
|
|
517
|
+
})
|
|
518
|
+
})
|
|
@@ -563,6 +563,84 @@ globalThis.describe("DCB DynamoDb integration — per-type fence granularity", (
|
|
|
563
563
|
});
|
|
564
564
|
});
|
|
565
565
|
|
|
566
|
+
globalThis.describe("DCB DynamoDb integration — an empty tag value does not break the append", () => {
|
|
567
|
+
let tags = [
|
|
568
|
+
{
|
|
569
|
+
key: "orderId",
|
|
570
|
+
value: "O-empty"
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
key: "customerId",
|
|
574
|
+
value: ""
|
|
575
|
+
}
|
|
576
|
+
];
|
|
577
|
+
globalThis.test("appends, and reads back through its partition with the tag intact", async () => {
|
|
578
|
+
let table = await DcbIntegrationHarness$ReventlessAws.freshTable();
|
|
579
|
+
let result = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendConditional(table, [event("OrderPlaced", tags)], {
|
|
580
|
+
query: [{
|
|
581
|
+
tags: tags
|
|
582
|
+
}]
|
|
583
|
+
}, {
|
|
584
|
+
TAG: "Simple",
|
|
585
|
+
_0: {
|
|
586
|
+
key: "orderId"
|
|
587
|
+
}
|
|
588
|
+
}, undefined);
|
|
589
|
+
globalThis.expect(isOk(result)).toBe(true);
|
|
590
|
+
let byPartition = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.read(table, undefined)([{
|
|
591
|
+
tags: [{
|
|
592
|
+
key: "orderId",
|
|
593
|
+
value: "O-empty"
|
|
594
|
+
}]
|
|
595
|
+
}], undefined);
|
|
596
|
+
globalThis.expect(byPartition.events.length).toBe(1);
|
|
597
|
+
let stored = byPartition.events[0];
|
|
598
|
+
globalThis.expect(stored.eventType).toBe("OrderPlaced");
|
|
599
|
+
globalThis.expect(stored.tags.map(t => [
|
|
600
|
+
t.key,
|
|
601
|
+
t.value
|
|
602
|
+
])).toEqual([
|
|
603
|
+
[
|
|
604
|
+
"orderId",
|
|
605
|
+
"O-empty"
|
|
606
|
+
],
|
|
607
|
+
[
|
|
608
|
+
"customerId",
|
|
609
|
+
""
|
|
610
|
+
]
|
|
611
|
+
]);
|
|
612
|
+
});
|
|
613
|
+
globalThis.test("is found by a composite (multi-tag) read", async () => {
|
|
614
|
+
let table = await DcbIntegrationHarness$ReventlessAws.freshTable();
|
|
615
|
+
await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendUnconditional(table, [event("OrderPlaced", tags)], {
|
|
616
|
+
TAG: "Simple",
|
|
617
|
+
_0: {
|
|
618
|
+
key: "orderId"
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
let composite = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.read(table, undefined)([{
|
|
622
|
+
tags: tags
|
|
623
|
+
}], undefined);
|
|
624
|
+
globalThis.expect(composite.events.length).toBe(1);
|
|
625
|
+
});
|
|
626
|
+
globalThis.test("is absent from that tag's single-tag index rather than erroring", async () => {
|
|
627
|
+
let table = await DcbIntegrationHarness$ReventlessAws.freshTable();
|
|
628
|
+
await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.appendUnconditional(table, [event("OrderPlaced", tags)], {
|
|
629
|
+
TAG: "Simple",
|
|
630
|
+
_0: {
|
|
631
|
+
key: "orderId"
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
let crossPartition = await DcbEventLogStorage_DynamoDb_Runtime$ReventlessAws.read(table, ["customerId"])([{
|
|
635
|
+
tags: [{
|
|
636
|
+
key: "customerId",
|
|
637
|
+
value: ""
|
|
638
|
+
}]
|
|
639
|
+
}], undefined);
|
|
640
|
+
globalThis.expect(crossPartition.events.length).toBe(0);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
566
644
|
let H;
|
|
567
645
|
|
|
568
646
|
let Runtime;
|