@reventlessdev/reventless-core 3.0.0-alpha.224 → 3.0.0-alpha.225
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 +14 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +6 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +9 -3
- package/src/admin/Platform_BakedManifest.res +154 -0
- package/src/admin/Platform_BakedManifest.res.mjs +156 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +9 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +18 -2
- package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
- package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
- package/src/components/Api/QueryDbListQuery.res +25 -1
- package/src/components/Api/QueryDbListQuery.res.mjs +10 -3
- package/src/components/Api/SuryToJsonSchema.res +19 -0
- package/src/components/Api/SuryToJsonSchema.res.mjs +18 -4
- package/src/components/CommandGenerator/CommandGenerator_Callback.res +48 -0
- package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +27 -1
- package/src/plugin/component/Plugin_Structure.res +59 -3
- package/src/plugin/component/Plugin_Structure.res.mjs +29 -9
- package/tests/admin/Platform_BakedManifestTest.res +244 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +10 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +24 -8
- package/tests/admin/Platform_PluginStructuresApiTest.res +2 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +6 -2
- package/tests/api/Api_IdsTest.res.mjs +1 -1
- package/tests/api/OwnerScopeDiagnosticsTest.res +109 -0
- package/tests/api/OwnerScopeDiagnosticsTest.res.mjs +91 -0
- package/tests/api/SuryToJsonSchemaTest.res +45 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +23 -0
- package/tests/commandgenerator/OwnerStampingTest.res +174 -0
- package/tests/commandgenerator/OwnerStampingTest.res.mjs +195 -0
- package/tests/plugin/PluginStructureAccessTest.res +110 -0
- package/tests/plugin/PluginStructureAccessTest.res.mjs +144 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res +15 -0
- package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +60 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +4 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// The baked manifest is curation, so what matters is exactly which components
|
|
2
|
+
// survive an include-list — and, decisively, that an entry which includes
|
|
3
|
+
// everything is byte-identical to what the served query returns. Anything less
|
|
4
|
+
// and a shell reading the file is reading a second, quietly different contract.
|
|
5
|
+
|
|
6
|
+
open JestGlobals
|
|
7
|
+
open Reventless.Plugin
|
|
8
|
+
|
|
9
|
+
let queryable = (~name, ~visibility=?, ~ownerField=?, ()): queryableDef => {
|
|
10
|
+
name,
|
|
11
|
+
queryField: `Ordering_${name}`,
|
|
12
|
+
schema: "{}",
|
|
13
|
+
consumedEventTypes: [],
|
|
14
|
+
linkedWriteSide: [],
|
|
15
|
+
labelField: "displayName",
|
|
16
|
+
searchableFields: ["displayName"],
|
|
17
|
+
labelFieldSource: None,
|
|
18
|
+
statusField: None,
|
|
19
|
+
visibility,
|
|
20
|
+
chapter: None,
|
|
21
|
+
singleQueryField: Some(`Ordering_${name}Single`),
|
|
22
|
+
idField: Some("id"),
|
|
23
|
+
idFieldSource: Some("convention"),
|
|
24
|
+
requiredAccess: None,
|
|
25
|
+
ownerField,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let command = (~name, ~references=[], ~ownerField=?, ()): commandDef => {
|
|
29
|
+
name,
|
|
30
|
+
schema: "{}",
|
|
31
|
+
level: Collection,
|
|
32
|
+
aggregateIdField: None,
|
|
33
|
+
mutationField: name->String.toLowerCase,
|
|
34
|
+
references,
|
|
35
|
+
allowedStates: None,
|
|
36
|
+
targetState: None,
|
|
37
|
+
apiExposed: Some(true),
|
|
38
|
+
requiredAccess: None,
|
|
39
|
+
ownerField,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let writable = (~name, ~commands): writableDef => {
|
|
43
|
+
name,
|
|
44
|
+
commands,
|
|
45
|
+
linkedViews: [],
|
|
46
|
+
consistencyRead: None,
|
|
47
|
+
producedEventTypes: [],
|
|
48
|
+
consumedEventTypes: [],
|
|
49
|
+
events: [],
|
|
50
|
+
errors: [],
|
|
51
|
+
chapter: None,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Mirrors the hybrid example's shape: a public order list, an Internal
|
|
55
|
+
// denormalised product mirror a command @refs, one end-user command and one
|
|
56
|
+
// operator command.
|
|
57
|
+
let structure: pluginStructure = {
|
|
58
|
+
readModels: [queryable(~name="Customers", ())],
|
|
59
|
+
stateViewSlices: [
|
|
60
|
+
queryable(~name="Orders", ~ownerField="customerId", ()),
|
|
61
|
+
queryable(~name="AvailableProducts", ~visibility="Internal", ()),
|
|
62
|
+
],
|
|
63
|
+
stateChangeSlices: [
|
|
64
|
+
writable(
|
|
65
|
+
~name="PlaceOrder",
|
|
66
|
+
~commands=[
|
|
67
|
+
command(
|
|
68
|
+
~name="PlaceOrder",
|
|
69
|
+
~references=[{fieldName: "productIds", entity: "AvailableProducts", plugin: None}],
|
|
70
|
+
~ownerField="customerId",
|
|
71
|
+
(),
|
|
72
|
+
),
|
|
73
|
+
],
|
|
74
|
+
),
|
|
75
|
+
writable(~name="ImportOrders", ~commands=[command(~name="ImportOrders", ())]),
|
|
76
|
+
],
|
|
77
|
+
aggregates: [],
|
|
78
|
+
automationSlices: [],
|
|
79
|
+
outboundTranslationSlices: [],
|
|
80
|
+
inboundTranslationSlices: [],
|
|
81
|
+
extensions: [],
|
|
82
|
+
extensionPoints: None,
|
|
83
|
+
requiredStores: None,
|
|
84
|
+
requiredStoreDeclarations: None,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let structures = [("Ordering@1.0.0", structure)]
|
|
88
|
+
|
|
89
|
+
let bake = selections =>
|
|
90
|
+
Platform_BakedManifest.curate(~structures, ~selections)
|
|
91
|
+
|
|
92
|
+
let entries = (json: JSON.t) => json->JSON.Decode.array->Option.getOr([])
|
|
93
|
+
|
|
94
|
+
let names = (entry: JSON.t, field: string) =>
|
|
95
|
+
entry
|
|
96
|
+
->JSON.Decode.object
|
|
97
|
+
->Option.flatMap(d => d->Dict.get(field))
|
|
98
|
+
->Option.flatMap(JSON.Decode.array)
|
|
99
|
+
->Option.getOr([])
|
|
100
|
+
->Array.filterMap(v =>
|
|
101
|
+
v->JSON.Decode.object->Option.flatMap(d => d->Dict.get("name"))->Option.flatMap(JSON.Decode.string)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
let firstEntry = (r: result<JSON.t, Platform_BakedManifest.error>) =>
|
|
105
|
+
switch r {
|
|
106
|
+
| Ok(json) => json->entries->Array.getUnsafe(0)
|
|
107
|
+
| Error(e) => JsError.throwWithMessage(Platform_BakedManifest.describe(e))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe("curate", () => {
|
|
111
|
+
testSync("selects exactly the named components", () => {
|
|
112
|
+
let entry =
|
|
113
|
+
bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["PlaceOrder"])}])
|
|
114
|
+
->firstEntry
|
|
115
|
+
expect((entry->names("stateViewSlices"), entry->names("readModels")))->toEqual((["Orders"], []))
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
testSync("drops a write side whose commands were all excluded", () => {
|
|
119
|
+
let entry =
|
|
120
|
+
bake([{plugin: "Ordering", views: Some([]), commands: Some(["PlaceOrder"])}])->firstEntry
|
|
121
|
+
expect(entry->names("stateChangeSlices"))->toEqual(["PlaceOrder"])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
testSync("unset views and commands include every public component", () => {
|
|
125
|
+
let entry = bake([{plugin: "Ordering", views: None, commands: None}])->firstEntry
|
|
126
|
+
expect((
|
|
127
|
+
entry->names("readModels"),
|
|
128
|
+
entry->names("stateViewSlices"),
|
|
129
|
+
entry->names("stateChangeSlices"),
|
|
130
|
+
))->toEqual((["Customers"], ["Orders"], ["PlaceOrder", "ImportOrders"]))
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// A shell reading a baked file has no admin API to recover a reference target
|
|
134
|
+
// from, so an Internal view a kept command points at has to survive the bake.
|
|
135
|
+
testSync("carries an Internal view referenced by an included command", () => {
|
|
136
|
+
let entry =
|
|
137
|
+
bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["PlaceOrder"])}])
|
|
138
|
+
->firstEntry
|
|
139
|
+
expect(entry->names("internalQueryables"))->toEqual(["AvailableProducts"])
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
testSync("omits an Internal view no included command references", () => {
|
|
143
|
+
let entry =
|
|
144
|
+
bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["ImportOrders"])}])
|
|
145
|
+
->firstEntry
|
|
146
|
+
expect(entry->names("internalQueryables"))->toEqual([])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
testSync("an Internal view is never selectable as a view", () =>
|
|
150
|
+
expect(
|
|
151
|
+
bake([{plugin: "Ordering", views: Some(["AvailableProducts"]), commands: None}]),
|
|
152
|
+
)->toEqual(
|
|
153
|
+
Error(Platform_BakedManifest.UnknownView({plugin: "Ordering", view: "AvailableProducts"})),
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
testSync("fails naming an unknown plugin", () =>
|
|
158
|
+
expect(bake([{plugin: "Shipping", views: None, commands: None}]))->toEqual(
|
|
159
|
+
Error(Platform_BakedManifest.UnknownPlugin("Shipping")),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
testSync("fails naming an unknown command", () =>
|
|
164
|
+
expect(bake([{plugin: "Ordering", views: None, commands: Some(["CancelOrder"])}]))->toEqual(
|
|
165
|
+
Error(Platform_BakedManifest.UnknownCommand({plugin: "Ordering", command: "CancelOrder"})),
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// The anti-drift assertion: include everything and the baked entry IS the
|
|
170
|
+
// served entry. Any future divergence in the encoder shows up here first.
|
|
171
|
+
testSync("an all-inclusive bake is byte-identical to the served entry", () => {
|
|
172
|
+
let baked = bake([{plugin: "Ordering", views: None, commands: None}])->firstEntry
|
|
173
|
+
let served = Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
|
|
174
|
+
~pluginId="Ordering@1.0.0",
|
|
175
|
+
structure,
|
|
176
|
+
)
|
|
177
|
+
expect(baked->JSON.stringify)->toEqual(served->JSON.stringify)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
testSync("one entry per selection, in declaration order", () => {
|
|
181
|
+
let json =
|
|
182
|
+
Platform_BakedManifest.curate(
|
|
183
|
+
~structures=[("Ordering@1.0.0", structure), ("Catalog@1.0.0", structure)],
|
|
184
|
+
~selections=[
|
|
185
|
+
{plugin: "Catalog", views: Some([]), commands: Some([])},
|
|
186
|
+
{plugin: "Ordering", views: Some([]), commands: Some([])},
|
|
187
|
+
],
|
|
188
|
+
)
|
|
189
|
+
let ids =
|
|
190
|
+
switch json {
|
|
191
|
+
| Ok(j) =>
|
|
192
|
+
j
|
|
193
|
+
->entries
|
|
194
|
+
->Array.filterMap(e =>
|
|
195
|
+
e
|
|
196
|
+
->JSON.Decode.object
|
|
197
|
+
->Option.flatMap(d => d->Dict.get("pluginId"))
|
|
198
|
+
->Option.flatMap(JSON.Decode.string)
|
|
199
|
+
)
|
|
200
|
+
| Error(_) => []
|
|
201
|
+
}
|
|
202
|
+
expect(ids)->toEqual(["Catalog", "Ordering"])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
// A shell reading a baked manifest has no admin API to ask instead, so a field
|
|
206
|
+
// the curation step forgot to copy is a field that view can never learn about.
|
|
207
|
+
// `curate` reuses the served encoder rather than writing its own, which is what
|
|
208
|
+
// makes this hold — the assertion guards that property, not a second code path.
|
|
209
|
+
testSync("a curated entry carries the owner field the served entry carries", () => {
|
|
210
|
+
let entry =
|
|
211
|
+
bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["PlaceOrder"])}])
|
|
212
|
+
->firstEntry
|
|
213
|
+
let ownerOf = (key, name) =>
|
|
214
|
+
entry
|
|
215
|
+
->JSON.Decode.object
|
|
216
|
+
->Option.flatMap(d => d->Dict.get(key))
|
|
217
|
+
->Option.flatMap(JSON.Decode.array)
|
|
218
|
+
->Option.getOr([])
|
|
219
|
+
->Array.filterMap(JSON.Decode.object)
|
|
220
|
+
->Array.find(d =>
|
|
221
|
+
d->Dict.get("name")->Option.flatMap(JSON.Decode.string) == Some(name)
|
|
222
|
+
)
|
|
223
|
+
->Option.flatMap(d => d->Dict.get("ownerField"))
|
|
224
|
+
->Option.flatMap(JSON.Decode.string)
|
|
225
|
+
let commandOwner =
|
|
226
|
+
entry
|
|
227
|
+
->JSON.Decode.object
|
|
228
|
+
->Option.flatMap(d => d->Dict.get("stateChangeSlices"))
|
|
229
|
+
->Option.flatMap(JSON.Decode.array)
|
|
230
|
+
->Option.getOr([])
|
|
231
|
+
->Array.filterMap(JSON.Decode.object)
|
|
232
|
+
->Array.flatMap(w =>
|
|
233
|
+
w->Dict.get("commands")->Option.flatMap(JSON.Decode.array)->Option.getOr([])
|
|
234
|
+
)
|
|
235
|
+
->Array.filterMap(JSON.Decode.object)
|
|
236
|
+
->Array.get(0)
|
|
237
|
+
->Option.flatMap(d => d->Dict.get("ownerField"))
|
|
238
|
+
->Option.flatMap(JSON.Decode.string)
|
|
239
|
+
expect((ownerOf("stateViewSlices", "Orders"), commandOwner))->toEqual((
|
|
240
|
+
Some("customerId"),
|
|
241
|
+
Some("customerId"),
|
|
242
|
+
))
|
|
243
|
+
})
|
|
244
|
+
})
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
7
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
|
+
import * as Platform_BakedManifest$ReventlessCore from "../../src/admin/Platform_BakedManifest.res.mjs";
|
|
9
|
+
import * as Platform_ComponentDefinitionsApi$ReventlessCore from "../../src/admin/Platform_ComponentDefinitionsApi.res.mjs";
|
|
10
|
+
|
|
11
|
+
function queryable(name, visibility, ownerField, param) {
|
|
12
|
+
return {
|
|
13
|
+
name: name,
|
|
14
|
+
queryField: `Ordering_` + name,
|
|
15
|
+
schema: "{}",
|
|
16
|
+
consumedEventTypes: [],
|
|
17
|
+
linkedWriteSide: [],
|
|
18
|
+
labelField: "displayName",
|
|
19
|
+
searchableFields: ["displayName"],
|
|
20
|
+
labelFieldSource: undefined,
|
|
21
|
+
statusField: undefined,
|
|
22
|
+
ownerField: ownerField,
|
|
23
|
+
visibility: visibility,
|
|
24
|
+
chapter: undefined,
|
|
25
|
+
singleQueryField: `Ordering_` + name + `Single`,
|
|
26
|
+
idField: "id",
|
|
27
|
+
idFieldSource: "convention",
|
|
28
|
+
requiredAccess: undefined
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function command(name, referencesOpt, ownerField, param) {
|
|
33
|
+
let references = referencesOpt !== undefined ? referencesOpt : [];
|
|
34
|
+
return {
|
|
35
|
+
name: name,
|
|
36
|
+
schema: "{}",
|
|
37
|
+
level: "Collection",
|
|
38
|
+
aggregateIdField: undefined,
|
|
39
|
+
mutationField: name.toLowerCase(),
|
|
40
|
+
references: references,
|
|
41
|
+
allowedStates: undefined,
|
|
42
|
+
targetState: undefined,
|
|
43
|
+
apiExposed: true,
|
|
44
|
+
requiredAccess: undefined,
|
|
45
|
+
ownerField: ownerField
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writable(name, commands) {
|
|
50
|
+
return {
|
|
51
|
+
name: name,
|
|
52
|
+
commands: commands,
|
|
53
|
+
producedEventTypes: [],
|
|
54
|
+
consumedEventTypes: [],
|
|
55
|
+
linkedViews: [],
|
|
56
|
+
consistencyRead: undefined,
|
|
57
|
+
events: [],
|
|
58
|
+
errors: [],
|
|
59
|
+
chapter: undefined
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let structure_readModels = [queryable("Customers", undefined, undefined, undefined)];
|
|
64
|
+
|
|
65
|
+
let structure_stateViewSlices = [
|
|
66
|
+
queryable("Orders", undefined, "customerId", undefined),
|
|
67
|
+
queryable("AvailableProducts", "Internal", undefined, undefined)
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
let structure_stateChangeSlices = [
|
|
71
|
+
writable("PlaceOrder", [command("PlaceOrder", [{
|
|
72
|
+
fieldName: "productIds",
|
|
73
|
+
entity: "AvailableProducts",
|
|
74
|
+
plugin: undefined
|
|
75
|
+
}], "customerId", undefined)]),
|
|
76
|
+
writable("ImportOrders", [command("ImportOrders", undefined, undefined, undefined)])
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
let structure_aggregates = [];
|
|
80
|
+
|
|
81
|
+
let structure_automationSlices = [];
|
|
82
|
+
|
|
83
|
+
let structure_outboundTranslationSlices = [];
|
|
84
|
+
|
|
85
|
+
let structure_inboundTranslationSlices = [];
|
|
86
|
+
|
|
87
|
+
let structure_extensions = [];
|
|
88
|
+
|
|
89
|
+
let structure = {
|
|
90
|
+
readModels: structure_readModels,
|
|
91
|
+
stateViewSlices: structure_stateViewSlices,
|
|
92
|
+
stateChangeSlices: structure_stateChangeSlices,
|
|
93
|
+
aggregates: structure_aggregates,
|
|
94
|
+
automationSlices: structure_automationSlices,
|
|
95
|
+
outboundTranslationSlices: structure_outboundTranslationSlices,
|
|
96
|
+
inboundTranslationSlices: structure_inboundTranslationSlices,
|
|
97
|
+
extensions: structure_extensions,
|
|
98
|
+
extensionPoints: undefined,
|
|
99
|
+
requiredStores: undefined,
|
|
100
|
+
requiredStoreDeclarations: undefined
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
let structures = [[
|
|
104
|
+
"Ordering@1.0.0",
|
|
105
|
+
structure
|
|
106
|
+
]];
|
|
107
|
+
|
|
108
|
+
function bake(selections) {
|
|
109
|
+
return Platform_BakedManifest$ReventlessCore.curate(structures, selections);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function entries(json) {
|
|
113
|
+
return Stdlib_Option.getOr(Stdlib_JSON.Decode.array(json), []);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function names(entry, field) {
|
|
117
|
+
return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), d => d[field]), Stdlib_JSON.Decode.array), []), v => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(v), d => d["name"]), Stdlib_JSON.Decode.string));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function firstEntry(r) {
|
|
121
|
+
if (r.TAG === "Ok") {
|
|
122
|
+
return entries(r._0)[0];
|
|
123
|
+
} else {
|
|
124
|
+
return Stdlib_JsError.throwWithMessage(Platform_BakedManifest$ReventlessCore.describe(r._0));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
globalThis.describe("curate", () => {
|
|
129
|
+
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
|
+
}]));
|
|
135
|
+
globalThis.expect([
|
|
136
|
+
names(entry, "stateViewSlices"),
|
|
137
|
+
names(entry, "readModels")
|
|
138
|
+
]).toEqual([
|
|
139
|
+
["Orders"],
|
|
140
|
+
[]
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
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
|
+
globalThis.expect(names(entry, "stateChangeSlices")).toEqual(["PlaceOrder"]);
|
|
150
|
+
});
|
|
151
|
+
globalThis.test("unset views and commands include every public component", () => {
|
|
152
|
+
let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
153
|
+
plugin: "Ordering",
|
|
154
|
+
views: undefined,
|
|
155
|
+
commands: undefined
|
|
156
|
+
}]));
|
|
157
|
+
globalThis.expect([
|
|
158
|
+
names(entry, "readModels"),
|
|
159
|
+
names(entry, "stateViewSlices"),
|
|
160
|
+
names(entry, "stateChangeSlices")
|
|
161
|
+
]).toEqual([
|
|
162
|
+
["Customers"],
|
|
163
|
+
["Orders"],
|
|
164
|
+
[
|
|
165
|
+
"PlaceOrder",
|
|
166
|
+
"ImportOrders"
|
|
167
|
+
]
|
|
168
|
+
]);
|
|
169
|
+
});
|
|
170
|
+
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
|
+
}]));
|
|
176
|
+
globalThis.expect(names(entry, "internalQueryables")).toEqual(["AvailableProducts"]);
|
|
177
|
+
});
|
|
178
|
+
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
|
+
}]));
|
|
184
|
+
globalThis.expect(names(entry, "internalQueryables")).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
globalThis.test("an Internal view is never selectable as a view", () => {
|
|
187
|
+
globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
188
|
+
plugin: "Ordering",
|
|
189
|
+
views: ["AvailableProducts"],
|
|
190
|
+
commands: undefined
|
|
191
|
+
}])).toEqual({
|
|
192
|
+
TAG: "Error",
|
|
193
|
+
_0: {
|
|
194
|
+
TAG: "UnknownView",
|
|
195
|
+
plugin: "Ordering",
|
|
196
|
+
view: "AvailableProducts"
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
globalThis.test("fails naming an unknown plugin", () => {
|
|
201
|
+
globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
202
|
+
plugin: "Shipping",
|
|
203
|
+
views: undefined,
|
|
204
|
+
commands: undefined
|
|
205
|
+
}])).toEqual({
|
|
206
|
+
TAG: "Error",
|
|
207
|
+
_0: {
|
|
208
|
+
TAG: "UnknownPlugin",
|
|
209
|
+
_0: "Shipping"
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
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({
|
|
219
|
+
TAG: "Error",
|
|
220
|
+
_0: {
|
|
221
|
+
TAG: "UnknownCommand",
|
|
222
|
+
plugin: "Ordering",
|
|
223
|
+
command: "CancelOrder"
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
globalThis.test("an all-inclusive bake is byte-identical to the served entry", () => {
|
|
228
|
+
let baked = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
|
|
229
|
+
plugin: "Ordering",
|
|
230
|
+
views: undefined,
|
|
231
|
+
commands: undefined
|
|
232
|
+
}]));
|
|
233
|
+
let served = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering@1.0.0", structure);
|
|
234
|
+
globalThis.expect(JSON.stringify(baked)).toEqual(JSON.stringify(served));
|
|
235
|
+
});
|
|
236
|
+
globalThis.test("one entry per selection, in declaration order", () => {
|
|
237
|
+
let json = Platform_BakedManifest$ReventlessCore.curate([
|
|
238
|
+
[
|
|
239
|
+
"Ordering@1.0.0",
|
|
240
|
+
structure
|
|
241
|
+
],
|
|
242
|
+
[
|
|
243
|
+
"Catalog@1.0.0",
|
|
244
|
+
structure
|
|
245
|
+
]
|
|
246
|
+
], [
|
|
247
|
+
{
|
|
248
|
+
plugin: "Catalog",
|
|
249
|
+
views: [],
|
|
250
|
+
commands: []
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
plugin: "Ordering",
|
|
254
|
+
views: [],
|
|
255
|
+
commands: []
|
|
256
|
+
}
|
|
257
|
+
]);
|
|
258
|
+
let ids;
|
|
259
|
+
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)) : [];
|
|
260
|
+
globalThis.expect(ids).toEqual([
|
|
261
|
+
"Catalog",
|
|
262
|
+
"Ordering"
|
|
263
|
+
]);
|
|
264
|
+
});
|
|
265
|
+
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
|
+
}]));
|
|
271
|
+
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
|
+
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
|
+
globalThis.expect([
|
|
274
|
+
ownerOf("stateViewSlices", "Orders"),
|
|
275
|
+
commandOwner
|
|
276
|
+
]).toEqual([
|
|
277
|
+
"customerId",
|
|
278
|
+
"customerId"
|
|
279
|
+
]);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export {
|
|
284
|
+
queryable,
|
|
285
|
+
command,
|
|
286
|
+
writable,
|
|
287
|
+
structure,
|
|
288
|
+
structures,
|
|
289
|
+
bake,
|
|
290
|
+
entries,
|
|
291
|
+
names,
|
|
292
|
+
firstEntry,
|
|
293
|
+
}
|
|
294
|
+
/* structure Not a pure module */
|
|
@@ -17,6 +17,8 @@ let cmd: commandDef = {
|
|
|
17
17
|
allowedStates: None,
|
|
18
18
|
targetState: None,
|
|
19
19
|
apiExposed: Some(true),
|
|
20
|
+
requiredAccess: None,
|
|
21
|
+
ownerField: None,
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
let qbl: queryableDef = {
|
|
@@ -34,6 +36,8 @@ let qbl: queryableDef = {
|
|
|
34
36
|
singleQueryField: Some("Catalog_Product"),
|
|
35
37
|
idField: Some("productId"),
|
|
36
38
|
idFieldSource: Some("convention"),
|
|
39
|
+
requiredAccess: None,
|
|
40
|
+
ownerField: None,
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
let wbl: writableDef = {
|
|
@@ -258,6 +262,8 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
|
|
|
258
262
|
singleQueryField: Some("Ordering_AvailableProduct"),
|
|
259
263
|
idField: Some("productId"),
|
|
260
264
|
idFieldSource: Some("sole"),
|
|
265
|
+
requiredAccess: None,
|
|
266
|
+
ownerField: None,
|
|
261
267
|
}
|
|
262
268
|
// A distinct name per source array: the complement has to be fed by both, and
|
|
263
269
|
// reusing one def would hide a version that only walks `readModels`.
|
|
@@ -334,6 +340,8 @@ describe("allowedStates + statusField populated", () => {
|
|
|
334
340
|
allowedStates: Some(["Inactive"]),
|
|
335
341
|
targetState: Some("Active"),
|
|
336
342
|
apiExposed: Some(true),
|
|
343
|
+
requiredAccess: None,
|
|
344
|
+
ownerField: None,
|
|
337
345
|
}
|
|
338
346
|
|
|
339
347
|
let qblWithStatus: queryableDef = {
|
|
@@ -351,6 +359,8 @@ describe("allowedStates + statusField populated", () => {
|
|
|
351
359
|
singleQueryField: Some("Platform_Plugin"),
|
|
352
360
|
idField: None,
|
|
353
361
|
idFieldSource: None,
|
|
362
|
+
requiredAccess: None,
|
|
363
|
+
ownerField: None,
|
|
354
364
|
}
|
|
355
365
|
|
|
356
366
|
let wblWithStates: writableDef = {
|
|
@@ -23,7 +23,9 @@ let cmd = {
|
|
|
23
23
|
references: cmd_references,
|
|
24
24
|
allowedStates: undefined,
|
|
25
25
|
targetState: undefined,
|
|
26
|
-
apiExposed: cmd_apiExposed
|
|
26
|
+
apiExposed: cmd_apiExposed,
|
|
27
|
+
requiredAccess: undefined,
|
|
28
|
+
ownerField: undefined
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
let qbl_consumedEventTypes = ["ProductAdded"];
|
|
@@ -50,11 +52,13 @@ let qbl = {
|
|
|
50
52
|
searchableFields: qbl_searchableFields,
|
|
51
53
|
labelFieldSource: qbl_labelFieldSource,
|
|
52
54
|
statusField: undefined,
|
|
55
|
+
ownerField: undefined,
|
|
53
56
|
visibility: undefined,
|
|
54
57
|
chapter: undefined,
|
|
55
58
|
singleQueryField: qbl_singleQueryField,
|
|
56
59
|
idField: qbl_idField,
|
|
57
|
-
idFieldSource: qbl_idFieldSource
|
|
60
|
+
idFieldSource: qbl_idFieldSource,
|
|
61
|
+
requiredAccess: undefined
|
|
58
62
|
};
|
|
59
63
|
|
|
60
64
|
let wbl_commands = [cmd];
|
|
@@ -279,11 +283,13 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
279
283
|
searchableFields: internalQbl_searchableFields,
|
|
280
284
|
labelFieldSource: internalQbl_labelFieldSource,
|
|
281
285
|
statusField: undefined,
|
|
286
|
+
ownerField: undefined,
|
|
282
287
|
visibility: internalQbl_visibility,
|
|
283
288
|
chapter: undefined,
|
|
284
289
|
singleQueryField: internalQbl_singleQueryField,
|
|
285
290
|
idField: internalQbl_idField,
|
|
286
|
-
idFieldSource: internalQbl_idFieldSource
|
|
291
|
+
idFieldSource: internalQbl_idFieldSource,
|
|
292
|
+
requiredAccess: undefined
|
|
287
293
|
};
|
|
288
294
|
let internalSlice_consumedEventTypes = internalQbl_consumedEventTypes;
|
|
289
295
|
let internalSlice_linkedWriteSide = internalQbl_linkedWriteSide;
|
|
@@ -303,11 +309,13 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
|
|
|
303
309
|
searchableFields: internalSlice_searchableFields,
|
|
304
310
|
labelFieldSource: internalSlice_labelFieldSource,
|
|
305
311
|
statusField: undefined,
|
|
312
|
+
ownerField: undefined,
|
|
306
313
|
visibility: internalSlice_visibility,
|
|
307
314
|
chapter: undefined,
|
|
308
315
|
singleQueryField: internalSlice_singleQueryField,
|
|
309
316
|
idField: internalSlice_idField,
|
|
310
|
-
idFieldSource: internalSlice_idFieldSource
|
|
317
|
+
idFieldSource: internalSlice_idFieldSource,
|
|
318
|
+
requiredAccess: undefined
|
|
311
319
|
};
|
|
312
320
|
let mixed_readModels = [
|
|
313
321
|
qbl,
|
|
@@ -385,7 +393,9 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
385
393
|
references: cmdWithStates_references,
|
|
386
394
|
allowedStates: cmdWithStates_allowedStates,
|
|
387
395
|
targetState: cmdWithStates_targetState,
|
|
388
|
-
apiExposed: cmdWithStates_apiExposed
|
|
396
|
+
apiExposed: cmdWithStates_apiExposed,
|
|
397
|
+
requiredAccess: undefined,
|
|
398
|
+
ownerField: undefined
|
|
389
399
|
};
|
|
390
400
|
let qblWithStatus_consumedEventTypes = [];
|
|
391
401
|
let qblWithStatus_linkedWriteSide = ["Plugin"];
|
|
@@ -403,11 +413,13 @@ globalThis.describe("allowedStates + statusField populated", () => {
|
|
|
403
413
|
searchableFields: qblWithStatus_searchableFields,
|
|
404
414
|
labelFieldSource: qblWithStatus_labelFieldSource,
|
|
405
415
|
statusField: qblWithStatus_statusField,
|
|
416
|
+
ownerField: undefined,
|
|
406
417
|
visibility: undefined,
|
|
407
418
|
chapter: undefined,
|
|
408
419
|
singleQueryField: qblWithStatus_singleQueryField,
|
|
409
420
|
idField: undefined,
|
|
410
|
-
idFieldSource: undefined
|
|
421
|
+
idFieldSource: undefined,
|
|
422
|
+
requiredAccess: undefined
|
|
411
423
|
};
|
|
412
424
|
let wblWithStates_commands = [cmdWithStates];
|
|
413
425
|
let wblWithStates_producedEventTypes = [];
|
|
@@ -480,11 +492,13 @@ globalThis.describe("singleQueryField reaches the caller", () => {
|
|
|
480
492
|
searchableFields: qbl_searchableFields,
|
|
481
493
|
labelFieldSource: "annotation",
|
|
482
494
|
statusField: undefined,
|
|
495
|
+
ownerField: undefined,
|
|
483
496
|
visibility: undefined,
|
|
484
497
|
chapter: undefined,
|
|
485
498
|
singleQueryField: undefined,
|
|
486
499
|
idField: "productId",
|
|
487
|
-
idFieldSource: "convention"
|
|
500
|
+
idFieldSource: "convention",
|
|
501
|
+
requiredAccess: undefined
|
|
488
502
|
}],
|
|
489
503
|
stateViewSlices: structure_stateViewSlices,
|
|
490
504
|
stateChangeSlices: structure_stateChangeSlices,
|
|
@@ -523,11 +537,13 @@ globalThis.describe("idField / idFieldSource reach the caller", () => {
|
|
|
523
537
|
searchableFields: qbl_searchableFields,
|
|
524
538
|
labelFieldSource: "annotation",
|
|
525
539
|
statusField: undefined,
|
|
540
|
+
ownerField: undefined,
|
|
526
541
|
visibility: undefined,
|
|
527
542
|
chapter: undefined,
|
|
528
543
|
singleQueryField: "Catalog_Product",
|
|
529
544
|
idField: undefined,
|
|
530
|
-
idFieldSource: undefined
|
|
545
|
+
idFieldSource: undefined,
|
|
546
|
+
requiredAccess: undefined
|
|
531
547
|
}],
|
|
532
548
|
stateViewSlices: structure_stateViewSlices,
|
|
533
549
|
stateChangeSlices: structure_stateChangeSlices,
|
|
@@ -22,6 +22,8 @@ let publicRm: queryableDef = {
|
|
|
22
22
|
singleQueryField: Some("Catalog_Product"),
|
|
23
23
|
idField: Some("productId"),
|
|
24
24
|
idFieldSource: Some("convention"),
|
|
25
|
+
requiredAccess: None,
|
|
26
|
+
ownerField: None,
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// The component `Platform_ComponentDefinitions` drops and this query must keep.
|