@znight/graphql 0.0.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1063 -0
- 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 +97 -1
- 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
package/src/pipeline.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The resolver pipeline (RFC 0005 §5.9, §7.3, §8.5): the resolve function of every resolver-backed
|
|
3
|
+
* field, decided once at boot.
|
|
4
|
+
*
|
|
5
|
+
* 1. the resolver instance: the singleton resolved at boot, or `container.get(token)` in the
|
|
6
|
+
* current scope (request-scoped and transient resolvers);
|
|
7
|
+
* 2. guards: global ones (on root fields, and on field resolvers carrying their own metadata), class
|
|
8
|
+
* `@UseGuard`, method `@UseGuard`; `false` fails the field with `FORBIDDEN`;
|
|
9
|
+
* 3. null handling of arguments (`nullAsUndefined`), only where a flag is reachable;
|
|
10
|
+
* 4. `invoke(resolver, parent, args, context, info)`.
|
|
11
|
+
*
|
|
12
|
+
* A field without guards nor normaliser calls `invoke` from the closure graphql-js needs anyway:
|
|
13
|
+
* no extra wrapper, no promise, no `await`. Plain property fields keep graphql-js' default resolver.
|
|
14
|
+
*/
|
|
15
|
+
import type { Container, Token } from '@znight/core'
|
|
16
|
+
import type { GraphQLFieldResolver, GraphQLResolveInfo } from 'graphql'
|
|
17
|
+
import type { OperationContext } from './context.ts'
|
|
18
|
+
import type { Resolvable } from './format.ts'
|
|
19
|
+
import type {
|
|
20
|
+
GraphQLArgumentDefinition,
|
|
21
|
+
GraphQLFieldDefinition,
|
|
22
|
+
GraphQLInputTypeDefinition,
|
|
23
|
+
GraphQLObjectTypeDefinition,
|
|
24
|
+
GraphQLResolverBinding,
|
|
25
|
+
} from './metadata.ts'
|
|
26
|
+
import type { FieldFunctions, FieldFunctionsFactory } from './schema.ts'
|
|
27
|
+
import type { GraphQLTypeRef } from './type-ref.ts'
|
|
28
|
+
import type { GraphQLArgs, GraphQLContext, GraphQLGuard, ResolverContext } from './types.ts'
|
|
29
|
+
|
|
30
|
+
import { isThenable } from '@znight/http'
|
|
31
|
+
|
|
32
|
+
import { GraphQLErrorCode, GraphQLException } from './errors.ts'
|
|
33
|
+
import { resolveIn } from './format.ts'
|
|
34
|
+
import { parseTypeRef } from './type-ref.ts'
|
|
35
|
+
|
|
36
|
+
/** What the pipeline needs, resolved at boot. */
|
|
37
|
+
export interface PipelineOptions {
|
|
38
|
+
readonly container: Container
|
|
39
|
+
/** Global guards (plugin option), in order. */
|
|
40
|
+
readonly globalGuards: readonly Resolvable<GraphQLGuard>[]
|
|
41
|
+
/** The guard provider of a `@UseGuard` token (memoised by the caller). */
|
|
42
|
+
readonly guard: (token: Token, coordinate: string) => Resolvable<GraphQLGuard>
|
|
43
|
+
/** The resolver provider of a binding (memoised by the caller). */
|
|
44
|
+
readonly resolver: (binding: GraphQLResolverBinding, coordinate: string) => Resolvable<object>
|
|
45
|
+
/** Input object types by name (null handling of nested input values). */
|
|
46
|
+
readonly inputs: ReadonlyMap<string, GraphQLInputTypeDefinition>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The generated invoker of a binding. */
|
|
50
|
+
type Invoke = GraphQLResolverBinding['invoke']
|
|
51
|
+
|
|
52
|
+
/** Mutates a coerced argument or input value in place: `null` → `undefined` where flagged. */
|
|
53
|
+
type Normalize = (value: unknown) => void
|
|
54
|
+
|
|
55
|
+
/** Whether global guards run on a field: root fields always, field resolvers when they carry metadata (§5.9). */
|
|
56
|
+
export function globalGuardsApply(binding: GraphQLResolverBinding): boolean {
|
|
57
|
+
return binding.operation !== 'field' || Object.keys(binding.meta).length > 0
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The {@link FieldFunctionsFactory} of the plugin. */
|
|
61
|
+
export function pipelineFactory(options: PipelineOptions): FieldFunctionsFactory {
|
|
62
|
+
const normalizers = new NormalizerBuilder(options.inputs)
|
|
63
|
+
return (binding, coordinate, field, parent) =>
|
|
64
|
+
fieldFunctions(options, normalizers, binding, coordinate, field, parent)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fieldFunctions(
|
|
68
|
+
options: PipelineOptions,
|
|
69
|
+
normalizers: NormalizerBuilder,
|
|
70
|
+
binding: GraphQLResolverBinding,
|
|
71
|
+
coordinate: string,
|
|
72
|
+
field: GraphQLFieldDefinition,
|
|
73
|
+
parent: GraphQLObjectTypeDefinition,
|
|
74
|
+
): FieldFunctions {
|
|
75
|
+
const container = options.container
|
|
76
|
+
const invoke: Invoke = binding.invoke
|
|
77
|
+
const target = options.resolver(binding, coordinate)
|
|
78
|
+
const guards: Resolvable<GraphQLGuard>[] = [
|
|
79
|
+
...(globalGuardsApply(binding) ? options.globalGuards : []),
|
|
80
|
+
...binding.guards.map(token => options.guard(token, coordinate)),
|
|
81
|
+
]
|
|
82
|
+
const normalize = field.args === undefined ? undefined : normalizers.args(field.args)
|
|
83
|
+
|
|
84
|
+
let resolve: GraphQLFieldResolver<unknown, unknown>
|
|
85
|
+
if (guards.length === 0) {
|
|
86
|
+
// the common case: one closure, invoke called directly (no promise, no await)
|
|
87
|
+
if (target.token === undefined) {
|
|
88
|
+
const instance = target.instance
|
|
89
|
+
resolve =
|
|
90
|
+
normalize === undefined
|
|
91
|
+
? (p, a, c, i) => invoke(instance, p, a as GraphQLArgs, c as GraphQLContext, i)
|
|
92
|
+
: (p, a, c, i) => {
|
|
93
|
+
normalize(a)
|
|
94
|
+
return invoke(instance, p, a as GraphQLArgs, c as GraphQLContext, i)
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
const token = target.token
|
|
98
|
+
resolve =
|
|
99
|
+
normalize === undefined
|
|
100
|
+
? (p, a, c, i) => invoke(container.get(token), p, a as GraphQLArgs, c as GraphQLContext, i)
|
|
101
|
+
: (p, a, c, i) => {
|
|
102
|
+
normalize(a)
|
|
103
|
+
return invoke(container.get(token), p, a as GraphQLArgs, c as GraphQLContext, i)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
const route = Object.freeze({ meta: binding.meta, coordinate })
|
|
108
|
+
const parentType = parent.name
|
|
109
|
+
const fieldName = field.name
|
|
110
|
+
const forbidden = (): never => {
|
|
111
|
+
throw new GraphQLException(`Not allowed to resolve ${coordinate}`, { code: GraphQLErrorCode.Forbidden })
|
|
112
|
+
}
|
|
113
|
+
resolve = (p, a, c, i) => {
|
|
114
|
+
const operation = c as OperationContext
|
|
115
|
+
const args = a as GraphQLArgs
|
|
116
|
+
const allowed = runGuards(
|
|
117
|
+
container,
|
|
118
|
+
guards,
|
|
119
|
+
new FieldContext(route, operation, parentType, fieldName, args, p, i),
|
|
120
|
+
)
|
|
121
|
+
// §8.5: guards (they see the coerced arguments), then null handling, then invoke
|
|
122
|
+
const call = (): unknown => {
|
|
123
|
+
if (normalize !== undefined) normalize(args)
|
|
124
|
+
return invoke(resolveIn(container, target), p, args, operation, i)
|
|
125
|
+
}
|
|
126
|
+
if (allowed === true) return call()
|
|
127
|
+
if (allowed === false) return forbidden()
|
|
128
|
+
return allowed.then(ok => (ok ? call() : forbidden()))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (binding.operation === 'subscription') {
|
|
132
|
+
// the method gives the source event stream; each event is the field value
|
|
133
|
+
return { subscribe: resolve, resolve: identity }
|
|
134
|
+
}
|
|
135
|
+
return { resolve }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function identity(payload: unknown): unknown {
|
|
139
|
+
return payload
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Runs guards in order, synchronously until one returns a promise. */
|
|
143
|
+
function runGuards(
|
|
144
|
+
container: Container,
|
|
145
|
+
guards: readonly Resolvable<GraphQLGuard>[],
|
|
146
|
+
ctx: ResolverContext,
|
|
147
|
+
start = 0,
|
|
148
|
+
): boolean | Promise<boolean> {
|
|
149
|
+
for (let index = start; index < guards.length; index++) {
|
|
150
|
+
const result = resolveIn(container, guards[index] as Resolvable<GraphQLGuard>).canResolve(ctx)
|
|
151
|
+
if (isThenable(result)) {
|
|
152
|
+
return Promise.resolve(result).then(ok => (ok ? runGuards(container, guards, ctx, index + 1) : false))
|
|
153
|
+
}
|
|
154
|
+
if (!result) return false
|
|
155
|
+
}
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What a guard receives (§5.9): a `RouteMetaCarrier` with the field's effective metadata. */
|
|
160
|
+
class FieldContext implements ResolverContext {
|
|
161
|
+
readonly route: ResolverContext['route']
|
|
162
|
+
readonly operation: OperationContext
|
|
163
|
+
readonly parentType: string
|
|
164
|
+
readonly field: string
|
|
165
|
+
readonly args: GraphQLArgs
|
|
166
|
+
readonly parent: unknown
|
|
167
|
+
readonly info: GraphQLResolveInfo
|
|
168
|
+
|
|
169
|
+
constructor(
|
|
170
|
+
route: ResolverContext['route'],
|
|
171
|
+
operation: OperationContext,
|
|
172
|
+
parentType: string,
|
|
173
|
+
field: string,
|
|
174
|
+
args: GraphQLArgs,
|
|
175
|
+
parent: unknown,
|
|
176
|
+
info: GraphQLResolveInfo,
|
|
177
|
+
) {
|
|
178
|
+
this.route = route
|
|
179
|
+
this.operation = operation
|
|
180
|
+
this.parentType = parentType
|
|
181
|
+
this.field = field
|
|
182
|
+
this.args = args
|
|
183
|
+
this.parent = parent
|
|
184
|
+
this.info = info
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
get http(): ResolverContext['http'] {
|
|
188
|
+
return this.operation.http
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** One step of a normaliser: an argument or input field. */
|
|
193
|
+
interface Step {
|
|
194
|
+
readonly name: string
|
|
195
|
+
/** The value itself: `null` → `undefined`. */
|
|
196
|
+
readonly own: boolean
|
|
197
|
+
/** Nested input values. */
|
|
198
|
+
readonly nested: Normalize | undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Builds normalisers (§7.3) at boot. Values are mutated in place: graphql-js creates the argument
|
|
203
|
+
* object of every call and the coerced input objects, and the flags of an input type are the same
|
|
204
|
+
* wherever it is used, so a value shared through a variable is normalised the same way twice.
|
|
205
|
+
*/
|
|
206
|
+
class NormalizerBuilder {
|
|
207
|
+
readonly #inputs: ReadonlyMap<string, GraphQLInputTypeDefinition>
|
|
208
|
+
/** Input type → its normaliser (`null`: nothing to do), `PENDING` while being built (cycles). */
|
|
209
|
+
readonly #types = new Map<string, Normalize | null | typeof PENDING>()
|
|
210
|
+
|
|
211
|
+
constructor(inputs: ReadonlyMap<string, GraphQLInputTypeDefinition>) {
|
|
212
|
+
this.#inputs = inputs
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The normaliser of a field's arguments, `undefined` when no flag is reachable. */
|
|
216
|
+
args(args: readonly GraphQLArgumentDefinition[]): ((args: unknown) => void) | undefined {
|
|
217
|
+
const steps = this.#steps(args)
|
|
218
|
+
return steps === undefined ? undefined : record => applySteps(record, steps)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
#steps(values: readonly GraphQLArgumentDefinition[]): readonly Step[] | undefined {
|
|
222
|
+
const steps: Step[] = []
|
|
223
|
+
for (const value of values) {
|
|
224
|
+
const own = value.nullAsUndefined === true
|
|
225
|
+
const ref = parseTypeRef(value.type)
|
|
226
|
+
const nested = ref === undefined ? undefined : this.#value(ref)
|
|
227
|
+
if (own || nested !== undefined) steps.push({ name: value.name, own, nested })
|
|
228
|
+
}
|
|
229
|
+
return steps.length === 0 ? undefined : steps
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#value(ref: GraphQLTypeRef): Normalize | undefined {
|
|
233
|
+
if (ref.kind === 'list') {
|
|
234
|
+
const item = this.#value(ref.of)
|
|
235
|
+
return item === undefined
|
|
236
|
+
? undefined
|
|
237
|
+
: value => {
|
|
238
|
+
if (Array.isArray(value)) for (const entry of value) item(entry)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return this.#input(ref.name)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
#input(name: string): Normalize | undefined {
|
|
245
|
+
const known = this.#types.get(name)
|
|
246
|
+
if (known === PENDING) {
|
|
247
|
+
// a cycle through this type: delegate to its normaliser once built
|
|
248
|
+
return value => {
|
|
249
|
+
const normalize = this.#types.get(name)
|
|
250
|
+
if (typeof normalize === 'function') normalize(value)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (known !== undefined) return known ?? undefined
|
|
254
|
+
const type = this.#inputs.get(name)
|
|
255
|
+
if (type === undefined) return undefined
|
|
256
|
+
this.#types.set(name, PENDING)
|
|
257
|
+
const steps = this.#steps(type.fields)
|
|
258
|
+
const normalize: Normalize | null = steps === undefined ? null : value => applySteps(value, steps)
|
|
259
|
+
this.#types.set(name, normalize)
|
|
260
|
+
return normalize ?? undefined
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const PENDING: unique symbol = Symbol('pending')
|
|
265
|
+
|
|
266
|
+
function applySteps(value: unknown, steps: readonly Step[]): void {
|
|
267
|
+
if (value === null || typeof value !== 'object') return
|
|
268
|
+
const record = value as Record<string, unknown>
|
|
269
|
+
for (const step of steps) {
|
|
270
|
+
const entry = record[step.name]
|
|
271
|
+
if (entry === null) {
|
|
272
|
+
if (step.own) record[step.name] = undefined
|
|
273
|
+
} else if (entry !== undefined && step.nested !== undefined) {
|
|
274
|
+
step.nested(entry)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|