@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.
@@ -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, `registerAll`,
6
- // and the `@aws_subscribe` stripping — live in core-adjacent
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
- * (after stripping `@aws_subscribe` directives not valid in yoga).
104
+ * (provider-neutralregistered as-is).
105
105
  */
106
106
  let registerAll = (
107
107
  ~server: ReventlessGraphqlServer.GraphQL_ServerInstance.t,
@@ -7,6 +7,7 @@ let noHooks: ReventlessCore.Plugin_Helpers.platformHooks = {
7
7
  schedulerRoleUrn: ref(Pulumi.Output.make("")),
8
8
  api: ref(None),
9
9
  apiRole: ref(None),
10
+ adminApi: ref(None),
10
11
  deployTarget: ref("Domain"),
11
12
  }
12
13
 
@@ -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
- // strips @aws_subscribe, and injects `scalar AWSJSON`.
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, strips @aws_subscribe, injects AWSJSON", () => {
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
- let sdlField =
61
- "onCatalogProduct_stateChanged: CatalogProduct\n @aws_subscribe(mutations: [\"addProduct\"])"
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, strips @aws_subscribe, injects AWSJSON", () => {
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\n @aws_subscribe(mutations: [\"addProduct\"])"], [], [{
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
+ })