@reventlessdev/reventless-aws 3.0.0-alpha.304 → 3.0.0-alpha.306
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 +9 -9
- 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/QueryDbBackend.res +1 -1
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +29 -15
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +6 -5
- 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 +8 -9
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res +36 -8
- package/src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs +18 -7
- 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 +1 -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/StateChangeSliceRuntime_Builder_Single.res +3 -1
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +4 -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/plugin/runtime/PluginRuntime_Builder.res +46 -22
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +16 -11
- package/src/util/Util_Bundle.res +1 -1
- package/src/util/Util_DynamoDbStream_Runtime.res +1 -1
- package/src/util/Util_LambdaEnvBudget.res +74 -0
- package/src/util/Util_LambdaEnvBudget.res.mjs +37 -0
- package/tests/AWS_TagsTest.res +2 -2
- 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/EventCollectorEntryPoint_OpsTest.res +13 -4
- package/tests/EventCollectorEntryPoint_OpsTest.res.mjs +19 -13
- package/tests/EventLogStorage_DynamoDb_RuntimeTest.res +1 -1
- package/tests/QueryDbResolvers_AppSyncTest.res +2 -2
- package/tests/Util_BundleEsmLoaderTest.res +1 -1
- package/tests/Util_BundleRuntimeExtensionTest.res +1 -1
- package/tests/Util_LambdaEnvBudgetTest.res +78 -0
- package/tests/Util_LambdaEnvBudgetTest.res.mjs +55 -0
- 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,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.306 (2026-08-16)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** attach the pre-token trigger through both fields Cognito keeps ([6d6e2e9](https://github.com/ReventlessDev/reventless-core/commit/6d6e2e90e1d6cb3defabaab8ae7f3c9d1955a5df))
|
|
11
|
+
* **aws:** scope by-key reads to the owner, not only lists ([8232fd4](https://github.com/ReventlessDev/reventless-core/commit/8232fd4c09c1098c7265e4a17882ee44884f3bec))
|
|
12
|
+
* **core:** stamp the owner from each DCB slice's own command schema ([6edbdf4](https://github.com/ReventlessDev/reventless-core/commit/6edbdf4680f2535597c3b560215aa514ceed27eb))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# 3.0.0-alpha.305 (2026-08-16)
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **aws:** move the EventCollector's queue-URL maps out of the environment ([31172cb](https://github.com/ReventlessDev/reventless-core/commit/31172cb793d267729f201794ff785f9f982315fa))
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.304 (2026-08-16)
|
|
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.306",
|
|
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-effect": "0.1.0-alpha.32",
|
|
16
15
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
16
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
17
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.11",
|
|
18
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.77",
|
|
19
18
|
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
20
|
-
"@reventlessdev/rescript-pulumi-
|
|
21
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.237",
|
|
19
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.78",
|
|
22
20
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
23
|
-
"@reventlessdev/
|
|
24
|
-
"@reventlessdev/reventless-
|
|
25
|
-
"@reventlessdev/reventless-
|
|
26
|
-
"@reventlessdev/reventless-
|
|
21
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.238",
|
|
23
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.144",
|
|
24
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.102",
|
|
25
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.116",
|
|
26
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.31"
|
|
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
|
|
@@ -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:
|
|
@@ -4,7 +4,7 @@ open PulumiAws.AppSync
|
|
|
4
4
|
// `NotFoundException: No field named X` (the schema-propagation race) with
|
|
5
5
|
// exponential backoff. Replaces AppSync_Resolver_Native (aws-native provider)
|
|
6
6
|
// which exhibited the same race in production.
|
|
7
|
-
// See docs/plans/appsync-resolver-aws-native-retry.md.
|
|
7
|
+
// See docs/plans/done/appsync-resolver-aws-native-retry.md.
|
|
8
8
|
module Resolver = AppSync_Resolver_Retrying
|
|
9
9
|
open Reventless.ReadModel
|
|
10
10
|
|
|
@@ -55,7 +55,7 @@ export function response(ctx) {
|
|
|
55
55
|
that carry no `name`. Returning `Some(attr)` makes the Connection Scan emit an
|
|
56
56
|
`attribute_exists(#attr)` filter so those rows never reach the non-null GraphQL schema.
|
|
57
57
|
`name` is the already-capitalized read-model name (spec name, e.g. "Plugins").
|
|
58
|
-
See docs/plans/platform-plugins-admin-connection-null-rows.md. */
|
|
58
|
+
See docs/plans/done/platform-plugins-admin-connection-null-rows.md. */
|
|
59
59
|
let internalRowRequiredAttr = (name: string): option<string> =>
|
|
60
60
|
name == "Plugins" ? Some("name") : None
|
|
61
61
|
|
|
@@ -69,7 +69,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
69
69
|
~idResolverConfigs: array<idResolverConfig>,
|
|
70
70
|
~idsResolverConfigs: array<idsResolverConfig>,
|
|
71
71
|
// AWS path enforces authorization via `@aws_cognito_user_pools(cognito_groups: …)` on the
|
|
72
|
-
// SDL fields (see Stage E in docs/plans/host-ui-login-core.md). Accepted but
|
|
72
|
+
// SDL fields (see Stage E in docs/plans/done/host-ui-login-core.md). Accepted but
|
|
73
73
|
// ignored here so the in-memory and AWS resolver maker signatures stay aligned.
|
|
74
74
|
~authorization as _: Reventless.Authorization.permission,
|
|
75
75
|
~opts,
|
|
@@ -167,13 +167,30 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
167
167
|
// This prevents a race condition where AppSync resolvers are deployed before
|
|
168
168
|
// the schema push completes and the new fields are ACTIVE in AppSync.
|
|
169
169
|
let resourcesMaker: ReventlessCore.QueryDb.resolversResourcesMaker = allQueryDbs => {
|
|
170
|
+
// The state's `@owner` field, derived from the same schema `capability` is
|
|
171
|
+
// derived from, so the two cannot disagree about this read model's fields.
|
|
172
|
+
//
|
|
173
|
+
// Read HERE, at the top, rather than beside the list resolver that first
|
|
174
|
+
// needed it. The by-id resolvers are built a few lines below and the lookup
|
|
175
|
+
// used to sit well after them, so they were generated without it: a list
|
|
176
|
+
// narrowed to the caller beside a by-id read that hands over any row it is
|
|
177
|
+
// asked for. That is not a partial delivery — the row is reachable by
|
|
178
|
+
// anyone who can name one, and the list gives no sign of it, because the
|
|
179
|
+
// list is correct.
|
|
180
|
+
let stateSchemaOpt = ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.get(name)
|
|
181
|
+
let ownerField = switch stateSchemaOpt {
|
|
182
|
+
| Some(s) => Reventless.Owner.fieldNames(s)->Array.get(0)
|
|
183
|
+
| None => None
|
|
184
|
+
}
|
|
185
|
+
let elevatedGroups = Reventless.OwnerScope.elevatedGroups()
|
|
186
|
+
|
|
170
187
|
let resolverByIdSingle = if includeIdParam {
|
|
171
188
|
makeQueryResolver(
|
|
172
189
|
~resolverName=fieldNameForSingle->String.capitalize,
|
|
173
190
|
~field=fieldNameForSingle->Pulumi.Input.make,
|
|
174
191
|
~code=switch subIdField {
|
|
175
|
-
| Some(sortField) => Resolver.Functions.queryByIdSort(sortField)
|
|
176
|
-
| None => Resolver.Functions.getItemById
|
|
192
|
+
| Some(sortField) => Resolver.Functions.queryByIdSort(sortField, ~ownerField?, ~elevatedGroups)
|
|
193
|
+
| None => Resolver.Functions.getItemById(~ownerField?, ~elevatedGroups)
|
|
177
194
|
},
|
|
178
195
|
)
|
|
179
196
|
} else {
|
|
@@ -188,7 +205,11 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
188
205
|
makeQueryResolver(
|
|
189
206
|
~resolverName=fieldNameForSingle->String.capitalize ++ "Items",
|
|
190
207
|
~field=(fieldNameForSingle ++ "Items")->Pulumi.Input.make,
|
|
191
|
-
~code=Resolver.Functions.queryItemsWithSortConditions(
|
|
208
|
+
~code=Resolver.Functions.queryItemsWithSortConditions(
|
|
209
|
+
sortField,
|
|
210
|
+
~ownerField?,
|
|
211
|
+
~elevatedGroups,
|
|
212
|
+
),
|
|
192
213
|
)
|
|
193
214
|
)
|
|
194
215
|
} else {
|
|
@@ -203,7 +224,6 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
203
224
|
// registered or has no indexable fields — the resolver template emits the same
|
|
204
225
|
// SDL the in-memory adapter does, so the AWS Filter / OrderBy stays in lockstep
|
|
205
226
|
// with the SDL emitted by GraphQL_FragmentGenerator at runtime.
|
|
206
|
-
let stateSchemaOpt = ReventlessCore.Plugin_Helpers.stateSchemaRegistry->Dict.get(name)
|
|
207
227
|
let capability = switch stateSchemaOpt {
|
|
208
228
|
| Some(s) => ReventlessCore.GraphQL_FragmentGenerator.deriveServerCapability(~entityName=name, s)
|
|
209
229
|
| None => ReventlessCore.GraphQL_FragmentGenerator.emptyCapability
|
|
@@ -236,14 +256,8 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
236
256
|
// resolves `name: String!` to null → non-null violation that nulls the entire
|
|
237
257
|
// Platform_PluginConnection. Exclude them with an `attribute_exists(#name)` filter
|
|
238
258
|
// (prefix-agnostic: real plugin rows always carry `name`, internal rows never do).
|
|
239
|
-
// See docs/plans/platform-plugins-admin-connection-null-rows.md.
|
|
259
|
+
// See docs/plans/done/platform-plugins-admin-connection-null-rows.md.
|
|
240
260
|
let requireAttribute = internalRowRequiredAttr(name)
|
|
241
|
-
// Derived from the same schema `capability` came from, so the two cannot
|
|
242
|
-
// disagree about this read model's fields.
|
|
243
|
-
let ownerField = switch stateSchemaOpt {
|
|
244
|
-
| Some(s) => Reventless.Owner.fieldNames(s)->Array.get(0)
|
|
245
|
-
| None => None
|
|
246
|
-
}
|
|
247
261
|
// A DynamoDB FilterExpression is applied AFTER the page is read, so a scoped
|
|
248
262
|
// list over a table with no index on the owner field returns short pages —
|
|
249
263
|
// correct, but pathological once a caller owns a small fraction of the rows.
|
|
@@ -310,7 +324,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
310
324
|
~sortFields=sortFieldNames,
|
|
311
325
|
~requireAttribute?,
|
|
312
326
|
~ownerField?,
|
|
313
|
-
~elevatedGroups
|
|
327
|
+
~elevatedGroups,
|
|
314
328
|
~retiredField?,
|
|
315
329
|
~retiredValues?,
|
|
316
330
|
)
|
|
@@ -91,10 +91,12 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
91
91
|
};
|
|
92
92
|
let fieldNameForAll = registryEntry !== undefined ? registryEntry.listFieldName : name$1 + "s";
|
|
93
93
|
let resourcesMaker = allQueryDbs => {
|
|
94
|
-
let resolverByIdSingle = includeIdParam ? makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, subIdField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIdSort(subIdField) : AppSync_Resolver_Functions$PulumiAws.getItemById) : makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
95
|
-
let resolverByIdMultiple = includeIdParam ? Stdlib_Option.map(subIdField, sortField => makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", AppSync_Resolver_Functions$PulumiAws.queryItemsWithSortConditions(sortField))) : undefined;
|
|
96
|
-
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
97
94
|
let stateSchemaOpt = Plugin_Helpers$ReventlessCore.stateSchemaRegistry[name$1];
|
|
95
|
+
let ownerField = stateSchemaOpt !== undefined ? Owner$Reventless.fieldNames(stateSchemaOpt)[0] : undefined;
|
|
96
|
+
let elevatedGroups = OwnerScope$Reventless.elevatedGroups();
|
|
97
|
+
let resolverByIdSingle = includeIdParam ? makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, subIdField !== undefined ? AppSync_Resolver_Functions$PulumiAws.queryByIdSort(subIdField, ownerField, elevatedGroups) : AppSync_Resolver_Functions$PulumiAws.getItemById(ownerField, elevatedGroups)) : makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle), fieldNameForSingle, AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
98
|
+
let resolverByIdMultiple = includeIdParam ? Stdlib_Option.map(subIdField, sortField => makeQueryResolver(Stdlib_String.capitalize(fieldNameForSingle) + "Items", fieldNameForSingle + "Items", AppSync_Resolver_Functions$PulumiAws.queryItemsWithSortConditions(sortField, ownerField, elevatedGroups))) : undefined;
|
|
99
|
+
let labelField = registryEntry !== undefined ? Stdlib_Option.getOr(registryEntry.labelField, "id") : "id";
|
|
98
100
|
let capability = stateSchemaOpt !== undefined ? GraphQL_FragmentGenerator$ReventlessCore.deriveServerCapability(name$1, stateSchemaOpt) : GraphQL_FragmentGenerator$ReventlessCore.emptyCapability;
|
|
99
101
|
let filterFieldNames = capability.filterFields.map(f => f.name);
|
|
100
102
|
let rangeFieldNames = Stdlib_Array.filterMap(capability.filterFields, f => {
|
|
@@ -110,7 +112,6 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
110
112
|
GraphQL_FragmentGenerator$ReventlessCore.validateScanSortAlignment(stateSchemaOpt, name$1, knownSortFields).forEach(msg => log.warn("QueryDbResolvers_AppSync", undefined, msg));
|
|
111
113
|
}
|
|
112
114
|
let requireAttribute = internalRowRequiredAttr(name$1);
|
|
113
|
-
let ownerField = stateSchemaOpt !== undefined ? Owner$Reventless.fieldNames(stateSchemaOpt)[0] : undefined;
|
|
114
115
|
OwnerScopeDiagnostics$ReventlessCore.warnIfNoElevatedGroups("QueryDbResolvers_AppSync", name$1, ownerField);
|
|
115
116
|
let isIndexed = f => {
|
|
116
117
|
if (indexes.some(ic => Stdlib_Option.getOr(ic.idField, ic.index) === f)) {
|
|
@@ -128,7 +129,7 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
128
129
|
if (retiredField !== undefined && !isIndexed(retiredField)) {
|
|
129
130
|
log.warn("QueryDbResolvers_AppSync", undefined, name$1 + `: @retired field "` + retiredField + `" is not the key of any index on this table. ` + "Reads that exclude retired rows will Scan and filter, so pages shrink as the archive's share of the rows grows. Add an @index on that field before this read model grows.");
|
|
130
131
|
}
|
|
131
|
-
let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute, ownerField,
|
|
132
|
+
let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute, ownerField, elevatedGroups, retiredField, retiredValues) : AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
132
133
|
let resolversByIndex = indexes.map(indexConfig => {
|
|
133
134
|
let index = indexConfig.index;
|
|
134
135
|
let stripLeadingBy = s => {
|
|
@@ -147,7 +147,7 @@ let make: ReventlessCore.QueryDb_Adapter.queryEngineMaker = allQueryDbs => {
|
|
|
147
147
|
// B3.1: Postgres-backed read models have NO DynamoDB resource — exclude them
|
|
148
148
|
// instead of failing findResource at deploy time. They are not reachable via
|
|
149
149
|
// this engine (getRuntimeResource raises its explicit not-found error at
|
|
150
|
-
// query time); see docs/plans/aws-postgres-querydb-adapter.md § B3.1b.
|
|
150
|
+
// query time); see docs/plans/done/aws-postgres-querydb-adapter.md § B3.1b.
|
|
151
151
|
let dynamoDbQueryDbs =
|
|
152
152
|
allQueryDbs
|
|
153
153
|
->Dict.toArray
|
|
@@ -22,7 +22,7 @@ import { commandOutcomeToJson, runInlineAndCollect } from "@reventlessdev/revent
|
|
|
22
22
|
import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
|
|
23
23
|
// Typed cold-start core — EventLog storage-ops selection/wiring, compiler-checked
|
|
24
24
|
// against the framework signatures (see the module header and
|
|
25
|
-
// docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
|
|
25
|
+
// docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md).
|
|
26
26
|
import { makeStorageOps } from "./AggregateEntryPoint_Ops.res.mjs";
|
|
27
27
|
import { DynamoDBClient, ScanCommand } from "@aws-sdk/client-dynamodb";
|
|
28
28
|
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Typed cold-start core for the Aggregate Lambda entry point.
|
|
2
2
|
//
|
|
3
|
-
// The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
|
|
3
|
+
// The "typed core, thin shell" split (docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md).
|
|
4
4
|
// The `.mjs` shell owns the untyped seams — reading `HANDLER_CONFIG`, the dynamic
|
|
5
5
|
// `import()` of user Spec/Behavior modules, and the functor wiring that consumes
|
|
6
6
|
// those runtime-loaded modules (`EventLog_Operations.Make`, `Aggregate_Callback.Make`,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// by source URN.
|
|
5
5
|
//
|
|
6
6
|
// Thin untyped shell ("typed core, thin shell" —
|
|
7
|
-
// docs/plans/minimize-lambda-entrypoint-mjs-shell.md): this file owns ONLY the
|
|
7
|
+
// docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md): this file owns ONLY the
|
|
8
8
|
// inherently-untyped seam — the dynamic `import()` of the spec + body modules
|
|
9
9
|
// named in HANDLER_CONFIG and the curried functor applications consuming them
|
|
10
10
|
// (AutomationSlice_Callback.Make(Spec)(Automation) /
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Typed cold-start core for the AutomationSlice / OutboundTranslationSlice
|
|
2
2
|
// Lambda entry point.
|
|
3
3
|
//
|
|
4
|
-
// The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
|
|
4
|
+
// The "typed core, thin shell" split (docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md):
|
|
5
5
|
// AutomationSliceEntryPoint.mjs keeps only the inherently-untyped seam — the
|
|
6
6
|
// dynamic `import()` of the spec + body modules named in HANDLER_CONFIG and
|
|
7
7
|
// the curried callback-functor applications consuming them
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Routes DynamoDB Stream events from references and counts tables.
|
|
5
5
|
//
|
|
6
6
|
// Thin untyped shell ("typed core, thin shell" —
|
|
7
|
-
// docs/plans/minimize-lambda-entrypoint-mjs-shell.md): this file owns ONLY the
|
|
7
|
+
// docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md): this file owns ONLY the
|
|
8
8
|
// seams that are inherently untyped — the dynamic `import()` of the
|
|
9
9
|
// target-spec/mappings modules named in HANDLER_CONFIG, the `patchSpecId`
|
|
10
10
|
// fix-up, and the MakeCounterHandler functor application consuming them.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Typed cold-start core for the Counter Lambda entry point.
|
|
2
2
|
//
|
|
3
|
-
// The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md):
|
|
3
|
+
// The "typed core, thin shell" split (docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md):
|
|
4
4
|
// CounterEntryPoint.mjs keeps only the untyped seam — the dynamic `import()`
|
|
5
5
|
// of the target-spec/mappings modules named in HANDLER_CONFIG, the
|
|
6
6
|
// `patchSpecId` fix-up, and the EventMapper_Callback.MakeCounterHandler
|
|
@@ -27,7 +27,7 @@ import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessde
|
|
|
27
27
|
// Typed cold-start core — scope/partitionTag derivation, storage-ops wiring, and
|
|
28
28
|
// per-slice handler building (functor + decode + handleCommands), all
|
|
29
29
|
// compiler-checked against the framework signatures (see the module header and
|
|
30
|
-
// docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
|
|
30
|
+
// docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md).
|
|
31
31
|
import { deriveScope, commandTypeNames, makeStorageOps, buildSliceHandler, buildInboundReceiver } from "./DcbCommandTopicEntryPoint_Ops.res.mjs";
|
|
32
32
|
import { makeDynamoQueryDbOps } from "./QueryDbEntryPoint_Ops.res.mjs";
|
|
33
33
|
|
|
@@ -95,6 +95,7 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
95
95
|
);
|
|
96
96
|
|
|
97
97
|
const handlersByType = {};
|
|
98
|
+
const commandSchemasByType = {};
|
|
98
99
|
const sharedDcbEventLogOps = dcbEventLogOperationsMake({
|
|
99
100
|
name: config.pluginName,
|
|
100
101
|
// Mirrors DcbEventLog_Builder.res's `name ++ "DcbEventLog"` — the
|
|
@@ -119,6 +120,19 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
119
120
|
);
|
|
120
121
|
for (const typeName of commandTypeNames(patchedSpec)) {
|
|
121
122
|
handlersByType[typeName] = jsonHandler;
|
|
123
|
+
// The schema this command's owner fields are read from. Kept beside the
|
|
124
|
+
// handler because the two answer the same question — "which slice is this
|
|
125
|
+
// command?" — and a command routed to a handler but not to a schema is
|
|
126
|
+
// exactly the state that stamps nothing while looking like it worked.
|
|
127
|
+
if (commandSchemasByType[typeName] !== undefined) {
|
|
128
|
+
log.error(
|
|
129
|
+
"two DCB slices declare the command \"" + typeName + "\"; owner stamping " +
|
|
130
|
+
"resolves a command by that name alone and cannot tell them apart",
|
|
131
|
+
{ comp: "DcbCommandTopicRuntime" },
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
commandSchemasByType[typeName] = patchedSpec.commandSchema;
|
|
135
|
+
}
|
|
122
136
|
}
|
|
123
137
|
});
|
|
124
138
|
|
|
@@ -191,9 +205,7 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
191
205
|
// Sync (default): inline-dispatch the command via the same composite handler
|
|
192
206
|
// that Route 2 uses, so the AppSync resolver gets a typed Accepted/Rejected
|
|
193
207
|
// outcome. Async: undefined → makeCommandGenerator falls back to publishJsons
|
|
194
|
-
// and returns Pending.
|
|
195
|
-
// already validated input against the SDL — per-slice schemas reapply inside
|
|
196
|
-
// the typed core's buildSliceHandler decode.
|
|
208
|
+
// and returns Pending.
|
|
197
209
|
const publishJsonsAndWait = DISPATCH_MODE === "async"
|
|
198
210
|
? undefined
|
|
199
211
|
: (jsons) => runInlineAndCollect(jsons, compositeJsonCommandsHandler);
|
|
@@ -202,14 +214,31 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
202
214
|
// the framework signature. stripIdFromParams=false: DCB slices may declare a
|
|
203
215
|
// literal `id` field as part of the command schema (composite partition keys
|
|
204
216
|
// etc.) — don't strip it.
|
|
205
|
-
|
|
217
|
+
//
|
|
218
|
+
// One generator per command, differing only in the schema. This used to be a
|
|
219
|
+
// single generator built on the permissive `jsonSchema`, on the still-true
|
|
220
|
+
// grounds that AppSync has already validated input against the SDL and that
|
|
221
|
+
// per-slice schemas reapply inside `buildSliceHandler`'s decode. What that
|
|
222
|
+
// reasoning did not cover is that the schema is also where `makeGenerateCommand`
|
|
223
|
+
// reads a command's `@owner` fields from: given `S.json` it finds none, for every
|
|
224
|
+
// command, and the write keeps whatever owner the CLIENT sent. Validation was
|
|
225
|
+
// never the only job.
|
|
226
|
+
const makeGeneratorFor = (commandSchema) => makeCommandGenerator(
|
|
206
227
|
publishJsons,
|
|
207
228
|
publishJsonsAndWait,
|
|
208
229
|
config.pluginName,
|
|
209
|
-
|
|
230
|
+
commandSchema,
|
|
210
231
|
"StateChangeSlice",
|
|
211
232
|
false,
|
|
212
233
|
);
|
|
234
|
+
const generatorsByType = {};
|
|
235
|
+
for (const typeName of Object.keys(commandSchemasByType)) {
|
|
236
|
+
generatorsByType[typeName] = makeGeneratorFor(commandSchemasByType[typeName]);
|
|
237
|
+
}
|
|
238
|
+
// A command no loaded slice claims keeps the permissive generator — what every
|
|
239
|
+
// command got before this change — so anything reaching this handler by another
|
|
240
|
+
// route behaves as it did. It also cannot stamp, hence the warning.
|
|
241
|
+
const generateFallback = makeGeneratorFor(jsonSchema);
|
|
213
242
|
|
|
214
243
|
const cmdGenHandler = (event) => {
|
|
215
244
|
// CommandGenerator.meta declares ip as array<string> (X-Forwarded-For chain).
|
|
@@ -225,6 +254,15 @@ export async function buildHandlersForConfig(config, opts = {}) {
|
|
|
225
254
|
groups: [],
|
|
226
255
|
provider: { TAG: "Custom", _0: "aws" },
|
|
227
256
|
};
|
|
257
|
+
let generateCommand = generatorsByType[event.command];
|
|
258
|
+
if (generateCommand === undefined) {
|
|
259
|
+
log.warn(
|
|
260
|
+
"no DCB slice declares the command \"" + event.command + "\"; dispatching " +
|
|
261
|
+
"without an owner stamp, so any @owner field keeps the value the caller sent",
|
|
262
|
+
{ comp: "DcbCommandTopicRuntime" },
|
|
263
|
+
);
|
|
264
|
+
generateCommand = generateFallback;
|
|
265
|
+
}
|
|
228
266
|
return generateCommand({ ...event, meta, identity });
|
|
229
267
|
};
|
|
230
268
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Typed cold-start core for the DCB CommandTopic Lambda entry point.
|
|
2
2
|
//
|
|
3
|
-
// The "typed core, thin shell" split (docs/plans/minimize-lambda-entrypoint-mjs-shell.md).
|
|
3
|
+
// The "typed core, thin shell" split (docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md).
|
|
4
4
|
// The `.mjs` shell owns the one boundary that is inherently untyped: reading
|
|
5
5
|
// `HANDLER_CONFIG` and dynamically `import()`-ing user Spec/Behavior modules at
|
|
6
6
|
// cold start, whose types are unknowable here. Everything below is compiler-
|