@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,727 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `analyzeClass` of the graphql extension (RFC 0005 §6.1, §6.5): registers `@Resolver` classes as
|
|
3
|
+
* providers (and roots), checks their field methods and parameters, binds `@UseGuard` guards and
|
|
4
|
+
* `Loader<L>` parameters with lazy edges (resolved per call, in the current scope), reads route
|
|
5
|
+
* decorators with the readers of `@znight/http/compiler`, and stores one {@link ResolverData} per
|
|
6
|
+
* resolver. Types are not read here: `validate` builds the schema model from the method signatures.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
AnalyzeContext,
|
|
11
|
+
ClassInfo,
|
|
12
|
+
ClassRef,
|
|
13
|
+
DecoratorInfo,
|
|
14
|
+
Location,
|
|
15
|
+
MethodInfo,
|
|
16
|
+
ParameterInfo,
|
|
17
|
+
StaticValue,
|
|
18
|
+
} from '@znight/compiler'
|
|
19
|
+
import type { Scope } from '@znight/core'
|
|
20
|
+
import type { MetaEntry } from '@znight/http/compiler'
|
|
21
|
+
import type { FieldData, FieldOperation, GraphQLData, ParamData, ResolverData } from './data.ts'
|
|
22
|
+
import type { ResolvedGraphQLCompilerOptions } from './options.ts'
|
|
23
|
+
|
|
24
|
+
import { effectiveRouteMeta, readRouteMeta } from '@znight/http/compiler'
|
|
25
|
+
|
|
26
|
+
import { isGraphQLName } from '../names.ts'
|
|
27
|
+
import { Codes, ROOT_REASON } from './constants.ts'
|
|
28
|
+
import {
|
|
29
|
+
EXTENSION_NAME,
|
|
30
|
+
FIELD_DECORATORS,
|
|
31
|
+
GraphQLIds,
|
|
32
|
+
HTTP_CONTROLLER,
|
|
33
|
+
HTTP_LOOK_ALIKES,
|
|
34
|
+
PARAMETER_DECORATORS,
|
|
35
|
+
RUNTIME_MODULE,
|
|
36
|
+
} from './ids.ts'
|
|
37
|
+
|
|
38
|
+
type Context = AnalyzeContext<GraphQLData>
|
|
39
|
+
|
|
40
|
+
const SCOPES: readonly Scope[] = ['singleton', 'request', 'transient']
|
|
41
|
+
|
|
42
|
+
/** Shape options reading `Loader<L>` parameters: the alias is kept, `L` stays a named shape. */
|
|
43
|
+
const LOADER_SHAPE = Object.freeze({ keep: Object.freeze([GraphQLIds.Loader]), jsdoc: false })
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Analyses a class carrying at least one graphql decorator (class, method or parameter). In
|
|
47
|
+
* schema-first mode (RFC 0007), `@Resolver('Type')` names the SDL type of the field resolvers and
|
|
48
|
+
* `@Parent()` is optional.
|
|
49
|
+
*/
|
|
50
|
+
export function analyzeGraphQLClass(cls: ClassInfo, ctx: Context, options: ResolvedGraphQLCompilerOptions): void {
|
|
51
|
+
const resolvers = cls.decorators.filter(decorator => decorator.identity === GraphQLIds.Resolver)
|
|
52
|
+
const resolver = resolvers[0]
|
|
53
|
+
if (resolver === undefined) {
|
|
54
|
+
reportOutsideResolver(cls, ctx)
|
|
55
|
+
storeClaims(ctx, cls, undefined, options)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
for (const extra of resolvers.slice(1)) {
|
|
59
|
+
error(ctx, Codes.InvalidDecorator, extra.location, `@Resolver is applied twice to ${cls.displayName}`, [
|
|
60
|
+
'Apply it once',
|
|
61
|
+
])
|
|
62
|
+
}
|
|
63
|
+
const controller = cls.decorators.find(decorator => decorator.identity === HTTP_CONTROLLER)
|
|
64
|
+
if (controller !== undefined) {
|
|
65
|
+
error(
|
|
66
|
+
ctx,
|
|
67
|
+
Codes.InvalidDecorator,
|
|
68
|
+
resolver.location,
|
|
69
|
+
`${cls.displayName} cannot be both a @Resolver and a @Controller`,
|
|
70
|
+
[
|
|
71
|
+
'A class is the provider of one extension: move the GraphQL fields to their own @Resolver() class (both may call the same service)',
|
|
72
|
+
],
|
|
73
|
+
[{ message: '@Controller is applied here', location: controller.location }],
|
|
74
|
+
)
|
|
75
|
+
storeClaims(ctx, cls, resolver, options)
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
if (ctx.options.library) {
|
|
79
|
+
error(
|
|
80
|
+
ctx,
|
|
81
|
+
Codes.InvalidResolver,
|
|
82
|
+
resolver.location,
|
|
83
|
+
`${cls.displayName} is a @Resolver in a compiled provider library: the schema is derived from the sources`,
|
|
84
|
+
['Publish the package as a source library (without library: true), so that applications compile its resolvers'],
|
|
85
|
+
)
|
|
86
|
+
storeClaims(ctx, cls, resolver, options)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (cls.abstract || cls.typeParameters.length > 0) {
|
|
90
|
+
error(
|
|
91
|
+
ctx,
|
|
92
|
+
Codes.InvalidDecorator,
|
|
93
|
+
resolver.location,
|
|
94
|
+
cls.abstract
|
|
95
|
+
? `@Resolver cannot be applied to the abstract class ${cls.displayName}`
|
|
96
|
+
: `@Resolver cannot be applied to the generic class ${cls.displayName}<${cls.typeParameters.join(', ')}>`,
|
|
97
|
+
[cls.abstract ? 'Resolvers are instantiated: remove `abstract`' : 'Remove the type parameters of the resolver'],
|
|
98
|
+
)
|
|
99
|
+
storeClaims(ctx, cls, resolver, options)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { scope, typeName } = resolverArguments(ctx, resolver, options.schema !== false)
|
|
104
|
+
ctx.registerProvider(cls, { scope })
|
|
105
|
+
// resolvers are entry points of the plugin: roots of ZNIGHT019 and prune
|
|
106
|
+
ctx.addRoot(cls, { reason: ROOT_REASON, location: resolver.location })
|
|
107
|
+
|
|
108
|
+
const classGuards = guardsOf(ctx, cls, cls.decorators)
|
|
109
|
+
const classMeta = readRouteMeta(ctx, cls)
|
|
110
|
+
const fields: FieldData[] = []
|
|
111
|
+
for (const method of cls.methods) {
|
|
112
|
+
const decorators = method.decorators.filter(isFieldDecorator)
|
|
113
|
+
if (decorators.length === 0) {
|
|
114
|
+
reportStrayMethodDecorators(cls, method, ctx)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
fields.push(...analyzeMethod(ctx, cls, method, decorators, classMeta, options.schema !== false))
|
|
118
|
+
}
|
|
119
|
+
const data: ResolverData = {
|
|
120
|
+
kind: 'resolver',
|
|
121
|
+
host: cls,
|
|
122
|
+
scope,
|
|
123
|
+
location: resolver.location,
|
|
124
|
+
guards: classGuards,
|
|
125
|
+
fields,
|
|
126
|
+
...(typeName === undefined ? {} : { typeName }),
|
|
127
|
+
}
|
|
128
|
+
if (options.schema !== false && typeName === undefined) {
|
|
129
|
+
for (const field of fields) {
|
|
130
|
+
if (field.operation !== 'field') continue
|
|
131
|
+
error(
|
|
132
|
+
ctx,
|
|
133
|
+
Codes.InvalidResolver,
|
|
134
|
+
field.location,
|
|
135
|
+
`@ResolveField ${cls.displayName}.${field.method} needs the type it resolves a field of: name it in @Resolver (schema-first)`,
|
|
136
|
+
[
|
|
137
|
+
`@Resolver('Event') on ${cls.displayName}, where Event is the object type of the SDL declaring '${field.name}'`,
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
ctx.store(data)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Schema-first: a class rejected above (no `@Resolver`, abstract, a controller...) still names the
|
|
147
|
+
* fields its methods resolve, stored as an `invalid` resolver, so that these fields are not also
|
|
148
|
+
* reported as missing a resolver (one diagnostic per mistake).
|
|
149
|
+
*/
|
|
150
|
+
function storeClaims(
|
|
151
|
+
ctx: Context,
|
|
152
|
+
cls: ClassInfo,
|
|
153
|
+
resolver: DecoratorInfo | undefined,
|
|
154
|
+
options: ResolvedGraphQLCompilerOptions,
|
|
155
|
+
): void {
|
|
156
|
+
if (options.schema === false) return
|
|
157
|
+
const typeArg = resolver?.args[0]
|
|
158
|
+
const typeName =
|
|
159
|
+
typeArg?.evaluated === true && typeof typeArg.value === 'string'
|
|
160
|
+
? { name: typeArg.value, location: typeArg.location }
|
|
161
|
+
: undefined
|
|
162
|
+
const fields: FieldData[] = []
|
|
163
|
+
for (const method of cls.methods) {
|
|
164
|
+
for (const decorator of method.decorators.filter(isFieldDecorator)) {
|
|
165
|
+
const arg = decorator.args[0]
|
|
166
|
+
fields.push({
|
|
167
|
+
method: method.name,
|
|
168
|
+
operation: FIELD_DECORATORS.get(decorator.identity as string) as FieldOperation,
|
|
169
|
+
name: arg?.evaluated === true && typeof arg.value === 'string' ? arg.value : method.name,
|
|
170
|
+
location: decorator.location,
|
|
171
|
+
methodLocation: method.location,
|
|
172
|
+
guards: [],
|
|
173
|
+
meta: [],
|
|
174
|
+
params: [],
|
|
175
|
+
complete: false,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (fields.length === 0) return
|
|
180
|
+
ctx.store({
|
|
181
|
+
kind: 'resolver',
|
|
182
|
+
host: cls,
|
|
183
|
+
scope: 'singleton',
|
|
184
|
+
location: resolver?.location ?? cls.location,
|
|
185
|
+
guards: [],
|
|
186
|
+
fields,
|
|
187
|
+
invalid: true,
|
|
188
|
+
...(typeName === undefined ? {} : { typeName }),
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isFieldDecorator(decorator: DecoratorInfo): boolean {
|
|
193
|
+
return decorator.identity !== undefined && FIELD_DECORATORS.has(decorator.identity)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function isParameterDecorator(decorator: DecoratorInfo): boolean {
|
|
197
|
+
return decorator.identity !== undefined && PARAMETER_DECORATORS.has(decorator.identity)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** `@Resolver()`, `@Resolver({ scope })`, `@Resolver('Event')`, `@Resolver('Event', { scope })` (schema-first). */
|
|
201
|
+
function resolverArguments(
|
|
202
|
+
ctx: Context,
|
|
203
|
+
decorator: DecoratorInfo,
|
|
204
|
+
schemaFirst: boolean,
|
|
205
|
+
): { scope: Scope; typeName?: { readonly name: string; readonly location: Location } } {
|
|
206
|
+
const [first, second] = decorator.args
|
|
207
|
+
if (first?.evaluated !== true || typeof first.value !== 'string') {
|
|
208
|
+
if (second !== undefined) {
|
|
209
|
+
error(ctx, Codes.InvalidDecorator, second.location, '@Resolver takes the options as its only argument here', [
|
|
210
|
+
"@Resolver({ scope: 'request' }), or @Resolver('Event', { scope: 'request' }) in schema-first mode",
|
|
211
|
+
])
|
|
212
|
+
}
|
|
213
|
+
return { scope: resolverScope(ctx, first) }
|
|
214
|
+
}
|
|
215
|
+
const scope = resolverScope(ctx, second)
|
|
216
|
+
const name = first.value
|
|
217
|
+
if (!schemaFirst) {
|
|
218
|
+
error(
|
|
219
|
+
ctx,
|
|
220
|
+
Codes.InvalidDecorator,
|
|
221
|
+
first.location,
|
|
222
|
+
`@Resolver('${name}'): naming the type is schema-first only, the code-first schema is derived from the types`,
|
|
223
|
+
[
|
|
224
|
+
'Remove the type name: @ResolveField methods add fields to the type of their @Parent() parameter',
|
|
225
|
+
"Or switch to schema-first: graphql.configure({ schema: 'src/**/*.graphql' }) in znight.config.ts",
|
|
226
|
+
],
|
|
227
|
+
)
|
|
228
|
+
return { scope }
|
|
229
|
+
}
|
|
230
|
+
if (!isGraphQLName(name)) {
|
|
231
|
+
error(ctx, Codes.NameConflict, first.location, `'${name}' is not a valid GraphQL type name (@Resolver)`, [
|
|
232
|
+
'GraphQL names match /^[_A-Za-z][_0-9A-Za-z]*$/ and do not start with __',
|
|
233
|
+
])
|
|
234
|
+
return { scope }
|
|
235
|
+
}
|
|
236
|
+
return { scope, typeName: { name, location: first.location } }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The `{ scope }` options of `@Resolver`. */
|
|
240
|
+
function resolverScope(ctx: Context, arg: DecoratorInfo['args'][number] | undefined): Scope {
|
|
241
|
+
if (arg === undefined || !arg.evaluated || arg.value === undefined) return 'singleton'
|
|
242
|
+
const value = arg.value
|
|
243
|
+
if (!isRecord(value)) {
|
|
244
|
+
error(ctx, Codes.InvalidDecorator, arg.location, `@Resolver expects { scope? }, got ${describe(value)}`, [
|
|
245
|
+
"@Resolver() or @Resolver({ scope: 'request' })",
|
|
246
|
+
])
|
|
247
|
+
return 'singleton'
|
|
248
|
+
}
|
|
249
|
+
let scope: Scope = 'singleton'
|
|
250
|
+
for (const [key, option] of Object.entries(value)) {
|
|
251
|
+
if (key !== 'scope') {
|
|
252
|
+
error(ctx, Codes.InvalidDecorator, arg.location, `@Resolver: unknown option "${key}"`, ['Options: { scope? }'])
|
|
253
|
+
} else if (SCOPES.includes(option as Scope)) {
|
|
254
|
+
scope = option as Scope
|
|
255
|
+
} else if (option !== undefined) {
|
|
256
|
+
error(ctx, Codes.InvalidDecorator, arg.location, `@Resolver: invalid scope ${describe(option)}`, [
|
|
257
|
+
`Use one of ${SCOPES.map(candidate => `'${candidate}'`).join(', ')}`,
|
|
258
|
+
])
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return scope
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** `@UseGuard(A, B)` decorators of a target: guard classes bound with lazy edges (C3). */
|
|
265
|
+
function guardsOf(ctx: Context, cls: ClassInfo, decorators: readonly DecoratorInfo[]): ClassRef[] {
|
|
266
|
+
const guards: ClassRef[] = []
|
|
267
|
+
for (const decorator of decorators) {
|
|
268
|
+
if (decorator.identity !== GraphQLIds.UseGuard) continue
|
|
269
|
+
if (decorator.args.length === 0) {
|
|
270
|
+
error(ctx, Codes.InvalidDecorator, decorator.location, '@UseGuard needs at least one guard class', [
|
|
271
|
+
'@UseGuard(AccessGuard)',
|
|
272
|
+
])
|
|
273
|
+
}
|
|
274
|
+
for (const arg of decorator.args) {
|
|
275
|
+
// resolved per resolver call in the current scope: a singleton resolver may use a request-scoped guard
|
|
276
|
+
const ref = ctx.resolveClassRef(arg, { from: cls, role: 'guard', lazy: true })
|
|
277
|
+
if (ref !== undefined) guards.push(ref)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return guards
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ------------------------------------------------------------------------------------------ methods
|
|
284
|
+
|
|
285
|
+
function analyzeMethod(
|
|
286
|
+
ctx: Context,
|
|
287
|
+
cls: ClassInfo,
|
|
288
|
+
method: MethodInfo,
|
|
289
|
+
decorators: readonly DecoratorInfo[],
|
|
290
|
+
classMeta: readonly MetaEntry[],
|
|
291
|
+
schemaFirst: boolean,
|
|
292
|
+
): FieldData[] {
|
|
293
|
+
const where = `${cls.displayName}.${method.name}`
|
|
294
|
+
const primary = decorators[0] as DecoratorInfo
|
|
295
|
+
let complete = checkMethodShape(ctx, method, primary, where)
|
|
296
|
+
complete = checkForeignHandler(ctx, method, primary, where) && complete
|
|
297
|
+
const params: (ParamData | undefined)[] = []
|
|
298
|
+
for (const param of method.params) {
|
|
299
|
+
const data = analyzeParameter(ctx, cls, param, where)
|
|
300
|
+
params.push(data)
|
|
301
|
+
if (data === undefined) complete = false
|
|
302
|
+
}
|
|
303
|
+
const parents = params.filter(param => param?.kind === 'parent')
|
|
304
|
+
for (const extra of parents.slice(1)) {
|
|
305
|
+
error(
|
|
306
|
+
ctx,
|
|
307
|
+
Codes.InvalidResolver,
|
|
308
|
+
(extra as { location: Location }).location,
|
|
309
|
+
`${where} has several @Parent() parameters`,
|
|
310
|
+
['Declare the parent object once'],
|
|
311
|
+
)
|
|
312
|
+
complete = false
|
|
313
|
+
}
|
|
314
|
+
const guards = guardsOf(ctx, cls, method.decorators)
|
|
315
|
+
const meta = effectiveRouteMeta(classMeta, readRouteMeta(ctx, cls, method))
|
|
316
|
+
|
|
317
|
+
const fields: FieldData[] = []
|
|
318
|
+
const seen = new Set<string>()
|
|
319
|
+
for (const decorator of decorators) {
|
|
320
|
+
const operation = FIELD_DECORATORS.get(decorator.identity as string) as FieldOperation
|
|
321
|
+
const explicit = fieldName(ctx, decorator, method, where)
|
|
322
|
+
// an invalid name is reported: the field stays known (incomplete), so no "no @Query" cascade
|
|
323
|
+
const name = explicit ?? method.name
|
|
324
|
+
const key = `${operation}\0${name}`
|
|
325
|
+
if (seen.has(key)) {
|
|
326
|
+
error(
|
|
327
|
+
ctx,
|
|
328
|
+
Codes.InvalidDecorator,
|
|
329
|
+
decorator.location,
|
|
330
|
+
`@${decorator.name}('${name}') is applied twice to ${where}`,
|
|
331
|
+
['Apply each field decorator once'],
|
|
332
|
+
)
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
seen.add(key)
|
|
336
|
+
let fieldComplete = complete && explicit !== undefined
|
|
337
|
+
// schema-first: the type is named by @Resolver('Type'), the parent is optional (NestJS)
|
|
338
|
+
if (operation === 'field' && parents.length === 0 && !schemaFirst) {
|
|
339
|
+
error(ctx, Codes.InvalidResolver, decorator.location, `@ResolveField ${where} has no @Parent() parameter`, [
|
|
340
|
+
`Declare the parent object first: ${method.name}(@Parent() parent: ParentType, ...)`,
|
|
341
|
+
])
|
|
342
|
+
fieldComplete = false
|
|
343
|
+
} else if (operation !== 'field' && parents.length > 0) {
|
|
344
|
+
error(
|
|
345
|
+
ctx,
|
|
346
|
+
Codes.InvalidResolver,
|
|
347
|
+
(parents[0] as { location: Location }).location,
|
|
348
|
+
`@Parent() is only available in @ResolveField methods, not in the @${decorator.name} ${where}`,
|
|
349
|
+
['Root fields have no parent object: remove the parameter'],
|
|
350
|
+
)
|
|
351
|
+
fieldComplete = false
|
|
352
|
+
}
|
|
353
|
+
fields.push({
|
|
354
|
+
method: method.name,
|
|
355
|
+
operation,
|
|
356
|
+
name,
|
|
357
|
+
location: decorator.location,
|
|
358
|
+
methodLocation: method.location,
|
|
359
|
+
guards,
|
|
360
|
+
meta,
|
|
361
|
+
params,
|
|
362
|
+
complete: fieldComplete,
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
return fields
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The field name: the static decorator argument, or the method name. */
|
|
369
|
+
function fieldName(ctx: Context, decorator: DecoratorInfo, method: MethodInfo, where: string): string | undefined {
|
|
370
|
+
const arg = decorator.args[0]
|
|
371
|
+
if (arg === undefined || (arg.evaluated && arg.value === undefined)) {
|
|
372
|
+
if (isGraphQLName(method.name)) return method.name
|
|
373
|
+
error(
|
|
374
|
+
ctx,
|
|
375
|
+
Codes.NameConflict,
|
|
376
|
+
decorator.location,
|
|
377
|
+
`'${method.name}' is not a valid GraphQL field name (${where}): name the field in the decorator`,
|
|
378
|
+
['GraphQL names match /^[_A-Za-z][_0-9A-Za-z]*$/ and do not start with __', `@${decorator.name}('validName')`],
|
|
379
|
+
)
|
|
380
|
+
return undefined
|
|
381
|
+
}
|
|
382
|
+
if (!arg.evaluated) return undefined // not statically evaluable: reported by the compiler
|
|
383
|
+
const value = arg.value
|
|
384
|
+
if (typeof value !== 'string') {
|
|
385
|
+
error(
|
|
386
|
+
ctx,
|
|
387
|
+
Codes.InvalidDecorator,
|
|
388
|
+
arg.location,
|
|
389
|
+
`@${decorator.name} on ${where} expects a field name, got ${describe(value)}`,
|
|
390
|
+
[`@${decorator.name}('${method.name}') or @${decorator.name}()`],
|
|
391
|
+
)
|
|
392
|
+
return undefined
|
|
393
|
+
}
|
|
394
|
+
if (!isGraphQLName(value)) {
|
|
395
|
+
error(ctx, Codes.NameConflict, arg.location, `'${value}' is not a valid GraphQL field name (${where})`, [
|
|
396
|
+
'GraphQL names match /^[_A-Za-z][_0-9A-Za-z]*$/ and do not start with __',
|
|
397
|
+
])
|
|
398
|
+
return undefined
|
|
399
|
+
}
|
|
400
|
+
return value
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Resolver methods are called by the generated code: `resolver.method(...)`. */
|
|
404
|
+
function checkMethodShape(ctx: Context, method: MethodInfo, decorator: DecoratorInfo, where: string): boolean {
|
|
405
|
+
const name = `@${decorator.name}`
|
|
406
|
+
let problem: { message: string; hint: string } | undefined
|
|
407
|
+
if (method.kind !== 'method') {
|
|
408
|
+
problem = { message: `${name} cannot be applied to the accessor ${where}`, hint: 'Use a regular method' }
|
|
409
|
+
} else if (method.static) {
|
|
410
|
+
problem = {
|
|
411
|
+
message: `${name} cannot be applied to the static method ${where}`,
|
|
412
|
+
hint: 'Fields are resolved on the resolver instance: remove `static`',
|
|
413
|
+
}
|
|
414
|
+
} else if (method.privateName) {
|
|
415
|
+
problem = {
|
|
416
|
+
message: `${name} cannot be applied to the #private method ${where}`,
|
|
417
|
+
hint: 'Use a public method: the generated code calls it',
|
|
418
|
+
}
|
|
419
|
+
} else if (method.visibility !== 'public') {
|
|
420
|
+
problem = {
|
|
421
|
+
message: `${where} must be public: ${name} methods are called by the generated code`,
|
|
422
|
+
hint: `Remove the '${method.visibility}' modifier`,
|
|
423
|
+
}
|
|
424
|
+
} else if (method.computed) {
|
|
425
|
+
problem = {
|
|
426
|
+
message: `${name} cannot be applied to ${where}: computed method names are not supported`,
|
|
427
|
+
hint: 'Use a plain method name',
|
|
428
|
+
}
|
|
429
|
+
} else if (method.typeParameters.length > 0) {
|
|
430
|
+
problem = {
|
|
431
|
+
message: `${name} cannot be applied to the generic method ${where}<${method.typeParameters.join(', ')}>`,
|
|
432
|
+
hint: 'The schema is derived from the signature: remove the type parameters',
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (problem === undefined) return true
|
|
436
|
+
error(ctx, Codes.InvalidDecorator, decorator.location, problem.message, [problem.hint])
|
|
437
|
+
return false
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** A resolver method cannot also be handled by another extension (`@Get`, `@OnMessage`, `@Cron`). */
|
|
441
|
+
function checkForeignHandler(ctx: Context, method: MethodInfo, decorator: DecoratorInfo, where: string): boolean {
|
|
442
|
+
for (const other of method.decorators) {
|
|
443
|
+
const owner = other.owner
|
|
444
|
+
if (owner === undefined || owner === EXTENSION_NAME || owner === 'core' || owner === 'manifest') continue
|
|
445
|
+
error(
|
|
446
|
+
ctx,
|
|
447
|
+
Codes.InvalidDecorator,
|
|
448
|
+
other.location,
|
|
449
|
+
`${where} cannot be both a GraphQL field (@${decorator.name}) and a @${other.name} handler`,
|
|
450
|
+
['A class is the provider of one extension: let both handlers call a shared service'],
|
|
451
|
+
)
|
|
452
|
+
return false
|
|
453
|
+
}
|
|
454
|
+
return true
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// --------------------------------------------------------------------------------------- parameters
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Graphql parameter decorators of a parameter, de-duplicated by position (the compiler reports the
|
|
461
|
+
* decorators of a parameter with a default value twice).
|
|
462
|
+
*/
|
|
463
|
+
function parameterDecorators(param: ParameterInfo): DecoratorInfo[] {
|
|
464
|
+
const seen = new Set<number>()
|
|
465
|
+
return param.decorators.filter(decorator => {
|
|
466
|
+
if (!isParameterDecorator(decorator) || seen.has(decorator.location.start)) return false
|
|
467
|
+
seen.add(decorator.location.start)
|
|
468
|
+
return true
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function analyzeParameter(ctx: Context, cls: ClassInfo, param: ParameterInfo, where: string): ParamData | undefined {
|
|
473
|
+
const decorators = parameterDecorators(param)
|
|
474
|
+
const decorator = decorators[0]
|
|
475
|
+
if (decorator === undefined) {
|
|
476
|
+
const lookAlike = param.decorators.find(
|
|
477
|
+
found => found.identity !== undefined && HTTP_LOOK_ALIKES.has(found.identity),
|
|
478
|
+
)
|
|
479
|
+
if (lookAlike !== undefined) {
|
|
480
|
+
const replacement = (HTTP_LOOK_ALIKES.get(lookAlike.identity as string) as string).split('#')[1] as string
|
|
481
|
+
error(
|
|
482
|
+
ctx,
|
|
483
|
+
Codes.InvalidDecorator,
|
|
484
|
+
lookAlike.location,
|
|
485
|
+
`@${lookAlike.name} on parameter '${param.name}' of ${where} is imported from '@znight/http': resolver parameters use @${replacement}() from '${RUNTIME_MODULE}'`,
|
|
486
|
+
[`Import it from '${RUNTIME_MODULE}': import { ${replacement} } from '${RUNTIME_MODULE}'`],
|
|
487
|
+
)
|
|
488
|
+
return undefined
|
|
489
|
+
}
|
|
490
|
+
error(
|
|
491
|
+
ctx,
|
|
492
|
+
Codes.InvalidDecorator,
|
|
493
|
+
param.location,
|
|
494
|
+
`Parameter '${param.name}' of ${where} has no @Args(), @Parent(), @Ctx(), @Info() or @Loader() decorator`,
|
|
495
|
+
[
|
|
496
|
+
'Every resolver parameter is filled by GraphQL: @Args(name?, schema?) for arguments, @Parent() for the parent object, @Ctx() for the GraphQLContext, @Info() for the GraphQLResolveInfo, @Loader() for a DataLoader',
|
|
497
|
+
],
|
|
498
|
+
)
|
|
499
|
+
return undefined
|
|
500
|
+
}
|
|
501
|
+
if (decorators.length > 1) {
|
|
502
|
+
error(
|
|
503
|
+
ctx,
|
|
504
|
+
Codes.InvalidDecorator,
|
|
505
|
+
(decorators[1] as DecoratorInfo).location,
|
|
506
|
+
`Parameter '${param.name}' of ${where} has several GraphQL decorators (${decorators.map(found => `@${found.name}`).join(', ')})`,
|
|
507
|
+
['Keep a single one'],
|
|
508
|
+
)
|
|
509
|
+
return undefined
|
|
510
|
+
}
|
|
511
|
+
if (param.rest) {
|
|
512
|
+
error(
|
|
513
|
+
ctx,
|
|
514
|
+
Codes.InvalidDecorator,
|
|
515
|
+
decorator.location,
|
|
516
|
+
`@${decorator.name} cannot be applied to the rest parameter '...${param.name}' of ${where}`,
|
|
517
|
+
)
|
|
518
|
+
return undefined
|
|
519
|
+
}
|
|
520
|
+
switch (PARAMETER_DECORATORS.get(decorator.identity as string)) {
|
|
521
|
+
case 'args':
|
|
522
|
+
return argsParameter(ctx, decorator, param, where)
|
|
523
|
+
case 'parent':
|
|
524
|
+
return { kind: 'parent', location: decorator.location }
|
|
525
|
+
case 'ctx':
|
|
526
|
+
return { kind: 'ctx' }
|
|
527
|
+
case 'info':
|
|
528
|
+
return { kind: 'info' }
|
|
529
|
+
default:
|
|
530
|
+
return loaderParameter(ctx, cls, param, where)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** `@Args()`, `@Args('name')`, `@Args('name', Schema)`, `@Args(Schema)`. */
|
|
535
|
+
function argsParameter(
|
|
536
|
+
ctx: Context,
|
|
537
|
+
decorator: DecoratorInfo,
|
|
538
|
+
param: ParameterInfo,
|
|
539
|
+
where: string,
|
|
540
|
+
): ParamData | undefined {
|
|
541
|
+
const [first, second, ...extra] = decorator.args
|
|
542
|
+
if (extra.length > 0) {
|
|
543
|
+
error(ctx, Codes.InvalidDecorator, decorator.location, `@Args takes at most two arguments (${where})`, [
|
|
544
|
+
"@Args(), @Args('name'), @Args('name', Schema) or @Args(Schema)",
|
|
545
|
+
])
|
|
546
|
+
return undefined
|
|
547
|
+
}
|
|
548
|
+
let name: string | undefined
|
|
549
|
+
let schemaArg = second
|
|
550
|
+
if (first !== undefined) {
|
|
551
|
+
if (!first.evaluated && first.ref === undefined) return undefined // reported by the compiler
|
|
552
|
+
if (typeof first.value === 'string') {
|
|
553
|
+
if (!isGraphQLName(first.value)) {
|
|
554
|
+
error(
|
|
555
|
+
ctx,
|
|
556
|
+
Codes.NameConflict,
|
|
557
|
+
first.location,
|
|
558
|
+
`'${first.value}' is not a valid GraphQL argument name (parameter '${param.name}' of ${where})`,
|
|
559
|
+
['GraphQL names match /^[_A-Za-z][_0-9A-Za-z]*$/ and do not start with __'],
|
|
560
|
+
)
|
|
561
|
+
return undefined
|
|
562
|
+
}
|
|
563
|
+
name = first.value
|
|
564
|
+
} else if (first.ref !== undefined) {
|
|
565
|
+
if (second !== undefined) {
|
|
566
|
+
error(ctx, Codes.InvalidDecorator, second.location, `@Args(Schema) takes no second argument (${where})`, [
|
|
567
|
+
"@Args('name', Schema) validates one argument, @Args(Schema) every argument",
|
|
568
|
+
])
|
|
569
|
+
return undefined
|
|
570
|
+
}
|
|
571
|
+
schemaArg = first
|
|
572
|
+
} else if (first.value !== undefined) {
|
|
573
|
+
error(
|
|
574
|
+
ctx,
|
|
575
|
+
Codes.InvalidDecorator,
|
|
576
|
+
first.location,
|
|
577
|
+
`@Args on parameter '${param.name}' of ${where} expects an argument name or a schema, got ${describe(first.value)}`,
|
|
578
|
+
["@Args('id'), @Args() or @Args('input', Schema)"],
|
|
579
|
+
)
|
|
580
|
+
return undefined
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (schemaArg !== undefined && !(schemaArg.evaluated && schemaArg.value === undefined)) {
|
|
584
|
+
if (schemaArg.ref === undefined) {
|
|
585
|
+
if (schemaArg.evaluated) {
|
|
586
|
+
error(
|
|
587
|
+
ctx,
|
|
588
|
+
Codes.InvalidDecorator,
|
|
589
|
+
schemaArg.location,
|
|
590
|
+
`The schema of @Args on parameter '${param.name}' of ${where} must be a reference to an exported schema`,
|
|
591
|
+
['Declare it with `export const Schema = z.object(...)` and pass Schema: the generated module imports it'],
|
|
592
|
+
)
|
|
593
|
+
}
|
|
594
|
+
return undefined
|
|
595
|
+
}
|
|
596
|
+
return { kind: 'args', name, schema: schemaArg.ref, location: decorator.location }
|
|
597
|
+
}
|
|
598
|
+
return { kind: 'args', name, schema: undefined, location: decorator.location }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/** `@Loader() authors: Loader<AuthorsById>`: binds `AuthorsById` with a lazy edge. */
|
|
602
|
+
function loaderParameter(ctx: Context, cls: ClassInfo, param: ParameterInfo, where: string): ParamData | undefined {
|
|
603
|
+
const root = ctx.types.shape({ parameter: param }, LOADER_SHAPE)
|
|
604
|
+
const shape = root.root
|
|
605
|
+
const argument = shape.kind === 'reference' && shape.identity === GraphQLIds.Loader ? shape.typeArgs[0] : undefined
|
|
606
|
+
const identity = argument?.kind === 'named' ? root.named.get(argument.key)?.identity : undefined
|
|
607
|
+
if (identity === undefined) {
|
|
608
|
+
error(
|
|
609
|
+
ctx,
|
|
610
|
+
Codes.InvalidLoader,
|
|
611
|
+
param.typeLocation,
|
|
612
|
+
`@Loader() parameter '${param.name}' of ${where} must be typed Loader<L>, with L a loader class`,
|
|
613
|
+
[
|
|
614
|
+
`${param.name}: Loader<AuthorsById>, where AuthorsById is a provider implementing BatchLoad<K, V> (import type { Loader } from '${RUNTIME_MODULE}')`,
|
|
615
|
+
],
|
|
616
|
+
)
|
|
617
|
+
return undefined
|
|
618
|
+
}
|
|
619
|
+
// the DataLoader is created per execution, in the current scope: a lazy edge (no ZNIGHT003)
|
|
620
|
+
const binding = ctx.bindInjection(
|
|
621
|
+
{ type: identity, location: param.typeLocation, label: `loader '${param.name}' of ${where}()` },
|
|
622
|
+
{ from: cls, lazy: true },
|
|
623
|
+
)
|
|
624
|
+
return { kind: 'loader', binding, identity, location: param.typeLocation }
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// ------------------------------------------------------------------------------------ outside resolvers
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Graphql decorators on a class without `@Resolver`: field methods are not inherited (a base
|
|
631
|
+
* class), and parameter or guard decorators have no effect.
|
|
632
|
+
*/
|
|
633
|
+
function reportOutsideResolver(cls: ClassInfo, ctx: Context): void {
|
|
634
|
+
for (const decorator of cls.decorators) {
|
|
635
|
+
if (decorator.identity === GraphQLIds.UseGuard) {
|
|
636
|
+
error(
|
|
637
|
+
ctx,
|
|
638
|
+
Codes.InvalidDecorator,
|
|
639
|
+
decorator.location,
|
|
640
|
+
`@UseGuard has no effect: ${cls.displayName} is not a @Resolver`,
|
|
641
|
+
[`Add @Resolver() to ${cls.displayName}`],
|
|
642
|
+
)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
for (const method of cls.methods) {
|
|
646
|
+
const fields = method.decorators.filter(isFieldDecorator)
|
|
647
|
+
for (const field of fields) {
|
|
648
|
+
error(
|
|
649
|
+
ctx,
|
|
650
|
+
Codes.InvalidResolver,
|
|
651
|
+
field.location,
|
|
652
|
+
`@${field.name} ${cls.displayName}.${method.name} is declared on a class that is not a @Resolver`,
|
|
653
|
+
[
|
|
654
|
+
cls.abstract
|
|
655
|
+
? `Resolver methods are not inherited: declare the field on the concrete @Resolver() class (it may call ${method.name} of the base class)`
|
|
656
|
+
: `Add @Resolver() to ${cls.displayName} (resolver methods are not inherited from base classes)`,
|
|
657
|
+
],
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
if (fields.length === 0) reportStrayMethodDecorators(cls, method, ctx)
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** `@UseGuard` or parameter decorators on a method that is not a field. */
|
|
665
|
+
function reportStrayMethodDecorators(cls: ClassInfo, method: MethodInfo, ctx: Context): void {
|
|
666
|
+
const where = `${cls.displayName}.${method.name}`
|
|
667
|
+
for (const decorator of method.decorators) {
|
|
668
|
+
if (decorator.identity === GraphQLIds.UseGuard) {
|
|
669
|
+
error(
|
|
670
|
+
ctx,
|
|
671
|
+
Codes.InvalidDecorator,
|
|
672
|
+
decorator.location,
|
|
673
|
+
`@UseGuard has no effect: ${where} is not a GraphQL field`,
|
|
674
|
+
['Add @Query(), @Mutation(), @Subscription() or @ResolveField() to the method'],
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
for (const param of method.params) {
|
|
679
|
+
for (const decorator of parameterDecorators(param)) {
|
|
680
|
+
error(
|
|
681
|
+
ctx,
|
|
682
|
+
Codes.InvalidDecorator,
|
|
683
|
+
decorator.location,
|
|
684
|
+
`@${decorator.name} has no effect: ${where} is not a GraphQL field`,
|
|
685
|
+
['Add @Query(), @Mutation(), @Subscription() or @ResolveField() to the method'],
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ------------------------------------------------------------------------------------------ helpers
|
|
692
|
+
|
|
693
|
+
function error(
|
|
694
|
+
ctx: Context,
|
|
695
|
+
code: (typeof Codes)[keyof typeof Codes],
|
|
696
|
+
location: Location,
|
|
697
|
+
message: string,
|
|
698
|
+
hints: readonly string[] = [],
|
|
699
|
+
related: readonly { readonly message: string; readonly location: Location }[] = [],
|
|
700
|
+
): void {
|
|
701
|
+
ctx.report({
|
|
702
|
+
code,
|
|
703
|
+
severity: 'error',
|
|
704
|
+
message,
|
|
705
|
+
location,
|
|
706
|
+
...(hints.length === 0 ? {} : { hints }),
|
|
707
|
+
...(related.length === 0 ? {} : { related }),
|
|
708
|
+
})
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** An evaluated object literal (not an array, not a reference). */
|
|
712
|
+
function isRecord(value: StaticValue): value is Readonly<Record<string, StaticValue>> {
|
|
713
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
714
|
+
const prototype: unknown = Object.getPrototypeOf(value)
|
|
715
|
+
return prototype === Object.prototype || prototype === null
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** Short description of a value for messages. */
|
|
719
|
+
export function describe(value: StaticValue): string {
|
|
720
|
+
if (value === undefined) return 'undefined'
|
|
721
|
+
if (value === null) return 'null'
|
|
722
|
+
if (typeof value === 'string') return JSON.stringify(value)
|
|
723
|
+
if (typeof value === 'bigint') return `${value}n`
|
|
724
|
+
if (typeof value !== 'object') return String(value)
|
|
725
|
+
if (Array.isArray(value)) return 'an array'
|
|
726
|
+
return isRecord(value) ? 'an object' : 'a reference'
|
|
727
|
+
}
|