@reventlessdev/reventless-local 3.0.0-alpha.142 → 3.0.0-alpha.144
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 +22 -0
- package/package.json +9 -9
- package/src/Platform.res +252 -110
- package/src/Platform.res.mjs +522 -166
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res +3 -3
- package/src/components/Aggregate_Builder.res +1 -0
- package/src/components/Aggregate_Builder.res.mjs +5 -0
- package/tests/PluginEventDecodeTest.res +47 -0
- package/tests/PluginEventDecodeTest.res.mjs +33 -0
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +6 -7
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res.mjs +2 -3
- package/tests/plugin/ApiFragmentRegistry_GWT.res +103 -0
- package/tests/plugin/ApiFragmentRegistry_GWT.res.mjs +196 -0
- package/tests/plugin/ApiFragments_GWT.res +103 -0
- package/tests/plugin/ApiFragments_GWT.res.mjs +170 -0
- package/tests/plugin/PluginBehavior_GWT.res +2 -18
- package/tests/plugin/PluginBehavior_GWT.res.mjs +0 -37
- package/tests/plugin/Plugin_Fixtures.res +0 -7
- package/tests/plugin/Plugin_Fixtures.res.mjs +0 -21
- package/tests/plugin/PluginsProjection_GWT.res +0 -12
- package/tests/plugin/PluginsProjection_GWT.res.mjs +0 -19
- package/tests/plugin/UiFragmentRegistry_GWT.res +53 -0
- package/tests/plugin/UiFragmentRegistry_GWT.res.mjs +92 -0
- package/tests/plugin/UiFragments_GWT.res +55 -0
- package/tests/plugin/UiFragments_GWT.res.mjs +84 -0
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res +0 -72
- package/tests/plugin/UIFragmentRegistryProjection_GWT.res.mjs +0 -117
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// In-process wiring of the local event bus into the promoted, transport-neutral
|
|
3
3
|
// GraphQL subscription bridge (`ReventlessGraphqlServer.GraphQL_SubscriptionBridge`).
|
|
4
4
|
//
|
|
5
|
-
// The generic pieces — topic naming, the field-resolver builder,
|
|
6
|
-
//
|
|
5
|
+
// The generic pieces — topic naming, the field-resolver builder, and
|
|
6
|
+
// `registerAll` — live in core-adjacent
|
|
7
7
|
// `GraphQL_SubscriptionBridge`, parameterized over a PubSub instance. This
|
|
8
8
|
// module supplies the in-process `GraphqlYoga.createPubSub()` singleton and the
|
|
9
9
|
// Bus→PubSub glue that is specific to the local in-process backend:
|
|
@@ -101,7 +101,7 @@ type subscriptionEntry = Bridge.subscriptionEntry = {
|
|
|
101
101
|
* Bridge must already be wired via bridgeSourceB before calling this.
|
|
102
102
|
*
|
|
103
103
|
* `sdlFields`: the `subscriptions` SDL field strings from the plugin fragment
|
|
104
|
-
* (
|
|
104
|
+
* (provider-neutral — registered as-is).
|
|
105
105
|
*/
|
|
106
106
|
let registerAll = (
|
|
107
107
|
~server: ReventlessGraphqlServer.GraphQL_ServerInstance.t,
|
|
@@ -30,6 +30,10 @@ let noHooks_apiRole = {
|
|
|
30
30
|
contents: undefined
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
let noHooks_adminApi = {
|
|
34
|
+
contents: undefined
|
|
35
|
+
};
|
|
36
|
+
|
|
33
37
|
let noHooks_deployTarget = {
|
|
34
38
|
contents: "Domain"
|
|
35
39
|
};
|
|
@@ -40,6 +44,7 @@ let noHooks = {
|
|
|
40
44
|
schedulerRoleUrn: noHooks_schedulerRoleUrn,
|
|
41
45
|
api: noHooks_api,
|
|
42
46
|
apiRole: noHooks_apiRole,
|
|
47
|
+
adminApi: noHooks_adminApi,
|
|
43
48
|
deployTarget: noHooks_deployTarget
|
|
44
49
|
};
|
|
45
50
|
|
|
@@ -49,3 +49,50 @@ describe("Platform.decodePluginEventEnvelope", () => {
|
|
|
49
49
|
)->toEqual(None)
|
|
50
50
|
})
|
|
51
51
|
})
|
|
52
|
+
|
|
53
|
+
// The onUIFragmentChange Source-C emission decodes UiFragmentRegistry slice
|
|
54
|
+
// events from the admin DcbEventLog's published envelope — the exact shape
|
|
55
|
+
// DcbEventLog_Operations produces: composeEventJson'(entityId, meta,
|
|
56
|
+
// combineMessage(eventType, data)).
|
|
57
|
+
describe("Platform.decodeUiFragmentRegistryEventEnvelope", () => {
|
|
58
|
+
let dcbMeta = ReventlessCore.Message.generateMeta(~service="AdminDcbEventLog")
|
|
59
|
+
|
|
60
|
+
let dcbEnvelopeOf = (event: ReventlessCore.UiFragmentRegistry.event) => {
|
|
61
|
+
let json = event->S.reverseConvertToJsonOrThrow(ReventlessCore.UiFragmentRegistry.eventSchema)
|
|
62
|
+
let (eventType, data) = json->ReventlessCore.Message.splitMessage
|
|
63
|
+
ReventlessCore.Message.composeEventJson'(
|
|
64
|
+
"Catalog",
|
|
65
|
+
dcbMeta,
|
|
66
|
+
ReventlessCore.Message.combineMessage(eventType, data),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
testSync("decodes a UiFragmentRegistered event from the published envelope", () => {
|
|
71
|
+
let event = ReventlessCore.UiFragmentRegistry.UiFragmentRegistered({
|
|
72
|
+
pluginId: "Catalog",
|
|
73
|
+
manifest: Plugin_Fixtures.uiManifest,
|
|
74
|
+
at: "t0",
|
|
75
|
+
})
|
|
76
|
+
expect(Platform.decodeUiFragmentRegistryEventEnvelope(dcbEnvelopeOf(event)))->toEqual(
|
|
77
|
+
Some(event),
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
testSync("decodes a UiFragmentDeregistered event", () => {
|
|
82
|
+
let event = ReventlessCore.UiFragmentRegistry.UiFragmentDeregistered({pluginId: "Catalog"})
|
|
83
|
+
expect(Platform.decodeUiFragmentRegistryEventEnvelope(dcbEnvelopeOf(event)))->toEqual(
|
|
84
|
+
Some(event),
|
|
85
|
+
)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
testSync("returns None for malformed input", () => {
|
|
89
|
+
expect(
|
|
90
|
+
Platform.decodeUiFragmentRegistryEventEnvelope(JSON.Encode.string("junk")),
|
|
91
|
+
)->toEqual(None)
|
|
92
|
+
expect(
|
|
93
|
+
Platform.decodeUiFragmentRegistryEventEnvelope(
|
|
94
|
+
JSON.Encode.object(Dict.fromArray([("event", JSON.Encode.object(Dict.make()))])),
|
|
95
|
+
),
|
|
96
|
+
)->toEqual(None)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -6,6 +6,7 @@ import * as Platform$ReventlessLocal from "../src/Platform.res.mjs";
|
|
|
6
6
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
7
7
|
import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
|
|
8
8
|
import * as Plugin_Fixtures$ReventlessLocal from "./plugin/Plugin_Fixtures.res.mjs";
|
|
9
|
+
import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs";
|
|
9
10
|
|
|
10
11
|
TestRunner$ReventlessLocal.setup();
|
|
11
12
|
|
|
@@ -56,6 +57,38 @@ globalThis.describe("Platform.decodePluginEventEnvelope", () => {
|
|
|
56
57
|
});
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
globalThis.describe("Platform.decodeUiFragmentRegistryEventEnvelope", () => {
|
|
61
|
+
let dcbMeta = Message$ReventlessCore.generateMeta("AdminDcbEventLog", undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
62
|
+
let dcbEnvelopeOf = event => {
|
|
63
|
+
let json = S.reverseConvertToJsonOrThrow(event, UiFragmentRegistry$ReventlessCore.eventSchema);
|
|
64
|
+
let match = Message$ReventlessCore.splitMessage(json);
|
|
65
|
+
return Message$ReventlessCore.composeEventJson$p("Catalog", dcbMeta, Message$ReventlessCore.combineMessage(match[0], match[1]));
|
|
66
|
+
};
|
|
67
|
+
globalThis.test("decodes a UiFragmentRegistered event from the published envelope", () => {
|
|
68
|
+
let event = {
|
|
69
|
+
TAG: "UiFragmentRegistered",
|
|
70
|
+
pluginId: "Catalog",
|
|
71
|
+
manifest: Plugin_Fixtures$ReventlessLocal.uiManifest,
|
|
72
|
+
at: "t0"
|
|
73
|
+
};
|
|
74
|
+
globalThis.expect(Platform$ReventlessLocal.decodeUiFragmentRegistryEventEnvelope(dcbEnvelopeOf(event))).toEqual(event);
|
|
75
|
+
});
|
|
76
|
+
globalThis.test("decodes a UiFragmentDeregistered event", () => {
|
|
77
|
+
let event = {
|
|
78
|
+
TAG: "UiFragmentDeregistered",
|
|
79
|
+
pluginId: "Catalog"
|
|
80
|
+
};
|
|
81
|
+
globalThis.expect(Platform$ReventlessLocal.decodeUiFragmentRegistryEventEnvelope(dcbEnvelopeOf(event))).toEqual(event);
|
|
82
|
+
});
|
|
83
|
+
globalThis.test("returns None for malformed input", () => {
|
|
84
|
+
globalThis.expect(Platform$ReventlessLocal.decodeUiFragmentRegistryEventEnvelope("junk")).toEqual(undefined);
|
|
85
|
+
globalThis.expect(Platform$ReventlessLocal.decodeUiFragmentRegistryEventEnvelope(Object.fromEntries([[
|
|
86
|
+
"event",
|
|
87
|
+
{}
|
|
88
|
+
]]))).toEqual(undefined);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
59
92
|
export {
|
|
60
93
|
meta,
|
|
61
94
|
envelopeOf,
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// **explicitly injected** in-process PubSub (never the local singleton) to
|
|
8
8
|
// prove the parameterized-PubSub seam is behavior-preserving:
|
|
9
9
|
//
|
|
10
|
-
// 1. registerAll builds the subscription SDL on a fresh GraphQL_ServerInstance
|
|
11
|
-
//
|
|
10
|
+
// 1. registerAll builds the subscription SDL on a fresh GraphQL_ServerInstance
|
|
11
|
+
// (fields register as-is — core SDL is provider-neutral) and injects
|
|
12
|
+
// `scalar AWSJSON`.
|
|
12
13
|
// 2. A resolver from makeFieldResolver, subscribed on the injected PubSub,
|
|
13
14
|
// receives a payload published via Bridge.publish on that same PubSub.
|
|
14
15
|
|
|
@@ -53,12 +54,12 @@ let yieldTick = (): promise<unit> =>
|
|
|
53
54
|
})
|
|
54
55
|
|
|
55
56
|
describe("GraphQL_SubscriptionBridge — injected PubSub seam", () => {
|
|
56
|
-
testSync("registerAll builds subscription SDL
|
|
57
|
+
testSync("registerAll builds subscription SDL as-is and injects AWSJSON", () => {
|
|
57
58
|
let pubSub = GraphqlYoga.createPubSub()
|
|
58
59
|
let server = Server.make(~label="SeamTest")
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
// Core emits provider-neutral subscription SDL — no directives to strip.
|
|
62
|
+
let sdlField = "onCatalogProduct_stateChanged: CatalogProduct"
|
|
62
63
|
|
|
63
64
|
Bridge.registerAll(
|
|
64
65
|
~server,
|
|
@@ -71,8 +72,6 @@ describe("GraphQL_SubscriptionBridge — injected PubSub seam", () => {
|
|
|
71
72
|
let sdl = server.buildSdl()
|
|
72
73
|
expect(sdl->String.includes("type Subscription"))->toBe(true)
|
|
73
74
|
expect(sdl->String.includes("onCatalogProduct_stateChanged"))->toBe(true)
|
|
74
|
-
// @aws_subscribe is AppSync-only and must be stripped for yoga.
|
|
75
|
-
expect(sdl->String.includes("@aws_subscribe"))->toBe(false)
|
|
76
75
|
// AWSJSON scalar injected so yoga accepts AppSync-derived event log types.
|
|
77
76
|
expect(sdl->String.includes("scalar AWSJSON"))->toBe(true)
|
|
78
77
|
})
|
|
@@ -34,17 +34,16 @@ function yieldTick() {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
globalThis.describe("GraphQL_SubscriptionBridge — injected PubSub seam", () => {
|
|
37
|
-
globalThis.test("registerAll builds subscription SDL
|
|
37
|
+
globalThis.test("registerAll builds subscription SDL as-is and injects AWSJSON", () => {
|
|
38
38
|
let pubSub = GraphqlYoga.createPubSub();
|
|
39
39
|
let server = GraphQL_ServerInstance$ReventlessGraphqlServer.make("SeamTest");
|
|
40
|
-
GraphQL_SubscriptionBridge$ReventlessGraphqlServer.registerAll(server, ["onCatalogProduct_stateChanged: CatalogProduct
|
|
40
|
+
GraphQL_SubscriptionBridge$ReventlessGraphqlServer.registerAll(server, ["onCatalogProduct_stateChanged: CatalogProduct"], [], [{
|
|
41
41
|
fieldName: "onCatalogProduct_stateChanged",
|
|
42
42
|
topic: "onCatalogProduct_stateChanged"
|
|
43
43
|
}], pubSub);
|
|
44
44
|
let sdl = server.buildSdl();
|
|
45
45
|
globalThis.expect(sdl.includes("type Subscription")).toBe(true);
|
|
46
46
|
globalThis.expect(sdl.includes("onCatalogProduct_stateChanged")).toBe(true);
|
|
47
|
-
globalThis.expect(sdl.includes("@aws_subscribe")).toBe(false);
|
|
48
47
|
globalThis.expect(sdl.includes("scalar AWSJSON")).toBe(true);
|
|
49
48
|
});
|
|
50
49
|
globalThis.test("makeFieldResolver + publish deliver over the injected PubSub", async () => {
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Behavior_GWT$ReventlessGwt from "@reventlessdev/reventless-gwt/src/Behavior_GWT.res.mjs";
|
|
4
|
+
import * as ApiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry.res.mjs";
|
|
5
|
+
import * as ApiFragmentRegistry_Behavior$ReventlessCore from "@reventlessdev/reventless-core/src/admin/ApiFragmentRegistry/StateChangeSlice/ApiFragmentRegistry_Behavior.res.mjs";
|
|
6
|
+
|
|
7
|
+
let Test = Behavior_GWT$ReventlessGwt.Make({
|
|
8
|
+
name: ApiFragmentRegistry$ReventlessCore.name,
|
|
9
|
+
consumedEventSchema: ApiFragmentRegistry$ReventlessCore.consumedEventSchema,
|
|
10
|
+
commandSchema: ApiFragmentRegistry$ReventlessCore.commandSchema,
|
|
11
|
+
errorSchema: ApiFragmentRegistry$ReventlessCore.errorSchema,
|
|
12
|
+
eventSchema: ApiFragmentRegistry$ReventlessCore.eventSchema
|
|
13
|
+
})({
|
|
14
|
+
initialState: undefined,
|
|
15
|
+
evolve: ApiFragmentRegistry_Behavior$ReventlessCore.evolve,
|
|
16
|
+
decide: ApiFragmentRegistry_Behavior$ReventlessCore.decide
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let fragment1 = {
|
|
20
|
+
encoded: `{"types":["type Catalog_Product { id: ID! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
21
|
+
protocol: "graphql"
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
let fragment2 = {
|
|
25
|
+
encoded: `{"types":["type Catalog_Product { id: ID!\\n name: String! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
|
|
26
|
+
protocol: "graphql"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
Test.describe("ApiFragmentRegistry StateChangeSlice", () => {
|
|
30
|
+
Test.test("register on empty state emits ApiFragmentRegistered", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([]), {
|
|
31
|
+
TAG: "RegisterApiFragment",
|
|
32
|
+
pluginId: "p1",
|
|
33
|
+
fragment: fragment1,
|
|
34
|
+
apiTarget: "Domain",
|
|
35
|
+
at: "t0"
|
|
36
|
+
}), {
|
|
37
|
+
TAG: "ApiFragmentRegistered",
|
|
38
|
+
pluginId: "p1",
|
|
39
|
+
fragment: fragment1,
|
|
40
|
+
apiTarget: "Domain",
|
|
41
|
+
at: "t0"
|
|
42
|
+
}));
|
|
43
|
+
Test.test("re-registering an identical fragment + target is idempotent (no event)", () => Test.thenNoEvent(Test.whenCmd(Test.givenEvents([{
|
|
44
|
+
TAG: "ApiFragmentRegistered",
|
|
45
|
+
pluginId: "p1",
|
|
46
|
+
fragment: fragment1,
|
|
47
|
+
apiTarget: "Domain",
|
|
48
|
+
at: "t0"
|
|
49
|
+
}]), {
|
|
50
|
+
TAG: "RegisterApiFragment",
|
|
51
|
+
pluginId: "p1",
|
|
52
|
+
fragment: fragment1,
|
|
53
|
+
apiTarget: "Domain",
|
|
54
|
+
at: "t1"
|
|
55
|
+
})));
|
|
56
|
+
Test.test("registering a changed fragment emits ApiFragmentUpdated (version supersession)", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([{
|
|
57
|
+
TAG: "ApiFragmentRegistered",
|
|
58
|
+
pluginId: "p1",
|
|
59
|
+
fragment: fragment1,
|
|
60
|
+
apiTarget: "Domain",
|
|
61
|
+
at: "t0"
|
|
62
|
+
}]), {
|
|
63
|
+
TAG: "RegisterApiFragment",
|
|
64
|
+
pluginId: "p1",
|
|
65
|
+
fragment: fragment2,
|
|
66
|
+
apiTarget: "Domain",
|
|
67
|
+
at: "t1"
|
|
68
|
+
}), {
|
|
69
|
+
TAG: "ApiFragmentUpdated",
|
|
70
|
+
pluginId: "p1",
|
|
71
|
+
previousFragment: fragment1,
|
|
72
|
+
newFragment: fragment2,
|
|
73
|
+
apiTarget: "Domain",
|
|
74
|
+
at: "t1"
|
|
75
|
+
}));
|
|
76
|
+
Test.test("retargeting a plugin with an identical fragment emits ApiFragmentUpdated", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([{
|
|
77
|
+
TAG: "ApiFragmentRegistered",
|
|
78
|
+
pluginId: "p1",
|
|
79
|
+
fragment: fragment1,
|
|
80
|
+
apiTarget: "Domain",
|
|
81
|
+
at: "t0"
|
|
82
|
+
}]), {
|
|
83
|
+
TAG: "RegisterApiFragment",
|
|
84
|
+
pluginId: "p1",
|
|
85
|
+
fragment: fragment1,
|
|
86
|
+
apiTarget: "Platform",
|
|
87
|
+
at: "t1"
|
|
88
|
+
}), {
|
|
89
|
+
TAG: "ApiFragmentUpdated",
|
|
90
|
+
pluginId: "p1",
|
|
91
|
+
previousFragment: fragment1,
|
|
92
|
+
newFragment: fragment1,
|
|
93
|
+
apiTarget: "Platform",
|
|
94
|
+
at: "t1"
|
|
95
|
+
}));
|
|
96
|
+
Test.test("deregister emits ApiFragmentDeregistered", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([{
|
|
97
|
+
TAG: "ApiFragmentRegistered",
|
|
98
|
+
pluginId: "p1",
|
|
99
|
+
fragment: fragment1,
|
|
100
|
+
apiTarget: "Domain",
|
|
101
|
+
at: "t0"
|
|
102
|
+
}]), {
|
|
103
|
+
TAG: "DeregisterApiFragment",
|
|
104
|
+
pluginId: "p1"
|
|
105
|
+
}), {
|
|
106
|
+
TAG: "ApiFragmentDeregistered",
|
|
107
|
+
pluginId: "p1"
|
|
108
|
+
}));
|
|
109
|
+
Test.test("deregistering an absent fragment is idempotent (no event)", () => Test.thenNoEvent(Test.whenCmd(Test.givenEvents([]), {
|
|
110
|
+
TAG: "DeregisterApiFragment",
|
|
111
|
+
pluginId: "p1"
|
|
112
|
+
})));
|
|
113
|
+
Test.test("recording a push outcome emits ApiFragmentPushRecorded", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([{
|
|
114
|
+
TAG: "ApiFragmentRegistered",
|
|
115
|
+
pluginId: "p1",
|
|
116
|
+
fragment: fragment1,
|
|
117
|
+
apiTarget: "Domain",
|
|
118
|
+
at: "t0"
|
|
119
|
+
}]), {
|
|
120
|
+
TAG: "RecordApiFragmentPush",
|
|
121
|
+
pluginId: "p1",
|
|
122
|
+
ok: true,
|
|
123
|
+
message: "",
|
|
124
|
+
at: "t1"
|
|
125
|
+
}), {
|
|
126
|
+
TAG: "ApiFragmentPushRecorded",
|
|
127
|
+
pluginId: "p1",
|
|
128
|
+
ok: true,
|
|
129
|
+
message: "",
|
|
130
|
+
at: "t1"
|
|
131
|
+
}));
|
|
132
|
+
Test.test("redelivering an identical push record is idempotent (no event)", () => Test.thenNoEvent(Test.whenCmd(Test.givenEvents([
|
|
133
|
+
{
|
|
134
|
+
TAG: "ApiFragmentRegistered",
|
|
135
|
+
pluginId: "p1",
|
|
136
|
+
fragment: fragment1,
|
|
137
|
+
apiTarget: "Domain",
|
|
138
|
+
at: "t0"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
TAG: "ApiFragmentPushRecorded",
|
|
142
|
+
pluginId: "p1",
|
|
143
|
+
ok: true,
|
|
144
|
+
message: "",
|
|
145
|
+
at: "t1"
|
|
146
|
+
}
|
|
147
|
+
]), {
|
|
148
|
+
TAG: "RecordApiFragmentPush",
|
|
149
|
+
pluginId: "p1",
|
|
150
|
+
ok: true,
|
|
151
|
+
message: "",
|
|
152
|
+
at: "t1"
|
|
153
|
+
})));
|
|
154
|
+
Test.test("a later distinct push outcome is recorded again", () => Test.thenEvent(Test.whenCmd(Test.givenEvents([
|
|
155
|
+
{
|
|
156
|
+
TAG: "ApiFragmentRegistered",
|
|
157
|
+
pluginId: "p1",
|
|
158
|
+
fragment: fragment1,
|
|
159
|
+
apiTarget: "Domain",
|
|
160
|
+
at: "t0"
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
TAG: "ApiFragmentPushRecorded",
|
|
164
|
+
pluginId: "p1",
|
|
165
|
+
ok: true,
|
|
166
|
+
message: "",
|
|
167
|
+
at: "t1"
|
|
168
|
+
}
|
|
169
|
+
]), {
|
|
170
|
+
TAG: "RecordApiFragmentPush",
|
|
171
|
+
pluginId: "p1",
|
|
172
|
+
ok: false,
|
|
173
|
+
message: "stitch failed",
|
|
174
|
+
at: "t2"
|
|
175
|
+
}), {
|
|
176
|
+
TAG: "ApiFragmentPushRecorded",
|
|
177
|
+
pluginId: "p1",
|
|
178
|
+
ok: false,
|
|
179
|
+
message: "stitch failed",
|
|
180
|
+
at: "t2"
|
|
181
|
+
}));
|
|
182
|
+
Test.test("recording a push for an unregistered plugin is dropped (no event)", () => Test.thenNoEvent(Test.whenCmd(Test.givenEvents([]), {
|
|
183
|
+
TAG: "RecordApiFragmentPush",
|
|
184
|
+
pluginId: "p1",
|
|
185
|
+
ok: true,
|
|
186
|
+
message: "",
|
|
187
|
+
at: "t1"
|
|
188
|
+
})));
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
Test,
|
|
193
|
+
fragment1,
|
|
194
|
+
fragment2,
|
|
195
|
+
}
|
|
196
|
+
/* Test Not a pure module */
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// StateViewSlice projection GWT for the ApiFragments read side — the current fragment
|
|
2
|
+
// per plugin name plus push status, replacing the deploy-schema:* rows as the durable
|
|
3
|
+
// stitch source (event-sourced-fragment-registries plan).
|
|
4
|
+
open ReventlessCore
|
|
5
|
+
module P = Reventless.Plugin // P.Domain / P.Platform — avoid opening apiSchemaFragment's labels
|
|
6
|
+
|
|
7
|
+
module Test = ReventlessGwt.Projection_GWT.Make(ApiFragments, ApiFragments_Projection)
|
|
8
|
+
open Test
|
|
9
|
+
open ApiFragments
|
|
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
|
+
let registeredState: ApiFragments.state = {
|
|
22
|
+
pluginId: "p1",
|
|
23
|
+
encoded: fragment1.encoded,
|
|
24
|
+
protocol: fragment1.protocol,
|
|
25
|
+
apiTarget: P.Domain,
|
|
26
|
+
registeredAt: "t0",
|
|
27
|
+
updatedAt: "t0",
|
|
28
|
+
pushStatus: "pending",
|
|
29
|
+
pushMessage: "",
|
|
30
|
+
pushedAt: "",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("ApiFragments StateViewSlice projection", () => {
|
|
34
|
+
test("ApiFragmentRegistered creates the row pending (registeredAt = updatedAt = event time)", () =>
|
|
35
|
+
givenEvents([])
|
|
36
|
+
->whenEvent(ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}))
|
|
37
|
+
->thenStateWithId("p1", registeredState)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
test("ApiFragmentPushRecorded(ok) marks the row ok with the push time", () =>
|
|
41
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
42
|
+
->whenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}))
|
|
43
|
+
->thenStateWithId("p1", {...registeredState, pushStatus: "ok", pushedAt: "t1"})
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
test("ApiFragmentPushRecorded(error) carries the message", () =>
|
|
47
|
+
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
|
+
)
|
|
51
|
+
->thenStateWithId(
|
|
52
|
+
"p1",
|
|
53
|
+
{...registeredState, pushStatus: "error", pushMessage: "stitch failed", pushedAt: "t1"},
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
test("ApiFragmentUpdated replaces the fragment and resets the row to pending", () =>
|
|
58
|
+
givenEvents([
|
|
59
|
+
ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}),
|
|
60
|
+
ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}),
|
|
61
|
+
])
|
|
62
|
+
->whenEvent(
|
|
63
|
+
ApiFragmentUpdated({
|
|
64
|
+
pluginId: "p1",
|
|
65
|
+
previousFragment: fragment1,
|
|
66
|
+
newFragment: fragment2,
|
|
67
|
+
apiTarget: P.Domain,
|
|
68
|
+
at: "t2",
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
->thenStateWithId(
|
|
72
|
+
"p1",
|
|
73
|
+
{
|
|
74
|
+
...registeredState,
|
|
75
|
+
encoded: fragment2.encoded,
|
|
76
|
+
updatedAt: "t2",
|
|
77
|
+
pushStatus: "pending",
|
|
78
|
+
pushMessage: "",
|
|
79
|
+
pushedAt: "t1",
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
test("ApiFragmentUpdated carrying a new target moves the row to that API", () =>
|
|
85
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
86
|
+
->whenEvent(
|
|
87
|
+
ApiFragmentUpdated({
|
|
88
|
+
pluginId: "p1",
|
|
89
|
+
previousFragment: fragment1,
|
|
90
|
+
newFragment: fragment1,
|
|
91
|
+
apiTarget: P.Platform,
|
|
92
|
+
at: "t1",
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
->thenStateWithId("p1", {...registeredState, apiTarget: P.Platform, updatedAt: "t1"})
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
test("ApiFragmentDeregistered removes the row", () =>
|
|
99
|
+
givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
|
|
100
|
+
->whenEvent(ApiFragmentDeregistered({pluginId: "p1"}))
|
|
101
|
+
->thenNoState
|
|
102
|
+
)
|
|
103
|
+
})
|