@znight/graphql 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1062 -2
- package/dist/compiler.d.mts +93 -0
- package/dist/compiler.mjs +2832 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/index.d.mts +542 -0
- package/dist/index.mjs +128 -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-BdBxr5NQ.mjs +3222 -0
- package/dist/plugin-BdBxr5NQ.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-CgbV9LjD.d.mts +234 -0
- package/package.json +95 -4
- package/src/compiler/analyze.ts +608 -0
- package/src/compiler/builder.ts +1640 -0
- package/src/compiler/constants.ts +74 -0
- package/src/compiler/data.ts +73 -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 +405 -0
- package/src/compiler/ids.ts +128 -0
- package/src/compiler/index.ts +93 -0
- package/src/compiler/locate.ts +44 -0
- package/src/compiler/model.ts +122 -0
- package/src/compiler/options.ts +54 -0
- package/src/compiler/scalars.ts +138 -0
- package/src/compiler/sdl-view.ts +59 -0
- package/src/context.ts +181 -0
- package/src/decisions.ts +169 -0
- package/src/decorators.ts +196 -0
- package/src/errors.ts +89 -0
- package/src/execution.ts +1064 -0
- package/src/format.ts +285 -0
- package/src/index.ts +84 -0
- package/src/limits.ts +177 -0
- package/src/loaders.ts +149 -0
- package/src/memory-pubsub.ts +133 -0
- package/src/metadata.ts +195 -0
- package/src/names.ts +47 -0
- package/src/options.ts +309 -0
- package/src/pipeline.ts +277 -0
- package/src/plugin.ts +557 -0
- package/src/pubsub.ts +36 -0
- package/src/scalars.ts +131 -0
- package/src/schema-hash.ts +18 -0
- package/src/schema.ts +557 -0
- package/src/sdl.ts +515 -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 +190 -0
- 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
|
+
}
|