@reventlessdev/reventless-aws 3.0.0-alpha.241 → 3.0.0-alpha.243
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 +17 -0
- package/package.json +7 -7
- package/src/Platform.res +88 -17
- package/src/Platform.res.mjs +63 -40
- package/src/adapter/Api/AppSync_SourceApiAssociation_Retrying.res +13 -3
- package/src/adapter/Api/AppSync_SourceApiAssociation_Retrying.res.mjs +5 -5
- package/src/util/Util_StoreLayout.res +39 -0
- package/src/util/Util_StoreLayout.res.mjs +21 -0
- package/tests/Util_StoreLayoutTest.res +74 -0
- package/tests/Util_StoreLayoutTest.res.mjs +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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.243 (2026-07-29)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** call the AppSync operations that exist, not the resource's names ([777c8ff](https://github.com/ReventlessDev/reventless-core/commit/777c8ff7e3641b4e7d2228b332f62c14fe690a42))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.242 (2026-07-29)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **platform:** export the bucket's physical name, not the layout's ([43b8ba4](https://github.com/ReventlessDev/reventless-core/commit/43b8ba4f04499c918c7c16921962ad640ea20198))
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **platform:** fail a plugin deploy that needs a store the platform lacks ([8f2b6e6](https://github.com/ReventlessDev/reventless-core/commit/8f2b6e6ca8ea472c70bee40e57c959a64f2a7b4e))
|
|
21
|
+
|
|
22
|
+
|
|
6
23
|
# 3.0.0-alpha.241 (2026-07-29)
|
|
7
24
|
|
|
8
25
|
### Features
|
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.243",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,15 +13,15 @@
|
|
|
13
13
|
"uuid": "^13.0.0",
|
|
14
14
|
"@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.25",
|
|
15
15
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
16
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.61",
|
|
17
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
18
16
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
17
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
18
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.61",
|
|
19
19
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
20
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
21
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
20
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.191",
|
|
21
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.109",
|
|
22
22
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
23
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
24
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
23
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.55",
|
|
24
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.84"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -61,7 +61,12 @@ type objectStoreEndpoint = {
|
|
|
61
61
|
// The path this store's keys are rooted at, and therefore the path it must be
|
|
62
62
|
// served under for a minted `/{key}` ref to resolve.
|
|
63
63
|
keyPrefix: string,
|
|
64
|
-
|
|
64
|
+
// The bucket's **physical** name, which is not the name the layout computes:
|
|
65
|
+
// Pulumi auto-names the resource, so a `SharedBucket` stack asking for
|
|
66
|
+
// `alpha-stores` gets `alpha-stores-507202d`. An Output because that suffix is
|
|
67
|
+
// only known after the bucket resolves — and a plain string here is exactly
|
|
68
|
+
// how this shipped exporting a bucket name that no ARN matches.
|
|
69
|
+
bucketName: Pulumi.Output.t<string>,
|
|
65
70
|
uploadUrl: Pulumi.Output.t<string>,
|
|
66
71
|
// Public base URL the store is served from, when the platform serves it
|
|
67
72
|
// itself. `None` when a host-UI bundle is deployed — there the shell's own
|
|
@@ -1586,7 +1591,11 @@ module MakeWithConfig = (
|
|
|
1586
1591
|
~bucketName=storeHandle.bucketName,
|
|
1587
1592
|
~servedPrefix=storeHandle.keyPrefix,
|
|
1588
1593
|
)
|
|
1589
|
-
|
|
1594
|
+
// `bucketName` is the layout's *logical* name and groups the served view
|
|
1595
|
+
// below; `storeHandle.bucketName` is the physical one Pulumi resolves, and
|
|
1596
|
+
// is what a consumer needs to build an ARN. Carrying both is the point —
|
|
1597
|
+
// they are different strings and only one of them exists in S3.
|
|
1598
|
+
(`${plugin}.${store}`, keyPrefix, bucketName, storeHandle.bucketName, presign.url)
|
|
1590
1599
|
})
|
|
1591
1600
|
|
|
1592
1601
|
// Group the served view by bucket: one origin and one bucket policy per
|
|
@@ -1597,7 +1606,7 @@ module MakeWithConfig = (
|
|
|
1597
1606
|
->Option.map(b => {
|
|
1598
1607
|
ReventlessInfra.Platform.id: bucketName,
|
|
1599
1608
|
prefixes: declaredStoreServices
|
|
1600
|
-
->Array.filterMap(((_, prefix, bn, _)) => bn == bucketName ? Some(prefix) : None),
|
|
1609
|
+
->Array.filterMap(((_, prefix, bn, _, _)) => bn == bucketName ? Some(prefix) : None),
|
|
1601
1610
|
bucketId: b.bucketId,
|
|
1602
1611
|
bucketArn: b.bucketArn,
|
|
1603
1612
|
bucketRegionalDomainName: b.bucketRegionalDomainName,
|
|
@@ -1634,9 +1643,16 @@ module MakeWithConfig = (
|
|
|
1634
1643
|
let declaredStoreEndpoints = declaredStoreServices->Array.map(((
|
|
1635
1644
|
store,
|
|
1636
1645
|
keyPrefix,
|
|
1637
|
-
|
|
1646
|
+
_logicalBucketName,
|
|
1647
|
+
physicalBucketName,
|
|
1638
1648
|
uploadUrl,
|
|
1639
|
-
)) => {
|
|
1649
|
+
)) => {
|
|
1650
|
+
store,
|
|
1651
|
+
keyPrefix,
|
|
1652
|
+
bucketName: physicalBucketName->Pulumi.Output.fromInput,
|
|
1653
|
+
uploadUrl,
|
|
1654
|
+
baseUrl: storeServingBaseUrl,
|
|
1655
|
+
})
|
|
1640
1656
|
objectStoreEndpointsRef := declaredStoreEndpoints
|
|
1641
1657
|
|
|
1642
1658
|
// Exported unconditionally — the provisioning above already is. Their only
|
|
@@ -1657,18 +1673,18 @@ module MakeWithConfig = (
|
|
|
1657
1673
|
"objectStores",
|
|
1658
1674
|
declaredStoreEndpoints
|
|
1659
1675
|
->Array.map(e =>
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1676
|
+
Pulumi.Output.all2((e.bucketName, e.baseUrl->Pulumi.Output.allOpt))->Pulumi.Output.apply(((
|
|
1677
|
+
bucketName,
|
|
1678
|
+
baseUrl,
|
|
1679
|
+
)) => (e, bucketName, baseUrl))
|
|
1664
1680
|
)
|
|
1665
1681
|
->Pulumi.Output.all
|
|
1666
|
-
->Pulumi.Output.apply(
|
|
1667
|
-
|
|
1668
|
-
->Array.map(((e, baseUrl)) => (
|
|
1682
|
+
->Pulumi.Output.apply(resolved =>
|
|
1683
|
+
resolved
|
|
1684
|
+
->Array.map(((e, bucketName, baseUrl)) => (
|
|
1669
1685
|
e.store,
|
|
1670
1686
|
[
|
|
1671
|
-
("bucketName", JSON.Encode.string(
|
|
1687
|
+
("bucketName", JSON.Encode.string(bucketName)),
|
|
1672
1688
|
("keyPrefix", JSON.Encode.string(e.keyPrefix)),
|
|
1673
1689
|
]
|
|
1674
1690
|
// Absent when the host shell serves the store: there the object is
|
|
@@ -1825,7 +1841,7 @@ module MakeWithConfig = (
|
|
|
1825
1841
|
// built before per-store binding reads it and is unaffected.
|
|
1826
1842
|
let storeUploadEndpointsOutput: Pulumi.Output.t<array<(string, string)>> =
|
|
1827
1843
|
declaredStoreServices
|
|
1828
|
-
->Array.map(((qualified, _, _, url)) => url->Pulumi.Output.apply(u => (qualified, u)))
|
|
1844
|
+
->Array.map(((qualified, _, _, _, url)) => url->Pulumi.Output.apply(u => (qualified, u)))
|
|
1829
1845
|
->Pulumi.Output.all
|
|
1830
1846
|
|
|
1831
1847
|
let configJsonContent =
|
|
@@ -2053,11 +2069,66 @@ module MakeWithConfig = (
|
|
|
2053
2069
|
~mergedApiIdentifier=checkedMergedApiArn,
|
|
2054
2070
|
~association,
|
|
2055
2071
|
)
|
|
2072
|
+
|
|
2073
|
+
// ── Capability coverage ────────────────────────────────────────────────
|
|
2074
|
+
//
|
|
2075
|
+
// `requiredStores` is what this plugin's fields *declare*; the platform's
|
|
2076
|
+
// `objectStores` output is what it actually provisioned. A store in the
|
|
2077
|
+
// first and not the second is the split-stack ordering hazard: the
|
|
2078
|
+
// platform deploys before the plugin and cannot see the plugin's schemas,
|
|
2079
|
+
// so the capability list is hand-written and can simply be wrong.
|
|
2080
|
+
//
|
|
2081
|
+
// It is worth failing on because every symptom is silent. The upload
|
|
2082
|
+
// input finds no per-store endpoint, falls back to the legacy single
|
|
2083
|
+
// service, and writes to whatever bucket that serves — a 2xx, a plausible
|
|
2084
|
+
// ref, and the wrong destination. A case slip in the hand-written name
|
|
2085
|
+
// produces exactly that, and so does forgetting the entry entirely.
|
|
2086
|
+
//
|
|
2087
|
+
// Two outcomes rather than one, because "you got it wrong" and "you have
|
|
2088
|
+
// not started" deserve different answers. A platform exporting no stores
|
|
2089
|
+
// at all has not adopted capability provisioning; failing it would break
|
|
2090
|
+
// deployments that are working. A platform exporting *some* stores but
|
|
2091
|
+
// not this one has adopted it and is missing or misspelling an entry.
|
|
2092
|
+
//
|
|
2093
|
+
// Folded into the association export for the same reason the merge gate
|
|
2094
|
+
// is: a dangling `apply` is not guaranteed to be evaluated, and a check
|
|
2095
|
+
// that might not run is not a check.
|
|
2096
|
+
let capabilityGate: Pulumi.Output.t<unit> =
|
|
2097
|
+
(pluginOutputs.pluginStructure, stackRef->Pulumi.StackReference.getOutput("objectStores"))
|
|
2098
|
+
->Pulumi.Output.all2
|
|
2099
|
+
->Pulumi.Output.apply((((structure, objectStores): (_, option<JSON.t>))) => {
|
|
2100
|
+
let required =
|
|
2101
|
+
structure->Option.flatMap(s => s.requiredStores)->Option.getOr([])
|
|
2102
|
+
let provisioned =
|
|
2103
|
+
objectStores
|
|
2104
|
+
->Option.flatMap(JSON.Decode.object)
|
|
2105
|
+
->Option.map(Dict.keysToArray)
|
|
2106
|
+
->Option.getOr([])
|
|
2107
|
+
switch Util_StoreLayout.coverageFor(~required, ~provisioned) {
|
|
2108
|
+
| Covered => ()
|
|
2109
|
+
| NotAdopted(missing) =>
|
|
2110
|
+
log.warn(
|
|
2111
|
+
~comp="Platform:deployPlugin",
|
|
2112
|
+
`declares ${missing->Array.join(", ")} but the platform stack provisions no object stores — ` ++
|
|
2113
|
+
`add them to the platform's ~capabilities and redeploy the platform first, ` ++
|
|
2114
|
+
`or uploads will fall back to the legacy service and write to the wrong bucket`,
|
|
2115
|
+
)
|
|
2116
|
+
| Missing({missing, provisioned}) =>
|
|
2117
|
+
JsError.throwWithMessage(
|
|
2118
|
+
`Plugin requires object store(s) the platform does not provision: ${missing->Array.join(", ")}.\n` ++
|
|
2119
|
+
` The platform stack provisions: ${provisioned->Array.join(", ")}.\n` ++
|
|
2120
|
+
` A store's key is {plugin}.{store}, where {plugin} is the name the plugin registers — ` ++
|
|
2121
|
+
`check the capability's spelling and case against it.\n` ++
|
|
2122
|
+
` Add the missing entr(ies) to the platform's ~capabilities and redeploy the platform stack first.`,
|
|
2123
|
+
)
|
|
2124
|
+
}
|
|
2125
|
+
})
|
|
2126
|
+
|
|
2056
2127
|
Pulumi.Pulumi.export(
|
|
2057
2128
|
"sourceApiAssociationId",
|
|
2058
|
-
(association.associationId, mergeGate)
|
|
2059
|
-
->Pulumi.Output.
|
|
2060
|
-
->Pulumi.Output.apply(((id, _)) => id),
|
|
2129
|
+
(association.associationId, mergeGate, capabilityGate)
|
|
2130
|
+
->Pulumi.Output.all3
|
|
2131
|
+
->Pulumi.Output.apply(((id, _, _)) => id),
|
|
2061
2132
|
)
|
|
2062
2133
|
Pulumi.Pulumi.export(
|
|
2063
2134
|
"pluginSourceApiId",
|
package/src/Platform.res.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output
|
|
|
11
11
|
import * as Pulumi$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Pulumi.res.mjs";
|
|
12
12
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
13
13
|
import * as Pulumi from "@pulumi/pulumi";
|
|
14
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
14
15
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
15
16
|
import * as Plugin$ReventlessAws from "./components/Plugin.res.mjs";
|
|
16
17
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
@@ -935,6 +936,7 @@ function MakeWithConfig(Config) {
|
|
|
935
936
|
plugin + `.` + store,
|
|
936
937
|
keyPrefix,
|
|
937
938
|
bucketName,
|
|
939
|
+
storeHandle.bucketName,
|
|
938
940
|
presign.url
|
|
939
941
|
];
|
|
940
942
|
});
|
|
@@ -963,8 +965,8 @@ function MakeWithConfig(Config) {
|
|
|
963
965
|
let declaredStoreEndpoints = declaredStoreServices.map(param => ({
|
|
964
966
|
store: param[0],
|
|
965
967
|
keyPrefix: param[1],
|
|
966
|
-
bucketName: param[
|
|
967
|
-
uploadUrl: param[
|
|
968
|
+
bucketName: param[3],
|
|
969
|
+
uploadUrl: param[4],
|
|
968
970
|
baseUrl: storeServingBaseUrl
|
|
969
971
|
}));
|
|
970
972
|
objectStoreEndpointsRef.contents = declaredStoreEndpoints;
|
|
@@ -973,28 +975,22 @@ function MakeWithConfig(Config) {
|
|
|
973
975
|
e.store,
|
|
974
976
|
u
|
|
975
977
|
]))).apply(pairs => Object.fromEntries(pairs)));
|
|
976
|
-
Pulumi$Pulumi.$$export("objectStores", Pulumi.all(declaredStoreEndpoints.map(e =>
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
e,
|
|
986
|
-
undefined
|
|
987
|
-
]);
|
|
988
|
-
}
|
|
989
|
-
})).apply(pairs => Object.fromEntries(pairs.map(param => {
|
|
990
|
-
let baseUrl = param[1];
|
|
978
|
+
Pulumi$Pulumi.$$export("objectStores", Pulumi.all(declaredStoreEndpoints.map(e => Pulumi.all([
|
|
979
|
+
e.bucketName,
|
|
980
|
+
Output$Pulumi.allOpt(e.baseUrl)
|
|
981
|
+
]).apply(param => [
|
|
982
|
+
e,
|
|
983
|
+
param[0],
|
|
984
|
+
param[1]
|
|
985
|
+
]))).apply(resolved => Object.fromEntries(resolved.map(param => {
|
|
986
|
+
let baseUrl = param[2];
|
|
991
987
|
let e = param[0];
|
|
992
988
|
return [
|
|
993
989
|
e.store,
|
|
994
990
|
Object.fromEntries([
|
|
995
991
|
[
|
|
996
992
|
"bucketName",
|
|
997
|
-
|
|
993
|
+
param[1]
|
|
998
994
|
],
|
|
999
995
|
[
|
|
1000
996
|
"keyPrefix",
|
|
@@ -1047,7 +1043,7 @@ function MakeWithConfig(Config) {
|
|
|
1047
1043
|
let uploadEndpointOutput = store$1 !== undefined ? Upload_Presign_S3$ReventlessAws.make(store$1.bucketName, undefined, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
1048
1044
|
let storeUploadEndpointsOutput = Pulumi.all(declaredStoreServices.map(param => {
|
|
1049
1045
|
let qualified = param[0];
|
|
1050
|
-
return param[
|
|
1046
|
+
return param[4].apply(u => [
|
|
1051
1047
|
qualified,
|
|
1052
1048
|
u
|
|
1053
1049
|
]);
|
|
@@ -1208,9 +1204,25 @@ function MakeWithConfig(Config) {
|
|
|
1208
1204
|
]).apply(param => param[0]);
|
|
1209
1205
|
let association = AppSync_MergedApi$ReventlessAws.associateSourceWithMergedArn("PluginSourceAssociation", arnAfterSchemaPush, domainApi, {});
|
|
1210
1206
|
let mergeGate = AppSync_MergedApi$ReventlessAws.mergeStatusGateWith(checkedMergedApiArn, association);
|
|
1207
|
+
let capabilityGate = Pulumi.all([
|
|
1208
|
+
pluginOutputs.pluginStructure,
|
|
1209
|
+
stackRef.getOutput("objectStores")
|
|
1210
|
+
]).apply(param => {
|
|
1211
|
+
let required = Stdlib_Option.getOr(Stdlib_Option.flatMap(param[0], s => s.requiredStores), []);
|
|
1212
|
+
let provisioned = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(param[1], Stdlib_JSON.Decode.object), prim => Object.keys(prim)), []);
|
|
1213
|
+
let missing = Util_StoreLayout$ReventlessAws.coverageFor(required, provisioned);
|
|
1214
|
+
if (typeof missing !== "object") {
|
|
1215
|
+
return;
|
|
1216
|
+
} else if (missing.TAG === "NotAdopted") {
|
|
1217
|
+
return log.warn("Platform:deployPlugin", undefined, `declares ` + missing._0.join(", ") + ` but the platform stack provisions no object stores — add them to the platform's ~capabilities and redeploy the platform first, or uploads will fall back to the legacy service and write to the wrong bucket`);
|
|
1218
|
+
} else {
|
|
1219
|
+
return Stdlib_JsError.throwWithMessage(`Plugin requires object store(s) the platform does not provision: ` + missing.missing.join(", ") + `.\n` + (` The platform stack provisions: ` + missing.provisioned.join(", ") + `.\n`) + ` A store's key is {plugin}.{store}, where {plugin} is the name the plugin registers — check the capability's spelling and case against it.\n Add the missing entr(ies) to the platform's ~capabilities and redeploy the platform stack first.`);
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1211
1222
|
Pulumi$Pulumi.$$export("sourceApiAssociationId", Pulumi.all([
|
|
1212
1223
|
association.associationId,
|
|
1213
|
-
mergeGate
|
|
1224
|
+
mergeGate,
|
|
1225
|
+
capabilityGate
|
|
1214
1226
|
]).apply(param => param[0]));
|
|
1215
1227
|
Pulumi$Pulumi.$$export("pluginSourceApiId", Output$Pulumi.flatMap(domainApi, api => api.id));
|
|
1216
1228
|
Pulumi$Pulumi.$$export("pluginSourceApiEndpoint", Output$Pulumi.flatMap(domainApi, api => api.uris.apply(uris => uris.GRAPHQL)));
|
|
@@ -2079,6 +2091,7 @@ function Make($star) {
|
|
|
2079
2091
|
plugin + `.` + store,
|
|
2080
2092
|
keyPrefix,
|
|
2081
2093
|
bucketName,
|
|
2094
|
+
storeHandle.bucketName,
|
|
2082
2095
|
presign.url
|
|
2083
2096
|
];
|
|
2084
2097
|
});
|
|
@@ -2107,8 +2120,8 @@ function Make($star) {
|
|
|
2107
2120
|
let declaredStoreEndpoints = declaredStoreServices.map(param => ({
|
|
2108
2121
|
store: param[0],
|
|
2109
2122
|
keyPrefix: param[1],
|
|
2110
|
-
bucketName: param[
|
|
2111
|
-
uploadUrl: param[
|
|
2123
|
+
bucketName: param[3],
|
|
2124
|
+
uploadUrl: param[4],
|
|
2112
2125
|
baseUrl: storeServingBaseUrl
|
|
2113
2126
|
}));
|
|
2114
2127
|
objectStoreEndpointsRef.contents = declaredStoreEndpoints;
|
|
@@ -2117,28 +2130,22 @@ function Make($star) {
|
|
|
2117
2130
|
e.store,
|
|
2118
2131
|
u
|
|
2119
2132
|
]))).apply(pairs => Object.fromEntries(pairs)));
|
|
2120
|
-
Pulumi$Pulumi.$$export("objectStores", Pulumi.all(declaredStoreEndpoints.map(e =>
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
e,
|
|
2130
|
-
undefined
|
|
2131
|
-
]);
|
|
2132
|
-
}
|
|
2133
|
-
})).apply(pairs => Object.fromEntries(pairs.map(param => {
|
|
2134
|
-
let baseUrl = param[1];
|
|
2133
|
+
Pulumi$Pulumi.$$export("objectStores", Pulumi.all(declaredStoreEndpoints.map(e => Pulumi.all([
|
|
2134
|
+
e.bucketName,
|
|
2135
|
+
Output$Pulumi.allOpt(e.baseUrl)
|
|
2136
|
+
]).apply(param => [
|
|
2137
|
+
e,
|
|
2138
|
+
param[0],
|
|
2139
|
+
param[1]
|
|
2140
|
+
]))).apply(resolved => Object.fromEntries(resolved.map(param => {
|
|
2141
|
+
let baseUrl = param[2];
|
|
2135
2142
|
let e = param[0];
|
|
2136
2143
|
return [
|
|
2137
2144
|
e.store,
|
|
2138
2145
|
Object.fromEntries([
|
|
2139
2146
|
[
|
|
2140
2147
|
"bucketName",
|
|
2141
|
-
|
|
2148
|
+
param[1]
|
|
2142
2149
|
],
|
|
2143
2150
|
[
|
|
2144
2151
|
"keyPrefix",
|
|
@@ -2191,7 +2198,7 @@ function Make($star) {
|
|
|
2191
2198
|
let uploadEndpointOutput = store$1 !== undefined ? Upload_Presign_S3$ReventlessAws.make(store$1.bucketName, undefined, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
2192
2199
|
let storeUploadEndpointsOutput = Pulumi.all(declaredStoreServices.map(param => {
|
|
2193
2200
|
let qualified = param[0];
|
|
2194
|
-
return param[
|
|
2201
|
+
return param[4].apply(u => [
|
|
2195
2202
|
qualified,
|
|
2196
2203
|
u
|
|
2197
2204
|
]);
|
|
@@ -2352,9 +2359,25 @@ function Make($star) {
|
|
|
2352
2359
|
]).apply(param => param[0]);
|
|
2353
2360
|
let association = AppSync_MergedApi$ReventlessAws.associateSourceWithMergedArn("PluginSourceAssociation", arnAfterSchemaPush, domainApi, {});
|
|
2354
2361
|
let mergeGate = AppSync_MergedApi$ReventlessAws.mergeStatusGateWith(checkedMergedApiArn, association);
|
|
2362
|
+
let capabilityGate = Pulumi.all([
|
|
2363
|
+
pluginOutputs.pluginStructure,
|
|
2364
|
+
stackRef.getOutput("objectStores")
|
|
2365
|
+
]).apply(param => {
|
|
2366
|
+
let required = Stdlib_Option.getOr(Stdlib_Option.flatMap(param[0], s => s.requiredStores), []);
|
|
2367
|
+
let provisioned = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(param[1], Stdlib_JSON.Decode.object), prim => Object.keys(prim)), []);
|
|
2368
|
+
let missing = Util_StoreLayout$ReventlessAws.coverageFor(required, provisioned);
|
|
2369
|
+
if (typeof missing !== "object") {
|
|
2370
|
+
return;
|
|
2371
|
+
} else if (missing.TAG === "NotAdopted") {
|
|
2372
|
+
return log.warn("Platform:deployPlugin", undefined, `declares ` + missing._0.join(", ") + ` but the platform stack provisions no object stores — add them to the platform's ~capabilities and redeploy the platform first, or uploads will fall back to the legacy service and write to the wrong bucket`);
|
|
2373
|
+
} else {
|
|
2374
|
+
return Stdlib_JsError.throwWithMessage(`Plugin requires object store(s) the platform does not provision: ` + missing.missing.join(", ") + `.\n` + (` The platform stack provisions: ` + missing.provisioned.join(", ") + `.\n`) + ` A store's key is {plugin}.{store}, where {plugin} is the name the plugin registers — check the capability's spelling and case against it.\n Add the missing entr(ies) to the platform's ~capabilities and redeploy the platform stack first.`);
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2355
2377
|
Pulumi$Pulumi.$$export("sourceApiAssociationId", Pulumi.all([
|
|
2356
2378
|
association.associationId,
|
|
2357
|
-
mergeGate
|
|
2379
|
+
mergeGate,
|
|
2380
|
+
capabilityGate
|
|
2358
2381
|
]).apply(param => param[0]));
|
|
2359
2382
|
Pulumi$Pulumi.$$export("pluginSourceApiId", Output$Pulumi.flatMap(domainApi, api => api.id));
|
|
2360
2383
|
Pulumi$Pulumi.$$export("pluginSourceApiEndpoint", Output$Pulumi.flatMap(domainApi, api => api.uris.apply(uris => uris.GRAPHQL)));
|
|
@@ -63,11 +63,21 @@ type deleteCmd
|
|
|
63
63
|
|
|
64
64
|
// Shape of the dynamically-imported `@aws-sdk/client-appsync` module. Only the
|
|
65
65
|
// three association commands are needed here.
|
|
66
|
+
//
|
|
67
|
+
// The create/delete names follow the *API operation*, not the resource: AppSync
|
|
68
|
+
// has `AssociateSourceGraphqlApi` / `DisassociateSourceGraphqlApi`, and there is
|
|
69
|
+
// no `Create…`/`Delete…SourceApiAssociation` command. Those names are only the
|
|
70
|
+
// CloudFormation/Pulumi resource's, and naming the constructors after the
|
|
71
|
+
// resource made `create` throw `C is not a constructor` on every fresh stack —
|
|
72
|
+
// `new undefined(...)`, since a missing export is just undefined on the module
|
|
73
|
+
// namespace. `Get` exists under the resource-style name, which is why read and
|
|
74
|
+
// diff always worked and only creation was broken. The request payloads below
|
|
75
|
+
// already match the operations exactly.
|
|
66
76
|
type sdkModule = {
|
|
67
77
|
@as("AppSyncClient") clientCtor: unit => appSyncClient,
|
|
68
|
-
@as("
|
|
78
|
+
@as("AssociateSourceGraphqlApiCommand") createCtor: createInput => createCmd,
|
|
69
79
|
@as("GetSourceApiAssociationCommand") getCtor: idInput => getCmd,
|
|
70
|
-
@as("
|
|
80
|
+
@as("DisassociateSourceGraphqlApiCommand") deleteCtor: idInput => deleteCmd,
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
// Calls a constructor with `new` — the SDK exports plain classes.
|
|
@@ -277,7 +287,7 @@ let extractIds = (resp: createResult): (string, string) =>
|
|
|
277
287
|
| Some({associationArn: ?Some(arn), associationId: ?Some(aid)}) => (arn, aid)
|
|
278
288
|
| _ =>
|
|
279
289
|
JsError.throwWithMessage(
|
|
280
|
-
"
|
|
290
|
+
"AssociateSourceGraphqlApi returned no associationArn / associationId",
|
|
281
291
|
)
|
|
282
292
|
}
|
|
283
293
|
|
|
@@ -181,11 +181,11 @@ function identifiersFrom(id, props) {
|
|
|
181
181
|
function extractIds(resp) {
|
|
182
182
|
let match = resp.sourceApiAssociation;
|
|
183
183
|
if (match === undefined) {
|
|
184
|
-
return Stdlib_JsError.throwWithMessage("
|
|
184
|
+
return Stdlib_JsError.throwWithMessage("AssociateSourceGraphqlApi returned no associationArn / associationId");
|
|
185
185
|
}
|
|
186
186
|
let aid = match.associationId;
|
|
187
187
|
if (aid === undefined) {
|
|
188
|
-
return Stdlib_JsError.throwWithMessage("
|
|
188
|
+
return Stdlib_JsError.throwWithMessage("AssociateSourceGraphqlApi returned no associationArn / associationId");
|
|
189
189
|
}
|
|
190
190
|
let arn = match.associationArn;
|
|
191
191
|
if (arn !== undefined) {
|
|
@@ -194,14 +194,14 @@ function extractIds(resp) {
|
|
|
194
194
|
aid
|
|
195
195
|
];
|
|
196
196
|
} else {
|
|
197
|
-
return Stdlib_JsError.throwWithMessage("
|
|
197
|
+
return Stdlib_JsError.throwWithMessage("AssociateSourceGraphqlApi returned no associationArn / associationId");
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
async function create(inputs) {
|
|
202
202
|
let sdk = await getSdk();
|
|
203
203
|
let client = await getClient();
|
|
204
|
-
let match = await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(newOf1(sdk.
|
|
204
|
+
let match = await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(newOf1(sdk.AssociateSourceGraphqlApiCommand, {
|
|
205
205
|
mergedApiIdentifier: inputs.mergedApiIdentifier,
|
|
206
206
|
sourceApiIdentifier: inputs.sourceApiIdentifier,
|
|
207
207
|
sourceApiAssociationConfig: {
|
|
@@ -235,7 +235,7 @@ async function delete_(id, props) {
|
|
|
235
235
|
return log.warn("AppSync_SourceApiAssociation_Retrying", undefined, `delete: could not derive association identifiers from id "` + id + `"; treating as already gone`);
|
|
236
236
|
}
|
|
237
237
|
try {
|
|
238
|
-
return await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(newOf1(sdk.
|
|
238
|
+
return await runWithRetry(undefined, undefined, undefined, undefined, () => client.send(newOf1(sdk.DisassociateSourceGraphqlApiCommand, ids)));
|
|
239
239
|
} catch (raw_exn) {
|
|
240
240
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
241
241
|
if (Stdlib_Option.mapOr(Stdlib_JsExn.fromException(exn), false, isAlreadyGoneError)) {
|
|
@@ -34,6 +34,17 @@ type protection =
|
|
|
34
34
|
| Protected
|
|
35
35
|
| Unprotected
|
|
36
36
|
|
|
37
|
+
/** Whether the platform provisions everything a plugin's fields declare. */
|
|
38
|
+
type coverage =
|
|
39
|
+
/** Every declared store is provisioned. */
|
|
40
|
+
| Covered
|
|
41
|
+
/** The platform provisions no stores at all — it has not adopted capability
|
|
42
|
+
provisioning, which is a different situation from getting it wrong. */
|
|
43
|
+
| NotAdopted(array<string>)
|
|
44
|
+
/** The platform provisions stores, but not these. Carries what it does
|
|
45
|
+
provision, because the usual cause is a near-miss worth showing. */
|
|
46
|
+
| Missing({missing: array<string>, provisioned: array<string>})
|
|
47
|
+
|
|
37
48
|
/** Which distribution fronts the declared stores. */
|
|
38
49
|
type serving =
|
|
39
50
|
/** No store is declared, so nothing is served. */
|
|
@@ -134,3 +145,31 @@ let servingFor = (~hasHostUiBundle: bool, ~declaredBucketCount: int): serving =>
|
|
|
134
145
|
| (true, _) => HostShell
|
|
135
146
|
| (false, _) => PlatformOwned
|
|
136
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
Does the platform provision what the plugin's fields declare?
|
|
151
|
+
|
|
152
|
+
The split-stack ordering hazard, stated as a set difference: the platform
|
|
153
|
+
deploys before the plugin and cannot read its schemas, so its capability list is
|
|
154
|
+
written by hand and can simply be wrong. `required` comes from
|
|
155
|
+
`pluginStructure.requiredStores`; `provisioned` from the platform's exported
|
|
156
|
+
`objectStores` keys. Both are qualified `{plugin}.{store}`.
|
|
157
|
+
|
|
158
|
+
**Three outcomes, not two.** A platform provisioning nothing has not adopted
|
|
159
|
+
capability provisioning; failing it would break deployments that work today. A
|
|
160
|
+
platform provisioning *some* stores but not this one has adopted it and has a
|
|
161
|
+
missing or misspelled entry. Collapsing those two into one verdict forces a
|
|
162
|
+
choice between breaking the first group and not helping the second.
|
|
163
|
+
|
|
164
|
+
Worth being strict about because every symptom is silent: the upload input finds
|
|
165
|
+
no per-store endpoint, falls back to the legacy single service, and writes to
|
|
166
|
+
whatever bucket that serves — a 2xx, a plausible ref, and the wrong destination.
|
|
167
|
+
*/
|
|
168
|
+
let coverageFor = (~required: array<string>, ~provisioned: array<string>): coverage => {
|
|
169
|
+
let missing = required->Array.filter(r => !(provisioned->Array.includes(r)))
|
|
170
|
+
switch (missing, provisioned) {
|
|
171
|
+
| ([], _) => Covered
|
|
172
|
+
| (missing, []) => NotAdopted(missing)
|
|
173
|
+
| (missing, provisioned) => Missing({missing, provisioned})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -44,6 +44,26 @@ function servingFor(hasHostUiBundle, declaredBucketCount) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function coverageFor(required, provisioned) {
|
|
48
|
+
let missing = required.filter(r => !provisioned.includes(r));
|
|
49
|
+
if (missing.length !== 0) {
|
|
50
|
+
if (provisioned.length !== 0) {
|
|
51
|
+
return {
|
|
52
|
+
TAG: "Missing",
|
|
53
|
+
missing: missing,
|
|
54
|
+
provisioned: provisioned
|
|
55
|
+
};
|
|
56
|
+
} else {
|
|
57
|
+
return {
|
|
58
|
+
TAG: "NotAdopted",
|
|
59
|
+
_0: missing
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
return "Covered";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
export {
|
|
48
68
|
defaultEphemeralPrefixes,
|
|
49
69
|
layoutFor,
|
|
@@ -51,5 +71,6 @@ export {
|
|
|
51
71
|
bucketNameFor,
|
|
52
72
|
keyPrefixFor,
|
|
53
73
|
servingFor,
|
|
74
|
+
coverageFor,
|
|
54
75
|
}
|
|
55
76
|
/* No side effect */
|
|
@@ -80,6 +80,80 @@ describe("Util_StoreLayout.servingFor", () => {
|
|
|
80
80
|
)
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
+
describe("Util_StoreLayout.coverageFor", () => {
|
|
84
|
+
testSync("everything declared is provisioned", () =>
|
|
85
|
+
expect(
|
|
86
|
+
Util_StoreLayout.coverageFor(
|
|
87
|
+
~required=["Catalog.productImages"],
|
|
88
|
+
~provisioned=["Catalog.productImages"],
|
|
89
|
+
),
|
|
90
|
+
)->toEqual(Util_StoreLayout.Covered)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
testSync("declaring nothing is covered, whatever the platform provisions", () =>
|
|
94
|
+
expect(Util_StoreLayout.coverageFor(~required=[], ~provisioned=["Catalog.productImages"]))->toEqual(
|
|
95
|
+
Util_StoreLayout.Covered,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
// A platform provisioning nothing has not adopted capability provisioning.
|
|
100
|
+
// Failing it would break deployments that work today, so this is the arm that
|
|
101
|
+
// must NOT be a hard error.
|
|
102
|
+
testSync("a platform provisioning nothing has not adopted, rather than got it wrong", () =>
|
|
103
|
+
expect(Util_StoreLayout.coverageFor(~required=["Catalog.productImages"], ~provisioned=[]))->toEqual(
|
|
104
|
+
Util_StoreLayout.NotAdopted(["Catalog.productImages"]),
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// The case that shipped: the platform declared the store under a lowercased
|
|
109
|
+
// plugin name, so both sides had a productImages store and neither matched.
|
|
110
|
+
// It carries what IS provisioned because the cause is usually a near-miss.
|
|
111
|
+
testSync("a near-miss reports both sides — this is the case-slip shape", () =>
|
|
112
|
+
expect(
|
|
113
|
+
Util_StoreLayout.coverageFor(
|
|
114
|
+
~required=["Catalog.productImages"],
|
|
115
|
+
~provisioned=["catalog.productImages"],
|
|
116
|
+
),
|
|
117
|
+
)->toEqual(
|
|
118
|
+
Util_StoreLayout.Missing({
|
|
119
|
+
missing: ["Catalog.productImages"],
|
|
120
|
+
provisioned: ["catalog.productImages"],
|
|
121
|
+
}),
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
testSync("only the uncovered stores are reported missing", () =>
|
|
126
|
+
expect(
|
|
127
|
+
Util_StoreLayout.coverageFor(
|
|
128
|
+
~required=["Catalog.productImages", "Catalog.manuals"],
|
|
129
|
+
~provisioned=["Catalog.productImages"],
|
|
130
|
+
),
|
|
131
|
+
)->toEqual(
|
|
132
|
+
Util_StoreLayout.Missing({
|
|
133
|
+
missing: ["Catalog.manuals"],
|
|
134
|
+
provisioned: ["Catalog.productImages"],
|
|
135
|
+
}),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// Matching is exact. Suffix or case-insensitive matching would "fix" the
|
|
140
|
+
// case slip above by silently binding to the wrong store, and two plugins may
|
|
141
|
+
// legitimately name a store the same.
|
|
142
|
+
testSync("matching is exact — a suffix match is not a match", () =>
|
|
143
|
+
expect(
|
|
144
|
+
Util_StoreLayout.coverageFor(
|
|
145
|
+
~required=["Catalog.productImages"],
|
|
146
|
+
~provisioned=["Ordering.productImages"],
|
|
147
|
+
),
|
|
148
|
+
)->toEqual(
|
|
149
|
+
Util_StoreLayout.Missing({
|
|
150
|
+
missing: ["Catalog.productImages"],
|
|
151
|
+
provisioned: ["Ordering.productImages"],
|
|
152
|
+
}),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
})
|
|
156
|
+
|
|
83
157
|
describe("Util_StoreLayout.protectionFor", () => {
|
|
84
158
|
// The pairing a single layout-driven switch would have got wrong: alpha
|
|
85
159
|
// shares a bucket and is still protected.
|
|
@@ -45,6 +45,45 @@ globalThis.describe("Util_StoreLayout.servingFor", () => {
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
globalThis.describe("Util_StoreLayout.coverageFor", () => {
|
|
49
|
+
globalThis.test("everything declared is provisioned", () => {
|
|
50
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.coverageFor(["Catalog.productImages"], ["Catalog.productImages"])).toEqual("Covered");
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("declaring nothing is covered, whatever the platform provisions", () => {
|
|
53
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.coverageFor([], ["Catalog.productImages"])).toEqual("Covered");
|
|
54
|
+
});
|
|
55
|
+
globalThis.test("a platform provisioning nothing has not adopted, rather than got it wrong", () => {
|
|
56
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.coverageFor(["Catalog.productImages"], [])).toEqual({
|
|
57
|
+
TAG: "NotAdopted",
|
|
58
|
+
_0: ["Catalog.productImages"]
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
globalThis.test("a near-miss reports both sides — this is the case-slip shape", () => {
|
|
62
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.coverageFor(["Catalog.productImages"], ["catalog.productImages"])).toEqual({
|
|
63
|
+
TAG: "Missing",
|
|
64
|
+
missing: ["Catalog.productImages"],
|
|
65
|
+
provisioned: ["catalog.productImages"]
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
globalThis.test("only the uncovered stores are reported missing", () => {
|
|
69
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.coverageFor([
|
|
70
|
+
"Catalog.productImages",
|
|
71
|
+
"Catalog.manuals"
|
|
72
|
+
], ["Catalog.productImages"])).toEqual({
|
|
73
|
+
TAG: "Missing",
|
|
74
|
+
missing: ["Catalog.manuals"],
|
|
75
|
+
provisioned: ["Catalog.productImages"]
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
globalThis.test("matching is exact — a suffix match is not a match", () => {
|
|
79
|
+
globalThis.expect(Util_StoreLayout$ReventlessAws.coverageFor(["Catalog.productImages"], ["Ordering.productImages"])).toEqual({
|
|
80
|
+
TAG: "Missing",
|
|
81
|
+
missing: ["Catalog.productImages"],
|
|
82
|
+
provisioned: ["Ordering.productImages"]
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
48
87
|
globalThis.describe("Util_StoreLayout.protectionFor", () => {
|
|
49
88
|
globalThis.test("alpha shares a bucket and is still protected", () => {
|
|
50
89
|
globalThis.expect(Util_StoreLayout$ReventlessAws.layoutFor("alpha", Util_HostUiDomain$ReventlessAws.defaultProdStacks)).toEqual("SharedBucket");
|