@reventlessdev/reventless-local 3.0.0-alpha.146 → 3.0.0-alpha.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +5 -5
- package/src/Platform.res +109 -74
- package/src/Platform.res.mjs +251 -227
- package/src/adapter/LocalSideEffectHandler.res +1 -0
- package/src/adapter/LocalSideEffectHandler.res.mjs +1 -1
- package/tests/components/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +1 -1
- package/tests/plugin/ApiFragmentRegistryBehavior_GWT.res +131 -0
- package/tests/plugin/{ApiFragmentRegistry_GWT.res.mjs → ApiFragmentRegistryBehavior_GWT.res.mjs} +114 -42
- package/tests/plugin/{ApiFragments_GWT.res → ApiFragmentsProjection_GWT.res} +16 -11
- package/tests/plugin/{ApiFragments_GWT.res.mjs → ApiFragmentsProjection_GWT.res.mjs} +15 -13
- package/tests/plugin/ApiFragmentRegistry_GWT.res +0 -103
|
@@ -16,6 +16,7 @@ let make = (
|
|
|
16
16
|
~resourceNaming as _,
|
|
17
17
|
~memorySize as _=?,
|
|
18
18
|
~timeout as _=?,
|
|
19
|
+
~extraEnvVars as _=?,
|
|
19
20
|
~opts=?,
|
|
20
21
|
): ReventlessCore.SideEffectHandler.component => {
|
|
21
22
|
let noopEnqueueEvent: ReventlessInfra.EventCollector.enqueueEvent = async (_, _, _) => ()
|
|
@@ -5,7 +5,7 @@ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/co
|
|
|
5
5
|
import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/src/ComponentType.res.mjs";
|
|
6
6
|
import * as SideEffectHandler$ReventlessCore from "@reventlessdev/reventless-core/src/components/SideEffectHandler/SideEffectHandler.res.mjs";
|
|
7
7
|
|
|
8
|
-
function make(name, param, param$1, param$2, param$3, param$4, scheduler, param$5, param$6, param$7, opts) {
|
|
8
|
+
function make(name, param, param$1, param$2, param$3, param$4, scheduler, param$5, param$6, param$7, param$8, opts) {
|
|
9
9
|
let noopEnqueueEvent = async (param, param$1, param$2) => {};
|
|
10
10
|
let ops_createSchedule = async schedule => await scheduler.createSchedule([], schedule);
|
|
11
11
|
let ops_deleteSchedule = async scheduleName => await scheduler.deleteSchedule([], scheduleName);
|
|
@@ -99,7 +99,7 @@ let sideEffects = [{
|
|
|
99
99
|
execute: execute
|
|
100
100
|
}];
|
|
101
101
|
|
|
102
|
-
let seh = SEHBuilder.make("TestSEH", sideEffects, allEventTopics, Pulumi.output({}), undefined, TestFixtures$ReventlessLocal.mockQueryEngine, TestFixtures$ReventlessLocal.mockScheduler, TestFixtures$ReventlessLocal.mockResourceNaming, undefined, undefined, undefined);
|
|
102
|
+
let seh = SEHBuilder.make("TestSEH", sideEffects, allEventTopics, Pulumi.output({}), undefined, TestFixtures$ReventlessLocal.mockQueryEngine, TestFixtures$ReventlessLocal.mockScheduler, TestFixtures$ReventlessLocal.mockResourceNaming, undefined, undefined, undefined, undefined);
|
|
103
103
|
|
|
104
104
|
let testMeta = TestFixtures$ReventlessLocal.makeTestMeta("TestSEHSource");
|
|
105
105
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Behavior GWT for the ApiFragmentRegistry SINGLETON AGGREGATE (the platform API-schema
|
|
2
|
+
// fragment registry — event-sourced-fragment-registries plan). Unlike the retired slice, the
|
|
3
|
+
// aggregate emits a SECOND event ApiSchemaComputed{snapshot} alongside each ApiFragment* fact,
|
|
4
|
+
// carrying the whole consistent per-plugin fragment set after the change (the reactive
|
|
5
|
+
// SideEffect's trigger). RecordApiFragmentPush changes no fragments, so it emits NO snapshot.
|
|
6
|
+
open ReventlessCore
|
|
7
|
+
module P = Reventless.Plugin // P.Domain / P.Platform — avoid shadowing apiSchemaFragment's labels
|
|
8
|
+
|
|
9
|
+
module Test = ReventlessGwt.Behavior_GWT.MakeFromAggregate(ApiFragmentRegistrySpec, ApiFragmentRegistryBehavior)
|
|
10
|
+
open Test
|
|
11
|
+
open ApiFragmentRegistrySpec
|
|
12
|
+
|
|
13
|
+
let fragment1: Reventless.Plugin.apiSchemaFragment = {
|
|
14
|
+
encoded: `{"types":["type Catalog_Product { id: ID! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
15
|
+
protocol: "graphql",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let fragment2: Reventless.Plugin.apiSchemaFragment = {
|
|
19
|
+
encoded: `{"types":["type Catalog_Product { id: ID!\\n name: String! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
20
|
+
protocol: "graphql",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A snapshot entry mirrors the behaviour's `snapshotOf` fold (pluginId + the fragment's
|
|
24
|
+
// encoded/protocol + its target).
|
|
25
|
+
let entry = (~pluginId, ~fragment: Reventless.Plugin.apiSchemaFragment, ~apiTarget): fragmentSnapshotEntry => {
|
|
26
|
+
pluginId,
|
|
27
|
+
encoded: fragment.encoded,
|
|
28
|
+
protocol: fragment.protocol,
|
|
29
|
+
apiTarget,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("ApiFragmentRegistry aggregate", () => {
|
|
33
|
+
test("register on empty state emits ApiFragmentRegistered + ApiSchemaComputed", () =>
|
|
34
|
+
givenEvents([])
|
|
35
|
+
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}))
|
|
36
|
+
->thenEvents([
|
|
37
|
+
ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}),
|
|
38
|
+
ApiSchemaComputed({snapshot: [entry(~pluginId="p1", ~fragment=fragment1, ~apiTarget=P.Domain)]}),
|
|
39
|
+
])
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
test("re-registering an identical fragment + target is idempotent (no event)", () =>
|
|
43
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
44
|
+
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t1"}))
|
|
45
|
+
->thenNoEvent
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
test("registering a changed fragment emits ApiFragmentUpdated + ApiSchemaComputed", () =>
|
|
49
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
50
|
+
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment2, apiTarget: P.Domain, at: "t1"}))
|
|
51
|
+
->thenEvents([
|
|
52
|
+
ApiFragmentUpdated({
|
|
53
|
+
pluginId: "p1",
|
|
54
|
+
previousFragment: fragment1,
|
|
55
|
+
newFragment: fragment2,
|
|
56
|
+
apiTarget: P.Domain,
|
|
57
|
+
at: "t1",
|
|
58
|
+
}),
|
|
59
|
+
ApiSchemaComputed({snapshot: [entry(~pluginId="p1", ~fragment=fragment2, ~apiTarget=P.Domain)]}),
|
|
60
|
+
])
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
test("retargeting a plugin with an identical fragment emits ApiFragmentUpdated (fields move APIs)", () =>
|
|
64
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
65
|
+
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: P.Platform, at: "t1"}))
|
|
66
|
+
->thenEvents([
|
|
67
|
+
ApiFragmentUpdated({
|
|
68
|
+
pluginId: "p1",
|
|
69
|
+
previousFragment: fragment1,
|
|
70
|
+
newFragment: fragment1,
|
|
71
|
+
apiTarget: P.Platform,
|
|
72
|
+
at: "t1",
|
|
73
|
+
}),
|
|
74
|
+
ApiSchemaComputed({snapshot: [entry(~pluginId="p1", ~fragment=fragment1, ~apiTarget=P.Platform)]}),
|
|
75
|
+
])
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
test("registering a second plugin carries BOTH plugins in the snapshot (consistent whole-registry fold)", () =>
|
|
79
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
80
|
+
->whenCmd(RegisterApiFragment({pluginId: "p2", fragment: fragment2, apiTarget: P.Platform, at: "t1"}))
|
|
81
|
+
->thenEvents([
|
|
82
|
+
ApiFragmentRegistered({pluginId: "p2", fragment: fragment2, apiTarget: P.Platform, at: "t1"}),
|
|
83
|
+
ApiSchemaComputed({
|
|
84
|
+
snapshot: [
|
|
85
|
+
entry(~pluginId="p1", ~fragment=fragment1, ~apiTarget=P.Domain),
|
|
86
|
+
entry(~pluginId="p2", ~fragment=fragment2, ~apiTarget=P.Platform),
|
|
87
|
+
],
|
|
88
|
+
}),
|
|
89
|
+
])
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
test("deregister emits ApiFragmentDeregistered + an empty-registry ApiSchemaComputed", () =>
|
|
93
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
94
|
+
->whenCmd(DeregisterApiFragment({pluginId: "p1"}))
|
|
95
|
+
->thenEvents([ApiFragmentDeregistered({pluginId: "p1"}), ApiSchemaComputed({snapshot: []})])
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
test("deregistering an absent fragment is idempotent (no event)", () =>
|
|
99
|
+
givenEvents([])->whenCmd(DeregisterApiFragment({pluginId: "p1"}))->thenNoEvent
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
test("recording a push outcome emits ONLY ApiFragmentPushRecorded (no snapshot, no recompute loop)", () =>
|
|
103
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
104
|
+
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
105
|
+
->thenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
test("redelivering an identical push record is idempotent (no event)", () =>
|
|
109
|
+
givenEvents([
|
|
110
|
+
ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}),
|
|
111
|
+
ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}),
|
|
112
|
+
])
|
|
113
|
+
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
114
|
+
->thenNoEvent
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
test("a later distinct push outcome is recorded again", () =>
|
|
118
|
+
givenEvents([
|
|
119
|
+
ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}),
|
|
120
|
+
ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}),
|
|
121
|
+
])
|
|
122
|
+
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: false, message: "stitch failed", at: "t2"}))
|
|
123
|
+
->thenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: false, message: "stitch failed", at: "t2"}))
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
test("recording a push for an unregistered plugin is dropped (no event)", () =>
|
|
127
|
+
givenEvents([])
|
|
128
|
+
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
129
|
+
->thenNoEvent
|
|
130
|
+
)
|
|
131
|
+
})
|
package/tests/plugin/{ApiFragmentRegistry_GWT.res.mjs → ApiFragmentRegistryBehavior_GWT.res.mjs}
RENAMED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Behavior_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/Behavior_GWT.res.mjs";
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
4
|
+
import * as ApiFragmentRegistrySpec$ReventlessCore from "@reventlessdev/reventless-core/src/admin/ApiFragmentRegistry/ApiFragmentRegistrySpec.res.mjs";
|
|
5
|
+
import * as ApiFragmentRegistryBehavior$ReventlessCore from "@reventlessdev/reventless-core/src/admin/ApiFragmentRegistry/ApiFragmentRegistryBehavior.res.mjs";
|
|
6
6
|
|
|
7
|
-
let Test = Behavior_GWT$ReventlessGwt.
|
|
8
|
-
name:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
errorSchema:
|
|
12
|
-
eventSchema: ApiFragmentRegistry$ReventlessCore.eventSchema
|
|
7
|
+
let Test = Behavior_GWT$ReventlessGwt.MakeFromAggregate({
|
|
8
|
+
name: ApiFragmentRegistrySpec$ReventlessCore.name,
|
|
9
|
+
commandSchema: ApiFragmentRegistrySpec$ReventlessCore.commandSchema,
|
|
10
|
+
eventSchema: ApiFragmentRegistrySpec$ReventlessCore.eventSchema,
|
|
11
|
+
errorSchema: ApiFragmentRegistrySpec$ReventlessCore.errorSchema
|
|
13
12
|
})({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
Spec: {
|
|
14
|
+
name: ApiFragmentRegistrySpec$ReventlessCore.name,
|
|
15
|
+
commandSchema: ApiFragmentRegistrySpec$ReventlessCore.commandSchema,
|
|
16
|
+
eventSchema: ApiFragmentRegistrySpec$ReventlessCore.eventSchema,
|
|
17
|
+
errorSchema: ApiFragmentRegistrySpec$ReventlessCore.errorSchema
|
|
18
|
+
},
|
|
19
|
+
initialState: ApiFragmentRegistryBehavior$ReventlessCore.initialState,
|
|
20
|
+
evolve: ApiFragmentRegistryBehavior$ReventlessCore.evolve,
|
|
21
|
+
decide: ApiFragmentRegistryBehavior$ReventlessCore.decide,
|
|
22
|
+
snapshot: undefined,
|
|
23
|
+
moduleUrl: ApiFragmentRegistryBehavior$ReventlessCore.moduleUrl
|
|
17
24
|
});
|
|
18
25
|
|
|
19
26
|
let fragment1 = {
|
|
@@ -26,20 +33,35 @@ let fragment2 = {
|
|
|
26
33
|
protocol: "graphql"
|
|
27
34
|
};
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
function entry(pluginId, fragment, apiTarget) {
|
|
37
|
+
return {
|
|
38
|
+
pluginId: pluginId,
|
|
39
|
+
encoded: fragment.encoded,
|
|
40
|
+
protocol: fragment.protocol,
|
|
41
|
+
apiTarget: apiTarget
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Test.describe("ApiFragmentRegistry aggregate", () => {
|
|
46
|
+
Test.test("register on empty state emits ApiFragmentRegistered + ApiSchemaComputed", () => Test.thenEvents(Test.whenCmd(Test.givenEvents([]), {
|
|
31
47
|
TAG: "RegisterApiFragment",
|
|
32
48
|
pluginId: "p1",
|
|
33
49
|
fragment: fragment1,
|
|
34
50
|
apiTarget: "Domain",
|
|
35
51
|
at: "t0"
|
|
36
|
-
}),
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
}), [
|
|
53
|
+
{
|
|
54
|
+
TAG: "ApiFragmentRegistered",
|
|
55
|
+
pluginId: "p1",
|
|
56
|
+
fragment: fragment1,
|
|
57
|
+
apiTarget: "Domain",
|
|
58
|
+
at: "t0"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
TAG: "ApiSchemaComputed",
|
|
62
|
+
snapshot: [entry("p1", fragment1, "Domain")]
|
|
63
|
+
}
|
|
64
|
+
]));
|
|
43
65
|
Test.test("re-registering an identical fragment + target is idempotent (no event)", () => Test.thenNoEvent(Test.whenCmd(Test.givenEvents([{
|
|
44
66
|
TAG: "ApiFragmentRegistered",
|
|
45
67
|
pluginId: "p1",
|
|
@@ -53,7 +75,7 @@ Test.describe("ApiFragmentRegistry StateChangeSlice", () => {
|
|
|
53
75
|
apiTarget: "Domain",
|
|
54
76
|
at: "t1"
|
|
55
77
|
})));
|
|
56
|
-
Test.test("registering a changed fragment emits ApiFragmentUpdated
|
|
78
|
+
Test.test("registering a changed fragment emits ApiFragmentUpdated + ApiSchemaComputed", () => Test.thenEvents(Test.whenCmd(Test.givenEvents([{
|
|
57
79
|
TAG: "ApiFragmentRegistered",
|
|
58
80
|
pluginId: "p1",
|
|
59
81
|
fragment: fragment1,
|
|
@@ -65,15 +87,21 @@ Test.describe("ApiFragmentRegistry StateChangeSlice", () => {
|
|
|
65
87
|
fragment: fragment2,
|
|
66
88
|
apiTarget: "Domain",
|
|
67
89
|
at: "t1"
|
|
68
|
-
}),
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
90
|
+
}), [
|
|
91
|
+
{
|
|
92
|
+
TAG: "ApiFragmentUpdated",
|
|
93
|
+
pluginId: "p1",
|
|
94
|
+
previousFragment: fragment1,
|
|
95
|
+
newFragment: fragment2,
|
|
96
|
+
apiTarget: "Domain",
|
|
97
|
+
at: "t1"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
TAG: "ApiSchemaComputed",
|
|
101
|
+
snapshot: [entry("p1", fragment2, "Domain")]
|
|
102
|
+
}
|
|
103
|
+
]));
|
|
104
|
+
Test.test("retargeting a plugin with an identical fragment emits ApiFragmentUpdated (fields move APIs)", () => Test.thenEvents(Test.whenCmd(Test.givenEvents([{
|
|
77
105
|
TAG: "ApiFragmentRegistered",
|
|
78
106
|
pluginId: "p1",
|
|
79
107
|
fragment: fragment1,
|
|
@@ -85,15 +113,49 @@ Test.describe("ApiFragmentRegistry StateChangeSlice", () => {
|
|
|
85
113
|
fragment: fragment1,
|
|
86
114
|
apiTarget: "Platform",
|
|
87
115
|
at: "t1"
|
|
88
|
-
}),
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
}), [
|
|
117
|
+
{
|
|
118
|
+
TAG: "ApiFragmentUpdated",
|
|
119
|
+
pluginId: "p1",
|
|
120
|
+
previousFragment: fragment1,
|
|
121
|
+
newFragment: fragment1,
|
|
122
|
+
apiTarget: "Platform",
|
|
123
|
+
at: "t1"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
TAG: "ApiSchemaComputed",
|
|
127
|
+
snapshot: [entry("p1", fragment1, "Platform")]
|
|
128
|
+
}
|
|
129
|
+
]));
|
|
130
|
+
Test.test("registering a second plugin carries BOTH plugins in the snapshot (consistent whole-registry fold)", () => Test.thenEvents(Test.whenCmd(Test.givenEvents([{
|
|
131
|
+
TAG: "ApiFragmentRegistered",
|
|
132
|
+
pluginId: "p1",
|
|
133
|
+
fragment: fragment1,
|
|
134
|
+
apiTarget: "Domain",
|
|
135
|
+
at: "t0"
|
|
136
|
+
}]), {
|
|
137
|
+
TAG: "RegisterApiFragment",
|
|
138
|
+
pluginId: "p2",
|
|
139
|
+
fragment: fragment2,
|
|
93
140
|
apiTarget: "Platform",
|
|
94
141
|
at: "t1"
|
|
95
|
-
})
|
|
96
|
-
|
|
142
|
+
}), [
|
|
143
|
+
{
|
|
144
|
+
TAG: "ApiFragmentRegistered",
|
|
145
|
+
pluginId: "p2",
|
|
146
|
+
fragment: fragment2,
|
|
147
|
+
apiTarget: "Platform",
|
|
148
|
+
at: "t1"
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
TAG: "ApiSchemaComputed",
|
|
152
|
+
snapshot: [
|
|
153
|
+
entry("p1", fragment1, "Domain"),
|
|
154
|
+
entry("p2", fragment2, "Platform")
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
]));
|
|
158
|
+
Test.test("deregister emits ApiFragmentDeregistered + an empty-registry ApiSchemaComputed", () => Test.thenEvents(Test.whenCmd(Test.givenEvents([{
|
|
97
159
|
TAG: "ApiFragmentRegistered",
|
|
98
160
|
pluginId: "p1",
|
|
99
161
|
fragment: fragment1,
|
|
@@ -102,15 +164,21 @@ Test.describe("ApiFragmentRegistry StateChangeSlice", () => {
|
|
|
102
164
|
}]), {
|
|
103
165
|
TAG: "DeregisterApiFragment",
|
|
104
166
|
pluginId: "p1"
|
|
105
|
-
}),
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
167
|
+
}), [
|
|
168
|
+
{
|
|
169
|
+
TAG: "ApiFragmentDeregistered",
|
|
170
|
+
pluginId: "p1"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
TAG: "ApiSchemaComputed",
|
|
174
|
+
snapshot: []
|
|
175
|
+
}
|
|
176
|
+
]));
|
|
109
177
|
Test.test("deregistering an absent fragment is idempotent (no event)", () => Test.thenNoEvent(Test.whenCmd(Test.givenEvents([]), {
|
|
110
178
|
TAG: "DeregisterApiFragment",
|
|
111
179
|
pluginId: "p1"
|
|
112
180
|
})));
|
|
113
|
-
Test.test("recording a push outcome emits ApiFragmentPushRecorded", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([{
|
|
181
|
+
Test.test("recording a push outcome emits ONLY ApiFragmentPushRecorded (no snapshot, no recompute loop)", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([{
|
|
114
182
|
TAG: "ApiFragmentRegistered",
|
|
115
183
|
pluginId: "p1",
|
|
116
184
|
fragment: fragment1,
|
|
@@ -188,9 +256,13 @@ Test.describe("ApiFragmentRegistry StateChangeSlice", () => {
|
|
|
188
256
|
})));
|
|
189
257
|
});
|
|
190
258
|
|
|
259
|
+
let P;
|
|
260
|
+
|
|
191
261
|
export {
|
|
262
|
+
P,
|
|
192
263
|
Test,
|
|
193
264
|
fragment1,
|
|
194
265
|
fragment2,
|
|
266
|
+
entry,
|
|
195
267
|
}
|
|
196
268
|
/* Test Not a pure module */
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Projection GWT for the ApiFragments read model — one row per plugin name holding the
|
|
2
|
+
// current fragment + push status, projected off the ApiFragmentRegistry aggregate's events
|
|
3
|
+
// (event-sourced-fragment-registries plan). Keyed by the payload pluginId (not the singleton
|
|
4
|
+
// aggregate id). ApiSchemaComputed is the reactive-push trigger only — it must NOT touch a row.
|
|
4
5
|
open ReventlessCore
|
|
5
6
|
module P = Reventless.Plugin // P.Domain / P.Platform — avoid opening apiSchemaFragment's labels
|
|
6
7
|
|
|
7
|
-
module Test = ReventlessGwt.
|
|
8
|
+
module Test = ReventlessGwt.MultiSourceProjection_GWT.Make(ApiFragmentsProjection.ApiFragmentsMapping)
|
|
8
9
|
open Test
|
|
9
|
-
open
|
|
10
|
+
open ApiFragmentRegistrySpec
|
|
10
11
|
|
|
11
12
|
let fragment1: Reventless.Plugin.apiSchemaFragment = {
|
|
12
13
|
encoded: `{"types":["type Catalog_Product { id: ID! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
@@ -18,7 +19,7 @@ let fragment2: Reventless.Plugin.apiSchemaFragment = {
|
|
|
18
19
|
protocol: "graphql",
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
let registeredState:
|
|
22
|
+
let registeredState: ApiFragmentsReadModelSpec.state = {
|
|
22
23
|
pluginId: "p1",
|
|
23
24
|
encoded: fragment1.encoded,
|
|
24
25
|
protocol: fragment1.protocol,
|
|
@@ -30,13 +31,19 @@ let registeredState: ApiFragments.state = {
|
|
|
30
31
|
pushedAt: "",
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
describe("ApiFragments
|
|
34
|
-
test("ApiFragmentRegistered creates the row pending (registeredAt = updatedAt = event
|
|
34
|
+
describe("ApiFragments projection", () => {
|
|
35
|
+
test("ApiFragmentRegistered creates the row pending (registeredAt = updatedAt = event `at`)", () =>
|
|
35
36
|
givenEvents([])
|
|
36
37
|
->whenEvent(ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}))
|
|
37
38
|
->thenStateWithId("p1", registeredState)
|
|
38
39
|
)
|
|
39
40
|
|
|
41
|
+
test("ApiSchemaComputed is ignored (no row change)", () =>
|
|
42
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
43
|
+
->whenEvent(ApiSchemaComputed({snapshot: []}))
|
|
44
|
+
->thenStateWithId("p1", registeredState)
|
|
45
|
+
)
|
|
46
|
+
|
|
40
47
|
test("ApiFragmentPushRecorded(ok) marks the row ok with the push time", () =>
|
|
41
48
|
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
42
49
|
->whenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
@@ -45,9 +52,7 @@ describe("ApiFragments StateViewSlice projection", () => {
|
|
|
45
52
|
|
|
46
53
|
test("ApiFragmentPushRecorded(error) carries the message", () =>
|
|
47
54
|
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
48
|
-
->whenEvent(
|
|
49
|
-
ApiFragmentPushRecorded({pluginId: "p1", ok: false, message: "stitch failed", at: "t1"}),
|
|
50
|
-
)
|
|
55
|
+
->whenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: false, message: "stitch failed", at: "t1"}))
|
|
51
56
|
->thenStateWithId(
|
|
52
57
|
"p1",
|
|
53
58
|
{...registeredState, pushStatus: "error", pushMessage: "stitch failed", pushedAt: "t1"},
|
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as
|
|
4
|
-
import * as
|
|
5
|
-
import * as ApiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/ApiFragmentRegistry/StateViewSlice/ApiFragments_Projection.res.mjs";
|
|
3
|
+
import * as ApiFragmentsProjection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/ApiFragmentRegistry/ApiFragmentsProjection.res.mjs";
|
|
4
|
+
import * as MultiSourceProjection_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/MultiSourceProjection_GWT.res.mjs";
|
|
6
5
|
|
|
7
|
-
let Test =
|
|
8
|
-
name: ApiFragments$ReventlessCore.name,
|
|
9
|
-
stateSchema: ApiFragments$ReventlessCore.stateSchema,
|
|
10
|
-
consumedEventSchema: ApiFragments$ReventlessCore.consumedEventSchema,
|
|
11
|
-
subIdConfig: undefined
|
|
12
|
-
})({
|
|
13
|
-
project: ApiFragments_Projection$ReventlessCore.project
|
|
14
|
-
});
|
|
6
|
+
let Test = MultiSourceProjection_GWT$ReventlessGwt.Make(ApiFragmentsProjection$ReventlessCore.ApiFragmentsMapping);
|
|
15
7
|
|
|
16
8
|
let fragment1 = {
|
|
17
9
|
encoded: `{"types":["type Catalog_Product { id: ID! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
@@ -35,14 +27,24 @@ let registeredState = {
|
|
|
35
27
|
pushedAt: ""
|
|
36
28
|
};
|
|
37
29
|
|
|
38
|
-
Test.describe("ApiFragments
|
|
39
|
-
Test.test("ApiFragmentRegistered creates the row pending (registeredAt = updatedAt = event
|
|
30
|
+
Test.describe("ApiFragments projection", () => {
|
|
31
|
+
Test.test("ApiFragmentRegistered creates the row pending (registeredAt = updatedAt = event `at`)", undefined, () => Test.thenStateWithId(Test.whenEvent(Test.givenEvents([]), {
|
|
40
32
|
TAG: "ApiFragmentRegistered",
|
|
41
33
|
pluginId: "p1",
|
|
42
34
|
fragment: fragment1,
|
|
43
35
|
apiTarget: "Domain",
|
|
44
36
|
at: "t0"
|
|
45
37
|
}), "p1", registeredState));
|
|
38
|
+
Test.test("ApiSchemaComputed is ignored (no row change)", undefined, () => Test.thenStateWithId(Test.whenEvent(Test.givenEvents([{
|
|
39
|
+
TAG: "ApiFragmentRegistered",
|
|
40
|
+
pluginId: "p1",
|
|
41
|
+
fragment: fragment1,
|
|
42
|
+
apiTarget: "Domain",
|
|
43
|
+
at: "t0"
|
|
44
|
+
}]), {
|
|
45
|
+
TAG: "ApiSchemaComputed",
|
|
46
|
+
snapshot: []
|
|
47
|
+
}), "p1", registeredState));
|
|
46
48
|
Test.test("ApiFragmentPushRecorded(ok) marks the row ok with the push time", undefined, () => Test.thenStateWithId(Test.whenEvent(Test.givenEvents([{
|
|
47
49
|
TAG: "ApiFragmentRegistered",
|
|
48
50
|
pluginId: "p1",
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// StateChangeSlice GWT for the ApiFragmentRegistry write side (the platform API-schema
|
|
2
|
-
// fragment registry replacing the deploy-schema:* keyspace —
|
|
3
|
-
// event-sourced-fragment-registries plan).
|
|
4
|
-
open ReventlessCore
|
|
5
|
-
open Reventless.Plugin // Domain / Platform constructors of apiTarget
|
|
6
|
-
|
|
7
|
-
module Test = ReventlessGwt.Behavior_GWT.Make(ApiFragmentRegistry, ApiFragmentRegistry_Behavior)
|
|
8
|
-
open Test
|
|
9
|
-
open ApiFragmentRegistry
|
|
10
|
-
|
|
11
|
-
let fragment1: Reventless.Plugin.apiSchemaFragment = {
|
|
12
|
-
encoded: `{"types":["type Catalog_Product { id: ID! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
13
|
-
protocol: "graphql",
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let fragment2: Reventless.Plugin.apiSchemaFragment = {
|
|
17
|
-
encoded: `{"types":["type Catalog_Product { id: ID!\\n name: String! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
18
|
-
protocol: "graphql",
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("ApiFragmentRegistry StateChangeSlice", () => {
|
|
22
|
-
test("register on empty state emits ApiFragmentRegistered", () =>
|
|
23
|
-
givenEvents([])
|
|
24
|
-
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"}))
|
|
25
|
-
->thenEvent(ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"}))
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
test("re-registering an identical fragment + target is idempotent (no event)", () =>
|
|
29
|
-
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"})])
|
|
30
|
-
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t1"}))
|
|
31
|
-
->thenNoEvent
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
test("registering a changed fragment emits ApiFragmentUpdated (version supersession)", () =>
|
|
35
|
-
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"})])
|
|
36
|
-
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment2, apiTarget: Domain, at: "t1"}))
|
|
37
|
-
->thenEvent(
|
|
38
|
-
ApiFragmentUpdated({
|
|
39
|
-
pluginId: "p1",
|
|
40
|
-
previousFragment: fragment1,
|
|
41
|
-
newFragment: fragment2,
|
|
42
|
-
apiTarget: Domain,
|
|
43
|
-
at: "t1",
|
|
44
|
-
}),
|
|
45
|
-
)
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
test("retargeting a plugin with an identical fragment emits ApiFragmentUpdated", () =>
|
|
49
|
-
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"})])
|
|
50
|
-
->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: Platform, at: "t1"}))
|
|
51
|
-
->thenEvent(
|
|
52
|
-
ApiFragmentUpdated({
|
|
53
|
-
pluginId: "p1",
|
|
54
|
-
previousFragment: fragment1,
|
|
55
|
-
newFragment: fragment1,
|
|
56
|
-
apiTarget: Platform,
|
|
57
|
-
at: "t1",
|
|
58
|
-
}),
|
|
59
|
-
)
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
test("deregister emits ApiFragmentDeregistered", () =>
|
|
63
|
-
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"})])
|
|
64
|
-
->whenCmd(DeregisterApiFragment({pluginId: "p1"}))
|
|
65
|
-
->thenEvent(ApiFragmentDeregistered({pluginId: "p1"}))
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
test("deregistering an absent fragment is idempotent (no event)", () =>
|
|
69
|
-
givenEvents([])
|
|
70
|
-
->whenCmd(DeregisterApiFragment({pluginId: "p1"}))
|
|
71
|
-
->thenNoEvent
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
test("recording a push outcome emits ApiFragmentPushRecorded", () =>
|
|
75
|
-
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"})])
|
|
76
|
-
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
77
|
-
->thenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
test("redelivering an identical push record is idempotent (no event)", () =>
|
|
81
|
-
givenEvents([
|
|
82
|
-
ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"}),
|
|
83
|
-
ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}),
|
|
84
|
-
])
|
|
85
|
-
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
86
|
-
->thenNoEvent
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
test("a later distinct push outcome is recorded again", () =>
|
|
90
|
-
givenEvents([
|
|
91
|
-
ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: Domain, at: "t0"}),
|
|
92
|
-
ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}),
|
|
93
|
-
])
|
|
94
|
-
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: false, message: "stitch failed", at: "t2"}))
|
|
95
|
-
->thenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: false, message: "stitch failed", at: "t2"}))
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
test("recording a push for an unregistered plugin is dropped (no event)", () =>
|
|
99
|
-
givenEvents([])
|
|
100
|
-
->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
101
|
-
->thenNoEvent
|
|
102
|
-
)
|
|
103
|
-
})
|