@reventlessdev/reventless-spec 3.0.0-alpha.123 → 3.0.0-alpha.125
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 +56 -0
- package/package.json +5 -3
- 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 +41 -3
- package/src/components/Aggregate.res +22 -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 +202 -422
- package/src/components/Plugin.res.mjs +65 -3
- 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/Currency.res +519 -502
- package/src/semantic/Currency.res.mjs +7 -655
- package/src/semantic/Messaging.res +127 -0
- package/src/semantic/Messaging.res.mjs +57 -0
- package/src/semantic/Money.res +123 -21
- package/src/semantic/Money.res.mjs +49 -2
- package/src/types/Trait.res +62 -0
- package/src/types/Trait.res.mjs +18 -0
- package/src/types/Transition.res +71 -0
- package/src/types/Transition.res.mjs +36 -0
- package/scripts/generate-currency.mjs +0 -215
- package/scripts/iso-4217-list-one.xml +0 -1956
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,62 @@
|
|
|
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.125 (2026-09-01)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **automation:** a mapping is handed the envelope's id ([9ee482c](https://github.com/ReventlessDev/reventless-core/commit/9ee482c5ad0aae09efd1259eeb7f39b781867b91))
|
|
11
|
+
* **capabilities:** a plugin's geocoding need was declared nowhere and failed silently ([67917dd](https://github.com/ReventlessDev/reventless-core/commit/67917dd504b43fa78b7c6a51644c9eae656b7f6b))
|
|
12
|
+
* feat(example)!: model product and category images as attachment sets ([6ae18d8](https://github.com/ReventlessDev/reventless-core/commit/6ae18d896215b448177ba0516e74bfda5f88d2db))
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
* **capabilities:** a plugin can send a message without naming a provider ([b001a1e](https://github.com/ReventlessDev/reventless-core/commit/b001a1e9361a0f4d0affe228cb4c08c38a5a995e))
|
|
16
|
+
* **plugin:** the admin lifecycle commands name their argument ([ea552ec](https://github.com/ReventlessDev/reventless-core/commit/ea552ec64e5db0b3dc465c0d9a80cac89627f727))
|
|
17
|
+
* **spec:** a command declares its lifecycle edge as a value ([40eee9f](https://github.com/ReventlessDev/reventless-core/commit/40eee9f7723dc05e418be680528f01967d074da4))
|
|
18
|
+
* **spec:** a graft leaves a trace the deployed plugin can read ([c08ff6c](https://github.com/ReventlessDev/reventless-core/commit/c08ff6c0f6177d58603e7ae1e5cec392d9bac16a))
|
|
19
|
+
* **spec:** graft-trait, a CLI that runs a trait's emitter ([1b87399](https://github.com/ReventlessDev/reventless-core/commit/1b87399987033765d6a1ee8ea22ab7f02056e9c0))
|
|
20
|
+
* **traits:** a conformance run leaves something a machine can read ([cd9cb81](https://github.com/ReventlessDev/reventless-core/commit/cd9cb81aa643f8e30ccf072df458fdc136897746))
|
|
21
|
+
* **traits:** a listing reads a trait instead of being told about it ([8a23219](https://github.com/ReventlessDev/reventless-core/commit/8a23219c5a69011ef9310ebf8bfcbf9315a577ba))
|
|
22
|
+
|
|
23
|
+
### BREAKING CHANGES
|
|
24
|
+
|
|
25
|
+
* ProductAdded/CategoryAdded lose their image field and
|
|
26
|
+
ChangeProductImage/ChangeCategoryImage are replaced; the alpha event log is
|
|
27
|
+
wiped on the next deploy.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 3.0.0-alpha.124 (2026-08-27)
|
|
32
|
+
|
|
33
|
+
* feat(spec)!: reflect the command direction across a port, both halves ([f2fe258](https://github.com/ReventlessDev/reventless-core/commit/f2fe258d195b74f4a61488edee305665341020ea))
|
|
34
|
+
* feat(spec)!: read the port's translation table off the mapping's arms ([956348a](https://github.com/ReventlessDev/reventless-core/commit/956348a9fcb256cfff9db51809bdc27d73360e6c))
|
|
35
|
+
* feat(spec)!: keep the amount in minor units, and add times and allocate ([6ff6308](https://github.com/ReventlessDev/reventless-core/commit/6ff630875f9b126b2f87b980a4dbb56582f8aebe))
|
|
36
|
+
* feat(spec)!: hold a money amount in the currency's own units ([94d3bba](https://github.com/ReventlessDev/reventless-core/commit/94d3bbaf0c47a639c20de6847c4615070ad51b38))
|
|
37
|
+
|
|
38
|
+
### BREAKING CHANGES
|
|
39
|
+
|
|
40
|
+
* `ExtensionPointMapping.Mapping` gains `acceptedCommands` and
|
|
41
|
+
`ExtensionMapping.Mapping` gains `issuedCommands`; both are derived, so a
|
|
42
|
+
mapping the ppx can read needs no source change, and one it cannot names itself
|
|
43
|
+
at compile time. Definitions persisted before the two new def fields must be
|
|
44
|
+
re-emitted before a consumer can read them as present — and until then they
|
|
45
|
+
decode as None, which means unknown, not "issues nothing".
|
|
46
|
+
* `ExtensionPointMapping.Mapping` requires `publishedEvents` and
|
|
47
|
+
`ExtensionMapping.Mapping` requires `handledEvents`; the ppx injects both for
|
|
48
|
+
mappings it can read, so app code must build against the matching ppx. Plugin
|
|
49
|
+
definitions persisted before the new fields must be re-emitted before a consumer
|
|
50
|
+
reads them as present.
|
|
51
|
+
* reverts the wire shape to whole minor units, so a log written
|
|
52
|
+
against the intervening commit reads a hundredfold low. Nothing released carried
|
|
53
|
+
the decimal form.
|
|
54
|
+
* stored amounts do not migrate. A log written before this holds
|
|
55
|
+
minor units and now decodes a hundredfold high, cleanly and without complaint —
|
|
56
|
+
a log that has to survive needs an upcaster written before this is deployed, and
|
|
57
|
+
a demo log needs discarding and reseeding. A stored currency outside the ten no
|
|
58
|
+
longer decodes; admit it in the generator or migrate the data.
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
6
62
|
# 3.0.0-alpha.123 (2026-08-23)
|
|
7
63
|
|
|
8
64
|
### Features
|
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.125",
|
|
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": [
|
|
@@ -42,7 +45,6 @@
|
|
|
42
45
|
"build": "rescript build",
|
|
43
46
|
"start": "rescript start",
|
|
44
47
|
"clean": "rescript clean",
|
|
45
|
-
"generate:currency": "node ./scripts/generate-currency.mjs",
|
|
46
48
|
"test": "NODE_OPTIONS='--experimental-vm-modules' jest",
|
|
47
49
|
"dev": "NODE_OPTIONS='--experimental-vm-modules' jest --watchAll"
|
|
48
50
|
}
|
|
@@ -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
|
|
@@ -37,6 +37,11 @@ type PageInfo {
|
|
|
37
37
|
startCursor: String
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
type Platform_AcceptedCommandDef {
|
|
41
|
+
name: String!
|
|
42
|
+
toCommandTypes: [String!]!
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
type Platform_AutomationSliceDef {
|
|
41
46
|
chapter: String
|
|
42
47
|
consumedEventTypes: [String!]!
|
|
@@ -88,13 +93,17 @@ type Platform_ExtensionDef {
|
|
|
88
93
|
commandTypes: [String!]!
|
|
89
94
|
delegateNames: [String!]!
|
|
90
95
|
eventTypes: [String!]!
|
|
96
|
+
handledEvents: [Platform_HandledEventDef!]
|
|
97
|
+
issuedCommands: [Platform_IssuedCommandDef!]
|
|
91
98
|
name: String!
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
type Platform_ExtensionPointDef {
|
|
102
|
+
acceptedCommands: [Platform_AcceptedCommandDef!]
|
|
95
103
|
commandTypes: [String!]
|
|
96
104
|
delegateNames: [String!]!
|
|
97
105
|
name: String!
|
|
106
|
+
publishedEvents: [Platform_PublishedEventDef!]
|
|
98
107
|
sourceEventTypes: [String!]!
|
|
99
108
|
}
|
|
100
109
|
|
|
@@ -104,6 +113,11 @@ type Platform_FieldReference {
|
|
|
104
113
|
plugin: String
|
|
105
114
|
}
|
|
106
115
|
|
|
116
|
+
type Platform_HandledEventDef {
|
|
117
|
+
name: String!
|
|
118
|
+
toCommandTypes: [String!]!
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
type Platform_InboundTranslationSliceDef {
|
|
108
122
|
chapter: String
|
|
109
123
|
commandTypes: [String!]!
|
|
@@ -112,6 +126,11 @@ type Platform_InboundTranslationSliceDef {
|
|
|
112
126
|
targetName: String
|
|
113
127
|
}
|
|
114
128
|
|
|
129
|
+
type Platform_IssuedCommandDef {
|
|
130
|
+
fromEventTypes: [String!]!
|
|
131
|
+
name: String!
|
|
132
|
+
}
|
|
133
|
+
|
|
115
134
|
type Platform_OutboundTranslationSliceDef {
|
|
116
135
|
chapter: String
|
|
117
136
|
consumedEventTypes: [String!]!
|
|
@@ -206,10 +225,17 @@ type Platform_PluginStructureEntry {
|
|
|
206
225
|
outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!
|
|
207
226
|
pluginId: String!
|
|
208
227
|
readModels: [Platform_ReadSideDef!]!
|
|
228
|
+
requiredCapabilities: [Platform_RequiredCapabilityDeclaration!]
|
|
209
229
|
requiredStoreDeclarations: [Platform_RequiredStoreDeclaration!]
|
|
210
230
|
requiredStores: [String!]
|
|
211
231
|
stateChangeSlices: [Platform_WriteSideDef!]!
|
|
212
232
|
stateViewSlices: [Platform_ReadSideDef!]!
|
|
233
|
+
traitDeclarations: [Platform_TraitDeclaration!]
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
type Platform_PublishedEventDef {
|
|
237
|
+
fromEventTypes: [String!]!
|
|
238
|
+
name: String!
|
|
213
239
|
}
|
|
214
240
|
|
|
215
241
|
type Platform_ReadSideDef {
|
|
@@ -234,6 +260,11 @@ type Platform_ReadSideDef {
|
|
|
234
260
|
visibility: String
|
|
235
261
|
}
|
|
236
262
|
|
|
263
|
+
type Platform_RequiredCapabilityDeclaration {
|
|
264
|
+
capability: String!
|
|
265
|
+
component: String!
|
|
266
|
+
}
|
|
267
|
+
|
|
237
268
|
type Platform_RequiredStoreDeclaration {
|
|
238
269
|
annotation: String
|
|
239
270
|
component: String!
|
|
@@ -241,6 +272,13 @@ type Platform_RequiredStoreDeclaration {
|
|
|
241
272
|
store: String!
|
|
242
273
|
}
|
|
243
274
|
|
|
275
|
+
type Platform_TraitDeclaration {
|
|
276
|
+
component: String!
|
|
277
|
+
posture: String!
|
|
278
|
+
trait: String!
|
|
279
|
+
version: String!
|
|
280
|
+
}
|
|
281
|
+
|
|
244
282
|
type Platform_UIFragmentEntry {
|
|
245
283
|
pages: [Platform_UIPage!]!
|
|
246
284
|
panels: [Platform_UIPanel!]!
|
|
@@ -61,4 +61,26 @@ 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`,
|
|
74
|
+
which leaves `@transition` in charge; a host that writes the switch by
|
|
75
|
+
hand takes charge instead, and gets an exhaustive one over typed states.
|
|
76
|
+
See `Transition`. */
|
|
77
|
+
let commandTransition: command => Transition.t<lifecycleState>
|
|
78
|
+
|
|
79
|
+
/** The domain traits grafted into this component, as values the trait packages
|
|
80
|
+
export — `[TraitAttachments.Attachments.declaration]`. Auto-injected as `[]`
|
|
81
|
+
by `@@reventless.spec`, so a component that is nobody's graft says so without
|
|
82
|
+
a line. A graft names its trait here and the structure records it, which is
|
|
83
|
+
the only way a deployed plugin can answer "where did this come from". See
|
|
84
|
+
`Trait`. */
|
|
85
|
+
let traits: array<Trait.t>
|
|
64
86
|
}
|
|
@@ -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
|
+
which leaves `@transition` in charge; a host that writes the switch by
|
|
76
|
+
hand takes charge instead, and 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
|
/**
|