@reventlessdev/reventless-core 3.0.0-alpha.223 → 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 +36 -0
- package/package.json +6 -6
- package/src/admin/Platform_Admin_Structure.res +16 -0
- package/src/admin/Platform_Admin_Structure.res.mjs +14 -3
- package/src/admin/Platform_BakedManifest.res +154 -0
- package/src/admin/Platform_BakedManifest.res.mjs +156 -0
- package/src/admin/Platform_ComponentDefinitionsApi.res +19 -2
- package/src/admin/Platform_ComponentDefinitionsApi.res.mjs +30 -2
- package/src/components/Api/Api_Ids.res +63 -0
- package/src/components/Api/Api_Ids.res.mjs +43 -0
- package/src/components/Api/GraphQL_FragmentGenerator.res +121 -28
- package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +97 -27
- package/src/components/Api/OwnerScopeDiagnostics.res +54 -0
- package/src/components/Api/OwnerScopeDiagnostics.res.mjs +27 -0
- package/src/components/Api/QueryDbListQuery.res +38 -6
- package/src/components/Api/QueryDbListQuery.res.mjs +14 -5
- package/src/components/Api/SchemaType.res +36 -2
- package/src/components/Api/SchemaType.res.mjs +31 -10
- 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 +78 -3
- package/src/plugin/component/Plugin_Structure.res.mjs +38 -9
- package/tests/admin/Platform_BakedManifestTest.res +244 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +294 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +73 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +135 -6
- package/tests/admin/Platform_PluginStructuresApiTest.res +12 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +27 -2
- package/tests/api/Api_IdsTest.res +143 -0
- package/tests/api/Api_IdsTest.res.mjs +97 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res +169 -0
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +179 -0
- 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/PluginStructureTest.res +90 -0
- package/tests/plugin/PluginStructureTest.res.mjs +86 -8
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res +28 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +84 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res +17 -0
- package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +57 -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 = {
|
|
@@ -31,6 +33,11 @@ let qbl: queryableDef = {
|
|
|
31
33
|
statusField: None,
|
|
32
34
|
visibility: None,
|
|
33
35
|
chapter: None,
|
|
36
|
+
singleQueryField: Some("Catalog_Product"),
|
|
37
|
+
idField: Some("productId"),
|
|
38
|
+
idFieldSource: Some("convention"),
|
|
39
|
+
requiredAccess: None,
|
|
40
|
+
ownerField: None,
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
let wbl: writableDef = {
|
|
@@ -252,6 +259,11 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
|
|
|
252
259
|
statusField: None,
|
|
253
260
|
visibility: Some("Internal"),
|
|
254
261
|
chapter: None,
|
|
262
|
+
singleQueryField: Some("Ordering_AvailableProduct"),
|
|
263
|
+
idField: Some("productId"),
|
|
264
|
+
idFieldSource: Some("sole"),
|
|
265
|
+
requiredAccess: None,
|
|
266
|
+
ownerField: None,
|
|
255
267
|
}
|
|
256
268
|
// A distinct name per source array: the complement has to be fed by both, and
|
|
257
269
|
// reusing one def would hide a version that only walks `readModels`.
|
|
@@ -328,6 +340,8 @@ describe("allowedStates + statusField populated", () => {
|
|
|
328
340
|
allowedStates: Some(["Inactive"]),
|
|
329
341
|
targetState: Some("Active"),
|
|
330
342
|
apiExposed: Some(true),
|
|
343
|
+
requiredAccess: None,
|
|
344
|
+
ownerField: None,
|
|
331
345
|
}
|
|
332
346
|
|
|
333
347
|
let qblWithStatus: queryableDef = {
|
|
@@ -342,6 +356,11 @@ describe("allowedStates + statusField populated", () => {
|
|
|
342
356
|
statusField: Some("status"),
|
|
343
357
|
visibility: None,
|
|
344
358
|
chapter: None,
|
|
359
|
+
singleQueryField: Some("Platform_Plugin"),
|
|
360
|
+
idField: None,
|
|
361
|
+
idFieldSource: None,
|
|
362
|
+
requiredAccess: None,
|
|
363
|
+
ownerField: None,
|
|
345
364
|
}
|
|
346
365
|
|
|
347
366
|
let wblWithStates: writableDef = {
|
|
@@ -395,3 +414,57 @@ describe("allowedStates + statusField populated", () => {
|
|
|
395
414
|
expect(json->String.includes("\"errors\":null"))->toEqual(false)
|
|
396
415
|
})
|
|
397
416
|
})
|
|
417
|
+
|
|
418
|
+
// The name exists so a consumer can stop re-implementing `Api_Naming.singularize`;
|
|
419
|
+
// it is only worth publishing if it reaches the caller intact, so assert the wire
|
|
420
|
+
// value and the SDL that types it, not just the record field.
|
|
421
|
+
describe("singleQueryField reaches the caller", () => {
|
|
422
|
+
testSync("encodes the stated singular beside the list field", () => {
|
|
423
|
+
expect(json->String.includes("\"queryField\":\"Catalog_Products\""))->toEqual(true)
|
|
424
|
+
expect(json->String.includes("\"singleQueryField\":\"Catalog_Product\""))->toEqual(true)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
// Nullable on purpose: a structure persisted before the field existed, or a
|
|
428
|
+
// hand-rolled def that declines to state it, must decode rather than fail.
|
|
429
|
+
testSync("encodes an unstated singular as null", () => {
|
|
430
|
+
let unstated =
|
|
431
|
+
{...structure, readModels: [{...qbl, singleQueryField: None}]}
|
|
432
|
+
->Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId="Catalog", _)
|
|
433
|
+
->JSON.stringify
|
|
434
|
+
expect(unstated->String.includes("\"singleQueryField\":null"))->toEqual(true)
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
testSync("types it as a nullable String on the read-side SDL", () => {
|
|
438
|
+
let sdl = Platform_ComponentDefinitionsApi.sdlTypes->Array.join("\n")
|
|
439
|
+
expect(sdl->String.includes("singleQueryField: String\n"))->toEqual(true)
|
|
440
|
+
expect(sdl->String.includes("singleQueryField: String!"))->toEqual(false)
|
|
441
|
+
})
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
// The key and its rung travel together: a consumer ranking its own guess against
|
|
445
|
+
// this one needs both, so a version that carried the field and dropped the
|
|
446
|
+
// provenance would be worse than useless.
|
|
447
|
+
describe("idField / idFieldSource reach the caller", () => {
|
|
448
|
+
testSync("encodes the key field and the rung that produced it", () => {
|
|
449
|
+
expect(json->String.includes("\"idField\":\"productId\""))->toEqual(true)
|
|
450
|
+
expect(json->String.includes("\"idFieldSource\":\"convention\""))->toEqual(true)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
// A state whose key cannot be resolved (several `*Id` fields and no name match,
|
|
454
|
+
// or none at all) has nothing to report — null, not an invented field name.
|
|
455
|
+
testSync("encodes an unresolved key as null on both fields", () => {
|
|
456
|
+
let unresolved =
|
|
457
|
+
{...structure, readModels: [{...qbl, idField: None, idFieldSource: None}]}
|
|
458
|
+
->Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId="Catalog", _)
|
|
459
|
+
->JSON.stringify
|
|
460
|
+
expect(unresolved->String.includes("\"idField\":null"))->toEqual(true)
|
|
461
|
+
expect(unresolved->String.includes("\"idFieldSource\":null"))->toEqual(true)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
testSync("types both as nullable Strings on the read-side SDL", () => {
|
|
465
|
+
let sdl = Platform_ComponentDefinitionsApi.sdlTypes->Array.join("\n")
|
|
466
|
+
expect(sdl->String.includes("idField: String\n"))->toEqual(true)
|
|
467
|
+
expect(sdl->String.includes("idFieldSource: String\n"))->toEqual(true)
|
|
468
|
+
expect(sdl->String.includes("idField: String!"))->toEqual(false)
|
|
469
|
+
})
|
|
470
|
+
})
|