@reventlessdev/reventless-local 3.0.0-alpha.138 → 3.0.0-alpha.140
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 +10 -9
- package/rescript.json +1 -0
- package/src/Platform.res +3 -3
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res +27 -54
- package/src/adapter/Api/LocalGraphQL_SubscriptionResolvers.res.mjs +13 -26
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +5 -5
- package/src/adapter/CommandGenerator/InboundTranslationResolvers_GraphQL.res +2 -2
- package/src/adapter/DomainGraphQL_Server.res +6 -6
- package/src/adapter/PlatformGraphQL_Server.res +6 -6
- package/src/adapter/PlatformGraphQL_Server.res.mjs +2 -2
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +11 -11
- package/tests/SplitApiFixtures.res +4 -4
- package/tests/SplitApiFixtures.res.mjs +2 -2
- package/tests/SplitApiTest.res +2 -2
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res +120 -0
- package/tests/adapter/GraphQLSubscriptionBridgeTest.res.mjs +96 -0
- package/src/adapter/GraphQL_ServerInstance.res +0 -310
- package/src/adapter/GraphQL_ServerInstance.res.mjs +0 -257
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Focused seam test for the promoted, transport-neutral GraphQL server runtime
|
|
2
|
+
// (`ReventlessGraphqlServer.GraphQL_ServerInstance` + `GraphQL_SubscriptionBridge`).
|
|
3
|
+
//
|
|
4
|
+
// The promotion introduced ONE behavioral seam: the subscription bridge takes
|
|
5
|
+
// its PubSub instance as a *parameter* instead of constructing a module
|
|
6
|
+
// singleton. This test drives the promoted modules directly with an
|
|
7
|
+
// **explicitly injected** in-process PubSub (never the local singleton) to
|
|
8
|
+
// prove the parameterized-PubSub seam is behavior-preserving:
|
|
9
|
+
//
|
|
10
|
+
// 1. registerAll builds the subscription SDL on a fresh GraphQL_ServerInstance,
|
|
11
|
+
// strips @aws_subscribe, and injects `scalar AWSJSON`.
|
|
12
|
+
// 2. A resolver from makeFieldResolver, subscribed on the injected PubSub,
|
|
13
|
+
// receives a payload published via Bridge.publish on that same PubSub.
|
|
14
|
+
|
|
15
|
+
@@warning("-44")
|
|
16
|
+
|
|
17
|
+
open JestGlobals
|
|
18
|
+
|
|
19
|
+
module Bridge = ReventlessGraphqlServer.GraphQL_SubscriptionBridge
|
|
20
|
+
module Server = ReventlessGraphqlServer.GraphQL_ServerInstance
|
|
21
|
+
|
|
22
|
+
// AsyncIterable.next() binding — yoga's PubSub returns {value, done}.
|
|
23
|
+
type asyncIterResult = {value: JSON.t, done: bool}
|
|
24
|
+
@send external iterNext: 'a => promise<asyncIterResult> = "next"
|
|
25
|
+
|
|
26
|
+
@val external setTimeout: (unit => unit, int) => int = "setTimeout"
|
|
27
|
+
|
|
28
|
+
// The subscription resolver object shape produced by makeFieldResolver.
|
|
29
|
+
// `subscribe` returns yoga's Repeater-based AsyncIterable (kept opaque).
|
|
30
|
+
type iterable
|
|
31
|
+
type subResolver = {
|
|
32
|
+
subscribe: (JSON.t, JSON.t, JSON.t) => iterable,
|
|
33
|
+
resolve: JSON.t => JSON.t,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let startConsumer = (iter: 'iter, timeoutMs: int): promise<Null.t<JSON.t>> => {
|
|
37
|
+
let timeoutP = Promise.make((resolve, _) => {
|
|
38
|
+
let _: int = setTimeout(() => resolve(Null.null), timeoutMs)
|
|
39
|
+
})
|
|
40
|
+
let consumeP = async () =>
|
|
41
|
+
try {
|
|
42
|
+
let result = await iter->iterNext
|
|
43
|
+
result.done ? Null.null : result.value->Null.make
|
|
44
|
+
} catch {
|
|
45
|
+
| _ => Null.null
|
|
46
|
+
}
|
|
47
|
+
Promise.race([timeoutP, consumeP()])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let yieldTick = (): promise<unit> =>
|
|
51
|
+
Promise.make((resolve, _) => {
|
|
52
|
+
let _: int = setTimeout(() => resolve(), 10)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe("GraphQL_SubscriptionBridge — injected PubSub seam", () => {
|
|
56
|
+
testSync("registerAll builds subscription SDL, strips @aws_subscribe, injects AWSJSON", () => {
|
|
57
|
+
let pubSub = GraphqlYoga.createPubSub()
|
|
58
|
+
let server = Server.make(~label="SeamTest")
|
|
59
|
+
|
|
60
|
+
let sdlField =
|
|
61
|
+
"onCatalogProduct_stateChanged: CatalogProduct\n @aws_subscribe(mutations: [\"addProduct\"])"
|
|
62
|
+
|
|
63
|
+
Bridge.registerAll(
|
|
64
|
+
~server,
|
|
65
|
+
~sdlFields=[sdlField],
|
|
66
|
+
~sourceAEntries=[],
|
|
67
|
+
~sourceBEntries=[{fieldName: "onCatalogProduct_stateChanged", topic: "onCatalogProduct_stateChanged"}],
|
|
68
|
+
~pubSub,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
let sdl = server.buildSdl()
|
|
72
|
+
expect(sdl->String.includes("type Subscription"))->toBe(true)
|
|
73
|
+
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
|
+
// AWSJSON scalar injected so yoga accepts AppSync-derived event log types.
|
|
77
|
+
expect(sdl->String.includes("scalar AWSJSON"))->toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
testPromise("makeFieldResolver + publish deliver over the injected PubSub", async () => {
|
|
81
|
+
let pubSub = GraphqlYoga.createPubSub()
|
|
82
|
+
let topic = "onCatalogProduct_stateChanged"
|
|
83
|
+
|
|
84
|
+
let resolver: subResolver = Obj.magic(Bridge.makeFieldResolver(~pubSub, topic))
|
|
85
|
+
let iter = resolver.subscribe(JSON.Encode.null, JSON.Encode.null, JSON.Encode.null)
|
|
86
|
+
let consumerPromise = startConsumer(iter, 1000)
|
|
87
|
+
await yieldTick()
|
|
88
|
+
|
|
89
|
+
let payload =
|
|
90
|
+
JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string("prod-1"))]))
|
|
91
|
+
Bridge.publish(~pubSub, topic, payload)
|
|
92
|
+
|
|
93
|
+
let received = await consumerPromise
|
|
94
|
+
switch received->Null.toOption {
|
|
95
|
+
| Some(msg) => expect(msg)->toEqual(payload)
|
|
96
|
+
| None => expect("injected-pubsub: timed out")->toBe("received")
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
testPromise("a separate injected PubSub does NOT receive another's publish", async () => {
|
|
101
|
+
// Proves isolation: publishing on PubSub A never reaches a subscriber on B.
|
|
102
|
+
let pubSubA = GraphqlYoga.createPubSub()
|
|
103
|
+
let pubSubB = GraphqlYoga.createPubSub()
|
|
104
|
+
let topic = "onCatalogProduct_stateChanged"
|
|
105
|
+
|
|
106
|
+
let resolverB: subResolver = Obj.magic(Bridge.makeFieldResolver(~pubSub=pubSubB, topic))
|
|
107
|
+
let iterB = resolverB.subscribe(JSON.Encode.null, JSON.Encode.null, JSON.Encode.null)
|
|
108
|
+
let consumerPromise = startConsumer(iterB, 300)
|
|
109
|
+
await yieldTick()
|
|
110
|
+
|
|
111
|
+
Bridge.publish(
|
|
112
|
+
~pubSub=pubSubA,
|
|
113
|
+
topic,
|
|
114
|
+
JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string("prod-1"))])),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
let received = await consumerPromise
|
|
118
|
+
expect(received->Null.toOption)->toEqual(None)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as GraphqlYoga from "graphql-yoga";
|
|
4
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
5
|
+
import * as GraphQL_ServerInstance$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_ServerInstance.res.mjs";
|
|
6
|
+
import * as GraphQL_SubscriptionBridge$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_SubscriptionBridge.res.mjs";
|
|
7
|
+
|
|
8
|
+
function startConsumer(iter, timeoutMs) {
|
|
9
|
+
let timeoutP = new Promise((resolve, param) => {
|
|
10
|
+
setTimeout(() => resolve(null), timeoutMs);
|
|
11
|
+
});
|
|
12
|
+
let consumeP = async () => {
|
|
13
|
+
try {
|
|
14
|
+
let result = await iter.next();
|
|
15
|
+
if (result.done) {
|
|
16
|
+
return null;
|
|
17
|
+
} else {
|
|
18
|
+
return result.value;
|
|
19
|
+
}
|
|
20
|
+
} catch (exn) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return Promise.race([
|
|
25
|
+
timeoutP,
|
|
26
|
+
consumeP()
|
|
27
|
+
]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function yieldTick() {
|
|
31
|
+
return new Promise((resolve, param) => {
|
|
32
|
+
setTimeout(() => resolve(), 10);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
globalThis.describe("GraphQL_SubscriptionBridge — injected PubSub seam", () => {
|
|
37
|
+
globalThis.test("registerAll builds subscription SDL, strips @aws_subscribe, injects AWSJSON", () => {
|
|
38
|
+
let pubSub = GraphqlYoga.createPubSub();
|
|
39
|
+
let server = GraphQL_ServerInstance$ReventlessGraphqlServer.make("SeamTest");
|
|
40
|
+
GraphQL_SubscriptionBridge$ReventlessGraphqlServer.registerAll(server, ["onCatalogProduct_stateChanged: CatalogProduct\n @aws_subscribe(mutations: [\"addProduct\"])"], [], [{
|
|
41
|
+
fieldName: "onCatalogProduct_stateChanged",
|
|
42
|
+
topic: "onCatalogProduct_stateChanged"
|
|
43
|
+
}], pubSub);
|
|
44
|
+
let sdl = server.buildSdl();
|
|
45
|
+
globalThis.expect(sdl.includes("type Subscription")).toBe(true);
|
|
46
|
+
globalThis.expect(sdl.includes("onCatalogProduct_stateChanged")).toBe(true);
|
|
47
|
+
globalThis.expect(sdl.includes("@aws_subscribe")).toBe(false);
|
|
48
|
+
globalThis.expect(sdl.includes("scalar AWSJSON")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
globalThis.test("makeFieldResolver + publish deliver over the injected PubSub", async () => {
|
|
51
|
+
let pubSub = GraphqlYoga.createPubSub();
|
|
52
|
+
let topic = "onCatalogProduct_stateChanged";
|
|
53
|
+
let resolver = GraphQL_SubscriptionBridge$ReventlessGraphqlServer.makeFieldResolver(pubSub, topic);
|
|
54
|
+
let iter = resolver.subscribe(null, null, null);
|
|
55
|
+
let consumerPromise = startConsumer(iter, 1000);
|
|
56
|
+
await yieldTick();
|
|
57
|
+
let payload = Object.fromEntries([[
|
|
58
|
+
"id",
|
|
59
|
+
"prod-1"
|
|
60
|
+
]]);
|
|
61
|
+
GraphQL_SubscriptionBridge$ReventlessGraphqlServer.publish(pubSub, topic, payload);
|
|
62
|
+
let received = await consumerPromise;
|
|
63
|
+
if (received !== null) {
|
|
64
|
+
globalThis.expect(received).toEqual(payload);
|
|
65
|
+
} else {
|
|
66
|
+
globalThis.expect("injected-pubsub: timed out").toBe("received");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
globalThis.test("a separate injected PubSub does NOT receive another's publish", async () => {
|
|
70
|
+
let pubSubA = GraphqlYoga.createPubSub();
|
|
71
|
+
let pubSubB = GraphqlYoga.createPubSub();
|
|
72
|
+
let topic = "onCatalogProduct_stateChanged";
|
|
73
|
+
let resolverB = GraphQL_SubscriptionBridge$ReventlessGraphqlServer.makeFieldResolver(pubSubB, topic);
|
|
74
|
+
let iterB = resolverB.subscribe(null, null, null);
|
|
75
|
+
let consumerPromise = startConsumer(iterB, 300);
|
|
76
|
+
await yieldTick();
|
|
77
|
+
GraphQL_SubscriptionBridge$ReventlessGraphqlServer.publish(pubSubA, topic, Object.fromEntries([[
|
|
78
|
+
"id",
|
|
79
|
+
"prod-1"
|
|
80
|
+
]]));
|
|
81
|
+
let received = await consumerPromise;
|
|
82
|
+
globalThis.expect(received === null ? undefined : Primitive_option.some(received)).toEqual(undefined);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
let Bridge;
|
|
87
|
+
|
|
88
|
+
let Server;
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
Bridge,
|
|
92
|
+
Server,
|
|
93
|
+
startConsumer,
|
|
94
|
+
yieldTick,
|
|
95
|
+
}
|
|
96
|
+
/* Not a pure module */
|
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
// GraphQL server instance factory.
|
|
2
|
-
// Creates independent server instances with isolated registries.
|
|
3
|
-
// Used by Platform to create separate core and plugin servers in split mode.
|
|
4
|
-
|
|
5
|
-
module YG = GraphqlYoga
|
|
6
|
-
|
|
7
|
-
let log = ReventlessCore.Logger.fromEnv()
|
|
8
|
-
|
|
9
|
-
type resolverFn = YG.resolverFn
|
|
10
|
-
|
|
11
|
-
// graphql-ws WebSocket transport for subscriptions. Yoga's built-in
|
|
12
|
-
// subscription transport is SSE-over-POST; clients using the graphql-ws
|
|
13
|
-
// protocol (e.g. host-shell, AppSync clients in prod) need an explicit
|
|
14
|
-
// WebSocket endpoint. We attach a `WebSocketServer` to the same HTTP server
|
|
15
|
-
// and hand it to `graphql-ws/lib/use/ws::useServer` along with the live
|
|
16
|
-
// schema, which fans out subscription events to connected sockets.
|
|
17
|
-
type wsServer
|
|
18
|
-
@new @module("ws")
|
|
19
|
-
external newWebSocketServer: {"server": YG.httpServer, "path": string} => wsServer =
|
|
20
|
-
"WebSocketServer"
|
|
21
|
-
@module("graphql-ws/lib/use/ws")
|
|
22
|
-
external wsUseServer: ({"schema": YG.schema}, wsServer) => unit = "useServer"
|
|
23
|
-
|
|
24
|
-
@val external processEnv: dict<string> = "process.env"
|
|
25
|
-
let debug = processEnv->Dict.get("GRAPHQL_DEBUG")->Option.isSome
|
|
26
|
-
|
|
27
|
-
type diagnostics = {
|
|
28
|
-
registeredTypeDefinitions: array<string>,
|
|
29
|
-
registeredMutationFields: array<string>,
|
|
30
|
-
registeredQueryFields: array<string>,
|
|
31
|
-
registeredMutationResolvers: array<string>,
|
|
32
|
-
registeredQueryResolvers: array<string>,
|
|
33
|
-
typeCount: int,
|
|
34
|
-
sdlMutationCount: int,
|
|
35
|
-
sdlQueryCount: int,
|
|
36
|
-
resolverMutationCount: int,
|
|
37
|
-
resolverQueryCount: int,
|
|
38
|
-
mismatches: array<string>,
|
|
39
|
-
fullSdl: option<string>,
|
|
40
|
-
serverRunning: bool,
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
type t = {
|
|
44
|
-
registerMutations: (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => unit,
|
|
45
|
-
registerQueries: (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => unit,
|
|
46
|
-
registerSubscriptions: (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => unit,
|
|
47
|
-
registerTypes: (~sdlTypes: array<string>) => unit,
|
|
48
|
-
getMutationResolver: string => option<resolverFn>,
|
|
49
|
-
getQueryResolver: string => option<resolverFn>,
|
|
50
|
-
// `~contextFactory` is the yoga context factory — when supplied, requests
|
|
51
|
-
// run through it so resolvers see `ctx.identity`. Omit (or pass None) for
|
|
52
|
-
// unauthenticated servers. In-memory `Platform.res` wires
|
|
53
|
-
// `Auth_GraphqlContext.buildAuthContext` here so the admin server enforces
|
|
54
|
-
// the same bearer-token rules as the data server instead of leaving
|
|
55
|
-
// Platform_* queries / mutations wide open.
|
|
56
|
-
start: (~port: int=?, ~contextFactory: YG.contextFactory=?, unit) => unit,
|
|
57
|
-
stop: unit => unit,
|
|
58
|
-
reset: unit => unit,
|
|
59
|
-
buildSdl: unit => string,
|
|
60
|
-
getFullSdl: unit => option<string>,
|
|
61
|
-
getSchema: unit => option<YG.schema>,
|
|
62
|
-
diagnostics: unit => diagnostics,
|
|
63
|
-
printDiagnostics: unit => unit,
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
let extractFieldName = (sdlField: string): string => {
|
|
67
|
-
let trimmed = sdlField->String.trim
|
|
68
|
-
trimmed
|
|
69
|
-
->String.split("(")
|
|
70
|
-
->Array.get(0)
|
|
71
|
-
->Option.getOr("")
|
|
72
|
-
->String.trim
|
|
73
|
-
->String.split(":")
|
|
74
|
-
->Array.get(0)
|
|
75
|
-
->Option.getOr("")
|
|
76
|
-
->String.trim
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
let make = (~label: string="GraphQL"): t => {
|
|
80
|
-
let mutationResolvers: ref<dict<resolverFn>> = ref(Dict.make())
|
|
81
|
-
let queryResolvers: ref<dict<resolverFn>> = ref(Dict.make())
|
|
82
|
-
let subscriptionResolvers: ref<dict<resolverFn>> = ref(Dict.make())
|
|
83
|
-
let mutationFields: ref<array<string>> = ref([])
|
|
84
|
-
let queryFields: ref<array<string>> = ref([])
|
|
85
|
-
let subscriptionFields: ref<array<string>> = ref([])
|
|
86
|
-
let typeDefinitions: ref<array<string>> = ref([])
|
|
87
|
-
let activeServer: ref<option<YG.httpServer>> = ref(None)
|
|
88
|
-
let activeSchema: ref<option<YG.schema>> = ref(None)
|
|
89
|
-
let lastFullSdl: ref<option<string>> = ref(None)
|
|
90
|
-
|
|
91
|
-
let registerMutations = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
|
|
92
|
-
mutationFields.contents = mutationFields.contents->Array.concat(sdlFields)
|
|
93
|
-
resolvers->Dict.toArray->Array.forEach(((k, v)) => mutationResolvers.contents->Dict.set(k, v))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
let registerQueries = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
|
|
97
|
-
queryFields.contents = queryFields.contents->Array.concat(sdlFields)
|
|
98
|
-
resolvers->Dict.toArray->Array.forEach(((k, v)) => queryResolvers.contents->Dict.set(k, v))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let registerSubscriptions = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
|
|
102
|
-
subscriptionFields.contents = subscriptionFields.contents->Array.concat(sdlFields)
|
|
103
|
-
resolvers->Dict.toArray->Array.forEach(((k, v)) =>
|
|
104
|
-
subscriptionResolvers.contents->Dict.set(k, v)
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let registerTypes = (~sdlTypes: array<string>) => {
|
|
109
|
-
typeDefinitions.contents = typeDefinitions.contents->Array.concat(sdlTypes)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let getMutationResolver = (fieldName: string): option<resolverFn> =>
|
|
113
|
-
mutationResolvers.contents->Dict.get(fieldName)
|
|
114
|
-
|
|
115
|
-
let getQueryResolver = (fieldName: string): option<resolverFn> =>
|
|
116
|
-
queryResolvers.contents->Dict.get(fieldName)
|
|
117
|
-
|
|
118
|
-
let buildSdl = () => {
|
|
119
|
-
let typesSdl =
|
|
120
|
-
typeDefinitions.contents->Array.length > 0
|
|
121
|
-
? typeDefinitions.contents->Array.join("\n\n")
|
|
122
|
-
: ""
|
|
123
|
-
let mutations =
|
|
124
|
-
mutationFields.contents->Array.length > 0
|
|
125
|
-
? mutationFields.contents->Array.join("\n")
|
|
126
|
-
: " _noop: String"
|
|
127
|
-
let queries =
|
|
128
|
-
queryFields.contents->Array.length > 0
|
|
129
|
-
? queryFields.contents->Array.join("\n")
|
|
130
|
-
: " _noop: String"
|
|
131
|
-
let queriesMutationsSdl = `type Query {
|
|
132
|
-
${queries}
|
|
133
|
-
}
|
|
134
|
-
type Mutation {
|
|
135
|
-
${mutations}
|
|
136
|
-
}`
|
|
137
|
-
let subscriptionsSdl =
|
|
138
|
-
subscriptionFields.contents->Array.length > 0
|
|
139
|
-
? `\n\ntype Subscription {\n${subscriptionFields.contents->Array.join("\n")}\n}`
|
|
140
|
-
: ""
|
|
141
|
-
let base =
|
|
142
|
-
typesSdl->String.length > 0
|
|
143
|
-
? typesSdl ++ "\n\n" ++ queriesMutationsSdl
|
|
144
|
-
: queriesMutationsSdl
|
|
145
|
-
base ++ subscriptionsSdl
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
let stop = () =>
|
|
149
|
-
switch activeServer.contents {
|
|
150
|
-
| Some(server) =>
|
|
151
|
-
server->YG.close(() => ())
|
|
152
|
-
activeServer.contents = None
|
|
153
|
-
activeSchema.contents = None
|
|
154
|
-
| None => ()
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let start = (~port: int=4000, ~contextFactory: option<YG.contextFactory>=?, ()) => {
|
|
158
|
-
let resolvers = Dict.make()
|
|
159
|
-
resolvers->Dict.set("Query", queryResolvers.contents)
|
|
160
|
-
resolvers->Dict.set("Mutation", mutationResolvers.contents)
|
|
161
|
-
if subscriptionResolvers.contents->Dict.keysToArray->Array.length > 0 {
|
|
162
|
-
resolvers->Dict.set("Subscription", subscriptionResolvers.contents)
|
|
163
|
-
}
|
|
164
|
-
let sdl = buildSdl()
|
|
165
|
-
lastFullSdl.contents = Some(sdl)
|
|
166
|
-
let schema = YG.createSchema({"typeDefs": sdl, "resolvers": resolvers})
|
|
167
|
-
activeSchema.contents = Some(schema)
|
|
168
|
-
let yoga = switch contextFactory {
|
|
169
|
-
| Some(ctx) =>
|
|
170
|
-
YG.createYogaWithContext({
|
|
171
|
-
"schema": schema,
|
|
172
|
-
"graphiql": true,
|
|
173
|
-
"logging": debug,
|
|
174
|
-
"maskedErrors": !debug,
|
|
175
|
-
"context": ctx,
|
|
176
|
-
})
|
|
177
|
-
| None =>
|
|
178
|
-
YG.createYoga({
|
|
179
|
-
"schema": schema,
|
|
180
|
-
"graphiql": true,
|
|
181
|
-
"logging": debug,
|
|
182
|
-
"maskedErrors": !debug,
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
let server = YG.createServer(yoga)
|
|
186
|
-
server->YG.listen(port, () =>
|
|
187
|
-
log.info(~comp=label, `listening on http://localhost:${port->Int.toString}/graphql`)
|
|
188
|
-
)
|
|
189
|
-
if subscriptionResolvers.contents->Dict.keysToArray->Array.length > 0 {
|
|
190
|
-
let wss = newWebSocketServer({"server": server, "path": "/graphql"})
|
|
191
|
-
wsUseServer({"schema": schema}, wss)
|
|
192
|
-
log.info(~comp=label, `graphql-ws subscriptions on ws://localhost:${port->Int.toString}/graphql`)
|
|
193
|
-
}
|
|
194
|
-
activeServer.contents = Some(server)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
let reset = () => {
|
|
198
|
-
mutationResolvers.contents = Dict.make()
|
|
199
|
-
queryResolvers.contents = Dict.make()
|
|
200
|
-
subscriptionResolvers.contents = Dict.make()
|
|
201
|
-
mutationFields.contents = []
|
|
202
|
-
queryFields.contents = []
|
|
203
|
-
subscriptionFields.contents = []
|
|
204
|
-
typeDefinitions.contents = []
|
|
205
|
-
activeSchema.contents = None
|
|
206
|
-
lastFullSdl.contents = None
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
let getFullSdl = () => lastFullSdl.contents
|
|
210
|
-
let getSchema = () => activeSchema.contents
|
|
211
|
-
|
|
212
|
-
let diagnostics = (): diagnostics => {
|
|
213
|
-
let mutFieldNames =
|
|
214
|
-
mutationFields.contents->Array.map(extractFieldName)->Array.filter(n => n != "_noop")
|
|
215
|
-
let queryFieldNames =
|
|
216
|
-
queryFields.contents->Array.map(extractFieldName)->Array.filter(n => n != "_noop")
|
|
217
|
-
let mutResolverNames = mutationResolvers.contents->Dict.keysToArray
|
|
218
|
-
let queryResolverNames = queryResolvers.contents->Dict.keysToArray
|
|
219
|
-
|
|
220
|
-
let mismatches: array<string> = []
|
|
221
|
-
|
|
222
|
-
let mutResolverSet = Set.fromArray(mutResolverNames)
|
|
223
|
-
mutFieldNames->Array.forEach(name =>
|
|
224
|
-
if !(mutResolverSet->Set.has(name)) {
|
|
225
|
-
mismatches->Array.push(`Mutation "${name}": SDL field but no resolver`)
|
|
226
|
-
}
|
|
227
|
-
)
|
|
228
|
-
let queryResolverSet = Set.fromArray(queryResolverNames)
|
|
229
|
-
queryFieldNames->Array.forEach(name =>
|
|
230
|
-
if !(queryResolverSet->Set.has(name)) {
|
|
231
|
-
mismatches->Array.push(`Query "${name}": SDL field but no resolver`)
|
|
232
|
-
}
|
|
233
|
-
)
|
|
234
|
-
let mutFieldSet = Set.fromArray(mutFieldNames)
|
|
235
|
-
mutResolverNames->Array.forEach(name =>
|
|
236
|
-
if !(mutFieldSet->Set.has(name)) {
|
|
237
|
-
mismatches->Array.push(`Mutation "${name}": resolver but no SDL field`)
|
|
238
|
-
}
|
|
239
|
-
)
|
|
240
|
-
let queryFieldSet = Set.fromArray(queryFieldNames)
|
|
241
|
-
queryResolverNames->Array.forEach(name =>
|
|
242
|
-
if !(queryFieldSet->Set.has(name)) {
|
|
243
|
-
mismatches->Array.push(`Query "${name}": resolver but no SDL field`)
|
|
244
|
-
}
|
|
245
|
-
)
|
|
246
|
-
let typeNames =
|
|
247
|
-
typeDefinitions.contents->Array.map(ReventlessCore.GraphQL_Stitcher.extractLeadingName)
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
registeredTypeDefinitions: typeNames,
|
|
251
|
-
registeredMutationFields: mutFieldNames,
|
|
252
|
-
registeredQueryFields: queryFieldNames,
|
|
253
|
-
registeredMutationResolvers: mutResolverNames,
|
|
254
|
-
registeredQueryResolvers: queryResolverNames,
|
|
255
|
-
typeCount: typeNames->Array.length,
|
|
256
|
-
sdlMutationCount: mutFieldNames->Array.length,
|
|
257
|
-
sdlQueryCount: queryFieldNames->Array.length,
|
|
258
|
-
resolverMutationCount: mutResolverNames->Array.length,
|
|
259
|
-
resolverQueryCount: queryResolverNames->Array.length,
|
|
260
|
-
mismatches,
|
|
261
|
-
fullSdl: lastFullSdl.contents,
|
|
262
|
-
serverRunning: activeServer.contents->Option.isSome,
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
let printDiagnostics = () => {
|
|
267
|
-
let p = s => log.info(~comp=label, s)
|
|
268
|
-
let d = diagnostics()
|
|
269
|
-
p("Diagnostics")
|
|
270
|
-
p(` Types: ${d.typeCount->Int.toString}`)
|
|
271
|
-
p(
|
|
272
|
-
` Mutations: ${d.sdlMutationCount->Int.toString} SDL fields, ${d.resolverMutationCount->Int.toString} resolvers`,
|
|
273
|
-
)
|
|
274
|
-
p(
|
|
275
|
-
` Queries: ${d.sdlQueryCount->Int.toString} SDL fields, ${d.resolverQueryCount->Int.toString} resolvers`,
|
|
276
|
-
)
|
|
277
|
-
if d.mismatches->Array.length > 0 {
|
|
278
|
-
p(` Mismatches (${d.mismatches->Array.length->Int.toString}):`)
|
|
279
|
-
d.mismatches->Array.forEach(m => p(` - ${m}`))
|
|
280
|
-
} else {
|
|
281
|
-
p(" No mismatches")
|
|
282
|
-
}
|
|
283
|
-
p(` Server running: ${d.serverRunning ? "yes" : "no"}`)
|
|
284
|
-
switch d.fullSdl {
|
|
285
|
-
| Some(sdl) =>
|
|
286
|
-
p("")
|
|
287
|
-
p("--- Full SDL ---")
|
|
288
|
-
sdl->String.split("\n")->Array.forEach(line => p(line))
|
|
289
|
-
p("--- End ---")
|
|
290
|
-
| None => p(" No SDL recorded")
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
{
|
|
295
|
-
registerMutations,
|
|
296
|
-
registerQueries,
|
|
297
|
-
registerSubscriptions,
|
|
298
|
-
registerTypes,
|
|
299
|
-
getMutationResolver,
|
|
300
|
-
getQueryResolver,
|
|
301
|
-
start,
|
|
302
|
-
stop,
|
|
303
|
-
reset,
|
|
304
|
-
buildSdl,
|
|
305
|
-
getFullSdl,
|
|
306
|
-
getSchema,
|
|
307
|
-
diagnostics,
|
|
308
|
-
printDiagnostics,
|
|
309
|
-
}
|
|
310
|
-
}
|