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