@reventlessdev/reventless-aws 3.0.0-alpha.239 → 3.0.0-alpha.241
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 +7 -7
- package/src/Platform.res +292 -15
- package/src/Platform.res.mjs +362 -48
- package/src/adapter/Upload/Upload_Presign_S3.res +19 -3
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +9 -9
- package/src/capability/Capability_ObjectStore_S3.res +69 -7
- package/src/capability/Capability_ObjectStore_S3.res.mjs +32 -6
- package/src/plugin/stack/Plugin_Stack.res +209 -6
- package/src/plugin/stack/Plugin_Stack.res.mjs +122 -6
- package/src/util/Util_HostUiDomain.res +17 -0
- package/src/util/Util_HostUiDomain.res.mjs +8 -1
- package/src/util/Util_StoreLayout.res +136 -0
- package/src/util/Util_StoreLayout.res.mjs +55 -0
- package/tests/Util_StoreLayoutTest.res +155 -0
- package/tests/Util_StoreLayoutTest.res.mjs +87 -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.241 (2026-07-29)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **platform:** serve and publish declared object stores without a host UI ([9cc4700](https://github.com/ReventlessDev/reventless-core/commit/9cc4700dc96f05e3d35fbc912a4c7be7a5531337))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.240 (2026-07-28)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **platform:** provision object stores from the fields that declare them ([b5e2a1e](https://github.com/ReventlessDev/reventless-core/commit/b5e2a1ec88099941c113e4963f9c4b346b96b0d6))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.239 (2026-07-28)
|
|
7
21
|
|
|
8
22
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
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.241",
|
|
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-jest": "1.0.0-alpha.9",
|
|
17
16
|
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.61",
|
|
18
17
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
19
|
-
"@reventlessdev/
|
|
20
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.107",
|
|
21
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
18
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
22
19
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-
|
|
20
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.190",
|
|
21
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.108",
|
|
22
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
23
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.54",
|
|
24
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.83"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -44,6 +44,40 @@ let splitApiOutputsRef: ref<option<splitApiOutputs>> = ref(None)
|
|
|
44
44
|
Call after `makePlatform` — returns `None` in unified mode. */
|
|
45
45
|
let getSplitApiOutputs = () => splitApiOutputsRef.contents
|
|
46
46
|
|
|
47
|
+
// Object stores provisioned from field declarations — populated by
|
|
48
|
+
// deployPlatform. Access via getObjectStoreEndpoints() after it has been called.
|
|
49
|
+
//
|
|
50
|
+
// A platform whose UI ships from its own stack needs each store's presign
|
|
51
|
+
// endpoint, and `deployPlatform` is the only place that knows it. Without this
|
|
52
|
+
// the store is provisioned and unnameable: the stores are built unconditionally,
|
|
53
|
+
// but every consumer of them used to sit inside `switch hostUiBundle`. Same
|
|
54
|
+
// reason `getSplitApiOutputs` exists — a root cannot re-export what it cannot
|
|
55
|
+
// reach.
|
|
56
|
+
type objectStoreEndpoint = {
|
|
57
|
+
// The store's qualified `{plugin}.{store}` name. The same string
|
|
58
|
+
// `pluginStructure.requiredStores` carries, so a UI's binding key and the
|
|
59
|
+
// declaration's identity are one value rather than two that can drift.
|
|
60
|
+
store: string,
|
|
61
|
+
// The path this store's keys are rooted at, and therefore the path it must be
|
|
62
|
+
// served under for a minted `/{key}` ref to resolve.
|
|
63
|
+
keyPrefix: string,
|
|
64
|
+
bucketName: string,
|
|
65
|
+
uploadUrl: Pulumi.Output.t<string>,
|
|
66
|
+
// Public base URL the store is served from, when the platform serves it
|
|
67
|
+
// itself. `None` when a host-UI bundle is deployed — there the shell's own
|
|
68
|
+
// origin serves the store same-origin and a relative `/{prefix}/…` resolves
|
|
69
|
+
// without a base.
|
|
70
|
+
baseUrl: option<Pulumi.Output.t<string>>,
|
|
71
|
+
}
|
|
72
|
+
let objectStoreEndpointsRef: ref<array<objectStoreEndpoint>> = ref([])
|
|
73
|
+
|
|
74
|
+
/** Returns the object stores this platform provisioned from field declarations.
|
|
75
|
+
|
|
76
|
+
Empty when nothing is declared — a legitimate answer rather than a
|
|
77
|
+
call-order mistake, so this returns `[]` instead of throwing the way
|
|
78
|
+
`getApiConfig` does. */
|
|
79
|
+
let getObjectStoreEndpoints = () => objectStoreEndpointsRef.contents
|
|
80
|
+
|
|
47
81
|
module MakeWithConfig = (
|
|
48
82
|
Config: {
|
|
49
83
|
let splitApi: bool
|
|
@@ -1046,7 +1080,11 @@ module MakeWithConfig = (
|
|
|
1046
1080
|
platformMergeGate: Pulumi.Output.t<unit>,
|
|
1047
1081
|
}
|
|
1048
1082
|
|
|
1049
|
-
let deployPlatform = (
|
|
1083
|
+
let deployPlatform = (
|
|
1084
|
+
~version,
|
|
1085
|
+
~hostUiBundle: option<hostUiBundleConfig>=?,
|
|
1086
|
+
~capabilities: array<ReventlessInfra.Platform.capability>=[],
|
|
1087
|
+
) => {
|
|
1050
1088
|
log.info(~comp="Platform:deployPlatform", `v${version}`)
|
|
1051
1089
|
let scheduler = makeScheduler()
|
|
1052
1090
|
hooks.scheduler := Some(scheduler)
|
|
@@ -1452,6 +1490,205 @@ module MakeWithConfig = (
|
|
|
1452
1490
|
})
|
|
1453
1491
|
})
|
|
1454
1492
|
|
|
1493
|
+
// ── Declared object stores ────────────────────────────────────────────
|
|
1494
|
+
//
|
|
1495
|
+
// The store a field declares is the store that gets provisioned. Before
|
|
1496
|
+
// this, a `@storageRef("productImages")` annotation and a hand-written
|
|
1497
|
+
// bucket name were two unrelated strings: deleting the annotation left the
|
|
1498
|
+
// bucket, and adding one provisioned nothing.
|
|
1499
|
+
//
|
|
1500
|
+
// Provisioned here, in the platform deploy, rather than in each plugin's
|
|
1501
|
+
// stack. Three things force it: the serving CDN lives here, the presign
|
|
1502
|
+
// services live here, and a shared-layout bucket has several plugins'
|
|
1503
|
+
// stores in it, so no single plugin stack can own it. Ownership is carried
|
|
1504
|
+
// in the tags instead — a per-store bucket is tagged to the plugin whose
|
|
1505
|
+
// field declared it; a shared bucket is platform substrate, because that is
|
|
1506
|
+
// what it is.
|
|
1507
|
+
let stackName = Pulumi.Pulumi.getStackName()
|
|
1508
|
+
let storeLayout = Util_StoreLayout.layoutFor(
|
|
1509
|
+
~stack=stackName,
|
|
1510
|
+
~prodStacks=Util_HostUiDomain.resolveProdStacks(),
|
|
1511
|
+
)
|
|
1512
|
+
let storeProtection = Util_StoreLayout.protectionFor(~stack=stackName)
|
|
1513
|
+
|
|
1514
|
+
// Dedup by `(plugin, store)` — that pair is a store's identity, and many
|
|
1515
|
+
// fields legitimately name one store.
|
|
1516
|
+
let declaredStores =
|
|
1517
|
+
capabilities
|
|
1518
|
+
->Array.filterMap(c =>
|
|
1519
|
+
switch c {
|
|
1520
|
+
| ObjectStore({plugin, store}) => Some((plugin, store))
|
|
1521
|
+
| Geocoding => None
|
|
1522
|
+
}
|
|
1523
|
+
)
|
|
1524
|
+
->Array.reduce([], (acc, (plugin, store)) =>
|
|
1525
|
+
acc->Array.some(((p, s)) => p == plugin && s == store)
|
|
1526
|
+
? acc
|
|
1527
|
+
: Array.concat(acc, [(plugin, store)])
|
|
1528
|
+
)
|
|
1529
|
+
|
|
1530
|
+
if declaredStores->Array.length > 0 {
|
|
1531
|
+
// The fail-open in `layoutFor` is only dangerous while it is silent: a
|
|
1532
|
+
// production stack that is not on the prod list gets the shared layout and
|
|
1533
|
+
// nothing errors. Log the choice on every deploy so it surfaces the first
|
|
1534
|
+
// time it happens rather than at an audit.
|
|
1535
|
+
log.info(
|
|
1536
|
+
~comp="Platform:deployPlatform",
|
|
1537
|
+
`object stores: ${switch storeLayout {
|
|
1538
|
+
| PerStore => "bucket per store"
|
|
1539
|
+
| SharedBucket => "shared bucket, {store}/… prefixes"
|
|
1540
|
+
}}, ${switch storeProtection {
|
|
1541
|
+
| Protected => "protected"
|
|
1542
|
+
| Unprotected => "unprotected (disposable stack)"
|
|
1543
|
+
}} — stack ${stackName}`,
|
|
1544
|
+
)
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
// One bucket per *physical name*, which is one per store under `PerStore`
|
|
1548
|
+
// and one for the whole stack under `SharedBucket`. Grouped rather than
|
|
1549
|
+
// mapped because those cardinalities differ, and a bucket carries only one
|
|
1550
|
+
// policy and one CDN origin however many stores live in it.
|
|
1551
|
+
let storeBuckets: Dict.t<ReventlessInfra.Platform.objectStore> = Dict.make()
|
|
1552
|
+
let storeBucketOrder: array<string> = []
|
|
1553
|
+
let declaredStoreServices = declaredStores->Array.map(((plugin, store)) => {
|
|
1554
|
+
let bucketName = Util_StoreLayout.bucketNameFor(
|
|
1555
|
+
~layout=storeLayout,
|
|
1556
|
+
~stack=stackName,
|
|
1557
|
+
~plugin,
|
|
1558
|
+
~store,
|
|
1559
|
+
)
|
|
1560
|
+
let keyPrefix = Util_StoreLayout.keyPrefixFor(~store)
|
|
1561
|
+
let bucket = switch storeBuckets->Dict.get(bucketName) {
|
|
1562
|
+
| Some(b) => b
|
|
1563
|
+
| None =>
|
|
1564
|
+
let b = Capability_ObjectStore_S3.make(
|
|
1565
|
+
~name=bucketName,
|
|
1566
|
+
~keyPrefix,
|
|
1567
|
+
// A shared bucket belongs to no single plugin; a dedicated one does.
|
|
1568
|
+
~plugin=?switch storeLayout {
|
|
1569
|
+
| PerStore => Some(plugin)
|
|
1570
|
+
| SharedBucket => None
|
|
1571
|
+
},
|
|
1572
|
+
~protect=storeProtection == Protected,
|
|
1573
|
+
// A protected bucket blocks `pulumi destroy`, so protecting a
|
|
1574
|
+
// disposable stack would leak exactly the buckets sharing exists to
|
|
1575
|
+
// save. The two settings are opposite faces of one decision.
|
|
1576
|
+
~forceDestroy=storeProtection == Unprotected,
|
|
1577
|
+
)
|
|
1578
|
+
storeBuckets->Dict.set(bucketName, b)
|
|
1579
|
+
storeBucketOrder->Array.push(bucketName)
|
|
1580
|
+
b
|
|
1581
|
+
}
|
|
1582
|
+
let storeHandle = bucket->Capability_ObjectStore_S3.underPrefix(~keyPrefix)
|
|
1583
|
+
// One presign service per store, scoped to that store's own prefix.
|
|
1584
|
+
let presign = Upload_Presign_S3.make(
|
|
1585
|
+
~name=`UploadPresign-${plugin}-${store}`,
|
|
1586
|
+
~bucketName=storeHandle.bucketName,
|
|
1587
|
+
~servedPrefix=storeHandle.keyPrefix,
|
|
1588
|
+
)
|
|
1589
|
+
(`${plugin}.${store}`, keyPrefix, bucketName, presign.url)
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1592
|
+
// Group the served view by bucket: one origin and one bucket policy per
|
|
1593
|
+
// bucket, carrying every prefix served from it.
|
|
1594
|
+
let declaredServedBuckets = storeBucketOrder->Array.filterMap(bucketName =>
|
|
1595
|
+
storeBuckets
|
|
1596
|
+
->Dict.get(bucketName)
|
|
1597
|
+
->Option.map(b => {
|
|
1598
|
+
ReventlessInfra.Platform.id: bucketName,
|
|
1599
|
+
prefixes: declaredStoreServices
|
|
1600
|
+
->Array.filterMap(((_, prefix, bn, _)) => bn == bucketName ? Some(prefix) : None),
|
|
1601
|
+
bucketId: b.bucketId,
|
|
1602
|
+
bucketArn: b.bucketArn,
|
|
1603
|
+
bucketRegionalDomainName: b.bucketRegionalDomainName,
|
|
1604
|
+
})
|
|
1605
|
+
)
|
|
1606
|
+
|
|
1607
|
+
// ── Serving the declared stores ───────────────────────────────────────
|
|
1608
|
+
//
|
|
1609
|
+
// A store's bucket blocks public policy and takes its read grant only from
|
|
1610
|
+
// a distribution's BucketPolicy, so "provisioned" and "readable" are two
|
|
1611
|
+
// different things. When a host UI bundle is deployed, its distribution
|
|
1612
|
+
// fronts the stores same-origin (below) and a relative `/{prefix}/…`
|
|
1613
|
+
// resolves with no base URL. When it is not, the platform fronts them
|
|
1614
|
+
// itself — otherwise every declared store is write-only.
|
|
1615
|
+
//
|
|
1616
|
+
// Never both: two distributions fronting one bucket would each write the
|
|
1617
|
+
// bucket's single allowed policy and silently unpick the other's grant.
|
|
1618
|
+
// `Util_StoreLayout.servingFor` is where that exclusion is decided, so it is
|
|
1619
|
+
// one testable choice rather than a condition spelled out here.
|
|
1620
|
+
let storeServingBaseUrl: option<Pulumi.Output.t<string>> = switch Util_StoreLayout.servingFor(
|
|
1621
|
+
~hasHostUiBundle=hostUiBundle->Option.isSome,
|
|
1622
|
+
~declaredBucketCount=declaredServedBuckets->Array.length,
|
|
1623
|
+
) {
|
|
1624
|
+
| NoStores | HostShell => None
|
|
1625
|
+
| PlatformOwned =>
|
|
1626
|
+
Some(
|
|
1627
|
+
Plugin_Stack.makeServedBucketDistribution(
|
|
1628
|
+
~name="object-stores",
|
|
1629
|
+
~servedBuckets=declaredServedBuckets,
|
|
1630
|
+
),
|
|
1631
|
+
)
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
let declaredStoreEndpoints = declaredStoreServices->Array.map(((
|
|
1635
|
+
store,
|
|
1636
|
+
keyPrefix,
|
|
1637
|
+
bucketName,
|
|
1638
|
+
uploadUrl,
|
|
1639
|
+
)) => {store, keyPrefix, bucketName, uploadUrl, baseUrl: storeServingBaseUrl})
|
|
1640
|
+
objectStoreEndpointsRef := declaredStoreEndpoints
|
|
1641
|
+
|
|
1642
|
+
// Exported unconditionally — the provisioning above already is. Their only
|
|
1643
|
+
// consumers used to sit inside `switch hostUiBundle`, which left a platform
|
|
1644
|
+
// whose UI ships from another stack holding stores it could not name.
|
|
1645
|
+
//
|
|
1646
|
+
// Omitted entirely when nothing is declared, so a deployment with no
|
|
1647
|
+
// declared store keeps a byte-identical output set.
|
|
1648
|
+
if declaredStoreEndpoints->Array.length > 0 {
|
|
1649
|
+
Pulumi.Pulumi.export(
|
|
1650
|
+
"uploadEndpoints",
|
|
1651
|
+
declaredStoreEndpoints
|
|
1652
|
+
->Array.map(e => e.uploadUrl->Pulumi.Output.apply(u => (e.store, JSON.Encode.string(u))))
|
|
1653
|
+
->Pulumi.Output.all
|
|
1654
|
+
->Pulumi.Output.apply(pairs => pairs->Dict.fromArray->JSON.Encode.object),
|
|
1655
|
+
)
|
|
1656
|
+
Pulumi.Pulumi.export(
|
|
1657
|
+
"objectStores",
|
|
1658
|
+
declaredStoreEndpoints
|
|
1659
|
+
->Array.map(e =>
|
|
1660
|
+
switch e.baseUrl {
|
|
1661
|
+
| Some(u) => u->Pulumi.Output.apply(b => (e, Some(b)))
|
|
1662
|
+
| None => Pulumi.Output.make((e, None))
|
|
1663
|
+
}
|
|
1664
|
+
)
|
|
1665
|
+
->Pulumi.Output.all
|
|
1666
|
+
->Pulumi.Output.apply(pairs =>
|
|
1667
|
+
pairs
|
|
1668
|
+
->Array.map(((e, baseUrl)) => (
|
|
1669
|
+
e.store,
|
|
1670
|
+
[
|
|
1671
|
+
("bucketName", JSON.Encode.string(e.bucketName)),
|
|
1672
|
+
("keyPrefix", JSON.Encode.string(e.keyPrefix)),
|
|
1673
|
+
]
|
|
1674
|
+
// Absent when the host shell serves the store: there the object is
|
|
1675
|
+
// addressable same-origin and a base URL would be a second, and
|
|
1676
|
+
// wrong, way to reach it.
|
|
1677
|
+
->Array.concat(
|
|
1678
|
+
switch baseUrl {
|
|
1679
|
+
| Some(b) => [("baseUrl", JSON.Encode.string(b))]
|
|
1680
|
+
| None => []
|
|
1681
|
+
},
|
|
1682
|
+
)
|
|
1683
|
+
->Dict.fromArray
|
|
1684
|
+
->JSON.Encode.object,
|
|
1685
|
+
))
|
|
1686
|
+
->Dict.fromArray
|
|
1687
|
+
->JSON.Encode.object
|
|
1688
|
+
),
|
|
1689
|
+
)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1455
1692
|
// Host UI shell deployment — opt-in via ~hostUiBundle. The shell SPA is
|
|
1456
1693
|
// hosted on its own CloudFront distribution; `config.json` is generated
|
|
1457
1694
|
// at deploy time with the resolved API endpoints, region, and Cognito
|
|
@@ -1494,17 +1731,27 @@ module MakeWithConfig = (
|
|
|
1494
1731
|
// roots its keys at, read from the service's own constant. This is the
|
|
1495
1732
|
// whole reason the store arrives as one value: the app no longer restates
|
|
1496
1733
|
// the prefix, so it cannot restate it wrongly.
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1734
|
+
//
|
|
1735
|
+
// The legacy hand-configured store is served alongside the declared ones,
|
|
1736
|
+
// not replaced by them. Refs live in an append-only event log, so the
|
|
1737
|
+
// `/uploads/…` values already written there can never be rewritten;
|
|
1738
|
+
// `uploads` is therefore treated as a store that happens to predate the
|
|
1739
|
+
// declaration, and keeps its prefix permanently.
|
|
1740
|
+
let servedBuckets = Array.concat(
|
|
1741
|
+
switch cfg.uploadBucket {
|
|
1742
|
+
| Some(store) => [
|
|
1743
|
+
{
|
|
1744
|
+
ReventlessInfra.Platform.id: store.keyPrefix,
|
|
1745
|
+
prefixes: [store.keyPrefix],
|
|
1746
|
+
bucketId: store.bucketId,
|
|
1747
|
+
bucketArn: store.bucketArn,
|
|
1748
|
+
bucketRegionalDomainName: store.bucketRegionalDomainName,
|
|
1749
|
+
},
|
|
1750
|
+
]
|
|
1751
|
+
| None => []
|
|
1752
|
+
},
|
|
1753
|
+
declaredServedBuckets,
|
|
1754
|
+
)
|
|
1508
1755
|
|
|
1509
1756
|
let {distributionUrl, bucketName} = Plugin_Stack.makeUiBundleDistribution(
|
|
1510
1757
|
~pluginId="host-ui",
|
|
@@ -1569,6 +1816,18 @@ module MakeWithConfig = (
|
|
|
1569
1816
|
| None => Pulumi.Output.make(None)
|
|
1570
1817
|
}
|
|
1571
1818
|
|
|
1819
|
+
// Per-store presign endpoints, keyed by the store's qualified name. This
|
|
1820
|
+
// is what lets an upload input bind to the store its field *declares*
|
|
1821
|
+
// rather than inferring one from the field's name — the endpoint exists
|
|
1822
|
+
// per store, so there is something concrete to bind to.
|
|
1823
|
+
//
|
|
1824
|
+
// `uploadEndpoint` (singular) stays as the legacy service's URL: a shell
|
|
1825
|
+
// built before per-store binding reads it and is unaffected.
|
|
1826
|
+
let storeUploadEndpointsOutput: Pulumi.Output.t<array<(string, string)>> =
|
|
1827
|
+
declaredStoreServices
|
|
1828
|
+
->Array.map(((qualified, _, _, url)) => url->Pulumi.Output.apply(u => (qualified, u)))
|
|
1829
|
+
->Pulumi.Output.all
|
|
1830
|
+
|
|
1572
1831
|
let configJsonContent =
|
|
1573
1832
|
(
|
|
1574
1833
|
(
|
|
@@ -1579,14 +1838,14 @@ module MakeWithConfig = (
|
|
|
1579
1838
|
)->Pulumi.Output.all4,
|
|
1580
1839
|
domainEventsEndpointOutput,
|
|
1581
1840
|
geocoderEndpointOutput,
|
|
1582
|
-
uploadEndpointOutput,
|
|
1841
|
+
(uploadEndpointOutput, storeUploadEndpointsOutput)->Pulumi.Output.all2,
|
|
1583
1842
|
)
|
|
1584
1843
|
->Pulumi.Output.all4
|
|
1585
1844
|
->Pulumi.Output.apply(((
|
|
1586
1845
|
(domainEp, platformEp, poolId, clientId),
|
|
1587
1846
|
eventsEpOpt,
|
|
1588
1847
|
geocoderEpOpt,
|
|
1589
|
-
uploadEpOpt,
|
|
1848
|
+
(uploadEpOpt, storeUploadEps),
|
|
1590
1849
|
)) => {
|
|
1591
1850
|
let fields = [
|
|
1592
1851
|
("apiEndpoint", JSON.Encode.string(domainEp)),
|
|
@@ -1623,7 +1882,25 @@ module MakeWithConfig = (
|
|
|
1623
1882
|
| Some(ep) => Array.concat(withGeocoder, [("uploadEndpoint", JSON.Encode.string(ep))])
|
|
1624
1883
|
| None => withGeocoder
|
|
1625
1884
|
}
|
|
1626
|
-
|
|
1885
|
+
// Omitted entirely when nothing is declared, so a deployment without
|
|
1886
|
+
// declared stores writes a byte-identical config.json.
|
|
1887
|
+
let withStoreUploads = switch storeUploadEps {
|
|
1888
|
+
| [] => withUpload
|
|
1889
|
+
| eps =>
|
|
1890
|
+
Array.concat(
|
|
1891
|
+
withUpload,
|
|
1892
|
+
[
|
|
1893
|
+
(
|
|
1894
|
+
"uploadEndpoints",
|
|
1895
|
+
eps
|
|
1896
|
+
->Array.map(((qualified, url)) => (qualified, JSON.Encode.string(url)))
|
|
1897
|
+
->Dict.fromArray
|
|
1898
|
+
->JSON.Encode.object,
|
|
1899
|
+
),
|
|
1900
|
+
],
|
|
1901
|
+
)
|
|
1902
|
+
}
|
|
1903
|
+
withStoreUploads->Dict.fromArray->JSON.Encode.object->JSON.stringify
|
|
1627
1904
|
})
|
|
1628
1905
|
|
|
1629
1906
|
let _ = PulumiAws.S3.BucketObject.make(
|