@reventlessdev/reventless-aws 3.0.0-alpha.305 → 3.0.0-alpha.307
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 +21 -0
- package/package.json +8 -8
- package/src/Platform.res +1 -1
- package/src/adapter/Api/AppSync_Resolver_Native.res +1 -1
- package/src/adapter/Api/AppSync_Resolver_Retrying.res +2 -2
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res +69 -11
- package/src/adapter/Auth/Auth_ActiveRolePoolAttachment.res.mjs +19 -1
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +2 -2
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Postgres.res +1 -1
- package/src/adapter/EventLog/EventLogStorage_DynamoDb_Runtime.res +1 -1
- package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +1 -1
- package/src/adapter/Postgres/PgProjectionFeed.res +1 -1
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +64 -0
- package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +54 -2
- package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +138 -46
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +25 -21
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res +19 -11
- package/src/adapter/QueryDb/QueryDbResolvers_Lambda.res.mjs +11 -10
- package/src/adapter/QueryEngine/QueryEngine_DynamoDb.res +1 -1
- package/src/adapter/Runtime/AggregateEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/AggregateEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/CounterEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/CounterEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +44 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/EventCollectorEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/EventMapperEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/EventMapperEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/ExtensionPointEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/ExtensionPointEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/PgQueryResolverEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +7 -1
- package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +2 -1
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +1 -1
- package/src/adapter/Runtime/ProjectionEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/QueryDbEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/ReadModelEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/ReadModelEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -1
- package/src/adapter/Runtime/SideEffectEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/SideEffectEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/StreamRoutedEntryPoint_Ops.res +1 -1
- package/src/adapter/Runtime/TaskBucketEntryPoint.mjs +1 -1
- package/src/adapter/Runtime/TaskBucketEntryPoint_Ops.res +1 -1
- package/src/util/Util_Bundle.res +1 -1
- package/src/util/Util_DynamoDbStream_Runtime.res +1 -1
- package/tests/AWS_TagsTest.res +2 -2
- package/tests/AppSync_RetirementNarrowingTest.res +272 -0
- package/tests/AppSync_RetirementNarrowingTest.res.mjs +193 -0
- package/tests/Auth_ActiveRolePoolAttachmentTest.res +140 -1
- package/tests/Auth_ActiveRolePoolAttachmentTest.res.mjs +117 -2
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +2 -2
- package/tests/EventLogStorage_DynamoDb_RuntimeTest.res +1 -1
- package/tests/PgQueryResolver_LambdaTest.res +2 -0
- package/tests/PgQueryResolver_LambdaTest.res.mjs +34 -30
- package/tests/QueryDbResolvers_AppSyncTest.res +2 -2
- package/tests/Util_BundleEsmLoaderTest.res +1 -1
- package/tests/Util_BundleRuntimeExtensionTest.res +1 -1
- package/tests/integration/AggregateEntryPoint_IntegrationTest.res +1 -1
- package/tests/integration/DcbCommandTopicEntryPoint_IntegrationTest.res +1 -1
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
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.307 (2026-08-18)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **api:** make the by-index door answer, and let an elevated caller widen it ([0fe0c6f](https://github.com/ReventlessDev/reventless-core/commit/0fe0c6f8dec6228ecaba39577e28d780b4f79c83))
|
|
11
|
+
* **aws:** apply [@owner](https://github.com/owner) on the DynamoDB by-ids and by-index doors ([a6b5afc](https://github.com/ReventlessDev/reventless-core/commit/a6b5afc2923bfa4d3e0b2990367f67e8ffdd8877))
|
|
12
|
+
* **aws:** narrow retirement on every DynamoDB door, not only the list ([d6a799b](https://github.com/ReventlessDev/reventless-core/commit/d6a799b287b28e9c1f75e193adbf3f6328a6bf2d))
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **core:** let a reference name a retired row, and let an elevated caller open one ([9e2623a](https://github.com/ReventlessDev/reventless-core/commit/9e2623a4b22487561607fcc0ca19d51726069ee4))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 3.0.0-alpha.306 (2026-08-16)
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **aws:** attach the pre-token trigger through both fields Cognito keeps ([6d6e2e9](https://github.com/ReventlessDev/reventless-core/commit/6d6e2e90e1d6cb3defabaab8ae7f3c9d1955a5df))
|
|
23
|
+
* **aws:** scope by-key reads to the owner, not only lists ([8232fd4](https://github.com/ReventlessDev/reventless-core/commit/8232fd4c09c1098c7265e4a17882ee44884f3bec))
|
|
24
|
+
* **core:** stamp the owner from each DCB slice's own command schema ([6edbdf4](https://github.com/ReventlessDev/reventless-core/commit/6edbdf4680f2535597c3b560215aa514ceed27eb))
|
|
25
|
+
|
|
26
|
+
|
|
6
27
|
# 3.0.0-alpha.305 (2026-08-16)
|
|
7
28
|
|
|
8
29
|
### 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.307",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
|
|
16
16
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
17
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
|
-
"@reventlessdev/rescript-
|
|
19
|
-
"@reventlessdev/rescript-
|
|
18
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.79",
|
|
19
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
21
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
23
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.143",
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.239",
|
|
24
23
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
25
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
26
|
-
"@reventlessdev/reventless-
|
|
24
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.103",
|
|
25
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.145",
|
|
26
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.117"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -656,7 +656,7 @@ module MakeWithConfig = (
|
|
|
656
656
|
}
|
|
657
657
|
module InboundTranslationSlice = InboundTranslationSlice_Builder.Make(ApiConfig)
|
|
658
658
|
|
|
659
|
-
// Admin UI-fragment registry slices (docs/plans/event-sourced-fragment-registries.md): the
|
|
659
|
+
// Admin UI-fragment registry slices (docs/plans/done/event-sourced-fragment-registries.md): the
|
|
660
660
|
// platform UI-fragment registry hosted as admin DCB slices sharing the admin DcbEventLog.
|
|
661
661
|
// Built once at platform-module level and passed into Admin.construct below (registers the
|
|
662
662
|
// slice's command handler exactly once).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** AppSync_Resolver_Native — drop-in replacement for
|
|
2
2
|
`PulumiAws.AppSync.Resolver` that uses `@pulumi/aws-native` (Cloud Control
|
|
3
3
|
API) so the CFN handler can internally wait out schema -> resolver
|
|
4
|
-
propagation. See docs/plans/appsync-resolver-aws-native.md. */
|
|
4
|
+
propagation. See docs/plans/done/appsync-resolver-aws-native.md. */
|
|
5
5
|
|
|
6
6
|
module Native = PulumiAws.AwsNative.AppSync.Resolver
|
|
7
7
|
module Functions = PulumiAws.AppSync.Resolver.Functions
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
`aws-native:appsync/resolver:Resolver` type so existing resources are
|
|
17
17
|
adopted in-place on first deploy (no delete+recreate).
|
|
18
18
|
|
|
19
|
-
See docs/plans/appsync-resolver-aws-native-retry.md. */
|
|
19
|
+
See docs/plans/done/appsync-resolver-aws-native-retry.md. */
|
|
20
20
|
|
|
21
21
|
// ── AWS SDK bindings ─────────────────────────────────────────────────────────
|
|
22
22
|
//
|
|
@@ -198,7 +198,7 @@ let jsThrow: JsExn.t => 'a = %raw(`e => { throw e }`)
|
|
|
198
198
|
|
|
199
199
|
- `NotFoundException / No field named` — schema-propagation race after a
|
|
200
200
|
successful `StartSchemaCreation`. Backstop for the per-plugin dedup
|
|
201
|
-
flow (see `docs/plans/appsync-schema-push-dedup.md`).
|
|
201
|
+
flow (see `docs/plans/done/appsync-schema-push-dedup.md`).
|
|
202
202
|
- `ConcurrentModificationException / Schema is currently being altered` —
|
|
203
203
|
API-level lock contention with an in-flight `StartSchemaCreation` on
|
|
204
204
|
the same API (admin resolvers racing the platform schema push from
|
|
@@ -62,8 +62,23 @@ let readOnlyKeys = [
|
|
|
62
62
|
"EmailConfigurationFailure",
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
-
/** The
|
|
66
|
-
|
|
65
|
+
/** The version of the pre-token-generation contract this resource attaches at.
|
|
66
|
+
|
|
67
|
+
[Auth_ActiveRoleTrigger_Ops] implements `V1_0` — it answers with
|
|
68
|
+
`claimsOverrideDetails` — and does so deliberately, because `V2_0`/`V3_0` buy
|
|
69
|
+
access-token customisation it does not need and are gated behind the
|
|
70
|
+
Essentials and Plus feature plans.
|
|
71
|
+
|
|
72
|
+
🚨 **A pool left at a version the handler does not implement does not fail —
|
|
73
|
+
it silently does nothing.** Cognito sends a `V2_0` event, the handler answers
|
|
74
|
+
in `V1_0` shape, and the service ignores the answer. Sign-in succeeds, tokens
|
|
75
|
+
mint, and the role narrowing never happens. So the version is not the pool's
|
|
76
|
+
to keep: the trigger and the contract it speaks are one unit, and attaching
|
|
77
|
+
the trigger means attaching its version too. */
|
|
78
|
+
let triggerVersion = "V1_0"
|
|
79
|
+
|
|
80
|
+
/** The pool's own configuration, sent back whole with the pre-token-generation
|
|
81
|
+
trigger set (or, when `preTokenGenerationArn` is `None`, removed).
|
|
67
82
|
|
|
68
83
|
Pure and total so the property that matters — a pool carrying non-default
|
|
69
84
|
settings still carries them afterwards — is checkable without a pool. */
|
|
@@ -89,21 +104,64 @@ let mergedUpdateInput = (
|
|
|
89
104
|
->Dict.get("LambdaConfig")
|
|
90
105
|
->Option.flatMap(JSON.Decode.object)
|
|
91
106
|
->Option.mapOr(Dict.make(), existing => Dict.fromArray(existing->Dict.toArray))
|
|
107
|
+
// 🚨 Cognito carries this one trigger in TWO fields: the legacy
|
|
108
|
+
// `PreTokenGeneration` string and the `PreTokenGenerationConfig` object.
|
|
109
|
+
// `UpdateUserPool` rejects the call outright when both are present naming
|
|
110
|
+
// different functions, so both are written, together, naming the same one.
|
|
111
|
+
//
|
|
112
|
+
// Writing only one and letting the other ride through on the copy above cannot
|
|
113
|
+
// work, and not merely because a customer might have set it: **AWS
|
|
114
|
+
// materialises the field we did not write.** Attach via the legacy field alone
|
|
115
|
+
// and the next `DescribeUserPool` reports a `PreTokenGenerationConfig` the
|
|
116
|
+
// service populated, which the merge would then feed back on the following
|
|
117
|
+
// deploy — so an attachment that succeeded once fails the next time with
|
|
118
|
+
// nothing changed but AWS normalising its own record. Writing the pair in
|
|
119
|
+
// agreement is the only form stable under repeated describe → merge → update.
|
|
92
120
|
switch preTokenGenerationArn {
|
|
93
|
-
| Some(arn) =>
|
|
94
|
-
|
|
121
|
+
| Some(arn) =>
|
|
122
|
+
let config = Dict.make()
|
|
123
|
+
config->Dict.set("LambdaArn", JSON.Encode.string(arn))
|
|
124
|
+
config->Dict.set("LambdaVersion", JSON.Encode.string(triggerVersion))
|
|
125
|
+
lambdaConfig->Dict.set("PreTokenGeneration", JSON.Encode.string(arn))
|
|
126
|
+
lambdaConfig->Dict.set("PreTokenGenerationConfig", JSON.Encode.object(config))
|
|
127
|
+
| None =>
|
|
128
|
+
// Both, or the destroyed function stays attached through whichever field was
|
|
129
|
+
// left behind — the "every sign-in fails on a pool nothing here would fix"
|
|
130
|
+
// hazard [delete_] exists to prevent.
|
|
131
|
+
lambdaConfig->Dict.delete("PreTokenGeneration")
|
|
132
|
+
lambdaConfig->Dict.delete("PreTokenGenerationConfig")
|
|
95
133
|
}
|
|
96
134
|
out->Dict.set("LambdaConfig", lambdaConfig->JSON.Encode.object)
|
|
97
135
|
out
|
|
98
136
|
}
|
|
99
137
|
|
|
100
|
-
/** The trigger currently attached to a described pool, if any.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
/** The trigger currently attached to a described pool, if any.
|
|
139
|
+
|
|
140
|
+
Prefers `PreTokenGenerationConfig`, since that is the field AWS populates and
|
|
141
|
+
the one that carries the version.
|
|
142
|
+
|
|
143
|
+
A pool whose `LambdaVersion` names a contract [triggerVersion] does not
|
|
144
|
+
implement reports `None` — *not attached* — even when it points at our own
|
|
145
|
+
function. That is what turns the silent-no-op case into ordinary drift:
|
|
146
|
+
refresh records nothing attached, and the next `up` rewrites the pair at the
|
|
147
|
+
right version. Reporting the ARN instead would leave a pool that mints
|
|
148
|
+
un-narrowed tokens looking correct forever. */
|
|
149
|
+
let attachedTrigger = (~described: dict<JSON.t>): option<string> => {
|
|
150
|
+
let lambdaConfig = described->Dict.get("LambdaConfig")->Option.flatMap(JSON.Decode.object)
|
|
151
|
+
switch lambdaConfig
|
|
152
|
+
->Option.flatMap(c => c->Dict.get("PreTokenGenerationConfig"))
|
|
153
|
+
->Option.flatMap(JSON.Decode.object) {
|
|
154
|
+
| Some(config) =>
|
|
155
|
+
switch config->Dict.get("LambdaVersion")->Option.flatMap(JSON.Decode.string) {
|
|
156
|
+
| Some(version) if version != triggerVersion => None
|
|
157
|
+
| _ => config->Dict.get("LambdaArn")->Option.flatMap(JSON.Decode.string)
|
|
158
|
+
}
|
|
159
|
+
| None =>
|
|
160
|
+
lambdaConfig
|
|
161
|
+
->Option.flatMap(c => c->Dict.get("PreTokenGeneration"))
|
|
162
|
+
->Option.flatMap(JSON.Decode.string)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
107
165
|
|
|
108
166
|
// ── AWS SDK bindings (lazily imported — see AppSync_SourceApiAssociation_Retrying) ──
|
|
109
167
|
|
|
@@ -30,6 +30,8 @@ let readOnlyKeys = [
|
|
|
30
30
|
"EmailConfigurationFailure"
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
+
let triggerVersion = "V1_0";
|
|
34
|
+
|
|
33
35
|
function mergedUpdateInput(described, userPoolId, preTokenGenerationArn) {
|
|
34
36
|
let out = {};
|
|
35
37
|
Stdlib_Dict.forEachWithKey(described, (value, key) => {
|
|
@@ -44,16 +46,31 @@ function mergedUpdateInput(described, userPoolId, preTokenGenerationArn) {
|
|
|
44
46
|
out["UserPoolId"] = userPoolId;
|
|
45
47
|
let lambdaConfig = Stdlib_Option.mapOr(Stdlib_Option.flatMap(described["LambdaConfig"], Stdlib_JSON.Decode.object), {}, existing => Object.fromEntries(Object.entries(existing)));
|
|
46
48
|
if (preTokenGenerationArn !== undefined) {
|
|
49
|
+
let config = {};
|
|
50
|
+
config["LambdaArn"] = preTokenGenerationArn;
|
|
51
|
+
config["LambdaVersion"] = triggerVersion;
|
|
47
52
|
lambdaConfig["PreTokenGeneration"] = preTokenGenerationArn;
|
|
53
|
+
lambdaConfig["PreTokenGenerationConfig"] = config;
|
|
48
54
|
} else {
|
|
49
55
|
Stdlib_Dict.$$delete(lambdaConfig, "PreTokenGeneration");
|
|
56
|
+
Stdlib_Dict.$$delete(lambdaConfig, "PreTokenGenerationConfig");
|
|
50
57
|
}
|
|
51
58
|
out["LambdaConfig"] = lambdaConfig;
|
|
52
59
|
return out;
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
function attachedTrigger(described) {
|
|
56
|
-
|
|
63
|
+
let lambdaConfig = Stdlib_Option.flatMap(described["LambdaConfig"], Stdlib_JSON.Decode.object);
|
|
64
|
+
let config = Stdlib_Option.flatMap(Stdlib_Option.flatMap(lambdaConfig, c => c["PreTokenGenerationConfig"]), Stdlib_JSON.Decode.object);
|
|
65
|
+
if (config === undefined) {
|
|
66
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(lambdaConfig, c => c["PreTokenGeneration"]), Stdlib_JSON.Decode.string);
|
|
67
|
+
}
|
|
68
|
+
let version = Stdlib_Option.flatMap(config["LambdaVersion"], Stdlib_JSON.Decode.string);
|
|
69
|
+
if (version !== undefined && version !== triggerVersion) {
|
|
70
|
+
return;
|
|
71
|
+
} else {
|
|
72
|
+
return Stdlib_Option.flatMap(config["LambdaArn"], Stdlib_JSON.Decode.string);
|
|
73
|
+
}
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
let newOf1 = ((C, x) => new C(x));
|
|
@@ -310,6 +327,7 @@ function make(nameOpt, props, opts) {
|
|
|
310
327
|
export {
|
|
311
328
|
log,
|
|
312
329
|
readOnlyKeys,
|
|
330
|
+
triggerVersion,
|
|
313
331
|
mergedUpdateInput,
|
|
314
332
|
attachedTrigger,
|
|
315
333
|
newOf1,
|
|
@@ -704,7 +704,7 @@ let collectEventTags = (
|
|
|
704
704
|
// local backends' true DCB query semantics (and fixing analysis Issue 4 — the
|
|
705
705
|
// one-event-type-per-attribute deadlock). It also folds the create guard into the
|
|
706
706
|
// fence (see `buildConditionalFenceUpdate`), retiring the `create#` rows.
|
|
707
|
-
// Plan: docs/plans/dcb-fence-event-type-granularity.md.
|
|
707
|
+
// Plan: docs/plans/done/dcb-fence-event-type-granularity.md.
|
|
708
708
|
let fenceTypeAttr = (eventType: string) => `pos#${eventType}`
|
|
709
709
|
|
|
710
710
|
let dedupStrings = (xs: array<string>): array<string> => {
|
|
@@ -975,7 +975,7 @@ let appendUnconditional = async (
|
|
|
975
975
|
// fan-out sharing those prefixes turns into a hot partition → `TransactionConflict`
|
|
976
976
|
// → `retries exhausted`; it also over-fenced (two DISTINCT composite entities
|
|
977
977
|
// sharing a member value serialized needlessly). Plan:
|
|
978
|
-
// docs/plans/
|
|
978
|
+
// docs/plans/done/dcb-hot-tag-fence-contention.md § "Root-cause correction".
|
|
979
979
|
let compositeFenceTagKey = "__dcb_composite__"
|
|
980
980
|
|
|
981
981
|
let makeCompositeFenceTag = (
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// The pool is lazy (`PgRuntime.poolFor`), so binding ops inside the deploy-time
|
|
12
12
|
// `Output.apply` opens no connection during `pulumi up` — a real query only runs
|
|
13
13
|
// inside the DcbCommandTopic Lambda at runtime. See
|
|
14
|
-
// docs/plans/aws-postgres-change-feed-bridge.md.
|
|
14
|
+
// docs/plans/done/aws-postgres-change-feed-bridge.md.
|
|
15
15
|
|
|
16
16
|
let make: ReventlessCore.DcbEventLog_Adapter.storageMaker = (
|
|
17
17
|
~name,
|
|
@@ -71,7 +71,7 @@ let append = table =>
|
|
|
71
71
|
)
|
|
72
72
|
->Effect.runPromise
|
|
73
73
|
|
|
74
|
-
// ─── Aggregate snapshots (docs/plans/aggregate-snapshotting.md) ───
|
|
74
|
+
// ─── Aggregate snapshots (docs/plans/done/aggregate-snapshotting.md) ───
|
|
75
75
|
// A snapshot lives in the SAME table as the events, on the reserved sort key
|
|
76
76
|
// `position = "SNAPSHOT"`. Event positions are 9-digit zero-padded numbers, so
|
|
77
77
|
// bounding the replay query to the all-digit range excludes the snapshot row
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// consumes — onto that queue. The plugin EventCollector (`EventCollectorEntryPoint`)
|
|
6
6
|
// then drives the full fan-out: read-model projections, aggregate command topics,
|
|
7
7
|
// and the cross-plugin SNS EventTopic. See
|
|
8
|
-
// docs/plans/aws-postgres-change-feed-bridge.md.
|
|
8
|
+
// docs/plans/done/aws-postgres-change-feed-bridge.md.
|
|
9
9
|
//
|
|
10
10
|
// The transform reuses the canonical DynamoDB shape producers
|
|
11
11
|
// (`derivePartitionKey` + `buildJsonEvent'`) so the emitted body is byte-identical
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// B3.0 — projection delivery on Postgres platforms
|
|
2
|
-
// (docs/plans/aws-postgres-querydb-adapter.md).
|
|
2
|
+
// (docs/plans/done/aws-postgres-querydb-adapter.md).
|
|
3
3
|
//
|
|
4
4
|
// On DynamoDB platforms the ReadModel / StateViewSlice Lambdas consume their
|
|
5
5
|
// source event logs via DynamoDB-stream event-source mappings. Postgres logs have
|
|
@@ -99,6 +99,10 @@ type binding = {
|
|
|
99
99
|
ownerField: option<string>,
|
|
100
100
|
retiredField: option<string>,
|
|
101
101
|
retiredValues: option<array<string>>,
|
|
102
|
+
/** Whether a reference to a retired row of this read model still resolves its
|
|
103
|
+
name — the `@namedWhenRetired` opt-in. Read only by the reference door;
|
|
104
|
+
every other door narrows as it always did. */
|
|
105
|
+
namedWhenRetired: bool,
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
// -- arg helpers -------------------------------------------------------------
|
|
@@ -253,6 +257,66 @@ let dispatch = async (
|
|
|
253
257
|
Array.concat(found, extra)->Array.filter(item => ownerAllows(item) && retiredAllows(item)),
|
|
254
258
|
)
|
|
255
259
|
|
|
260
|
+
| "refs" =>
|
|
261
|
+
// The reference door. The same by-ids read, projected on the way out to
|
|
262
|
+
// what naming a row needs, with the retirement lifted for a view that said
|
|
263
|
+
// its retired rows keep their names — and the owner rule left exactly where
|
|
264
|
+
// it was, so a row that is somebody's is still only theirs.
|
|
265
|
+
let ids = payload.arguments->argStrs("ids")
|
|
266
|
+
let found = await binding.pushdowns.byIds(~readModelName=rm, ids)
|
|
267
|
+
let missing =
|
|
268
|
+
found->Array.length < ids->Array.length
|
|
269
|
+
? ids->Array.filterMap(ReventlessCore.Api_Ids.alternateKey)
|
|
270
|
+
: []
|
|
271
|
+
let extra = missing->Array.length > 0
|
|
272
|
+
? await binding.pushdowns.byIds(~readModelName=rm, missing)
|
|
273
|
+
: []
|
|
274
|
+
let retirementOf = (item: JSON.t) =>
|
|
275
|
+
switch binding.retiredField {
|
|
276
|
+
| None => (false, None)
|
|
277
|
+
| Some(field) =>
|
|
278
|
+
let cell = item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(field))
|
|
279
|
+
let scope: Reventless.OwnerScope.retiredScope = {
|
|
280
|
+
field,
|
|
281
|
+
values: binding.retiredValues,
|
|
282
|
+
}
|
|
283
|
+
let retired = scope->Reventless.OwnerScope.isRetiredValue(cell)
|
|
284
|
+
(
|
|
285
|
+
retired,
|
|
286
|
+
retired && binding.retiredValues->Option.isSome
|
|
287
|
+
? cell->Option.flatMap(JSON.Decode.string)
|
|
288
|
+
: None,
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
JSON.Encode.array(
|
|
292
|
+
Array.concat(found, extra)
|
|
293
|
+
->Array.filter(ownerAllows)
|
|
294
|
+
->Array.filterMap(item => {
|
|
295
|
+
let (retired, state) = retirementOf(item)
|
|
296
|
+
if retired && !binding.namedWhenRetired {
|
|
297
|
+
None
|
|
298
|
+
} else {
|
|
299
|
+
let get = key => item->JSON.Decode.object->Option.flatMap(d => d->Dict.get(key))
|
|
300
|
+
let id = get("id")->Option.flatMap(JSON.Decode.string)->Option.getOr("")
|
|
301
|
+
Some(
|
|
302
|
+
Dict.fromArray([
|
|
303
|
+
("id", JSON.Encode.string(id)),
|
|
304
|
+
(
|
|
305
|
+
"label",
|
|
306
|
+
JSON.Encode.string(
|
|
307
|
+
get(binding.labelField)
|
|
308
|
+
->Option.flatMap(JSON.Decode.string)
|
|
309
|
+
->Option.getOr(id),
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
("retired", JSON.Encode.bool(retired)),
|
|
313
|
+
("retiredState", state->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
|
|
314
|
+
])->JSON.Encode.object,
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
}),
|
|
318
|
+
)
|
|
319
|
+
|
|
256
320
|
| "items" =>
|
|
257
321
|
// Sub-id connection: {single}Items(id, filter, first/after/last/before).
|
|
258
322
|
// Requires a subIdField; without one it's an empty connection.
|
|
@@ -185,11 +185,63 @@ async function dispatch(binding, lookupBindingOpt, payload) {
|
|
|
185
185
|
let items$1 = await binding.pushdowns.scanAll(rm);
|
|
186
186
|
return QueryDbListQuery$ReventlessCore.run(items$1, argsDict, binding.capability, binding.labelField, undefined, ownerScope, retiredScope);
|
|
187
187
|
break;
|
|
188
|
+
case "refs" :
|
|
189
|
+
let ids$1 = argStrs(payload.arguments, "ids");
|
|
190
|
+
let found$2 = await binding.pushdowns.byIds(rm, ids$1);
|
|
191
|
+
let missing$1 = found$2.length < ids$1.length ? Stdlib_Array.filterMap(ids$1, Api_Ids$ReventlessCore.alternateKey) : [];
|
|
192
|
+
let extra$1 = missing$1.length !== 0 ? await binding.pushdowns.byIds(rm, missing$1) : [];
|
|
193
|
+
let retirementOf = item => {
|
|
194
|
+
let field = binding.retiredField;
|
|
195
|
+
if (field === undefined) {
|
|
196
|
+
return [
|
|
197
|
+
false,
|
|
198
|
+
undefined
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
let cell = Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[field]);
|
|
202
|
+
let scope_values = binding.retiredValues;
|
|
203
|
+
let scope = {
|
|
204
|
+
field: field,
|
|
205
|
+
values: scope_values
|
|
206
|
+
};
|
|
207
|
+
let retired = OwnerScope$Reventless.isRetiredValue(scope, cell);
|
|
208
|
+
return [
|
|
209
|
+
retired,
|
|
210
|
+
retired && Stdlib_Option.isSome(binding.retiredValues) ? Stdlib_Option.flatMap(cell, Stdlib_JSON.Decode.string) : undefined
|
|
211
|
+
];
|
|
212
|
+
};
|
|
213
|
+
return Stdlib_Array.filterMap(found$2.concat(extra$1).filter(ownerAllows), item => {
|
|
214
|
+
let match = retirementOf(item);
|
|
215
|
+
let retired = match[0];
|
|
216
|
+
if (retired && !binding.namedWhenRetired) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let get = key => Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(item), d => d[key]);
|
|
220
|
+
let id = Stdlib_Option.getOr(Stdlib_Option.flatMap(get("id"), Stdlib_JSON.Decode.string), "");
|
|
221
|
+
return Object.fromEntries([
|
|
222
|
+
[
|
|
223
|
+
"id",
|
|
224
|
+
id
|
|
225
|
+
],
|
|
226
|
+
[
|
|
227
|
+
"label",
|
|
228
|
+
Stdlib_Option.getOr(Stdlib_Option.flatMap(get(binding.labelField), Stdlib_JSON.Decode.string), id)
|
|
229
|
+
],
|
|
230
|
+
[
|
|
231
|
+
"retired",
|
|
232
|
+
retired
|
|
233
|
+
],
|
|
234
|
+
[
|
|
235
|
+
"retiredState",
|
|
236
|
+
Stdlib_Option.mapOr(match[1], null, prim => prim)
|
|
237
|
+
]
|
|
238
|
+
]);
|
|
239
|
+
});
|
|
188
240
|
case "resolveMany" :
|
|
189
241
|
let target = Stdlib_Option.getOr(payload.target, rm);
|
|
190
242
|
let source = Stdlib_Option.getOr(payload.source, null);
|
|
191
|
-
let ids$
|
|
192
|
-
return await binding.pushdowns.byIds(target, ids$
|
|
243
|
+
let ids$2 = argStrs(source, Stdlib_Option.getOr(payload.sourceIdsField, ""));
|
|
244
|
+
return await binding.pushdowns.byIds(target, ids$2);
|
|
193
245
|
case "resolveOne" :
|
|
194
246
|
let target$1 = Stdlib_Option.getOr(payload.target, rm);
|
|
195
247
|
let source$1 = Stdlib_Option.getOr(payload.source, null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// AWS-side selection for the QueryDb (read model) storage backend — the QueryDb
|
|
2
2
|
// analogue of `EventLogBackend` / `DcbBackend` (B3.1,
|
|
3
|
-
// docs/plans/aws-postgres-querydb-adapter.md).
|
|
3
|
+
// docs/plans/done/aws-postgres-querydb-adapter.md).
|
|
4
4
|
//
|
|
5
5
|
// When `Platform.MakeWithConfig` is given a `~pgConnection`, it records the
|
|
6
6
|
// resolved selection here BEFORE any admin/plugin `construct` runs. Read by:
|