@reventlessdev/reventless-spec 3.0.0-alpha.124 → 3.0.0-alpha.126
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 +33 -0
- package/package.json +5 -2
- package/run-certify-trait.mjs +2 -0
- package/run-graft-trait.mjs +2 -0
- package/run-trait-manifest.mjs +2 -0
- package/schema/platform-api.graphql +17 -3
- package/src/components/Aggregate.res +23 -0
- package/src/components/AutomationSlice.res +29 -3
- package/src/components/CapabilityManifest.res +52 -24
- package/src/components/CapabilityManifest.res.mjs +35 -11
- package/src/components/InboundTranslationSlice.res +14 -0
- package/src/components/OutboundTranslationSlice.res +24 -0
- package/src/components/Plugin.res +55 -2
- package/src/components/Plugin.res.mjs +23 -1
- package/src/components/StateAnnotations.res +2 -2
- package/src/components/StateChangeSlice.res +22 -0
- package/src/components/TraitCertificate.res +105 -0
- package/src/components/TraitCertificate.res.mjs +65 -0
- package/src/components/TraitManifest.res +90 -0
- package/src/components/TraitManifest.res.mjs +48 -0
- package/src/generator/CertifyTrait.res +190 -0
- package/src/generator/CertifyTrait.res.mjs +154 -0
- package/src/generator/GraftTrait.res +230 -0
- package/src/generator/GraftTrait.res.mjs +193 -0
- package/src/generator/PlatformCodegen.res +44 -30
- package/src/generator/PlatformCodegen.res.mjs +36 -17
- package/src/generator/TraitManifestCli.res +138 -0
- package/src/generator/TraitManifestCli.res.mjs +105 -0
- package/src/semantic/Capabilities.res +19 -3
- package/src/semantic/Capabilities.res.mjs +18 -2
- package/src/semantic/CapabilityNeed.res +81 -0
- package/src/semantic/CapabilityNeed.res.mjs +46 -0
- package/src/semantic/Messaging.res +127 -0
- package/src/semantic/Messaging.res.mjs +57 -0
- package/src/types/Trait.res +62 -0
- package/src/types/Trait.res.mjs +18 -0
- package/src/types/Transition.res +70 -0
- package/src/types/Transition.res.mjs +36 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,39 @@
|
|
|
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.126 (2026-09-01)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-spec
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.125 (2026-09-01)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **automation:** a mapping is handed the envelope's id ([9ee482c](https://github.com/ReventlessDev/reventless-core/commit/9ee482c5ad0aae09efd1259eeb7f39b781867b91))
|
|
19
|
+
* **capabilities:** a plugin's geocoding need was declared nowhere and failed silently ([67917dd](https://github.com/ReventlessDev/reventless-core/commit/67917dd504b43fa78b7c6a51644c9eae656b7f6b))
|
|
20
|
+
* feat(example)!: model product and category images as attachment sets ([6ae18d8](https://github.com/ReventlessDev/reventless-core/commit/6ae18d896215b448177ba0516e74bfda5f88d2db))
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* **capabilities:** a plugin can send a message without naming a provider ([b001a1e](https://github.com/ReventlessDev/reventless-core/commit/b001a1e9361a0f4d0affe228cb4c08c38a5a995e))
|
|
24
|
+
* **plugin:** the admin lifecycle commands name their argument ([ea552ec](https://github.com/ReventlessDev/reventless-core/commit/ea552ec64e5db0b3dc465c0d9a80cac89627f727))
|
|
25
|
+
* **spec:** a command declares its lifecycle edge as a value ([40eee9f](https://github.com/ReventlessDev/reventless-core/commit/40eee9f7723dc05e418be680528f01967d074da4))
|
|
26
|
+
* **spec:** a graft leaves a trace the deployed plugin can read ([c08ff6c](https://github.com/ReventlessDev/reventless-core/commit/c08ff6c0f6177d58603e7ae1e5cec392d9bac16a))
|
|
27
|
+
* **spec:** graft-trait, a CLI that runs a trait's emitter ([1b87399](https://github.com/ReventlessDev/reventless-core/commit/1b87399987033765d6a1ee8ea22ab7f02056e9c0))
|
|
28
|
+
* **traits:** a conformance run leaves something a machine can read ([cd9cb81](https://github.com/ReventlessDev/reventless-core/commit/cd9cb81aa643f8e30ccf072df458fdc136897746))
|
|
29
|
+
* **traits:** a listing reads a trait instead of being told about it ([8a23219](https://github.com/ReventlessDev/reventless-core/commit/8a23219c5a69011ef9310ebf8bfcbf9315a577ba))
|
|
30
|
+
|
|
31
|
+
### BREAKING CHANGES
|
|
32
|
+
|
|
33
|
+
* ProductAdded/CategoryAdded lose their image field and
|
|
34
|
+
ChangeProductImage/ChangeCategoryImage are replaced; the alpha event log is
|
|
35
|
+
wiped on the next deploy.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
6
39
|
# 3.0.0-alpha.124 (2026-08-27)
|
|
7
40
|
|
|
8
41
|
* feat(spec)!: reflect the command direction across a port, both halves ([f2fe258](https://github.com/ReventlessDev/reventless-core/commit/f2fe258d195b74f4a61488edee305665341020ea))
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-spec",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.126",
|
|
4
4
|
"description": "Specifications for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
7
7
|
"generate-plugin": "./run-generator.mjs",
|
|
8
|
-
"generate-platform": "./run-platform-generator.mjs"
|
|
8
|
+
"generate-platform": "./run-platform-generator.mjs",
|
|
9
|
+
"graft-trait": "./run-graft-trait.mjs",
|
|
10
|
+
"certify-trait": "./run-certify-trait.mjs",
|
|
11
|
+
"trait-manifest": "./run-trait-manifest.mjs"
|
|
9
12
|
},
|
|
10
13
|
"jest": {
|
|
11
14
|
"testMatch": [
|
|
@@ -18,9 +18,9 @@ union CommandResult = CommandAccepted | CommandPending | CommandRejected
|
|
|
18
18
|
|
|
19
19
|
type Mutation {
|
|
20
20
|
Platform_PluginStatusChanged(pluginId: ID!, status: PluginStatus!): PluginStatusChangeEvent
|
|
21
|
-
Platform_Plugin_Activate(
|
|
22
|
-
Platform_Plugin_Deactivate(
|
|
23
|
-
Platform_Plugin_Retire(
|
|
21
|
+
Platform_Plugin_Activate(id: ID!, version: String!): CommandResult!
|
|
22
|
+
Platform_Plugin_Deactivate(id: ID!, version: String!): CommandResult!
|
|
23
|
+
Platform_Plugin_Retire(id: ID!, version: String!): CommandResult!
|
|
24
24
|
Platform_UIFragmentDeregistered(pluginId: ID!): UIFragmentChangeEvent
|
|
25
25
|
Platform_UIFragmentRegistered(manifest: String, pluginId: ID!): UIFragmentChangeEvent
|
|
26
26
|
Platform_UIFragmentUpdated(manifest: String, pluginId: ID!): UIFragmentChangeEvent
|
|
@@ -225,10 +225,12 @@ type Platform_PluginStructureEntry {
|
|
|
225
225
|
outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!
|
|
226
226
|
pluginId: String!
|
|
227
227
|
readModels: [Platform_ReadSideDef!]!
|
|
228
|
+
requiredCapabilities: [Platform_RequiredCapabilityDeclaration!]
|
|
228
229
|
requiredStoreDeclarations: [Platform_RequiredStoreDeclaration!]
|
|
229
230
|
requiredStores: [String!]
|
|
230
231
|
stateChangeSlices: [Platform_WriteSideDef!]!
|
|
231
232
|
stateViewSlices: [Platform_ReadSideDef!]!
|
|
233
|
+
traitDeclarations: [Platform_TraitDeclaration!]
|
|
232
234
|
}
|
|
233
235
|
|
|
234
236
|
type Platform_PublishedEventDef {
|
|
@@ -258,6 +260,11 @@ type Platform_ReadSideDef {
|
|
|
258
260
|
visibility: String
|
|
259
261
|
}
|
|
260
262
|
|
|
263
|
+
type Platform_RequiredCapabilityDeclaration {
|
|
264
|
+
capability: String!
|
|
265
|
+
component: String!
|
|
266
|
+
}
|
|
267
|
+
|
|
261
268
|
type Platform_RequiredStoreDeclaration {
|
|
262
269
|
annotation: String
|
|
263
270
|
component: String!
|
|
@@ -265,6 +272,13 @@ type Platform_RequiredStoreDeclaration {
|
|
|
265
272
|
store: String!
|
|
266
273
|
}
|
|
267
274
|
|
|
275
|
+
type Platform_TraitDeclaration {
|
|
276
|
+
component: String!
|
|
277
|
+
posture: String!
|
|
278
|
+
trait: String!
|
|
279
|
+
version: String!
|
|
280
|
+
}
|
|
281
|
+
|
|
268
282
|
type Platform_UIFragmentEntry {
|
|
269
283
|
pages: [Platform_UIPage!]!
|
|
270
284
|
panels: [Platform_UIPanel!]!
|
|
@@ -61,4 +61,27 @@ module type Spec = {
|
|
|
61
61
|
`AllowAuthenticated`; override at the file/module level with
|
|
62
62
|
`@@reventless.authorize(<rule>)`. */
|
|
63
63
|
let commandAuthorization: command => Authorization.permission
|
|
64
|
+
|
|
65
|
+
/** The lifecycle enum this component's commands move a row through — the
|
|
66
|
+
linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
|
|
67
|
+
Auto-injected as `unit` alongside the default below; a host that declares
|
|
68
|
+
`commandTransition` declares this too, and the pair is what makes every
|
|
69
|
+
edge name one lifecycle. */
|
|
70
|
+
type lifecycleState
|
|
71
|
+
|
|
72
|
+
/** The lifecycle edge each command owns, read while the plugin structure is
|
|
73
|
+
assembled. Auto-injected as `_ => Unrestricted` by `@@reventless.spec`, so
|
|
74
|
+
a component whose commands guard nothing needs no line; a host that writes
|
|
75
|
+
the switch by hand gets an exhaustive one over typed states. Refused rather
|
|
76
|
+
than injected when the command type splices, since a spliced command must
|
|
77
|
+
be answered for. See `Transition`. */
|
|
78
|
+
let commandTransition: command => Transition.t<lifecycleState>
|
|
79
|
+
|
|
80
|
+
/** The domain traits grafted into this component, as values the trait packages
|
|
81
|
+
export — `[TraitAttachments.Attachments.declaration]`. Auto-injected as `[]`
|
|
82
|
+
by `@@reventless.spec`, so a component that is nobody's graft says so without
|
|
83
|
+
a line. A graft names its trait here and the structure records it, which is
|
|
84
|
+
the only way a deployed plugin can answer "where did this come from". See
|
|
85
|
+
`Trait`. */
|
|
86
|
+
let traits: array<Trait.t>
|
|
64
87
|
}
|
|
@@ -83,6 +83,16 @@ module type Spec = {
|
|
|
83
83
|
// validation step run before each command publish. `process` remains shared in
|
|
84
84
|
// the slice's `Automation` module since it operates on `todoItem` regardless of
|
|
85
85
|
// the originating source.
|
|
86
|
+
//
|
|
87
|
+
// `collect` is handed `~sourceId` alongside the event, so a mapping over an
|
|
88
|
+
// Aggregate source can key its todo item even though the payload does not repeat
|
|
89
|
+
// the id that addressed it — `Registered({email, address})` is the case this
|
|
90
|
+
// exists for. A DCB event usually names its own subject and can ignore it.
|
|
91
|
+
//
|
|
92
|
+
// An `OutboundTranslationSlice` remains the better shape for a relay that simply
|
|
93
|
+
// forwards: its item completes when the command is published rather than when an
|
|
94
|
+
// answering event arrives, so the target command is free to be idempotent instead
|
|
95
|
+
// of having to emit a fact whose only job is closing the row.
|
|
86
96
|
|
|
87
97
|
/**
|
|
88
98
|
Ambient deployment context plumbed to every mapping function.
|
|
@@ -125,7 +135,15 @@ module type Mapping = {
|
|
|
125
135
|
type command
|
|
126
136
|
let sourceEventSchema: S.t<sourceEvent>
|
|
127
137
|
let sourceName: string
|
|
128
|
-
|
|
138
|
+
/**
|
|
139
|
+
`~sourceId` is the id of the entity the event was published for — the
|
|
140
|
+
envelope's `id`, not part of the event payload. A DCB event usually names its
|
|
141
|
+
own subject (`OrderPlaced({orderId, …})`) and can ignore this; an Aggregate's
|
|
142
|
+
event generally does not, because the aggregate id is what addressed it in the
|
|
143
|
+
first place. Without it a mapping over `Registered({email, address})` would
|
|
144
|
+
have no way to say which customer its todo item is for.
|
|
145
|
+
*/
|
|
146
|
+
let collect: (sourceEvent, ~sourceId: string, context) => array<(string, todoItem)>
|
|
129
147
|
let resolve: sourceEvent => option<string>
|
|
130
148
|
}
|
|
131
149
|
|
|
@@ -199,7 +217,15 @@ module type MappingImpl = {
|
|
|
199
217
|
type sourceEvent
|
|
200
218
|
type todoItem
|
|
201
219
|
type command
|
|
202
|
-
|
|
220
|
+
/**
|
|
221
|
+
`~sourceId` is the id of the entity the event was published for — the
|
|
222
|
+
envelope's `id`, not part of the event payload. A DCB event usually names its
|
|
223
|
+
own subject (`OrderPlaced({orderId, …})`) and can ignore this; an Aggregate's
|
|
224
|
+
event generally does not, because the aggregate id is what addressed it in the
|
|
225
|
+
first place. Without it a mapping over `Registered({email, address})` would
|
|
226
|
+
have no way to say which customer its todo item is for.
|
|
227
|
+
*/
|
|
228
|
+
let collect: (sourceEvent, ~sourceId: string, context) => array<(string, todoItem)>
|
|
203
229
|
let resolve: sourceEvent => option<string>
|
|
204
230
|
}
|
|
205
231
|
|
|
@@ -242,7 +268,7 @@ module FromOrderShipped = Reventless.AutomationSlice.Mapping.Make(
|
|
|
242
268
|
OrderSpec, // Source: Aggregate spec module
|
|
243
269
|
AutoFulfillmentSpec, // Target: this slice's spec
|
|
244
270
|
{
|
|
245
|
-
let collect = (event, _ctx) =>
|
|
271
|
+
let collect = (event, ~sourceId as _, _ctx) =>
|
|
246
272
|
switch event {
|
|
247
273
|
| OrderSpec.OrderShipped({orderId, productId}) =>
|
|
248
274
|
[(orderId ++ ":" ++ productId, {AutoFulfillmentSpec.orderId, productId})]
|
|
@@ -2,29 +2,38 @@
|
|
|
2
2
|
The per-plugin capability manifest — `capabilities.json`, written beside the
|
|
3
3
|
generated `Plugin.res` by the plugin package's build.
|
|
4
4
|
|
|
5
|
-
Each entry states one capability the plugin
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
`pluginStructure
|
|
9
|
-
structure, never a second scan of the sources, so the two cannot
|
|
10
|
-
fact differently.
|
|
5
|
+
Each entry states one capability the plugin declares it needs, keyed by that
|
|
6
|
+
capability's identity — the qualified `{plugin}.{store}` for an object store, the
|
|
7
|
+
capability's own name for everything else — with the declaring sites as
|
|
8
|
+
provenance. The keys are taken verbatim from `pluginStructure`: the manifest is a
|
|
9
|
+
rendering of the structure, never a second scan of the sources, so the two cannot
|
|
10
|
+
spell one fact differently.
|
|
11
11
|
|
|
12
12
|
The platform generator unions these files across a deployment's plugins and
|
|
13
13
|
emits the platform's capability list from them.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
@schema
|
|
17
|
-
type kind =
|
|
17
|
+
type kind =
|
|
18
|
+
| ObjectStore
|
|
19
|
+
/** Address geocoding, reached through `Capabilities.geocode`. Declared by a
|
|
20
|
+
slice rather than by a field, so its entry carries no `field`. */
|
|
21
|
+
| Geocoding
|
|
22
|
+
/** Sending a message, reached through `Capabilities.messaging`. Slice-declared
|
|
23
|
+
like `Geocoding`, and for the same reason carries no `field`. */
|
|
24
|
+
| Messaging
|
|
18
25
|
|
|
19
|
-
/** The declaration site: the component's spec name and
|
|
20
|
-
`@storageRef` annotation
|
|
26
|
+
/** The declaration site: the component's spec name, and — for a store — the field
|
|
27
|
+
carrying the `@storageRef` annotation plus the store exactly as that field
|
|
28
|
+
spells it.
|
|
21
29
|
|
|
22
|
-
`annotation`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
`field` and `annotation` are optional. `annotation` because a manifest emitted
|
|
31
|
+
before it existed must still parse, and a reader that cannot say what the
|
|
32
|
+
source says omits the claim rather than inventing one; `field` because a
|
|
33
|
+
capability a slice declares has no declaring field, and naming one would be a
|
|
34
|
+
fiction. Every store entry emitted now carries both. */
|
|
26
35
|
@schema
|
|
27
|
-
type provenance = {component: string, field
|
|
36
|
+
type provenance = {component: string, field?: string, annotation?: string}
|
|
28
37
|
|
|
29
38
|
@schema
|
|
30
39
|
type entry = {
|
|
@@ -39,17 +48,18 @@ type t = {capabilities: array<entry>}
|
|
|
39
48
|
/**
|
|
40
49
|
Build the manifest from a plugin's structure.
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
to what the deployed plugin reports at runtime — the contract the
|
|
44
|
-
coverage assertion checks against.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
Store keys iterate `requiredStores` itself, so a manifest's key set is
|
|
52
|
+
byte-identical to what the deployed plugin reports at runtime — the contract the
|
|
53
|
+
deploy-time coverage assertion checks against. Capability entries follow, one per
|
|
54
|
+
distinct capability, ordered by name so the file does not churn. A structure with
|
|
55
|
+
no declarations yields an empty `capabilities` list, not an absent file:
|
|
56
|
+
"declares nothing" is a statement, and the generator reading the manifests must
|
|
57
|
+
be able to tell it apart from "was never built".
|
|
48
58
|
*/
|
|
49
59
|
let fromStructure = (structure: Plugin.pluginStructure): t => {
|
|
50
60
|
let declarations = structure.requiredStoreDeclarations->Option.getOr([])
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
let stores =
|
|
62
|
+
structure.requiredStores
|
|
53
63
|
->Option.getOr([])
|
|
54
64
|
->Array.map(key => {
|
|
55
65
|
kind: ObjectStore,
|
|
@@ -59,8 +69,26 @@ let fromStructure = (structure: Plugin.pluginStructure): t => {
|
|
|
59
69
|
? Some({component: d.component, field: d.field, annotation: ?d.annotation})
|
|
60
70
|
: None
|
|
61
71
|
),
|
|
62
|
-
})
|
|
63
|
-
|
|
72
|
+
})
|
|
73
|
+
let needs = structure.requiredCapabilities->Option.getOr([])
|
|
74
|
+
let capabilityKeys =
|
|
75
|
+
needs->Array.map(d => d.capability)->Belt.Set.String.fromArray->Belt.Set.String.toArray
|
|
76
|
+
let capabilities = capabilityKeys->Array.filterMap(key =>
|
|
77
|
+
// An unrecognised capability is dropped rather than passed through: the
|
|
78
|
+
// generator downstream renders a real `Platform.capability` arm, and a name
|
|
79
|
+
// this build cannot map has no arm to render.
|
|
80
|
+
CapabilityNeed.fromString(key)->Option.map(need => {
|
|
81
|
+
kind: switch need {
|
|
82
|
+
| Geocoding => Geocoding
|
|
83
|
+
| Messaging => Messaging
|
|
84
|
+
},
|
|
85
|
+
key,
|
|
86
|
+
declaredBy: needs->Array.filterMap(d =>
|
|
87
|
+
d.capability == key ? Some({component: d.component}) : None
|
|
88
|
+
),
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
{capabilities: Array.concat(stores, capabilities)}
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
/** Deterministic rendering: 2-space indent, trailing newline. Rebuilding with
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
import * as Sury from "sury";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
6
7
|
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
8
|
+
import * as CapabilityNeed$Reventless from "../semantic/CapabilityNeed.res.mjs";
|
|
7
9
|
|
|
8
|
-
let kindSchema = Sury.
|
|
10
|
+
let kindSchema = Sury.union([
|
|
11
|
+
Sury.literal("ObjectStore"),
|
|
12
|
+
Sury.literal("Geocoding"),
|
|
13
|
+
Sury.literal("Messaging")
|
|
14
|
+
]);
|
|
9
15
|
|
|
10
16
|
let provenanceSchema = Sury.$schema(s => ({
|
|
11
17
|
component: s.m(Sury.string),
|
|
12
|
-
field: s.m(Sury.string),
|
|
18
|
+
field: s.m(Sury.$option(Sury.string)),
|
|
13
19
|
annotation: s.m(Sury.$option(Sury.string))
|
|
14
20
|
}));
|
|
15
21
|
|
|
@@ -25,20 +31,38 @@ let schema = Sury.$schema(s => ({
|
|
|
25
31
|
|
|
26
32
|
function fromStructure(structure) {
|
|
27
33
|
let declarations = Stdlib_Option.getOr(structure.requiredStoreDeclarations, []);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
let stores = Stdlib_Option.getOr(structure.requiredStores, []).map(key => ({
|
|
35
|
+
kind: "ObjectStore",
|
|
36
|
+
key: key,
|
|
37
|
+
declaredBy: Stdlib_Array.filterMap(declarations, d => {
|
|
38
|
+
if (d.store === key) {
|
|
39
|
+
return {
|
|
40
|
+
component: d.component,
|
|
41
|
+
field: d.field,
|
|
42
|
+
annotation: d.annotation
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}));
|
|
47
|
+
let needs = Stdlib_Option.getOr(structure.requiredCapabilities, []);
|
|
48
|
+
let capabilityKeys = Belt_SetString.toArray(Belt_SetString.fromArray(needs.map(d => d.capability)));
|
|
49
|
+
let capabilities = Stdlib_Array.filterMap(capabilityKeys, key => Stdlib_Option.map(CapabilityNeed$Reventless.fromString(key), need => {
|
|
50
|
+
let tmp;
|
|
51
|
+
tmp = need === "Geocoding" ? "Geocoding" : "Messaging";
|
|
52
|
+
return {
|
|
53
|
+
kind: tmp,
|
|
31
54
|
key: key,
|
|
32
|
-
declaredBy: Stdlib_Array.filterMap(
|
|
33
|
-
if (d.
|
|
55
|
+
declaredBy: Stdlib_Array.filterMap(needs, d => {
|
|
56
|
+
if (d.capability === key) {
|
|
34
57
|
return {
|
|
35
|
-
component: d.component
|
|
36
|
-
field: d.field,
|
|
37
|
-
annotation: d.annotation
|
|
58
|
+
component: d.component
|
|
38
59
|
};
|
|
39
60
|
}
|
|
40
61
|
})
|
|
41
|
-
}
|
|
62
|
+
};
|
|
63
|
+
}));
|
|
64
|
+
return {
|
|
65
|
+
capabilities: stores.concat(capabilities)
|
|
42
66
|
};
|
|
43
67
|
}
|
|
44
68
|
|
|
@@ -62,6 +62,20 @@ module type Spec = {
|
|
|
62
62
|
on structurally-detected inline spec modules — defaults to
|
|
63
63
|
`AllowAuthenticated`. */
|
|
64
64
|
let commandAuthorization: command => Authorization.permission
|
|
65
|
+
|
|
66
|
+
/** The lifecycle enum this component's commands move a row through — the
|
|
67
|
+
linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
|
|
68
|
+
Auto-injected as `unit` alongside the default below; a host that declares
|
|
69
|
+
`commandTransition` declares this too, and the pair is what makes every
|
|
70
|
+
edge name one lifecycle. */
|
|
71
|
+
type lifecycleState
|
|
72
|
+
|
|
73
|
+
/** The lifecycle edge each command owns, read while the plugin structure is
|
|
74
|
+
assembled. Auto-injected as `_ => Unrestricted` by `@@reventless.spec`,
|
|
75
|
+
so a component whose commands guard nothing needs no line; a host that
|
|
76
|
+
writes the switch by hand gets an exhaustive one over typed states.
|
|
77
|
+
See `Transition`. */
|
|
78
|
+
let commandTransition: command => Transition.t<lifecycleState>
|
|
65
79
|
}
|
|
66
80
|
|
|
67
81
|
/**
|
|
@@ -106,6 +106,30 @@ module type Spec = {
|
|
|
106
106
|
in the Event Graph / Context Map.
|
|
107
107
|
Auto-injected by `@@reventless.spec` defaulting to `None` — set it to name the box. */
|
|
108
108
|
let externalSystem: option<string>
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
The platform capabilities this slice's `translate` reaches for.
|
|
112
|
+
|
|
113
|
+
`[]` — the common case — means `translate` calls a service the framework does
|
|
114
|
+
not broker, and the deployment provisions nothing on its behalf. Naming a
|
|
115
|
+
capability makes the need a checked fact: it reaches `capabilities.json`, the
|
|
116
|
+
platform's generated capability list, and the deploy-time gate, which refuses a
|
|
117
|
+
plugin whose platform provisions none of it.
|
|
118
|
+
|
|
119
|
+
Declared rather than inferred because what `translate` reads off
|
|
120
|
+
`Capabilities.t` is only visible in its body, and provisioning infrastructure
|
|
121
|
+
from a guess at a function body is not a service. A trait exports the value for
|
|
122
|
+
its host to name, so grafting one cannot leave the need unstated.
|
|
123
|
+
*/
|
|
124
|
+
let capabilityNeeds: array<CapabilityNeed.t>
|
|
125
|
+
|
|
126
|
+
/** The domain traits grafted into this component, as values the trait packages
|
|
127
|
+
export — `[TraitAttachments.Attachments.declaration]`. Auto-injected as `[]`
|
|
128
|
+
by `@@reventless.spec`, so a component that is nobody's graft says so without
|
|
129
|
+
a line. A graft names its trait here and the structure records it, which is
|
|
130
|
+
the only way a deployed plugin can answer "where did this come from". See
|
|
131
|
+
`Trait`. */
|
|
132
|
+
let traits: array<Trait.t>
|
|
109
133
|
}
|
|
110
134
|
|
|
111
135
|
/**
|
|
@@ -137,10 +137,10 @@ type commandDef = {
|
|
|
137
137
|
aggregateIdField: @s.matches(stringOptionSchema) option<string>,
|
|
138
138
|
mutationField: string,
|
|
139
139
|
references: array<fieldReference>,
|
|
140
|
-
/** The
|
|
140
|
+
/** The declared *from* set — lifecycle states this command is meaningful in.
|
|
141
141
|
`None` means always available; `Some([])` means never show. */
|
|
142
142
|
allowedStates: @s.matches(stringArrayOptionSchema) option<array<string>>,
|
|
143
|
-
/** The
|
|
143
|
+
/** The declared *to* state this command's handler writes. `None` with a
|
|
144
144
|
from-set present means the command does not move the row. */
|
|
145
145
|
targetState: @s.matches(stringOptionSchema) option<string>,
|
|
146
146
|
/** Whether the variant is exposed in the generated API (non-`@noApi`). */
|
|
@@ -380,6 +380,47 @@ type requiredStoreDeclaration = {
|
|
|
380
380
|
let requiredStoreDeclarationArrayOptionSchema =
|
|
381
381
|
S.array(requiredStoreDeclarationSchema)->S.nullAsOption
|
|
382
382
|
|
|
383
|
+
/**
|
|
384
|
+
One component's capability requirement, with its provenance.
|
|
385
|
+
|
|
386
|
+
`capability` is `CapabilityNeed.toString` — a string rather than an enum so a
|
|
387
|
+
plugin built against a newer framework still decodes here; `component` names the
|
|
388
|
+
slice that declared it, so a diff can say which component added or removed the
|
|
389
|
+
need. Unlike a store there is no field: what a `translate` reaches for is not
|
|
390
|
+
expressible as an annotation on one, which is why the need is declared.
|
|
391
|
+
*/
|
|
392
|
+
@schema
|
|
393
|
+
type requiredCapabilityDeclaration = {capability: string, component: string}
|
|
394
|
+
|
|
395
|
+
let requiredCapabilityDeclarationArrayOptionSchema =
|
|
396
|
+
S.array(requiredCapabilityDeclarationSchema)->S.nullAsOption
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
One graft's provenance: which trait, at which version, on which component.
|
|
400
|
+
|
|
401
|
+
Strings rather than the `Trait.t` variant for `posture`, on the same rule the
|
|
402
|
+
capability above follows — a plugin built against a newer framework, naming a
|
|
403
|
+
posture this one has never heard of, still decodes here rather than failing the
|
|
404
|
+
whole structure.
|
|
405
|
+
|
|
406
|
+
`component` is not declared by the trait or by the host: the structure fills it in
|
|
407
|
+
while it walks the components, because it is the only party that knows which one
|
|
408
|
+
carried the declaration. Nothing in this record is a string a developer typed.
|
|
409
|
+
|
|
410
|
+
It records ORIGIN, not behaviour — a grafted file is the host's to edit
|
|
411
|
+
afterwards. What answers "does it still behave like the trait" is the trait's own
|
|
412
|
+
conformance suite, which runs in the consumer's build and is not this field.
|
|
413
|
+
*/
|
|
414
|
+
@schema
|
|
415
|
+
type traitDeclaration = {
|
|
416
|
+
trait: string,
|
|
417
|
+
version: string,
|
|
418
|
+
posture: string,
|
|
419
|
+
component: string,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let traitDeclarationArrayOptionSchema = S.array(traitDeclarationSchema)->S.nullAsOption
|
|
423
|
+
|
|
383
424
|
/**
|
|
384
425
|
Adding a field here? It must be a shape a stale event can be healed into — the
|
|
385
426
|
lifecycle aggregate replays its own log before every decision, so one event that
|
|
@@ -409,6 +450,18 @@ type pluginStructure = {
|
|
|
409
450
|
`requiredStores` is derived from it, so the two cannot disagree. */
|
|
410
451
|
requiredStoreDeclarations: @s.matches(requiredStoreDeclarationArrayOptionSchema)
|
|
411
452
|
option<array<requiredStoreDeclaration>>,
|
|
453
|
+
/** The platform capabilities this plugin's components declare they need, one
|
|
454
|
+
entry per declaring component. Object stores are not here — a store need is
|
|
455
|
+
a field's, and travels as `requiredStores`. Absent → None, read as []. */
|
|
456
|
+
requiredCapabilities: @s.matches(requiredCapabilityDeclarationArrayOptionSchema)
|
|
457
|
+
option<array<requiredCapabilityDeclaration>>,
|
|
458
|
+
/** The domain traits grafted into this plugin, one entry per declaring
|
|
459
|
+
component. Absent → None, read as []. The only signal a graft leaves that
|
|
460
|
+
survives into a deployed plugin — every other one (the dependency, the
|
|
461
|
+
variant spread, the rules alias, the conformance binding) is source-side.
|
|
462
|
+
A claim about origin, never about behaviour: see `Trait`. */
|
|
463
|
+
traitDeclarations: @s.matches(traitDeclarationArrayOptionSchema)
|
|
464
|
+
option<array<traitDeclaration>>,
|
|
412
465
|
}
|
|
413
466
|
|
|
414
467
|
let pluginStructureOffloadSchema = Offload.optionSchema(~store="pluginStructures", pluginStructureSchema)
|
|
@@ -240,6 +240,22 @@ let requiredStoreDeclarationSchema = Sury.$schema(s => ({
|
|
|
240
240
|
|
|
241
241
|
let requiredStoreDeclarationArrayOptionSchema = Sury.$nullAsOption(Sury.array(requiredStoreDeclarationSchema));
|
|
242
242
|
|
|
243
|
+
let requiredCapabilityDeclarationSchema = Sury.$schema(s => ({
|
|
244
|
+
capability: s.m(Sury.string),
|
|
245
|
+
component: s.m(Sury.string)
|
|
246
|
+
}));
|
|
247
|
+
|
|
248
|
+
let requiredCapabilityDeclarationArrayOptionSchema = Sury.$nullAsOption(Sury.array(requiredCapabilityDeclarationSchema));
|
|
249
|
+
|
|
250
|
+
let traitDeclarationSchema = Sury.$schema(s => ({
|
|
251
|
+
trait: s.m(Sury.string),
|
|
252
|
+
version: s.m(Sury.string),
|
|
253
|
+
posture: s.m(Sury.string),
|
|
254
|
+
component: s.m(Sury.string)
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
let traitDeclarationArrayOptionSchema = Sury.$nullAsOption(Sury.array(traitDeclarationSchema));
|
|
258
|
+
|
|
243
259
|
let pluginStructureSchema = Sury.$schema(s => ({
|
|
244
260
|
readModels: s.m(Sury.array(queryableDefSchema)),
|
|
245
261
|
stateViewSlices: s.m(Sury.array(queryableDefSchema)),
|
|
@@ -251,7 +267,9 @@ let pluginStructureSchema = Sury.$schema(s => ({
|
|
|
251
267
|
extensions: s.m(Sury.array(extensionDefSchema)),
|
|
252
268
|
extensionPoints: s.m(extensionPointDefArrayOptionSchema),
|
|
253
269
|
requiredStores: s.m(stringArrayOptionSchema),
|
|
254
|
-
requiredStoreDeclarations: s.m(requiredStoreDeclarationArrayOptionSchema)
|
|
270
|
+
requiredStoreDeclarations: s.m(requiredStoreDeclarationArrayOptionSchema),
|
|
271
|
+
requiredCapabilities: s.m(requiredCapabilityDeclarationArrayOptionSchema),
|
|
272
|
+
traitDeclarations: s.m(traitDeclarationArrayOptionSchema)
|
|
255
273
|
}));
|
|
256
274
|
|
|
257
275
|
let pluginStructureOffloadSchema = Offload$Reventless.optionSchema(undefined, "pluginStructures", undefined, pluginStructureSchema);
|
|
@@ -314,6 +332,10 @@ export {
|
|
|
314
332
|
extensionPointDefArrayOptionSchema,
|
|
315
333
|
requiredStoreDeclarationSchema,
|
|
316
334
|
requiredStoreDeclarationArrayOptionSchema,
|
|
335
|
+
requiredCapabilityDeclarationSchema,
|
|
336
|
+
requiredCapabilityDeclarationArrayOptionSchema,
|
|
337
|
+
traitDeclarationSchema,
|
|
338
|
+
traitDeclarationArrayOptionSchema,
|
|
317
339
|
pluginStructureSchema,
|
|
318
340
|
pluginStructureOffloadSchema,
|
|
319
341
|
pluginDefinitionSchema,
|
|
@@ -54,7 +54,7 @@ otherwise see the same negative marker on every record they can read.
|
|
|
54
54
|
two-valued enum.
|
|
55
55
|
- `Some(v)` — the **state** form. The row is retired when the field equals `v`,
|
|
56
56
|
and the field is the record's `@lifecycle` field. One field then carries the
|
|
57
|
-
fact once instead of twice, and
|
|
57
|
+
fact once instead of twice, and `Moves([Deactivated], Active)` on the
|
|
58
58
|
way-back command is enough to make a generated menu offer it there and nowhere
|
|
59
59
|
else —
|
|
60
60
|
because retirement is finally expressible in the vocabulary that stance is
|
|
@@ -158,7 +158,7 @@ type stateAnnotationSpec = {
|
|
|
158
158
|
metric: array<(string, metricSpec)>,
|
|
159
159
|
/**
|
|
160
160
|
Field annotated `@lifecycle` on the state record (PPX-emitted) — the enum a
|
|
161
|
-
command's
|
|
161
|
+
command's declared edge is written in terms of, a board draws its columns
|
|
162
162
|
from and a state diagram renders. `Some(name)` when one such annotation
|
|
163
163
|
exists; the PPX errors on duplicate `@lifecycle` annotations within the same
|
|
164
164
|
record. Codegen consumes this to populate `queryableDef.lifecycleField`, and
|
|
@@ -85,6 +85,28 @@ module type Spec = {
|
|
|
85
85
|
`@@reventless.authorize(<rule>)`. */
|
|
86
86
|
let commandAuthorization: command => Authorization.permission
|
|
87
87
|
|
|
88
|
+
/** The lifecycle enum this component's commands move a row through — the
|
|
89
|
+
linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
|
|
90
|
+
Auto-injected as `unit` alongside the default below; a host that declares
|
|
91
|
+
`commandTransition` declares this too, and the pair is what makes every
|
|
92
|
+
edge name one lifecycle. */
|
|
93
|
+
type lifecycleState
|
|
94
|
+
|
|
95
|
+
/** The lifecycle edge each command owns, read while the plugin structure is
|
|
96
|
+
assembled. Auto-injected as `_ => Unrestricted` by `@@reventless.spec`,
|
|
97
|
+
so a component whose commands guard nothing needs no line; a host that
|
|
98
|
+
writes the switch by hand gets an exhaustive one over typed states.
|
|
99
|
+
See `Transition`. */
|
|
100
|
+
let commandTransition: command => Transition.t<lifecycleState>
|
|
101
|
+
|
|
102
|
+
/** The domain traits grafted into this component, as values the trait packages
|
|
103
|
+
export — `[TraitAttachments.Attachments.declaration]`. Auto-injected as `[]`
|
|
104
|
+
by `@@reventless.spec`, so a component that is nobody's graft says so without
|
|
105
|
+
a line. A graft names its trait here and the structure records it, which is
|
|
106
|
+
the only way a deployed plugin can answer "where did this come from". See
|
|
107
|
+
`Trait`. */
|
|
108
|
+
let traits: array<Trait.t>
|
|
109
|
+
|
|
88
110
|
/** Decision-read consistency mode for this slice's optimistic-concurrency
|
|
89
111
|
retry loop. Auto-injected by `@@reventless.spec` and on
|
|
90
112
|
structurally-detected inline spec modules — defaults to
|