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