@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 +31 -0
- package/package.json +7 -7
- package/src/adapter/Runtime/Runtime.res +9 -1
- package/src/adapter/Runtime/Runtime.res.mjs +1 -2
- package/src/admin/Platform_Admin.res +1 -1
- package/src/admin/Platform_Admin.res.mjs +1 -0
- package/src/admin/Platform_AdminApi.res +1 -1
- package/src/components/Api/StorageRefFields.res +124 -0
- package/src/components/Api/StorageRefFields.res.mjs +88 -0
- package/src/components/Task/Task_Builder.res +75 -4
- package/src/components/Task/Task_Builder.res.mjs +33 -1
- package/src/plugin/component/Plugin_Builder.res +1 -1
- package/src/plugin/component/Plugin_Builder.res.mjs +1 -0
- package/src/plugin/component/Plugin_Helpers.res +5 -0
- package/src/plugin/component/Plugin_Structure.res +5 -1
- package/src/plugin/component/Plugin_Structure.res.mjs +2 -1
- package/src/util/StorePrefixCollision.res +96 -0
- package/src/util/StorePrefixCollision.res.mjs +55 -0
- package/tests/StorePrefixCollisionTest.res +160 -0
- package/tests/StorePrefixCollisionTest.res.mjs +174 -0
- package/tests/TaskBucketNamingTest.res +97 -0
- package/tests/TaskBucketNamingTest.res.mjs +60 -0
- package/tests/api/StorageRefFieldsTest.res +148 -0
- package/tests/api/StorageRefFieldsTest.res.mjs +152 -0
- package/tests/api/SuryToJsonSchemaTest.res +46 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +39 -0
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.
|
|
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-
|
|
40
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
|
173
|
-
// default unnamed bucket
|
|
174
|
-
//
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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.
|
|
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,
|