@reventlessdev/reventless-aws 3.0.0-alpha.181 → 3.0.0-alpha.183
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 +14 -0
- package/package.json +6 -6
- package/src/Platform.res +71 -34
- package/src/Platform.res.mjs +58 -70
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +74 -6
- package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +61 -12
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +140 -0
- package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +158 -0
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res +164 -20
- package/tests/integration/DcbEventLogStorage_DynamoDb_IntegrationTest.res.mjs +149 -2
- package/tests/integration/DcbIntegrationHarness.res +19 -8
- package/tests/integration/DcbIntegrationHarness.res.mjs +5 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.183 (2026-07-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **reventless-aws:** stop admin-base schema clobber of split-mode DomainApi ([afce85f](https://github.com/ReventlessDev/reventless-core/commit/afce85fa57474735cea3dcb1c2a151c2d8804f0e))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.182 (2026-07-07)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **reventless-aws:** fence composite-partition DCB slices on one composite key ([e5f2d95](https://github.com/ReventlessDev/reventless-core/commit/e5f2d95652d795e4dea60e28548f96100a997e78))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.181 (2026-07-07)
|
|
7
21
|
|
|
8
22
|
### 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.183",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"sury": "11.0.0-alpha.4",
|
|
10
10
|
"uuid": "^13.0.0",
|
|
11
11
|
"@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.20",
|
|
12
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.6",
|
|
13
12
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.25",
|
|
13
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.6",
|
|
14
14
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.46",
|
|
15
15
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
|
|
16
16
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
|
|
17
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
17
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.145",
|
|
18
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.90",
|
|
18
19
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.24",
|
|
19
|
-
"@reventlessdev/reventless-
|
|
20
|
-
"@reventlessdev/reventless-
|
|
21
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.90"
|
|
20
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.9",
|
|
21
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.68"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"rescript": "^12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -689,37 +689,56 @@ module MakeWithConfig = (
|
|
|
689
689
|
// - split mode: platformApi from splitApiOutputsRef
|
|
690
690
|
// - unified mode / not-yet-populated: domainApi
|
|
691
691
|
preAdminResolversSchemaHook: (~adminBarrier) => {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
692
|
+
// The admin-base SDL stitches AdminApi.baseFragment with an EMPTY plugin
|
|
693
|
+
// list, and startSchemaCreation REPLACES the whole schema. In split mode
|
|
694
|
+
// the admin schema belongs on the PlatformApi ONLY — the DomainApi carries
|
|
695
|
+
// plugin fields (emptyBaseFragment). If we pushed the admin-base-only SDL
|
|
696
|
+
// to the DomainApi it would wipe every plugin field, leaving exactly the
|
|
697
|
+
// admin-base set (the alpha 2026-07-08 clobber). So in split mode we push
|
|
698
|
+
// ONLY when the PlatformApi is known; if the ref is not yet populated we
|
|
699
|
+
// SKIP (never fall back to domainApi). Unified mode legitimately shares one
|
|
700
|
+
// API, so an unpopulated ref there means domainApi.
|
|
701
|
+
let targetApiOpt = switch (Config.splitApi, splitApiOutputsRef.contents) {
|
|
702
|
+
| (_, Some({platformApi})) => Some(platformApi)
|
|
703
|
+
| (false, None) => Some(domainApi)
|
|
704
|
+
| (true, None) => None
|
|
695
705
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
)
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
)
|
|
717
|
-
|
|
718
|
-
|
|
706
|
+
switch targetApiOpt {
|
|
707
|
+
| None =>
|
|
708
|
+
log.error(
|
|
709
|
+
~comp="preAdminResolversSchemaHook",
|
|
710
|
+
"split mode but the PlatformApi is not available at hook time — SKIPPING the admin schema push to avoid clobbering the DomainApi (pushing admin-base here would wipe every plugin field)",
|
|
711
|
+
)
|
|
712
|
+
adminBarrier
|
|
713
|
+
| Some(targetApi) =>
|
|
714
|
+
let adminBaseFragment = AppSync_Adapter.injectAwsAuthAll(
|
|
715
|
+
ReventlessCore.AdminApi.baseFragment(~cloner=Config.cloner),
|
|
716
|
+
~group="Admin",
|
|
717
|
+
)
|
|
718
|
+
let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
|
|
719
|
+
~baseFragment=adminBaseFragment,
|
|
720
|
+
~pluginFragments=[],
|
|
721
|
+
)->AppSync_Adapter.stampSharedIamTypes
|
|
722
|
+
(targetApi, adminBarrier)
|
|
723
|
+
->Pulumi.Output.all2
|
|
724
|
+
->Pulumi.Output.flatMap(((api, _)) =>
|
|
725
|
+
api.id->Pulumi.Output.flatMap(apiId => {
|
|
726
|
+
log.info(~comp="preAdminResolversSchemaHook", `Pushing admin schema to ${apiId}`)
|
|
727
|
+
let client = AppSync_Adapter.getClient()
|
|
728
|
+
client
|
|
729
|
+
->AppSync_Adapter.startSchemaCreationRetrying({apiId, definition: sdl})
|
|
730
|
+
->Promise.then(async _ => {
|
|
731
|
+
log.info(
|
|
732
|
+
~comp="preAdminResolversSchemaHook",
|
|
733
|
+
"startSchemaCreation called, waiting for ACTIVE",
|
|
734
|
+
)
|
|
735
|
+
await AppSync_Adapter.waitForSchemaActive(client, apiId)
|
|
736
|
+
log.info(~comp="preAdminResolversSchemaHook", "schema is ACTIVE")
|
|
737
|
+
})
|
|
738
|
+
->Pulumi.Output.fromPromise
|
|
719
739
|
})
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
)
|
|
740
|
+
)
|
|
741
|
+
}
|
|
723
742
|
},
|
|
724
743
|
|
|
725
744
|
// Accumulate fragments across independent plugin deployments: each plugin
|
|
@@ -957,26 +976,44 @@ module MakeWithConfig = (
|
|
|
957
976
|
~sdl=s,
|
|
958
977
|
~typeName="Subscription",
|
|
959
978
|
)
|
|
979
|
+
// Identity-aware drift check (not a bare count): the live schema is
|
|
980
|
+
// "intact" only when it is a SUPERSET of every root field we would
|
|
981
|
+
// push. Comparing name SETS heals equal-cardinality drift and field
|
|
982
|
+
// *swaps* — an admin-base clobber that leaves the DomainApi with the
|
|
983
|
+
// SAME number of root fields but the WRONG ones (admin-base instead
|
|
984
|
+
// of plugin fields) has a matching count yet is missing every
|
|
985
|
+
// expected plugin field, so a count test would wrongly skip.
|
|
986
|
+
let missingFields = ReventlessCore.GraphQL_Stitcher.missingRootFields(
|
|
987
|
+
~expectedSdl=sdl,
|
|
988
|
+
~liveSdl,
|
|
989
|
+
)
|
|
960
990
|
let skipPush = switch storedHash {
|
|
961
991
|
| Some(prev) if prev == currentHash =>
|
|
962
|
-
let expected = countRoots(sdl)
|
|
963
|
-
let live = countRoots(liveSdl)
|
|
964
992
|
if liveSdl == "" {
|
|
965
993
|
log.info(
|
|
966
994
|
~comp="preResolversSchemaHook",
|
|
967
995
|
`hash matches but live schema could not be introspected — forcing repair push (check appsync:GetIntrospectionSchema permission)`,
|
|
968
996
|
)
|
|
969
997
|
false
|
|
970
|
-
} else if
|
|
998
|
+
} else if missingFields->Array.length > 0 {
|
|
971
999
|
log.info(
|
|
972
1000
|
~comp="preResolversSchemaHook",
|
|
973
|
-
`hash matches but live schema
|
|
1001
|
+
`hash matches but live schema is missing ${missingFields
|
|
1002
|
+
->Array.length
|
|
1003
|
+
->Int.toString} expected root field(s) (e.g. ${missingFields
|
|
1004
|
+
->Array.slice(~start=0, ~end=5)
|
|
1005
|
+
->Array.join(", ")}) — forcing repair push`,
|
|
974
1006
|
)
|
|
975
1007
|
false
|
|
976
1008
|
} else {
|
|
977
1009
|
log.info(
|
|
978
1010
|
~comp="preResolversSchemaHook",
|
|
979
|
-
`SDL unchanged (hash ${currentHash->String.slice(
|
|
1011
|
+
`SDL unchanged (hash ${currentHash->String.slice(
|
|
1012
|
+
~start=0,
|
|
1013
|
+
~end=12,
|
|
1014
|
+
)}…) and live schema is a superset of the expected root fields (${countRoots(
|
|
1015
|
+
liveSdl,
|
|
1016
|
+
)->Int.toString} live); skipping push`,
|
|
980
1017
|
)
|
|
981
1018
|
true
|
|
982
1019
|
}
|
package/src/Platform.res.mjs
CHANGED
|
@@ -645,23 +645,12 @@ function MakeWithConfig(Config) {
|
|
|
645
645
|
let client = AppSync_Adapter$ReventlessAws.getClient();
|
|
646
646
|
let liveSdl = await AppSync_Adapter$ReventlessAws.getIntrospectionSdl(client, apiId);
|
|
647
647
|
let countRoots = s => (GraphQL_Stitcher$ReventlessCore.countRootTypeFields(s, "Mutation") + GraphQL_Stitcher$ReventlessCore.countRootTypeFields(s, "Query") | 0) + GraphQL_Stitcher$ReventlessCore.countRootTypeFields(s, "Subscription") | 0;
|
|
648
|
-
let
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
skipPush = false;
|
|
655
|
-
} else if (live < expected) {
|
|
656
|
-
log.info("preResolversSchemaHook", undefined, `hash matches but live schema drifted (` + live.toString() + ` root field(s) live vs ` + expected.toString() + ` expected) — forcing repair push`);
|
|
657
|
-
skipPush = false;
|
|
658
|
-
} else {
|
|
659
|
-
log.info("preResolversSchemaHook", undefined, `SDL unchanged (hash ` + currentHash.slice(0, 12) + `…) and live schema intact (` + live.toString() + ` root fields); skipping push`);
|
|
660
|
-
skipPush = true;
|
|
661
|
-
}
|
|
662
|
-
} else {
|
|
663
|
-
skipPush = false;
|
|
664
|
-
}
|
|
648
|
+
let missingFields = GraphQL_Stitcher$ReventlessCore.missingRootFields(sdl, liveSdl);
|
|
649
|
+
let skipPush = storedHash !== undefined && storedHash === currentHash ? (
|
|
650
|
+
liveSdl === "" ? (log.info("preResolversSchemaHook", undefined, `hash matches but live schema could not be introspected — forcing repair push (check appsync:GetIntrospectionSchema permission)`), false) : (
|
|
651
|
+
missingFields.length !== 0 ? (log.info("preResolversSchemaHook", undefined, `hash matches but live schema is missing ` + missingFields.length.toString() + ` expected root field(s) (e.g. ` + missingFields.slice(0, 5).join(", ") + `) — forcing repair push`), false) : (log.info("preResolversSchemaHook", undefined, `SDL unchanged (hash ` + currentHash.slice(0, 12) + `…) and live schema is a superset of the expected root fields (` + countRoots(liveSdl).toString() + ` live); skipping push`), true)
|
|
652
|
+
)
|
|
653
|
+
) : false;
|
|
665
654
|
if (!skipPush) {
|
|
666
655
|
if (GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(liveSdl, sdl, deploySchemaShrinkThreshold)) {
|
|
667
656
|
return log.error("preResolversSchemaHook", undefined, `ABORTED schema push for ` + apiId + `: stitched SDL (` + countRoots(sdl).toString() + ` root field(s)) would catastrophically shrink the live schema (` + countRoots(liveSdl).toString() + ` root field(s), threshold ` + deploySchemaShrinkThreshold.toString() + `) — refusing to clobber resolvers (likely a stale concurrent-deploy scan)`);
|
|
@@ -692,24 +681,30 @@ function MakeWithConfig(Config) {
|
|
|
692
681
|
};
|
|
693
682
|
let hooks_preAdminResolversSchemaHook = adminBarrier => {
|
|
694
683
|
let match = splitApiOutputsRef.contents;
|
|
695
|
-
let
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
684
|
+
let targetApiOpt = match !== undefined ? match.platformApi : (
|
|
685
|
+
Config.splitApi ? undefined : domainApi
|
|
686
|
+
);
|
|
687
|
+
if (targetApiOpt !== undefined) {
|
|
688
|
+
let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(Config.cloner), "Admin", undefined);
|
|
689
|
+
let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []));
|
|
690
|
+
return Output$Pulumi.flatMap(Pulumi.all([
|
|
691
|
+
targetApiOpt,
|
|
692
|
+
adminBarrier
|
|
693
|
+
]), param => Output$Pulumi.flatMap(param[0].id, apiId => {
|
|
694
|
+
log.info("preAdminResolversSchemaHook", undefined, `Pushing admin schema to ` + apiId);
|
|
695
|
+
let client = AppSync_Adapter$ReventlessAws.getClient();
|
|
696
|
+
return AppSync_Adapter$ReventlessAws.startSchemaCreationRetrying(client, {
|
|
697
|
+
apiId: apiId,
|
|
698
|
+
definition: sdl
|
|
699
|
+
}).then(async () => {
|
|
700
|
+
log.info("preAdminResolversSchemaHook", undefined, "startSchemaCreation called, waiting for ACTIVE");
|
|
701
|
+
await AppSync_Adapter$ReventlessAws.waitForSchemaActive(client, apiId, undefined, undefined);
|
|
702
|
+
return log.info("preAdminResolversSchemaHook", undefined, "schema is ACTIVE");
|
|
703
|
+
});
|
|
704
|
+
}));
|
|
705
|
+
}
|
|
706
|
+
log.error("preAdminResolversSchemaHook", undefined, "split mode but the PlatformApi is not available at hook time — SKIPPING the admin schema push to avoid clobbering the DomainApi (pushing admin-base here would wipe every plugin field)");
|
|
707
|
+
return adminBarrier;
|
|
713
708
|
};
|
|
714
709
|
let hooks_inboundAppSyncResolverHook = param => InboundTranslationResolvers_AppSync$ReventlessAws.make(resolveHookedApi(), param.runtime, param.fieldNames, param.opts);
|
|
715
710
|
let hooks_dcbAppSyncResolverHook = param => CommandGeneratorResolvers_AppSync$ReventlessAws.makeDcb(resolveHookedApi(), param.runtime, param.fieldNames, param.tags, param.opts);
|
|
@@ -1876,23 +1871,12 @@ function Make($star) {
|
|
|
1876
1871
|
let client = AppSync_Adapter$ReventlessAws.getClient();
|
|
1877
1872
|
let liveSdl = await AppSync_Adapter$ReventlessAws.getIntrospectionSdl(client, apiId);
|
|
1878
1873
|
let countRoots = s => (GraphQL_Stitcher$ReventlessCore.countRootTypeFields(s, "Mutation") + GraphQL_Stitcher$ReventlessCore.countRootTypeFields(s, "Query") | 0) + GraphQL_Stitcher$ReventlessCore.countRootTypeFields(s, "Subscription") | 0;
|
|
1879
|
-
let
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
skipPush = false;
|
|
1886
|
-
} else if (live < expected) {
|
|
1887
|
-
log.info("preResolversSchemaHook", undefined, `hash matches but live schema drifted (` + live.toString() + ` root field(s) live vs ` + expected.toString() + ` expected) — forcing repair push`);
|
|
1888
|
-
skipPush = false;
|
|
1889
|
-
} else {
|
|
1890
|
-
log.info("preResolversSchemaHook", undefined, `SDL unchanged (hash ` + currentHash.slice(0, 12) + `…) and live schema intact (` + live.toString() + ` root fields); skipping push`);
|
|
1891
|
-
skipPush = true;
|
|
1892
|
-
}
|
|
1893
|
-
} else {
|
|
1894
|
-
skipPush = false;
|
|
1895
|
-
}
|
|
1874
|
+
let missingFields = GraphQL_Stitcher$ReventlessCore.missingRootFields(sdl, liveSdl);
|
|
1875
|
+
let skipPush = storedHash !== undefined && storedHash === currentHash ? (
|
|
1876
|
+
liveSdl === "" ? (log.info("preResolversSchemaHook", undefined, `hash matches but live schema could not be introspected — forcing repair push (check appsync:GetIntrospectionSchema permission)`), false) : (
|
|
1877
|
+
missingFields.length !== 0 ? (log.info("preResolversSchemaHook", undefined, `hash matches but live schema is missing ` + missingFields.length.toString() + ` expected root field(s) (e.g. ` + missingFields.slice(0, 5).join(", ") + `) — forcing repair push`), false) : (log.info("preResolversSchemaHook", undefined, `SDL unchanged (hash ` + currentHash.slice(0, 12) + `…) and live schema is a superset of the expected root fields (` + countRoots(liveSdl).toString() + ` live); skipping push`), true)
|
|
1878
|
+
)
|
|
1879
|
+
) : false;
|
|
1896
1880
|
if (!skipPush) {
|
|
1897
1881
|
if (GraphQL_Stitcher$ReventlessCore.isCatastrophicSchemaShrink(liveSdl, sdl, deploySchemaShrinkThreshold)) {
|
|
1898
1882
|
return log.error("preResolversSchemaHook", undefined, `ABORTED schema push for ` + apiId + `: stitched SDL (` + countRoots(sdl).toString() + ` root field(s)) would catastrophically shrink the live schema (` + countRoots(liveSdl).toString() + ` root field(s), threshold ` + deploySchemaShrinkThreshold.toString() + `) — refusing to clobber resolvers (likely a stale concurrent-deploy scan)`);
|
|
@@ -1923,24 +1907,28 @@ function Make($star) {
|
|
|
1923
1907
|
};
|
|
1924
1908
|
let hooks_preAdminResolversSchemaHook = adminBarrier => {
|
|
1925
1909
|
let match = splitApiOutputsRef.contents;
|
|
1926
|
-
let
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1910
|
+
let targetApiOpt = match !== undefined ? match.platformApi : undefined;
|
|
1911
|
+
if (targetApiOpt !== undefined) {
|
|
1912
|
+
let adminBaseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
|
|
1913
|
+
let sdl = AppSync_Adapter$ReventlessAws.stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(adminBaseFragment, []));
|
|
1914
|
+
return Output$Pulumi.flatMap(Pulumi.all([
|
|
1915
|
+
targetApiOpt,
|
|
1916
|
+
adminBarrier
|
|
1917
|
+
]), param => Output$Pulumi.flatMap(param[0].id, apiId => {
|
|
1918
|
+
log.info("preAdminResolversSchemaHook", undefined, `Pushing admin schema to ` + apiId);
|
|
1919
|
+
let client = AppSync_Adapter$ReventlessAws.getClient();
|
|
1920
|
+
return AppSync_Adapter$ReventlessAws.startSchemaCreationRetrying(client, {
|
|
1921
|
+
apiId: apiId,
|
|
1922
|
+
definition: sdl
|
|
1923
|
+
}).then(async () => {
|
|
1924
|
+
log.info("preAdminResolversSchemaHook", undefined, "startSchemaCreation called, waiting for ACTIVE");
|
|
1925
|
+
await AppSync_Adapter$ReventlessAws.waitForSchemaActive(client, apiId, undefined, undefined);
|
|
1926
|
+
return log.info("preAdminResolversSchemaHook", undefined, "schema is ACTIVE");
|
|
1927
|
+
});
|
|
1928
|
+
}));
|
|
1929
|
+
}
|
|
1930
|
+
log.error("preAdminResolversSchemaHook", undefined, "split mode but the PlatformApi is not available at hook time — SKIPPING the admin schema push to avoid clobbering the DomainApi (pushing admin-base here would wipe every plugin field)");
|
|
1931
|
+
return adminBarrier;
|
|
1944
1932
|
};
|
|
1945
1933
|
let hooks_inboundAppSyncResolverHook = param => InboundTranslationResolvers_AppSync$ReventlessAws.make(resolveHookedApi(), param.runtime, param.fieldNames, param.opts);
|
|
1946
1934
|
let hooks_dcbAppSyncResolverHook = param => CommandGeneratorResolvers_AppSync$ReventlessAws.makeDcb(resolveHookedApi(), param.runtime, param.fieldNames, param.tags, param.opts);
|
|
@@ -3,10 +3,33 @@ open AwsSdk.DynamoDb.DocumentClient
|
|
|
3
3
|
|
|
4
4
|
// --- Position Generation ---
|
|
5
5
|
|
|
6
|
+
// Hybrid-logical-clock minimal variant. The module-level refs live for the life of
|
|
7
|
+
// a warm Lambda container (reset on cold start), giving strictly-monotonic positions
|
|
8
|
+
// per call WITHIN a container: same-millisecond calls increment `counter`; a forward
|
|
9
|
+
// tick resets it to 0. No cross-container coordination — two same-ms writers on
|
|
10
|
+
// different containers both start at counter 0 and are ordered by the UUID tiebreaker
|
|
11
|
+
// (best-effort, exactly as the old `<ms>-<uuid>` format was). Format:
|
|
12
|
+
// `<ms>-<6-digit counter>-<uuid>`. Correctness never depends on this — fence
|
|
13
|
+
// comparisons anchor to what a slice observed, and `TransactWriteItems` serialises
|
|
14
|
+
// commits; this only makes reader/replay ordering predictable per container. Old
|
|
15
|
+
// `<ms>-<uuid>` positions remain valid: the ms prefix keeps the same 13-digit width,
|
|
16
|
+
// so cross-format comparison still orders by timestamp. See
|
|
17
|
+
// docs/plans/done/dcb-monotonic-position-generation.md.
|
|
18
|
+
let lastMs = ref(0.0)
|
|
19
|
+
let counter = ref(0)
|
|
20
|
+
|
|
6
21
|
let generatePosition = () => {
|
|
7
|
-
let
|
|
22
|
+
let now = Date.make()->Date.getTime
|
|
23
|
+
if now == lastMs.contents {
|
|
24
|
+
counter := counter.contents + 1
|
|
25
|
+
} else {
|
|
26
|
+
lastMs := now
|
|
27
|
+
counter := 0
|
|
28
|
+
}
|
|
29
|
+
let ms = now->Float.toString
|
|
30
|
+
let counterStr = counter.contents->Int.toString->String.padStart(6, "0")
|
|
8
31
|
let uuid = Uuid.v4()
|
|
9
|
-
`${
|
|
32
|
+
`${ms}-${counterStr}-${uuid}`
|
|
10
33
|
}
|
|
11
34
|
|
|
12
35
|
let generatePositionForBatch = (basePosition, index) => {
|
|
@@ -915,20 +938,39 @@ let appendUnconditional = async (
|
|
|
915
938
|
}
|
|
916
939
|
}
|
|
917
940
|
|
|
941
|
+
// A Composite partition fences on the WHOLE composite value, not on each member.
|
|
942
|
+
// The synthetic fence tag is keyed on `getCompositePartitionKeyValue` — the same
|
|
943
|
+
// value `derivePartitionKey` uses for the base-table `id` — so it is exactly as
|
|
944
|
+
// selective as the entity's storage partition (and the `tag_composite` read
|
|
945
|
+
// scope). Fencing per-member (the historical behaviour) gave every low-cardinality
|
|
946
|
+
// member (e.g. `environment`, `platformName`) its own fence, which a deploy-time
|
|
947
|
+
// fan-out sharing those prefixes turns into a hot partition → `TransactionConflict`
|
|
948
|
+
// → `retries exhausted`; it also over-fenced (two DISTINCT composite entities
|
|
949
|
+
// sharing a member value serialized needlessly). Plan:
|
|
950
|
+
// docs/plans/Backlog/dcb-hot-tag-fence-contention.md § "Root-cause correction".
|
|
951
|
+
let compositeFenceTagKey = "__dcb_composite__"
|
|
952
|
+
|
|
953
|
+
let makeCompositeFenceTag = (
|
|
954
|
+
tags: array<Reventless.DcbTag.tag>,
|
|
955
|
+
spec: Reventless.DcbTag.compositePartitionSpec,
|
|
956
|
+
): Reventless.DcbTag.tag => {
|
|
957
|
+
key: compositeFenceTagKey,
|
|
958
|
+
value: Reventless.DcbTag.getCompositePartitionKeyValue(tags, spec),
|
|
959
|
+
}
|
|
960
|
+
|
|
918
961
|
// The partition tag(s) of a written event — the ONLY fences an append may BUMP.
|
|
919
962
|
// A tag's fence must track exactly the partition-scoped events a single-tag read
|
|
920
963
|
// of that tag observes (events are stored under `id="<partitionKey>:<value>"`),
|
|
921
964
|
// so only the partition tag may advance it. Mirrors `derivePartitionKey`.
|
|
922
965
|
//
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
// tag) rather than risk under-fencing composite-partition slices.
|
|
966
|
+
// A Composite partition collapses to a single synthetic composite fence tag (see
|
|
967
|
+
// `makeCompositeFenceTag`) — one fence per composite entity, not one per member.
|
|
926
968
|
let eventPartitionTags = (
|
|
927
969
|
event: ReventlessCore.DcbEventLog_Adapter.rawStoredEvent,
|
|
928
970
|
~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>,
|
|
929
971
|
): array<Reventless.DcbTag.tag> =>
|
|
930
972
|
switch partitionTag {
|
|
931
|
-
| Some(Composite(
|
|
973
|
+
| Some(Composite(spec)) => [makeCompositeFenceTag(event.tags, spec)]
|
|
932
974
|
| Some(Simple(pt)) =>
|
|
933
975
|
switch event.tags->Array.find(t => t.key == pt.key) {
|
|
934
976
|
| Some(t) => [t]
|
|
@@ -994,6 +1036,32 @@ let buildConditionalTransactItems = (
|
|
|
994
1036
|
~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>=?,
|
|
995
1037
|
~crossPartitionTagKeys: array<string>=[],
|
|
996
1038
|
): array<TransactWriteCommand.transactWriteItem> => {
|
|
1039
|
+
// For a Composite partition, fold the multi-tag composite read clause into a
|
|
1040
|
+
// single synthetic composite fence tag (`makeCompositeFenceTag`) so the rest of
|
|
1041
|
+
// this function fences it exactly like a Simple partition — one fence per
|
|
1042
|
+
// composite entity — instead of once per member (the hot-fence source). Only the
|
|
1043
|
+
// FENCE view of the query is rewritten here; the read path (`readStream`) keeps
|
|
1044
|
+
// the original member tags and its `tag_composite` GSI lookup. Gated on
|
|
1045
|
+
// `Composite`: Simple-partition composite-read slices (e.g. RecordProductDemand's
|
|
1046
|
+
// `{productId, orderId}` pair, where `productId` is a real independent partition)
|
|
1047
|
+
// are left untouched. `@crossPartition` carriers are unaffected — they are
|
|
1048
|
+
// handled by `crossPartitionEventTags` below, not by this composite clause.
|
|
1049
|
+
let cond = switch partitionTag {
|
|
1050
|
+
| Some(Composite(spec)) => {
|
|
1051
|
+
...cond,
|
|
1052
|
+
query: cond.query->Array.map(qi =>
|
|
1053
|
+
switch qi.tags {
|
|
1054
|
+
| Some(clauseTags) if clauseTags->Array.length > 1 => {
|
|
1055
|
+
...qi,
|
|
1056
|
+
tags: [makeCompositeFenceTag(clauseTags, spec)],
|
|
1057
|
+
}
|
|
1058
|
+
| _ => qi
|
|
1059
|
+
}
|
|
1060
|
+
),
|
|
1061
|
+
}
|
|
1062
|
+
| _ => cond
|
|
1063
|
+
}
|
|
1064
|
+
|
|
997
1065
|
// The partitions this append writes into — the only fences it may BUMP
|
|
998
1066
|
// (partition-scoped tags only; cross-partition tags below override this).
|
|
999
1067
|
let partitionTags = collectEventPartitionTags(events, ~partitionTag)
|
|
@@ -20,10 +20,26 @@ import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.m
|
|
|
20
20
|
import * as DynamoDb_DocumentClient$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DocumentClient.res.mjs";
|
|
21
21
|
import * as Util_DynamoDb_Runtime$ReventlessAws from "../../util/Util_DynamoDb_Runtime.res.mjs";
|
|
22
22
|
|
|
23
|
+
let lastMs = {
|
|
24
|
+
contents: 0.0
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let counter = {
|
|
28
|
+
contents: 0
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
function generatePosition() {
|
|
24
|
-
let
|
|
32
|
+
let now = new Date().getTime();
|
|
33
|
+
if (now === lastMs.contents) {
|
|
34
|
+
counter.contents = counter.contents + 1 | 0;
|
|
35
|
+
} else {
|
|
36
|
+
lastMs.contents = now;
|
|
37
|
+
counter.contents = 0;
|
|
38
|
+
}
|
|
39
|
+
let ms = now.toString();
|
|
40
|
+
let counterStr = counter.contents.toString().padStart(6, "0");
|
|
25
41
|
let uuid = Uuid.v4();
|
|
26
|
-
return
|
|
42
|
+
return ms + `-` + counterStr + `-` + uuid;
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
function generatePositionForBatch(basePosition, index) {
|
|
@@ -695,10 +711,19 @@ async function appendUnconditional(table, events, partitionTag) {
|
|
|
695
711
|
return await runTransactWrite(input, basePosition, "DCB append failed");
|
|
696
712
|
}
|
|
697
713
|
|
|
714
|
+
let compositeFenceTagKey = "__dcb_composite__";
|
|
715
|
+
|
|
716
|
+
function makeCompositeFenceTag(tags, spec) {
|
|
717
|
+
return {
|
|
718
|
+
key: compositeFenceTagKey,
|
|
719
|
+
value: DcbTag$Reventless.getCompositePartitionKeyValue(tags, spec)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
698
723
|
function eventPartitionTags(event, partitionTag) {
|
|
699
724
|
if (partitionTag !== undefined) {
|
|
700
725
|
if (partitionTag.TAG !== "Simple") {
|
|
701
|
-
return event.tags;
|
|
726
|
+
return [makeCompositeFenceTag(event.tags, partitionTag._0)];
|
|
702
727
|
}
|
|
703
728
|
let pt = partitionTag._0;
|
|
704
729
|
let t = event.tags.find(t => t.key === pt.key);
|
|
@@ -753,6 +778,26 @@ function partitionTypesByTag(events, partitionTag) {
|
|
|
753
778
|
|
|
754
779
|
function buildConditionalTransactItems(table, events, cond, basePosition, partitionTag, crossPartitionTagKeysOpt) {
|
|
755
780
|
let crossPartitionTagKeys = crossPartitionTagKeysOpt !== undefined ? crossPartitionTagKeysOpt : [];
|
|
781
|
+
let cond$1;
|
|
782
|
+
if (partitionTag !== undefined && partitionTag.TAG !== "Simple") {
|
|
783
|
+
let spec = partitionTag._0;
|
|
784
|
+
let newrecord = {...cond};
|
|
785
|
+
newrecord.query = cond.query.map(qi => {
|
|
786
|
+
let clauseTags = qi.tags;
|
|
787
|
+
if (clauseTags === undefined) {
|
|
788
|
+
return qi;
|
|
789
|
+
}
|
|
790
|
+
if (clauseTags.length <= 1) {
|
|
791
|
+
return qi;
|
|
792
|
+
}
|
|
793
|
+
let newrecord = {...qi};
|
|
794
|
+
newrecord.tags = [makeCompositeFenceTag(clauseTags, spec)];
|
|
795
|
+
return newrecord;
|
|
796
|
+
});
|
|
797
|
+
cond$1 = newrecord;
|
|
798
|
+
} else {
|
|
799
|
+
cond$1 = cond;
|
|
800
|
+
}
|
|
756
801
|
let partitionTags = collectEventPartitionTags(events, partitionTag);
|
|
757
802
|
let partitionKeySet = new Set();
|
|
758
803
|
partitionTags.forEach(t => {
|
|
@@ -770,7 +815,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
770
815
|
)[k], []);
|
|
771
816
|
};
|
|
772
817
|
let consumedMap = {};
|
|
773
|
-
cond.query.forEach(qi => {
|
|
818
|
+
cond$1.query.forEach(qi => {
|
|
774
819
|
let match = qi.tags;
|
|
775
820
|
let match$1 = qi.eventTypes;
|
|
776
821
|
if (match !== undefined && match$1 !== undefined) {
|
|
@@ -785,7 +830,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
785
830
|
let consumedTypesFor = t => Stdlib_Option.getOr(consumedMap[t.key + `:` + t.value], []);
|
|
786
831
|
let compositeKeySet = new Set();
|
|
787
832
|
let compositeQueryTags = [];
|
|
788
|
-
cond.query.forEach(qi => {
|
|
833
|
+
cond$1.query.forEach(qi => {
|
|
789
834
|
let tags = qi.tags;
|
|
790
835
|
if (tags !== undefined && tags.length > 1) {
|
|
791
836
|
tags.forEach(tag => {
|
|
@@ -812,7 +857,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
812
857
|
return;
|
|
813
858
|
}
|
|
814
859
|
};
|
|
815
|
-
cond.query.forEach(qi => {
|
|
860
|
+
cond$1.query.forEach(qi => {
|
|
816
861
|
let tags = qi.tags;
|
|
817
862
|
if (tags === undefined) {
|
|
818
863
|
return;
|
|
@@ -821,7 +866,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
821
866
|
if (tags.length <= 1) {
|
|
822
867
|
return;
|
|
823
868
|
}
|
|
824
|
-
let match = cond.after;
|
|
869
|
+
let match = cond$1.after;
|
|
825
870
|
if (match !== undefined) {
|
|
826
871
|
tags.forEach(pushUpdate);
|
|
827
872
|
return;
|
|
@@ -833,7 +878,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
833
878
|
if (crossPartitionTagKeys.includes(tag.key)) {
|
|
834
879
|
return pushUpdate(tag);
|
|
835
880
|
}
|
|
836
|
-
let match$1 = cond.after;
|
|
881
|
+
let match$1 = cond$1.after;
|
|
837
882
|
if (match$1 !== undefined) {
|
|
838
883
|
if (isPartition(tag) || isCompositeQueryTag(tag)) {
|
|
839
884
|
return pushUpdate(tag);
|
|
@@ -853,7 +898,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
853
898
|
return;
|
|
854
899
|
}
|
|
855
900
|
});
|
|
856
|
-
let match = cond.after;
|
|
901
|
+
let match = cond$1.after;
|
|
857
902
|
if (match !== undefined) {
|
|
858
903
|
|
|
859
904
|
} else {
|
|
@@ -864,7 +909,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
864
909
|
bumpSeen.add(t.key + `:` + t.value);
|
|
865
910
|
});
|
|
866
911
|
let bumpTags = [];
|
|
867
|
-
let match$1 = cond.after;
|
|
912
|
+
let match$1 = cond$1.after;
|
|
868
913
|
let candidateBumps = match$1 !== undefined ? partitionTags.concat(crossPartitionEventTags) : partitionTags.concat(compositeQueryTags.concat(crossPartitionEventTags));
|
|
869
914
|
candidateBumps.forEach(tag => {
|
|
870
915
|
let k = tag.key + `:` + tag.value;
|
|
@@ -881,7 +926,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
881
926
|
return;
|
|
882
927
|
} else {
|
|
883
928
|
return {
|
|
884
|
-
Update: buildConditionalFenceUpdate(table.name, tag, consumedTypesFor(tag), producedTypes, basePosition, cond.after)
|
|
929
|
+
Update: buildConditionalFenceUpdate(table.name, tag, consumedTypesFor(tag), producedTypes, basePosition, cond$1.after)
|
|
885
930
|
};
|
|
886
931
|
}
|
|
887
932
|
});
|
|
@@ -891,7 +936,7 @@ function buildConditionalTransactItems(table, events, cond, basePosition, partit
|
|
|
891
936
|
return;
|
|
892
937
|
} else {
|
|
893
938
|
return {
|
|
894
|
-
ConditionCheck: buildFenceConditionCheck(table.name, tag, consumedTypes, cond.after)
|
|
939
|
+
ConditionCheck: buildFenceConditionCheck(table.name, tag, consumedTypes, cond$1.after)
|
|
895
940
|
};
|
|
896
941
|
}
|
|
897
942
|
});
|
|
@@ -1102,6 +1147,8 @@ function readStream(table, $staropt$star) {
|
|
|
1102
1147
|
let transactWriteItemsLimit = 100;
|
|
1103
1148
|
|
|
1104
1149
|
export {
|
|
1150
|
+
lastMs,
|
|
1151
|
+
counter,
|
|
1105
1152
|
generatePosition,
|
|
1106
1153
|
generatePositionForBatch,
|
|
1107
1154
|
tagToAttributeName,
|
|
@@ -1140,6 +1187,8 @@ export {
|
|
|
1140
1187
|
buildEventPuts,
|
|
1141
1188
|
runTransactWrite,
|
|
1142
1189
|
appendUnconditional,
|
|
1190
|
+
compositeFenceTagKey,
|
|
1191
|
+
makeCompositeFenceTag,
|
|
1143
1192
|
eventPartitionTags,
|
|
1144
1193
|
collectEventPartitionTags,
|
|
1145
1194
|
partitionTypesByTag,
|