@reventlessdev/reventless-core 3.0.0-alpha.199 → 3.0.0-alpha.201

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 CHANGED
@@ -3,6 +3,37 @@
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.201 (2026-08-02)
7
+
8
+ ### Features
9
+
10
+ * **aws,core,doc:** environment-tiered CloudWatch log retention and levels ([8032023](https://github.com/ReventlessDev/reventless-core/commit/803202305a7d352484b8df5e58df03c92ca58b5f))
11
+ * **aws,core,spec,seed-aws:** expire uploads nobody committed a reference to ([f63e84c](https://github.com/ReventlessDev/reventless-core/commit/f63e84c1a11cc350b799a6f69a2e7427cf1ea6e9))
12
+
13
+
14
+ # 3.0.0-alpha.200 (2026-08-01)
15
+
16
+ * feat(aws,core,spec)!: qualify store prefixes by plugin and refuse name collisions ([da39405](https://github.com/ReventlessDev/reventless-core/commit/da394059d9f8f981bf7adc79e2c1ce2b429e0267))
17
+ * feat(core,aws)!: name task buckets for their plugin and task ([05203cb](https://github.com/ReventlessDev/reventless-core/commit/05203cb3b0b019401a6f32cf78109f397b6799d5))
18
+ ### Features
19
+
20
+ * **spec:** a coordinate is one declared point, not two fields and a name guess ([bfe2f90](https://github.com/ReventlessDev/reventless-core/commit/bfe2f90241492422d6c242e3f50e31b81ed2a010))
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ * objects minted under the old bare prefix are orphaned and their
25
+ refs unresolvable. The migration is `seed:reset` for the owning plugin, then
26
+ re-seed. Legacy-prefix grandfathering was considered and deliberately dropped —
27
+ it would have added a permanent prefix SET across the deploy argument, both store
28
+ configs, the presign IAM fan-out, the release scope check and the stack output,
29
+ to spare a disposable stack one wipe.
30
+ * a bucket name change is a replace. TaskBucket_S3 now sets
31
+ forceDestroy on disposable stacks (Util_StoreLayout.protectionFor), so pr-*
32
+ stacks recreate cleanly; protected stacks must have the task bucket drained by
33
+ hand before the first deploy after this, or the replace fails BucketNotEmpty.
34
+
35
+
36
+
6
37
  # 3.0.0-alpha.199 (2026-08-01)
7
38
 
8
39
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.199",
3
+ "version": "3.0.0-alpha.201",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -28,22 +28,22 @@
28
28
  "dependencies": {
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.0",
31
32
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
33
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
34
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
35
  "@reventlessdev/rescript-node": "2.0.0-alpha.0",
35
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.0",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.0",
38
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.118",
38
39
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.116",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.91",
41
- "@reventlessdev/reventless-interop": "3.0.0-alpha.30"
40
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.93"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
45
45
  "sury-ppx": "11.0.0-alpha.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.59"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.60"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
@@ -200,6 +200,11 @@ type commandHandlerConfig = {
200
200
  sqsBatchSize?: int,
201
201
  ephemeralStorageMb?: int,
202
202
  logRetentionDays?: int,
203
+ // Minimum log level for this handler ("silent" | "error" | "warn" | "info" |
204
+ // "debug"), mapped onto the `LOG_LEVEL` env var the logger reads. Overrides the
205
+ // per-environment tier default. Transport-neutral: the in-memory platform reads
206
+ // it the same way via the env var.
207
+ logLevel?: string,
203
208
  envVars?: dict<string>,
204
209
  }
205
210
 
@@ -220,5 +225,8 @@ module CommandHandlerDefaults = {
220
225
  let timeout = 30
221
226
  let sqsBatchSize = 10
222
227
  let ephemeralStorageMb = 512
223
- let logRetentionDays = 7
228
+ // No `logRetentionDays` / `logLevel` default here: both are resolved per
229
+ // environment tier by the AWS platform (`Util_LogRetention`), not by a flat
230
+ // framework constant. An unset field means "follow the stack's tier", which a
231
+ // single number could not express.
224
232
  }
@@ -48,8 +48,7 @@ let CommandHandlerDefaults = {
48
48
  memorySize: 1024,
49
49
  timeout: 30,
50
50
  sqsBatchSize: 10,
51
- ephemeralStorageMb: 512,
52
- logRetentionDays: 7
51
+ ephemeralStorageMb: 512
53
52
  };
54
53
 
55
54
  export {
@@ -309,7 +309,7 @@ module Make = (
309
309
  })
310
310
  let eventLogEntries = Array.concat(eventLogEntriesFromAggregates, dcbResult.eventLogEntries)
311
311
  Config.hooks.subscriptionInfraHook->Option.forEach(hook =>
312
- hook({allQueryDbs, allEventTopics, eventLogEntries, opts})
312
+ hook({pluginName: name, allQueryDbs, allEventTopics, eventLogEntries, opts})
313
313
  )
314
314
 
315
315
  let queryEngine = QueryEngineAdapter.make(allQueryDbs)
@@ -150,6 +150,7 @@ function Make(RuntimeEnvironment) {
150
150
  }));
151
151
  let eventLogEntries = eventLogEntriesFromAggregates.concat(dcbResult.eventLogEntries);
152
152
  Stdlib_Option.forEach(Config.hooks.subscriptionInfraHook, hook => hook({
153
+ pluginName: Platform_Admin_Structure$ReventlessCore.pluginId,
153
154
  allQueryDbs: allQueryDbs,
154
155
  allEventTopics: aggregateEventTopics,
155
156
  eventLogEntries: eventLogEntries,
@@ -89,7 +89,7 @@ let pluginStatusSubscriptionSource: GraphQL_Stitcher.subscriptionSource = {
89
89
  // (`injectAwsAuthAll(~group="Admin")`), and uploads are a regular authenticated-user
90
90
  // operation. They are added instead to the **domain** API's platform-owned base
91
91
  // (`domainBaseFragment` in the AWS/local Platform), which takes the default
92
- // `AllowAuthenticated` auth. See [docs/plans/upload-release-path.md] § "Which API".
92
+ // `AllowAuthenticated` auth. See [docs/plans/done/upload-release-path.md] § "Which API".
93
93
  let uploadTypes = [
94
94
  `type Upload_Ticket {\n uploadUrl: String!\n storageRef: String!\n}`,
95
95
  `type Upload_ReleaseResult {\n released: Boolean!\n reason: String\n}`,
@@ -0,0 +1,124 @@
1
+ // Which fields of which committed events can hold a storage ref — read off the
2
+ // declarations rather than hand-listed.
3
+ //
4
+ // This is the deploy-time half of the claim mechanism (see
5
+ // [docs/plans/done/upload-pending-claim-and-expiry.md]). An uploaded object is minted
6
+ // carrying a "pending" tag and expires while it still has one, so something has
7
+ // to notice when an event finally references it. That something needs to know
8
+ // three things, and a `@storageRef` declaration already says all three:
9
+ //
10
+ // which events to observe only variants with at least one declared field
11
+ // where the refs sit the field name, and whether it holds one or many
12
+ // which store they're in the target, qualified to `{plugin}.{store}`
13
+ //
14
+ // Deriving it here rather than in the claimer means the runtime carries a flat
15
+ // lookup table and no schema machinery: the Lambda reads `{eventType → fields}`
16
+ // and never sees a sury schema. It also means an annotation added, moved or
17
+ // deleted changes what gets claimed on the next deploy, with nothing to keep in
18
+ // sync by hand.
19
+ //
20
+ // Read the same way `Plugin_Structure` reads the same declarations — top-level
21
+ // properties of each variant, via `StorageRef.getFieldStore`. A store nested
22
+ // inside an object is not provisioned there and so is not claimed here: one
23
+ // reading, consistently narrow, rather than two that could disagree about which
24
+ // fields exist.
25
+
26
+ /** One declared ref-bearing field of one event variant. */
27
+ type refField = {
28
+ /** The field's name on the event's payload (`data` on the wire). */
29
+ field: string,
30
+ /** Whether the field holds one ref or an array of them. */
31
+ arity: Reventless.StorageRef.arity,
32
+ /** The store the refs live in, qualified `{plugin}.{store}` — already
33
+ resolved against the declaring plugin, so a reader never has to know
34
+ whose plugin it came from. */
35
+ store: string,
36
+ }
37
+
38
+ /** The ref-bearing fields of one event variant. Only variants with at least
39
+ one are produced, so an empty result means "this event log carries no
40
+ storage refs" and the caller can skip it entirely. */
41
+ type eventRefFields = {
42
+ eventType: string,
43
+ fields: array<refField>,
44
+ }
45
+
46
+ // A variant is `{TAG: "<Name>", …fields}` (payload-less variants are a bare
47
+ // string const and can hold nothing, so they drop out here).
48
+ let fromVariant = (~plugin: string, variant: S.t<unknown>): option<eventRefFields> =>
49
+ switch variant {
50
+ | S.Object({properties}) =>
51
+ switch properties->Dict.get("TAG") {
52
+ | Some(String({const: ?Some(eventType)})) =>
53
+ let fields =
54
+ properties
55
+ ->Dict.toArray
56
+ ->Array.filterMap(((field, fieldSchema)) =>
57
+ Reventless.StorageRef.getFieldStore(fieldSchema)->Option.map(((target, arity)) => {
58
+ field,
59
+ arity,
60
+ // An unqualified store belongs to the declaring plugin — the same
61
+ // resolution `Plugin_Structure` makes, so the key the claimer looks
62
+ // up is the key the deploy provisioned.
63
+ store: target.plugin->Option.getOr(plugin) ++ "." ++ target.store,
64
+ })
65
+ )
66
+ ->Array.toSorted((a, b) => String.compare(a.field, b.field))
67
+ fields->Array.length == 0 ? None : Some({eventType, fields})
68
+ | _ => None
69
+ }
70
+ | _ => None
71
+ }
72
+
73
+ /**
74
+ The ref-bearing fields of every variant in an event log's event schema.
75
+
76
+ `~plugin` is the registered name of the plugin the event log belongs to, used
77
+ to qualify a store the annotation left unqualified.
78
+ */
79
+ let fromEventSchema = (~plugin: string, eventSchema: S.t<unknown>): array<eventRefFields> =>
80
+ switch eventSchema {
81
+ | Union({anyOf}) => anyOf->Array.filterMap(v => fromVariant(~plugin, v))
82
+ | other => fromVariant(~plugin, other)->Option.mapOr([], e => [e])
83
+ }
84
+
85
+ /**
86
+ The wire form the claimer reads: `{"<eventType>": [{"field", "arity", "store"}]}`.
87
+
88
+ A flat JSON object rather than a schema, because it crosses into a Lambda's
89
+ environment: the runtime side does a dictionary lookup and a field read, and
90
+ carries no sury dependency to do it.
91
+ */
92
+ let toJson = (entries: array<eventRefFields>): JSON.t =>
93
+ entries
94
+ ->Array.map(({eventType, fields}) => (
95
+ eventType,
96
+ fields
97
+ ->Array.map(({field, arity, store}) =>
98
+ Dict.fromArray([
99
+ ("field", JSON.Encode.string(field)),
100
+ (
101
+ "arity",
102
+ JSON.Encode.string(
103
+ switch arity {
104
+ | Single => "one"
105
+ | Multiple => "many"
106
+ },
107
+ ),
108
+ ),
109
+ ("store", JSON.Encode.string(store)),
110
+ ])->JSON.Encode.object
111
+ )
112
+ ->JSON.Encode.array,
113
+ ))
114
+ ->Dict.fromArray
115
+ ->JSON.Encode.object
116
+
117
+ /** The distinct stores an event log's declarations point at. What the claimer's
118
+ IAM has to cover for that log, and what a coverage check compares against
119
+ the set of stores the deploy actually provisioned. */
120
+ let storesOf = (entries: array<eventRefFields>): array<string> =>
121
+ entries
122
+ ->Array.flatMap(({fields}) => fields->Array.map(({store}) => store))
123
+ ->Belt.Set.String.fromArray
124
+ ->Belt.Set.String.toArray
@@ -0,0 +1,88 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
6
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
+ import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
8
+
9
+ function fromVariant(plugin, variant) {
10
+ if (variant.type !== "object") {
11
+ return;
12
+ }
13
+ let properties = variant.properties;
14
+ let match = properties["TAG"];
15
+ if (match === undefined) {
16
+ return;
17
+ }
18
+ if (match.type !== "string") {
19
+ return;
20
+ }
21
+ let eventType = match.const;
22
+ if (eventType === undefined) {
23
+ return;
24
+ }
25
+ let fields = Stdlib_Array.filterMap(Object.entries(properties), param => {
26
+ let field = param[0];
27
+ return Stdlib_Option.map(StorageRef$Reventless.getFieldStore(param[1]), param => {
28
+ let target = param[0];
29
+ return {
30
+ field: field,
31
+ arity: param[1],
32
+ store: Stdlib_Option.getOr(target.plugin, plugin) + "." + target.store
33
+ };
34
+ });
35
+ }).toSorted((a, b) => Primitive_string.compare(a.field, b.field));
36
+ if (fields.length === 0) {
37
+ return;
38
+ } else {
39
+ return {
40
+ eventType: eventType,
41
+ fields: fields
42
+ };
43
+ }
44
+ }
45
+
46
+ function fromEventSchema(plugin, eventSchema) {
47
+ if (eventSchema.type === "union") {
48
+ return Stdlib_Array.filterMap(eventSchema.anyOf, v => fromVariant(plugin, v));
49
+ } else {
50
+ return Stdlib_Option.mapOr(fromVariant(plugin, eventSchema), [], e => [e]);
51
+ }
52
+ }
53
+
54
+ function toJson(entries) {
55
+ return Object.fromEntries(entries.map(param => [
56
+ param.eventType,
57
+ param.fields.map(param => {
58
+ let tmp;
59
+ tmp = param.arity === "Single" ? "one" : "many";
60
+ return Object.fromEntries([
61
+ [
62
+ "field",
63
+ param.field
64
+ ],
65
+ [
66
+ "arity",
67
+ tmp
68
+ ],
69
+ [
70
+ "store",
71
+ param.store
72
+ ]
73
+ ]);
74
+ })
75
+ ]));
76
+ }
77
+
78
+ function storesOf(entries) {
79
+ return Belt_SetString.toArray(Belt_SetString.fromArray(entries.flatMap(param => param.fields.map(param => param.store))));
80
+ }
81
+
82
+ export {
83
+ fromVariant,
84
+ fromEventSchema,
85
+ toJson,
86
+ storesOf,
87
+ }
88
+ /* StorageRef-Reventless Not a pure module */
@@ -14,6 +14,71 @@ let pascalCase = s =>
14
14
  )
15
15
  ->Array.join("")
16
16
 
17
+ // Split words out of a PascalCase/camelCase run so kebab-casing keeps the word
18
+ // boundaries. Two passes: `aB` splits an ordinary hump, `ABc` splits the tail of
19
+ // an acronym off the word that follows it ("HTTPServer" -> "HTTP-Server").
20
+ let humpBoundary = %re("/([a-z0-9])([A-Z])/g")
21
+ let acronymBoundary = %re("/([A-Z]+)([A-Z][a-z])/g")
22
+
23
+ // Kebab-case an identifier for use in an S3 bucket name: "ImportProducts" and
24
+ // "product-imports" both reduce to "import-products".
25
+ //
26
+ // S3 lowercases bucket names, so a PascalCase segment collapses into an
27
+ // unreadable run-on ("importproductsproductimportsbucket"). Kebab survives the
28
+ // lowercasing with its word boundaries intact, which is why the framework's
29
+ // declared-store buckets (`Util_StoreLayout.bucketNameFor`) already use it.
30
+ let kebabCase = s =>
31
+ s
32
+ ->String.replaceRegExp(acronymBoundary, "$1-$2")
33
+ ->String.replaceRegExp(humpBoundary, "$1-$2")
34
+ ->String.split("-")
35
+ ->Array.flatMap(p => p->String.split("_"))
36
+ ->Array.filter(p => p->String.length > 0)
37
+ ->Array.map(String.toLowerCase)
38
+ ->Array.join("-")
39
+
40
+ // S3 caps a bucket name at 63 characters and Pulumi appends `-` plus a 7-char
41
+ // uniqueness suffix, so the composed name has 55 to work with.
42
+ let maxBucketNameLength = 55
43
+
44
+ /**
45
+ The Pulumi resource name for a task's bucket: `{plugin}-{task}` for a task's
46
+ default bucket, `{plugin}-{task}-{bucketId}` when the declaration names one.
47
+
48
+ `plugin` is the ambient plugin under construction, absent for a task built
49
+ outside any plugin. No `Bucket` suffix — `aws:s3/bucket` and the
50
+ `reventless:role=Bucket` tag both already say what it is.
51
+
52
+ Composed rather than sanitised, so a name too long for S3 is the caller's to fix:
53
+ truncating here would silently manufacture a collision between two long names
54
+ that share a prefix.
55
+ */
56
+ let bucketResourceName = (
57
+ ~plugin: option<string>,
58
+ ~task: string,
59
+ ~bucketId: option<string>,
60
+ ): string => {
61
+ let name =
62
+ [plugin, Some(task), bucketId]
63
+ ->Array.filterMap(segment => segment)
64
+ ->Array.map(kebabCase)
65
+ ->Array.filter(segment => segment != "")
66
+ ->Array.join("-")
67
+ if name->String.length > maxBucketNameLength {
68
+ JsError.throwWithMessage(
69
+ `Task bucket name "${name}" is ${name
70
+ ->String.length
71
+ ->Int.toString} characters; S3 allows ${maxBucketNameLength->Int.toString} once Pulumi's ` ++
72
+ `uniqueness suffix is added. Shorten the task name` ++
73
+ switch bucketId {
74
+ | Some(id) => ` or the bucket id "${id}".`
75
+ | None => "."
76
+ },
77
+ )
78
+ }
79
+ name
80
+ }
81
+
17
82
  module Make = (
18
83
  Spec: Task.Spec,
19
84
  RuntimeEnvironment: Runtime.Environment,
@@ -169,11 +234,17 @@ module Make = (
169
234
  buckets
170
235
  ->Array.map(bucketSpec => {
171
236
  let bucketName = bucketSpec.bucketName->Option.getOr("Bucket")
172
- // `bucketStem` is the PascalCase resource-name segment (empty for the
173
- // default unnamed bucket so the name stays `<Task>Bucket`, not
174
- // `<Task>BucketBucket`). `bucketName` above remains the runtime key.
237
+ // `bucketStem` is the PascalCase segment naming this bucket's Lambda
238
+ // (empty for the default unnamed bucket). `bucketName` above remains the
239
+ // runtime key into `Task.bucketNames`; `name` is the bucket's own
240
+ // resource name, kebab-cased and plugin-qualified because S3 lowercases
241
+ // it — see `bucketResourceName`.
175
242
  let bucketStem = bucketSpec.bucketName->Option.mapOr("", pascalCase)
176
- let name = taskName ++ bucketStem ++ "Bucket"
243
+ let name = bucketResourceName(
244
+ ~plugin=ResourceAttribution.current.contents.plugin,
245
+ ~task=taskName,
246
+ ~bucketId=bucketSpec.bucketName,
247
+ )
177
248
  let bucket = TaskBucket.make(~name, ~opts)
178
249
  let opts = {Pulumi.ComponentResource.parent: bucket.parts->Pulumi.Resource.makeFromJs}
179
250
 
@@ -1,9 +1,11 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
5
  import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
5
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
7
  import * as Pulumi from "@pulumi/pulumi";
8
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
9
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
8
10
  import * as Task$ReventlessCore from "./Task.res.mjs";
9
11
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
@@ -13,6 +15,7 @@ import * as Util_Promise$ReventlessCore from "../../util/Util_Promise.res.mjs";
13
15
  import * as ComponentType$ReventlessCore from "../../ComponentType.res.mjs";
14
16
  import * as RuntimeHints$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/RuntimeHints.res.mjs";
15
17
  import * as Builder_Helpers$ReventlessCore from "../Builder_Helpers.res.mjs";
18
+ import * as ResourceAttribution$ReventlessCore from "../../ResourceAttribution.res.mjs";
16
19
 
17
20
  let log = Logger$ReventlessCore.fromEnv();
18
21
 
@@ -20,6 +23,28 @@ function pascalCase(s) {
20
23
  return s.split("-").flatMap(p => p.split("_")).filter(p => p.length > 0).map(p => p.slice(0, 1).toUpperCase() + p.slice(1, p.length)).join("");
21
24
  }
22
25
 
26
+ let humpBoundary = /([a-z0-9])([A-Z])/g;
27
+
28
+ let acronymBoundary = /([A-Z]+)([A-Z][a-z])/g;
29
+
30
+ function kebabCase(s) {
31
+ return s.replace(acronymBoundary, "$1-$2").replace(humpBoundary, "$1-$2").split("-").flatMap(p => p.split("_")).filter(p => p.length > 0).map(prim => prim.toLowerCase()).join("-");
32
+ }
33
+
34
+ function bucketResourceName(plugin, task, bucketId) {
35
+ let name = Stdlib_Array.filterMap([
36
+ plugin,
37
+ task,
38
+ bucketId
39
+ ], segment => segment).map(kebabCase).filter(segment => segment !== "").join("-");
40
+ if (name.length > 55) {
41
+ Stdlib_JsError.throwWithMessage(`Task bucket name "` + name + `" is ` + name.length.toString() + ` characters; S3 allows ` + (55).toString() + ` once Pulumi's uniqueness suffix is added. Shorten the task name` + (
42
+ bucketId !== undefined ? ` or the bucket id "` + bucketId + `".` : "."
43
+ ));
44
+ }
45
+ return name;
46
+ }
47
+
23
48
  function Make(Spec) {
24
49
  return RuntimeEnvironment => (EventCollectorChannel => (EventCollectorRuntimeBuilder => (TaskRuntimeBuilder => (TaskBucket => (SpecificSideEffectHandler => (Defaults => {
25
50
  let make = (queryBucketName, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, resourceNaming, allAggregates, runtime, opts) => Component$ReventlessCore.make(ComponentType$ReventlessCore.toString(Task$ReventlessCore.componentType), Spec.name, (extra, extra$1) => {
@@ -85,7 +110,7 @@ function Make(Spec) {
85
110
  let bucketNames = Stdlib_Option.map(config.buckets, buckets => Object.fromEntries(buckets.map(bucketSpec => {
86
111
  let bucketName = Stdlib_Option.getOr(bucketSpec.bucketName, "Bucket");
87
112
  let bucketStem = Stdlib_Option.mapOr(bucketSpec.bucketName, "", pascalCase);
88
- let name = extra$1 + bucketStem + "Bucket";
113
+ let name = bucketResourceName(ResourceAttribution$ReventlessCore.current.contents.plugin, extra$1, bucketSpec.bucketName);
89
114
  let bucket = TaskBucket.make(name, opts);
90
115
  let opts_parent = bucket.parts;
91
116
  let opts$1 = {
@@ -112,9 +137,16 @@ function Make(Spec) {
112
137
  }))))));
113
138
  }
114
139
 
140
+ let maxBucketNameLength = 55;
141
+
115
142
  export {
116
143
  log,
117
144
  pascalCase,
145
+ humpBoundary,
146
+ acronymBoundary,
147
+ kebabCase,
148
+ maxBucketNameLength,
149
+ bucketResourceName,
118
150
  Make,
119
151
  }
120
152
  /* log Not a pure module */
@@ -888,7 +888,7 @@ module Make = (
888
888
  // Gives the AWS platform access to allQueryDbs, allEventTopics, and eventLogEntries
889
889
  // without coupling reventless-core to reventless-aws.
890
890
  Spec.hooks.subscriptionInfraHook->Option.forEach(hook =>
891
- hook({allQueryDbs, allEventTopics, eventLogEntries, opts})
891
+ hook({pluginName: name, allQueryDbs, allEventTopics, eventLogEntries, opts})
892
892
  )
893
893
 
894
894
  module SpecificHeartbeat = Heartbeat_Builder.Make(HeartbeatRunner)
@@ -547,6 +547,7 @@ function Make(Spec) {
547
547
  let tasksOutputs = Plugin_Helpers$ReventlessCore.createTasks(tasks, aggregatesOutputs, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, Spec.resourceNaming, componentRuntime, opts);
548
548
  let resolvers = Plugin_Helpers$ReventlessCore.createResolvers(allQueryDbs);
549
549
  Stdlib_Option.forEach(Spec.hooks.subscriptionInfraHook, hook => hook({
550
+ pluginName: extra$1,
550
551
  allQueryDbs: allQueryDbs,
551
552
  allEventTopics: aggregateEventTopics,
552
553
  eventLogEntries: eventLogEntries,
@@ -911,6 +911,11 @@ type eventQueryRegistrationParams = {
911
911
  // are assembled inside builderOutputs. Lets the AWS platform wire StateTopic and
912
912
  // EventLogSubscription Lambdas without touching reventless-core.
913
913
  type subscriptionInfraParams = {
914
+ /** The plugin's registered name. Carried because infrastructure wired from
915
+ here can be per plugin rather than per component — and because a
916
+ declaration that names a store without qualifying it means "this plugin's
917
+ own", which is unreadable without knowing whose plugin it is. */
918
+ pluginName: string,
914
919
  allQueryDbs: ReventlessInfra.QueryDb.allOutputs,
915
920
  allEventTopics: dict<ReventlessInfra.EventTopic.outputs>,
916
921
  eventLogEntries: array<ReventlessInfra.Api.eventLogSchemaEntry>,
@@ -503,7 +503,11 @@ let make = (
503
503
  properties
504
504
  ->Dict.toArray
505
505
  ->Array.filterMap(((field, fieldSchema)) =>
506
- Reventless.StorageRef.getStore(fieldSchema)->Option.map(target => {
506
+ // `getFieldStore`, not `getStore`: a `@storageRef("s") urls: array<string>`
507
+ // field carries the marker on its *element*, so reading the field schema
508
+ // directly answered `None` and the store went unprovisioned — the same
509
+ // silence as never having written the annotation.
510
+ Reventless.StorageRef.getFieldStore(fieldSchema)->Option.map(((target, _arity)) => {
507
511
  Reventless.Plugin.store: target.plugin->Option.getOr(name) ++ "." ++ target.store,
508
512
  component,
509
513
  field,
@@ -446,7 +446,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
446
446
  let properties = v.properties;
447
447
  return Stdlib_Array.filterMap(Object.entries(properties), param => {
448
448
  let field = param[0];
449
- return Stdlib_Option.map(StorageRef$Reventless.getStore(param[1]), target => {
449
+ return Stdlib_Option.map(StorageRef$Reventless.getFieldStore(param[1]), param => {
450
+ let target = param[0];
450
451
  let plugin = target.plugin;
451
452
  return {
452
453
  store: Stdlib_Option.getOr(target.plugin, name) + "." + target.store,