@reventlessdev/reventless-aws 3.0.0-alpha.182 → 3.0.0-alpha.184
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/QueryDb/QueryDbResolvers_AppSync.res +21 -0
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +9 -1
- package/tests/QueryDbResolvers_AppSyncTest.res +40 -0
- package/tests/QueryDbResolvers_AppSyncTest.res.mjs +35 -0
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.184 (2026-07-08)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **reventless-aws:** exclude internal rows from Platform_Plugins connection scan ([df14af2](https://github.com/ReventlessDev/reventless-core/commit/df14af2cbd70d49062f2afc03418cdbf18d151d6))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.183 (2026-07-08)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **reventless-aws:** stop admin-base schema clobber of split-mode DomainApi ([afce85f](https://github.com/ReventlessDev/reventless-core/commit/afce85fa57474735cea3dcb1c2a151c2d8804f0e))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.182 (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.184",
|
|
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
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.46",
|
|
14
12
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.25",
|
|
13
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.6",
|
|
14
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.47",
|
|
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
18
|
"@reventlessdev/reventless-infra": "3.0.0-alpha.90",
|
|
19
19
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.24",
|
|
20
|
-
"@reventlessdev/reventless-
|
|
21
|
-
"@reventlessdev/reventless-
|
|
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);
|
|
@@ -57,6 +57,17 @@ export function response(ctx) {
|
|
|
57
57
|
}
|
|
58
58
|
`->Pulumi.Input.make
|
|
59
59
|
|
|
60
|
+
/** The discriminator attribute whose absence marks an internal bookkeeping row for a
|
|
61
|
+
read model whose physical DynamoDB table co-hosts rows written outside the projection.
|
|
62
|
+
Only the Plugins admin RM has this: its table also holds `deploy-schema:*` /
|
|
63
|
+
`plugin-info:*` / `deploy-schema-hash:*` rows (Platform.res preResolversSchemaHook)
|
|
64
|
+
that carry no `name`. Returning `Some(attr)` makes the Connection Scan emit an
|
|
65
|
+
`attribute_exists(#attr)` filter so those rows never reach the non-null GraphQL schema.
|
|
66
|
+
`name` is the already-capitalized read-model name (spec name, e.g. "Plugins").
|
|
67
|
+
See docs/plans/platform-plugins-admin-connection-null-rows.md. */
|
|
68
|
+
let internalRowRequiredAttr = (name: string): option<string> =>
|
|
69
|
+
name == "Plugins" ? Some("name") : None
|
|
70
|
+
|
|
60
71
|
let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
61
72
|
~name: string,
|
|
62
73
|
~api: api,
|
|
@@ -217,6 +228,15 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
217
228
|
)->Array.forEach(msg => log.warn(~comp="QueryDbResolvers_AppSync", msg))
|
|
218
229
|
| None => ()
|
|
219
230
|
}
|
|
231
|
+
// The Plugins admin RM's DynamoDB table co-hosts deploy-time infra rows
|
|
232
|
+
// (`deploy-schema:<name>`, `plugin-info:<name>`, `deploy-schema-hash:<apiId>`)
|
|
233
|
+
// written directly by the platform (Platform.res preResolversSchemaHook), not by
|
|
234
|
+
// the projection. Those rows carry no `name` attribute, so an unfiltered Scan
|
|
235
|
+
// resolves `name: String!` to null → non-null violation that nulls the entire
|
|
236
|
+
// Platform_PluginConnection. Exclude them with an `attribute_exists(#name)` filter
|
|
237
|
+
// (prefix-agnostic: real plugin rows always carry `name`, internal rows never do).
|
|
238
|
+
// See docs/plans/platform-plugins-admin-connection-null-rows.md.
|
|
239
|
+
let requireAttribute = internalRowRequiredAttr(name)
|
|
220
240
|
let resolverAll = makeQueryResolver(
|
|
221
241
|
~resolverName=fieldNameForAll->String.capitalize,
|
|
222
242
|
~field=fieldNameForAll->Pulumi.Input.make,
|
|
@@ -226,6 +246,7 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
|
|
|
226
246
|
~filterFields=filterFieldNames,
|
|
227
247
|
~rangeFields=rangeFieldNames,
|
|
228
248
|
~sortFields=sortFieldNames,
|
|
249
|
+
~requireAttribute?,
|
|
229
250
|
)
|
|
230
251
|
} else {
|
|
231
252
|
Resolver.Functions.listAllItems
|
|
@@ -58,6 +58,12 @@ export function response(ctx) {
|
|
|
58
58
|
`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function internalRowRequiredAttr(name) {
|
|
62
|
+
if (name === "Plugins") {
|
|
63
|
+
return "name";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolverConfigs, idsResolverConfigs, param, opts) {
|
|
62
68
|
let name$1 = Stdlib_String.capitalize(name);
|
|
63
69
|
let registryEntry = Plugin_Helpers$ReventlessCore.queryFieldNamesRegistry[name$1];
|
|
@@ -100,7 +106,8 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
|
|
|
100
106
|
).concat(Stdlib_Array.filterMap(indexes, param => param.subIdField));
|
|
101
107
|
GraphQL_FragmentGenerator$ReventlessCore.validateScanSortAlignment(stateSchemaOpt, name$1, knownSortFields).forEach(msg => log.warn("QueryDbResolvers_AppSync", undefined, msg));
|
|
102
108
|
}
|
|
103
|
-
let
|
|
109
|
+
let requireAttribute = internalRowRequiredAttr(name$1);
|
|
110
|
+
let resolverAll = makeQueryResolver(Stdlib_String.capitalize(fieldNameForAll), fieldNameForAll, connectionSpec ? AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection(labelField, filterFieldNames, rangeFieldNames, sortFieldNames, requireAttribute) : AppSync_Resolver_Functions$PulumiAws.listAllItems);
|
|
104
111
|
let resolversByIndex = indexes.map(indexConfig => {
|
|
105
112
|
let index = indexConfig.index;
|
|
106
113
|
let stripLeadingBy = s => {
|
|
@@ -238,6 +245,7 @@ export {
|
|
|
238
245
|
log,
|
|
239
246
|
queryInterceptorConfig,
|
|
240
247
|
interceptorCode,
|
|
248
|
+
internalRowRequiredAttr,
|
|
241
249
|
make,
|
|
242
250
|
}
|
|
243
251
|
/* log Not a pure module */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Regression guard for docs/plans/platform-plugins-admin-connection-null-rows.md:
|
|
4
|
+
// the Plugins admin RM shares its DynamoDB table with internal bookkeeping rows
|
|
5
|
+
// (`deploy-schema:*`, `plugin-info:*`, `deploy-schema-hash:*`) that carry no `name`.
|
|
6
|
+
// The auto-generated Connection Scan must exclude them, or `name: String!` resolves
|
|
7
|
+
// to null and nulls the entire Platform_PluginConnection ("No data" on the page).
|
|
8
|
+
|
|
9
|
+
// `Pulumi.Input.make` is `%identity`, so a generated resolver's `Pulumi.Input.t<string>`
|
|
10
|
+
// IS the underlying JS string — recover it with Obj.magic for assertion.
|
|
11
|
+
let codeOf = (input: Pulumi.Input.t<string>): string => Obj.magic(input)
|
|
12
|
+
|
|
13
|
+
describe("QueryDbResolvers_AppSync.internalRowRequiredAttr", () => {
|
|
14
|
+
testSync("Plugins RM requires the `name` discriminator", () => {
|
|
15
|
+
expect(QueryDbResolvers_AppSync.internalRowRequiredAttr("Plugins"))->toEqual(Some("name"))
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
testSync("ordinary read models require nothing (unchanged behaviour)", () => {
|
|
19
|
+
expect(QueryDbResolvers_AppSync.internalRowRequiredAttr("Products"))->toEqual(None)
|
|
20
|
+
expect(QueryDbResolvers_AppSync.internalRowRequiredAttr("Orders"))->toEqual(None)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe("AppSync_Resolver_Retrying.Functions.listAllItemsConnection", () => {
|
|
25
|
+
testSync("with ~requireAttribute emits an attribute_exists filter that excludes internal rows", () => {
|
|
26
|
+
let code =
|
|
27
|
+
AppSync_Resolver_Retrying.Functions.listAllItemsConnection(
|
|
28
|
+
~labelField="name",
|
|
29
|
+
~requireAttribute="name",
|
|
30
|
+
)->codeOf
|
|
31
|
+
expect(code->String.includes("attribute_exists(#name)"))->toBe(true)
|
|
32
|
+
expect(code->String.includes("names['#name'] = 'name'"))->toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
testSync("without ~requireAttribute emits no attribute_exists filter (default read models unchanged)", () => {
|
|
36
|
+
let code =
|
|
37
|
+
AppSync_Resolver_Retrying.Functions.listAllItemsConnection(~labelField="name")->codeOf
|
|
38
|
+
expect(code->String.includes("attribute_exists"))->toBe(false)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
|
|
4
|
+
import * as QueryDbResolvers_AppSync$ReventlessAws from "../src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs";
|
|
5
|
+
|
|
6
|
+
function codeOf(input) {
|
|
7
|
+
return input;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
globalThis.describe("QueryDbResolvers_AppSync.internalRowRequiredAttr", () => {
|
|
11
|
+
globalThis.test("Plugins RM requires the `name` discriminator", () => {
|
|
12
|
+
globalThis.expect(QueryDbResolvers_AppSync$ReventlessAws.internalRowRequiredAttr("Plugins")).toEqual("name");
|
|
13
|
+
});
|
|
14
|
+
globalThis.test("ordinary read models require nothing (unchanged behaviour)", () => {
|
|
15
|
+
globalThis.expect(QueryDbResolvers_AppSync$ReventlessAws.internalRowRequiredAttr("Products")).toEqual(undefined);
|
|
16
|
+
globalThis.expect(QueryDbResolvers_AppSync$ReventlessAws.internalRowRequiredAttr("Orders")).toEqual(undefined);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
globalThis.describe("AppSync_Resolver_Retrying.Functions.listAllItemsConnection", () => {
|
|
21
|
+
globalThis.test("with ~requireAttribute emits an attribute_exists filter that excludes internal rows", () => {
|
|
22
|
+
let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, "name");
|
|
23
|
+
globalThis.expect(code.includes("attribute_exists(#name)")).toBe(true);
|
|
24
|
+
globalThis.expect(code.includes("names['#name'] = 'name'")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
globalThis.test("without ~requireAttribute emits no attribute_exists filter (default read models unchanged)", () => {
|
|
27
|
+
let code = AppSync_Resolver_Functions$PulumiAws.listAllItemsConnection("name", undefined, undefined, undefined, undefined);
|
|
28
|
+
globalThis.expect(code.includes("attribute_exists")).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
codeOf,
|
|
34
|
+
}
|
|
35
|
+
/* Not a pure module */
|