@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,419 @@
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, DiscriminatorModel, 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
+ case 'interface': {
110
+ const entries: [string, GenInput][] = [...head]
111
+ if (type.interfaces.length > 0) entries.push(['interfaces', ctx.lit([...type.interfaces])])
112
+ entries.push(['fields', ctx.arr(type.fields.map(field => fieldNode(ctx, imports, field)))])
113
+ if (type.kind === 'interface') entries.push(['discriminator', discriminatorNode(ctx, type.discriminator)])
114
+ return ctx.obj(entries)
115
+ }
116
+ case 'input':
117
+ return ctx.obj([...head, ['fields', ctx.arr(type.fields.map(field => inputValueNode(ctx, field)))]])
118
+ case 'enum':
119
+ return ctx.obj([
120
+ ...head,
121
+ [
122
+ 'values',
123
+ ctx.arr(
124
+ type.values.map(value => {
125
+ const entries: [string, GenInput][] = [
126
+ ['name', value.name],
127
+ ['value', value.value],
128
+ ]
129
+ if (value.description !== undefined) entries.push(['description', value.description])
130
+ if (value.deprecationReason !== undefined) entries.push(['deprecationReason', value.deprecationReason])
131
+ return ctx.obj(entries)
132
+ }),
133
+ ),
134
+ ],
135
+ ])
136
+ case 'union':
137
+ return ctx.obj([
138
+ ...head,
139
+ ['members', ctx.lit([...type.members])],
140
+ ['discriminator', discriminatorNode(ctx, type.discriminator)],
141
+ ])
142
+ case 'scalar': {
143
+ // built-in scalars carry no description (the runtime builds them, sdl.ts prints them alike);
144
+ // code-first user scalars bring theirs with the imported definition, schema-first ones take
145
+ // the SDL's (they win over the definition's)
146
+ if (type.definition === undefined) return ctx.obj([...head, ['builtin', ctx.lit(true)]])
147
+ const entries: [string, GenInput][] = [...head, ['scalar', ctx.importValue(type.definition)]]
148
+ if (type.fromSdl === true) {
149
+ if (type.description !== undefined) entries.push(['description', type.description])
150
+ if (type.specifiedByURL !== undefined) entries.push(['specifiedByURL', type.specifiedByURL])
151
+ entries.push(['sdl', ctx.lit(true)])
152
+ }
153
+ return ctx.obj(entries)
154
+ }
155
+ }
156
+ }
157
+
158
+ /** How the runtime tells the members of a union or the implementations of an interface apart. */
159
+ function discriminatorNode(ctx: Context, discriminator: DiscriminatorModel | undefined): GenNode {
160
+ if (discriminator === undefined || discriminator.by === 'typename') return ctx.obj([['by', 'typename']])
161
+ if (discriminator.by === 'property') {
162
+ return ctx.obj([
163
+ ['by', 'property'],
164
+ ['property', discriminator.property],
165
+ ['values', ctx.obj(Object.entries(discriminator.values))],
166
+ ])
167
+ }
168
+ return ctx.obj([
169
+ ['by', 'class'],
170
+ ['classes', ctx.arr(discriminator.classes.map(entry => ctx.arr([ctx.importValue(entry.identity), entry.name])))],
171
+ ])
172
+ }
173
+
174
+ function fieldNode(ctx: Context, imports: Imports, field: FieldModel): GenNode {
175
+ const entries: [string, GenInput][] = [
176
+ ['name', field.name],
177
+ ['type', field.type],
178
+ ]
179
+ if (field.description !== undefined) entries.push(['description', field.description])
180
+ if (field.deprecationReason !== undefined) entries.push(['deprecationReason', field.deprecationReason])
181
+ if (field.args.length > 0) entries.push(['args', ctx.arr(field.args.map(arg => inputValueNode(ctx, arg)))])
182
+ if (field.binding !== undefined) {
183
+ entries.push(['resolver', resolverNode(ctx, imports, field.binding.resolver, field.binding.field)])
184
+ }
185
+ return ctx.obj(entries)
186
+ }
187
+
188
+ function inputValueNode(ctx: Context, arg: ArgModel): GenNode {
189
+ const entries: [string, GenInput][] = [
190
+ ['name', arg.name],
191
+ ['type', arg.type],
192
+ ]
193
+ if (arg.description !== undefined) entries.push(['description', arg.description])
194
+ if (arg.deprecationReason !== undefined) entries.push(['deprecationReason', arg.deprecationReason])
195
+ if (arg.defaultValue !== undefined) entries.push(['defaultValue', ctx.lit(arg.defaultValue as never)])
196
+ if (arg.nullAsUndefined) entries.push(['nullAsUndefined', ctx.lit(true)])
197
+ return ctx.obj(entries)
198
+ }
199
+
200
+ function resolverNode(ctx: Context, imports: Imports, resolver: ResolverData, field: FieldData): GenNode {
201
+ const guards = [...resolver.guards, ...field.guards].map(ref => ctx.tokenOf(ref) ?? ref.token)
202
+ const entries: [string, GenInput][] = [
203
+ ['token', ctx.tokenOf(resolver.host) ?? resolver.host.identity],
204
+ ['method', field.method],
205
+ ['operation', field.operation],
206
+ ['guards', ctx.lit(guards)],
207
+ ['meta', ctx.obj(field.meta.map(entry => [entry.key, ctx.lit(entry.value as never)] as const))],
208
+ ]
209
+ const loaders = field.params.flatMap(param => (param?.kind === 'loader' ? [loaderToken(ctx, param)] : []))
210
+ if (loaders.length > 0) entries.push(['loaders', ctx.lit(loaders)])
211
+ entries.push(['invoke', invokeNode(ctx, imports, resolver, field)])
212
+ return ctx.obj(entries)
213
+ }
214
+
215
+ /** The provider token a loader parameter is bound to. */
216
+ function loaderToken(ctx: Context, param: Extract<ParamData, { kind: 'loader' }>): Token {
217
+ const ref = ctx.binding(param.binding)
218
+ if (typeof ref === 'string') return ref
219
+ if (ref !== undefined && 'token' in ref) return ref.token
220
+ return param.identity
221
+ }
222
+
223
+ // ------------------------------------------------------------------------------------------- invoke
224
+
225
+ /** Which `invoke` parameters a field uses: `(r, p, a, c, i)`. */
226
+ function usedParameters(field: FieldData): readonly [p: boolean, a: boolean, c: boolean, i: boolean] {
227
+ let p = false
228
+ let a = false
229
+ let c = false
230
+ let i = false
231
+ for (const param of field.params) {
232
+ switch (param?.kind) {
233
+ case 'parent':
234
+ p = true
235
+ break
236
+ case 'args':
237
+ a = true
238
+ break
239
+ case 'ctx':
240
+ case 'loader':
241
+ c = true
242
+ break
243
+ case 'info':
244
+ i = true
245
+ break
246
+ }
247
+ }
248
+ return [p, a, c, i]
249
+ }
250
+
251
+ /**
252
+ * `(r: Resolver, p: unknown, a: GraphQLArgs, c: GraphQLContext, i: GraphQLResolveInfo) => r.m(...)`:
253
+ * unused middle parameters are `_`-prefixed, trailing ones omitted (`noUnusedParameters`).
254
+ */
255
+ function invokeNode(ctx: Context, imports: Imports, resolver: ResolverData, field: FieldData): GenNode {
256
+ const used = usedParameters(field)
257
+ const last = used.lastIndexOf(true)
258
+ const types = [() => ctx.code`unknown`, imports.argsType, imports.contextType, imports.infoType]
259
+ const names = ['p', 'a', 'c', 'i']
260
+ const params: [string, GenNode][] = [['r', imports.hostType(resolver.host.identity)]]
261
+ for (let index = 0; index <= last; index++) {
262
+ params.push([`${used[index] ? '' : '_'}${names[index] as string}`, (types[index] as () => GenNode)()])
263
+ }
264
+ const validated = field.params.flatMap((param, index) =>
265
+ param?.kind === 'args' && param.schema !== undefined ? [index] : [],
266
+ )
267
+ return ctx.arrow(params, (r, ...rest) => {
268
+ const [p, a, c, i] = rest as (GenParam | undefined)[]
269
+ const scope: Scope = { p, a, c, i }
270
+ if (validated.length === 0) return call(ctx, r, field, index => argument(ctx, scope, field, index))
271
+ return validatingBlock(ctx, imports, r, scope, field, validated)
272
+ })
273
+ }
274
+
275
+ /** The parameters of the `invoke` arrow (absent when not declared). */
276
+ interface Scope {
277
+ readonly p: GenNode | undefined
278
+ readonly a: GenNode | undefined
279
+ readonly c: GenNode | undefined
280
+ readonly i: GenNode | undefined
281
+ }
282
+
283
+ /** `r.method(args)`. */
284
+ function call(ctx: Context, r: GenNode, field: FieldData, argumentOf: (index: number) => GenNode): GenNode {
285
+ return ctx.call(
286
+ ctx.member(r, field.method),
287
+ field.params.map((_, index) => argumentOf(index)),
288
+ )
289
+ }
290
+
291
+ /** The expression passed for the parameter `index` (validation not applied). */
292
+ function argument(ctx: Context, scope: Scope, field: FieldData, index: number): GenNode {
293
+ const param = field.params[index]
294
+ switch (param?.kind) {
295
+ case 'parent':
296
+ return ctx.code`${scope.p as GenNode} as never`
297
+ case 'ctx':
298
+ return scope.c as GenNode
299
+ case 'info':
300
+ return scope.i as GenNode
301
+ case 'loader':
302
+ return ctx.code`${ctx.call(ctx.member(scope.c as GenNode, 'loader'), [loaderToken(ctx, param)])} as never`
303
+ case 'args':
304
+ return ctx.code`${argsValue(ctx, scope, param.name)} as never`
305
+ default:
306
+ return ctx.lit(undefined)
307
+ }
308
+ }
309
+
310
+ /** `a['name']`, or `a` for `@Args()`. */
311
+ function argsValue(ctx: Context, scope: Scope, name: string | undefined): GenNode {
312
+ const a = scope.a as GenNode
313
+ return name === undefined ? a : ctx.code`${a}[${name}]`
314
+ }
315
+
316
+ /** `validateArgs(Schema, a['input'], 'input')`. */
317
+ function validation(
318
+ ctx: Context,
319
+ imports: Imports,
320
+ scope: Scope,
321
+ param: Extract<ParamData, { kind: 'args' }>,
322
+ ): GenNode {
323
+ return ctx.call(imports.validateArgs(), [
324
+ ctx.ref(param.schema as NonNullable<typeof param.schema>),
325
+ argsValue(ctx, scope, param.name),
326
+ param.name ?? 'args',
327
+ ])
328
+ }
329
+
330
+ /**
331
+ * The body of the `invoke` of a field validating arguments: each validation is evaluated in order
332
+ * into a `const`; while they return values the method is called synchronously, the first thenable
333
+ * resumes in an `async` arrow that awaits it and the later validations (the order of evaluation and
334
+ * the error reported first are those of an `async` invoke awaiting every validation).
335
+ *
336
+ * ```ts
337
+ * (r: BookResolver, _p: unknown, a: GraphQLArgs) => {
338
+ * const v0 = validateArgs(NewBookInput, a['input'], 'input')
339
+ * if (isThenable(v0)) return (async () => r.addBook(await v0 as never))()
340
+ * return r.addBook(v0 as never)
341
+ * }
342
+ * ```
343
+ */
344
+ function validatingBlock(
345
+ ctx: Context,
346
+ imports: Imports,
347
+ r: GenNode,
348
+ scope: Scope,
349
+ field: FieldData,
350
+ validated: readonly number[],
351
+ ): GenNode {
352
+ return ctx.block(
353
+ validated.map(index => `v${index}`),
354
+ (...locals) => {
355
+ const local = new Map(validated.map((index, position) => [index, locals[position] as GenNode]))
356
+ const statements: GenNode[] = []
357
+ for (const index of validated) {
358
+ const name = local.get(index) as GenNode
359
+ const param = field.params[index] as Extract<ParamData, { kind: 'args' }>
360
+ statements.push(ctx.code`const ${name} = ${validation(ctx, imports, scope, param)}`)
361
+ const resume = ctx.arrow(
362
+ [],
363
+ () =>
364
+ call(ctx, r, field, position => {
365
+ const value = local.get(position)
366
+ if (value === undefined) return argument(ctx, scope, field, position)
367
+ if (position < index) return ctx.code`${value} as never`
368
+ if (position === index) return ctx.code`await ${value} as never`
369
+ const later = field.params[position] as Extract<ParamData, { kind: 'args' }>
370
+ return ctx.code`await ${validation(ctx, imports, scope, later)} as never`
371
+ }),
372
+ { async: true },
373
+ )
374
+ statements.push(ctx.code`if (${ctx.call(imports.isThenable(), [name])}) return (${resume})()`)
375
+ }
376
+ statements.push(
377
+ ctx.code`return ${call(ctx, r, field, position => {
378
+ const value = local.get(position)
379
+ return value === undefined ? argument(ctx, scope, field, position) : ctx.code`${value} as never`
380
+ })}`,
381
+ )
382
+ return statements
383
+ },
384
+ )
385
+ }
386
+
387
+ // ------------------------------------------------------------------------------------------- groups
388
+
389
+ /**
390
+ * `groups`: the route decorator groups the runtime needs (required, holding several keys, or named
391
+ * differently from their key), sorted; `undefined` when there is none (RFC 0001, as `extensions.http`).
392
+ */
393
+ function groupsNode(ctx: Context, meta: readonly MetaEntry[]): GenNode | undefined {
394
+ const groups = new Map<string, { readonly keys: Set<string>; required: boolean }>()
395
+ for (const entry of meta) {
396
+ const group = groups.get(entry.group)
397
+ if (group === undefined) groups.set(entry.group, { keys: new Set([entry.key]), required: entry.required })
398
+ else {
399
+ group.keys.add(entry.key)
400
+ group.required ||= entry.required
401
+ }
402
+ }
403
+ const needed = [...groups]
404
+ .filter(([name, group]) => group.required || group.keys.size > 1 || !group.keys.has(name))
405
+ .sort(([a], [b]) => compareStrings(a, b))
406
+ if (needed.length === 0) return undefined
407
+ return ctx.obj(
408
+ needed.map(
409
+ ([name, group]) =>
410
+ [
411
+ name,
412
+ ctx.obj([
413
+ ['keys', ctx.lit([...group.keys].sort(compareStrings))],
414
+ ['required', group.required],
415
+ ]),
416
+ ] as const,
417
+ ),
418
+ )
419
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The graphql-js modules schema-first compiles use (RFC 0007 §7): the language (parser, AST), the
3
+ * type system (`validateSchema`, type predicates) and the utilities (`buildASTSchema`,
4
+ * `concatAST`, `valueFromASTUntyped`). Loaded lazily, on the first schema-first compile, with
5
+ * `createRequire`: code-first compiles never load graphql-js, and the execution engine
6
+ * (`graphql/execution`, the `graphql` root entry) and dataloader are never loaded at build time
7
+ * (tests/boundaries.test.ts). graphql is a required peer of `@znight/graphql`.
8
+ */
9
+ import type * as Language from 'graphql/language'
10
+ import type * as Type from 'graphql/type'
11
+ import type * as Utilities from 'graphql/utilities'
12
+
13
+ import { createRequire } from 'node:module'
14
+
15
+ /** The build-time graphql-js modules. */
16
+ export interface GraphQLBuildModules {
17
+ readonly language: typeof Language
18
+ readonly type: typeof Type
19
+ readonly utilities: typeof Utilities
20
+ }
21
+
22
+ /** The only graphql-js modules the compiler extension may load (checked by tests/boundaries.test.ts). */
23
+ export const GRAPHQL_BUILD_MODULES: readonly string[] = Object.freeze([
24
+ 'graphql/language',
25
+ 'graphql/type',
26
+ 'graphql/utilities',
27
+ ])
28
+
29
+ let loaded: GraphQLBuildModules | undefined
30
+
31
+ /** The graphql-js modules, loaded on first use. */
32
+ export function graphqlModules(): GraphQLBuildModules {
33
+ if (loaded === undefined) {
34
+ const load = createRequire(import.meta.url)
35
+ loaded = Object.freeze({
36
+ language: load('graphql/language') as typeof Language,
37
+ type: load('graphql/type') as typeof Type,
38
+ utilities: load('graphql/utilities') as typeof Utilities,
39
+ })
40
+ }
41
+ return loaded
42
+ }
@@ -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,98 @@
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
+ import { buildSchemaFirstModel } from './schema-first.ts'
36
+
37
+ export type { FieldData, FieldOperation, GraphQLData, ParamData, ResolverData } from './data.ts'
38
+ export type { GraphQLCompilerOptions, ResolvedGraphQLCompilerOptions } from './options.ts'
39
+
40
+ const FIELD: DecoratorSpec = { targets: ['method'], params: ['static'] }
41
+ const NO_ARGUMENT: DecoratorSpec = { targets: ['parameter'], params: [] }
42
+
43
+ /** Decorators owned by the extension, by canonical identity (static / value arguments, §4.6). */
44
+ const decorators: Readonly<Record<string, DecoratorSpec>> = Object.freeze({
45
+ [GraphQLIds.Resolver]: { targets: ['class'], params: ['static', 'static'] },
46
+ [GraphQLIds.Query]: FIELD,
47
+ [GraphQLIds.Mutation]: FIELD,
48
+ [GraphQLIds.Subscription]: FIELD,
49
+ [GraphQLIds.ResolveField]: FIELD,
50
+ [GraphQLIds.Args]: { targets: ['parameter'], params: ['value', 'value'] },
51
+ [GraphQLIds.Parent]: NO_ARGUMENT,
52
+ [GraphQLIds.Ctx]: NO_ARGUMENT,
53
+ [GraphQLIds.Info]: NO_ARGUMENT,
54
+ [GraphQLIds.Loader]: NO_ARGUMENT,
55
+ [GraphQLIds.UseGuard]: { targets: ['class', 'method'], params: [], rest: 'value' },
56
+ } satisfies Record<string, DecoratorSpec>)
57
+
58
+ /** The `@znight/graphql` compiler extension, with its options and `configure()`. */
59
+ export interface GraphQLCompilerExtension extends CompilerExtension<GraphQLData> {
60
+ /** The options of this copy (defaults filled in). */
61
+ readonly options: ResolvedGraphQLCompilerOptions
62
+ /** A copy of the extension with `options` merged onto the options of this one. */
63
+ configure(options: GraphQLCompilerOptions): GraphQLCompilerExtension
64
+ }
65
+
66
+ function createExtension(options: ResolvedGraphQLCompilerOptions): GraphQLCompilerExtension {
67
+ /**
68
+ * Models by compile, built by `validate`, read by `generate`. The key is `graph.extensions`: the
69
+ * validate and generate contexts of one compile get different graph objects sharing this array.
70
+ */
71
+ const models = new WeakMap<object, GraphQLModel | undefined>()
72
+ const modelOf = (graph: ApplicationGraph): GraphQLModel | undefined => models.get(graph.extensions)
73
+ return Object.freeze({
74
+ apiVersion: 1,
75
+ name: EXTENSION_NAME,
76
+ decorators,
77
+ options,
78
+ analyzeClass: (cls, ctx) => analyzeGraphQLClass(cls, ctx, options),
79
+ validate(ctx) {
80
+ const schema = options.schema
81
+ models.set(
82
+ ctx.graph.extensions,
83
+ schema === false ? buildModel(ctx, options) : buildSchemaFirstModel(ctx, schema, options.sdl),
84
+ )
85
+ },
86
+ generate(ctx) {
87
+ return generateGraphQL(ctx, modelOf(ctx.graph), options)
88
+ },
89
+ configure(next: GraphQLCompilerOptions): GraphQLCompilerExtension {
90
+ return createExtension(resolveOptions(options, next))
91
+ },
92
+ } satisfies GraphQLCompilerExtension)
93
+ }
94
+
95
+ /** The `@znight/graphql` compiler extension with its default options. */
96
+ export const graphqlCompilerExtension: GraphQLCompilerExtension = createExtension(DEFAULT_OPTIONS)
97
+
98
+ export default graphqlCompilerExtension