@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.
- package/LICENSE +21 -0
- package/README.md +1346 -0
- package/dist/compiler.d.mts +114 -0
- package/dist/compiler.mjs +4599 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.d.mts +583 -0
- package/dist/index.mjs +134 -0
- package/dist/index.mjs.map +1 -0
- package/dist/names-BlzDM5cF.mjs +111 -0
- package/dist/names-BlzDM5cF.mjs.map +1 -0
- package/dist/plugin-CR_wVfRU.mjs +3445 -0
- package/dist/plugin-CR_wVfRU.mjs.map +1 -0
- package/dist/testing.d.mts +101 -0
- package/dist/testing.mjs +306 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-DQ713UgI.d.mts +266 -0
- package/package.json +97 -1
- package/src/compiler/analyze.ts +727 -0
- package/src/compiler/builder.ts +1561 -0
- package/src/compiler/checks.ts +105 -0
- package/src/compiler/conformance.ts +1068 -0
- package/src/compiler/constants.ts +80 -0
- package/src/compiler/data.ts +83 -0
- package/src/compiler/defaults.ts +92 -0
- package/src/compiler/discriminators.ts +53 -0
- package/src/compiler/docs.ts +55 -0
- package/src/compiler/generate.ts +419 -0
- package/src/compiler/graphql-js.ts +42 -0
- package/src/compiler/ids.ts +128 -0
- package/src/compiler/index.ts +98 -0
- package/src/compiler/locate.ts +82 -0
- package/src/compiler/model.ts +137 -0
- package/src/compiler/options.ts +85 -0
- package/src/compiler/scalars.ts +138 -0
- package/src/compiler/schema-first.ts +595 -0
- package/src/compiler/sdl-source.ts +529 -0
- package/src/compiler/sdl-view.ts +61 -0
- package/src/compiler/suggest.ts +39 -0
- package/src/context.ts +181 -0
- package/src/decisions.ts +169 -0
- package/src/decorators.ts +202 -0
- package/src/errors.ts +89 -0
- package/src/execution.ts +1064 -0
- package/src/format.ts +285 -0
- package/src/index.ts +89 -0
- package/src/limits.ts +177 -0
- package/src/loaders.ts +149 -0
- package/src/memory-pubsub.ts +133 -0
- package/src/metadata.ts +222 -0
- package/src/names.ts +47 -0
- package/src/options.ts +331 -0
- package/src/pipeline.ts +338 -0
- package/src/plugin.ts +579 -0
- package/src/pubsub.ts +36 -0
- package/src/results.ts +206 -0
- package/src/scalars.ts +138 -0
- package/src/schema-hash.ts +18 -0
- package/src/schema.ts +635 -0
- package/src/sdl.ts +530 -0
- package/src/testing.ts +454 -0
- package/src/tokens.ts +22 -0
- package/src/transport/abort.ts +80 -0
- package/src/transport/accept.ts +155 -0
- package/src/transport/graphiql.ts +106 -0
- package/src/transport/http.ts +373 -0
- package/src/transport/sse.ts +45 -0
- package/src/type-ref.ts +89 -0
- package/src/types.ts +225 -0
- package/src/validate-args.ts +48 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDL view of the model (`sdl.ts` prints it), from which `schemaHash` and the SDL artifact
|
|
3
|
+
* derive.
|
|
4
|
+
*/
|
|
5
|
+
import type { SdlInputValue, SdlType } from '../sdl.ts'
|
|
6
|
+
import type { ArgModel, TypeModel } from './model.ts'
|
|
7
|
+
|
|
8
|
+
/** The SDL view of a model type. */
|
|
9
|
+
export function sdlType(model: TypeModel): SdlType {
|
|
10
|
+
const base = model.description === undefined ? {} : { description: model.description }
|
|
11
|
+
switch (model.kind) {
|
|
12
|
+
case 'object':
|
|
13
|
+
case 'interface':
|
|
14
|
+
return {
|
|
15
|
+
kind: model.kind,
|
|
16
|
+
name: model.name,
|
|
17
|
+
...base,
|
|
18
|
+
...(model.interfaces.length === 0 ? {} : { interfaces: model.interfaces }),
|
|
19
|
+
fields: model.fields.map(field => ({
|
|
20
|
+
name: field.name,
|
|
21
|
+
type: field.type,
|
|
22
|
+
...(field.description === undefined ? {} : { description: field.description }),
|
|
23
|
+
...(field.deprecationReason === undefined ? {} : { deprecationReason: field.deprecationReason }),
|
|
24
|
+
...(field.args.length === 0 ? {} : { args: field.args.map(sdlInputValue) }),
|
|
25
|
+
})),
|
|
26
|
+
}
|
|
27
|
+
case 'input':
|
|
28
|
+
return { kind: 'input', name: model.name, ...base, fields: model.fields.map(sdlInputValue) }
|
|
29
|
+
case 'enum':
|
|
30
|
+
return {
|
|
31
|
+
kind: 'enum',
|
|
32
|
+
name: model.name,
|
|
33
|
+
...base,
|
|
34
|
+
values: model.values.map(value => ({
|
|
35
|
+
name: value.name,
|
|
36
|
+
...(value.description === undefined ? {} : { description: value.description }),
|
|
37
|
+
...(value.deprecationReason === undefined ? {} : { deprecationReason: value.deprecationReason }),
|
|
38
|
+
})),
|
|
39
|
+
}
|
|
40
|
+
case 'union':
|
|
41
|
+
return { kind: 'union', name: model.name, ...base, members: model.members }
|
|
42
|
+
case 'scalar':
|
|
43
|
+
return {
|
|
44
|
+
kind: 'scalar',
|
|
45
|
+
name: model.name,
|
|
46
|
+
...base,
|
|
47
|
+
...(model.definition === undefined ? { builtin: true } : {}),
|
|
48
|
+
...(model.specifiedByURL === undefined ? {} : { specifiedByURL: model.specifiedByURL }),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sdlInputValue(arg: ArgModel): SdlInputValue {
|
|
54
|
+
return {
|
|
55
|
+
name: arg.name,
|
|
56
|
+
type: arg.type,
|
|
57
|
+
...(arg.description === undefined ? {} : { description: arg.description }),
|
|
58
|
+
...(arg.deprecationReason === undefined ? {} : { deprecationReason: arg.deprecationReason }),
|
|
59
|
+
...(arg.defaultValue === undefined ? {} : { defaultValue: arg.defaultValue }),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -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
|
+
}
|