@reventlessdev/reventless-core 3.0.0-alpha.251 → 3.0.0-alpha.252
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 +15 -0
- package/package.json +7 -7
- package/src/admin/Platform_Admin_Structure.res +4 -1
- package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
- package/src/admin/Platform_BakedManifest.res.mjs +3 -1
- package/src/admin/Platform_PluginStructuresApi.res +43 -7
- package/src/admin/Platform_PluginStructuresApi.res.mjs +47 -1
- package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +8 -0
- package/src/components/Api/ApiNoApiHelpers.res +37 -3
- package/src/components/Api/ApiNoApiHelpers.res.mjs +29 -2
- package/src/components/AutomationSlice/AutomationSlice_Callback.res +4 -4
- package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +3 -3
- package/src/plugin/api/PluginBaseFragment.res +3 -3
- package/src/plugin/component/Plugin_Structure.res +175 -22
- package/src/plugin/component/Plugin_Structure.res.mjs +89 -17
- package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +3 -1
- package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +3 -1
- package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +3 -1
- package/src/plugin/lifecycle/PluginBehavior.res +11 -11
- package/src/plugin/lifecycle/PluginBehavior.res.mjs +11 -11
- package/src/plugin/lifecycle/PluginSpec.res +11 -3
- package/src/plugin/lifecycle/PluginSpec.res.mjs +11 -3
- package/tests/admin/AdminApiSchemaDriftTest.res +27 -1
- package/tests/admin/AdminApiSchemaDriftTest.res.mjs +22 -2
- package/tests/admin/Platform_Admin_StructureTest.res +8 -3
- package/tests/admin/Platform_Admin_StructureTest.res.mjs +8 -2
- package/tests/admin/Platform_BakedManifestTest.res +2 -0
- package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res +4 -0
- package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -7
- package/tests/admin/Platform_PluginStructuresApiTest.res +16 -0
- package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +17 -3
- package/tests/aggregate/AggregateCacheTest.res.mjs +15 -3
- package/tests/aggregate/AggregateConflictTest.res.mjs +15 -3
- package/tests/aggregate/AggregateFixtures.res.mjs +15 -3
- package/tests/aggregate/AggregateSnapshotTest.res.mjs +21 -5
- package/tests/api/ApiNoApiTest.res +46 -0
- package/tests/api/ApiNoApiTest.res.mjs +49 -1
- package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +27 -3
- package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +12 -2
- package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +2 -0
- package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +12 -2
- package/tests/dcb/DcbFixtures.res.mjs +8 -0
- package/tests/dcb/DcbStateChangeSliceTest.res.mjs +18 -0
- package/tests/eventmapper/EventMapperFixtures.res.mjs +15 -3
- package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +9 -1
- package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -1
- package/tests/message/MessageTest.res +2 -0
- package/tests/message/MessageTest.res.mjs +3 -1
- package/tests/plugin/PluginStructureAccessTest.res.mjs +2 -0
- package/tests/plugin/PluginStructureTest.res +185 -1
- package/tests/plugin/PluginStructureTest.res.mjs +246 -3
- package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res +59 -0
- package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res.mjs +115 -0
- package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -0
- package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +8 -0
- package/tests/plugin/pluginDefinitionRequiredScalars.txt +18 -0
- package/tests/util/CommandPublisherTest.res.mjs +9 -1
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
open JestGlobals
|
|
2
2
|
open ApiNoApiHelpers
|
|
3
3
|
|
|
4
|
+
// The exclusion has to survive a variant spread. Splicing copies a union's
|
|
5
|
+
// members, not its metadata, so an exclusion recorded only on the union vanishes
|
|
6
|
+
// on the way into the host — and the host publishes as a mutation and a
|
|
7
|
+
// subscription a command whose author marked it internal. No error, no warning.
|
|
8
|
+
//
|
|
9
|
+
// Built the way the PPX builds it (`markNoApiVariants` on a freshly made union),
|
|
10
|
+
// then spliced the way the compiler splices it (concatenating `anyOf`), so this
|
|
11
|
+
// exercises the real path rather than a hand-stamped stand-in.
|
|
12
|
+
let member = name =>
|
|
13
|
+
S.object(o => {
|
|
14
|
+
let _ = o.field("TAG", S.literal(name))
|
|
15
|
+
})->S.castToUnknown
|
|
16
|
+
|
|
17
|
+
let sourceUnion =
|
|
18
|
+
S.union([member("Public"), member("Internal")])->ReventlessInfra.Api.markNoApiVariants([
|
|
19
|
+
"Internal",
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
let spliced = switch sourceUnion {
|
|
23
|
+
| AnyOf({anyOf}) => S.union(Array.concat([member("HostOwn")], anyOf))
|
|
24
|
+
| _ => sourceUnion
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("a spread carries its exclusions", () => {
|
|
28
|
+
testSync("the source union still reports its own exclusion", () =>
|
|
29
|
+
expect(getExcludedVariants(sourceUnion)->Option.map(Set.toArray))->toEqual(Some(["Internal"]))
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
testSync("a host that splices the members inherits the exclusion", () =>
|
|
33
|
+
expect(getExcludedVariants(spliced)->Option.map(Set.toArray))->toEqual(Some(["Internal"]))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// The failure this closes, stated as the thing it must not do: the spliced
|
|
37
|
+
// internal variant must not appear among the host's callable fields.
|
|
38
|
+
testSync("the spliced internal variant is filtered out of a host's fields", () =>
|
|
39
|
+
expect(filterNoApiVariants(["HostOwn", "Public", "Internal"], spliced))->toEqual([
|
|
40
|
+
"HostOwn",
|
|
41
|
+
"Public",
|
|
42
|
+
])
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
testSync("nothing else is filtered", () =>
|
|
46
|
+
expect(filterNoApiVariants(["HostOwn", "Public"], spliced))->toEqual(["HostOwn", "Public"])
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
4
50
|
describe("ApiNoApiHelpers:", () => {
|
|
5
51
|
testSync("isNoApi returns false for schema without noApi metadata", () => {
|
|
6
52
|
let schema = S.string->S.castToUnknown
|
|
@@ -5,6 +5,49 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
5
5
|
import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
|
|
6
6
|
import * as ApiNoApiHelpers$ReventlessCore from "../../src/components/Api/ApiNoApiHelpers.res.mjs";
|
|
7
7
|
|
|
8
|
+
function member(name) {
|
|
9
|
+
return Sury.object(o => {
|
|
10
|
+
o.f("TAG", Sury.literal(name));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let sourceUnion = Api$ReventlessInfra.markNoApiVariants(Sury.union([
|
|
15
|
+
member("Public"),
|
|
16
|
+
member("Internal")
|
|
17
|
+
]), ["Internal"]);
|
|
18
|
+
|
|
19
|
+
let spliced;
|
|
20
|
+
|
|
21
|
+
spliced = sourceUnion.type === "anyOf" ? Sury.union([member("HostOwn")].concat(sourceUnion.anyOf)) : sourceUnion;
|
|
22
|
+
|
|
23
|
+
globalThis.describe("a spread carries its exclusions", () => {
|
|
24
|
+
globalThis.test("the source union still reports its own exclusion", () => {
|
|
25
|
+
globalThis.expect(Stdlib_Option.map(ApiNoApiHelpers$ReventlessCore.getExcludedVariants(sourceUnion), prim => Array.from(prim))).toEqual(["Internal"]);
|
|
26
|
+
});
|
|
27
|
+
globalThis.test("a host that splices the members inherits the exclusion", () => {
|
|
28
|
+
globalThis.expect(Stdlib_Option.map(ApiNoApiHelpers$ReventlessCore.getExcludedVariants(spliced), prim => Array.from(prim))).toEqual(["Internal"]);
|
|
29
|
+
});
|
|
30
|
+
globalThis.test("the spliced internal variant is filtered out of a host's fields", () => {
|
|
31
|
+
globalThis.expect(ApiNoApiHelpers$ReventlessCore.filterNoApiVariants([
|
|
32
|
+
"HostOwn",
|
|
33
|
+
"Public",
|
|
34
|
+
"Internal"
|
|
35
|
+
], spliced)).toEqual([
|
|
36
|
+
"HostOwn",
|
|
37
|
+
"Public"
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
globalThis.test("nothing else is filtered", () => {
|
|
41
|
+
globalThis.expect(ApiNoApiHelpers$ReventlessCore.filterNoApiVariants([
|
|
42
|
+
"HostOwn",
|
|
43
|
+
"Public"
|
|
44
|
+
], spliced)).toEqual([
|
|
45
|
+
"HostOwn",
|
|
46
|
+
"Public"
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
8
51
|
globalThis.describe("ApiNoApiHelpers:", () => {
|
|
9
52
|
globalThis.test("isNoApi returns false for schema without noApi metadata", () => {
|
|
10
53
|
let schema = Sury.string;
|
|
@@ -95,4 +138,9 @@ globalThis.describe("ApiNoApiHelpers:", () => {
|
|
|
95
138
|
});
|
|
96
139
|
});
|
|
97
140
|
|
|
98
|
-
|
|
141
|
+
export {
|
|
142
|
+
member,
|
|
143
|
+
sourceUnion,
|
|
144
|
+
spliced,
|
|
145
|
+
}
|
|
146
|
+
/* sourceUnion Not a pure module */
|
|
@@ -24,9 +24,17 @@ function commandAuthorization(param) {
|
|
|
24
24
|
return "AllowAuthenticated";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function commandTransition(param) {
|
|
28
|
+
return "Unrestricted";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let traits = [];
|
|
32
|
+
|
|
27
33
|
let OrderSliceCmd = {
|
|
28
34
|
commandSchema: commandSchema,
|
|
29
|
-
commandAuthorization: commandAuthorization
|
|
35
|
+
commandAuthorization: commandAuthorization,
|
|
36
|
+
commandTransition: commandTransition,
|
|
37
|
+
traits: traits
|
|
30
38
|
};
|
|
31
39
|
|
|
32
40
|
function mutationFor(fragment, needle) {
|
|
@@ -196,9 +204,17 @@ function commandAuthorization$1(param) {
|
|
|
196
204
|
return "AllowAuthenticated";
|
|
197
205
|
}
|
|
198
206
|
|
|
207
|
+
function commandTransition$1(param) {
|
|
208
|
+
return "Unrestricted";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let traits$1 = [];
|
|
212
|
+
|
|
199
213
|
let AddProductCmd = {
|
|
200
214
|
commandSchema: commandSchema$1,
|
|
201
|
-
commandAuthorization: commandAuthorization$1
|
|
215
|
+
commandAuthorization: commandAuthorization$1,
|
|
216
|
+
commandTransition: commandTransition$1,
|
|
217
|
+
traits: traits$1
|
|
202
218
|
};
|
|
203
219
|
|
|
204
220
|
let commandSchema$2 = Sury.union([
|
|
@@ -217,9 +233,17 @@ function commandAuthorization$2(param) {
|
|
|
217
233
|
return "AllowAuthenticated";
|
|
218
234
|
}
|
|
219
235
|
|
|
236
|
+
function commandTransition$2(param) {
|
|
237
|
+
return "Unrestricted";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let traits$2 = [];
|
|
241
|
+
|
|
220
242
|
let ChangePriceCmd = {
|
|
221
243
|
commandSchema: commandSchema$2,
|
|
222
|
-
commandAuthorization: commandAuthorization$2
|
|
244
|
+
commandAuthorization: commandAuthorization$2,
|
|
245
|
+
commandTransition: commandTransition$2,
|
|
246
|
+
traits: traits$2
|
|
223
247
|
};
|
|
224
248
|
|
|
225
249
|
globalThis.describe("semantic composites are named once, not once per field", () => {
|
|
@@ -32,6 +32,12 @@ function commandAuthorization(param) {
|
|
|
32
32
|
return "AllowAuthenticated";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function commandTransition(param) {
|
|
36
|
+
return "Unrestricted";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let traits = [];
|
|
40
|
+
|
|
35
41
|
let CmdGenAggSpec = {
|
|
36
42
|
Id: undefined,
|
|
37
43
|
name: name,
|
|
@@ -39,7 +45,9 @@ let CmdGenAggSpec = {
|
|
|
39
45
|
eventSchema: eventSchema,
|
|
40
46
|
errorSchema: errorSchema,
|
|
41
47
|
moduleUrl: moduleUrl,
|
|
42
|
-
commandAuthorization: commandAuthorization
|
|
48
|
+
commandAuthorization: commandAuthorization,
|
|
49
|
+
commandTransition: commandTransition,
|
|
50
|
+
traits: traits
|
|
43
51
|
};
|
|
44
52
|
|
|
45
53
|
let moduleUrl$1 = import.meta.url;
|
|
@@ -106,7 +114,9 @@ let TestGenerator = CommandGenerator_Callback$ReventlessCore.Make(MockPublishSpe
|
|
|
106
114
|
errorSchema: errorSchema,
|
|
107
115
|
commandSchema: commandSchema,
|
|
108
116
|
moduleUrl: moduleUrl,
|
|
109
|
-
commandAuthorization: commandAuthorization
|
|
117
|
+
commandAuthorization: commandAuthorization,
|
|
118
|
+
commandTransition: commandTransition,
|
|
119
|
+
traits: traits
|
|
110
120
|
});
|
|
111
121
|
|
|
112
122
|
let singleTagCommandSchema = Sury.$schema(s => ({
|
|
@@ -65,6 +65,8 @@ let SliceHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
65
65
|
eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
|
|
66
66
|
commandSchema: DcbFixtures$ReventlessCore.TestCommandSpec.commandSchema,
|
|
67
67
|
commandAuthorization: DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization,
|
|
68
|
+
commandTransition: DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition,
|
|
69
|
+
traits: DcbFixtures$ReventlessCore.TestCommandSpec.traits,
|
|
68
70
|
readConsistency: DcbFixtures$ReventlessCore.TestCommandSpec.readConsistency
|
|
69
71
|
})(SliceBehavior);
|
|
70
72
|
|
|
@@ -25,11 +25,19 @@ function commandAuthorization(param) {
|
|
|
25
25
|
return "AllowAuthenticated";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function commandTransition(param) {
|
|
29
|
+
return "Unrestricted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let traits = [];
|
|
33
|
+
|
|
28
34
|
let TestSpec = {
|
|
29
35
|
Id: undefined,
|
|
30
36
|
name: name,
|
|
31
37
|
commandSchema: commandSchema,
|
|
32
|
-
commandAuthorization: commandAuthorization
|
|
38
|
+
commandAuthorization: commandAuthorization,
|
|
39
|
+
commandTransition: commandTransition,
|
|
40
|
+
traits: traits
|
|
33
41
|
};
|
|
34
42
|
|
|
35
43
|
let capturedItems = {
|
|
@@ -64,7 +72,9 @@ let TestOps_Spec = {
|
|
|
64
72
|
Id: Id$Reventless.StringPure,
|
|
65
73
|
name: name,
|
|
66
74
|
commandSchema: commandSchema,
|
|
67
|
-
commandAuthorization: commandAuthorization
|
|
75
|
+
commandAuthorization: commandAuthorization,
|
|
76
|
+
commandTransition: commandTransition,
|
|
77
|
+
traits: traits
|
|
68
78
|
};
|
|
69
79
|
|
|
70
80
|
let TestOps = {
|
|
@@ -156,6 +156,12 @@ function commandAuthorization(param) {
|
|
|
156
156
|
return "AllowAuthenticated";
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
function commandTransition(param) {
|
|
160
|
+
return "Unrestricted";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let traits = [];
|
|
164
|
+
|
|
159
165
|
let TestCommandSpec_initialState = {
|
|
160
166
|
exists: false,
|
|
161
167
|
currentName: undefined
|
|
@@ -173,6 +179,8 @@ let TestCommandSpec = {
|
|
|
173
179
|
evolve: evolve,
|
|
174
180
|
decide: decide,
|
|
175
181
|
commandAuthorization: commandAuthorization,
|
|
182
|
+
commandTransition: commandTransition,
|
|
183
|
+
traits: traits,
|
|
176
184
|
readConsistency: "EscalateOnRetry"
|
|
177
185
|
};
|
|
178
186
|
|
|
@@ -61,6 +61,8 @@ let TestHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
61
61
|
eventSchema: DcbFixtures$ReventlessCore.TestCommandSpec.eventSchema,
|
|
62
62
|
commandSchema: DcbFixtures$ReventlessCore.TestCommandSpec.commandSchema,
|
|
63
63
|
commandAuthorization: DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization,
|
|
64
|
+
commandTransition: DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition,
|
|
65
|
+
traits: DcbFixtures$ReventlessCore.TestCommandSpec.traits,
|
|
64
66
|
readConsistency: DcbFixtures$ReventlessCore.TestCommandSpec.readConsistency
|
|
65
67
|
})(TestCommandBehavior);
|
|
66
68
|
|
|
@@ -78,6 +80,10 @@ let errorSchema = DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema;
|
|
|
78
80
|
|
|
79
81
|
let commandAuthorization = DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization;
|
|
80
82
|
|
|
83
|
+
let commandTransition = DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition;
|
|
84
|
+
|
|
85
|
+
let traits = DcbFixtures$ReventlessCore.TestCommandSpec.traits;
|
|
86
|
+
|
|
81
87
|
let StrongSpec_Id = DcbFixtures$ReventlessCore.TestCommandSpec.Id;
|
|
82
88
|
|
|
83
89
|
let StrongSpec_initialState = DcbFixtures$ReventlessCore.TestCommandSpec.initialState;
|
|
@@ -98,6 +104,8 @@ let StrongSpec = {
|
|
|
98
104
|
evolve: StrongSpec_evolve,
|
|
99
105
|
decide: StrongSpec_decide,
|
|
100
106
|
commandAuthorization: commandAuthorization,
|
|
107
|
+
commandTransition: commandTransition,
|
|
108
|
+
traits: traits,
|
|
101
109
|
readConsistency: "AlwaysStrong"
|
|
102
110
|
};
|
|
103
111
|
|
|
@@ -115,6 +123,10 @@ let errorSchema$1 = DcbFixtures$ReventlessCore.TestCommandSpec.errorSchema;
|
|
|
115
123
|
|
|
116
124
|
let commandAuthorization$1 = DcbFixtures$ReventlessCore.TestCommandSpec.commandAuthorization;
|
|
117
125
|
|
|
126
|
+
let commandTransition$1 = DcbFixtures$ReventlessCore.TestCommandSpec.commandTransition;
|
|
127
|
+
|
|
128
|
+
let traits$1 = DcbFixtures$ReventlessCore.TestCommandSpec.traits;
|
|
129
|
+
|
|
118
130
|
let EventualSpec_Id = DcbFixtures$ReventlessCore.TestCommandSpec.Id;
|
|
119
131
|
|
|
120
132
|
let EventualSpec_initialState = DcbFixtures$ReventlessCore.TestCommandSpec.initialState;
|
|
@@ -135,6 +147,8 @@ let EventualSpec = {
|
|
|
135
147
|
evolve: EventualSpec_evolve,
|
|
136
148
|
decide: EventualSpec_decide,
|
|
137
149
|
commandAuthorization: commandAuthorization$1,
|
|
150
|
+
commandTransition: commandTransition$1,
|
|
151
|
+
traits: traits$1,
|
|
138
152
|
readConsistency: "AlwaysEventual"
|
|
139
153
|
};
|
|
140
154
|
|
|
@@ -147,6 +161,8 @@ let StrongHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
147
161
|
eventSchema: eventSchema,
|
|
148
162
|
commandSchema: commandSchema,
|
|
149
163
|
commandAuthorization: commandAuthorization,
|
|
164
|
+
commandTransition: commandTransition,
|
|
165
|
+
traits: traits,
|
|
150
166
|
readConsistency: "AlwaysStrong"
|
|
151
167
|
})(TestCommandBehavior);
|
|
152
168
|
|
|
@@ -159,6 +175,8 @@ let EventualHandler = StateChangeSlice_Callback$ReventlessCore.Make({
|
|
|
159
175
|
eventSchema: eventSchema$1,
|
|
160
176
|
commandSchema: commandSchema$1,
|
|
161
177
|
commandAuthorization: commandAuthorization$1,
|
|
178
|
+
commandTransition: commandTransition$1,
|
|
179
|
+
traits: traits$1,
|
|
162
180
|
readConsistency: "AlwaysEventual"
|
|
163
181
|
})(TestCommandBehavior);
|
|
164
182
|
|
|
@@ -153,12 +153,20 @@ function commandAuthorization(param) {
|
|
|
153
153
|
return "AllowAuthenticated";
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
function commandTransition(param) {
|
|
157
|
+
return "Unrestricted";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let traits = [];
|
|
161
|
+
|
|
156
162
|
let CmdTargetSpec = {
|
|
157
163
|
Id: undefined,
|
|
158
164
|
name: name$3,
|
|
159
165
|
commandSchema: commandSchema,
|
|
160
166
|
moduleUrl: moduleUrl$2,
|
|
161
|
-
commandAuthorization: commandAuthorization
|
|
167
|
+
commandAuthorization: commandAuthorization,
|
|
168
|
+
commandTransition: commandTransition,
|
|
169
|
+
traits: traits
|
|
162
170
|
};
|
|
163
171
|
|
|
164
172
|
function map(id, event, _queryEngine) {
|
|
@@ -233,7 +241,9 @@ let OrderMappings_Target = {
|
|
|
233
241
|
name: name$3,
|
|
234
242
|
commandSchema: commandSchema,
|
|
235
243
|
moduleUrl: moduleUrl$2,
|
|
236
|
-
commandAuthorization: commandAuthorization
|
|
244
|
+
commandAuthorization: commandAuthorization,
|
|
245
|
+
commandTransition: commandTransition,
|
|
246
|
+
traits: traits
|
|
237
247
|
};
|
|
238
248
|
|
|
239
249
|
let OrderMappings = {
|
|
@@ -262,7 +272,9 @@ let CountOrderMappings_Target = {
|
|
|
262
272
|
name: name$3,
|
|
263
273
|
commandSchema: commandSchema,
|
|
264
274
|
moduleUrl: moduleUrl$2,
|
|
265
|
-
commandAuthorization: commandAuthorization
|
|
275
|
+
commandAuthorization: commandAuthorization,
|
|
276
|
+
commandTransition: commandTransition,
|
|
277
|
+
traits: traits
|
|
266
278
|
};
|
|
267
279
|
|
|
268
280
|
let CountOrderMappings = {
|
|
@@ -30,13 +30,21 @@ function commandAuthorization(param) {
|
|
|
30
30
|
return "AllowAuthenticated";
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function commandTransition(param) {
|
|
34
|
+
return "Unrestricted";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let traits = [];
|
|
38
|
+
|
|
33
39
|
let TestEPSpec = {
|
|
34
40
|
name: name,
|
|
35
41
|
commandSchema: commandSchema,
|
|
36
42
|
eventSchema: eventSchema,
|
|
37
43
|
directiveSchema: directiveSchema,
|
|
38
44
|
moduleUrl: moduleUrl,
|
|
39
|
-
commandAuthorization: commandAuthorization
|
|
45
|
+
commandAuthorization: commandAuthorization,
|
|
46
|
+
commandTransition: commandTransition,
|
|
47
|
+
traits: traits
|
|
40
48
|
};
|
|
41
49
|
|
|
42
50
|
let capturedPublishedCmds = {
|
|
@@ -25,13 +25,21 @@ function commandAuthorization(param) {
|
|
|
25
25
|
return "AllowAuthenticated";
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function commandTransition(param) {
|
|
29
|
+
return "Unrestricted";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let traits = [];
|
|
33
|
+
|
|
28
34
|
let OpsEPSpec = {
|
|
29
35
|
name: name,
|
|
30
36
|
commandSchema: commandSchema,
|
|
31
37
|
eventSchema: eventSchema,
|
|
32
38
|
directiveSchema: directiveSchema,
|
|
33
39
|
moduleUrl: moduleUrl,
|
|
34
|
-
commandAuthorization: commandAuthorization
|
|
40
|
+
commandAuthorization: commandAuthorization,
|
|
41
|
+
commandTransition: commandTransition,
|
|
42
|
+
traits: traits
|
|
35
43
|
};
|
|
36
44
|
|
|
37
45
|
let capturedPublished = {
|
|
@@ -124,7 +124,9 @@ globalThis.describe("Message should", () => {
|
|
|
124
124
|
extensions: [],
|
|
125
125
|
extensionPoints: undefined,
|
|
126
126
|
requiredStores: undefined,
|
|
127
|
-
requiredStoreDeclarations: undefined
|
|
127
|
+
requiredStoreDeclarations: undefined,
|
|
128
|
+
requiredCapabilities: undefined,
|
|
129
|
+
traitDeclarations: undefined
|
|
128
130
|
}
|
|
129
131
|
},
|
|
130
132
|
dcbEventLog: undefined,
|
|
@@ -28,6 +28,8 @@ let PsGatedCommandsSlice_Spec = {
|
|
|
28
28
|
eventSchema: PsGatedCommands$ReventlessCore.eventSchema,
|
|
29
29
|
commandSchema: PsGatedCommands$ReventlessCore.commandSchema,
|
|
30
30
|
commandAuthorization: PsGatedCommands$ReventlessCore.commandAuthorization,
|
|
31
|
+
commandTransition: PsGatedCommands$ReventlessCore.commandTransition,
|
|
32
|
+
traits: PsGatedCommands$ReventlessCore.traits,
|
|
31
33
|
readConsistency: PsGatedCommands$ReventlessCore.readConsistency
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -185,6 +185,19 @@ module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
|
185
185
|
type component = scsComponent
|
|
186
186
|
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
187
187
|
}
|
|
188
|
+
module PsTransitionSwitchSlice: ReventlessInfra.StateChangeSlice.T = {
|
|
189
|
+
module Spec = PsTransitionSwitch
|
|
190
|
+
module Behavior = {
|
|
191
|
+
type state = PsTransitionSwitch.state
|
|
192
|
+
let initialState = PsTransitionSwitch.initialState
|
|
193
|
+
let evolve = PsTransitionSwitch.evolve
|
|
194
|
+
let decide = PsTransitionSwitch.decide
|
|
195
|
+
let moduleUrl = PsTransitionSwitch.moduleUrl
|
|
196
|
+
}
|
|
197
|
+
let isAsync = false
|
|
198
|
+
type component = scsComponent
|
|
199
|
+
let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
|
|
200
|
+
}
|
|
188
201
|
module PsShipmentsViewSlice: ReventlessInfra.StateViewSlice.T = {
|
|
189
202
|
module Spec = PsShipmentsView
|
|
190
203
|
module Projection = {
|
|
@@ -290,6 +303,87 @@ describe("lifecycle topology", () => {
|
|
|
290
303
|
})
|
|
291
304
|
})
|
|
292
305
|
|
|
306
|
+
describe("commandTransition — the edge as a switch", () => {
|
|
307
|
+
// The seam a variant spread needs. `@transition` lowers to a dict on the
|
|
308
|
+
// parent union keyed by variant name, and a spread splices members, so a
|
|
309
|
+
// command a host did not declare cannot be annotated at all. A switch over the
|
|
310
|
+
// command reaches every constructor, spliced or not, and the compiler makes
|
|
311
|
+
// it exhaustive.
|
|
312
|
+
let structure = Plugin_Structure.make(
|
|
313
|
+
~name="TransitionSwitchPlugin",
|
|
314
|
+
~stateChangeSlices=[module(PsTransitionSwitchSlice)],
|
|
315
|
+
~stateViewSlices=[],
|
|
316
|
+
)
|
|
317
|
+
let slice = structure.stateChangeSlices->Array.getUnsafe(0)
|
|
318
|
+
let byName = name => slice.commands->Array.find(c => c.name == name)
|
|
319
|
+
|
|
320
|
+
testSync("the switch fills allowedStates and targetState", () => {
|
|
321
|
+
expect((
|
|
322
|
+
byName("Book")->Option.flatMap(c => c.allowedStates),
|
|
323
|
+
byName("Book")->Option.flatMap(c => c.targetState),
|
|
324
|
+
))->toEqual((Some(["Draft"]), Some("Booked")))
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
testSync("a guard declares its from-set and no target", () => {
|
|
328
|
+
// The same positive claim the one-sided `@transition` makes: legal in these
|
|
329
|
+
// states, and it moves the row nowhere.
|
|
330
|
+
expect((
|
|
331
|
+
byName("Rebook")->Option.flatMap(c => c.allowedStates),
|
|
332
|
+
byName("Rebook")->Option.flatMap(c => c.targetState),
|
|
333
|
+
))->toEqual((Some(["Booked"]), None))
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
testSync("the switch wins over an @transition on the same command", () => {
|
|
337
|
+
// `Rebook` carries both, and they disagree on purpose: the attribute says
|
|
338
|
+
// Draft. Two sources of truth need a stated winner, and it is the one the
|
|
339
|
+
// compiler checked.
|
|
340
|
+
expect(byName("Rebook")->Option.flatMap(c => c.allowedStates))->toEqual(Some(["Booked"]))
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
testSync("Unrestricted declares nothing, leaving the command unconstrained", () => {
|
|
344
|
+
// Distinct from a from-set naming every state, which says the same thing to
|
|
345
|
+
// a menu and something different to a reader.
|
|
346
|
+
expect((
|
|
347
|
+
byName("Abandon")->Option.flatMap(c => c.allowedStates),
|
|
348
|
+
byName("Abandon")->Option.flatMap(c => c.targetState),
|
|
349
|
+
))->toEqual((None, None))
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
describe("traitDeclarations — a graft leaves a trace", () => {
|
|
354
|
+
// The one signal a graft leaves that survives into a deployed plugin. Every
|
|
355
|
+
// other one — the package dependency, the variant spread, the rules alias, the
|
|
356
|
+
// conformance binding — is source-side and invisible to a running estate.
|
|
357
|
+
let structure = Plugin_Structure.make(
|
|
358
|
+
~name="TraitPlugin",
|
|
359
|
+
~stateChangeSlices=[module(PsTransitionSwitchSlice), module(PsShipOrderSlice)],
|
|
360
|
+
~stateViewSlices=[],
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
testSync("a declaring component's trait reaches the structure", () => {
|
|
364
|
+
expect(
|
|
365
|
+
structure.traitDeclarations->Option.getOr([])->Array.map(d => (d.trait, d.component)),
|
|
366
|
+
)->toEqual([("@reventlessdev/trait-booking", "TransitionSwitch")])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
testSync("the component name is filled in by the structure, not declared", () => {
|
|
370
|
+
// The trait exports its identity and posture; it cannot know which component a
|
|
371
|
+
// host grafted it onto, and a host writing that name down would be the one
|
|
372
|
+
// hand-typed string this mechanism exists to avoid.
|
|
373
|
+
expect(
|
|
374
|
+
structure.traitDeclarations
|
|
375
|
+
->Option.getOr([])
|
|
376
|
+
->Array.map(d => (d.version, d.posture, d.component)),
|
|
377
|
+
)->toEqual([("9.9.9", "SelfContained", "TransitionSwitch")])
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
testSync("a component that is nobody's graft contributes nothing", () => {
|
|
381
|
+
// `PsShipOrder` declares no traits, and the injected default is empty — so the
|
|
382
|
+
// absence of a graft is silence rather than a row.
|
|
383
|
+
expect(structure.traitDeclarations->Option.getOr([])->Array.length)->toBe(1)
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
|
|
293
387
|
describe("@transition cross-check", () => {
|
|
294
388
|
// The exit criterion: a state no linked view declares stops the build, and the
|
|
295
389
|
// message says which command, which state, and what it was checked against.
|
|
@@ -1366,6 +1460,45 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1366
1460
|
expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
|
|
1367
1461
|
)
|
|
1368
1462
|
|
|
1463
|
+
// A capability a slice declares rides the same manifest as the stores, so
|
|
1464
|
+
// the platform generator reads one file. It carries no `field`: nothing
|
|
1465
|
+
// annotated it, and naming one would be a fiction.
|
|
1466
|
+
describe("declared capabilities", () => {
|
|
1467
|
+
let withCapability = {
|
|
1468
|
+
...structure,
|
|
1469
|
+
requiredCapabilities: Some([
|
|
1470
|
+
({capability: "Geocoding", component: "GeocodeAddress"}: Reventless.Plugin.requiredCapabilityDeclaration),
|
|
1471
|
+
{capability: "Geocoding", component: "GeocodeDropOff"},
|
|
1472
|
+
]),
|
|
1473
|
+
traitDeclarations: None,
|
|
1474
|
+
}
|
|
1475
|
+
let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
|
|
1476
|
+
|
|
1477
|
+
testSync("two components needing one capability yield one entry", () =>
|
|
1478
|
+
expect(entries->Array.map(e => e.key))->toEqual(["Geocoding"])
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
testSync("both declaring components are kept, with no field", () =>
|
|
1482
|
+
expect((entries->Array.getUnsafe(0)).declaredBy)->toEqual([
|
|
1483
|
+
({component: "GeocodeAddress"}: Reventless.CapabilityManifest.provenance),
|
|
1484
|
+
{component: "GeocodeDropOff"},
|
|
1485
|
+
])
|
|
1486
|
+
)
|
|
1487
|
+
|
|
1488
|
+
// The generator downstream renders a real `Platform.capability` arm, and
|
|
1489
|
+
// a name this build cannot map has no arm to render — so it is dropped
|
|
1490
|
+
// here rather than passed through as an unrenderable key.
|
|
1491
|
+
testSync("a capability this build does not know is dropped", () =>
|
|
1492
|
+
expect(
|
|
1493
|
+
Reventless.CapabilityManifest.fromStructure({
|
|
1494
|
+
...structure,
|
|
1495
|
+
requiredCapabilities: Some([{capability: "Telepathy", component: "Guess"}]),
|
|
1496
|
+
traitDeclarations: None,
|
|
1497
|
+
}).capabilities,
|
|
1498
|
+
)->toEqual([])
|
|
1499
|
+
)
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1369
1502
|
testSync("rendering is deterministic and newline-terminated", () => {
|
|
1370
1503
|
let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
|
|
1371
1504
|
expect(rendered)->toBe(
|
|
@@ -1671,7 +1804,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
|
|
|
1671
1804
|
let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
|
|
1672
1805
|
expect(
|
|
1673
1806
|
manifest.capabilities->Array.every(entry =>
|
|
1674
|
-
entry.declaredBy->Array.every(site => site.field != "photoUrl")
|
|
1807
|
+
entry.declaredBy->Array.every(site => site.field != Some("photoUrl"))
|
|
1675
1808
|
),
|
|
1676
1809
|
)->toBe(true)
|
|
1677
1810
|
})
|
|
@@ -1856,3 +1989,54 @@ describe("the subscriber's half of the table", () => {
|
|
|
1856
1989
|
)->toBe(1)
|
|
1857
1990
|
)
|
|
1858
1991
|
})
|
|
1992
|
+
|
|
1993
|
+
// ── One command name, one handler ────────────────────────────────────────────
|
|
1994
|
+
//
|
|
1995
|
+
// A DCB plugin routes a command by its bare type name, so two slices declaring
|
|
1996
|
+
// one name have one handler between them. The runtime notices only when it goes
|
|
1997
|
+
// to stamp owner fields — by which point the deploy has succeeded and the losing
|
|
1998
|
+
// slice is silently unreachable — so the structure refuses it instead.
|
|
1999
|
+
|
|
2000
|
+
describe("duplicate DCB command names", () => {
|
|
2001
|
+
// The same slice twice is the smallest possible collision, and it needs no
|
|
2002
|
+
// fixture that exists only to collide.
|
|
2003
|
+
testSync("two slices declaring one command are refused", () =>
|
|
2004
|
+
expect(
|
|
2005
|
+
switch Plugin_Structure.make(
|
|
2006
|
+
~name="TestPlugin",
|
|
2007
|
+
~stateChangeSlices=[module(PsShipOrderSlice), module(PsShipOrderSlice)],
|
|
2008
|
+
) {
|
|
2009
|
+
| _ => false
|
|
2010
|
+
| exception _ => true
|
|
2011
|
+
},
|
|
2012
|
+
)->toBe(true)
|
|
2013
|
+
)
|
|
2014
|
+
|
|
2015
|
+
testSync("the refusal names the command and both slices", () =>
|
|
2016
|
+
switch Plugin_Structure.make(
|
|
2017
|
+
~name="TestPlugin",
|
|
2018
|
+
~stateChangeSlices=[module(PsShipOrderSlice), module(PsShipOrderSlice)],
|
|
2019
|
+
) {
|
|
2020
|
+
| _ => expect("no refusal")->toBe("a refusal naming the command")
|
|
2021
|
+
| exception JsExn(e) =>
|
|
2022
|
+
let message = e->JsExn.message->Option.getOr("")
|
|
2023
|
+
expect((
|
|
2024
|
+
message->String.includes("ShipOrder"),
|
|
2025
|
+
message->String.includes("routes a command by its bare name"),
|
|
2026
|
+
))->toEqual((true, true))
|
|
2027
|
+
}
|
|
2028
|
+
)
|
|
2029
|
+
|
|
2030
|
+
// Distinct names are the ordinary case and must stay silent.
|
|
2031
|
+
testSync("distinct command names are fine", () =>
|
|
2032
|
+
expect(
|
|
2033
|
+
switch Plugin_Structure.make(
|
|
2034
|
+
~name="TestPlugin",
|
|
2035
|
+
~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
|
|
2036
|
+
) {
|
|
2037
|
+
| _ => true
|
|
2038
|
+
| exception _ => false
|
|
2039
|
+
},
|
|
2040
|
+
)->toBe(true)
|
|
2041
|
+
)
|
|
2042
|
+
})
|