@reventlessdev/reventless-core 3.0.0-alpha.200 → 3.0.0-alpha.202
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 +16 -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/plugin/component/Plugin_Builder.res +28 -3
- package/src/plugin/component/Plugin_Builder.res.mjs +20 -2
- package/src/plugin/component/Plugin_Helpers.res +26 -0
- package/src/plugin/component/Plugin_Helpers.res.mjs +15 -0
- package/src/plugin/component/Plugin_Structure.res +5 -1
- package/src/plugin/component/Plugin_Structure.res.mjs +2 -1
- package/src/plugin/lifecycle/PluginsReadModelSpec.res +6 -4
- package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +2 -2
- package/tests/api/StorageRefFieldsTest.res +148 -0
- package/tests/api/StorageRefFieldsTest.res.mjs +152 -0
- package/tests/message/MessageTest.res +4 -3
- package/tests/message/MessageTest.res.mjs +25 -21
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
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.202 (2026-08-02)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** deploy-time offload hook seam for plugin payloads ([7f970eb](https://github.com/ReventlessDev/reventless-core/commit/7f970eb449765e781559c6596ee49dd74b9451bc))
|
|
11
|
+
* **spec:** make plugin structure/apiSchemaFragment offloadable payloads ([a0391bf](https://github.com/ReventlessDev/reventless-core/commit/a0391bf26f91035460cbf90f52d105a1003414c3))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.201 (2026-08-02)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **aws,core,doc:** environment-tiered CloudWatch log retention and levels ([8032023](https://github.com/ReventlessDev/reventless-core/commit/803202305a7d352484b8df5e58df03c92ca58b5f))
|
|
19
|
+
* **aws,core,spec,seed-aws:** expire uploads nobody committed a reference to ([f63e84c](https://github.com/ReventlessDev/reventless-core/commit/f63e84c1a11cc350b799a6f69a2e7427cf1ea6e9))
|
|
20
|
+
|
|
21
|
+
|
|
6
22
|
# 3.0.0-alpha.200 (2026-08-01)
|
|
7
23
|
|
|
8
24
|
* 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-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.202",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -30,20 +30,20 @@
|
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
33
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.0",
|
|
33
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
34
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
35
35
|
"@reventlessdev/rescript-node": "2.0.0-alpha.0",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
37
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.0",
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.117",
|
|
36
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
39
37
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
40
|
-
"@reventlessdev/
|
|
38
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.0",
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.119",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.94",
|
|
41
41
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30"
|
|
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 */
|
|
@@ -738,6 +738,31 @@ module Make = (
|
|
|
738
738
|
// so reading it inside the async callback would always yield "Domain".
|
|
739
739
|
let capturedDeployTarget = Spec.hooks.deployTarget.contents
|
|
740
740
|
|
|
741
|
+
// Offload the two large pluginDefinition fields at deploy time when a
|
|
742
|
+
// provider offers an object store (AWS writes a content-addressed object
|
|
743
|
+
// and returns a reference); unset locally, so they stay Inline. Both
|
|
744
|
+
// values are concrete here, so the hook — and the object write it does —
|
|
745
|
+
// runs at graph-construction time, before the Pulumi.Output.apply below
|
|
746
|
+
// (a resource cannot be created inside .apply). Unconditional: these
|
|
747
|
+
// fields are always large enough to be worth offloading.
|
|
748
|
+
let toPayload = (value, ~schema, ~store) =>
|
|
749
|
+
switch Plugin_Helpers.offloadHook.contents {
|
|
750
|
+
| Some(hook) =>
|
|
751
|
+
Reventless.Offload.Offloaded(
|
|
752
|
+
hook(~store, ~bytes=value->S.reverseConvertToJsonStringOrThrow(schema)),
|
|
753
|
+
)
|
|
754
|
+
| None => Reventless.Offload.Inline(value)
|
|
755
|
+
}
|
|
756
|
+
let apiSchemaFragmentPayload = toPayload(
|
|
757
|
+
apiSchemaFragment,
|
|
758
|
+
~schema=Reventless.Plugin.apiSchemaFragmentSchema,
|
|
759
|
+
~store="pluginApiFragments",
|
|
760
|
+
)
|
|
761
|
+
let structurePayload =
|
|
762
|
+
pluginStructure->Option.map(s =>
|
|
763
|
+
toPayload(s, ~schema=Reventless.Plugin.pluginStructureSchema, ~store="pluginStructures")
|
|
764
|
+
)
|
|
765
|
+
|
|
741
766
|
// DCB EventLog definition — Some when this plugin has a DcbEventLog
|
|
742
767
|
// component, None otherwise. Admin's manageSubscriptions reads this to
|
|
743
768
|
// provision cross-plugin SNS subscriptions from peer plugins' DCB
|
|
@@ -769,9 +794,9 @@ module Make = (
|
|
|
769
794
|
extensions: extensionsDefinitions,
|
|
770
795
|
eventCollector: eventCollectorUrn,
|
|
771
796
|
extensionProtocols: [],
|
|
772
|
-
apiSchemaFragment: Some(
|
|
797
|
+
apiSchemaFragment: Some(apiSchemaFragmentPayload),
|
|
773
798
|
apiTarget: Some(capturedDeployTarget),
|
|
774
|
-
structure:
|
|
799
|
+
structure: structurePayload,
|
|
775
800
|
dcbEventLog: dcbEventLogDef,
|
|
776
801
|
// Business role from the deploy-time metadata registry (set via
|
|
777
802
|
// registerPluginMetadata). Unset → Domain. Rides the handshake to the
|
|
@@ -888,7 +913,7 @@ module Make = (
|
|
|
888
913
|
// Gives the AWS platform access to allQueryDbs, allEventTopics, and eventLogEntries
|
|
889
914
|
// without coupling reventless-core to reventless-aws.
|
|
890
915
|
Spec.hooks.subscriptionInfraHook->Option.forEach(hook =>
|
|
891
|
-
hook({allQueryDbs, allEventTopics, eventLogEntries, opts})
|
|
916
|
+
hook({pluginName: name, allQueryDbs, allEventTopics, eventLogEntries, opts})
|
|
892
917
|
)
|
|
893
918
|
|
|
894
919
|
module SpecificHeartbeat = Heartbeat_Builder.Make(HeartbeatRunner)
|
|
@@ -8,6 +8,7 @@ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
|
8
8
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
9
9
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
10
10
|
import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
|
|
11
|
+
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
11
12
|
import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
|
|
12
13
|
import * as Plugin$ReventlessCore from "./Plugin.res.mjs";
|
|
13
14
|
import * as StackReference$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/StackReference.res.mjs";
|
|
@@ -486,6 +487,22 @@ function Make(Spec) {
|
|
|
486
487
|
let match$3 = EventCollectorHelper.make(childName, eventTopics, opts);
|
|
487
488
|
let eventCollector = match$3[0];
|
|
488
489
|
let capturedDeployTarget = Spec.hooks.deployTarget.contents;
|
|
490
|
+
let toPayload = (value, schema, store) => {
|
|
491
|
+
let hook = Plugin_Helpers$ReventlessCore.offloadHook.contents;
|
|
492
|
+
if (hook !== undefined) {
|
|
493
|
+
return {
|
|
494
|
+
TAG: "Offloaded",
|
|
495
|
+
_0: hook(store, S.reverseConvertToJsonStringOrThrow(value, schema, undefined))
|
|
496
|
+
};
|
|
497
|
+
} else {
|
|
498
|
+
return {
|
|
499
|
+
TAG: "Inline",
|
|
500
|
+
_0: value
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
let apiSchemaFragmentPayload = toPayload(apiSchemaFragment, Plugin$Reventless.apiSchemaFragmentSchema, "pluginApiFragments");
|
|
505
|
+
let structurePayload = Stdlib_Option.map(pluginStructure, s => toPayload(s, Plugin$Reventless.pluginStructureSchema, "pluginStructures"));
|
|
489
506
|
let dcbOutputs$1 = dcbResult.dcbEventLogOutputs;
|
|
490
507
|
let dcbEventLogDef;
|
|
491
508
|
if (dcbOutputs$1 !== undefined) {
|
|
@@ -509,9 +526,9 @@ function Make(Spec) {
|
|
|
509
526
|
extensions: extensionsDefinitions,
|
|
510
527
|
eventCollector: param[1],
|
|
511
528
|
extensionProtocols: [],
|
|
512
|
-
apiSchemaFragment:
|
|
529
|
+
apiSchemaFragment: apiSchemaFragmentPayload,
|
|
513
530
|
apiTarget: capturedDeployTarget,
|
|
514
|
-
structure:
|
|
531
|
+
structure: structurePayload,
|
|
515
532
|
dcbEventLog: param[2],
|
|
516
533
|
kind: Stdlib_Option.getOr(Stdlib_Option.flatMap(Plugin_Helpers$ReventlessCore.pluginMetadataRegistry.contents, m => m.kind), "Domain")
|
|
517
534
|
}));
|
|
@@ -547,6 +564,7 @@ function Make(Spec) {
|
|
|
547
564
|
let tasksOutputs = Plugin_Helpers$ReventlessCore.createTasks(tasks, aggregatesOutputs, scheduler, schedulerRoleUrn, publishToAggregates, queryEngine, Spec.resourceNaming, componentRuntime, opts);
|
|
548
565
|
let resolvers = Plugin_Helpers$ReventlessCore.createResolvers(allQueryDbs);
|
|
549
566
|
Stdlib_Option.forEach(Spec.hooks.subscriptionInfraHook, hook => hook({
|
|
567
|
+
pluginName: extra$1,
|
|
550
568
|
allQueryDbs: allQueryDbs,
|
|
551
569
|
allEventTopics: aggregateEventTopics,
|
|
552
570
|
eventLogEntries: eventLogEntries,
|
|
@@ -785,6 +785,27 @@ let clearOnPluginDeployed = () => {
|
|
|
785
785
|
onPluginDeployedHook.contents = None
|
|
786
786
|
}
|
|
787
787
|
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
// Deploy-time offload hook — content-addresses a large pluginDefinition field
|
|
790
|
+
// to an object store and returns the reference. Set by a provider that can
|
|
791
|
+
// write objects (AWS creates a BucketObject); unset on the local platform, so
|
|
792
|
+
// the fields stay Inline. `bytes` is the already-serialized JSON of the value;
|
|
793
|
+
// the provider hashes it, stores it at `sha256/<hash>`, and returns the ref.
|
|
794
|
+
// Fired synchronously at graph-construction time (the value is concrete and a
|
|
795
|
+
// resource cannot be created inside a Pulumi.Output.apply).
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
let offloadHook: ref<option<(~store: string, ~bytes: string) => Reventless.Offload.offloadedRef>> = ref(
|
|
798
|
+
None,
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
let registerOffload = (hook: (~store: string, ~bytes: string) => Reventless.Offload.offloadedRef) => {
|
|
802
|
+
offloadHook.contents = Some(hook)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
let clearOffload = () => {
|
|
806
|
+
offloadHook.contents = None
|
|
807
|
+
}
|
|
808
|
+
|
|
788
809
|
// ---------------------------------------------------------------------------
|
|
789
810
|
// Platform-deployed hook — fires after deployPlatform / makePlatform
|
|
790
811
|
// completes, providing platform-level metadata with resolved values.
|
|
@@ -911,6 +932,11 @@ type eventQueryRegistrationParams = {
|
|
|
911
932
|
// are assembled inside builderOutputs. Lets the AWS platform wire StateTopic and
|
|
912
933
|
// EventLogSubscription Lambdas without touching reventless-core.
|
|
913
934
|
type subscriptionInfraParams = {
|
|
935
|
+
/** The plugin's registered name. Carried because infrastructure wired from
|
|
936
|
+
here can be per plugin rather than per component — and because a
|
|
937
|
+
declaration that names a store without qualifying it means "this plugin's
|
|
938
|
+
own", which is unreadable without knowing whose plugin it is. */
|
|
939
|
+
pluginName: string,
|
|
914
940
|
allQueryDbs: ReventlessInfra.QueryDb.allOutputs,
|
|
915
941
|
allEventTopics: dict<ReventlessInfra.EventTopic.outputs>,
|
|
916
942
|
eventLogEntries: array<ReventlessInfra.Api.eventLogSchemaEntry>,
|
|
@@ -418,6 +418,18 @@ function clearOnPluginDeployed() {
|
|
|
418
418
|
onPluginDeployedHook.contents = undefined;
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
let offloadHook = {
|
|
422
|
+
contents: undefined
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
function registerOffload(hook) {
|
|
426
|
+
offloadHook.contents = hook;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function clearOffload() {
|
|
430
|
+
offloadHook.contents = undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
421
433
|
let onPlatformDeployedHook = {
|
|
422
434
|
contents: undefined
|
|
423
435
|
};
|
|
@@ -1015,6 +1027,9 @@ export {
|
|
|
1015
1027
|
onPluginDeployedHook,
|
|
1016
1028
|
registerOnPluginDeployed,
|
|
1017
1029
|
clearOnPluginDeployed,
|
|
1030
|
+
offloadHook,
|
|
1031
|
+
registerOffload,
|
|
1032
|
+
clearOffload,
|
|
1018
1033
|
onPlatformDeployedHook,
|
|
1019
1034
|
lastPlatformDeployedInfo,
|
|
1020
1035
|
registerOnPlatformDeployed,
|
|
@@ -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,
|
|
@@ -24,13 +24,15 @@ type state = {
|
|
|
24
24
|
extensions: array<Reventless.Plugin.extensionDefinition>,
|
|
25
25
|
status: status,
|
|
26
26
|
statusChange: Message.statusChange,
|
|
27
|
-
apiSchemaFragment: @s.matches(Reventless.Plugin.
|
|
27
|
+
apiSchemaFragment: @s.matches(Reventless.Plugin.apiSchemaFragmentOffloadSchema)
|
|
28
|
+
option<Reventless.Offload.payload<Reventless.Plugin.apiSchemaFragment>>,
|
|
28
29
|
// API target for split-API schema routing. Absent/None means "Domain" (backward compat).
|
|
29
30
|
// "Platform" → excluded from DomainApi runtime schema stitching in updateApiSchema.
|
|
30
31
|
apiTarget?: string,
|
|
31
32
|
// Plugin structure (component metadata) — surfaced via Platform_ComponentDefinitions.
|
|
32
33
|
// None for older plugins whose protocol version did not carry the field.
|
|
33
|
-
structure: @s.matches(Reventless.Plugin.
|
|
34
|
+
structure: @s.matches(Reventless.Plugin.pluginStructureOffloadSchema)
|
|
35
|
+
option<Reventless.Offload.payload<Reventless.Plugin.pluginStructure>>,
|
|
34
36
|
// DCB EventLog definition for plugins that bundle a DcbEventLog component.
|
|
35
37
|
// Admin's manageSubscriptions uses this to wire cross-plugin SNS subscriptions
|
|
36
38
|
// from this plugin's DCB topic → peer EventCollectors (and vice-versa). None
|
|
@@ -72,9 +74,9 @@ type queryResult = {
|
|
|
72
74
|
extensionNames: array<string>,
|
|
73
75
|
extensions: array<Reventless.Plugin.extensionDefinition>,
|
|
74
76
|
status: status,
|
|
75
|
-
apiSchemaFragment: option<Reventless.Plugin.apiSchemaFragment
|
|
77
|
+
apiSchemaFragment: option<Reventless.Offload.payload<Reventless.Plugin.apiSchemaFragment>>,
|
|
76
78
|
apiTarget?: string,
|
|
77
|
-
structure: option<Reventless.Plugin.pluginStructure
|
|
79
|
+
structure: option<Reventless.Offload.payload<Reventless.Plugin.pluginStructure>>,
|
|
78
80
|
dcbEventLog: option<Reventless.Plugin.dcbEventLogDefinition>,
|
|
79
81
|
kind: option<Reventless.Plugin.pluginKind>,
|
|
80
82
|
}
|
|
@@ -23,9 +23,9 @@ let stateSchema = S.schema(s => ({
|
|
|
23
23
|
extensions: s.m(S.array(Plugin$Reventless.extensionDefinitionSchema)),
|
|
24
24
|
status: s.m(statusSchema),
|
|
25
25
|
statusChange: s.m(Message$ReventlessCore.statusChangeSchema),
|
|
26
|
-
apiSchemaFragment: s.m(Plugin$Reventless.
|
|
26
|
+
apiSchemaFragment: s.m(Plugin$Reventless.apiSchemaFragmentOffloadSchema),
|
|
27
27
|
apiTarget: s.m(S.option(S.string)),
|
|
28
|
-
structure: s.m(Plugin$Reventless.
|
|
28
|
+
structure: s.m(Plugin$Reventless.pluginStructureOffloadSchema),
|
|
29
29
|
dcbEventLog: s.m(Plugin$Reventless.dcbEventLogOptionSchema),
|
|
30
30
|
kind: s.m(S.option(Plugin$Reventless.pluginKindSchema)),
|
|
31
31
|
otherConnectedVersions: s.m(S.array(Plugin$Reventless.versionSchema))
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// What the claim component ever gets to see, decided here.
|
|
2
|
+
//
|
|
3
|
+
// `StorageRefFields.fromEventSchema` is the deploy-time reading of the
|
|
4
|
+
// `@storageRef` declarations that becomes the claimer's entire input: an event
|
|
5
|
+
// variant it does not produce is an event the claimer never reads a record for,
|
|
6
|
+
// and a field it does not list is a ref that never gets claimed — so the object
|
|
7
|
+
// keeps its pending tag and, on a store with expiry enabled, is eventually
|
|
8
|
+
// deleted while something still references it.
|
|
9
|
+
//
|
|
10
|
+
// Which makes the interesting assertions the negative ones: what is *absent*
|
|
11
|
+
// from this output is what silently rots.
|
|
12
|
+
|
|
13
|
+
open JestGlobals
|
|
14
|
+
|
|
15
|
+
module Fields = StorageRefFields
|
|
16
|
+
|
|
17
|
+
// An event union, shaped the way sury encodes a ReScript variant: one object per
|
|
18
|
+
// variant, discriminated by a `TAG` string constant.
|
|
19
|
+
let variant = (~tag: string, fields: array<(string, S.t<unknown>)>): S.t<unknown> =>
|
|
20
|
+
S.object(s => {
|
|
21
|
+
let _ = s.field("TAG", S.literal(tag)->S.castToUnknown)
|
|
22
|
+
fields->Array.forEach(((name, schema)) => {
|
|
23
|
+
let _ = s.field(name, schema)
|
|
24
|
+
})
|
|
25
|
+
()
|
|
26
|
+
})->S.castToUnknown
|
|
27
|
+
|
|
28
|
+
let ref_ = (~plugin: option<string>=?, ~store: string) =>
|
|
29
|
+
Reventless.StorageRef.forStore(~plugin?, ~store)->S.castToUnknown
|
|
30
|
+
|
|
31
|
+
describe("StorageRefFields.fromEventSchema", () => {
|
|
32
|
+
testSync("finds a declared single-valued ref field and qualifies its store", () => {
|
|
33
|
+
let schema = S.union([variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))])])->S.castToUnknown
|
|
34
|
+
expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([
|
|
35
|
+
{
|
|
36
|
+
Fields.eventType: "ProductImageChanged",
|
|
37
|
+
fields: [
|
|
38
|
+
{Fields.field: "imageUrl", arity: Single, store: "Catalog.productImages"},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// The annotation's own qualification wins: a field pointing at another
|
|
45
|
+
// plugin's store must not be re-attributed to the plugin that wrote it, or the
|
|
46
|
+
// claimer resolves the wrong bucket and refuses the ref as out-of-store.
|
|
47
|
+
testSync("a qualified declaration keeps the store it names", () => {
|
|
48
|
+
let schema =
|
|
49
|
+
S.union([
|
|
50
|
+
variant(~tag="OrderPhotoAttached", [("photo", ref_(~plugin="Catalog", ~store="productImages"))]),
|
|
51
|
+
])->S.castToUnknown
|
|
52
|
+
expect(
|
|
53
|
+
Fields.fromEventSchema(~plugin="Ordering", schema)
|
|
54
|
+
->Array.flatMap(e => e.fields->Array.map(f => f.store)),
|
|
55
|
+
)->toEqual(["Catalog.productImages"])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// `@storageRef` on an `array<string>` attaches the marker to the ELEMENT, so
|
|
59
|
+
// reading the field schema directly answers None. The claimer needs the arity
|
|
60
|
+
// to know whether to read one value or iterate.
|
|
61
|
+
testSync("finds a multi-valued ref field and records its arity", () => {
|
|
62
|
+
let schema =
|
|
63
|
+
S.union([
|
|
64
|
+
variant(~tag="GalleryReplaced", [("imageUrls", S.array(ref_(~store="productImages"))->S.castToUnknown)]),
|
|
65
|
+
])->S.castToUnknown
|
|
66
|
+
expect(
|
|
67
|
+
Fields.fromEventSchema(~plugin="Catalog", schema)->Array.flatMap(e => e.fields),
|
|
68
|
+
)->toEqual([{Fields.field: "imageUrls", arity: Multiple, store: "Catalog.productImages"}])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
// Narrow input by construction: a platform whose plugins declare no store
|
|
72
|
+
// provisions no claimer at all, and an event log carrying no declaration is
|
|
73
|
+
// never subscribed to.
|
|
74
|
+
testSync("a variant with no declared field is omitted entirely", () => {
|
|
75
|
+
let schema =
|
|
76
|
+
S.union([
|
|
77
|
+
variant(~tag="ProductRenamed", [("name", S.string->S.castToUnknown)]),
|
|
78
|
+
variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))]),
|
|
79
|
+
])->S.castToUnknown
|
|
80
|
+
expect(Fields.fromEventSchema(~plugin="Catalog", schema)->Array.map(e => e.eventType))->toEqual([
|
|
81
|
+
"ProductImageChanged",
|
|
82
|
+
])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
testSync("an event schema with no declaration anywhere yields nothing", () => {
|
|
86
|
+
let schema = S.union([variant(~tag="ProductRenamed", [("name", S.string->S.castToUnknown)])])->S.castToUnknown
|
|
87
|
+
expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// A string field that merely looks like a ref is not one. Declaration
|
|
91
|
+
// outranks inference everywhere else in the framework, and this is the reading
|
|
92
|
+
// that has to agree with `Plugin_Structure`'s — the claimer can only reach a
|
|
93
|
+
// store the deploy actually provisioned.
|
|
94
|
+
testSync("an unannotated string field is not treated as a ref", () => {
|
|
95
|
+
let schema =
|
|
96
|
+
S.union([variant(~tag="ProductImported", [("imageUrl", S.string->S.castToUnknown)])])->S.castToUnknown
|
|
97
|
+
expect(Fields.fromEventSchema(~plugin="Catalog", schema))->toEqual([])
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
testSync("a single-variant (non-union) event schema is read the same way", () => {
|
|
101
|
+
let schema = variant(~tag="ProductImageChanged", [("imageUrl", ref_(~store="productImages"))])
|
|
102
|
+
expect(Fields.fromEventSchema(~plugin="Catalog", schema)->Array.map(e => e.eventType))->toEqual([
|
|
103
|
+
"ProductImageChanged",
|
|
104
|
+
])
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe("StorageRefFields.toJson", () => {
|
|
109
|
+
// The wire form crosses into a Lambda's environment, where the handler decodes
|
|
110
|
+
// it with plain JSON reads. Both ends are pinned here because a drift is
|
|
111
|
+
// silent: an unreadable entry is indistinguishable from an absent one, and an
|
|
112
|
+
// absent one means "nothing to claim".
|
|
113
|
+
testSync("emits field, arity and store per event type", () => {
|
|
114
|
+
let entries: array<Fields.eventRefFields> = [
|
|
115
|
+
{
|
|
116
|
+
eventType: "ProductImageChanged",
|
|
117
|
+
fields: [{field: "imageUrl", arity: Single, store: "Catalog.productImages"}],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
eventType: "GalleryReplaced",
|
|
121
|
+
fields: [{field: "imageUrls", arity: Multiple, store: "Catalog.productImages"}],
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
expect(Fields.toJson(entries)->JSON.stringify)->toBe(
|
|
125
|
+
`{"ProductImageChanged":[{"field":"imageUrl","arity":"one","store":"Catalog.productImages"}],` ++
|
|
126
|
+
`"GalleryReplaced":[{"field":"imageUrls","arity":"many","store":"Catalog.productImages"}]}`,
|
|
127
|
+
)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("StorageRefFields.storesOf", () => {
|
|
132
|
+
testSync("collects the distinct stores an event log's declarations reach", () => {
|
|
133
|
+
let entries: array<Fields.eventRefFields> = [
|
|
134
|
+
{
|
|
135
|
+
eventType: "A",
|
|
136
|
+
fields: [{field: "a", arity: Single, store: "Catalog.productImages"}],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
eventType: "B",
|
|
140
|
+
fields: [
|
|
141
|
+
{field: "b", arity: Single, store: "Catalog.productImages"},
|
|
142
|
+
{field: "c", arity: Multiple, store: "Ordering.receipts"},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
]
|
|
146
|
+
expect(Fields.storesOf(entries))->toEqual(["Catalog.productImages", "Ordering.receipts"])
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
5
|
+
import * as StorageRefFields$ReventlessCore from "../../src/components/Api/StorageRefFields.res.mjs";
|
|
6
|
+
|
|
7
|
+
function variant(tag, fields) {
|
|
8
|
+
return S.object(s => {
|
|
9
|
+
s.f("TAG", S.literal(tag));
|
|
10
|
+
fields.forEach(param => {
|
|
11
|
+
s.f(param[0], param[1]);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let ref_ = StorageRef$Reventless.forStore;
|
|
17
|
+
|
|
18
|
+
globalThis.describe("StorageRefFields.fromEventSchema", () => {
|
|
19
|
+
globalThis.test("finds a declared single-valued ref field and qualifies its store", () => {
|
|
20
|
+
let schema = S.union([variant("ProductImageChanged", [[
|
|
21
|
+
"imageUrl",
|
|
22
|
+
StorageRef$Reventless.forStore(undefined, "productImages")
|
|
23
|
+
]])]);
|
|
24
|
+
globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([{
|
|
25
|
+
eventType: "ProductImageChanged",
|
|
26
|
+
fields: [{
|
|
27
|
+
field: "imageUrl",
|
|
28
|
+
arity: "Single",
|
|
29
|
+
store: "Catalog.productImages"
|
|
30
|
+
}]
|
|
31
|
+
}]);
|
|
32
|
+
});
|
|
33
|
+
globalThis.test("a qualified declaration keeps the store it names", () => {
|
|
34
|
+
let schema = S.union([variant("OrderPhotoAttached", [[
|
|
35
|
+
"photo",
|
|
36
|
+
StorageRef$Reventless.forStore("Catalog", "productImages")
|
|
37
|
+
]])]);
|
|
38
|
+
globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Ordering", schema).flatMap(e => e.fields.map(f => f.store))).toEqual(["Catalog.productImages"]);
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("finds a multi-valued ref field and records its arity", () => {
|
|
41
|
+
let schema = S.union([variant("GalleryReplaced", [[
|
|
42
|
+
"imageUrls",
|
|
43
|
+
S.array(StorageRef$Reventless.forStore(undefined, "productImages"))
|
|
44
|
+
]])]);
|
|
45
|
+
globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).flatMap(e => e.fields)).toEqual([{
|
|
46
|
+
field: "imageUrls",
|
|
47
|
+
arity: "Multiple",
|
|
48
|
+
store: "Catalog.productImages"
|
|
49
|
+
}]);
|
|
50
|
+
});
|
|
51
|
+
globalThis.test("a variant with no declared field is omitted entirely", () => {
|
|
52
|
+
let schema = S.union([
|
|
53
|
+
variant("ProductRenamed", [[
|
|
54
|
+
"name",
|
|
55
|
+
S.string
|
|
56
|
+
]]),
|
|
57
|
+
variant("ProductImageChanged", [[
|
|
58
|
+
"imageUrl",
|
|
59
|
+
StorageRef$Reventless.forStore(undefined, "productImages")
|
|
60
|
+
]])
|
|
61
|
+
]);
|
|
62
|
+
globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).map(e => e.eventType)).toEqual(["ProductImageChanged"]);
|
|
63
|
+
});
|
|
64
|
+
globalThis.test("an event schema with no declaration anywhere yields nothing", () => {
|
|
65
|
+
let schema = S.union([variant("ProductRenamed", [[
|
|
66
|
+
"name",
|
|
67
|
+
S.string
|
|
68
|
+
]])]);
|
|
69
|
+
globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
globalThis.test("an unannotated string field is not treated as a ref", () => {
|
|
72
|
+
let schema = S.union([variant("ProductImported", [[
|
|
73
|
+
"imageUrl",
|
|
74
|
+
S.string
|
|
75
|
+
]])]);
|
|
76
|
+
globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema)).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
globalThis.test("a single-variant (non-union) event schema is read the same way", () => {
|
|
79
|
+
let schema = variant("ProductImageChanged", [[
|
|
80
|
+
"imageUrl",
|
|
81
|
+
StorageRef$Reventless.forStore(undefined, "productImages")
|
|
82
|
+
]]);
|
|
83
|
+
globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", schema).map(e => e.eventType)).toEqual(["ProductImageChanged"]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
globalThis.describe("StorageRefFields.toJson", () => {
|
|
88
|
+
globalThis.test("emits field, arity and store per event type", () => {
|
|
89
|
+
let entries = [
|
|
90
|
+
{
|
|
91
|
+
eventType: "ProductImageChanged",
|
|
92
|
+
fields: [{
|
|
93
|
+
field: "imageUrl",
|
|
94
|
+
arity: "Single",
|
|
95
|
+
store: "Catalog.productImages"
|
|
96
|
+
}]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
eventType: "GalleryReplaced",
|
|
100
|
+
fields: [{
|
|
101
|
+
field: "imageUrls",
|
|
102
|
+
arity: "Multiple",
|
|
103
|
+
store: "Catalog.productImages"
|
|
104
|
+
}]
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
globalThis.expect(JSON.stringify(StorageRefFields$ReventlessCore.toJson(entries))).toBe(`{"ProductImageChanged":[{"field":"imageUrl","arity":"one","store":"Catalog.productImages"}],"GalleryReplaced":[{"field":"imageUrls","arity":"many","store":"Catalog.productImages"}]}`);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
globalThis.describe("StorageRefFields.storesOf", () => {
|
|
112
|
+
globalThis.test("collects the distinct stores an event log's declarations reach", () => {
|
|
113
|
+
let entries = [
|
|
114
|
+
{
|
|
115
|
+
eventType: "A",
|
|
116
|
+
fields: [{
|
|
117
|
+
field: "a",
|
|
118
|
+
arity: "Single",
|
|
119
|
+
store: "Catalog.productImages"
|
|
120
|
+
}]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
eventType: "B",
|
|
124
|
+
fields: [
|
|
125
|
+
{
|
|
126
|
+
field: "b",
|
|
127
|
+
arity: "Single",
|
|
128
|
+
store: "Catalog.productImages"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
field: "c",
|
|
132
|
+
arity: "Multiple",
|
|
133
|
+
store: "Ordering.receipts"
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
];
|
|
138
|
+
globalThis.expect(StorageRefFields$ReventlessCore.storesOf(entries)).toEqual([
|
|
139
|
+
"Catalog.productImages",
|
|
140
|
+
"Ordering.receipts"
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
let Fields;
|
|
146
|
+
|
|
147
|
+
export {
|
|
148
|
+
Fields,
|
|
149
|
+
variant,
|
|
150
|
+
ref_,
|
|
151
|
+
}
|
|
152
|
+
/* Not a pure module */
|
|
@@ -109,7 +109,7 @@ describe("Message should", () => {
|
|
|
109
109
|
extensionProtocols: [],
|
|
110
110
|
apiSchemaFragment: None,
|
|
111
111
|
apiTarget: None,
|
|
112
|
-
structure: Some({
|
|
112
|
+
structure: Some(Reventless.Offload.Inline({
|
|
113
113
|
readModels: [],
|
|
114
114
|
stateViewSlices: [],
|
|
115
115
|
stateChangeSlices: [],
|
|
@@ -132,7 +132,7 @@ describe("Message should", () => {
|
|
|
132
132
|
extensionPoints: None,
|
|
133
133
|
requiredStores: None,
|
|
134
134
|
requiredStoreDeclarations: None,
|
|
135
|
-
}),
|
|
135
|
+
})),
|
|
136
136
|
dcbEventLog: None,
|
|
137
137
|
kind: Domain,
|
|
138
138
|
}
|
|
@@ -186,7 +186,8 @@ describe("Message should", () => {
|
|
|
186
186
|
},
|
|
187
187
|
)->toBe("Domain")
|
|
188
188
|
expect(d.apiSchemaFragment->Option.isNone)->toBe(true)
|
|
189
|
-
let structure: Reventless.Plugin.pluginStructure =
|
|
189
|
+
let structure: Reventless.Plugin.pluginStructure =
|
|
190
|
+
d.structure->Option.flatMap(Reventless.Offload.getInline)->Option.getOrThrow
|
|
190
191
|
let agg = structure.aggregates->Array.getUnsafe(0)
|
|
191
192
|
expect(agg.events->Array.length)->toBe(0)
|
|
192
193
|
expect(agg.chapter->Option.isNone)->toBe(true)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Offload$Reventless from "@reventlessdev/reventless-spec/src/semantic/Offload.res.mjs";
|
|
5
6
|
import * as Message$ReventlessCore from "../../src/Message.res.mjs";
|
|
6
7
|
import * as PluginSpec$ReventlessCore from "../../src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
7
8
|
|
|
@@ -100,26 +101,29 @@ globalThis.describe("Message should", () => {
|
|
|
100
101
|
apiSchemaFragment: undefined,
|
|
101
102
|
apiTarget: undefined,
|
|
102
103
|
structure: {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
104
|
+
TAG: "Inline",
|
|
105
|
+
_0: {
|
|
106
|
+
readModels: [],
|
|
107
|
+
stateViewSlices: [],
|
|
108
|
+
stateChangeSlices: [],
|
|
109
|
+
aggregates: [{
|
|
110
|
+
name: "Order",
|
|
111
|
+
commands: [],
|
|
112
|
+
producedEventTypes: [],
|
|
113
|
+
consumedEventTypes: [],
|
|
114
|
+
linkedViews: [],
|
|
115
|
+
consistencyRead: undefined,
|
|
116
|
+
events: [],
|
|
117
|
+
chapter: undefined
|
|
118
|
+
}],
|
|
119
|
+
automationSlices: [],
|
|
120
|
+
outboundTranslationSlices: [],
|
|
121
|
+
inboundTranslationSlices: [],
|
|
122
|
+
extensions: [],
|
|
123
|
+
extensionPoints: undefined,
|
|
124
|
+
requiredStores: undefined,
|
|
125
|
+
requiredStoreDeclarations: undefined
|
|
126
|
+
}
|
|
123
127
|
},
|
|
124
128
|
dcbEventLog: undefined,
|
|
125
129
|
kind: "Domain"
|
|
@@ -169,7 +173,7 @@ globalThis.describe("Message should", () => {
|
|
|
169
173
|
tmp = match === "Domain" ? "Domain" : "other";
|
|
170
174
|
globalThis.expect(tmp).toBe("Domain");
|
|
171
175
|
globalThis.expect(Stdlib_Option.isNone(d$1.apiSchemaFragment)).toBe(true);
|
|
172
|
-
let structure = Stdlib_Option.getOrThrow(d$1.structure, undefined);
|
|
176
|
+
let structure = Stdlib_Option.getOrThrow(Stdlib_Option.flatMap(d$1.structure, Offload$Reventless.getInline), undefined);
|
|
173
177
|
let agg = structure.aggregates[0];
|
|
174
178
|
globalThis.expect(agg.events.length).toBe(0);
|
|
175
179
|
globalThis.expect(Stdlib_Option.isNone(agg.chapter)).toBe(true);
|