@reventlessdev/reventless-local 3.0.0-alpha.152 → 3.0.0-alpha.153
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 +10 -10
- package/src/Platform.res +40 -184
- package/src/Platform.res.mjs +54 -353
- package/src/adapter/Api/LocalGraphQL_Adapter.res +1 -11
- package/src/adapter/Api/LocalGraphQL_Adapter.res.mjs +0 -7
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res +6 -6
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_GraphQL.res.mjs +3 -5
- package/src/adapter/DomainGraphQL_Server.res +323 -131
- package/src/adapter/DomainGraphQL_Server.res.mjs +252 -123
- package/src/adapter/GraphQL_Server.res.mjs +41 -17
- package/tests/adapter/DomainGraphQL_ServerTest.res +161 -0
- package/tests/adapter/DomainGraphQL_ServerTest.res.mjs +192 -0
- package/tests/plugin/ApiFragmentRegistryBehavior_GWT.res +0 -131
- package/tests/plugin/ApiFragmentRegistryBehavior_GWT.res.mjs +0 -268
- package/tests/plugin/ApiFragmentsProjection_GWT.res +0 -108
- package/tests/plugin/ApiFragmentsProjection_GWT.res.mjs +0 -172
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// LocalGraphQL_Adapter — implements ReventlessInfra.Api_Adapter.Provider for
|
|
2
2
|
// the in-memory graphql-yoga server.
|
|
3
3
|
//
|
|
4
|
-
// makeApiResource: no-op (server lifecycle is managed by DomainGraphQL_Server.start
|
|
4
|
+
// makeApiResource: no-op (server lifecycle is managed by DomainGraphQL_Server.start)
|
|
5
5
|
// generateFragment: delegates to ReventlessCore.GraphQL_FragmentGenerator
|
|
6
|
-
// updateSchema: rebuilds the stitched SDL via DomainGraphQL_Server.rebuildSchema
|
|
7
6
|
|
|
8
7
|
type api = unit
|
|
9
8
|
type role = unit
|
|
@@ -18,12 +17,3 @@ let generateFragment = (
|
|
|
18
17
|
~queryEntries: array<ReventlessInfra.Api.querySchemaEntry>,
|
|
19
18
|
): Reventless.Plugin.apiSchemaFragment =>
|
|
20
19
|
ReventlessCore.GraphQL_FragmentGenerator.generate(~mutationEntries, ~queryEntries)
|
|
21
|
-
|
|
22
|
-
let updateSchema = (
|
|
23
|
-
~api as _: Pulumi.Output.t<api>,
|
|
24
|
-
~baseFragment: Reventless.Plugin.apiSchemaFragment,
|
|
25
|
-
~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
|
|
26
|
-
): promise<unit> => {
|
|
27
|
-
DomainGraphQL_Server.rebuildSchema(~baseFragment, ~pluginFragments)
|
|
28
|
-
Promise.resolve()
|
|
29
|
-
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Pulumi from "@pulumi/pulumi";
|
|
4
|
-
import * as DomainGraphQL_Server$ReventlessLocal from "../DomainGraphQL_Server.res.mjs";
|
|
5
4
|
import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
|
|
6
5
|
|
|
7
6
|
function makeApiResource(param, param$1) {
|
|
@@ -13,14 +12,8 @@ function makeApiResource(param, param$1) {
|
|
|
13
12
|
|
|
14
13
|
let generateFragment = GraphQL_FragmentGenerator$ReventlessCore.generate;
|
|
15
14
|
|
|
16
|
-
function updateSchema(param, baseFragment, pluginFragments) {
|
|
17
|
-
DomainGraphQL_Server$ReventlessLocal.rebuildSchema(baseFragment, pluginFragments);
|
|
18
|
-
return Promise.resolve();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
export {
|
|
22
16
|
makeApiResource,
|
|
23
17
|
generateFragment,
|
|
24
|
-
updateSchema,
|
|
25
18
|
}
|
|
26
19
|
/* @pulumi/pulumi Not a pure module */
|
|
@@ -100,15 +100,15 @@ let capitalize = s =>
|
|
|
100
100
|
// the AWS stitched schema and the in-memory graphql-yoga server agree on the
|
|
101
101
|
// CommandResult shape. We join the 4 declarations into a single SDL block for
|
|
102
102
|
// the in-memory server's registerTypes API.
|
|
103
|
-
// Registered
|
|
103
|
+
// Registered unconditionally: registerTypes dedupes identical blocks, so on
|
|
104
|
+
// the scoped domain server every plugin scope gets its own copy (keeping each
|
|
105
|
+
// subgraph standalone-valid, mirroring the AWS source-API SDL) while repeat
|
|
106
|
+
// registrations within one scope/instance collapse to a single definition.
|
|
104
107
|
|
|
105
108
|
let commandResultSdl = GraphQL_FragmentGenerator.commandResultSdlTypes->Array.join("\n\n")
|
|
106
109
|
|
|
107
|
-
let ensureCommandResultTypes = (server: ReventlessGraphqlServer.GraphQL_ServerInstance.t) =>
|
|
108
|
-
|
|
109
|
-
server.registerTypes(~sdlTypes=[commandResultSdl])
|
|
110
|
-
}
|
|
111
|
-
}
|
|
110
|
+
let ensureCommandResultTypes = (server: ReventlessGraphqlServer.GraphQL_ServerInstance.t) =>
|
|
111
|
+
server.registerTypes(~sdlTypes=[commandResultSdl])
|
|
112
112
|
|
|
113
113
|
// Re-export the shared encoder. Source of truth is
|
|
114
114
|
// CommandTopic_Helpers.commandOutcomeToJson — same encoder is also reused by
|
|
@@ -106,9 +106,7 @@ function capitalize(s) {
|
|
|
106
106
|
let commandResultSdl = GraphQL_FragmentGenerator$ReventlessCore.commandResultSdlTypes.join("\n\n");
|
|
107
107
|
|
|
108
108
|
function ensureCommandResultTypes(server) {
|
|
109
|
-
|
|
110
|
-
return server.registerTypes([commandResultSdl]);
|
|
111
|
-
}
|
|
109
|
+
server.registerTypes([commandResultSdl]);
|
|
112
110
|
}
|
|
113
111
|
|
|
114
112
|
function extractCommandName(fieldName) {
|
|
@@ -143,7 +141,7 @@ function deriveSdlField(fieldName, variantSchema) {
|
|
|
143
141
|
let handlerRefs = {};
|
|
144
142
|
|
|
145
143
|
function register(fields, commandSchema, commandAuthorization, server) {
|
|
146
|
-
|
|
144
|
+
server.registerTypes([commandResultSdl]);
|
|
147
145
|
let sdlFields = fields.map(field => {
|
|
148
146
|
let variantIndex = variantIndexForField(commandSchema, field);
|
|
149
147
|
let sdl = deriveSdlField(field, extractVariantSchema(commandSchema, variantIndex));
|
|
@@ -197,7 +195,7 @@ function register(fields, commandSchema, commandAuthorization, server) {
|
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
function registerDcb(fieldName, commandSchema, commandAuthorization, server) {
|
|
200
|
-
|
|
198
|
+
server.registerTypes([commandResultSdl]);
|
|
201
199
|
let constructorNames = DcbTag$Reventless.extractAllVariantNames(commandSchema);
|
|
202
200
|
let variantIndex = variantIndexForField(commandSchema, fieldName);
|
|
203
201
|
let variantSchema = extractVariantSchema(commandSchema, variantIndex >= 0 ? variantIndex : 0);
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
// - Relay Global ID encoding/decoding (encodeGlobalId / decodeGlobalId)
|
|
9
9
|
// - Node type registry (registerNodeType / registerNodeResolverCallback)
|
|
10
10
|
// - /sdl HTTP endpoint for Relay compiler
|
|
11
|
-
// - rebuildSchema for schema stitching
|
|
12
11
|
//
|
|
13
12
|
// Note: the old setRegistrationTarget redirect hack has been removed. Routing
|
|
14
13
|
// to the platform server is now done explicitly via resolveTargetGraphQL() in
|
|
@@ -119,7 +118,7 @@ let _isInvalidBearer = (req: nodeRequest): bool =>
|
|
|
119
118
|
| _ => false
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
// Shared dispatch for start()
|
|
121
|
+
// Shared dispatch for start(). Order matters: built-in
|
|
123
122
|
// endpoints win over the yoga catch-all so they aren't shadowed by graphql.
|
|
124
123
|
let _dispatch = (req: nodeRequest, res: nodeResponse, yoga: YG.yoga, getSdl: unit => string): unit => {
|
|
125
124
|
// Strip query string before path matching.
|
|
@@ -185,44 +184,155 @@ let decodeGlobalId = (globalId: string): option<(string, string)> =>
|
|
|
185
184
|
| _ => None
|
|
186
185
|
}
|
|
187
186
|
|
|
188
|
-
// --
|
|
187
|
+
// -- Scoped registry ---------------------------------------------------------
|
|
188
|
+
// Mirrors the AWS plugin=source-API, platform=merged-API model: every
|
|
189
|
+
// registration lands in the bucket of the *current scope*. The default
|
|
190
|
+
// "platform" scope holds admin/base registrations; Platform.res wraps each
|
|
191
|
+
// plugin's construction in its own scope (setScope/relabelScope/resetScope)
|
|
192
|
+
// so a plugin's SDL fragment + resolvers form a standalone subgraph that is
|
|
193
|
+
// validated in isolation and composed at start().
|
|
194
|
+
|
|
195
|
+
let platformScope = "platform"
|
|
196
|
+
|
|
197
|
+
type bucket = {
|
|
198
|
+
mutable mutationFields: array<string>,
|
|
199
|
+
mutable queryFields: array<string>,
|
|
200
|
+
mutable subscriptionFields: array<string>,
|
|
201
|
+
mutable typeDefinitions: array<string>,
|
|
202
|
+
mutationResolvers: dict<resolverFn>,
|
|
203
|
+
queryResolvers: dict<resolverFn>,
|
|
204
|
+
subscriptionResolvers: dict<resolverFn>,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let makeBucket = (~scope: string): bucket => {
|
|
208
|
+
mutationFields: [],
|
|
209
|
+
queryFields: [],
|
|
210
|
+
subscriptionFields: [],
|
|
211
|
+
// Non-platform (plugin) buckets are seeded with the Relay base types so
|
|
212
|
+
// their standalone documents are self-contained — mirrors the AWS path
|
|
213
|
+
// where stitchStandalone embeds the shared types in every subgraph SDL.
|
|
214
|
+
// Identical copies dedupe in the final merge.
|
|
215
|
+
typeDefinitions: scope == platformScope
|
|
216
|
+
? []
|
|
217
|
+
: ReventlessCore.GraphQL_Stitcher.relayBaseTypes->Array.copy,
|
|
218
|
+
mutationResolvers: Dict.make(),
|
|
219
|
+
queryResolvers: Dict.make(),
|
|
220
|
+
subscriptionResolvers: Dict.make(),
|
|
221
|
+
}
|
|
189
222
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let
|
|
223
|
+
// Insertion order = composition order (platform bucket is forced first in
|
|
224
|
+
// orderedBuckets regardless of creation order).
|
|
225
|
+
let buckets: ref<dict<bucket>> = ref(Dict.make())
|
|
226
|
+
let currentScope: ref<string> = ref(platformScope)
|
|
227
|
+
|
|
228
|
+
let bucketFor = (scope: string): bucket =>
|
|
229
|
+
switch buckets.contents->Dict.get(scope) {
|
|
230
|
+
| Some(b) => b
|
|
231
|
+
| None =>
|
|
232
|
+
let b = makeBucket(~scope)
|
|
233
|
+
buckets.contents->Dict.set(scope, b)
|
|
234
|
+
b
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let currentBucket = () => bucketFor(currentScope.contents)
|
|
193
238
|
|
|
194
|
-
|
|
195
|
-
let
|
|
196
|
-
|
|
197
|
-
let
|
|
239
|
+
/** Route subsequent registrations into the named scope's bucket. */
|
|
240
|
+
let setScope = (name: string) => {
|
|
241
|
+
currentScope.contents = name
|
|
242
|
+
let _ = bucketFor(name)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Route subsequent registrations back into the default "platform" bucket. */
|
|
246
|
+
let resetScope = () => currentScope.contents = platformScope
|
|
247
|
+
|
|
248
|
+
/** Rename a scope bucket. Platform.res scopes a plugin's construction with a
|
|
249
|
+
token (the plugin name is only known after construction) and relabels the
|
|
250
|
+
bucket to the plugin name afterwards. Merges into `to_` if it exists. */
|
|
251
|
+
let relabelScope = (~from: string, ~to_: string) =>
|
|
252
|
+
if from != to_ {
|
|
253
|
+
switch buckets.contents->Dict.get(from) {
|
|
254
|
+
| None => ()
|
|
255
|
+
| Some(b) =>
|
|
256
|
+
buckets.contents->Dict.delete(from)
|
|
257
|
+
switch buckets.contents->Dict.get(to_) {
|
|
258
|
+
| None => buckets.contents->Dict.set(to_, b)
|
|
259
|
+
| Some(existing) =>
|
|
260
|
+
existing.mutationFields = existing.mutationFields->Array.concat(b.mutationFields)
|
|
261
|
+
existing.queryFields = existing.queryFields->Array.concat(b.queryFields)
|
|
262
|
+
existing.subscriptionFields =
|
|
263
|
+
existing.subscriptionFields->Array.concat(b.subscriptionFields)
|
|
264
|
+
b.typeDefinitions->Array.forEach(t =>
|
|
265
|
+
if !(existing.typeDefinitions->Array.includes(t)) {
|
|
266
|
+
existing.typeDefinitions->Array.push(t)
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
b.mutationResolvers
|
|
270
|
+
->Dict.toArray
|
|
271
|
+
->Array.forEach(((k, v)) => existing.mutationResolvers->Dict.set(k, v))
|
|
272
|
+
b.queryResolvers
|
|
273
|
+
->Dict.toArray
|
|
274
|
+
->Array.forEach(((k, v)) => existing.queryResolvers->Dict.set(k, v))
|
|
275
|
+
b.subscriptionResolvers
|
|
276
|
+
->Dict.toArray
|
|
277
|
+
->Array.forEach(((k, v)) => existing.subscriptionResolvers->Dict.set(k, v))
|
|
278
|
+
}
|
|
279
|
+
if currentScope.contents == from {
|
|
280
|
+
currentScope.contents = to_
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Platform bucket first, then the plugin buckets in creation order.
|
|
286
|
+
let orderedBuckets = (): array<(string, bucket)> => {
|
|
287
|
+
let entries = buckets.contents->Dict.toArray
|
|
288
|
+
entries
|
|
289
|
+
->Array.filter(((scope, _)) => scope == platformScope)
|
|
290
|
+
->Array.concat(entries->Array.filter(((scope, _)) => scope != platformScope))
|
|
291
|
+
}
|
|
198
292
|
|
|
199
293
|
let registerMutations = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
|
|
200
|
-
|
|
201
|
-
|
|
294
|
+
let b = currentBucket()
|
|
295
|
+
b.mutationFields = b.mutationFields->Array.concat(sdlFields)
|
|
296
|
+
resolvers->Dict.toArray->Array.forEach(((k, v)) => b.mutationResolvers->Dict.set(k, v))
|
|
202
297
|
}
|
|
203
298
|
|
|
204
299
|
let registerQueries = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
|
|
205
|
-
|
|
206
|
-
|
|
300
|
+
let b = currentBucket()
|
|
301
|
+
b.queryFields = b.queryFields->Array.concat(sdlFields)
|
|
302
|
+
resolvers->Dict.toArray->Array.forEach(((k, v)) => b.queryResolvers->Dict.set(k, v))
|
|
207
303
|
}
|
|
208
304
|
|
|
209
305
|
let registerSubscriptions = (~sdlFields: array<string>, ~resolvers: dict<resolverFn>) => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
)
|
|
306
|
+
let b = currentBucket()
|
|
307
|
+
b.subscriptionFields = b.subscriptionFields->Array.concat(sdlFields)
|
|
308
|
+
resolvers->Dict.toArray->Array.forEach(((k, v)) => b.subscriptionResolvers->Dict.set(k, v))
|
|
214
309
|
}
|
|
215
310
|
|
|
216
|
-
/** Look up a registered mutation resolver by field name (used by MCP_Server).
|
|
311
|
+
/** Look up a registered mutation resolver by field name (used by MCP_Server).
|
|
312
|
+
Searches all scope buckets. */
|
|
217
313
|
let getMutationResolver = (fieldName: string): option<resolverFn> =>
|
|
218
|
-
|
|
314
|
+
buckets.contents
|
|
315
|
+
->Dict.valuesToArray
|
|
316
|
+
->Array.findMap(b => b.mutationResolvers->Dict.get(fieldName))
|
|
219
317
|
|
|
220
|
-
/** Look up a registered query resolver by field name (used by MCP_Server).
|
|
318
|
+
/** Look up a registered query resolver by field name (used by MCP_Server).
|
|
319
|
+
Searches all scope buckets. */
|
|
221
320
|
let getQueryResolver = (fieldName: string): option<resolverFn> =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
321
|
+
buckets.contents
|
|
322
|
+
->Dict.valuesToArray
|
|
323
|
+
->Array.findMap(b => b.queryResolvers->Dict.get(fieldName))
|
|
324
|
+
|
|
325
|
+
// Identical type blocks dedupe within a bucket, so shared-type registrations
|
|
326
|
+
// (CommandResult union family, Relay base types) can be repeated per scope —
|
|
327
|
+
// each plugin bucket carries its own copy and stays standalone-valid.
|
|
328
|
+
let registerTypes = (~sdlTypes: array<string>) => {
|
|
329
|
+
let b = currentBucket()
|
|
330
|
+
sdlTypes->Array.forEach(t =>
|
|
331
|
+
if !(b.typeDefinitions->Array.includes(t)) {
|
|
332
|
+
b.typeDefinitions->Array.push(t)
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
}
|
|
226
336
|
|
|
227
337
|
// -- Relay Node type registry -----------------------------------------------
|
|
228
338
|
// Maps GraphQL type names to QueryDb component names for node(id: ID!) resolution.
|
|
@@ -272,54 +382,176 @@ let activeServer: ref<option<YG.httpServer>> = ref(None)
|
|
|
272
382
|
let activeSchema: ref<option<YG.schema>> = ref(None)
|
|
273
383
|
let lastFullSdl: ref<option<string>> = ref(None)
|
|
274
384
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
let
|
|
288
|
-
|
|
385
|
+
// Shared SDL assembly for one bucket. `withRootDefaults` keeps the historical
|
|
386
|
+
// behavior of always emitting Query/Mutation (with a `_noop` placeholder when
|
|
387
|
+
// empty) — required for the platform bucket so the composed schema always has
|
|
388
|
+
// a Query type. Plugin buckets omit empty root types (their standalone
|
|
389
|
+
// documents mirror an AWS source-API schema).
|
|
390
|
+
let assembleBucketSdl = (
|
|
391
|
+
~typeDefinitions: array<string>,
|
|
392
|
+
~queryFields: array<string>,
|
|
393
|
+
~mutationFields: array<string>,
|
|
394
|
+
~subscriptionFields: array<string>,
|
|
395
|
+
~withRootDefaults: bool,
|
|
396
|
+
): string => {
|
|
397
|
+
let sections = []
|
|
398
|
+
if typeDefinitions->Array.length > 0 {
|
|
399
|
+
sections->Array.push(typeDefinitions->Array.join("\n\n"))
|
|
400
|
+
}
|
|
401
|
+
if withRootDefaults || queryFields->Array.length > 0 {
|
|
402
|
+
let queries =
|
|
403
|
+
queryFields->Array.length > 0 ? queryFields->Array.join("\n") : " _noop: String"
|
|
404
|
+
sections->Array.push(`type Query {\n${queries}\n}`)
|
|
405
|
+
}
|
|
406
|
+
if withRootDefaults || mutationFields->Array.length > 0 {
|
|
407
|
+
let mutations =
|
|
408
|
+
mutationFields->Array.length > 0 ? mutationFields->Array.join("\n") : " _noop: String"
|
|
409
|
+
sections->Array.push(`type Mutation {\n${mutations}\n}`)
|
|
410
|
+
}
|
|
411
|
+
if subscriptionFields->Array.length > 0 {
|
|
412
|
+
sections->Array.push(`type Subscription {\n${subscriptionFields->Array.join("\n")}\n}`)
|
|
413
|
+
}
|
|
414
|
+
sections->Array.join("\n\n")
|
|
289
415
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
416
|
+
|
|
417
|
+
// Standalone subgraph document for a plugin bucket (empty root types omitted).
|
|
418
|
+
let buildScopeSdl = (b: bucket): string =>
|
|
419
|
+
assembleBucketSdl(
|
|
420
|
+
~typeDefinitions=b.typeDefinitions,
|
|
421
|
+
~queryFields=b.queryFields,
|
|
422
|
+
~mutationFields=b.mutationFields,
|
|
423
|
+
~subscriptionFields=b.subscriptionFields,
|
|
424
|
+
~withRootDefaults=false,
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
// Aggregated SDL across all buckets (diagnostics / registered-SDL view).
|
|
428
|
+
// Identical type blocks registered in multiple buckets appear once.
|
|
429
|
+
let buildSdl = () => {
|
|
430
|
+
let seenTypes = Set.make()
|
|
431
|
+
let allTypes: array<string> = []
|
|
432
|
+
let allQueries: array<string> = []
|
|
433
|
+
let allMutations: array<string> = []
|
|
434
|
+
let allSubscriptions: array<string> = []
|
|
435
|
+
orderedBuckets()->Array.forEach(((_, b)) => {
|
|
436
|
+
b.typeDefinitions->Array.forEach(t =>
|
|
437
|
+
if !(seenTypes->Set.has(t)) {
|
|
438
|
+
seenTypes->Set.add(t)
|
|
439
|
+
allTypes->Array.push(t)
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
allQueries->Array.pushMany(b.queryFields)
|
|
443
|
+
allMutations->Array.pushMany(b.mutationFields)
|
|
444
|
+
allSubscriptions->Array.pushMany(b.subscriptionFields)
|
|
445
|
+
})
|
|
446
|
+
assembleBucketSdl(
|
|
447
|
+
~typeDefinitions=allTypes,
|
|
448
|
+
~queryFields=allQueries,
|
|
449
|
+
~mutationFields=allMutations,
|
|
450
|
+
~subscriptionFields=allSubscriptions,
|
|
451
|
+
~withRootDefaults=true,
|
|
452
|
+
)
|
|
302
453
|
}
|
|
303
454
|
|
|
304
455
|
// `~contextFactory` is accepted to satisfy the `ReventlessGraphqlServer.GraphQL_ServerInstance.t`
|
|
305
456
|
// interface but ignored — the data server always wires `buildAuthContext`
|
|
306
457
|
// internally so the bearer-token rules + /__inmemory/login + /sdl dispatch
|
|
307
458
|
// stay consistent across callers.
|
|
459
|
+
let exnMessage = (exn: exn): string =>
|
|
460
|
+
exn
|
|
461
|
+
->JsExn.fromException
|
|
462
|
+
->Option.flatMap(JsExn.message)
|
|
463
|
+
->Option.getOr("unknown error")
|
|
464
|
+
|
|
465
|
+
// Per-bucket root resolver map ({Query, Mutation, Subscription} — empty roots
|
|
466
|
+
// omitted except for the platform bucket which always carries Query/Mutation,
|
|
467
|
+
// matching its `_noop` root defaults).
|
|
468
|
+
let scopeResolverMap = (b: bucket, ~withRootDefaults: bool): dict<dict<resolverFn>> => {
|
|
469
|
+
let resolvers = Dict.make()
|
|
470
|
+
if withRootDefaults || b.queryResolvers->Dict.keysToArray->Array.length > 0 {
|
|
471
|
+
resolvers->Dict.set("Query", b.queryResolvers)
|
|
472
|
+
}
|
|
473
|
+
if withRootDefaults || b.mutationResolvers->Dict.keysToArray->Array.length > 0 {
|
|
474
|
+
resolvers->Dict.set("Mutation", b.mutationResolvers)
|
|
475
|
+
}
|
|
476
|
+
if b.subscriptionResolvers->Dict.keysToArray->Array.length > 0 {
|
|
477
|
+
resolvers->Dict.set("Subscription", b.subscriptionResolvers)
|
|
478
|
+
}
|
|
479
|
+
resolvers
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let scopeHasFields = (b: bucket): bool =>
|
|
483
|
+
b.mutationFields->Array.length > 0 ||
|
|
484
|
+
b.queryFields->Array.length > 0 ||
|
|
485
|
+
b.subscriptionFields->Array.length > 0
|
|
486
|
+
|
|
487
|
+
/** Validate every plugin bucket's standalone document, then compose the final
|
|
488
|
+
schema from one document + one resolver map per bucket (platform first) via
|
|
489
|
+
graphql-tools merge semantics — mirrors AWS source-API validation followed
|
|
490
|
+
by the merged-API build. Exposed for start() and tests. */
|
|
491
|
+
let composeSchema = (): YG.schema => {
|
|
492
|
+
// Standalone validation per plugin bucket — a plugin whose document
|
|
493
|
+
// references types it does not define itself fails HERE, with attribution,
|
|
494
|
+
// before the cross-plugin merge (mirrors AWS: each source API schema must
|
|
495
|
+
// be valid standalone).
|
|
496
|
+
orderedBuckets()->Array.forEach(((scope, b)) =>
|
|
497
|
+
if scope != platformScope && scopeHasFields(b) {
|
|
498
|
+
let doc = buildScopeSdl(b)
|
|
499
|
+
try {
|
|
500
|
+
let _ = YG.createSchema({
|
|
501
|
+
"typeDefs": doc,
|
|
502
|
+
"resolvers": scopeResolverMap(b, ~withRootDefaults=false),
|
|
503
|
+
})
|
|
504
|
+
} catch {
|
|
505
|
+
| e =>
|
|
506
|
+
JsError.throwWithMessage(
|
|
507
|
+
`Plugin "${scope}" subgraph document is not valid standalone: ${exnMessage(e)}`,
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
// Composition — graphql-tools mergeTypeDefs/mergeResolvers semantics:
|
|
514
|
+
// identical duplicate type definitions dedupe; conflicting same-named
|
|
515
|
+
// definitions throw naming the type (the local equivalent of MERGE_FAILED).
|
|
516
|
+
let platformBucket = bucketFor(platformScope)
|
|
517
|
+
let docs = [
|
|
518
|
+
assembleBucketSdl(
|
|
519
|
+
~typeDefinitions=platformBucket.typeDefinitions,
|
|
520
|
+
~queryFields=platformBucket.queryFields,
|
|
521
|
+
~mutationFields=platformBucket.mutationFields,
|
|
522
|
+
~subscriptionFields=platformBucket.subscriptionFields,
|
|
523
|
+
~withRootDefaults=true,
|
|
524
|
+
),
|
|
525
|
+
]
|
|
526
|
+
let resolverMaps = [scopeResolverMap(platformBucket, ~withRootDefaults=true)]
|
|
527
|
+
orderedBuckets()->Array.forEach(((scope, b)) =>
|
|
528
|
+
if scope != platformScope {
|
|
529
|
+
let doc = buildScopeSdl(b)
|
|
530
|
+
if doc->String.length > 0 {
|
|
531
|
+
docs->Array.push(doc)
|
|
532
|
+
resolverMaps->Array.push(scopeResolverMap(b, ~withRootDefaults=false))
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
)
|
|
536
|
+
try {
|
|
537
|
+
YG.createSchemaMulti({"typeDefs": docs, "resolvers": resolverMaps})
|
|
538
|
+
} catch {
|
|
539
|
+
| e =>
|
|
540
|
+
JsError.throwWithMessage(
|
|
541
|
+
`Cross-plugin schema merge failed (mirrors AWS MERGE_FAILED): ${exnMessage(e)}`,
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
308
546
|
let start = (~port: int=4000, ~contextFactory as _: option<YG.contextFactory>=?, ()) => {
|
|
309
|
-
// Register node resolver if callback is set
|
|
547
|
+
// Register node resolver if callback is set — the node()/Node registry is
|
|
548
|
+
// platform-owned (local keeps its working in-process node()).
|
|
310
549
|
switch buildNodeResolver() {
|
|
311
|
-
| Some((name, resolver)) => queryResolvers
|
|
550
|
+
| Some((name, resolver)) => bucketFor(platformScope).queryResolvers->Dict.set(name, resolver)
|
|
312
551
|
| None => ()
|
|
313
552
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
resolvers->Dict.set("Mutation", mutationResolvers.contents)
|
|
317
|
-
if subscriptionResolvers.contents->Dict.keysToArray->Array.length > 0 {
|
|
318
|
-
resolvers->Dict.set("Subscription", subscriptionResolvers.contents)
|
|
319
|
-
}
|
|
320
|
-
let sdl = buildSdl()
|
|
321
|
-
lastFullSdl.contents = Some(sdl)
|
|
322
|
-
let schema = YG.createSchema({"typeDefs": sdl, "resolvers": resolvers})
|
|
553
|
+
lastFullSdl.contents = Some(buildSdl())
|
|
554
|
+
let schema = composeSchema()
|
|
323
555
|
activeSchema.contents = Some(schema)
|
|
324
556
|
let yoga = YG.createYogaWithContext({
|
|
325
557
|
"schema": schema,
|
|
@@ -350,69 +582,11 @@ let stop = () =>
|
|
|
350
582
|
| None => ()
|
|
351
583
|
}
|
|
352
584
|
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
// then uses buildSdl() which includes types + Query + Mutation.
|
|
356
|
-
// Stops the existing server and restarts with the new SDL.
|
|
357
|
-
let rebuildSchema = (
|
|
358
|
-
~baseFragment: Reventless.Plugin.apiSchemaFragment,
|
|
359
|
-
~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
|
|
360
|
-
) => {
|
|
361
|
-
stop()
|
|
362
|
-
// Merge any fragment types not already registered via the hook
|
|
363
|
-
let allFragments = Array.concat([baseFragment], pluginFragments)
|
|
364
|
-
let fragmentTypes = allFragments->Array.flatMap(frag => ReventlessCore.GraphQL_Stitcher.decode(frag).types)
|
|
365
|
-
let existingNames = Set.fromArray(typeDefinitions.contents->Array.map(ReventlessCore.GraphQL_Stitcher.extractLeadingName))
|
|
366
|
-
fragmentTypes->Array.forEach(typeDef => {
|
|
367
|
-
let name = ReventlessCore.GraphQL_Stitcher.extractLeadingName(typeDef)
|
|
368
|
-
if !(existingNames->Set.has(name)) {
|
|
369
|
-
typeDefinitions.contents->Array.push(typeDef)
|
|
370
|
-
existingNames->Set.add(name)
|
|
371
|
-
}
|
|
372
|
-
})
|
|
373
|
-
// Register node resolver if callback is set
|
|
374
|
-
switch buildNodeResolver() {
|
|
375
|
-
| Some((name, resolver)) => queryResolvers.contents->Dict.set(name, resolver)
|
|
376
|
-
| None => ()
|
|
377
|
-
}
|
|
378
|
-
let fullSdl = buildSdl()
|
|
379
|
-
lastFullSdl.contents = Some(fullSdl)
|
|
380
|
-
let resolvers = Dict.make()
|
|
381
|
-
resolvers->Dict.set("Query", queryResolvers.contents)
|
|
382
|
-
resolvers->Dict.set("Mutation", mutationResolvers.contents)
|
|
383
|
-
if subscriptionResolvers.contents->Dict.keysToArray->Array.length > 0 {
|
|
384
|
-
resolvers->Dict.set("Subscription", subscriptionResolvers.contents)
|
|
385
|
-
}
|
|
386
|
-
let schema = YG.createSchema({"typeDefs": fullSdl, "resolvers": resolvers})
|
|
387
|
-
activeSchema.contents = Some(schema)
|
|
388
|
-
let yoga = YG.createYogaWithContext({
|
|
389
|
-
"schema": schema,
|
|
390
|
-
"graphiql": true,
|
|
391
|
-
"logging": debug,
|
|
392
|
-
"maskedErrors": !debug,
|
|
393
|
-
"context": buildAuthContext,
|
|
394
|
-
})
|
|
395
|
-
let getSdl = () =>
|
|
396
|
-
switch activeSchema.contents {
|
|
397
|
-
| Some(s) => YG.printSchema(s)
|
|
398
|
-
| None => lastFullSdl.contents->Option.getOr("")
|
|
399
|
-
}
|
|
400
|
-
let server = createServerWithHandler((req, res) => _dispatch(req, res, yoga, getSdl))
|
|
401
|
-
server->YG.listen(4000, () =>
|
|
402
|
-
log.info(~comp="GraphQL:Domain", "rebuilt schema - http://localhost:4000/graphql (SDL: /sdl)")
|
|
403
|
-
)
|
|
404
|
-
activeServer.contents = Some(server)
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Reset registry state (call between isolated test suites).
|
|
585
|
+
// Reset registry state (call between isolated test suites). Clears all scope
|
|
586
|
+
// buckets and restores the default "platform" scope.
|
|
408
587
|
let reset = () => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
subscriptionResolvers.contents = Dict.make()
|
|
412
|
-
mutationFields.contents = []
|
|
413
|
-
queryFields.contents = []
|
|
414
|
-
subscriptionFields.contents = []
|
|
415
|
-
typeDefinitions.contents = []
|
|
588
|
+
buckets.contents = Dict.make()
|
|
589
|
+
currentScope.contents = platformScope
|
|
416
590
|
nodeTypeRegistry.contents = Dict.make()
|
|
417
591
|
nodeResolverCallback.contents = None
|
|
418
592
|
activeSchema.contents = None
|
|
@@ -426,8 +600,8 @@ let getRegisteredSdl = () => buildSdl()
|
|
|
426
600
|
type resolverNames = {mutations: array<string>, queries: array<string>}
|
|
427
601
|
|
|
428
602
|
let getRegisteredResolverNames = (): resolverNames => {
|
|
429
|
-
mutations: mutationResolvers
|
|
430
|
-
queries: queryResolvers
|
|
603
|
+
mutations: orderedBuckets()->Array.flatMap(((_, b)) => b.mutationResolvers->Dict.keysToArray),
|
|
604
|
+
queries: orderedBuckets()->Array.flatMap(((_, b)) => b.queryResolvers->Dict.keysToArray),
|
|
431
605
|
}
|
|
432
606
|
|
|
433
607
|
let getFullSdl = () => lastFullSdl.contents
|
|
@@ -460,12 +634,19 @@ let extractFieldName = (sdlField: string): string => {
|
|
|
460
634
|
}
|
|
461
635
|
|
|
462
636
|
let diagnostics = (): diagnostics => {
|
|
637
|
+
let allBuckets = orderedBuckets()->Array.map(((_, b)) => b)
|
|
463
638
|
let mutFieldNames =
|
|
464
|
-
|
|
639
|
+
allBuckets
|
|
640
|
+
->Array.flatMap(b => b.mutationFields)
|
|
641
|
+
->Array.map(extractFieldName)
|
|
642
|
+
->Array.filter(n => n != "_noop")
|
|
465
643
|
let queryFieldNames =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
644
|
+
allBuckets
|
|
645
|
+
->Array.flatMap(b => b.queryFields)
|
|
646
|
+
->Array.map(extractFieldName)
|
|
647
|
+
->Array.filter(n => n != "_noop")
|
|
648
|
+
let mutResolverNames = allBuckets->Array.flatMap(b => b.mutationResolvers->Dict.keysToArray)
|
|
649
|
+
let queryResolverNames = allBuckets->Array.flatMap(b => b.queryResolvers->Dict.keysToArray)
|
|
469
650
|
|
|
470
651
|
let mismatches: array<string> = []
|
|
471
652
|
|
|
@@ -497,7 +678,18 @@ let diagnostics = (): diagnostics => {
|
|
|
497
678
|
}
|
|
498
679
|
)
|
|
499
680
|
|
|
500
|
-
|
|
681
|
+
// Identical type blocks registered in multiple scopes count once (they
|
|
682
|
+
// dedupe in the composed schema).
|
|
683
|
+
let seenTypeBlocks = Set.make()
|
|
684
|
+
let typeNames: array<string> = []
|
|
685
|
+
allBuckets->Array.forEach(b =>
|
|
686
|
+
b.typeDefinitions->Array.forEach(t =>
|
|
687
|
+
if !(seenTypeBlocks->Set.has(t)) {
|
|
688
|
+
seenTypeBlocks->Set.add(t)
|
|
689
|
+
typeNames->Array.push(ReventlessCore.GraphQL_Stitcher.extractLeadingName(t))
|
|
690
|
+
}
|
|
691
|
+
)
|
|
692
|
+
)
|
|
501
693
|
|
|
502
694
|
{
|
|
503
695
|
registeredTypeDefinitions: typeNames,
|