@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1346 -0
  3. package/dist/compiler.d.mts +114 -0
  4. package/dist/compiler.mjs +4599 -0
  5. package/dist/compiler.mjs.map +1 -0
  6. package/dist/index.d.mts +583 -0
  7. package/dist/index.mjs +134 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/names-BlzDM5cF.mjs +111 -0
  10. package/dist/names-BlzDM5cF.mjs.map +1 -0
  11. package/dist/plugin-CR_wVfRU.mjs +3445 -0
  12. package/dist/plugin-CR_wVfRU.mjs.map +1 -0
  13. package/dist/testing.d.mts +101 -0
  14. package/dist/testing.mjs +306 -0
  15. package/dist/testing.mjs.map +1 -0
  16. package/dist/types-DQ713UgI.d.mts +266 -0
  17. package/package.json +97 -1
  18. package/src/compiler/analyze.ts +727 -0
  19. package/src/compiler/builder.ts +1561 -0
  20. package/src/compiler/checks.ts +105 -0
  21. package/src/compiler/conformance.ts +1068 -0
  22. package/src/compiler/constants.ts +80 -0
  23. package/src/compiler/data.ts +83 -0
  24. package/src/compiler/defaults.ts +92 -0
  25. package/src/compiler/discriminators.ts +53 -0
  26. package/src/compiler/docs.ts +55 -0
  27. package/src/compiler/generate.ts +419 -0
  28. package/src/compiler/graphql-js.ts +42 -0
  29. package/src/compiler/ids.ts +128 -0
  30. package/src/compiler/index.ts +98 -0
  31. package/src/compiler/locate.ts +82 -0
  32. package/src/compiler/model.ts +137 -0
  33. package/src/compiler/options.ts +85 -0
  34. package/src/compiler/scalars.ts +138 -0
  35. package/src/compiler/schema-first.ts +595 -0
  36. package/src/compiler/sdl-source.ts +529 -0
  37. package/src/compiler/sdl-view.ts +61 -0
  38. package/src/compiler/suggest.ts +39 -0
  39. package/src/context.ts +181 -0
  40. package/src/decisions.ts +169 -0
  41. package/src/decorators.ts +202 -0
  42. package/src/errors.ts +89 -0
  43. package/src/execution.ts +1064 -0
  44. package/src/format.ts +285 -0
  45. package/src/index.ts +89 -0
  46. package/src/limits.ts +177 -0
  47. package/src/loaders.ts +149 -0
  48. package/src/memory-pubsub.ts +133 -0
  49. package/src/metadata.ts +222 -0
  50. package/src/names.ts +47 -0
  51. package/src/options.ts +331 -0
  52. package/src/pipeline.ts +338 -0
  53. package/src/plugin.ts +579 -0
  54. package/src/pubsub.ts +36 -0
  55. package/src/results.ts +206 -0
  56. package/src/scalars.ts +138 -0
  57. package/src/schema-hash.ts +18 -0
  58. package/src/schema.ts +635 -0
  59. package/src/sdl.ts +530 -0
  60. package/src/testing.ts +454 -0
  61. package/src/tokens.ts +22 -0
  62. package/src/transport/abort.ts +80 -0
  63. package/src/transport/accept.ts +155 -0
  64. package/src/transport/graphiql.ts +106 -0
  65. package/src/transport/http.ts +373 -0
  66. package/src/transport/sse.ts +45 -0
  67. package/src/type-ref.ts +89 -0
  68. package/src/types.ts +225 -0
  69. 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
+ }