@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,529 @@
1
+ /**
2
+ * The SDL of a schema-first application (RFC 0007 §5.1): the `.graphql` files matching the
3
+ * `schema` globs, parsed with `graphql/language`, checked structurally (duplicates, unknown types
4
+ * and directives, extensions of missing types: located diagnostics with suggestions), then built
5
+ * with `buildASTSchema` and validated with `validateSchema`, so that the build and the runtime agree
6
+ * on what a valid schema is. Every problem is a ZNIGHT067 located in the `.graphql` file.
7
+ */
8
+ import type { Diagnostic, Location, ValidateContext } from '@znight/compiler'
9
+ import type {
10
+ ASTNode,
11
+ DefinitionNode,
12
+ DirectiveNode,
13
+ DocumentNode,
14
+ FieldDefinitionNode,
15
+ GraphQLSchema,
16
+ InputValueDefinitionNode,
17
+ NamedTypeNode,
18
+ NameNode,
19
+ TypeNode,
20
+ } from 'graphql'
21
+ import type { GraphQLData } from './data.ts'
22
+
23
+ import { Codes, compareStrings } from './constants.ts'
24
+ import { graphqlModules } from './graphql-js.ts'
25
+ import { closest } from './suggest.ts'
26
+
27
+ /** Directives graphql-js knows (specified, or `@defer`/`@stream` of graphql 17). */
28
+ const KNOWN_DIRECTIVES: ReadonlySet<string> = new Set([
29
+ 'deprecated',
30
+ 'specifiedBy',
31
+ 'oneOf',
32
+ 'include',
33
+ 'skip',
34
+ 'defer',
35
+ 'stream',
36
+ ])
37
+
38
+ /** A directive as the SDL checks see it: where it applies, its arguments, repeatable or not. */
39
+ interface DirectiveSpec {
40
+ readonly locations: ReadonlySet<string>
41
+ /** Argument name → required (non-null without default). */
42
+ readonly args: ReadonlyMap<string, boolean>
43
+ readonly repeatable: boolean
44
+ }
45
+
46
+ /** The built-in directives allowed in an SDL (the others are executable directives). */
47
+ const SDL_DIRECTIVES: ReadonlyMap<string, DirectiveSpec> = new Map([
48
+ [
49
+ 'deprecated',
50
+ {
51
+ locations: new Set(['FIELD_DEFINITION', 'ARGUMENT_DEFINITION', 'INPUT_FIELD_DEFINITION', 'ENUM_VALUE']),
52
+ args: new Map([['reason', false]]),
53
+ repeatable: false,
54
+ },
55
+ ],
56
+ ['specifiedBy', { locations: new Set(['SCALAR']), args: new Map([['url', true]]), repeatable: false }],
57
+ ['oneOf', { locations: new Set(['INPUT_OBJECT']), args: new Map(), repeatable: false }],
58
+ ])
59
+
60
+ /** The directive location of each kind of type definition and extension. */
61
+ const TYPE_LOCATIONS: Readonly<Record<string, string>> = {
62
+ type: 'OBJECT',
63
+ interface: 'INTERFACE',
64
+ union: 'UNION',
65
+ enum: 'ENUM',
66
+ input: 'INPUT_OBJECT',
67
+ scalar: 'SCALAR',
68
+ }
69
+
70
+ /** Names of the types graphql-js always provides. */
71
+ const PROVIDED_TYPES: ReadonlySet<string> = new Set(['String', 'Int', 'Float', 'Boolean', 'ID'])
72
+
73
+ /** The parsed and validated SDL. */
74
+ export interface SdlSource {
75
+ readonly schema: GraphQLSchema
76
+ /** Names of the custom directives (left out of the served schema). */
77
+ readonly customDirectives: ReadonlySet<string>
78
+ /** The location of an AST node (its name when it has one); `undefined` for nodes without location. */
79
+ locate(node: ASTNode | null | undefined): Location | undefined
80
+ }
81
+
82
+ /** Directive locations in messages. */
83
+ const LOCATION_NAMES: Readonly<Record<string, string>> = {
84
+ SCHEMA: 'the schema',
85
+ SCALAR: 'scalars',
86
+ OBJECT: 'object types',
87
+ FIELD_DEFINITION: 'fields',
88
+ ARGUMENT_DEFINITION: 'arguments',
89
+ INTERFACE: 'interfaces',
90
+ UNION: 'unions',
91
+ ENUM: 'enums',
92
+ ENUM_VALUE: 'enum values',
93
+ INPUT_OBJECT: 'input objects',
94
+ INPUT_FIELD_DEFINITION: 'input fields',
95
+ }
96
+
97
+ const TYPE_KINDS: Readonly<Record<string, string>> = {
98
+ ObjectTypeDefinition: 'type',
99
+ ObjectTypeExtension: 'type',
100
+ InterfaceTypeDefinition: 'interface',
101
+ InterfaceTypeExtension: 'interface',
102
+ UnionTypeDefinition: 'union',
103
+ UnionTypeExtension: 'union',
104
+ EnumTypeDefinition: 'enum',
105
+ EnumTypeExtension: 'enum',
106
+ InputObjectTypeDefinition: 'input',
107
+ InputObjectTypeExtension: 'input',
108
+ ScalarTypeDefinition: 'scalar',
109
+ ScalarTypeExtension: 'scalar',
110
+ }
111
+
112
+ /** A type definition or extension. */
113
+ type TypeNodeLike = Extract<DefinitionNode, { readonly name: NameNode }> & { readonly kind: string }
114
+
115
+ /**
116
+ * Reads, parses and validates the SDL; `undefined` after an error (reported). Warnings (custom
117
+ * directives) do not stop the build.
118
+ */
119
+ export function loadSdl(
120
+ ctx: ValidateContext<GraphQLData>,
121
+ patterns: readonly string[],
122
+ report: (diagnostic: Diagnostic) => void,
123
+ ): SdlSource | undefined {
124
+ const files = ctx.inputFiles(patterns)
125
+ if (files.length === 0) {
126
+ report({
127
+ code: Codes.InvalidSdl,
128
+ severity: 'error',
129
+ message: `No GraphQL schema file matches ${patterns.map(pattern => `'${pattern}'`).join(', ')} (schema-first)`,
130
+ hints: [
131
+ "The globs of graphql.configure({ schema }) are relative to the project root: 'src/**/*.graphql'",
132
+ 'Remove the schema option to derive the schema from the TypeScript types (code-first)',
133
+ ],
134
+ })
135
+ return undefined
136
+ }
137
+ const { language, type, utilities } = graphqlModules()
138
+ const locator = new Locator(new Map(files.map(file => [file.file, file.text])))
139
+ let errors = 0
140
+ const fail = (
141
+ message: string,
142
+ node: ASTNode | null | undefined,
143
+ hints: readonly string[] = [],
144
+ related?: ASTNode,
145
+ ) => {
146
+ errors++
147
+ const location = locator.locate(node)
148
+ const other = related === undefined ? undefined : locator.locate(related)
149
+ report({
150
+ code: Codes.InvalidSdl,
151
+ severity: 'error',
152
+ message,
153
+ ...(location === undefined ? {} : { location }),
154
+ ...(hints.length === 0 ? {} : { hints }),
155
+ ...(other === undefined ? {} : { related: [{ message: 'first declared here', location: other }] }),
156
+ })
157
+ }
158
+
159
+ const documents: DocumentNode[] = []
160
+ for (const file of files) {
161
+ // a file with comments only (a placeholder) defines nothing: graphql-js would reject it
162
+ if (/^(?:\s|,|#[^\n\r]*)*$/.test(file.text.replace(/^\uFEFF/, ''))) continue
163
+ try {
164
+ documents.push(language.parse(new language.Source(file.text, file.file)))
165
+ } catch (error) {
166
+ const position = (error as { positions?: readonly number[] }).positions?.[0]
167
+ errors++
168
+ const location = position === undefined ? undefined : locator.at(file.file, position, position + 1)
169
+ report({
170
+ code: Codes.InvalidSdl,
171
+ severity: 'error',
172
+ message: `${(error as Error).message} (${file.relativeFile})`,
173
+ ...(location === undefined ? {} : { location }),
174
+ })
175
+ }
176
+ }
177
+ if (errors > 0) return undefined
178
+ if (documents.length === 0) {
179
+ report({
180
+ code: Codes.InvalidSdl,
181
+ severity: 'error',
182
+ message: `The GraphQL schema files matching ${patterns.map(pattern => `'${pattern}'`).join(', ')} define nothing (schema-first)`,
183
+ hints: ['Declare the types of the schema, starting with type Query { ... }'],
184
+ })
185
+ return undefined
186
+ }
187
+ const document = utilities.concatAST(documents)
188
+ const customDirectives = checkStructure(document, fail, (message, node) => {
189
+ const location = locator.locate(node)
190
+ report({
191
+ code: Codes.InvalidSdl,
192
+ severity: 'warning',
193
+ message,
194
+ ...(location === undefined ? {} : { location }),
195
+ hints: [
196
+ 'Znight serves the schema without custom directives (and so does the SDL artifact): remove it, or keep it for other tools',
197
+ ],
198
+ })
199
+ })
200
+ if (errors > 0) return undefined
201
+
202
+ let schema: GraphQLSchema
203
+ try {
204
+ schema = utilities.buildASTSchema(document, { assumeValidSDL: true })
205
+ } catch (error) {
206
+ fail(`Invalid schema: ${(error as Error).message}`, (error as { nodes?: readonly ASTNode[] }).nodes?.[0])
207
+ return undefined
208
+ }
209
+ for (const problem of type.validateSchema(schema)) {
210
+ const [node, ...others] = (problem.nodes ?? []).filter(candidate => candidate.loc !== undefined)
211
+ errors++
212
+ const location = locator.locate(node)
213
+ const related = others.flatMap(other => {
214
+ const at = locator.locate(other)
215
+ return at === undefined ? [] : [{ message: 'see also', location: at }]
216
+ })
217
+ report({
218
+ code: Codes.InvalidSdl,
219
+ severity: 'error',
220
+ message: `Invalid schema: ${problem.message}`,
221
+ ...(location === undefined ? {} : { location }),
222
+ ...(related.length === 0 ? {} : { related }),
223
+ // the one schema-wide error without node: no query type
224
+ ...(node === undefined && schema.getQueryType() == null
225
+ ? { hints: ['Declare the query root type in one of the files: type Query { ... }'] }
226
+ : {}),
227
+ })
228
+ }
229
+ if (errors > 0) return undefined
230
+ return { schema, customDirectives, locate: node => locator.locate(node) }
231
+ }
232
+
233
+ /**
234
+ * The checks graphql-js leaves to its SDL validation (not exported): unique type, field, value and
235
+ * argument names, known types and directives, extensions of existing types of the same kind, one
236
+ * schema definition. Returns the names of the custom directives (each reported as a warning).
237
+ */
238
+ function checkStructure(
239
+ document: DocumentNode,
240
+ fail: (message: string, node: ASTNode, hints?: readonly string[], related?: ASTNode) => void,
241
+ warn: (message: string, node: ASTNode) => void,
242
+ ): ReadonlySet<string> {
243
+ const definitions = new Map<string, TypeNodeLike>()
244
+ const directives = new Map<string, ASTNode>()
245
+ const specs = new Map<string, DirectiveSpec>(SDL_DIRECTIVES)
246
+ let schemaDefinition: ASTNode | undefined
247
+ for (const definition of document.definitions) {
248
+ if (definition.kind === 'SchemaDefinition') {
249
+ if (schemaDefinition === undefined) schemaDefinition = definition
250
+ else
251
+ fail(
252
+ 'The schema is defined twice (schema { ... })',
253
+ definition,
254
+ ['Keep one schema definition'],
255
+ schemaDefinition,
256
+ )
257
+ continue
258
+ }
259
+ if (definition.kind === 'DirectiveDefinition') {
260
+ const name = definition.name.value
261
+ const previous = directives.get(name)
262
+ if (previous !== undefined) fail(`The directive @${name} is defined twice`, definition.name, [], previous)
263
+ else if (KNOWN_DIRECTIVES.has(name))
264
+ fail(`@${name} is a built-in directive: it cannot be redefined`, definition.name)
265
+ else {
266
+ directives.set(name, definition.name)
267
+ specs.set(name, {
268
+ locations: new Set(definition.locations.map(location => location.value)),
269
+ args: new Map(
270
+ (definition.arguments ?? []).map(arg => [
271
+ arg.name.value,
272
+ arg.type.kind === 'NonNullType' && arg.defaultValue === undefined,
273
+ ]),
274
+ ),
275
+ repeatable: definition.repeatable,
276
+ })
277
+ warn(`The custom directive @${name} is ignored`, definition.name)
278
+ }
279
+ continue
280
+ }
281
+ const kind = TYPE_KINDS[definition.kind]
282
+ if (kind === undefined || definition.kind.endsWith('Extension')) continue
283
+ const node = definition as TypeNodeLike
284
+ const previous = definitions.get(node.name.value)
285
+ if (previous !== undefined) {
286
+ fail(
287
+ `The type ${node.name.value} is defined twice`,
288
+ node.name,
289
+ [
290
+ kind === 'scalar'
291
+ ? 'Keep one of them, or rename one'
292
+ : `Extend it instead (${declaration(`extend ${kind}`, kind, node.name.value)}), or rename one of them`,
293
+ ],
294
+ previous.name,
295
+ )
296
+ continue
297
+ }
298
+ definitions.set(node.name.value, node)
299
+ }
300
+ const typeNames = [...definitions.keys(), ...PROVIDED_TYPES].sort(compareStrings)
301
+ const known = (name: NamedTypeNode, where: string): void => {
302
+ const value = name.name.value
303
+ if (definitions.has(value) || PROVIDED_TYPES.has(value)) return
304
+ const suggestion = closest(value, typeNames)
305
+ fail(
306
+ `Unknown type ${value} (${where})`,
307
+ name,
308
+ suggestion === undefined ? ['Define it, or fix the name'] : [`Did you mean ${suggestion}?`],
309
+ )
310
+ }
311
+ /** The directives applied at `location`: known, allowed there, once unless repeatable, valid arguments. */
312
+ const directivesOf = (nodes: readonly DirectiveNode[] | undefined, location: string): void => {
313
+ const seen = new Map<string, ASTNode>()
314
+ for (const directive of nodes ?? []) {
315
+ const name = directive.name.value
316
+ const spec = specs.get(name)
317
+ if (spec === undefined) {
318
+ fail(
319
+ KNOWN_DIRECTIVES.has(name)
320
+ ? `@${name} is an executable directive: it cannot be used in a schema`
321
+ : `Unknown directive @${name}`,
322
+ directive.name,
323
+ KNOWN_DIRECTIVES.has(name) ? ['Remove it'] : [`Define it (directive @${name} on ${location}), or remove it`],
324
+ )
325
+ continue
326
+ }
327
+ if (!spec.locations.has(location)) {
328
+ fail(`@${name} cannot be used on ${LOCATION_NAMES[location] ?? location}`, directive.name, [
329
+ `@${name} applies to ${[...spec.locations].map(where => LOCATION_NAMES[where] ?? where).join(', ')}`,
330
+ ])
331
+ }
332
+ const previous = seen.get(name)
333
+ if (previous !== undefined && !spec.repeatable) {
334
+ fail(`@${name} is applied twice`, directive.name, ['Apply it once'], previous)
335
+ }
336
+ seen.set(name, directive.name)
337
+ const given = new Set<string>()
338
+ for (const arg of directive.arguments ?? []) {
339
+ given.add(arg.name.value)
340
+ if (!spec.args.has(arg.name.value)) {
341
+ const names = [...spec.args.keys()]
342
+ fail(`@${name} has no argument '${arg.name.value}'`, arg.name, [
343
+ names.length === 0 ? `@${name} takes no argument` : `Its arguments: ${names.join(', ')}`,
344
+ ])
345
+ }
346
+ }
347
+ for (const [arg, required] of spec.args) {
348
+ if (required && !given.has(arg))
349
+ fail(`@${name} needs its argument '${arg}'`, directive.name, [`@${name}(${arg}: ...)`])
350
+ }
351
+ }
352
+ }
353
+ /** Members of a type: its definition and extensions, by name (fields, values, input fields). */
354
+ const members = new Map<string, Map<string, ASTNode>>()
355
+ const member = (typeName: string, kind: string, name: NameNode): void => {
356
+ let seen = members.get(typeName)
357
+ if (seen === undefined) {
358
+ seen = new Map()
359
+ members.set(typeName, seen)
360
+ }
361
+ const previous = seen.get(name.value)
362
+ if (previous !== undefined) {
363
+ fail(`${typeName}.${name.value} is declared twice`, name, [`Keep one ${kind} named ${name.value}`], previous)
364
+ return
365
+ }
366
+ seen.set(name.value, name)
367
+ }
368
+ const inputValues = (owner: string, values: readonly InputValueDefinitionNode[] | undefined): void => {
369
+ const seen = new Map<string, ASTNode>()
370
+ for (const value of values ?? []) {
371
+ const previous = seen.get(value.name.value)
372
+ if (previous !== undefined) {
373
+ fail(`Argument ${owner}(${value.name.value}:) is declared twice`, value.name, [], previous)
374
+ }
375
+ seen.set(value.name.value, value.name)
376
+ known(namedType(value.type), `${owner}(${value.name.value}:)`)
377
+ directivesOf(value.directives, 'ARGUMENT_DEFINITION')
378
+ }
379
+ }
380
+ const fields = (owner: string, list: readonly FieldDefinitionNode[] | undefined): void => {
381
+ for (const field of list ?? []) {
382
+ member(owner, 'field', field.name)
383
+ known(namedType(field.type), `${owner}.${field.name.value}`)
384
+ inputValues(`${owner}.${field.name.value}`, field.arguments)
385
+ directivesOf(field.directives, 'FIELD_DEFINITION')
386
+ }
387
+ }
388
+ const operations = new Map<string, ASTNode>()
389
+ for (const definition of document.definitions) {
390
+ if (definition.kind === 'SchemaDefinition' || definition.kind === 'SchemaExtension') {
391
+ for (const operation of definition.operationTypes ?? []) {
392
+ known(operation.type, `schema ${operation.operation}`)
393
+ const previous = operations.get(operation.operation)
394
+ if (previous !== undefined) {
395
+ fail(`The ${operation.operation} root type is declared twice`, operation, ['Keep one'], previous)
396
+ }
397
+ operations.set(operation.operation, operation)
398
+ }
399
+ directivesOf(definition.directives, 'SCHEMA')
400
+ continue
401
+ }
402
+ if (definition.kind === 'DirectiveDefinition') {
403
+ inputValues(`@${definition.name.value}`, definition.arguments)
404
+ continue
405
+ }
406
+ const kind = TYPE_KINDS[definition.kind]
407
+ if (kind === undefined) continue
408
+ const node = definition as TypeNodeLike & {
409
+ readonly fields?: readonly (FieldDefinitionNode | InputValueDefinitionNode)[]
410
+ readonly interfaces?: readonly NamedTypeNode[]
411
+ readonly types?: readonly NamedTypeNode[]
412
+ readonly values?: readonly { readonly name: NameNode; readonly directives?: readonly DirectiveNode[] }[]
413
+ readonly directives?: readonly DirectiveNode[]
414
+ }
415
+ const name = node.name.value
416
+ if (definition.kind.endsWith('Extension')) {
417
+ const base = definitions.get(name)
418
+ if (base === undefined) {
419
+ if (!PROVIDED_TYPES.has(name)) {
420
+ const suggestion = closest(name, typeNames)
421
+ fail(`Cannot extend ${kind} ${name}: it is not defined`, node.name, [
422
+ suggestion === undefined ? `Define it: ${declaration(kind, kind, name)}` : `Did you mean ${suggestion}?`,
423
+ ])
424
+ }
425
+ continue
426
+ }
427
+ if (TYPE_KINDS[base.kind] !== kind) {
428
+ const actual = TYPE_KINDS[base.kind] as string
429
+ fail(
430
+ `Cannot extend ${name} as ${article(kind)} ${kind}: it is ${article(actual)} ${actual}`,
431
+ node.name,
432
+ [`Use ${declaration(`extend ${actual}`, actual, name)}`],
433
+ base.name,
434
+ )
435
+ continue
436
+ }
437
+ }
438
+ directivesOf(node.directives, TYPE_LOCATIONS[kind] as string)
439
+ for (const implemented of node.interfaces ?? []) known(implemented, `${name} implements`)
440
+ for (const target of node.types ?? []) known(target, `union ${name}`)
441
+ for (const value of node.values ?? []) {
442
+ member(name, 'value', value.name)
443
+ directivesOf(value.directives, 'ENUM_VALUE')
444
+ }
445
+ if (kind === 'input') {
446
+ for (const field of (node.fields ?? []) as readonly InputValueDefinitionNode[]) {
447
+ member(name, 'field', field.name)
448
+ known(namedType(field.type), `${name}.${field.name.value}`)
449
+ directivesOf(field.directives, 'INPUT_FIELD_DEFINITION')
450
+ }
451
+ } else {
452
+ fields(name, node.fields as readonly FieldDefinitionNode[] | undefined)
453
+ }
454
+ }
455
+ return new Set(directives.keys())
456
+ }
457
+
458
+ /** `extend type Book { ... }`, `union Hit = ...`, `scalar Url`: valid SDL for a hint. */
459
+ function declaration(keyword: string, kind: string, name: string): string {
460
+ if (kind === 'scalar') return `${keyword} ${name}`
461
+ if (kind === 'union') return `${keyword} ${name} = ...`
462
+ return `${keyword} ${name} { ... }`
463
+ }
464
+
465
+ function article(word: string): string {
466
+ return /^[aeiou]/.test(word) ? 'an' : 'a'
467
+ }
468
+
469
+ function namedType(type: TypeNode): NamedTypeNode {
470
+ let current = type
471
+ while (current.kind !== 'NamedType') current = current.type
472
+ return current
473
+ }
474
+
475
+ /** Source locations of AST nodes (graphql-js `Source.name` is the absolute path of the file). */
476
+ class Locator {
477
+ readonly #texts: ReadonlyMap<string, string>
478
+ readonly #lines = new Map<string, number[]>()
479
+
480
+ constructor(texts: ReadonlyMap<string, string>) {
481
+ this.#texts = texts
482
+ }
483
+
484
+ locate(node: ASTNode | null | undefined): Location | undefined {
485
+ if (node === null || node === undefined) return undefined
486
+ const named = 'name' in node && node.name !== undefined && typeof node.name === 'object' ? node.name : node
487
+ const loc = (named as ASTNode).loc ?? node.loc
488
+ if (loc === undefined) return undefined
489
+ return this.at(loc.source.name, loc.start, loc.end)
490
+ }
491
+
492
+ at(file: string, start: number, end: number): Location | undefined {
493
+ const text = this.#texts.get(file)
494
+ if (text === undefined) return undefined
495
+ const lines = this.#lineStarts(file, text)
496
+ const first = position(lines, start)
497
+ const last = position(lines, Math.min(end, text.length))
498
+ return { file, line: first.line, column: first.column, endLine: last.line, endColumn: last.column, start, end }
499
+ }
500
+
501
+ #lineStarts(file: string, text: string): number[] {
502
+ let lines = this.#lines.get(file)
503
+ if (lines === undefined) {
504
+ lines = [0]
505
+ for (let index = 0; index < text.length; index++) {
506
+ const code = text.charCodeAt(index)
507
+ if (code === 0x0a) lines.push(index + 1)
508
+ else if (code === 0x0d) {
509
+ if (text.charCodeAt(index + 1) === 0x0a) index++
510
+ lines.push(index + 1)
511
+ }
512
+ }
513
+ this.#lines.set(file, lines)
514
+ }
515
+ return lines
516
+ }
517
+ }
518
+
519
+ /** 1-based line and column (UTF-16 code units) of an offset. */
520
+ function position(lines: readonly number[], offset: number): { line: number; column: number } {
521
+ let low = 0
522
+ let high = lines.length - 1
523
+ while (low < high) {
524
+ const middle = (low + high + 1) >> 1
525
+ if ((lines[middle] as number) <= offset) low = middle
526
+ else high = middle - 1
527
+ }
528
+ return { line: low + 1, column: offset - (lines[low] as number) + 1 }
529
+ }
@@ -10,10 +10,12 @@ export function sdlType(model: TypeModel): SdlType {
10
10
  const base = model.description === undefined ? {} : { description: model.description }
11
11
  switch (model.kind) {
12
12
  case 'object':
13
+ case 'interface':
13
14
  return {
14
- kind: 'object',
15
+ kind: model.kind,
15
16
  name: model.name,
16
17
  ...base,
18
+ ...(model.interfaces.length === 0 ? {} : { interfaces: model.interfaces }),
17
19
  fields: model.fields.map(field => ({
18
20
  name: field.name,
19
21
  type: field.type,
@@ -0,0 +1,39 @@
1
+ /**
2
+ * "Did you mean" suggestions of the schema-first diagnostics (RFC 0007 §5.3). Import-free.
3
+ */
4
+
5
+ /**
6
+ * The candidate closest to `name` (Levenshtein distance, case differences counting half), when it
7
+ * is close enough to be a typo: at most a third of the name's length, at least 1. Ties keep the
8
+ * first candidate, so callers pass them sorted.
9
+ */
10
+ export function closest(name: string, candidates: readonly string[]): string | undefined {
11
+ const limit = Math.max(1, Math.floor(name.length / 3))
12
+ let best: string | undefined
13
+ let bestDistance = Number.POSITIVE_INFINITY
14
+ for (const candidate of candidates) {
15
+ if (candidate === name) continue
16
+ const distance = candidate.toLowerCase() === name.toLowerCase() ? 0.5 : levenshtein(name, candidate)
17
+ if (distance <= limit && distance < bestDistance) {
18
+ best = candidate
19
+ bestDistance = distance
20
+ }
21
+ }
22
+ return best
23
+ }
24
+
25
+ function levenshtein(a: string, b: string): number {
26
+ if (Math.abs(a.length - b.length) > Math.max(a.length, b.length) / 2) return Number.POSITIVE_INFINITY
27
+ let previous = Array.from({ length: b.length + 1 }, (_, index) => index)
28
+ for (let i = 1; i <= a.length; i++) {
29
+ const current = [i]
30
+ for (let j = 1; j <= b.length; j++) {
31
+ const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1
32
+ current.push(
33
+ Math.min((previous[j] as number) + 1, (current[j - 1] as number) + 1, (previous[j - 1] as number) + cost),
34
+ )
35
+ }
36
+ previous = current
37
+ }
38
+ return previous[b.length] as number
39
+ }
package/src/decorators.ts CHANGED
@@ -60,6 +60,10 @@ export interface ResolverOptions {
60
60
  * `@Subscription` and `@ResolveField` methods are fields of the schema. Route decorators of
61
61
  * `@znight/http` (`createRouteDecorator`) on the class are the default decisions of its fields.
62
62
  *
63
+ * Schema-first (RFC 0007): `@Resolver('Event')` names the SDL object type its `@ResolveField`
64
+ * methods resolve fields of (`@Resolver('Event', { scope })` with options). Code-first rejects a
65
+ * type name (ZNIGHT005): the type is the one of the `@Parent()` parameter.
66
+ *
63
67
  * ```ts
64
68
  * @Resolver()
65
69
  * export class BookResolver {
@@ -72,9 +76,10 @@ export interface ResolverOptions {
72
76
  * }
73
77
  * ```
74
78
  *
75
- * @param options static: `{ scope? }`.
79
+ * @param typeOrOptions static: the SDL type name (schema-first), or `{ scope? }`.
80
+ * @param options static: `{ scope? }`, after a type name.
76
81
  */
77
- export function Resolver(_options?: ResolverOptions): ClassMarker {
82
+ export function Resolver(_typeOrOptions?: string | ResolverOptions, _options?: ResolverOptions): ClassMarker {
78
83
  return classMarker
79
84
  }
80
85
 
@@ -109,7 +114,8 @@ export function Subscription(_name?: string): MethodMarker {
109
114
 
110
115
  /**
111
116
  * A field resolver: adds a field to the type of the `@Parent()` parameter (mandatory), or replaces
112
- * the property field of the same name.
117
+ * the property field of the same name. Schema-first (RFC 0007): resolves the field of the type named
118
+ * by `@Resolver('Type')`, `@Parent()` optional.
113
119
  *
114
120
  * @param name static: field name. Default: the method name.
115
121
  */
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export type {
15
15
  GraphQLHttpOptions,
16
16
  GraphQLLimits,
17
17
  GraphQLPluginOptions,
18
+ GraphQLResultMapperRef,
18
19
  GraphQLSubscriptionOptions,
19
20
  } from './options.ts'
20
21
  export type {
@@ -28,6 +29,9 @@ export type {
28
29
  GraphQLGuard,
29
30
  GraphQLGuardClass,
30
31
  GraphQLOperationKind,
32
+ GraphQLResultMapper,
33
+ GraphQLResultMapperClass,
34
+ GraphQLResultTypes,
31
35
  GraphQLTransport,
32
36
  LoaderKey,
33
37
  LoaderOptions,
@@ -70,6 +74,7 @@ export {
70
74
  type GraphQLFieldDefinition,
71
75
  type GraphQLInputFieldDefinition,
72
76
  type GraphQLInputTypeDefinition,
77
+ type GraphQLInterfaceTypeDefinition,
73
78
  type GraphQLMetadata,
74
79
  type GraphQLObjectTypeDefinition,
75
80
  type GraphQLResolverBinding,