@reventlessdev/reventless-local 3.0.0-alpha.152 → 3.0.0-alpha.154
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 +14 -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 +334 -132
- package/src/adapter/DomainGraphQL_Server.res.mjs +274 -126
- 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,59 +382,191 @@ 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)
|
|
556
|
+
// Route yoga's own logging into the framework logger. error/warn ALWAYS surface
|
|
557
|
+
// (so a masked resolver failure — which yoga reports via logger.error with the
|
|
558
|
+
// original Error even when the client response is masked — is never swallowed
|
|
559
|
+
// again), while verbose debug/info stay gated on GRAPHQL_DEBUG.
|
|
560
|
+
let yogaLogging: YG.yogaLogger = {
|
|
561
|
+
debug: a => if debug { log.debug(~comp="GraphQL:Domain", YG.logArgToString(a)) },
|
|
562
|
+
info: a => if debug { log.info(~comp="GraphQL:Domain", YG.logArgToString(a)) },
|
|
563
|
+
warn: a => log.warn(~comp="GraphQL:Domain", YG.logArgToString(a)),
|
|
564
|
+
error: a => log.error(~comp="GraphQL:Domain", YG.logArgToString(a)),
|
|
565
|
+
}
|
|
324
566
|
let yoga = YG.createYogaWithContext({
|
|
325
567
|
"schema": schema,
|
|
326
568
|
"graphiql": true,
|
|
327
|
-
"logging":
|
|
569
|
+
"logging": yogaLogging,
|
|
328
570
|
"maskedErrors": !debug,
|
|
329
571
|
"context": buildAuthContext,
|
|
330
572
|
})
|
|
@@ -350,69 +592,11 @@ let stop = () =>
|
|
|
350
592
|
| None => ()
|
|
351
593
|
}
|
|
352
594
|
|
|
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).
|
|
595
|
+
// Reset registry state (call between isolated test suites). Clears all scope
|
|
596
|
+
// buckets and restores the default "platform" scope.
|
|
408
597
|
let reset = () => {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
subscriptionResolvers.contents = Dict.make()
|
|
412
|
-
mutationFields.contents = []
|
|
413
|
-
queryFields.contents = []
|
|
414
|
-
subscriptionFields.contents = []
|
|
415
|
-
typeDefinitions.contents = []
|
|
598
|
+
buckets.contents = Dict.make()
|
|
599
|
+
currentScope.contents = platformScope
|
|
416
600
|
nodeTypeRegistry.contents = Dict.make()
|
|
417
601
|
nodeResolverCallback.contents = None
|
|
418
602
|
activeSchema.contents = None
|
|
@@ -426,8 +610,8 @@ let getRegisteredSdl = () => buildSdl()
|
|
|
426
610
|
type resolverNames = {mutations: array<string>, queries: array<string>}
|
|
427
611
|
|
|
428
612
|
let getRegisteredResolverNames = (): resolverNames => {
|
|
429
|
-
mutations: mutationResolvers
|
|
430
|
-
queries: queryResolvers
|
|
613
|
+
mutations: orderedBuckets()->Array.flatMap(((_, b)) => b.mutationResolvers->Dict.keysToArray),
|
|
614
|
+
queries: orderedBuckets()->Array.flatMap(((_, b)) => b.queryResolvers->Dict.keysToArray),
|
|
431
615
|
}
|
|
432
616
|
|
|
433
617
|
let getFullSdl = () => lastFullSdl.contents
|
|
@@ -460,12 +644,19 @@ let extractFieldName = (sdlField: string): string => {
|
|
|
460
644
|
}
|
|
461
645
|
|
|
462
646
|
let diagnostics = (): diagnostics => {
|
|
647
|
+
let allBuckets = orderedBuckets()->Array.map(((_, b)) => b)
|
|
463
648
|
let mutFieldNames =
|
|
464
|
-
|
|
649
|
+
allBuckets
|
|
650
|
+
->Array.flatMap(b => b.mutationFields)
|
|
651
|
+
->Array.map(extractFieldName)
|
|
652
|
+
->Array.filter(n => n != "_noop")
|
|
465
653
|
let queryFieldNames =
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
654
|
+
allBuckets
|
|
655
|
+
->Array.flatMap(b => b.queryFields)
|
|
656
|
+
->Array.map(extractFieldName)
|
|
657
|
+
->Array.filter(n => n != "_noop")
|
|
658
|
+
let mutResolverNames = allBuckets->Array.flatMap(b => b.mutationResolvers->Dict.keysToArray)
|
|
659
|
+
let queryResolverNames = allBuckets->Array.flatMap(b => b.queryResolvers->Dict.keysToArray)
|
|
469
660
|
|
|
470
661
|
let mismatches: array<string> = []
|
|
471
662
|
|
|
@@ -497,7 +688,18 @@ let diagnostics = (): diagnostics => {
|
|
|
497
688
|
}
|
|
498
689
|
)
|
|
499
690
|
|
|
500
|
-
|
|
691
|
+
// Identical type blocks registered in multiple scopes count once (they
|
|
692
|
+
// dedupe in the composed schema).
|
|
693
|
+
let seenTypeBlocks = Set.make()
|
|
694
|
+
let typeNames: array<string> = []
|
|
695
|
+
allBuckets->Array.forEach(b =>
|
|
696
|
+
b.typeDefinitions->Array.forEach(t =>
|
|
697
|
+
if !(seenTypeBlocks->Set.has(t)) {
|
|
698
|
+
seenTypeBlocks->Set.add(t)
|
|
699
|
+
typeNames->Array.push(ReventlessCore.GraphQL_Stitcher.extractLeadingName(t))
|
|
700
|
+
}
|
|
701
|
+
)
|
|
702
|
+
)
|
|
501
703
|
|
|
502
704
|
{
|
|
503
705
|
registeredTypeDefinitions: typeNames,
|