@znight/graphql 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1063 -0
  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 +97 -1
  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,405 @@
1
+ /**
2
+ * `generate` of the graphql extension (RFC 0005 §7.1, §7.3): `metadata.extensions.graphql` from the
3
+ * model `validate` built, and the SDL artifact when the extension is configured with `{ sdl }`.
4
+ *
5
+ * Data, not code, except `invoke`: one arrow per resolver field mapping the graphql-js arguments to
6
+ * the method parameters. It is synchronous unless an argument is validated with a Standard Schema
7
+ * whose validation turns out asynchronous: like the `invoke` of `@znight/http` routes, a
8
+ * synchronous validation (Zod, Valibot, ArkType without async refinements) costs no promise.
9
+ *
10
+ * ```ts
11
+ * ['invoke']: (r: BookResolver, p: unknown, _a: GraphQLArgs, c: GraphQLContext) =>
12
+ * r.author(p as never, c.loader('src/authors-by-id.loader.ts#AuthorsById') as never),
13
+ * ```
14
+ */
15
+
16
+ import type { GenerateContext, GenInput, GenNode, GenParam } from '@znight/compiler'
17
+ import type { Token } from '@znight/core'
18
+ import type { MetaEntry } from '@znight/http/compiler'
19
+ import type { FieldData, GraphQLData, ParamData, ResolverData } from './data.ts'
20
+ import type { ArgModel, FieldModel, GraphQLModel, TypeModel } from './model.ts'
21
+ import type { ResolvedGraphQLCompilerOptions } from './options.ts'
22
+
23
+ import { GRAPHQL_METADATA_VERSION } from '../metadata.ts'
24
+ import { compareStrings } from './constants.ts'
25
+ import { RUNTIME_MODULE } from './ids.ts'
26
+
27
+ type Context = GenerateContext<GraphQLData>
28
+
29
+ /** Module of `isThenable` (a required peer of `@znight/graphql`). */
30
+ const HTTP_MODULE = '@znight/http'
31
+
32
+ /** Imports created on first use: an unused import would break `noUnusedLocals`. */
33
+ interface Imports {
34
+ argsType(): GenNode
35
+ contextType(): GenNode
36
+ infoType(): GenNode
37
+ validateArgs(): GenNode
38
+ isThenable(): GenNode
39
+ hostType(identity: string): GenNode
40
+ }
41
+
42
+ /** Builds the graphql section; `undefined` (no section) when the application has no resolver. */
43
+ export function generateGraphQL(
44
+ ctx: Context,
45
+ model: GraphQLModel | undefined,
46
+ options: ResolvedGraphQLCompilerOptions,
47
+ ): GenNode | undefined {
48
+ if (model === undefined) return undefined
49
+ if (options.sdl !== false) ctx.emitFile(options.sdl, `${model.sdl}\n`)
50
+ const imports = createImports(ctx)
51
+ const entries: [string, GenInput][] = [
52
+ ['version', ctx.lit(GRAPHQL_METADATA_VERSION)],
53
+ ['schemaHash', model.schemaHash],
54
+ ['roots', ctx.obj(Object.entries(model.roots))],
55
+ ['types', ctx.arr(model.types.map(type => typeNode(ctx, imports, type)))],
56
+ ]
57
+ const groups = groupsNode(ctx, model.meta)
58
+ if (groups !== undefined) entries.push(['groups', groups])
59
+ return ctx.obj(entries)
60
+ }
61
+
62
+ function createImports(ctx: Context): Imports {
63
+ let args: GenNode | undefined
64
+ let context: GenNode | undefined
65
+ let info: GenNode | undefined
66
+ let validateArgs: GenNode | undefined
67
+ let isThenable: GenNode | undefined
68
+ const hosts = new Map<string, GenNode>()
69
+ return {
70
+ argsType: () => {
71
+ args ??= ctx.importFrom(RUNTIME_MODULE, 'GraphQLArgs', { type: true })
72
+ return args
73
+ },
74
+ contextType: () => {
75
+ context ??= ctx.importFrom(RUNTIME_MODULE, 'GraphQLContext', { type: true })
76
+ return context
77
+ },
78
+ infoType: () => {
79
+ info ??= ctx.importFrom('graphql', 'GraphQLResolveInfo', { type: true })
80
+ return info
81
+ },
82
+ validateArgs: () => {
83
+ validateArgs ??= ctx.importFrom(RUNTIME_MODULE, 'validateArgs')
84
+ return validateArgs
85
+ },
86
+ isThenable: () => {
87
+ isThenable ??= ctx.importFrom(HTTP_MODULE, 'isThenable')
88
+ return isThenable
89
+ },
90
+ hostType: identity => {
91
+ let node = hosts.get(identity)
92
+ if (node === undefined) {
93
+ node = ctx.importType(identity)
94
+ hosts.set(identity, node)
95
+ }
96
+ return node
97
+ },
98
+ }
99
+ }
100
+
101
+ function typeNode(ctx: Context, imports: Imports, type: TypeModel): GenNode {
102
+ const head: [string, GenInput][] = [
103
+ ['kind', type.kind],
104
+ ['name', type.name],
105
+ ]
106
+ if (type.description !== undefined && type.kind !== 'scalar') head.push(['description', type.description])
107
+ switch (type.kind) {
108
+ case 'object':
109
+ return ctx.obj([...head, ['fields', ctx.arr(type.fields.map(field => fieldNode(ctx, imports, field)))]])
110
+ case 'input':
111
+ return ctx.obj([...head, ['fields', ctx.arr(type.fields.map(field => inputValueNode(ctx, field)))]])
112
+ case 'enum':
113
+ return ctx.obj([
114
+ ...head,
115
+ [
116
+ 'values',
117
+ ctx.arr(
118
+ type.values.map(value => {
119
+ const entries: [string, GenInput][] = [
120
+ ['name', value.name],
121
+ ['value', value.value],
122
+ ]
123
+ if (value.description !== undefined) entries.push(['description', value.description])
124
+ if (value.deprecationReason !== undefined) entries.push(['deprecationReason', value.deprecationReason])
125
+ return ctx.obj(entries)
126
+ }),
127
+ ),
128
+ ],
129
+ ])
130
+ case 'union': {
131
+ const discriminator = type.discriminator
132
+ let node: GenNode
133
+ if (discriminator === undefined || discriminator.by === 'typename') node = ctx.obj([['by', 'typename']])
134
+ else if (discriminator.by === 'property') {
135
+ node = ctx.obj([
136
+ ['by', 'property'],
137
+ ['property', discriminator.property],
138
+ ['values', ctx.obj(Object.entries(discriminator.values))],
139
+ ])
140
+ } else {
141
+ node = ctx.obj([
142
+ ['by', 'class'],
143
+ [
144
+ 'classes',
145
+ ctx.arr(discriminator.classes.map(entry => ctx.arr([ctx.importValue(entry.identity), entry.name]))),
146
+ ],
147
+ ])
148
+ }
149
+ return ctx.obj([...head, ['members', ctx.lit([...type.members])], ['discriminator', node]])
150
+ }
151
+ case 'scalar':
152
+ // built-in scalars carry no description (the runtime builds them, sdl.ts prints them alike);
153
+ // user scalars bring theirs with the imported definition
154
+ return type.definition === undefined
155
+ ? ctx.obj([...head, ['builtin', ctx.lit(true)]])
156
+ : ctx.obj([...head, ['scalar', ctx.importValue(type.definition)]])
157
+ }
158
+ }
159
+
160
+ function fieldNode(ctx: Context, imports: Imports, field: FieldModel): GenNode {
161
+ const entries: [string, GenInput][] = [
162
+ ['name', field.name],
163
+ ['type', field.type],
164
+ ]
165
+ if (field.description !== undefined) entries.push(['description', field.description])
166
+ if (field.deprecationReason !== undefined) entries.push(['deprecationReason', field.deprecationReason])
167
+ if (field.args.length > 0) entries.push(['args', ctx.arr(field.args.map(arg => inputValueNode(ctx, arg)))])
168
+ if (field.binding !== undefined) {
169
+ entries.push(['resolver', resolverNode(ctx, imports, field.binding.resolver, field.binding.field)])
170
+ }
171
+ return ctx.obj(entries)
172
+ }
173
+
174
+ function inputValueNode(ctx: Context, arg: ArgModel): GenNode {
175
+ const entries: [string, GenInput][] = [
176
+ ['name', arg.name],
177
+ ['type', arg.type],
178
+ ]
179
+ if (arg.description !== undefined) entries.push(['description', arg.description])
180
+ if (arg.deprecationReason !== undefined) entries.push(['deprecationReason', arg.deprecationReason])
181
+ if (arg.defaultValue !== undefined) entries.push(['defaultValue', ctx.lit(arg.defaultValue as never)])
182
+ if (arg.nullAsUndefined) entries.push(['nullAsUndefined', ctx.lit(true)])
183
+ return ctx.obj(entries)
184
+ }
185
+
186
+ function resolverNode(ctx: Context, imports: Imports, resolver: ResolverData, field: FieldData): GenNode {
187
+ const guards = [...resolver.guards, ...field.guards].map(ref => ctx.tokenOf(ref) ?? ref.token)
188
+ const entries: [string, GenInput][] = [
189
+ ['token', ctx.tokenOf(resolver.host) ?? resolver.host.identity],
190
+ ['method', field.method],
191
+ ['operation', field.operation],
192
+ ['guards', ctx.lit(guards)],
193
+ ['meta', ctx.obj(field.meta.map(entry => [entry.key, ctx.lit(entry.value as never)] as const))],
194
+ ]
195
+ const loaders = field.params.flatMap(param => (param?.kind === 'loader' ? [loaderToken(ctx, param)] : []))
196
+ if (loaders.length > 0) entries.push(['loaders', ctx.lit(loaders)])
197
+ entries.push(['invoke', invokeNode(ctx, imports, resolver, field)])
198
+ return ctx.obj(entries)
199
+ }
200
+
201
+ /** The provider token a loader parameter is bound to. */
202
+ function loaderToken(ctx: Context, param: Extract<ParamData, { kind: 'loader' }>): Token {
203
+ const ref = ctx.binding(param.binding)
204
+ if (typeof ref === 'string') return ref
205
+ if (ref !== undefined && 'token' in ref) return ref.token
206
+ return param.identity
207
+ }
208
+
209
+ // ------------------------------------------------------------------------------------------- invoke
210
+
211
+ /** Which `invoke` parameters a field uses: `(r, p, a, c, i)`. */
212
+ function usedParameters(field: FieldData): readonly [p: boolean, a: boolean, c: boolean, i: boolean] {
213
+ let p = false
214
+ let a = false
215
+ let c = false
216
+ let i = false
217
+ for (const param of field.params) {
218
+ switch (param?.kind) {
219
+ case 'parent':
220
+ p = true
221
+ break
222
+ case 'args':
223
+ a = true
224
+ break
225
+ case 'ctx':
226
+ case 'loader':
227
+ c = true
228
+ break
229
+ case 'info':
230
+ i = true
231
+ break
232
+ }
233
+ }
234
+ return [p, a, c, i]
235
+ }
236
+
237
+ /**
238
+ * `(r: Resolver, p: unknown, a: GraphQLArgs, c: GraphQLContext, i: GraphQLResolveInfo) => r.m(...)`:
239
+ * unused middle parameters are `_`-prefixed, trailing ones omitted (`noUnusedParameters`).
240
+ */
241
+ function invokeNode(ctx: Context, imports: Imports, resolver: ResolverData, field: FieldData): GenNode {
242
+ const used = usedParameters(field)
243
+ const last = used.lastIndexOf(true)
244
+ const types = [() => ctx.code`unknown`, imports.argsType, imports.contextType, imports.infoType]
245
+ const names = ['p', 'a', 'c', 'i']
246
+ const params: [string, GenNode][] = [['r', imports.hostType(resolver.host.identity)]]
247
+ for (let index = 0; index <= last; index++) {
248
+ params.push([`${used[index] ? '' : '_'}${names[index] as string}`, (types[index] as () => GenNode)()])
249
+ }
250
+ const validated = field.params.flatMap((param, index) =>
251
+ param?.kind === 'args' && param.schema !== undefined ? [index] : [],
252
+ )
253
+ return ctx.arrow(params, (r, ...rest) => {
254
+ const [p, a, c, i] = rest as (GenParam | undefined)[]
255
+ const scope: Scope = { p, a, c, i }
256
+ if (validated.length === 0) return call(ctx, r, field, index => argument(ctx, scope, field, index))
257
+ return validatingBlock(ctx, imports, r, scope, field, validated)
258
+ })
259
+ }
260
+
261
+ /** The parameters of the `invoke` arrow (absent when not declared). */
262
+ interface Scope {
263
+ readonly p: GenNode | undefined
264
+ readonly a: GenNode | undefined
265
+ readonly c: GenNode | undefined
266
+ readonly i: GenNode | undefined
267
+ }
268
+
269
+ /** `r.method(args)`. */
270
+ function call(ctx: Context, r: GenNode, field: FieldData, argumentOf: (index: number) => GenNode): GenNode {
271
+ return ctx.call(
272
+ ctx.member(r, field.method),
273
+ field.params.map((_, index) => argumentOf(index)),
274
+ )
275
+ }
276
+
277
+ /** The expression passed for the parameter `index` (validation not applied). */
278
+ function argument(ctx: Context, scope: Scope, field: FieldData, index: number): GenNode {
279
+ const param = field.params[index]
280
+ switch (param?.kind) {
281
+ case 'parent':
282
+ return ctx.code`${scope.p as GenNode} as never`
283
+ case 'ctx':
284
+ return scope.c as GenNode
285
+ case 'info':
286
+ return scope.i as GenNode
287
+ case 'loader':
288
+ return ctx.code`${ctx.call(ctx.member(scope.c as GenNode, 'loader'), [loaderToken(ctx, param)])} as never`
289
+ case 'args':
290
+ return ctx.code`${argsValue(ctx, scope, param.name)} as never`
291
+ default:
292
+ return ctx.lit(undefined)
293
+ }
294
+ }
295
+
296
+ /** `a['name']`, or `a` for `@Args()`. */
297
+ function argsValue(ctx: Context, scope: Scope, name: string | undefined): GenNode {
298
+ const a = scope.a as GenNode
299
+ return name === undefined ? a : ctx.code`${a}[${name}]`
300
+ }
301
+
302
+ /** `validateArgs(Schema, a['input'], 'input')`. */
303
+ function validation(
304
+ ctx: Context,
305
+ imports: Imports,
306
+ scope: Scope,
307
+ param: Extract<ParamData, { kind: 'args' }>,
308
+ ): GenNode {
309
+ return ctx.call(imports.validateArgs(), [
310
+ ctx.ref(param.schema as NonNullable<typeof param.schema>),
311
+ argsValue(ctx, scope, param.name),
312
+ param.name ?? 'args',
313
+ ])
314
+ }
315
+
316
+ /**
317
+ * The body of the `invoke` of a field validating arguments: each validation is evaluated in order
318
+ * into a `const`; while they return values the method is called synchronously, the first thenable
319
+ * resumes in an `async` arrow that awaits it and the later validations (the order of evaluation and
320
+ * the error reported first are those of an `async` invoke awaiting every validation).
321
+ *
322
+ * ```ts
323
+ * (r: BookResolver, _p: unknown, a: GraphQLArgs) => {
324
+ * const v0 = validateArgs(NewBookInput, a['input'], 'input')
325
+ * if (isThenable(v0)) return (async () => r.addBook(await v0 as never))()
326
+ * return r.addBook(v0 as never)
327
+ * }
328
+ * ```
329
+ */
330
+ function validatingBlock(
331
+ ctx: Context,
332
+ imports: Imports,
333
+ r: GenNode,
334
+ scope: Scope,
335
+ field: FieldData,
336
+ validated: readonly number[],
337
+ ): GenNode {
338
+ return ctx.block(
339
+ validated.map(index => `v${index}`),
340
+ (...locals) => {
341
+ const local = new Map(validated.map((index, position) => [index, locals[position] as GenNode]))
342
+ const statements: GenNode[] = []
343
+ for (const index of validated) {
344
+ const name = local.get(index) as GenNode
345
+ const param = field.params[index] as Extract<ParamData, { kind: 'args' }>
346
+ statements.push(ctx.code`const ${name} = ${validation(ctx, imports, scope, param)}`)
347
+ const resume = ctx.arrow(
348
+ [],
349
+ () =>
350
+ call(ctx, r, field, position => {
351
+ const value = local.get(position)
352
+ if (value === undefined) return argument(ctx, scope, field, position)
353
+ if (position < index) return ctx.code`${value} as never`
354
+ if (position === index) return ctx.code`await ${value} as never`
355
+ const later = field.params[position] as Extract<ParamData, { kind: 'args' }>
356
+ return ctx.code`await ${validation(ctx, imports, scope, later)} as never`
357
+ }),
358
+ { async: true },
359
+ )
360
+ statements.push(ctx.code`if (${ctx.call(imports.isThenable(), [name])}) return (${resume})()`)
361
+ }
362
+ statements.push(
363
+ ctx.code`return ${call(ctx, r, field, position => {
364
+ const value = local.get(position)
365
+ return value === undefined ? argument(ctx, scope, field, position) : ctx.code`${value} as never`
366
+ })}`,
367
+ )
368
+ return statements
369
+ },
370
+ )
371
+ }
372
+
373
+ // ------------------------------------------------------------------------------------------- groups
374
+
375
+ /**
376
+ * `groups`: the route decorator groups the runtime needs (required, holding several keys, or named
377
+ * differently from their key), sorted; `undefined` when there is none (RFC 0001, as `extensions.http`).
378
+ */
379
+ function groupsNode(ctx: Context, meta: readonly MetaEntry[]): GenNode | undefined {
380
+ const groups = new Map<string, { readonly keys: Set<string>; required: boolean }>()
381
+ for (const entry of meta) {
382
+ const group = groups.get(entry.group)
383
+ if (group === undefined) groups.set(entry.group, { keys: new Set([entry.key]), required: entry.required })
384
+ else {
385
+ group.keys.add(entry.key)
386
+ group.required ||= entry.required
387
+ }
388
+ }
389
+ const needed = [...groups]
390
+ .filter(([name, group]) => group.required || group.keys.size > 1 || !group.keys.has(name))
391
+ .sort(([a], [b]) => compareStrings(a, b))
392
+ if (needed.length === 0) return undefined
393
+ return ctx.obj(
394
+ needed.map(
395
+ ([name, group]) =>
396
+ [
397
+ name,
398
+ ctx.obj([
399
+ ['keys', ctx.lit([...group.keys].sort(compareStrings))],
400
+ ['required', group.required],
401
+ ]),
402
+ ] as const,
403
+ ),
404
+ )
405
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Canonical identities the `@znight/graphql` compiler extension recognises (RFC 0005 §5.14,
3
+ * §6.2, §6.5): its decorators, `scalar<T>()`, the scalar aliases, the loader and context types, and
4
+ * the look-alike decorators of `@znight/http` it reports (ZNIGHT005).
5
+ *
6
+ * Constants only: this module must stay free of runtime imports (the compiler loads the extension
7
+ * entry, which must never load the GraphQL runtime, graphql-js or dataloader).
8
+ */
9
+
10
+ /** Package name: the prefix of every identity below. */
11
+ export const GRAPHQL_PACKAGE = '@znight/graphql'
12
+
13
+ /** Name of the extension, also the key of `metadata.extensions.graphql`. */
14
+ export const EXTENSION_NAME = 'graphql'
15
+
16
+ /** Module the generated code imports its runtime helpers and types from. */
17
+ export const RUNTIME_MODULE = '@znight/graphql'
18
+
19
+ /** Canonical identities of the exports of `@znight/graphql` the compiler reads. */
20
+ export const GraphQLIds: {
21
+ /** `@Resolver(options?)`, class decorator. */
22
+ readonly Resolver: string
23
+ /** `@Query(name?)`, method decorator. */
24
+ readonly Query: string
25
+ /** `@Mutation(name?)`, method decorator. */
26
+ readonly Mutation: string
27
+ /** `@Subscription(name?)`, method decorator. */
28
+ readonly Subscription: string
29
+ /** `@ResolveField(name?)`, method decorator. */
30
+ readonly ResolveField: string
31
+ /** `@Args(nameOrSchema?, schema?)`, parameter decorator. */
32
+ readonly Args: string
33
+ /** `@Parent()`, parameter decorator. */
34
+ readonly Parent: string
35
+ /** `@Ctx()`, parameter decorator (`GraphQLContext`). */
36
+ readonly Ctx: string
37
+ /** `@Info()`, parameter decorator (graphql-js `GraphQLResolveInfo`). */
38
+ readonly Info: string
39
+ /** `@Loader()` parameter decorator **and** the `Loader<L>` parameter type (same export name). */
40
+ readonly Loader: string
41
+ /** `@UseGuard(...guards)`, class or method decorator. */
42
+ readonly UseGuard: string
43
+ /** `scalar<T>(name, definition)`. */
44
+ readonly scalar: string
45
+ /** `type ID = string` → `ID`. */
46
+ readonly ID: string
47
+ /** `type Int = number` → `Int`. */
48
+ readonly Int: string
49
+ /** `type Float = number` → `Float`. */
50
+ readonly Float: string
51
+ /** `type JSONValue` → `JSON`. */
52
+ readonly JSONValue: string
53
+ /** `interface BatchLoad<K, V>`: what loader providers implement (ZNIGHT065). */
54
+ readonly BatchLoad: string
55
+ /** `interface GraphQLContext`: the request-scoped external (manifest `provides`). */
56
+ readonly GraphQLContext: string
57
+ /** `interface ResolverContext`: what guards receive. */
58
+ readonly ResolverContext: string
59
+ /** `abstract class PubSub`: the singleton external (manifest `provides`). */
60
+ readonly PubSub: string
61
+ /** `GraphQLPlugin`: the singleton external (manifest `provides`). */
62
+ readonly GraphQLPlugin: string
63
+ } = Object.freeze({
64
+ Resolver: '@znight/graphql#Resolver',
65
+ Query: '@znight/graphql#Query',
66
+ Mutation: '@znight/graphql#Mutation',
67
+ Subscription: '@znight/graphql#Subscription',
68
+ ResolveField: '@znight/graphql#ResolveField',
69
+ Args: '@znight/graphql#Args',
70
+ Parent: '@znight/graphql#Parent',
71
+ Ctx: '@znight/graphql#Ctx',
72
+ Info: '@znight/graphql#Info',
73
+ Loader: '@znight/graphql#Loader',
74
+ UseGuard: '@znight/graphql#UseGuard',
75
+ scalar: '@znight/graphql#scalar',
76
+ ID: '@znight/graphql#ID',
77
+ Int: '@znight/graphql#Int',
78
+ Float: '@znight/graphql#Float',
79
+ JSONValue: '@znight/graphql#JSONValue',
80
+ BatchLoad: '@znight/graphql#BatchLoad',
81
+ GraphQLContext: '@znight/graphql#GraphQLContext',
82
+ ResolverContext: '@znight/graphql#ResolverContext',
83
+ PubSub: '@znight/graphql#PubSub',
84
+ GraphQLPlugin: '@znight/graphql#GraphQLPlugin',
85
+ })
86
+
87
+ /** Field decorators → the kind of field they declare (`GraphQLResolverBinding.operation`). */
88
+ export const FIELD_DECORATORS: ReadonlyMap<string, 'query' | 'mutation' | 'subscription' | 'field'> = new Map([
89
+ [GraphQLIds.Query, 'query'],
90
+ [GraphQLIds.Mutation, 'mutation'],
91
+ [GraphQLIds.Subscription, 'subscription'],
92
+ [GraphQLIds.ResolveField, 'field'],
93
+ ])
94
+
95
+ /** Parameter decorators → what the generated `invoke` passes (§7.3). */
96
+ export const PARAMETER_DECORATORS: ReadonlyMap<string, 'args' | 'parent' | 'ctx' | 'info' | 'loader'> = new Map([
97
+ [GraphQLIds.Args, 'args'],
98
+ [GraphQLIds.Parent, 'parent'],
99
+ [GraphQLIds.Ctx, 'ctx'],
100
+ [GraphQLIds.Info, 'info'],
101
+ [GraphQLIds.Loader, 'loader'],
102
+ ])
103
+
104
+ /** Scalar aliases of `@znight/graphql` → their GraphQL scalar (§6.2). */
105
+ export const SCALAR_ALIASES: ReadonlyMap<string, 'ID' | 'Int' | 'Float' | 'JSON'> = new Map([
106
+ [GraphQLIds.ID, 'ID'],
107
+ [GraphQLIds.Int, 'Int'],
108
+ [GraphQLIds.Float, 'Float'],
109
+ [GraphQLIds.JSONValue, 'JSON'],
110
+ ])
111
+
112
+ /**
113
+ * Decorators of `@znight/http` that look like GraphQL ones, on a resolver: ZNIGHT005 with the
114
+ * `@znight/graphql` decorator to import instead (§6.5).
115
+ */
116
+ export const HTTP_LOOK_ALIKES: ReadonlyMap<string, string> = new Map([
117
+ ['@znight/http#Ctx', GraphQLIds.Ctx],
118
+ ['@znight/http#Query', GraphQLIds.Args],
119
+ ['@znight/http#Param', GraphQLIds.Args],
120
+ ['@znight/http#Body', GraphQLIds.Args],
121
+ ['@znight/http#UseGuard', GraphQLIds.UseGuard],
122
+ ])
123
+
124
+ /** `@Controller` of `@znight/http`: a class cannot be both a controller and a resolver (ZNIGHT005). */
125
+ export const HTTP_CONTROLLER = '@znight/http#Controller'
126
+
127
+ /** graphql-js `GraphQLResolveInfo`, the type of `@Info()` parameters. */
128
+ export const GRAPHQL_RESOLVE_INFO = 'graphql#GraphQLResolveInfo'
@@ -0,0 +1,93 @@
1
+ /**
2
+ * `@znight/graphql/compiler`: the compiler extension of `@znight/graphql` (RFC 0005 §6).
3
+ *
4
+ * Loaded by `@znight/compiler` through the package manifest (`package.json#znight.compiler`):
5
+ *
6
+ * - `analyzeClass`: `@Resolver` classes become providers (and roots), their field methods and
7
+ * parameters are checked, guards and loaders bound with lazy edges, route decorators read;
8
+ * - `validate`: the schema model, derived from the TypeScript types reachable from the root fields
9
+ * (§6.2–§6.6), with every ZNIGHT060–065 diagnostic and the access decisions (ZNIGHT023);
10
+ * - `generate`: `metadata.extensions.graphql` (§7), and the SDL artifact when configured.
11
+ *
12
+ * The default export is the extension with its default options; `configure(options)` returns a
13
+ * configured copy, declared in `znight.config.ts` (it replaces the discovered extension):
14
+ *
15
+ * ```ts
16
+ * import graphql from '@znight/graphql/compiler'
17
+ * export default { extensions: [graphql.configure({ sdl: 'schema.graphql' })] } satisfies ZnightOptions
18
+ * ```
19
+ *
20
+ * This entry only `import type`s from `@znight/compiler`, imports the runtime-free
21
+ * `@znight/http/compiler`, and never loads the GraphQL runtime, graphql-js nor dataloader: it
22
+ * reaches the import-free modules `src/type-ref.ts`, `src/sdl.ts`, `src/names.ts`,
23
+ * `src/metadata.ts` and `src/schema-hash.ts` (`node:crypto`) only (tests/boundaries.test.ts).
24
+ */
25
+ import type { ApplicationGraph, CompilerExtension, DecoratorSpec } from '@znight/compiler'
26
+ import type { GraphQLData } from './data.ts'
27
+ import type { GraphQLModel } from './model.ts'
28
+ import type { GraphQLCompilerOptions, ResolvedGraphQLCompilerOptions } from './options.ts'
29
+
30
+ import { analyzeGraphQLClass } from './analyze.ts'
31
+ import { buildModel } from './builder.ts'
32
+ import { generateGraphQL } from './generate.ts'
33
+ import { EXTENSION_NAME, GraphQLIds } from './ids.ts'
34
+ import { DEFAULT_OPTIONS, resolveOptions } from './options.ts'
35
+
36
+ export type { FieldData, FieldOperation, GraphQLData, ParamData, ResolverData } from './data.ts'
37
+ export type { GraphQLCompilerOptions, ResolvedGraphQLCompilerOptions } from './options.ts'
38
+
39
+ const FIELD: DecoratorSpec = { targets: ['method'], params: ['static'] }
40
+ const NO_ARGUMENT: DecoratorSpec = { targets: ['parameter'], params: [] }
41
+
42
+ /** Decorators owned by the extension, by canonical identity (static / value arguments, §4.6). */
43
+ const decorators: Readonly<Record<string, DecoratorSpec>> = Object.freeze({
44
+ [GraphQLIds.Resolver]: { targets: ['class'], params: ['static'] },
45
+ [GraphQLIds.Query]: FIELD,
46
+ [GraphQLIds.Mutation]: FIELD,
47
+ [GraphQLIds.Subscription]: FIELD,
48
+ [GraphQLIds.ResolveField]: FIELD,
49
+ [GraphQLIds.Args]: { targets: ['parameter'], params: ['value', 'value'] },
50
+ [GraphQLIds.Parent]: NO_ARGUMENT,
51
+ [GraphQLIds.Ctx]: NO_ARGUMENT,
52
+ [GraphQLIds.Info]: NO_ARGUMENT,
53
+ [GraphQLIds.Loader]: NO_ARGUMENT,
54
+ [GraphQLIds.UseGuard]: { targets: ['class', 'method'], params: [], rest: 'value' },
55
+ } satisfies Record<string, DecoratorSpec>)
56
+
57
+ /** The `@znight/graphql` compiler extension, with its options and `configure()`. */
58
+ export interface GraphQLCompilerExtension extends CompilerExtension<GraphQLData> {
59
+ /** The options of this copy (defaults filled in). */
60
+ readonly options: ResolvedGraphQLCompilerOptions
61
+ /** A copy of the extension with `options` merged onto the options of this one. */
62
+ configure(options: GraphQLCompilerOptions): GraphQLCompilerExtension
63
+ }
64
+
65
+ function createExtension(options: ResolvedGraphQLCompilerOptions): GraphQLCompilerExtension {
66
+ /**
67
+ * Models by compile, built by `validate`, read by `generate`. The key is `graph.extensions`: the
68
+ * validate and generate contexts of one compile get different graph objects sharing this array.
69
+ */
70
+ const models = new WeakMap<object, GraphQLModel | undefined>()
71
+ const modelOf = (graph: ApplicationGraph): GraphQLModel | undefined => models.get(graph.extensions)
72
+ return Object.freeze({
73
+ apiVersion: 1,
74
+ name: EXTENSION_NAME,
75
+ decorators,
76
+ options,
77
+ analyzeClass: analyzeGraphQLClass,
78
+ validate(ctx) {
79
+ models.set(ctx.graph.extensions, buildModel(ctx, options))
80
+ },
81
+ generate(ctx) {
82
+ return generateGraphQL(ctx, modelOf(ctx.graph), options)
83
+ },
84
+ configure(next: GraphQLCompilerOptions): GraphQLCompilerExtension {
85
+ return createExtension(resolveOptions(options, next))
86
+ },
87
+ } satisfies GraphQLCompilerExtension)
88
+ }
89
+
90
+ /** The `@znight/graphql` compiler extension with its default options. */
91
+ export const graphqlCompilerExtension: GraphQLCompilerExtension = createExtension(DEFAULT_OPTIONS)
92
+
93
+ export default graphqlCompilerExtension
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Source locations of diagnostics. Computed only when a diagnostic is reported: the happy path
3
+ * never reads source text.
4
+ */
5
+ import type { Location, Project } from '@znight/compiler'
6
+
7
+ /** From the member name: `name?: ` or `name(): ` (a getter), up to the annotation. */
8
+ const MEMBER_HEAD = /^(?:[\w$]+|'[^']*'|"[^"]*")\s*\??\s*(?:\(\s*\))?\s*:\s*/
9
+
10
+ const OPENING = new Set(['(', '[', '{', '<'])
11
+ const CLOSING = new Set([')', ']', '}', '>'])
12
+
13
+ /**
14
+ * The type annotation of a property, a property signature or a getter (`get x(): T`), from the
15
+ * location of the member (its name); the member itself when the annotation cannot be found.
16
+ */
17
+ export function annotationLocation(project: Project, member: Location | undefined): Location | undefined {
18
+ if (member === undefined) return undefined
19
+ const file = project.getFile(member.file)
20
+ if (file === undefined) return member
21
+ const text = file.text
22
+ const head = MEMBER_HEAD.exec(text.slice(member.start, member.start + 256))
23
+ if (head === null) return member
24
+ const start = member.start + head[0].length
25
+ let depth = 0
26
+ let end = start
27
+ for (let index = start; index < text.length; index++) {
28
+ const char = text[index] as string
29
+ if (char === '=' && text[index + 1] === '>') {
30
+ index++
31
+ continue
32
+ }
33
+ // a getter body or an initializer after the annotation
34
+ if (depth === 0 && end > start && (char === '{' || char === '=')) break
35
+ if (OPENING.has(char)) depth++
36
+ else if (CLOSING.has(char)) {
37
+ if (depth === 0) break
38
+ depth--
39
+ } else if (depth === 0 && (char === ';' || char === ',')) break
40
+ else if (depth === 0 && char === '\n' && !/^\s*[|&]/.test(text.slice(index + 1, index + 64))) break
41
+ if (!/\s/.test(char)) end = index + 1
42
+ }
43
+ return end > start ? file.location({ start, end }) : member
44
+ }