@znight/graphql 0.0.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1346 -0
- package/dist/compiler.d.mts +114 -0
- package/dist/compiler.mjs +4599 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.d.mts +583 -0
- package/dist/index.mjs +134 -0
- package/dist/index.mjs.map +1 -0
- package/dist/names-BlzDM5cF.mjs +111 -0
- package/dist/names-BlzDM5cF.mjs.map +1 -0
- package/dist/plugin-CR_wVfRU.mjs +3445 -0
- package/dist/plugin-CR_wVfRU.mjs.map +1 -0
- package/dist/testing.d.mts +101 -0
- package/dist/testing.mjs +306 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-DQ713UgI.d.mts +266 -0
- package/package.json +97 -1
- package/src/compiler/analyze.ts +727 -0
- package/src/compiler/builder.ts +1561 -0
- package/src/compiler/checks.ts +105 -0
- package/src/compiler/conformance.ts +1068 -0
- package/src/compiler/constants.ts +80 -0
- package/src/compiler/data.ts +83 -0
- package/src/compiler/defaults.ts +92 -0
- package/src/compiler/discriminators.ts +53 -0
- package/src/compiler/docs.ts +55 -0
- package/src/compiler/generate.ts +419 -0
- package/src/compiler/graphql-js.ts +42 -0
- package/src/compiler/ids.ts +128 -0
- package/src/compiler/index.ts +98 -0
- package/src/compiler/locate.ts +82 -0
- package/src/compiler/model.ts +137 -0
- package/src/compiler/options.ts +85 -0
- package/src/compiler/scalars.ts +138 -0
- package/src/compiler/schema-first.ts +595 -0
- package/src/compiler/sdl-source.ts +529 -0
- package/src/compiler/sdl-view.ts +61 -0
- package/src/compiler/suggest.ts +39 -0
- package/src/context.ts +181 -0
- package/src/decisions.ts +169 -0
- package/src/decorators.ts +202 -0
- package/src/errors.ts +89 -0
- package/src/execution.ts +1064 -0
- package/src/format.ts +285 -0
- package/src/index.ts +89 -0
- package/src/limits.ts +177 -0
- package/src/loaders.ts +149 -0
- package/src/memory-pubsub.ts +133 -0
- package/src/metadata.ts +222 -0
- package/src/names.ts +47 -0
- package/src/options.ts +331 -0
- package/src/pipeline.ts +338 -0
- package/src/plugin.ts +579 -0
- package/src/pubsub.ts +36 -0
- package/src/results.ts +206 -0
- package/src/scalars.ts +138 -0
- package/src/schema-hash.ts +18 -0
- package/src/schema.ts +635 -0
- package/src/sdl.ts +530 -0
- package/src/testing.ts +454 -0
- package/src/tokens.ts +22 -0
- package/src/transport/abort.ts +80 -0
- package/src/transport/accept.ts +155 -0
- package/src/transport/graphiql.ts +106 -0
- package/src/transport/http.ts +373 -0
- package/src/transport/sse.ts +45 -0
- package/src/type-ref.ts +89 -0
- package/src/types.ts +225 -0
- package/src/validate-args.ts +48 -0
|
@@ -0,0 +1,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
|