@znight/graphql 0.0.0 → 0.2.1

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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1346 -0
  3. package/dist/compiler.d.mts +114 -0
  4. package/dist/compiler.mjs +4599 -0
  5. package/dist/compiler.mjs.map +1 -0
  6. package/dist/index.d.mts +583 -0
  7. package/dist/index.mjs +134 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/names-BlzDM5cF.mjs +111 -0
  10. package/dist/names-BlzDM5cF.mjs.map +1 -0
  11. package/dist/plugin-CR_wVfRU.mjs +3445 -0
  12. package/dist/plugin-CR_wVfRU.mjs.map +1 -0
  13. package/dist/testing.d.mts +101 -0
  14. package/dist/testing.mjs +306 -0
  15. package/dist/testing.mjs.map +1 -0
  16. package/dist/types-DQ713UgI.d.mts +266 -0
  17. package/package.json +97 -1
  18. package/src/compiler/analyze.ts +727 -0
  19. package/src/compiler/builder.ts +1561 -0
  20. package/src/compiler/checks.ts +105 -0
  21. package/src/compiler/conformance.ts +1068 -0
  22. package/src/compiler/constants.ts +80 -0
  23. package/src/compiler/data.ts +83 -0
  24. package/src/compiler/defaults.ts +92 -0
  25. package/src/compiler/discriminators.ts +53 -0
  26. package/src/compiler/docs.ts +55 -0
  27. package/src/compiler/generate.ts +419 -0
  28. package/src/compiler/graphql-js.ts +42 -0
  29. package/src/compiler/ids.ts +128 -0
  30. package/src/compiler/index.ts +98 -0
  31. package/src/compiler/locate.ts +82 -0
  32. package/src/compiler/model.ts +137 -0
  33. package/src/compiler/options.ts +85 -0
  34. package/src/compiler/scalars.ts +138 -0
  35. package/src/compiler/schema-first.ts +595 -0
  36. package/src/compiler/sdl-source.ts +529 -0
  37. package/src/compiler/sdl-view.ts +61 -0
  38. package/src/compiler/suggest.ts +39 -0
  39. package/src/context.ts +181 -0
  40. package/src/decisions.ts +169 -0
  41. package/src/decorators.ts +202 -0
  42. package/src/errors.ts +89 -0
  43. package/src/execution.ts +1064 -0
  44. package/src/format.ts +285 -0
  45. package/src/index.ts +89 -0
  46. package/src/limits.ts +177 -0
  47. package/src/loaders.ts +149 -0
  48. package/src/memory-pubsub.ts +133 -0
  49. package/src/metadata.ts +222 -0
  50. package/src/names.ts +47 -0
  51. package/src/options.ts +331 -0
  52. package/src/pipeline.ts +338 -0
  53. package/src/plugin.ts +579 -0
  54. package/src/pubsub.ts +36 -0
  55. package/src/results.ts +206 -0
  56. package/src/scalars.ts +138 -0
  57. package/src/schema-hash.ts +18 -0
  58. package/src/schema.ts +635 -0
  59. package/src/sdl.ts +530 -0
  60. package/src/testing.ts +454 -0
  61. package/src/tokens.ts +22 -0
  62. package/src/transport/abort.ts +80 -0
  63. package/src/transport/accept.ts +155 -0
  64. package/src/transport/graphiql.ts +106 -0
  65. package/src/transport/http.ts +373 -0
  66. package/src/transport/sse.ts +45 -0
  67. package/src/type-ref.ts +89 -0
  68. package/src/types.ts +225 -0
  69. package/src/validate-args.ts +48 -0
package/src/schema.ts ADDED
@@ -0,0 +1,635 @@
1
+ /**
2
+ * The `GraphQLSchema` of `metadata.extensions.graphql` (RFC 0005 §7.3, §8.1): structural checks of
3
+ * the section (it can be hand-written), then graphql-js 17 constructors (thunks for cycles, every
4
+ * type passed in metadata order so that `printSchema()` matches `printSdl()`), then
5
+ * `validateSchema`. Every problem is collected and reported as one ZNIGHT066 error.
6
+ *
7
+ * Resolve functions are not decided here: the caller supplies them per resolver-backed field
8
+ * (`pipeline.ts`), so this module stays about the shape of the schema.
9
+ */
10
+ import type { ZnightMetadata } from '@znight/core'
11
+ import type {
12
+ GraphQLArgumentConfig,
13
+ GraphQLEnumValueConfig,
14
+ GraphQLFieldConfig,
15
+ GraphQLFieldResolver,
16
+ GraphQLInputFieldConfig,
17
+ GraphQLInputType,
18
+ GraphQLNamedType,
19
+ GraphQLOutputType,
20
+ } from 'graphql'
21
+ import type {
22
+ GraphQLArgumentDefinition,
23
+ GraphQLDiscriminator,
24
+ GraphQLFieldDefinition,
25
+ GraphQLInputTypeDefinition,
26
+ GraphQLInterfaceTypeDefinition,
27
+ GraphQLMetadata,
28
+ GraphQLObjectTypeDefinition,
29
+ GraphQLResolverBinding,
30
+ GraphQLTypeDefinition,
31
+ } from './metadata.ts'
32
+ import type { GraphQLTypeRef } from './type-ref.ts'
33
+ import type { ScalarDefinition } from './types.ts'
34
+
35
+ import { ErrorCode, ZnightError } from '@znight/core'
36
+ import {
37
+ GraphQLBoolean,
38
+ GraphQLEnumType,
39
+ GraphQLFloat,
40
+ GraphQLID,
41
+ GraphQLInputObjectType,
42
+ GraphQLInt,
43
+ GraphQLInterfaceType,
44
+ GraphQLList,
45
+ GraphQLNonNull,
46
+ GraphQLObjectType,
47
+ GraphQLScalarType,
48
+ GraphQLSchema,
49
+ GraphQLString,
50
+ GraphQLUnionType,
51
+ validateSchema,
52
+ } from 'graphql'
53
+
54
+ import { GRAPHQL_METADATA_VERSION } from './metadata.ts'
55
+ import { BUILTIN_SCALARS, isGraphQLName, RESERVED_ENUM_VALUES, SPECIFIED_SCALARS } from './names.ts'
56
+ import { builtinScalar, definitionScalar } from './scalars.ts'
57
+ import { parseTypeRef } from './type-ref.ts'
58
+
59
+ /** The resolve functions of a resolver-backed field, decided by the caller (`pipeline.ts`). */
60
+ export interface FieldFunctions {
61
+ readonly resolve?: GraphQLFieldResolver<unknown, unknown>
62
+ /** Root subscription fields: the source event stream. */
63
+ readonly subscribe?: GraphQLFieldResolver<unknown, unknown>
64
+ }
65
+
66
+ /** Builds the resolve functions of the field `coordinate` (`Query.books`) backed by `binding`. */
67
+ export type FieldFunctionsFactory = (
68
+ binding: GraphQLResolverBinding,
69
+ coordinate: string,
70
+ field: GraphQLFieldDefinition,
71
+ parent: GraphQLObjectTypeDefinition,
72
+ ) => FieldFunctions
73
+
74
+ /** The graphql section of the metadata, or ZNIGHT066 when the application has none. */
75
+ export function graphqlSection(metadata: ZnightMetadata): GraphQLMetadata {
76
+ const section: unknown = metadata.extensions?.graphql
77
+ if (section === undefined) {
78
+ throw new ZnightError(
79
+ ErrorCode.InvalidGraphQLSetup,
80
+ 'GraphQLPlugin: the metadata has no GraphQL schema (metadata.extensions.graphql)',
81
+ {
82
+ hints: [
83
+ 'Declare at least one @Resolver() class with a @Query() method',
84
+ 'Regenerate the metadata (znight generate) when resolvers exist',
85
+ ],
86
+ },
87
+ )
88
+ }
89
+ return section as GraphQLMetadata
90
+ }
91
+
92
+ const OPERATIONS: ReadonlySet<string> = new Set(['query', 'mutation', 'subscription', 'field'])
93
+ const INPUT_KINDS: ReadonlySet<string> = new Set(['input', 'enum', 'scalar'])
94
+ const OUTPUT_KINDS: ReadonlySet<string> = new Set(['object', 'interface', 'enum', 'scalar', 'union'])
95
+
96
+ function isRecord(value: unknown): value is Record<string, unknown> {
97
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
98
+ }
99
+
100
+ function show(value: unknown): string {
101
+ if (typeof value === 'function') return 'a function'
102
+ try {
103
+ return JSON.stringify(value) ?? String(value)
104
+ } catch {
105
+ return typeof value
106
+ }
107
+ }
108
+
109
+ /**
110
+ * The structural problems of a graphql section (empty when it can be built): version, roots,
111
+ * names, kinds, type references, resolver bindings, discriminators, scalars and groups.
112
+ */
113
+ export function checkGraphQLSection(section: unknown): string[] {
114
+ const problems: string[] = []
115
+ if (!isRecord(section)) return [`expected an object (received ${show(section)})`]
116
+ if (section['version'] !== GRAPHQL_METADATA_VERSION) {
117
+ problems.push(
118
+ `unsupported version ${show(section['version'])}: this @znight/graphql reads version ${GRAPHQL_METADATA_VERSION} (rebuild the application with the same @znight/graphql version)`,
119
+ )
120
+ return problems
121
+ }
122
+ if (typeof section['schemaHash'] !== 'string') problems.push('schemaHash must be a string')
123
+ const types = section['types']
124
+ if (!Array.isArray(types)) {
125
+ problems.push('types must be an array')
126
+ return problems
127
+ }
128
+ const kinds = new Map<string, string>()
129
+ for (const [index, type] of types.entries()) {
130
+ if (!isRecord(type) || typeof type['name'] !== 'string' || typeof type['kind'] !== 'string') {
131
+ problems.push(`types[${index}] must be { kind, name, ... } (received ${show(type)})`)
132
+ continue
133
+ }
134
+ const name = type['name']
135
+ if (!isGraphQLName(name)) problems.push(`invalid type name ${show(name)}`)
136
+ else if (SPECIFIED_SCALARS.has(name)) problems.push(`the specified scalar ${name} must not be declared`)
137
+ else if (kinds.has(name)) problems.push(`the type ${name} is declared twice`)
138
+ kinds.set(name, type['kind'])
139
+ }
140
+ const kindOf = (name: string): string | undefined => (SPECIFIED_SCALARS.has(name) ? 'scalar' : kinds.get(name))
141
+ const checkRef = (where: string, text: unknown, allowed: ReadonlySet<string>, position: string): void => {
142
+ const ref = typeof text === 'string' ? parseTypeRef(text) : undefined
143
+ if (ref === undefined) {
144
+ problems.push(`${where}: invalid type reference ${show(text)}`)
145
+ return
146
+ }
147
+ let named: GraphQLTypeRef = ref
148
+ while (named.kind === 'list') named = named.of
149
+ const kind = kindOf(named.name)
150
+ if (kind === undefined) problems.push(`${where}: unknown type ${named.name}`)
151
+ else if (!allowed.has(kind))
152
+ problems.push(`${where}: ${named.name} (${kind}) cannot be used as an ${position} type`)
153
+ }
154
+ const checkInputValues = (where: string, values: unknown, required: boolean): void => {
155
+ if (!Array.isArray(values) || (required && values.length === 0)) {
156
+ problems.push(`${where} must be a ${required ? 'non-empty ' : ''}array`)
157
+ return
158
+ }
159
+ const seen = new Set<string>()
160
+ for (const value of values) {
161
+ if (!isRecord(value) || typeof value['name'] !== 'string') {
162
+ problems.push(`${where}: expected { name, type } (received ${show(value)})`)
163
+ continue
164
+ }
165
+ const name = value['name']
166
+ if (!isGraphQLName(name)) problems.push(`${where}: invalid name ${show(name)}`)
167
+ if (seen.has(name)) problems.push(`${where}: ${name} is declared twice`)
168
+ seen.add(name)
169
+ checkRef(`${where}.${name}`, value['type'], INPUT_KINDS, 'input')
170
+ }
171
+ }
172
+ for (const type of types) {
173
+ if (!isRecord(type) || typeof type['name'] !== 'string') continue
174
+ const name = type['name']
175
+ switch (type['kind']) {
176
+ case 'object':
177
+ case 'interface': {
178
+ const isInterface = type['kind'] === 'interface'
179
+ checkInterfaces(name, type['interfaces'], kinds, problems)
180
+ const fields = type['fields']
181
+ if (!Array.isArray(fields) || fields.length === 0) {
182
+ problems.push(`${name}: fields must be a non-empty array`)
183
+ break
184
+ }
185
+ const seen = new Set<string>()
186
+ for (const field of fields) {
187
+ if (!isRecord(field) || typeof field['name'] !== 'string') {
188
+ problems.push(`${name}: expected fields { name, type } (received ${show(field)})`)
189
+ continue
190
+ }
191
+ const coordinate = `${name}.${field['name']}`
192
+ if (!isGraphQLName(field['name'])) problems.push(`${name}: invalid field name ${show(field['name'])}`)
193
+ if (seen.has(field['name'])) problems.push(`${coordinate} is declared twice`)
194
+ seen.add(field['name'])
195
+ checkRef(coordinate, field['type'], OUTPUT_KINDS, 'output')
196
+ if (field['args'] !== undefined) checkInputValues(`${coordinate} arguments`, field['args'], false)
197
+ if (field['resolver'] === undefined) continue
198
+ if (isInterface)
199
+ problems.push(`${coordinate}: interface fields have no resolver (bind it on the object types)`)
200
+ else checkBinding(coordinate, field['resolver'], problems)
201
+ }
202
+ if (isInterface) {
203
+ const implementations = types.filter(
204
+ other =>
205
+ isRecord(other) &&
206
+ other['kind'] === 'object' &&
207
+ Array.isArray(other['interfaces']) &&
208
+ other['interfaces'].includes(name),
209
+ )
210
+ checkDiscriminator(
211
+ name,
212
+ type['discriminator'],
213
+ implementations.map(other => (other as Record<string, unknown>)['name']),
214
+ problems,
215
+ )
216
+ }
217
+ break
218
+ }
219
+ case 'input':
220
+ checkInputValues(`${name} fields`, type['fields'], true)
221
+ break
222
+ case 'enum': {
223
+ const values = type['values']
224
+ if (!Array.isArray(values) || values.length === 0) {
225
+ problems.push(`${name}: values must be a non-empty array`)
226
+ break
227
+ }
228
+ const seen = new Set<string>()
229
+ for (const value of values) {
230
+ const valueName = isRecord(value) ? value['name'] : undefined
231
+ if (
232
+ !isRecord(value) ||
233
+ typeof valueName !== 'string' ||
234
+ !isGraphQLName(valueName) ||
235
+ RESERVED_ENUM_VALUES.has(valueName) ||
236
+ (typeof value['value'] !== 'string' && typeof value['value'] !== 'number')
237
+ ) {
238
+ problems.push(`${name}: invalid enum value ${show(value)} (expected { name, value: string | number })`)
239
+ continue
240
+ }
241
+ if (seen.has(valueName)) problems.push(`${name}.${valueName} is declared twice`)
242
+ seen.add(valueName)
243
+ }
244
+ break
245
+ }
246
+ case 'union': {
247
+ const members = type['members']
248
+ if (!Array.isArray(members) || members.length === 0) {
249
+ problems.push(`${name}: members must be a non-empty array`)
250
+ break
251
+ }
252
+ for (const member of members) {
253
+ if (typeof member !== 'string' || kinds.get(member) !== 'object') {
254
+ problems.push(`${name}: the member ${show(member)} is not an object type`)
255
+ }
256
+ }
257
+ checkDiscriminator(name, type['discriminator'], members, problems)
258
+ break
259
+ }
260
+ case 'scalar':
261
+ if (type['builtin'] === true) {
262
+ if (!BUILTIN_SCALARS.has(name))
263
+ problems.push(`${name} is not a built-in scalar (${[...BUILTIN_SCALARS.keys()].join(', ')})`)
264
+ } else {
265
+ const scalar = type['scalar']
266
+ if (scalar instanceof GraphQLScalarType) {
267
+ if (scalar.name !== name) {
268
+ problems.push(
269
+ `the scalar ${name} is implemented by the graphql-js scalar ${scalar.name} (names must match)`,
270
+ )
271
+ }
272
+ } else if (
273
+ !isRecord(scalar) ||
274
+ typeof scalar['serialize'] !== 'function' ||
275
+ typeof scalar['parseValue'] !== 'function' ||
276
+ (scalar['parseLiteral'] !== undefined && typeof scalar['parseLiteral'] !== 'function')
277
+ ) {
278
+ problems.push(
279
+ `the scalar ${name} needs a definition { serialize, parseValue, parseLiteral? } or a GraphQLScalarType (received ${show(scalar)})`,
280
+ )
281
+ }
282
+ }
283
+ for (const key of ['description', 'specifiedByURL'] as const) {
284
+ if (type[key] !== undefined && typeof type[key] !== 'string')
285
+ problems.push(`${name}: ${key} must be a string`)
286
+ }
287
+ if (type['sdl'] !== undefined && type['sdl'] !== true) problems.push(`${name}: sdl must be true when present`)
288
+ break
289
+ default:
290
+ problems.push(`${name}: unknown kind ${show(type['kind'])}`)
291
+ }
292
+ }
293
+ checkRoots(section['roots'], kinds, types, problems)
294
+ checkGroups(section['groups'], problems)
295
+ return problems
296
+ }
297
+
298
+ /** `interfaces` of an object or interface type: absent, or interface type names. */
299
+ function checkInterfaces(
300
+ name: string,
301
+ interfaces: unknown,
302
+ kinds: ReadonlyMap<string, string>,
303
+ problems: string[],
304
+ ): void {
305
+ if (interfaces === undefined) return
306
+ if (!Array.isArray(interfaces)) {
307
+ problems.push(`${name}: interfaces must be an array of interface type names (received ${show(interfaces)})`)
308
+ return
309
+ }
310
+ for (const entry of interfaces) {
311
+ if (typeof entry !== 'string' || kinds.get(entry) !== 'interface') {
312
+ problems.push(`${name}: ${show(entry)} is not an interface type`)
313
+ }
314
+ }
315
+ }
316
+
317
+ function checkBinding(coordinate: string, binding: unknown, problems: string[]): void {
318
+ if (!isRecord(binding)) {
319
+ problems.push(`${coordinate}: resolver must be an object (received ${show(binding)})`)
320
+ return
321
+ }
322
+ const invalid: string[] = []
323
+ if (typeof binding['token'] !== 'string' || binding['token'] === '') invalid.push('token')
324
+ if (typeof binding['method'] !== 'string') invalid.push('method')
325
+ if (typeof binding['operation'] !== 'string' || !OPERATIONS.has(binding['operation'])) invalid.push('operation')
326
+ const guards = binding['guards']
327
+ if (!Array.isArray(guards) || !guards.every(guard => typeof guard === 'string' && guard !== ''))
328
+ invalid.push('guards')
329
+ if (!isRecord(binding['meta'])) invalid.push('meta')
330
+ if (typeof binding['invoke'] !== 'function') invalid.push('invoke')
331
+ if (invalid.length > 0) problems.push(`${coordinate}: invalid resolver binding (${invalid.join(', ')})`)
332
+ }
333
+
334
+ function checkDiscriminator(
335
+ name: string,
336
+ discriminator: unknown,
337
+ members: readonly unknown[],
338
+ problems: string[],
339
+ ): void {
340
+ const memberSet = new Set(members)
341
+ if (!isRecord(discriminator)) {
342
+ problems.push(`${name}: discriminator must be an object (received ${show(discriminator)})`)
343
+ return
344
+ }
345
+ switch (discriminator['by']) {
346
+ case 'typename':
347
+ return
348
+ case 'property': {
349
+ const values = discriminator['values']
350
+ if (typeof discriminator['property'] !== 'string' || !isRecord(values)) {
351
+ problems.push(`${name}: a property discriminator needs { property, values }`)
352
+ return
353
+ }
354
+ for (const member of Object.values(values)) {
355
+ if (!memberSet.has(member)) problems.push(`${name}: the discriminator names ${show(member)}, not a member`)
356
+ }
357
+ return
358
+ }
359
+ case 'class': {
360
+ const classes = discriminator['classes']
361
+ if (!Array.isArray(classes)) {
362
+ problems.push(`${name}: a class discriminator needs classes: [Class, member][]`)
363
+ return
364
+ }
365
+ for (const entry of classes) {
366
+ if (!Array.isArray(entry) || typeof entry[0] !== 'function' || !memberSet.has(entry[1])) {
367
+ problems.push(`${name}: invalid class discriminator entry ${show(entry)} (expected [Class, member])`)
368
+ }
369
+ }
370
+ return
371
+ }
372
+ default:
373
+ problems.push(`${name}: unknown discriminator ${show(discriminator['by'])}`)
374
+ }
375
+ }
376
+
377
+ function checkRoots(
378
+ roots: unknown,
379
+ kinds: ReadonlyMap<string, string>,
380
+ types: readonly unknown[],
381
+ problems: string[],
382
+ ): void {
383
+ if (!isRecord(roots) || typeof roots['query'] !== 'string') {
384
+ problems.push(`roots must be { query, mutation?, subscription? } (received ${show(roots)})`)
385
+ return
386
+ }
387
+ for (const operation of ['query', 'mutation', 'subscription'] as const) {
388
+ const root = roots[operation]
389
+ if (root === undefined) continue
390
+ if (typeof root !== 'string' || kinds.get(root) !== 'object') {
391
+ problems.push(`the ${operation} root ${show(root)} is not an object type`)
392
+ }
393
+ }
394
+ const subscription = roots['subscription']
395
+ if (typeof subscription !== 'string') return
396
+ for (const type of types) {
397
+ if (!isRecord(type) || type['name'] !== subscription || !Array.isArray(type['fields'])) continue
398
+ for (const field of type['fields'] as unknown[]) {
399
+ if (!isRecord(field)) continue
400
+ const binding = field['resolver']
401
+ if (!isRecord(binding) || binding['operation'] !== 'subscription') {
402
+ problems.push(`${subscription}.${show(field['name'])} needs a subscription resolver (its source event stream)`)
403
+ }
404
+ }
405
+ }
406
+ }
407
+
408
+ function checkGroups(groups: unknown, problems: string[]): void {
409
+ if (groups === undefined) return
410
+ if (!isRecord(groups)) {
411
+ problems.push(`groups must be an object (received ${show(groups)})`)
412
+ return
413
+ }
414
+ for (const [group, entry] of Object.entries(groups)) {
415
+ const keys = isRecord(entry) ? entry['keys'] : undefined
416
+ if (
417
+ !isRecord(entry) ||
418
+ !Array.isArray(keys) ||
419
+ keys.length === 0 ||
420
+ !keys.every(key => typeof key === 'string' && key !== '') ||
421
+ typeof entry['required'] !== 'boolean'
422
+ ) {
423
+ problems.push(`group ${show(group)} must be { keys: [non-empty strings], required: boolean }`)
424
+ }
425
+ }
426
+ }
427
+
428
+ /** Throws one ZNIGHT066 error listing `problems`. */
429
+ export function invalidSchema(problems: readonly string[]): never {
430
+ throw new ZnightError(
431
+ ErrorCode.InvalidGraphQLSetup,
432
+ `GraphQLPlugin: invalid GraphQL schema (metadata.extensions.graphql), ${problems.length} ${problems.length === 1 ? 'problem' : 'problems'}:\n${problems.map(problem => ` - ${problem}`).join('\n')}`,
433
+ {
434
+ hints: [
435
+ 'Regenerate the metadata with the @znight/graphql version the application runs (znight generate)',
436
+ 'Hand-written metadata must follow GraphQLMetadata (@znight/graphql, RFC 0005 §7.1)',
437
+ ],
438
+ },
439
+ )
440
+ }
441
+
442
+ /** The `resolveType` of a union or an interface, from its discriminator: data read once, at boot. */
443
+ export function typeResolver(discriminator: GraphQLDiscriminator): (value: unknown) => string | undefined {
444
+ switch (discriminator.by) {
445
+ case 'property': {
446
+ const property = discriminator.property
447
+ const values = new Map(Object.entries(discriminator.values))
448
+ return value =>
449
+ value !== null && typeof value === 'object'
450
+ ? (values.get(String((value as Record<string, unknown>)[property])) ?? typenameOf(value))
451
+ : undefined
452
+ }
453
+ case 'class': {
454
+ const classes = discriminator.classes
455
+ return value => {
456
+ for (const [cls, name] of classes) if (value instanceof cls) return name
457
+ return typenameOf(value)
458
+ }
459
+ }
460
+ default:
461
+ return typenameOf
462
+ }
463
+ }
464
+
465
+ function typenameOf(value: unknown): string | undefined {
466
+ if (value === null || typeof value !== 'object') return undefined
467
+ const name = (value as { __typename?: unknown }).__typename
468
+ return typeof name === 'string' ? name : undefined
469
+ }
470
+
471
+ /**
472
+ * Builds the schema of a section. `functions` decides the resolve functions of every
473
+ * resolver-backed field; plain property fields keep the graphql-js default resolver.
474
+ *
475
+ * @throws ZnightError ZNIGHT066 listing every structural, construction and `validateSchema` problem.
476
+ */
477
+ export function buildGraphQLSchema(section: GraphQLMetadata, functions: FieldFunctionsFactory): GraphQLSchema {
478
+ const structural = checkGraphQLSection(section)
479
+ if (structural.length > 0) invalidSchema(structural)
480
+ const named = new Map<string, GraphQLNamedType>([
481
+ ['String', GraphQLString],
482
+ ['Int', GraphQLInt],
483
+ ['Float', GraphQLFloat],
484
+ ['Boolean', GraphQLBoolean],
485
+ ['ID', GraphQLID],
486
+ ])
487
+ const problems: string[] = []
488
+ const lookup = (name: string): GraphQLNamedType => named.get(name) as GraphQLNamedType
489
+ const typeOf = (ref: GraphQLTypeRef): GraphQLInputType & GraphQLOutputType => {
490
+ const inner = ref.kind === 'named' ? lookup(ref.name) : new GraphQLList(typeOf(ref.of))
491
+ return (ref.nonNull ? new GraphQLNonNull(inner) : inner) as GraphQLInputType & GraphQLOutputType
492
+ }
493
+ // references were checked by checkGraphQLSection
494
+ const typeRef = (text: string): GraphQLInputType & GraphQLOutputType => typeOf(parseTypeRef(text) as GraphQLTypeRef)
495
+ const inputValue = (value: GraphQLArgumentDefinition): GraphQLArgumentConfig & GraphQLInputFieldConfig => ({
496
+ type: typeRef(value.type),
497
+ ...(value.description === undefined ? {} : { description: value.description }),
498
+ ...(value.deprecationReason === undefined ? {} : { deprecationReason: value.deprecationReason }),
499
+ // external value (enum values by name): what printSdl() assumes (sdl.ts)
500
+ ...(value.defaultValue === undefined ? {} : { default: { value: value.defaultValue } }),
501
+ })
502
+ const fieldConfig = (
503
+ type: GraphQLObjectTypeDefinition | GraphQLInterfaceTypeDefinition,
504
+ field: GraphQLFieldDefinition,
505
+ ): GraphQLFieldConfig<unknown, unknown> => {
506
+ const config: GraphQLFieldConfig<unknown, unknown> = {
507
+ type: typeRef(field.type),
508
+ ...(field.description === undefined ? {} : { description: field.description }),
509
+ ...(field.deprecationReason === undefined ? {} : { deprecationReason: field.deprecationReason }),
510
+ ...(field.args === undefined || field.args.length === 0
511
+ ? {}
512
+ : { args: Object.fromEntries(field.args.map(arg => [arg.name, inputValue(arg)])) }),
513
+ }
514
+ if (field.resolver === undefined || type.kind === 'interface') return config
515
+ const resolved = functions(field.resolver, `${type.name}.${field.name}`, field, type)
516
+ return {
517
+ ...config,
518
+ ...(resolved.resolve === undefined ? {} : { resolve: resolved.resolve }),
519
+ ...(resolved.subscribe === undefined ? {} : { subscribe: resolved.subscribe }),
520
+ }
521
+ }
522
+ try {
523
+ for (const type of section.types) named.set(type.name, createType(type, fieldConfig, inputValue, lookup))
524
+ } catch (error) {
525
+ invalidSchema([`cannot build the schema: ${error instanceof Error ? error.message : String(error)}`])
526
+ }
527
+ const root = (name: string | undefined): GraphQLObjectType | undefined =>
528
+ name === undefined ? undefined : (lookup(name) as GraphQLObjectType)
529
+ let schema: GraphQLSchema | undefined
530
+ try {
531
+ schema = new GraphQLSchema({
532
+ query: root(section.roots.query),
533
+ mutation: root(section.roots.mutation),
534
+ subscription: root(section.roots.subscription),
535
+ // metadata order (sorted by name): printSchema() follows it, like printSdl()
536
+ types: section.types.map(type => lookup(type.name)),
537
+ })
538
+ for (const error of validateSchema(schema)) problems.push(error.message)
539
+ } catch (error) {
540
+ // thunks run here: a resolver factory (a missing provider keeps its own code) or a constructor
541
+ if (error instanceof ZnightError) throw error
542
+ problems.push(error instanceof Error ? error.message : String(error))
543
+ }
544
+ if (problems.length > 0 || schema === undefined) invalidSchema(problems)
545
+ return schema
546
+ }
547
+
548
+ function createType(
549
+ type: GraphQLTypeDefinition,
550
+ fieldConfig: (
551
+ type: GraphQLObjectTypeDefinition | GraphQLInterfaceTypeDefinition,
552
+ field: GraphQLFieldDefinition,
553
+ ) => GraphQLFieldConfig<unknown, unknown>,
554
+ inputValue: (value: GraphQLArgumentDefinition) => GraphQLInputFieldConfig,
555
+ lookup: (name: string) => GraphQLNamedType,
556
+ ): GraphQLNamedType {
557
+ const description = 'description' in type && type.description !== undefined ? { description: type.description } : {}
558
+ switch (type.kind) {
559
+ case 'object':
560
+ return new GraphQLObjectType({
561
+ name: type.name,
562
+ ...description,
563
+ ...interfacesOf(type, lookup),
564
+ // thunk: object types reference each other (cycles)
565
+ fields: () => Object.fromEntries(type.fields.map(field => [field.name, fieldConfig(type, field)])),
566
+ })
567
+ case 'interface':
568
+ return new GraphQLInterfaceType({
569
+ name: type.name,
570
+ ...description,
571
+ ...interfacesOf(type, lookup),
572
+ fields: () => Object.fromEntries(type.fields.map(field => [field.name, fieldConfig(type, field)])),
573
+ resolveType: typeResolver(type.discriminator),
574
+ })
575
+ case 'input':
576
+ return new GraphQLInputObjectType({
577
+ name: type.name,
578
+ ...description,
579
+ fields: () =>
580
+ Object.fromEntries((type as GraphQLInputTypeDefinition).fields.map(field => [field.name, inputValue(field)])),
581
+ })
582
+ case 'enum':
583
+ return new GraphQLEnumType({
584
+ name: type.name,
585
+ ...description,
586
+ values: Object.fromEntries(
587
+ type.values.map((value): [string, GraphQLEnumValueConfig] => [
588
+ value.name,
589
+ {
590
+ value: value.value,
591
+ ...(value.description === undefined ? {} : { description: value.description }),
592
+ ...(value.deprecationReason === undefined ? {} : { deprecationReason: value.deprecationReason }),
593
+ },
594
+ ]),
595
+ ),
596
+ })
597
+ case 'union':
598
+ return new GraphQLUnionType({
599
+ name: type.name,
600
+ ...description,
601
+ types: () => type.members.map(member => lookup(member) as GraphQLObjectType),
602
+ resolveType: typeResolver(type.discriminator),
603
+ })
604
+ default: {
605
+ if ('builtin' in type) return builtinScalar(type.name) as GraphQLScalarType
606
+ // `sdl`: the entry's description and specifiedByURL replace the definition's, absent ones too
607
+ const sdl = type.sdl === true
608
+ const overrides = {
609
+ ...(type.description === undefined && !sdl ? {} : { description: type.description }),
610
+ ...(type.specifiedByURL === undefined && !sdl ? {} : { specifiedByURL: type.specifiedByURL }),
611
+ }
612
+ const scalar = type.scalar
613
+ if (scalar instanceof GraphQLScalarType) {
614
+ if (Object.keys(overrides).length === 0) return scalar
615
+ const config = scalar.toConfig()
616
+ return new GraphQLScalarType({
617
+ ...config,
618
+ description: 'description' in overrides ? overrides.description : config.description,
619
+ specifiedByURL: 'specifiedByURL' in overrides ? overrides.specifiedByURL : config.specifiedByURL,
620
+ })
621
+ }
622
+ return definitionScalar(type.name, scalar as ScalarDefinition<unknown>, overrides)
623
+ }
624
+ }
625
+ }
626
+
627
+ /** The `interfaces` thunk of an object or interface type (none: no key, as before RFC 0007). */
628
+ function interfacesOf(
629
+ type: GraphQLObjectTypeDefinition | GraphQLInterfaceTypeDefinition,
630
+ lookup: (name: string) => GraphQLNamedType,
631
+ ): { interfaces?: () => GraphQLInterfaceType[] } {
632
+ const names = type.interfaces
633
+ if (names === undefined || names.length === 0) return {}
634
+ return { interfaces: () => names.map(name => lookup(name) as GraphQLInterfaceType) }
635
+ }