@znight/graphql 0.2.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/README.md +288 -5
- package/dist/compiler.d.mts +24 -3
- package/dist/compiler.mjs +1876 -109
- package/dist/compiler.mjs.map +1 -1
- package/dist/index.d.mts +49 -8
- package/dist/index.mjs +10 -4
- package/dist/index.mjs.map +1 -1
- package/dist/names-BlzDM5cF.mjs.map +1 -1
- package/dist/{plugin-BdBxr5NQ.mjs → plugin-CR_wVfRU.mjs} +247 -24
- package/dist/plugin-CR_wVfRU.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/dist/{types-CgbV9LjD.d.mts → types-DQ713UgI.d.mts} +34 -2
- package/package.json +8 -8
- package/src/compiler/analyze.ts +129 -10
- package/src/compiler/builder.ts +6 -85
- package/src/compiler/checks.ts +105 -0
- package/src/compiler/conformance.ts +1068 -0
- package/src/compiler/constants.ts +6 -0
- package/src/compiler/data.ts +10 -0
- package/src/compiler/generate.ts +41 -27
- package/src/compiler/graphql-js.ts +42 -0
- package/src/compiler/index.ts +8 -3
- package/src/compiler/locate.ts +39 -1
- package/src/compiler/model.ts +16 -1
- package/src/compiler/options.ts +39 -8
- package/src/compiler/schema-first.ts +595 -0
- package/src/compiler/sdl-source.ts +529 -0
- package/src/compiler/sdl-view.ts +3 -1
- package/src/compiler/suggest.ts +39 -0
- package/src/decorators.ts +9 -3
- package/src/index.ts +5 -0
- package/src/metadata.ts +28 -1
- package/src/options.ts +23 -1
- package/src/pipeline.ts +66 -5
- package/src/plugin.ts +27 -5
- package/src/results.ts +206 -0
- package/src/scalars.ts +10 -3
- package/src/schema.ts +90 -12
- package/src/sdl.ts +17 -2
- package/src/types.ts +35 -0
- 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
|
+
}
|