@reventlessdev/reventless-aws 3.0.0-alpha.253 → 3.0.0-alpha.255

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +10 -10
  3. package/src/Platform.res +134 -20
  4. package/src/Platform.res.mjs +95 -6
  5. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +14 -0
  6. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +4 -1
  7. package/src/adapter/Api/Platform_UIFragments_Lambda.res +14 -0
  8. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +4 -1
  9. package/src/adapter/Geocoder/Geocoder_AwsLocation.res +14 -0
  10. package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +4 -1
  11. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +5 -0
  12. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +3 -0
  13. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res +5 -0
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Single_Async.res.mjs +3 -0
  15. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +57 -8
  16. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +15 -3
  17. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +5 -0
  18. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +3 -0
  19. package/src/adapter/StateTopic/StateTopic_AppSync.res +14 -0
  20. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +4 -1
  21. package/src/adapter/Upload/Upload_Claim_S3.res +375 -0
  22. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +206 -0
  23. package/src/adapter/Upload/Upload_Claim_S3_Ops.res +279 -0
  24. package/src/adapter/Upload/Upload_Claim_S3_Ops.res.mjs +261 -0
  25. package/src/adapter/Upload/Upload_PendingTag.res +41 -0
  26. package/src/adapter/Upload/Upload_PendingTag.res.mjs +15 -0
  27. package/src/adapter/Upload/Upload_Presign_S3.res +26 -2
  28. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +5 -1
  29. package/src/adapter/Upload/Upload_Presign_S3_Ops.res +13 -1
  30. package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +3 -1
  31. package/src/capability/Capability_ObjectStore_S3.res +42 -1
  32. package/src/capability/Capability_ObjectStore_S3.res.mjs +19 -1
  33. package/src/components/Api/AppSync_Adapter.res +36 -0
  34. package/src/components/Api/AppSync_Adapter.res.mjs +14 -0
  35. package/src/util/Util_DeadLetterQueue.res +1 -0
  36. package/src/util/Util_DeadLetterQueue.res.mjs +6 -2
  37. package/src/util/Util_LambdaLogging.res +83 -0
  38. package/src/util/Util_LambdaLogging.res.mjs +59 -0
  39. package/src/util/Util_LogRetention.res +86 -0
  40. package/src/util/Util_LogRetention.res.mjs +43 -0
  41. package/src/util/Util_StoreLayout.res +37 -0
  42. package/src/util/Util_StoreLayout.res.mjs +18 -0
  43. package/tests/Upload_ClaimTest.res +132 -0
  44. package/tests/Upload_ClaimTest.res.mjs +101 -0
  45. package/tests/Util_LogRetentionTest.res +122 -0
  46. package/tests/Util_LogRetentionTest.res.mjs +95 -0
  47. package/tests/Util_StoreLayoutTest.res +63 -0
  48. package/tests/Util_StoreLayoutTest.res.mjs +42 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
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.255 (2026-08-02)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** apply tier LOG_LEVEL + managed log groups to bespoke Lambda builders ([8814627](https://github.com/ReventlessDev/reventless-core/commit/8814627a200199616f407f4011d4e0b0e1895397))
11
+
12
+
13
+ # 3.0.0-alpha.254 (2026-08-02)
14
+
15
+ ### Features
16
+
17
+ * **aws,core,doc:** environment-tiered CloudWatch log retention and levels ([8032023](https://github.com/ReventlessDev/reventless-core/commit/803202305a7d352484b8df5e58df03c92ca58b5f))
18
+ * **aws,core,spec,seed-aws:** expire uploads nobody committed a reference to ([f63e84c](https://github.com/ReventlessDev/reventless-core/commit/f63e84c1a11cc350b799a6f69a2e7427cf1ea6e9))
19
+
20
+
6
21
  # 3.0.0-alpha.253 (2026-08-01)
7
22
 
8
23
  * feat(aws,core,spec)!: qualify store prefixes by plugin and refuse name collisions ([da39405](https://github.com/ReventlessDev/reventless-core/commit/da394059d9f8f981bf7adc79e2c1ce2b429e0267))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.253",
3
+ "version": "3.0.0-alpha.255",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -11,18 +11,18 @@
11
11
  "@aws-sdk/s3-request-presigner": "3.970.0",
12
12
  "sury": "11.0.0-alpha.4",
13
13
  "uuid": "^13.0.0",
14
- "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.0",
14
+ "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.1",
15
15
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
16
- "@reventlessdev/rescript-node": "2.0.0-alpha.0",
17
16
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.62",
19
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
17
+ "@reventlessdev/rescript-node": "2.0.0-alpha.0",
18
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.63",
20
19
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
21
- "@reventlessdev/reventless-core": "3.0.0-alpha.200",
22
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.64",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.117",
24
- "@reventlessdev/reventless-spec": "3.0.0-alpha.92",
25
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30"
20
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.201",
22
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.118",
23
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.65",
24
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.93"
26
26
  },
27
27
  "devDependencies": {
28
28
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -157,6 +157,58 @@ module MakeWithConfig = (
157
157
  ->Pulumi.Config.get("stack")
158
158
  ->Option.map(stack => Pulumi.StackReference.make(stack))
159
159
 
160
+ // The declared object stores, as the upload claimer needs them: qualified
161
+ // name, physical bucket, served prefix.
162
+ //
163
+ // Stores are provisioned by the *platform* deploy; the claimer is wired into
164
+ // the *plugin* stack, because that is where the event log streams it reads
165
+ // exist. So the two halves meet here — across the stack reference in plugin
166
+ // mode, and through the platform's own in-process record when platform and
167
+ // plugin are one program.
168
+ //
169
+ // Read lazily (a thunk, not a value) for the monolithic case: `deployPlatform`
170
+ // fills `objectStoreEndpointsRef` as it provisions, and this is called during
171
+ // a later plugin build.
172
+ let claimStores = (): Pulumi.Output.t<array<Upload_Claim_S3.storeConfig>> =>
173
+ switch platformStackRef {
174
+ | Some(stackRef) =>
175
+ // The `objectStores` export: `{ "<plugin>.<store>": {bucketName, keyPrefix} }`.
176
+ // A platform that provisions none omits the export entirely, which reads
177
+ // as "no store to claim in" — the claimer then builds nothing, rather
178
+ // than building something that can reach nothing.
179
+ (stackRef->Pulumi.StackReference.getOutput("objectStores"): Pulumi.Output.t<option<JSON.t>>)
180
+ ->Pulumi.Output.apply(objectStores =>
181
+ objectStores
182
+ ->Option.flatMap(JSON.Decode.object)
183
+ ->Option.getOr(Dict.make())
184
+ ->Dict.toArray
185
+ ->Array.filterMap(((qualified, v)) =>
186
+ v
187
+ ->JSON.Decode.object
188
+ ->Option.flatMap(o =>
189
+ switch (
190
+ o->Dict.get("bucketName")->Option.flatMap(JSON.Decode.string),
191
+ o->Dict.get("keyPrefix")->Option.flatMap(JSON.Decode.string),
192
+ ) {
193
+ | (Some(bucketName), Some(servedPrefix)) =>
194
+ Some({Upload_Claim_S3.qualified: qualified, bucketName, servedPrefix})
195
+ | _ => None
196
+ }
197
+ )
198
+ )
199
+ )
200
+ | None =>
201
+ objectStoreEndpointsRef.contents
202
+ ->Array.map(e =>
203
+ e.bucketName->Pulumi.Output.apply(bucketName => {
204
+ Upload_Claim_S3.qualified: e.store,
205
+ bucketName,
206
+ servedPrefix: e.keyPrefix,
207
+ })
208
+ )
209
+ ->Pulumi.Output.all
210
+ }
211
+
160
212
  // ── Merged-mode source SDL assembly (merged-api plan, Phase 3) ────────────
161
213
  // Empty base fragment — no types, no mutations, no queries. Used by the
162
214
  // plugin Api in split mode (so plugin schema has no core fields) and as the
@@ -172,7 +224,7 @@ module MakeWithConfig = (
172
224
  // The upload service (route B) lives on the domain base, not the Admin-gated admin
173
225
  // base: `Upload_Presign`/`Upload_Release` take the domain API's default
174
226
  // `AllowAuthenticated` auth so any authenticated user can mint/release their own
175
- // uploads. See [docs/plans/upload-release-path.md] § "Which API". (Split mode only —
227
+ // uploads. See [docs/plans/done/upload-release-path.md] § "Which API". (Split mode only —
176
228
  // unified mode carries the admin base here instead; it declares no stores in practice,
177
229
  // so the upload resolvers below are never created there.)
178
230
  let domainBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
@@ -763,7 +815,7 @@ module MakeWithConfig = (
763
815
  // Only active when the Events API resource exists (platform/monolithic mode).
764
816
  subscriptionInfraHook: ?domainEventsApiOpt->Option.map(eventsApi =>
765
817
  (params: ReventlessCore.Plugin_Helpers.subscriptionInfraParams) => {
766
- let {allQueryDbs, allEventTopics, eventLogEntries, opts} = params
818
+ let {pluginName, allQueryDbs, allEventTopics, eventLogEntries, opts} = params
767
819
  let customOpts =
768
820
  opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
769
821
  // StateTopic Lambda per stream-enabled QueryDb — publishes row changes
@@ -799,19 +851,34 @@ module MakeWithConfig = (
799
851
  let isSns =
800
852
  EventTopicPublisher_SNS.snsRegistry->Set.has(entry.displayName) ||
801
853
  EventTopicPublisher_SNS.snsRegistry->Set.has(entry.busKey)
802
- let eventTopicOutputs =
803
- isSns
804
- ? allEventTopics
805
- ->Dict.get(entry.displayName)
806
- ->Option.orElse(allEventTopics->Dict.get(entry.busKey))
807
- : None
808
- eventTopicOutputs->Option.forEach(outputs =>
809
- EventLogSubscription_AppSync.make(
810
- ~name=entry.displayName,
811
- ~topicName=entry.displayName,
854
+ let topicOutputs =
855
+ allEventTopics
856
+ ->Dict.get(entry.displayName)
857
+ ->Option.orElse(allEventTopics->Dict.get(entry.busKey))
858
+ isSns
859
+ ? topicOutputs->Option.forEach(outputs =>
860
+ EventLogSubscription_AppSync.make(
861
+ ~name=entry.displayName,
862
+ ~topicName=entry.displayName,
863
+ ~eventTopicOutputs=outputs,
864
+ ~eventsApi,
865
+ ~opts=customOpts,
866
+ )
867
+ )
868
+ : ()
869
+ // Register this event log with the upload claimer if — and only if —
870
+ // its events declare a `@storageRef` field. An event log that declares
871
+ // none is not registered, so a platform whose plugins declare no store
872
+ // provisions no claimer at all. `isSns` is the same synchronous answer
873
+ // the subscription above turns on, so the two cannot disagree about
874
+ // which channel an event log publishes through.
875
+ topicOutputs->Option.forEach(outputs =>
876
+ Upload_Claim_S3.make(
877
+ ~plugin=pluginName,
878
+ ~eventLogName=entry.displayName,
879
+ ~eventSchema=entry.eventSchema,
812
880
  ~eventTopicOutputs=outputs,
813
- ~eventsApi,
814
- ~opts=customOpts,
881
+ ~isStreamBacked=!isSns,
815
882
  )
816
883
  )
817
884
  })
@@ -822,6 +889,9 @@ module MakeWithConfig = (
822
889
  // empty because the hook hasn't run yet. Admin's hook fires synchronously,
823
890
  // but the call site is unified here so admin and plugins share one path.
824
891
  StateTopic_AppSync.finish(~eventsApi, ~opts={})
892
+ // Same timing constraint, same reason: the claimer's registry is filled
893
+ // by the loop just above and drained here.
894
+ Upload_Claim_S3.finish(~plugin=pluginName, ~stores=claimStores, ~opts=customOpts)
825
895
  }
826
896
  ),
827
897
  }
@@ -1063,12 +1133,15 @@ module MakeWithConfig = (
1063
1133
  // omitted (byte-identical config.json).
1064
1134
  geocoderPlaceIndex?: ReventlessInfra.Platform.geocoderIndex,
1065
1135
  // Optional object store for direct-to-S3 uploads, as returned by
1066
- // `Capability_ObjectStore_S3.make`. When set, the deploy provisions the
1067
- // presign service (Upload_Presign_S3) against it, threads that service's URL
1068
- // into config.json as `uploadEndpoint`, and fronts the store read-only on
1069
- // the host-shell CloudFront distribution under the presign service's own
1070
- // prefix (a `{prefix}/*` ordered cache behavior + origin + scoped
1071
- // BucketPolicy, private, OAC-read).
1136
+ // `Capability_ObjectStore_S3.make`. When set, the deploy registers it with
1137
+ // the platform's one Upload service (`Upload_Presign_S3`) under the `uploads`
1138
+ // store key, and fronts the store read-only on the host-shell CloudFront
1139
+ // distribution under that prefix (a `{prefix}/*` ordered cache behavior +
1140
+ // origin + scoped BucketPolicy, private, OAC-read).
1141
+ //
1142
+ // No endpoint is written into config.json: under route B a caller mints
1143
+ // through the domain API's `Upload_Presign` mutation, naming the store, so
1144
+ // there is no per-store URL to publish or to resolve.
1072
1145
  //
1073
1146
  // One field replaces the former `enableUploads` / `uploadBucketName` /
1074
1147
  // `servedBuckets` trio, which were three ways of saying one thing and left
@@ -1584,6 +1657,46 @@ module MakeWithConfig = (
1584
1657
  // policy and one CDN origin however many stores live in it.
1585
1658
  let storeBuckets: Dict.t<ReventlessInfra.Platform.objectStore> = Dict.make()
1586
1659
  let storeBucketOrder: array<string> = []
1660
+
1661
+ // Which stores expire their never-claimed uploads, grouped by the bucket
1662
+ // they live in. Computed before any bucket is created, because a bucket
1663
+ // carries its lifecycle rules from birth and a shared-layout bucket is
1664
+ // created once for several stores — the first store through the loop below
1665
+ // would otherwise be the only one whose setting was ever read.
1666
+ //
1667
+ // Off unless a deployment names the store. See `Util_StoreLayout.pendingExpiryFor`
1668
+ // for why this is a deployment's setting and not a declaration's, and why
1669
+ // there is deliberately no default.
1670
+ let pendingExpiryConfig = Util_LocalConfig.get("pendingUploadExpiryDays")
1671
+ let pendingExpiryByBucket: Dict.t<array<Capability_ObjectStore_S3.pendingExpiry>> = Dict.make()
1672
+ declaredStores->Array.forEach(((plugin, store)) =>
1673
+ Util_StoreLayout.pendingExpiryFor(
1674
+ ~config=pendingExpiryConfig,
1675
+ ~store=`${plugin}.${store}`,
1676
+ )->Option.forEach(days => {
1677
+ let bucketName = Util_StoreLayout.bucketNameFor(
1678
+ ~layout=storeLayout,
1679
+ ~stack=stackName,
1680
+ ~plugin,
1681
+ ~store,
1682
+ )
1683
+ let prefix = Util_StoreLayout.keyPrefixFor(~plugin, ~store)
1684
+ pendingExpiryByBucket->Dict.set(
1685
+ bucketName,
1686
+ pendingExpiryByBucket
1687
+ ->Dict.get(bucketName)
1688
+ ->Option.getOr([])
1689
+ ->Array.concat([{Capability_ObjectStore_S3.prefix, days}]),
1690
+ )
1691
+ log.info(
1692
+ ~comp="Platform:deployPlatform",
1693
+ `object store ${plugin}.${store}: never-claimed uploads under ${prefix}/ expire after ` ++
1694
+ `${days->Int.toString} days. Claimed objects and objects minted before the claim ` ++
1695
+ `component existed carry no pending tag and are outside the rule.`,
1696
+ )
1697
+ })
1698
+ )
1699
+
1587
1700
  let declaredStoreServices = declaredStores->Array.map(((plugin, store)) => {
1588
1701
  let bucketName = Util_StoreLayout.bucketNameFor(
1589
1702
  ~layout=storeLayout,
@@ -1608,6 +1721,7 @@ module MakeWithConfig = (
1608
1721
  // disposable stack would leak exactly the buckets sharing exists to
1609
1722
  // save. The two settings are opposite faces of one decision.
1610
1723
  ~forceDestroy=storeProtection == Unprotected,
1724
+ ~expirePending=pendingExpiryByBucket->Dict.get(bucketName)->Option.getOr([]),
1611
1725
  )
1612
1726
  storeBuckets->Dict.set(bucketName, b)
1613
1727
  storeBucketOrder->Array.push(bucketName)
@@ -35,6 +35,7 @@ import * as EventLogBackend$ReventlessAws from "./adapter/EventLog/EventLogBacke
35
35
  import * as Platform_Admin$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin.res.mjs";
36
36
  import * as PluginBehavior$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginBehavior.res.mjs";
37
37
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
38
+ import * as Upload_Claim_S3$ReventlessAws from "./adapter/Upload/Upload_Claim_S3.res.mjs";
38
39
  import * as PgProjectionFeed$ReventlessAws from "./adapter/Postgres/PgProjectionFeed.res.mjs";
39
40
  import * as Util_LocalConfig$ReventlessAws from "./util/Util_LocalConfig.res.mjs";
40
41
  import * as Util_StoreLayout$ReventlessAws from "./util/Util_StoreLayout.res.mjs";
@@ -156,6 +157,30 @@ function MakeWithConfig(Config) {
156
157
  contents: "Domain"
157
158
  };
158
159
  let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
160
+ let claimStores = () => {
161
+ if (platformStackRef !== undefined) {
162
+ return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
163
+ let qualified = param[0];
164
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(param[1]), o => {
165
+ let match = Stdlib_Option.flatMap(o["bucketName"], Stdlib_JSON.Decode.string);
166
+ let match$1 = Stdlib_Option.flatMap(o["keyPrefix"], Stdlib_JSON.Decode.string);
167
+ if (match !== undefined && match$1 !== undefined) {
168
+ return {
169
+ qualified: qualified,
170
+ bucketName: match,
171
+ servedPrefix: match$1
172
+ };
173
+ }
174
+ });
175
+ }));
176
+ } else {
177
+ return Pulumi.all(objectStoreEndpointsRef.contents.map(e => e.bucketName.apply(bucketName => ({
178
+ qualified: e.store,
179
+ bucketName: bucketName,
180
+ servedPrefix: e.keyPrefix
181
+ }))));
182
+ }
183
+ };
159
184
  let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
160
185
  types: Platform_AdminApi$ReventlessCore.uploadTypes,
161
186
  mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
@@ -457,6 +482,7 @@ function MakeWithConfig(Config) {
457
482
  let hooks_subscriptionInfraHook = Stdlib_Option.map(domainEventsApiOpt, eventsApi => (params => {
458
483
  let allEventTopics = params.allEventTopics;
459
484
  let allQueryDbs = params.allQueryDbs;
485
+ let pluginName = params.pluginName;
460
486
  let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(params.opts);
461
487
  Stdlib_Dict.forEachWithKey(allQueryDbs, (_queryDbOutputs, readModelName) => {
462
488
  if (!QueryDbStorage_DynamoDbStream$ReventlessAws.streamRegistry.has(readModelName)) {
@@ -467,10 +493,14 @@ function MakeWithConfig(Config) {
467
493
  });
468
494
  params.eventLogEntries.forEach(entry => {
469
495
  let isSns = EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.displayName) || EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.busKey);
470
- let eventTopicOutputs = isSns ? Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]) : undefined;
471
- Stdlib_Option.forEach(eventTopicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
496
+ let topicOutputs = Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]);
497
+ if (isSns) {
498
+ Stdlib_Option.forEach(topicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
499
+ }
500
+ Stdlib_Option.forEach(topicOutputs, outputs => Upload_Claim_S3$ReventlessAws.make(pluginName, entry.displayName, entry.eventSchema, outputs, !isSns));
472
501
  });
473
502
  StateTopic_AppSync$ReventlessAws.finish(eventsApi, {});
503
+ Upload_Claim_S3$ReventlessAws.finish(pluginName, claimStores, undefined, customOpts);
474
504
  }));
475
505
  let hooks_preResolversSchemaHook = (name, version, pluginFragment) => {
476
506
  let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
@@ -925,6 +955,21 @@ function MakeWithConfig(Config) {
925
955
  }
926
956
  let storeBuckets = {};
927
957
  let storeBucketOrder = [];
958
+ let pendingExpiryConfig = Util_LocalConfig$ReventlessAws.get("pendingUploadExpiryDays");
959
+ let pendingExpiryByBucket = {};
960
+ declaredStores.forEach(param => {
961
+ let store = param[1];
962
+ let plugin = param[0];
963
+ Stdlib_Option.forEach(Util_StoreLayout$ReventlessAws.pendingExpiryFor(pendingExpiryConfig, plugin + `.` + store), days => {
964
+ let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
965
+ let prefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store);
966
+ pendingExpiryByBucket[bucketName] = Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []).concat([{
967
+ prefix: prefix,
968
+ days: days
969
+ }]);
970
+ log.info("Platform:deployPlatform", undefined, `object store ` + plugin + `.` + store + `: never-claimed uploads under ` + prefix + `/ expire after ` + (days.toString() + ` days. Claimed objects and objects minted before the claim `) + `component existed carry no pending tag and are outside the rule.`);
971
+ });
972
+ });
928
973
  let declaredStoreServices = declaredStores.map(param => {
929
974
  let store = param[1];
930
975
  let plugin = param[0];
@@ -937,7 +982,7 @@ function MakeWithConfig(Config) {
937
982
  } else {
938
983
  let tmp;
939
984
  tmp = storeLayout === "PerStore" ? plugin : undefined;
940
- let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", undefined, undefined);
985
+ let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []), undefined, undefined);
941
986
  storeBuckets[bucketName] = b$1;
942
987
  storeBucketOrder.push(bucketName);
943
988
  bucket = b$1;
@@ -1324,6 +1369,30 @@ function Make($star) {
1324
1369
  contents: "Domain"
1325
1370
  };
1326
1371
  let platformStackRef = Stdlib_Option.map(new Pulumi.Config("platform").get("stack"), stack => new Pulumi.StackReference(stack));
1372
+ let claimStores = () => {
1373
+ if (platformStackRef !== undefined) {
1374
+ return Primitive_option.valFromOption(platformStackRef).getOutput("objectStores").apply(objectStores => Stdlib_Array.filterMap(Object.entries(Stdlib_Option.getOr(Stdlib_Option.flatMap(objectStores, Stdlib_JSON.Decode.object), {})), param => {
1375
+ let qualified = param[0];
1376
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(param[1]), o => {
1377
+ let match = Stdlib_Option.flatMap(o["bucketName"], Stdlib_JSON.Decode.string);
1378
+ let match$1 = Stdlib_Option.flatMap(o["keyPrefix"], Stdlib_JSON.Decode.string);
1379
+ if (match !== undefined && match$1 !== undefined) {
1380
+ return {
1381
+ qualified: qualified,
1382
+ bucketName: match,
1383
+ servedPrefix: match$1
1384
+ };
1385
+ }
1386
+ });
1387
+ }));
1388
+ } else {
1389
+ return Pulumi.all(objectStoreEndpointsRef.contents.map(e => e.bucketName.apply(bucketName => ({
1390
+ qualified: e.store,
1391
+ bucketName: bucketName,
1392
+ servedPrefix: e.keyPrefix
1393
+ }))));
1394
+ }
1395
+ };
1327
1396
  let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
1328
1397
  types: Platform_AdminApi$ReventlessCore.uploadTypes,
1329
1398
  mutations: Platform_AdminApi$ReventlessCore.uploadMutationFields,
@@ -1625,6 +1694,7 @@ function Make($star) {
1625
1694
  let hooks_subscriptionInfraHook = Stdlib_Option.map(domainEventsApiOpt, eventsApi => (params => {
1626
1695
  let allEventTopics = params.allEventTopics;
1627
1696
  let allQueryDbs = params.allQueryDbs;
1697
+ let pluginName = params.pluginName;
1628
1698
  let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(params.opts);
1629
1699
  Stdlib_Dict.forEachWithKey(allQueryDbs, (_queryDbOutputs, readModelName) => {
1630
1700
  if (!QueryDbStorage_DynamoDbStream$ReventlessAws.streamRegistry.has(readModelName)) {
@@ -1635,10 +1705,14 @@ function Make($star) {
1635
1705
  });
1636
1706
  params.eventLogEntries.forEach(entry => {
1637
1707
  let isSns = EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.displayName) || EventTopicPublisher_SNS$ReventlessAws.snsRegistry.has(entry.busKey);
1638
- let eventTopicOutputs = isSns ? Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]) : undefined;
1639
- Stdlib_Option.forEach(eventTopicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
1708
+ let topicOutputs = Stdlib_Option.orElse(allEventTopics[entry.displayName], allEventTopics[entry.busKey]);
1709
+ if (isSns) {
1710
+ Stdlib_Option.forEach(topicOutputs, outputs => EventLogSubscription_AppSync$ReventlessAws.make(entry.displayName, entry.displayName, outputs, eventsApi, customOpts));
1711
+ }
1712
+ Stdlib_Option.forEach(topicOutputs, outputs => Upload_Claim_S3$ReventlessAws.make(pluginName, entry.displayName, entry.eventSchema, outputs, !isSns));
1640
1713
  });
1641
1714
  StateTopic_AppSync$ReventlessAws.finish(eventsApi, {});
1715
+ Upload_Claim_S3$ReventlessAws.finish(pluginName, claimStores, undefined, customOpts);
1642
1716
  }));
1643
1717
  let hooks_preResolversSchemaHook = (name, version, pluginFragment) => {
1644
1718
  let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
@@ -2070,6 +2144,21 @@ function Make($star) {
2070
2144
  }
2071
2145
  let storeBuckets = {};
2072
2146
  let storeBucketOrder = [];
2147
+ let pendingExpiryConfig = Util_LocalConfig$ReventlessAws.get("pendingUploadExpiryDays");
2148
+ let pendingExpiryByBucket = {};
2149
+ declaredStores.forEach(param => {
2150
+ let store = param[1];
2151
+ let plugin = param[0];
2152
+ Stdlib_Option.forEach(Util_StoreLayout$ReventlessAws.pendingExpiryFor(pendingExpiryConfig, plugin + `.` + store), days => {
2153
+ let bucketName = Util_StoreLayout$ReventlessAws.bucketNameFor(storeLayout, stackName, plugin, store);
2154
+ let prefix = Util_StoreLayout$ReventlessAws.keyPrefixFor(plugin, store);
2155
+ pendingExpiryByBucket[bucketName] = Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []).concat([{
2156
+ prefix: prefix,
2157
+ days: days
2158
+ }]);
2159
+ log.info("Platform:deployPlatform", undefined, `object store ` + plugin + `.` + store + `: never-claimed uploads under ` + prefix + `/ expire after ` + (days.toString() + ` days. Claimed objects and objects minted before the claim `) + `component existed carry no pending tag and are outside the rule.`);
2160
+ });
2161
+ });
2073
2162
  let declaredStoreServices = declaredStores.map(param => {
2074
2163
  let store = param[1];
2075
2164
  let plugin = param[0];
@@ -2082,7 +2171,7 @@ function Make($star) {
2082
2171
  } else {
2083
2172
  let tmp;
2084
2173
  tmp = storeLayout === "PerStore" ? plugin : undefined;
2085
- let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", undefined, undefined);
2174
+ let b$1 = Capability_ObjectStore_S3$ReventlessAws.make(bucketName, keyPrefix, tmp, storeProtection === "Protected", storeProtection === "Unprotected", Stdlib_Option.getOr(pendingExpiryByBucket[bucketName], []), undefined, undefined);
2086
2175
  storeBuckets[bucketName] = b$1;
2087
2176
  storeBucketOrder.push(bucketName);
2088
2177
  bucket = b$1;
@@ -120,6 +120,7 @@ let make = (
120
120
  ("ADMIN_ENTRY_JSON", adminEntryJson->Pulumi.Input.make),
121
121
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
122
122
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
123
+ Util_LambdaLogging.logLevelEntry(),
123
124
  ]),
124
125
  }: Lambda.Function.functionEnvironment
125
126
  )->Pulumi.Input.make,
@@ -127,6 +128,19 @@ let make = (
127
128
  ~opts,
128
129
  )
129
130
 
131
+ Util_LambdaLogging.makeManagedLogGroup(
132
+ ~name=name ++ "Lambda",
133
+ ~lambdaName=lambda.name,
134
+ ~tags=AWS.Tags.make(
135
+ ~name=name ++ "LambdaLogGroup",
136
+ ~kind=ReventlessCore.ComponentType.Platform,
137
+ ~role=Logs,
138
+ ~scope=Platform,
139
+ ),
140
+ ~opts,
141
+ (),
142
+ )
143
+
130
144
  let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
131
145
  ~name=name ++ "DataSource",
132
146
  ~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
@@ -11,6 +11,7 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
+ import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
14
15
  import * as AppSync_Resolver_Native$ReventlessAws from "./AppSync_Resolver_Native.res.mjs";
15
16
  import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
16
17
  import * as Platform_ComponentDefinitionsApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_ComponentDefinitionsApi.res.mjs";
@@ -86,11 +87,13 @@ function make(api, pluginReadModelTableName, opts) {
86
87
  [
87
88
  "ESM_FALLBACK_DIRS",
88
89
  Util_Bundle$ReventlessAws.esmFallbackDirs
89
- ]
90
+ ],
91
+ Util_LambdaLogging$ReventlessAws.logLevelEntry()
90
92
  ])
91
93
  },
92
94
  sourceCodeHash: match.sourceCodeHash
93
95
  }, opts$1);
96
+ Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", lambda.name, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
94
97
  let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
95
98
  Pulumi.all([
96
99
  lambda.arn,
@@ -111,6 +111,7 @@ let make = (
111
111
  ("UI_FRAGMENT_RM_TABLE", uiFragmentRegistryTableName->Pulumi.Output.asInput),
112
112
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
113
113
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
114
+ Util_LambdaLogging.logLevelEntry(),
114
115
  ]),
115
116
  }: Lambda.Function.functionEnvironment
116
117
  )->Pulumi.Input.make,
@@ -118,6 +119,19 @@ let make = (
118
119
  ~opts,
119
120
  )
120
121
 
122
+ Util_LambdaLogging.makeManagedLogGroup(
123
+ ~name=name ++ "Lambda",
124
+ ~lambdaName=lambda.name,
125
+ ~tags=AWS.Tags.make(
126
+ ~name=name ++ "LambdaLogGroup",
127
+ ~kind=ReventlessCore.ComponentType.Platform,
128
+ ~role=Logs,
129
+ ~scope=Platform,
130
+ ),
131
+ ~opts,
132
+ (),
133
+ )
134
+
121
135
  let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
122
136
  ~name=name ++ "DataSource",
123
137
  ~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
@@ -11,6 +11,7 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
+ import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
14
15
  import * as AppSync_Resolver_Native$ReventlessAws from "./AppSync_Resolver_Native.res.mjs";
15
16
 
16
17
  let resolverCode = `
@@ -79,11 +80,13 @@ function make(api, uiFragmentRegistryTableName, schemaReady, opts) {
79
80
  [
80
81
  "ESM_FALLBACK_DIRS",
81
82
  Util_Bundle$ReventlessAws.esmFallbackDirs
82
- ]
83
+ ],
84
+ Util_LambdaLogging$ReventlessAws.logLevelEntry()
83
85
  ])
84
86
  },
85
87
  sourceCodeHash: match.sourceCodeHash
86
88
  }, opts$1);
89
+ Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", lambda.name, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
87
90
  let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
88
91
  Pulumi.all([
89
92
  lambda.arn,
@@ -122,6 +122,7 @@ let make = (
122
122
  ("PLACE_INDEX_NAME", placeIndexName),
123
123
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
124
124
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
125
+ Util_LambdaLogging.logLevelEntry(),
125
126
  ]),
126
127
  }: Lambda.Function.functionEnvironment
127
128
  )->Pulumi.Input.make,
@@ -129,6 +130,19 @@ let make = (
129
130
  ~opts?,
130
131
  )
131
132
 
133
+ Util_LambdaLogging.makeManagedLogGroup(
134
+ ~name=serviceName,
135
+ ~lambdaName=lambda.name,
136
+ ~tags=AWS.Tags.make(
137
+ ~name=serviceName ++ "LogGroup",
138
+ ~kind=ReventlessCore.ComponentType.Platform,
139
+ ~role=Logs,
140
+ ~scope=Platform,
141
+ ),
142
+ ~opts?,
143
+ (),
144
+ )
145
+
132
146
  let functionUrl = FunctionUrl.make(
133
147
  ~name=`${serviceName}Url`,
134
148
  ~args={
@@ -11,6 +11,7 @@ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
+ import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
14
15
 
15
16
  function make(placeIndexName, corsOriginsOpt, opts) {
16
17
  let corsOrigins = corsOriginsOpt !== undefined ? corsOriginsOpt : ["*"];
@@ -73,11 +74,13 @@ function make(placeIndexName, corsOriginsOpt, opts) {
73
74
  [
74
75
  "ESM_FALLBACK_DIRS",
75
76
  Util_Bundle$ReventlessAws.esmFallbackDirs
76
- ]
77
+ ],
78
+ Util_LambdaLogging$ReventlessAws.logLevelEntry()
77
79
  ])
78
80
  },
79
81
  sourceCodeHash: match.sourceCodeHash
80
82
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
83
+ Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(serviceName, lambda.name, AWS_Tags$ReventlessAws.make(serviceName + "LogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
81
84
  let functionUrl = new (Aws.lambda.FunctionUrl)(serviceName + `Url`, {
82
85
  authorizationType: "NONE",
83
86
  functionName: lambda.name,
@@ -298,6 +298,11 @@ let finish = () =>
298
298
  }
299
299
  })
300
300
  )
301
+ // First-class log-level override → LOG_LEVEL, winning over any generic
302
+ // envVars entry. When unset, makeFromCodeAsset applies the tier default.
303
+ cfg.logLevel->Option.forEach(level =>
304
+ envVars->Dict.set("LOG_LEVEL", level->Pulumi.Output.make->Pulumi.Output.asInput)
305
+ )
301
306
 
302
307
  // Build AssetArchive: static re-export + user packages
303
308
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
@@ -197,6 +197,9 @@ function finish() {
197
197
  return;
198
198
  }
199
199
  }));
200
+ Stdlib_Option.forEach(cfg.logLevel, level => {
201
+ envVars["LOG_LEVEL"] = Pulumi.output(level);
202
+ });
200
203
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AggregateEntryPoint.mjs", packageDirs, undefined);
201
204
  Stdlib_Option.forEach(cfg.sqsBatchSize, CommandTopicChannel_SQS_Sync$ReventlessAws.setBatchSize);
202
205
  let vpcConfig = pgSelection !== undefined ? pgSelection.securityGroupId.apply(sgId => ({
@@ -276,6 +276,11 @@ let finish = () =>
276
276
  }
277
277
  })
278
278
  )
279
+ // First-class log-level override → LOG_LEVEL, winning over any generic
280
+ // envVars entry. When unset, makeFromCodeAsset applies the tier default.
281
+ cfg.logLevel->Option.forEach(level =>
282
+ envVars->Dict.set("LOG_LEVEL", level->Pulumi.Output.make->Pulumi.Output.asInput)
283
+ )
279
284
 
280
285
  // Build AssetArchive: static re-export + user packages
281
286
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(