@reventlessdev/reventless-core 3.0.0-alpha.234 → 3.0.0-alpha.235
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 +7 -0
- package/package.json +7 -7
- package/src/admin/Platform_BakedManifest.res +41 -2
- package/src/admin/Platform_BakedManifest.res.mjs +22 -1
- package/src/admin/Platform_ComponentDefinitionsApi.res +21 -3
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +6 -2
- package/tests/admin/Platform_BakedManifestTest.res +61 -16
- package/tests/admin/Platform_BakedManifestTest.res.mjs +60 -49
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +7 -7
- package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.235 (2026-08-15)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **core:** curate the pages a shell builds across a plugin's views ([589835a](https://github.com/ReventlessDev/reventless-core/commit/589835a1ad428c5e2dea8bf6e4c64e49d2f67e0d))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.234 (2026-08-14)
|
|
7
14
|
|
|
8
15
|
### Features
|
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.235",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
|
-
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.
|
|
32
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.7",
|
|
33
33
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
34
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.7",
|
|
34
35
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
35
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.6",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
37
|
-
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.
|
|
38
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.141",
|
|
39
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.7",
|
|
40
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
41
|
-
"@reventlessdev/reventless-
|
|
39
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
40
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.114",
|
|
41
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.142"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -20,6 +20,22 @@ type error =
|
|
|
20
20
|
| UnknownPlugin(string)
|
|
21
21
|
| UnknownView({plugin: string, view: string})
|
|
22
22
|
| UnknownCommand({plugin: string, command: string})
|
|
23
|
+
| UnknownDerived({plugin: string, kind: string})
|
|
24
|
+
|
|
25
|
+
/** The pages a shell builds across a plugin's views rather than from any one of
|
|
26
|
+
them, as the vocabulary a deployment curates them by.
|
|
27
|
+
|
|
28
|
+
A closed set, and deliberately kinds rather than page names. A dashboard and
|
|
29
|
+
a lifecycle page are named by the framework, but a canvas is named by the
|
|
30
|
+
views it draws — from that deployment's hints and the view modes its shell
|
|
31
|
+
registered, neither of which this side can see. Curating by kind is the
|
|
32
|
+
question this side CAN answer, and it is also the question an author is
|
|
33
|
+
asking: "does this audience get calendars", not "does it get the one called
|
|
34
|
+
Deliveries".
|
|
35
|
+
|
|
36
|
+
Ordered as they appear in a menu (the dashboard tops the group, canvases and
|
|
37
|
+
schedulers tail it), so the vocabulary reads as the page set it curates. */
|
|
38
|
+
let derivedKinds = ["dashboard", "lifecycles", "canvas", "scheduler"]
|
|
23
39
|
|
|
24
40
|
let describe = (e: error): string =>
|
|
25
41
|
switch e {
|
|
@@ -28,16 +44,26 @@ let describe = (e: error): string =>
|
|
|
28
44
|
`baked manifest: plugin "${plugin}" has no view named "${view}"`
|
|
29
45
|
| UnknownCommand({plugin, command}) =>
|
|
30
46
|
`baked manifest: plugin "${plugin}" has no command named "${command}"`
|
|
47
|
+
| UnknownDerived({plugin, kind}) =>
|
|
48
|
+
`baked manifest: plugin "${plugin}" names derived page kind "${kind}" — ` ++
|
|
49
|
+
`known kinds: ${derivedKinds->Array.join(", ")}`
|
|
31
50
|
}
|
|
32
51
|
|
|
33
52
|
// Views are named by component name — a view IS the surface. Commands are named
|
|
34
53
|
// by command name, not by the write-side component that carries them: the
|
|
35
54
|
// command is what a user invokes, and which slice or aggregate it lives on is an
|
|
36
55
|
// implementation detail the author should not have to know to curate a shop.
|
|
56
|
+
//
|
|
57
|
+
// `derived` is neither: it names page KINDS, for the reason `derivedKinds`
|
|
58
|
+
// gives. Unlike the other two, what it selects is not filtered here — there is
|
|
59
|
+
// nothing in a `pluginStructure` to filter, because the pages do not exist until
|
|
60
|
+
// a shell builds them. It travels to the shell instead, which is the only place
|
|
61
|
+
// that can honour it.
|
|
37
62
|
type selection = {
|
|
38
63
|
plugin: string,
|
|
39
64
|
views: option<array<string>>,
|
|
40
65
|
commands: option<array<string>>,
|
|
66
|
+
derived: option<array<string>>,
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
let publicViews = (def: pluginStructure): array<queryableDef> =>
|
|
@@ -65,7 +91,16 @@ let validate = (~def: pluginStructure, sel: selection): result<unit, error> => {
|
|
|
65
91
|
| None =>
|
|
66
92
|
switch sel.commands->known(def->commandNames) {
|
|
67
93
|
| Some(command) => Error(UnknownCommand({plugin: sel.plugin, command}))
|
|
68
|
-
| None =>
|
|
94
|
+
| None =>
|
|
95
|
+
// Against the vocabulary rather than against this plugin: a kind that
|
|
96
|
+
// generates no page here is a fact about the plugin's schemas, not a
|
|
97
|
+
// mistake — a shop naming `lifecycles` for a plugin whose views gain a
|
|
98
|
+
// status field next month is stating an intention, and failing it would
|
|
99
|
+
// punish the deployment for being early.
|
|
100
|
+
switch sel.derived->known(derivedKinds) {
|
|
101
|
+
| Some(kind) => Error(UnknownDerived({plugin: sel.plugin, kind}))
|
|
102
|
+
| None => Ok()
|
|
103
|
+
}
|
|
69
104
|
}
|
|
70
105
|
}
|
|
71
106
|
}
|
|
@@ -182,7 +217,11 @@ let curate = (
|
|
|
182
217
|
| Some((pluginId, def)) =>
|
|
183
218
|
curateStructure(~pluginId, ~def, sel)->Result.map(curated => {
|
|
184
219
|
entries->Array.push(
|
|
185
|
-
Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
|
|
220
|
+
Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
|
|
221
|
+
~pluginId,
|
|
222
|
+
~derived=?sel.derived,
|
|
223
|
+
curated,
|
|
224
|
+
),
|
|
186
225
|
)
|
|
187
226
|
entries
|
|
188
227
|
})
|
|
@@ -6,6 +6,13 @@ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
|
|
|
6
6
|
import * as Plugin$ReventlessCore from "../plugin/component/Plugin.res.mjs";
|
|
7
7
|
import * as Platform_ComponentDefinitionsApi$ReventlessCore from "./Platform_ComponentDefinitionsApi.res.mjs";
|
|
8
8
|
|
|
9
|
+
let derivedKinds = [
|
|
10
|
+
"dashboard",
|
|
11
|
+
"lifecycles",
|
|
12
|
+
"canvas",
|
|
13
|
+
"scheduler"
|
|
14
|
+
];
|
|
15
|
+
|
|
9
16
|
function describe(e) {
|
|
10
17
|
switch (e.TAG) {
|
|
11
18
|
case "UnknownPlugin" :
|
|
@@ -14,6 +21,8 @@ function describe(e) {
|
|
|
14
21
|
return `baked manifest: plugin "` + e.plugin + `" has no view named "` + e.view + `"`;
|
|
15
22
|
case "UnknownCommand" :
|
|
16
23
|
return `baked manifest: plugin "` + e.plugin + `" has no command named "` + e.command + `"`;
|
|
24
|
+
case "UnknownDerived" :
|
|
25
|
+
return `baked manifest: plugin "` + e.plugin + `" names derived page kind "` + e.kind + `" — known kinds: ` + derivedKinds.join(", ");
|
|
17
26
|
}
|
|
18
27
|
}
|
|
19
28
|
|
|
@@ -52,6 +61,17 @@ function validate(def, sel) {
|
|
|
52
61
|
command: command
|
|
53
62
|
}
|
|
54
63
|
};
|
|
64
|
+
}
|
|
65
|
+
let kind = known(sel.derived, derivedKinds);
|
|
66
|
+
if (kind !== undefined) {
|
|
67
|
+
return {
|
|
68
|
+
TAG: "Error",
|
|
69
|
+
_0: {
|
|
70
|
+
TAG: "UnknownDerived",
|
|
71
|
+
plugin: sel.plugin,
|
|
72
|
+
kind: kind
|
|
73
|
+
}
|
|
74
|
+
};
|
|
55
75
|
} else {
|
|
56
76
|
return {
|
|
57
77
|
TAG: "Ok",
|
|
@@ -189,13 +209,14 @@ function curate(structures, selections) {
|
|
|
189
209
|
}
|
|
190
210
|
let pluginId = match[0];
|
|
191
211
|
return Stdlib_Result.map(curateStructure(pluginId, match[1], sel), curated => {
|
|
192
|
-
entries.push(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(pluginId, curated));
|
|
212
|
+
entries.push(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(pluginId, sel.derived, curated));
|
|
193
213
|
return entries;
|
|
194
214
|
});
|
|
195
215
|
})), prim => prim);
|
|
196
216
|
}
|
|
197
217
|
|
|
198
218
|
export {
|
|
219
|
+
derivedKinds,
|
|
199
220
|
describe,
|
|
200
221
|
publicViews,
|
|
201
222
|
writeSides,
|
|
@@ -207,8 +207,21 @@ let encodeExtensionDef = (e: extensionDef): JSON.t =>
|
|
|
207
207
|
("commandTypes", encodeStrings(e.commandTypes)),
|
|
208
208
|
])->JSON.Encode.object
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// `derived` is curation, not structure: it says which of the pages a shell
|
|
211
|
+
// builds across this plugin's views that shell may build. It therefore arrives
|
|
212
|
+
// as an argument rather than off `def` — nothing in a compiled plugin decides
|
|
213
|
+
// it, and the admin path, which curates nothing, passes none.
|
|
214
|
+
//
|
|
215
|
+
// Absent key ⇒ every kind, which is what an uncurated entry means and what
|
|
216
|
+
// every manifest written before the field says. A curated entry that wants none
|
|
217
|
+
// says so with `[]`, and the two are only distinguishable because the key is
|
|
218
|
+
// omitted rather than nulled.
|
|
219
|
+
let encodePluginStructureEntry = (
|
|
220
|
+
~pluginId: string,
|
|
221
|
+
~derived: option<array<string>>=?,
|
|
222
|
+
def: pluginStructure,
|
|
223
|
+
): JSON.t => {
|
|
224
|
+
let entry = Dict.fromArray([
|
|
212
225
|
("pluginId", JSON.Encode.string(Plugin.name(pluginId))),
|
|
213
226
|
(
|
|
214
227
|
"readModels",
|
|
@@ -247,4 +260,9 @@ let encodePluginStructureEntry = (~pluginId: string, def: pluginStructure): JSON
|
|
|
247
260
|
->JSON.Encode.array,
|
|
248
261
|
),
|
|
249
262
|
("extensions", def.extensions->Array.map(encodeExtensionDef)->JSON.Encode.array),
|
|
250
|
-
])
|
|
263
|
+
])
|
|
264
|
+
derived->Option.forEach(kinds =>
|
|
265
|
+
entry->Dict.set("derivedPages", kinds->Array.map(JSON.Encode.string)->JSON.Encode.array)
|
|
266
|
+
)
|
|
267
|
+
entry->JSON.Encode.object
|
|
268
|
+
}
|
|
@@ -339,8 +339,8 @@ function encodeExtensionDef(e) {
|
|
|
339
339
|
]);
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
function encodePluginStructureEntry(pluginId, def) {
|
|
343
|
-
|
|
342
|
+
function encodePluginStructureEntry(pluginId, derived, def) {
|
|
343
|
+
let entry = Object.fromEntries([
|
|
344
344
|
[
|
|
345
345
|
"pluginId",
|
|
346
346
|
Plugin$ReventlessCore.name(pluginId)
|
|
@@ -382,6 +382,10 @@ function encodePluginStructureEntry(pluginId, def) {
|
|
|
382
382
|
def.extensions.map(encodeExtensionDef)
|
|
383
383
|
]
|
|
384
384
|
]);
|
|
385
|
+
Stdlib_Option.forEach(derived, kinds => {
|
|
386
|
+
entry["derivedPages"] = kinds.map(prim => prim);
|
|
387
|
+
});
|
|
388
|
+
return entry;
|
|
385
389
|
}
|
|
386
390
|
|
|
387
391
|
let sdlQueryField = ` Platform_ComponentDefinitions: [Platform_ComponentDefinitionEntry!]!`;
|
|
@@ -89,6 +89,15 @@ let structures = [("Ordering@1.0.0", structure)]
|
|
|
89
89
|
let bake = selections =>
|
|
90
90
|
Platform_BakedManifest.curate(~structures, ~selections)
|
|
91
91
|
|
|
92
|
+
// Every case below varies one include-list and means "everything" by the rest,
|
|
93
|
+
// which is exactly what an omitted labelled argument says.
|
|
94
|
+
let sel = (~plugin, ~views=?, ~commands=?, ~derived=?): Platform_BakedManifest.selection => {
|
|
95
|
+
plugin,
|
|
96
|
+
views,
|
|
97
|
+
commands,
|
|
98
|
+
derived,
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
let entries = (json: JSON.t) => json->JSON.Decode.array->Option.getOr([])
|
|
93
102
|
|
|
94
103
|
let names = (entry: JSON.t, field: string) =>
|
|
@@ -110,19 +119,17 @@ let firstEntry = (r: result<JSON.t, Platform_BakedManifest.error>) =>
|
|
|
110
119
|
describe("curate", () => {
|
|
111
120
|
testSync("selects exactly the named components", () => {
|
|
112
121
|
let entry =
|
|
113
|
-
bake([
|
|
114
|
-
->firstEntry
|
|
122
|
+
bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["PlaceOrder"])])->firstEntry
|
|
115
123
|
expect((entry->names("stateViewSlices"), entry->names("readModels")))->toEqual((["Orders"], []))
|
|
116
124
|
})
|
|
117
125
|
|
|
118
126
|
testSync("drops a write side whose commands were all excluded", () => {
|
|
119
|
-
let entry =
|
|
120
|
-
bake([{plugin: "Ordering", views: Some([]), commands: Some(["PlaceOrder"])}])->firstEntry
|
|
127
|
+
let entry = bake([sel(~plugin="Ordering", ~views=[], ~commands=["PlaceOrder"])])->firstEntry
|
|
121
128
|
expect(entry->names("stateChangeSlices"))->toEqual(["PlaceOrder"])
|
|
122
129
|
})
|
|
123
130
|
|
|
124
131
|
testSync("unset views and commands include every public component", () => {
|
|
125
|
-
let entry = bake([
|
|
132
|
+
let entry = bake([sel(~plugin="Ordering")])->firstEntry
|
|
126
133
|
expect((
|
|
127
134
|
entry->names("readModels"),
|
|
128
135
|
entry->names("stateViewSlices"),
|
|
@@ -134,34 +141,72 @@ describe("curate", () => {
|
|
|
134
141
|
// from, so an Internal view a kept command points at has to survive the bake.
|
|
135
142
|
testSync("carries an Internal view referenced by an included command", () => {
|
|
136
143
|
let entry =
|
|
137
|
-
bake([
|
|
144
|
+
bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["PlaceOrder"])])
|
|
138
145
|
->firstEntry
|
|
139
146
|
expect(entry->names("internalQueryables"))->toEqual(["AvailableProducts"])
|
|
140
147
|
})
|
|
141
148
|
|
|
142
149
|
testSync("omits an Internal view no included command references", () => {
|
|
143
150
|
let entry =
|
|
144
|
-
bake([
|
|
151
|
+
bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["ImportOrders"])])
|
|
145
152
|
->firstEntry
|
|
146
153
|
expect(entry->names("internalQueryables"))->toEqual([])
|
|
147
154
|
})
|
|
148
155
|
|
|
149
|
-
|
|
156
|
+
// The pages a shell builds across a plugin's views do not exist on this side,
|
|
157
|
+
// so what the bake can do about them is state which kinds an audience gets and
|
|
158
|
+
// carry that to the shell. Absent and empty therefore have to stay two
|
|
159
|
+
// different answers all the way out to the JSON, which is what these three
|
|
160
|
+
// cases pin: nothing said ⇒ no key ⇒ every kind, and `[]` ⇒ a key ⇒ none.
|
|
161
|
+
let derivedOf = (entry: JSON.t) =>
|
|
162
|
+
entry
|
|
163
|
+
->JSON.Decode.object
|
|
164
|
+
->Option.flatMap(d => d->Dict.get("derivedPages"))
|
|
165
|
+
->Option.map(v => v->JSON.Decode.array->Option.getOr([])->Array.filterMap(JSON.Decode.string))
|
|
166
|
+
|
|
167
|
+
testSync("unset derived says nothing, leaving every kind to the shell", () =>
|
|
168
|
+
expect(bake([sel(~plugin="Ordering")])->firstEntry->derivedOf)->toEqual(None)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
testSync("an empty derived list is carried, and is not the same as unset", () =>
|
|
172
|
+
expect(bake([sel(~plugin="Ordering", ~derived=[])])->firstEntry->derivedOf)->toEqual(Some([]))
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
testSync("carries exactly the derived kinds named", () =>
|
|
176
|
+
expect(
|
|
177
|
+
bake([sel(~plugin="Ordering", ~derived=["lifecycles", "canvas"])])->firstEntry->derivedOf,
|
|
178
|
+
)->toEqual(Some(["lifecycles", "canvas"]))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Against the vocabulary, never against this plugin: a kind that generates no
|
|
182
|
+
// page for these schemas is a fact about the plugin, not a typo, and a shop
|
|
183
|
+
// that names `lifecycles` before its views carry a status is early, not wrong.
|
|
184
|
+
testSync("fails naming a derived kind outside the vocabulary", () =>
|
|
185
|
+
expect(bake([sel(~plugin="Ordering", ~derived=["dashboards"])]))->toEqual(
|
|
186
|
+
Error(Platform_BakedManifest.UnknownDerived({plugin: "Ordering", kind: "dashboards"})),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
testSync("accepts a kind this plugin generates no page for", () =>
|
|
150
191
|
expect(
|
|
151
|
-
bake([
|
|
152
|
-
)->toEqual(
|
|
192
|
+
bake([sel(~plugin="Ordering", ~derived=["scheduler"])])->firstEntry->derivedOf,
|
|
193
|
+
)->toEqual(Some(["scheduler"]))
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
testSync("an Internal view is never selectable as a view", () =>
|
|
197
|
+
expect(bake([sel(~plugin="Ordering", ~views=["AvailableProducts"])]))->toEqual(
|
|
153
198
|
Error(Platform_BakedManifest.UnknownView({plugin: "Ordering", view: "AvailableProducts"})),
|
|
154
199
|
)
|
|
155
200
|
)
|
|
156
201
|
|
|
157
202
|
testSync("fails naming an unknown plugin", () =>
|
|
158
|
-
expect(bake([
|
|
203
|
+
expect(bake([sel(~plugin="Shipping")]))->toEqual(
|
|
159
204
|
Error(Platform_BakedManifest.UnknownPlugin("Shipping")),
|
|
160
205
|
)
|
|
161
206
|
)
|
|
162
207
|
|
|
163
208
|
testSync("fails naming an unknown command", () =>
|
|
164
|
-
expect(bake([
|
|
209
|
+
expect(bake([sel(~plugin="Ordering", ~commands=["CancelOrder"])]))->toEqual(
|
|
165
210
|
Error(Platform_BakedManifest.UnknownCommand({plugin: "Ordering", command: "CancelOrder"})),
|
|
166
211
|
)
|
|
167
212
|
)
|
|
@@ -169,7 +214,7 @@ describe("curate", () => {
|
|
|
169
214
|
// The anti-drift assertion: include everything and the baked entry IS the
|
|
170
215
|
// served entry. Any future divergence in the encoder shows up here first.
|
|
171
216
|
testSync("an all-inclusive bake is byte-identical to the served entry", () => {
|
|
172
|
-
let baked = bake([
|
|
217
|
+
let baked = bake([sel(~plugin="Ordering")])->firstEntry
|
|
173
218
|
let served = Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
|
|
174
219
|
~pluginId="Ordering@1.0.0",
|
|
175
220
|
structure,
|
|
@@ -182,8 +227,8 @@ describe("curate", () => {
|
|
|
182
227
|
Platform_BakedManifest.curate(
|
|
183
228
|
~structures=[("Ordering@1.0.0", structure), ("Catalog@1.0.0", structure)],
|
|
184
229
|
~selections=[
|
|
185
|
-
|
|
186
|
-
|
|
230
|
+
sel(~plugin="Catalog", ~views=[], ~commands=[]),
|
|
231
|
+
sel(~plugin="Ordering", ~views=[], ~commands=[]),
|
|
187
232
|
],
|
|
188
233
|
)
|
|
189
234
|
let ids =
|
|
@@ -208,7 +253,7 @@ describe("curate", () => {
|
|
|
208
253
|
// makes this hold — the assertion guards that property, not a second code path.
|
|
209
254
|
testSync("a curated entry carries the owner field the served entry carries", () => {
|
|
210
255
|
let entry =
|
|
211
|
-
bake([
|
|
256
|
+
bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["PlaceOrder"])])
|
|
212
257
|
->firstEntry
|
|
213
258
|
let ownerOf = (key, name) =>
|
|
214
259
|
entry
|
|
@@ -109,6 +109,15 @@ function bake(selections) {
|
|
|
109
109
|
return Platform_BakedManifest$ReventlessCore.curate(structures, selections);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function sel(plugin, views, commands, derived) {
|
|
113
|
+
return {
|
|
114
|
+
plugin: plugin,
|
|
115
|
+
views: views,
|
|
116
|
+
commands: commands,
|
|
117
|
+
derived: derived
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
function entries(json) {
|
|
113
122
|
return Stdlib_Option.getOr(Stdlib_JSON.Decode.array(json), []);
|
|
114
123
|
}
|
|
@@ -127,11 +136,7 @@ function firstEntry(r) {
|
|
|
127
136
|
|
|
128
137
|
globalThis.describe("curate", () => {
|
|
129
138
|
globalThis.test("selects exactly the named components", () => {
|
|
130
|
-
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [
|
|
131
|
-
plugin: "Ordering",
|
|
132
|
-
views: ["Orders"],
|
|
133
|
-
commands: ["PlaceOrder"]
|
|
134
|
-
}]));
|
|
139
|
+
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["PlaceOrder"], undefined)]));
|
|
135
140
|
globalThis.expect([
|
|
136
141
|
names(entry, "stateViewSlices"),
|
|
137
142
|
names(entry, "readModels")
|
|
@@ -141,18 +146,15 @@ globalThis.describe("curate", () => {
|
|
|
141
146
|
]);
|
|
142
147
|
});
|
|
143
148
|
globalThis.test("drops a write side whose commands were all excluded", () => {
|
|
144
|
-
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [
|
|
145
|
-
plugin: "Ordering",
|
|
146
|
-
views: [],
|
|
147
|
-
commands: ["PlaceOrder"]
|
|
148
|
-
}]));
|
|
149
|
+
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", [], ["PlaceOrder"], undefined)]));
|
|
149
150
|
globalThis.expect(names(entry, "stateChangeSlices")).toEqual(["PlaceOrder"]);
|
|
150
151
|
});
|
|
151
152
|
globalThis.test("unset views and commands include every public component", () => {
|
|
152
153
|
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
153
154
|
plugin: "Ordering",
|
|
154
155
|
views: undefined,
|
|
155
|
-
commands: undefined
|
|
156
|
+
commands: undefined,
|
|
157
|
+
derived: undefined
|
|
156
158
|
}]));
|
|
157
159
|
globalThis.expect([
|
|
158
160
|
names(entry, "readModels"),
|
|
@@ -168,27 +170,49 @@ globalThis.describe("curate", () => {
|
|
|
168
170
|
]);
|
|
169
171
|
});
|
|
170
172
|
globalThis.test("carries an Internal view referenced by an included command", () => {
|
|
171
|
-
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [
|
|
172
|
-
plugin: "Ordering",
|
|
173
|
-
views: ["Orders"],
|
|
174
|
-
commands: ["PlaceOrder"]
|
|
175
|
-
}]));
|
|
173
|
+
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["PlaceOrder"], undefined)]));
|
|
176
174
|
globalThis.expect(names(entry, "internalQueryables")).toEqual(["AvailableProducts"]);
|
|
177
175
|
});
|
|
178
176
|
globalThis.test("omits an Internal view no included command references", () => {
|
|
179
|
-
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [
|
|
180
|
-
plugin: "Ordering",
|
|
181
|
-
views: ["Orders"],
|
|
182
|
-
commands: ["ImportOrders"]
|
|
183
|
-
}]));
|
|
177
|
+
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["ImportOrders"], undefined)]));
|
|
184
178
|
globalThis.expect(names(entry, "internalQueryables")).toEqual([]);
|
|
185
179
|
});
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
let derivedOf = entry => Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), d => d["derivedPages"]), v => Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(v), []), Stdlib_JSON.Decode.string));
|
|
181
|
+
globalThis.test("unset derived says nothing, leaving every kind to the shell", () => {
|
|
182
|
+
globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
188
183
|
plugin: "Ordering",
|
|
189
|
-
views:
|
|
190
|
-
commands: undefined
|
|
191
|
-
|
|
184
|
+
views: undefined,
|
|
185
|
+
commands: undefined,
|
|
186
|
+
derived: undefined
|
|
187
|
+
}])))).toEqual(undefined);
|
|
188
|
+
});
|
|
189
|
+
globalThis.test("an empty derived list is carried, and is not the same as unset", () => {
|
|
190
|
+
globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, [])])))).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
globalThis.test("carries exactly the derived kinds named", () => {
|
|
193
|
+
globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, [
|
|
194
|
+
"lifecycles",
|
|
195
|
+
"canvas"
|
|
196
|
+
])])))).toEqual([
|
|
197
|
+
"lifecycles",
|
|
198
|
+
"canvas"
|
|
199
|
+
]);
|
|
200
|
+
});
|
|
201
|
+
globalThis.test("fails naming a derived kind outside the vocabulary", () => {
|
|
202
|
+
globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, ["dashboards"])])).toEqual({
|
|
203
|
+
TAG: "Error",
|
|
204
|
+
_0: {
|
|
205
|
+
TAG: "UnknownDerived",
|
|
206
|
+
plugin: "Ordering",
|
|
207
|
+
kind: "dashboards"
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
globalThis.test("accepts a kind this plugin generates no page for", () => {
|
|
212
|
+
globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, ["scheduler"])])))).toEqual(["scheduler"]);
|
|
213
|
+
});
|
|
214
|
+
globalThis.test("an Internal view is never selectable as a view", () => {
|
|
215
|
+
globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["AvailableProducts"], undefined, undefined)])).toEqual({
|
|
192
216
|
TAG: "Error",
|
|
193
217
|
_0: {
|
|
194
218
|
TAG: "UnknownView",
|
|
@@ -201,7 +225,8 @@ globalThis.describe("curate", () => {
|
|
|
201
225
|
globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
202
226
|
plugin: "Shipping",
|
|
203
227
|
views: undefined,
|
|
204
|
-
commands: undefined
|
|
228
|
+
commands: undefined,
|
|
229
|
+
derived: undefined
|
|
205
230
|
}])).toEqual({
|
|
206
231
|
TAG: "Error",
|
|
207
232
|
_0: {
|
|
@@ -211,11 +236,7 @@ globalThis.describe("curate", () => {
|
|
|
211
236
|
});
|
|
212
237
|
});
|
|
213
238
|
globalThis.test("fails naming an unknown command", () => {
|
|
214
|
-
globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
215
|
-
plugin: "Ordering",
|
|
216
|
-
views: undefined,
|
|
217
|
-
commands: ["CancelOrder"]
|
|
218
|
-
}])).toEqual({
|
|
239
|
+
globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, ["CancelOrder"], undefined)])).toEqual({
|
|
219
240
|
TAG: "Error",
|
|
220
241
|
_0: {
|
|
221
242
|
TAG: "UnknownCommand",
|
|
@@ -228,9 +249,10 @@ globalThis.describe("curate", () => {
|
|
|
228
249
|
let baked = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
229
250
|
plugin: "Ordering",
|
|
230
251
|
views: undefined,
|
|
231
|
-
commands: undefined
|
|
252
|
+
commands: undefined,
|
|
253
|
+
derived: undefined
|
|
232
254
|
}]));
|
|
233
|
-
let served = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering@1.0.0", structure);
|
|
255
|
+
let served = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering@1.0.0", undefined, structure);
|
|
234
256
|
globalThis.expect(JSON.stringify(baked)).toEqual(JSON.stringify(served));
|
|
235
257
|
});
|
|
236
258
|
globalThis.test("one entry per selection, in declaration order", () => {
|
|
@@ -244,16 +266,8 @@ globalThis.describe("curate", () => {
|
|
|
244
266
|
structure
|
|
245
267
|
]
|
|
246
268
|
], [
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
views: [],
|
|
250
|
-
commands: []
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
plugin: "Ordering",
|
|
254
|
-
views: [],
|
|
255
|
-
commands: []
|
|
256
|
-
}
|
|
269
|
+
sel("Catalog", [], [], undefined),
|
|
270
|
+
sel("Ordering", [], [], undefined)
|
|
257
271
|
]);
|
|
258
272
|
let ids;
|
|
259
273
|
ids = json.TAG === "Ok" ? Stdlib_Array.filterMap(entries(json._0), e => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(e), d => d["pluginId"]), Stdlib_JSON.Decode.string)) : [];
|
|
@@ -263,11 +277,7 @@ globalThis.describe("curate", () => {
|
|
|
263
277
|
]);
|
|
264
278
|
});
|
|
265
279
|
globalThis.test("a curated entry carries the owner field the served entry carries", () => {
|
|
266
|
-
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [
|
|
267
|
-
plugin: "Ordering",
|
|
268
|
-
views: ["Orders"],
|
|
269
|
-
commands: ["PlaceOrder"]
|
|
270
|
-
}]));
|
|
280
|
+
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["PlaceOrder"], undefined)]));
|
|
271
281
|
let ownerOf = (key, name) => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), d => d[key]), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.object).find(d => Primitive_object.equal(Stdlib_Option.flatMap(d["name"], Stdlib_JSON.Decode.string), name)), d => d["ownerField"]), Stdlib_JSON.Decode.string);
|
|
272
282
|
let commandOwner = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Array.filterMap(Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), d => d["stateChangeSlices"]), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.object).flatMap(w => Stdlib_Option.getOr(Stdlib_Option.flatMap(w["commands"], Stdlib_JSON.Decode.array), [])), Stdlib_JSON.Decode.object)[0], d => d["ownerField"]), Stdlib_JSON.Decode.string);
|
|
273
283
|
globalThis.expect([
|
|
@@ -287,6 +297,7 @@ export {
|
|
|
287
297
|
structure,
|
|
288
298
|
structures,
|
|
289
299
|
bake,
|
|
300
|
+
sel,
|
|
290
301
|
entries,
|
|
291
302
|
names,
|
|
292
303
|
firstEntry,
|
|
@@ -147,7 +147,7 @@ let structure = {
|
|
|
147
147
|
requiredStoreDeclarations: undefined
|
|
148
148
|
};
|
|
149
149
|
|
|
150
|
-
let encoded = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", structure);
|
|
150
|
+
let encoded = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, structure);
|
|
151
151
|
|
|
152
152
|
let json = JSON.stringify(encoded);
|
|
153
153
|
|
|
@@ -255,7 +255,7 @@ globalThis.describe("translation-slice externalSystem round-trip", () => {
|
|
|
255
255
|
requiredStores: undefined,
|
|
256
256
|
requiredStoreDeclarations: undefined
|
|
257
257
|
};
|
|
258
|
-
let externalJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", externalStructure));
|
|
258
|
+
let externalJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, externalStructure));
|
|
259
259
|
globalThis.test("surfaces Some outbound externalSystem as its inner string", () => {
|
|
260
260
|
globalThis.expect(externalJson.includes("\"externalSystem\":\"ShipperGateway\"")).toEqual(true);
|
|
261
261
|
});
|
|
@@ -341,7 +341,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
341
341
|
requiredStores: undefined,
|
|
342
342
|
requiredStoreDeclarations: undefined
|
|
343
343
|
};
|
|
344
|
-
let mixedEntry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", mixed);
|
|
344
|
+
let mixedEntry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", undefined, mixed);
|
|
345
345
|
let namesAt = (entry, key) => Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), o => o[key]), Stdlib_JSON.Decode.array), []), c => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(c), o => o["name"]), Stdlib_JSON.Decode.string));
|
|
346
346
|
globalThis.test("excludes an Internal queryableDef from the encoded read-side", () => {
|
|
347
347
|
globalThis.expect(namesAt(mixedEntry, "readModels")).toEqual(["Products"]);
|
|
@@ -357,7 +357,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
357
357
|
]);
|
|
358
358
|
});
|
|
359
359
|
globalThis.test("encodes internalQueryables as [] for an all-Public entry, not a missing key", () => {
|
|
360
|
-
let entry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", {
|
|
360
|
+
let entry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", undefined, {
|
|
361
361
|
readModels: [qbl],
|
|
362
362
|
stateViewSlices: [],
|
|
363
363
|
stateChangeSlices: structure_stateChangeSlices,
|
|
@@ -459,7 +459,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
459
459
|
requiredStores: undefined,
|
|
460
460
|
requiredStoreDeclarations: undefined
|
|
461
461
|
};
|
|
462
|
-
let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Platform", structureWithStates));
|
|
462
|
+
let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Platform", undefined, structureWithStates));
|
|
463
463
|
globalThis.test("encodes populated allowedStates as a JSON array", () => {
|
|
464
464
|
globalThis.expect(json.includes("\"allowedStates\":[\"Inactive\"]")).toEqual(true);
|
|
465
465
|
});
|
|
@@ -481,7 +481,7 @@ globalThis.describe("singleQueryField reaches the caller", () => {
|
|
|
481
481
|
globalThis.expect(json.includes("\"singleQueryField\":\"Catalog_Product\"")).toEqual(true);
|
|
482
482
|
});
|
|
483
483
|
globalThis.test("encodes an unstated singular as null", () => {
|
|
484
|
-
let unstated = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
|
|
484
|
+
let unstated = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, {
|
|
485
485
|
readModels: [{
|
|
486
486
|
name: "Products",
|
|
487
487
|
queryField: "Catalog_Products",
|
|
@@ -526,7 +526,7 @@ globalThis.describe("idField / idFieldSource reach the caller", () => {
|
|
|
526
526
|
globalThis.expect(json.includes("\"idFieldSource\":\"convention\"")).toEqual(true);
|
|
527
527
|
});
|
|
528
528
|
globalThis.test("encodes an unresolved key as null on both fields", () => {
|
|
529
|
-
let unresolved = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
|
|
529
|
+
let unresolved = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, {
|
|
530
530
|
readModels: [{
|
|
531
531
|
name: "Products",
|
|
532
532
|
queryField: "Catalog_Products",
|
|
@@ -120,7 +120,7 @@ globalThis.describe("requiredAccess derived from the authorization rule", () =>
|
|
|
120
120
|
globalThis.expect(viewNamed("Orders")).toEqual(undefined);
|
|
121
121
|
});
|
|
122
122
|
globalThis.test("nothing to require encodes as null, not an empty list", () => {
|
|
123
|
-
let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("AccessPlugin", structure));
|
|
123
|
+
let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("AccessPlugin", undefined, structure));
|
|
124
124
|
globalThis.expect([
|
|
125
125
|
json.includes(`"requiredAccess":null`),
|
|
126
126
|
json.includes(`"requiredAccess":["Admin","Ops"]`),
|