@znight/graphql 0.2.0 → 0.2.2

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 (42) hide show
  1. package/README.md +307 -11
  2. package/dist/compiler.d.mts +24 -3
  3. package/dist/compiler.mjs +1876 -109
  4. package/dist/compiler.mjs.map +1 -1
  5. package/dist/index.d.mts +49 -8
  6. package/dist/index.mjs +10 -4
  7. package/dist/index.mjs.map +1 -1
  8. package/dist/names-BlzDM5cF.mjs.map +1 -1
  9. package/dist/{plugin-BdBxr5NQ.mjs → plugin-CR_wVfRU.mjs} +247 -24
  10. package/dist/plugin-CR_wVfRU.mjs.map +1 -0
  11. package/dist/testing.d.mts +1 -1
  12. package/dist/testing.mjs +1 -1
  13. package/dist/{types-CgbV9LjD.d.mts → types-DQ713UgI.d.mts} +34 -2
  14. package/package.json +14 -9
  15. package/src/compiler/analyze.ts +129 -10
  16. package/src/compiler/builder.ts +6 -85
  17. package/src/compiler/checks.ts +105 -0
  18. package/src/compiler/conformance.ts +1068 -0
  19. package/src/compiler/constants.ts +6 -0
  20. package/src/compiler/data.ts +10 -0
  21. package/src/compiler/generate.ts +41 -27
  22. package/src/compiler/graphql-js.ts +42 -0
  23. package/src/compiler/index.ts +8 -3
  24. package/src/compiler/locate.ts +39 -1
  25. package/src/compiler/model.ts +16 -1
  26. package/src/compiler/options.ts +39 -8
  27. package/src/compiler/schema-first.ts +595 -0
  28. package/src/compiler/sdl-source.ts +529 -0
  29. package/src/compiler/sdl-view.ts +3 -1
  30. package/src/compiler/suggest.ts +39 -0
  31. package/src/decorators.ts +9 -3
  32. package/src/index.ts +5 -0
  33. package/src/metadata.ts +28 -1
  34. package/src/options.ts +23 -1
  35. package/src/pipeline.ts +66 -5
  36. package/src/plugin.ts +27 -5
  37. package/src/results.ts +206 -0
  38. package/src/scalars.ts +10 -3
  39. package/src/schema.ts +90 -12
  40. package/src/sdl.ts +17 -2
  41. package/src/types.ts +35 -0
  42. package/dist/plugin-BdBxr5NQ.mjs.map +0 -1
@@ -0,0 +1,595 @@
1
+ /**
2
+ * The schema-first model (RFC 0007): the SDL is the schema. `validate` loads and validates the
3
+ * `.graphql` files (`sdl-source.ts`), binds every resolver method to its field by name (coverage,
4
+ * §5.3), checks the resolver signatures against the SDL (`conformance.ts`, §5.4) and turns the
5
+ * validated graphql-js schema into the same {@link GraphQLModel} as code-first, so that `generate`,
6
+ * the SDL artifact, `schemaHash` and the runtime are shared.
7
+ */
8
+ import type { ClassInfo, Diagnostic, Location, MethodInfo, ValidateContext } from '@znight/compiler'
9
+ import type {
10
+ GraphQLArgument,
11
+ GraphQLEnumType,
12
+ GraphQLField,
13
+ GraphQLInputField,
14
+ GraphQLInputObjectType,
15
+ GraphQLInterfaceType,
16
+ GraphQLNamedType,
17
+ GraphQLObjectType,
18
+ GraphQLScalarType,
19
+ GraphQLSchema,
20
+ GraphQLUnionType,
21
+ } from 'graphql'
22
+ import type { Binding } from './conformance.ts'
23
+ import type { FieldData, GraphQLData, ResolverData } from './data.ts'
24
+ import type {
25
+ ArgModel,
26
+ FieldModel,
27
+ GraphQLModel,
28
+ InputModel,
29
+ InterfaceModel,
30
+ ObjectModel,
31
+ ScalarModel,
32
+ TypeModel,
33
+ UnionModel,
34
+ } from './model.ts'
35
+ import type { ScalarDeclaration } from './scalars.ts'
36
+ import type { SdlSource } from './sdl-source.ts'
37
+
38
+ import { BUILTIN_SCALARS, SPECIFIED_SCALARS } from '../names.ts'
39
+ import { schemaHash } from '../schema-hash.ts'
40
+ import { printSdl } from '../sdl.ts'
41
+ import { parseTypeRef } from '../type-ref.ts'
42
+ import { checkDecisions, checkLoaders } from './checks.ts'
43
+ import { checkConformance } from './conformance.ts'
44
+ import { Codes, compareStrings } from './constants.ts'
45
+ import { graphqlModules } from './graphql-js.ts'
46
+ import { readScalars } from './scalars.ts'
47
+ import { loadSdl } from './sdl-source.ts'
48
+ import { sdlType } from './sdl-view.ts'
49
+ import { closest } from './suggest.ts'
50
+
51
+ type Context = ValidateContext<GraphQLData>
52
+ type Operation = 'query' | 'mutation' | 'subscription'
53
+
54
+ const DECORATORS: Readonly<Record<FieldData['operation'], string>> = {
55
+ query: '@Query',
56
+ mutation: '@Mutation',
57
+ subscription: '@Subscription',
58
+ field: '@ResolveField',
59
+ }
60
+
61
+ /**
62
+ * Builds the model from the SDL files matching `patterns`; `undefined` after an error. `artifact`:
63
+ * the path of the SDL artifact (option `sdl`), never read back as an input.
64
+ */
65
+ export function buildSchemaFirstModel(
66
+ ctx: Context,
67
+ patterns: readonly string[],
68
+ artifact: string | false,
69
+ ): GraphQLModel | undefined {
70
+ // the artifact is the output: matched by the globs, it would define every type a second time
71
+ return new SchemaFirstBuilder(ctx).build(artifact === false ? patterns : [...patterns, `!${artifact}`])
72
+ }
73
+
74
+ class SchemaFirstBuilder {
75
+ readonly #ctx: Context
76
+ #errors = 0
77
+ readonly #methods = new Map<string, MethodInfo | undefined>()
78
+
79
+ constructor(ctx: Context) {
80
+ this.#ctx = ctx
81
+ }
82
+
83
+ build(patterns: readonly string[]): GraphQLModel | undefined {
84
+ const report = (diagnostic: Diagnostic): void => this.#report(diagnostic)
85
+ const stored = [...this.#ctx.stored].sort((a, b) => compareStrings(a.host.identity, b.host.identity))
86
+ const resolvers = stored.filter(resolver => resolver.invalid !== true)
87
+ const rejected = stored.filter(resolver => resolver.invalid === true)
88
+ checkLoaders(this.#ctx, resolvers, report)
89
+ const scalarDeclarations = readScalars(this.#ctx, report)
90
+ const sdl = loadSdl(this.#ctx, patterns, report)
91
+ if (sdl === undefined) {
92
+ checkDecisions(this.#ctx, resolvers, report)
93
+ return undefined
94
+ }
95
+ const schema = sdl.schema
96
+ const roots = {
97
+ query: (schema.getQueryType() as GraphQLObjectType).name,
98
+ ...(schema.getMutationType() ? { mutation: (schema.getMutationType() as GraphQLObjectType).name } : {}),
99
+ ...(schema.getSubscriptionType()
100
+ ? { subscription: (schema.getSubscriptionType() as GraphQLObjectType).name }
101
+ : {}),
102
+ }
103
+ checkDecisions(this.#ctx, resolvers, report, roots)
104
+ const types = Object.values(schema.getTypeMap())
105
+ .filter(type => !type.name.startsWith('__') && !SPECIFIED_SCALARS.has(type.name))
106
+ .sort((a, b) => compareStrings(a.name, b.name))
107
+ this.#checkUnsupported(sdl, types)
108
+ const scalars = this.#scalars(sdl, types, scalarDeclarations)
109
+ const { bindings, claimed: bound } = this.#bind(sdl, resolvers, rejected)
110
+ const conformance = checkConformance({
111
+ ctx: this.#ctx,
112
+ schema,
113
+ locate: node => sdl.locate(node),
114
+ scalars: new Map([...scalarDeclarations.values()].map(declaration => [declaration.name, declaration])),
115
+ bindings,
116
+ bound,
117
+ report,
118
+ })
119
+ if (this.#errors > 0) return undefined
120
+ const byCoordinate = new Map(bindings.map(binding => [`${binding.type.name}.${binding.sdlField.name}`, binding]))
121
+ const models: TypeModel[] = []
122
+ for (const type of types) {
123
+ const model = this.#model(sdl, type, byCoordinate, scalars, conformance)
124
+ if (model !== undefined) models.push(model)
125
+ }
126
+ const meta = resolvers.flatMap(resolver => resolver.fields.flatMap(field => field.meta))
127
+ const printed = printSdl({ roots, types: models.map(sdlType) })
128
+ return { roots, types: models, meta, sdl: printed, schemaHash: schemaHash(printed) }
129
+ }
130
+
131
+ #report(diagnostic: Diagnostic): void {
132
+ if (diagnostic.severity === 'error') this.#errors++
133
+ this.#ctx.report(diagnostic)
134
+ }
135
+
136
+ #mismatch(
137
+ location: Location | undefined,
138
+ message: string,
139
+ hints: readonly string[],
140
+ related?: Diagnostic['related'],
141
+ ): void {
142
+ this.#report({
143
+ code: Codes.SchemaMismatch,
144
+ severity: 'error',
145
+ message,
146
+ ...(location === undefined ? {} : { location }),
147
+ ...(hints.length === 0 ? {} : { hints }),
148
+ ...(related === undefined || related.length === 0 ? {} : { related }),
149
+ })
150
+ }
151
+
152
+ // ------------------------------------------------------------------------------- unsupported
153
+
154
+ /** `@oneOf` input objects (the runtime does not model them). */
155
+ #checkUnsupported(sdl: SdlSource, types: readonly GraphQLNamedType[]): void {
156
+ const { isInputObjectType } = graphqlModules().type
157
+ for (const type of types) {
158
+ if (!isInputObjectType(type) || !type.isOneOf) continue
159
+ const location = sdl.locate(type.astNode)
160
+ this.#report({
161
+ code: Codes.InvalidSdl,
162
+ severity: 'error',
163
+ message: `@oneOf input objects are not supported yet (${type.name})`,
164
+ ...(location === undefined ? {} : { location }),
165
+ hints: ['Declare one optional field per case and check that exactly one is set in the resolver'],
166
+ })
167
+ }
168
+ }
169
+
170
+ // ------------------------------------------------------------------------------------- scalars
171
+
172
+ /** The scalar models: `scalar<T>()` declarations by name, the built-in scalars, else ZNIGHT067. */
173
+ #scalars(
174
+ sdl: SdlSource,
175
+ types: readonly GraphQLNamedType[],
176
+ declarations: ReadonlyMap<string, ScalarDeclaration>,
177
+ ): ReadonlyMap<string, ScalarModel> {
178
+ const { isScalarType } = graphqlModules().type
179
+ const byName = new Map([...declarations.values()].map(declaration => [declaration.name, declaration]))
180
+ const models = new Map<string, ScalarModel>()
181
+ for (const type of types) {
182
+ if (!isScalarType(type)) continue
183
+ const location = sdl.locate(type.astNode)
184
+ const declaration = byName.get(type.name)
185
+ const description = type.description ?? undefined
186
+ const specifiedByURL = type.specifiedByURL ?? undefined
187
+ if (declaration !== undefined) {
188
+ models.set(type.name, {
189
+ kind: 'scalar',
190
+ name: type.name,
191
+ key: `#scalar:${declaration.identity}`,
192
+ description,
193
+ location,
194
+ definition: declaration.identity,
195
+ specifiedByURL,
196
+ fromSdl: true,
197
+ })
198
+ continue
199
+ }
200
+ const builtin = BUILTIN_SCALARS.get(type.name)
201
+ if (builtin !== undefined) {
202
+ if (description !== undefined || (specifiedByURL !== undefined && specifiedByURL !== builtin.specifiedByURL)) {
203
+ this.#report({
204
+ code: Codes.InvalidSdl,
205
+ severity: 'warning',
206
+ message: `scalar ${type.name} is the built-in scalar of @znight/graphql: its description and @specifiedBy in the SDL are ignored`,
207
+ ...(location === undefined ? {} : { location }),
208
+ hints: [
209
+ `Remove them, or implement the scalar yourself: export const ${type.name}Scalar = scalar<T>('${type.name}', { ... })`,
210
+ ],
211
+ })
212
+ }
213
+ models.set(type.name, {
214
+ kind: 'scalar',
215
+ name: type.name,
216
+ key: `#builtin:${type.name}`,
217
+ description: undefined,
218
+ location,
219
+ definition: undefined,
220
+ specifiedByURL: undefined,
221
+ })
222
+ continue
223
+ }
224
+ this.#report({
225
+ code: Codes.InvalidSdl,
226
+ severity: 'error',
227
+ message: `scalar ${type.name} has no implementation: declare it with scalar<T>('${type.name}', ...)`,
228
+ ...(location === undefined ? {} : { location }),
229
+ hints: [
230
+ `export const ${type.name}Scalar = scalar<${type.name}>('${type.name}', { serialize: value => value as ${type.name}, parseValue: value => value as ${type.name} }), with ${type.name} the TypeScript type of its values (the one graphql-codegen maps it to)`,
231
+ ],
232
+ })
233
+ }
234
+ return models
235
+ }
236
+
237
+ // ------------------------------------------------------------------------------------- binding
238
+
239
+ /**
240
+ * Binds every resolver method to its SDL field (coverage, RFC 0007 §5.3). `claimed`: the
241
+ * coordinates a resolver method names, bound or not (a method with an analysis error already
242
+ * reported), so that their fields are never also reported as missing a resolver.
243
+ */
244
+ #bind(
245
+ sdl: SdlSource,
246
+ resolvers: readonly ResolverData[],
247
+ rejected: readonly ResolverData[],
248
+ ): { bindings: Binding[]; claimed: Set<string> } {
249
+ const schema = sdl.schema
250
+ const bindings: Binding[] = []
251
+ const claimed = new Set<string>()
252
+ for (const resolver of rejected) {
253
+ for (const field of resolver.fields) {
254
+ const owner = field.operation === 'field' ? resolver.typeName?.name : rootType(schema, field.operation)?.name
255
+ if (owner !== undefined) claimed.add(`${owner}.${field.name}`)
256
+ }
257
+ }
258
+ const byCoordinate = new Map<string, Binding>()
259
+ for (const resolver of resolvers) {
260
+ const target = this.#resolverType(sdl, resolver)
261
+ for (const field of resolver.fields) {
262
+ const owner = field.operation === 'field' ? resolver.typeName?.name : rootType(schema, field.operation)?.name
263
+ if (owner !== undefined) claimed.add(`${owner}.${field.name}`)
264
+ if (!field.complete) continue
265
+ const method = this.#method(resolver, field)
266
+ if (method === undefined) continue
267
+ const where = `${resolver.host.displayName}.${field.method}`
268
+ let type: GraphQLObjectType | undefined
269
+ if (field.operation === 'field') {
270
+ type = target
271
+ if (type === undefined) continue
272
+ } else {
273
+ type = rootType(schema, field.operation)
274
+ if (type === undefined) {
275
+ const name = ROOTS[field.operation]
276
+ this.#mismatch(field.location, `${DECORATORS[field.operation]} ${where}: the schema has no ${name} type`, [
277
+ `Declare it in the SDL: type ${name} { ${field.name}: ... }`,
278
+ ])
279
+ continue
280
+ }
281
+ }
282
+ const sdlField = type.getFields()[field.name]
283
+ if (sdlField === undefined) {
284
+ this.#unknownField(schema, type, field, where)
285
+ continue
286
+ }
287
+ const coordinate = `${type.name}.${field.name}`
288
+ const previous = byCoordinate.get(coordinate)
289
+ if (previous !== undefined) {
290
+ this.#report({
291
+ code: Codes.NameConflict,
292
+ severity: 'error',
293
+ message: `Two resolvers bind ${coordinate}: ${where} and ${previous.resolver.host.displayName}.${previous.field.method}`,
294
+ location: field.location,
295
+ related: [{ message: 'the other resolver', location: previous.field.location }],
296
+ hints: ['Keep one of them'],
297
+ })
298
+ continue
299
+ }
300
+ this.#checkArguments(sdl, field, method, sdlField, coordinate, where)
301
+ const binding: Binding = { resolver, field, method, type, sdlField }
302
+ byCoordinate.set(coordinate, binding)
303
+ bindings.push(binding)
304
+ }
305
+ }
306
+ for (const operation of ['query', 'mutation', 'subscription'] as const) {
307
+ const root = rootType(schema, operation)
308
+ if (root === undefined) continue
309
+ for (const field of Object.values(root.getFields())) {
310
+ if (claimed.has(`${root.name}.${field.name}`)) continue
311
+ this.#mismatch(sdl.locate(field.astNode), `Root field ${root.name}.${field.name} has no resolver`, [
312
+ `Add a method to a @Resolver() class: ${DECORATORS[operation]}() ${field.name}(...) (or ${DECORATORS[operation]}('${field.name}') on another method)`,
313
+ ])
314
+ }
315
+ }
316
+ return { bindings, claimed }
317
+ }
318
+
319
+ /** The object type of `@Resolver('Type')`, reported when it is not one. */
320
+ #resolverType(sdl: SdlSource, resolver: ResolverData): GraphQLObjectType | undefined {
321
+ const typeName = resolver.typeName
322
+ if (typeName === undefined) return undefined
323
+ const { isObjectType, isInterfaceType } = graphqlModules().type
324
+ const schema = sdl.schema
325
+ const type = schema.getType(typeName.name)
326
+ const owner = resolver.host.displayName
327
+ if (type === undefined) {
328
+ const objects = Object.values(schema.getTypeMap())
329
+ .filter(candidate => isObjectType(candidate) && !candidate.name.startsWith('__'))
330
+ .map(candidate => candidate.name)
331
+ .sort(compareStrings)
332
+ const suggestion = closest(typeName.name, objects)
333
+ this.#mismatch(
334
+ typeName.location,
335
+ `@Resolver('${typeName.name}') of ${owner}: the schema has no type ${typeName.name}`,
336
+ [
337
+ suggestion === undefined
338
+ ? `Declare it in the SDL: type ${typeName.name} { ... }`
339
+ : `Did you mean '${suggestion}'?`,
340
+ ],
341
+ )
342
+ return undefined
343
+ }
344
+ const declared = sdl.locate(type.astNode)
345
+ const related = declared === undefined ? [] : [{ message: `${type.name} is declared here`, location: declared }]
346
+ if (isInterfaceType(type)) {
347
+ const implementations = schema.getPossibleTypes(type).map(object => object.name)
348
+ this.#mismatch(
349
+ typeName.location,
350
+ `@Resolver('${type.name}') of ${owner}: ${type.name} is an interface, field resolvers bind the fields of object types`,
351
+ [
352
+ implementations.length === 0
353
+ ? `Declare an object type implementing ${type.name} and bind the field on it`
354
+ : `Bind the field on each implementation: @Resolver('${implementations[0]}')${implementations.length > 1 ? ` (and ${implementations.slice(1).join(', ')})` : ''}`,
355
+ ],
356
+ related,
357
+ )
358
+ return undefined
359
+ }
360
+ if (!isObjectType(type)) {
361
+ this.#mismatch(
362
+ typeName.location,
363
+ `@Resolver('${type.name}') of ${owner}: ${type.name} is not an object type, it has no field to resolve`,
364
+ ['Name an object type of the SDL (type ... { ... })'],
365
+ related,
366
+ )
367
+ return undefined
368
+ }
369
+ const operation = (['query', 'mutation', 'subscription'] as const).find(
370
+ candidate => rootType(sdl.schema, candidate) === type,
371
+ )
372
+ if (operation !== undefined && resolver.fields.some(field => field.operation === 'field')) {
373
+ this.#mismatch(
374
+ typeName.location,
375
+ `@Resolver('${type.name}') of ${owner}: ${type.name} is the ${operation} root type, its fields are bound with ${DECORATORS[operation]}()`,
376
+ [`Replace @ResolveField() with ${DECORATORS[operation]}() and use @Resolver()`],
377
+ related,
378
+ )
379
+ return undefined
380
+ }
381
+ return type
382
+ }
383
+
384
+ #unknownField(schema: GraphQLSchema, type: GraphQLObjectType, field: FieldData, where: string): void {
385
+ const hints: string[] = []
386
+ if (field.operation !== 'field') {
387
+ const elsewhere = (['query', 'mutation', 'subscription'] as const).find(
388
+ operation =>
389
+ operation !== field.operation && rootType(schema, operation)?.getFields()[field.name] !== undefined,
390
+ )
391
+ if (elsewhere !== undefined) {
392
+ hints.push(`${ROOTS[elsewhere]}.${field.name} exists: use ${DECORATORS[elsewhere]}()`)
393
+ }
394
+ }
395
+ if (hints.length === 0) {
396
+ const suggestion = closest(field.name, Object.keys(type.getFields()).sort(compareStrings))
397
+ if (suggestion !== undefined) {
398
+ hints.push(`Did you mean '${suggestion}'? ${DECORATORS[field.operation]}('${suggestion}')`)
399
+ }
400
+ }
401
+ hints.push(
402
+ `Declare it in the SDL (${field.operation === 'field' ? `extend type ${type.name}` : `extend type ${type.name}`} { ${field.name}: ... }), or remove the method`,
403
+ )
404
+ this.#mismatch(
405
+ field.location,
406
+ `${DECORATORS[field.operation]} ${where}: ${type.name} has no field '${field.name}'`,
407
+ hints,
408
+ )
409
+ }
410
+
411
+ /** `@Args('name')`: the argument must exist. */
412
+ #checkArguments(
413
+ sdl: SdlSource,
414
+ field: FieldData,
415
+ method: MethodInfo,
416
+ sdlField: GraphQLField<unknown, unknown>,
417
+ coordinate: string,
418
+ where: string,
419
+ ): void {
420
+ const names = sdlField.args.map(arg => arg.name)
421
+ field.params.forEach((param, index) => {
422
+ if (param?.kind !== 'args' || param.name === undefined || names.includes(param.name)) return
423
+ const suggestion = closest(param.name, [...names].sort(compareStrings))
424
+ const declared = sdl.locate(sdlField.astNode)
425
+ let hint = `Its arguments: ${names.join(', ')}`
426
+ if (suggestion !== undefined) hint = `Did you mean '${suggestion}'?`
427
+ else if (names.length === 0)
428
+ hint = `${coordinate} takes no argument: declare it in the SDL, or remove the parameter`
429
+ this.#mismatch(
430
+ param.location,
431
+ `@Args('${param.name}') on parameter '${method.params[index]?.name ?? param.name}' of ${where}: ${coordinate} has no argument '${param.name}'`,
432
+ [hint],
433
+ declared === undefined ? [] : [{ message: `${coordinate} is declared here`, location: declared }],
434
+ )
435
+ })
436
+ }
437
+
438
+ #method(resolver: ResolverData, field: FieldData): MethodInfo | undefined {
439
+ const key = `${resolver.host.identity}#${field.method}`
440
+ if (!this.#methods.has(key)) {
441
+ const cls: ClassInfo | undefined = this.#ctx.classInfo(resolver.host.identity)
442
+ this.#methods.set(
443
+ key,
444
+ cls?.methods.find(method => method.name === field.method && method.kind === 'method'),
445
+ )
446
+ }
447
+ return this.#methods.get(key)
448
+ }
449
+
450
+ // --------------------------------------------------------------------------------------- model
451
+
452
+ #model(
453
+ sdl: SdlSource,
454
+ type: GraphQLNamedType,
455
+ bindings: ReadonlyMap<string, Binding>,
456
+ scalars: ReadonlyMap<string, ScalarModel>,
457
+ conformance: ReturnType<typeof checkConformance>,
458
+ ): TypeModel | undefined {
459
+ const { isObjectType, isInterfaceType, isInputObjectType, isEnumType, isUnionType } = graphqlModules().type
460
+ const base = {
461
+ name: type.name,
462
+ key: `#sdl:${type.name}`,
463
+ description: type.description ?? undefined,
464
+ location: sdl.locate(type.astNode),
465
+ }
466
+ const nulls = conformance.nullAsUndefined
467
+ if (isObjectType(type) || isInterfaceType(type)) {
468
+ const fields = Object.values(type.getFields()).map(field =>
469
+ this.#field(sdl, type, field, bindings.get(`${type.name}.${field.name}`), nulls),
470
+ )
471
+ const interfaces = type.getInterfaces().map(implemented => implemented.name)
472
+ if (isInterfaceType(type)) {
473
+ const model: InterfaceModel = {
474
+ ...base,
475
+ kind: 'interface',
476
+ fields,
477
+ interfaces,
478
+ discriminator: conformance.discriminators.get(type.name),
479
+ }
480
+ return model
481
+ }
482
+ const model: ObjectModel = {
483
+ ...base,
484
+ kind: 'object',
485
+ fields,
486
+ trail: type.name,
487
+ root: undefined,
488
+ classIdentity: undefined,
489
+ properties: [],
490
+ interfaces,
491
+ }
492
+ return model
493
+ }
494
+ if (isInputObjectType(type)) {
495
+ const model: InputModel = {
496
+ ...base,
497
+ kind: 'input',
498
+ fields: Object.values((type as GraphQLInputObjectType).getFields()).map(field =>
499
+ inputValue(sdl, field, `${type.name}.${field.name}`, nulls),
500
+ ),
501
+ }
502
+ return model
503
+ }
504
+ if (isEnumType(type)) {
505
+ return {
506
+ ...base,
507
+ kind: 'enum',
508
+ values: (type as GraphQLEnumType).getValues().map(value => ({
509
+ name: value.name,
510
+ value: value.name,
511
+ description: value.description ?? undefined,
512
+ deprecationReason: value.deprecationReason ?? undefined,
513
+ })),
514
+ }
515
+ }
516
+ if (isUnionType(type)) {
517
+ const model: UnionModel = {
518
+ ...base,
519
+ kind: 'union',
520
+ members: (type as GraphQLUnionType)
521
+ .getTypes()
522
+ .map(member => member.name)
523
+ .sort(compareStrings),
524
+ discriminator: conformance.discriminators.get(type.name),
525
+ }
526
+ return model
527
+ }
528
+ return scalars.get((type as GraphQLScalarType).name)
529
+ }
530
+
531
+ #field(
532
+ sdl: SdlSource,
533
+ type: GraphQLObjectType | GraphQLInterfaceType,
534
+ field: GraphQLField<unknown, unknown>,
535
+ binding: Binding | undefined,
536
+ nulls: ReadonlySet<string>,
537
+ ): FieldModel {
538
+ const coordinate = `${type.name}.${field.name}`
539
+ return {
540
+ name: field.name,
541
+ type: String(field.type),
542
+ description: field.description ?? undefined,
543
+ deprecationReason: field.deprecationReason ?? undefined,
544
+ args: field.args.map(arg => inputValue(sdl, arg, `${coordinate}(${arg.name}:)`, nulls)),
545
+ binding: binding === undefined ? undefined : { resolver: binding.resolver, field: binding.field },
546
+ location: sdl.locate(field.astNode),
547
+ }
548
+ }
549
+ }
550
+
551
+ const ROOTS: Readonly<Record<Operation, string>> = {
552
+ query: 'Query',
553
+ mutation: 'Mutation',
554
+ subscription: 'Subscription',
555
+ }
556
+
557
+ function rootType(schema: GraphQLSchema, operation: Operation): GraphQLObjectType | undefined {
558
+ switch (operation) {
559
+ case 'query':
560
+ return schema.getQueryType() ?? undefined
561
+ case 'mutation':
562
+ return schema.getMutationType() ?? undefined
563
+ default:
564
+ return schema.getSubscriptionType() ?? undefined
565
+ }
566
+ }
567
+
568
+ /** An argument or an input field: its default as an external value (enum values by name). */
569
+ function inputValue(
570
+ sdl: SdlSource,
571
+ value: GraphQLArgument | GraphQLInputField,
572
+ coordinate: string,
573
+ nulls: ReadonlySet<string>,
574
+ ): ArgModel {
575
+ const type = String(value.type)
576
+ const ref = parseTypeRef(type) as NonNullable<ReturnType<typeof parseTypeRef>>
577
+ const defaultInput = value.default
578
+ let defaultValue: unknown
579
+ if (defaultInput !== undefined) {
580
+ defaultValue =
581
+ defaultInput.literal === undefined
582
+ ? defaultInput.value
583
+ : graphqlModules().utilities.valueFromASTUntyped(defaultInput.literal)
584
+ }
585
+ return {
586
+ name: value.name,
587
+ type,
588
+ ref,
589
+ description: value.description ?? undefined,
590
+ deprecationReason: value.deprecationReason ?? undefined,
591
+ defaultValue,
592
+ nullAsUndefined: !ref.nonNull && nulls.has(coordinate),
593
+ location: sdl.locate(value.astNode),
594
+ }
595
+ }