@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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The schema model built by `validate` (RFC 0005 §6.1) and emitted by `generate` (§7): the GraphQL
|
|
3
|
+
* types reachable from the root fields, with their TypeScript origins (locations, resolver data).
|
|
4
|
+
*/
|
|
5
|
+
import type { Location } from '@znight/compiler'
|
|
6
|
+
import type { MetaEntry } from '@znight/http/compiler'
|
|
7
|
+
import type { GraphQLTypeRef } from '../type-ref.ts'
|
|
8
|
+
import type { FieldData, ResolverData } from './data.ts'
|
|
9
|
+
|
|
10
|
+
/** A field of an object type. */
|
|
11
|
+
export interface FieldModel {
|
|
12
|
+
readonly name: string
|
|
13
|
+
/** Printed type reference (`'[Book!]!'`). */
|
|
14
|
+
type: string
|
|
15
|
+
description: string | undefined
|
|
16
|
+
deprecationReason: string | undefined
|
|
17
|
+
readonly args: ArgModel[]
|
|
18
|
+
/** The resolver method behind the field; `undefined`: the default resolver (`parent[name]`). */
|
|
19
|
+
binding: { readonly resolver: ResolverData; readonly field: FieldData } | undefined
|
|
20
|
+
readonly location: Location | undefined
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** An argument or an input field. */
|
|
24
|
+
export interface ArgModel {
|
|
25
|
+
readonly name: string
|
|
26
|
+
type: string
|
|
27
|
+
ref: GraphQLTypeRef
|
|
28
|
+
readonly description: string | undefined
|
|
29
|
+
readonly deprecationReason: string | undefined
|
|
30
|
+
/** External default value (enum values by name); `undefined`: no default. */
|
|
31
|
+
defaultValue: unknown
|
|
32
|
+
/** The TypeScript type does not accept `null` (§5.4). */
|
|
33
|
+
nullAsUndefined: boolean
|
|
34
|
+
readonly location: Location | undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What every named type of the model has. */
|
|
38
|
+
interface BaseModel {
|
|
39
|
+
readonly name: string
|
|
40
|
+
/** The declaration key (shapes) or a synthetic key (`#root:query`, `#builtin:DateTime`). */
|
|
41
|
+
readonly key: string
|
|
42
|
+
readonly description: string | undefined
|
|
43
|
+
readonly location: Location | undefined
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An object type (root types included). */
|
|
47
|
+
export interface ObjectModel extends BaseModel {
|
|
48
|
+
readonly kind: 'object'
|
|
49
|
+
readonly fields: FieldModel[]
|
|
50
|
+
/** How the type was first reached (`Query.book → Book`), for "reached from" diagnostics. */
|
|
51
|
+
readonly trail: string
|
|
52
|
+
/** The root field the type was first reached from. */
|
|
53
|
+
readonly root: Location | undefined
|
|
54
|
+
/** Identity of the class when the type is a non-generic class (union discriminators). */
|
|
55
|
+
readonly classIdentity: string | undefined
|
|
56
|
+
/** Property names of the declaration, with their literal value when they have one (discriminators). */
|
|
57
|
+
readonly properties: readonly DiscriminantProperty[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A property of an object type's declaration, as union discriminators see it. */
|
|
61
|
+
export interface DiscriminantProperty {
|
|
62
|
+
readonly name: string
|
|
63
|
+
readonly optional: boolean
|
|
64
|
+
/** The literal value of a literal-typed property. */
|
|
65
|
+
readonly literal: string | number | boolean | undefined
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** An input object type. */
|
|
69
|
+
export interface InputModel extends BaseModel {
|
|
70
|
+
readonly kind: 'input'
|
|
71
|
+
readonly fields: ArgModel[]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** An enum type. */
|
|
75
|
+
export interface EnumModel extends BaseModel {
|
|
76
|
+
readonly kind: 'enum'
|
|
77
|
+
readonly values: readonly EnumValueModel[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A value of an enum. */
|
|
81
|
+
export interface EnumValueModel {
|
|
82
|
+
readonly name: string
|
|
83
|
+
readonly value: string | number
|
|
84
|
+
readonly description: string | undefined
|
|
85
|
+
readonly deprecationReason: string | undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A union type. */
|
|
89
|
+
export interface UnionModel extends BaseModel {
|
|
90
|
+
readonly kind: 'union'
|
|
91
|
+
members: readonly string[]
|
|
92
|
+
discriminator: DiscriminatorModel | undefined
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** How the runtime tells union members apart. */
|
|
96
|
+
export type DiscriminatorModel =
|
|
97
|
+
| { readonly by: 'property'; readonly property: string; readonly values: Readonly<Record<string, string>> }
|
|
98
|
+
| { readonly by: 'class'; readonly classes: readonly { readonly identity: string; readonly name: string }[] }
|
|
99
|
+
| { readonly by: 'typename' }
|
|
100
|
+
|
|
101
|
+
/** A scalar: built in (`DateTime`, `BigInt`, `JSON`) or declared with `scalar<T>()`. */
|
|
102
|
+
export interface ScalarModel extends BaseModel {
|
|
103
|
+
readonly kind: 'scalar'
|
|
104
|
+
/** Identity of the exported `scalar<T>()` constant; `undefined` for a built-in scalar. */
|
|
105
|
+
readonly definition: string | undefined
|
|
106
|
+
readonly specifiedByURL: string | undefined
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A named type of the model. */
|
|
110
|
+
export type TypeModel = ObjectModel | InputModel | EnumModel | UnionModel | ScalarModel
|
|
111
|
+
|
|
112
|
+
/** The schema of the application, built once per compile. */
|
|
113
|
+
export interface GraphQLModel {
|
|
114
|
+
readonly roots: { readonly query: string; readonly mutation?: string; readonly subscription?: string }
|
|
115
|
+
/** Sorted by name. */
|
|
116
|
+
readonly types: readonly TypeModel[]
|
|
117
|
+
/** Route metadata entries of every field (effective), for `groups`. */
|
|
118
|
+
readonly meta: readonly MetaEntry[]
|
|
119
|
+
/** The SDL (`printSdl`), without trailing newline. */
|
|
120
|
+
readonly sdl: string
|
|
121
|
+
readonly schemaHash: string
|
|
122
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options of the compiler extension (RFC 0005 §6.7), given with `graphql.configure({ ... })` in
|
|
3
|
+
* `znight.config.ts`. Import-free.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Options of `graphql.configure()`. */
|
|
7
|
+
export interface GraphQLCompilerOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Path of the SDL artifact, relative to the project root (`'schema.graphql'`), written next to the
|
|
10
|
+
* generated module and checked by `znight generate --check`. Default `false`: no file.
|
|
11
|
+
*/
|
|
12
|
+
readonly sdl?: string | false
|
|
13
|
+
/** GraphQL type of `number`. Default `'Float'`; `'Int'` suits counts and identifiers. */
|
|
14
|
+
readonly number?: 'Float' | 'Int'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** {@link GraphQLCompilerOptions} with their defaults. */
|
|
18
|
+
export interface ResolvedGraphQLCompilerOptions {
|
|
19
|
+
readonly sdl: string | false
|
|
20
|
+
readonly number: 'Float' | 'Int'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The defaults. */
|
|
24
|
+
export const DEFAULT_OPTIONS: ResolvedGraphQLCompilerOptions = Object.freeze({ sdl: false, number: 'Float' })
|
|
25
|
+
|
|
26
|
+
const KNOWN = new Set(['sdl', 'number'])
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Merges `options` onto `base`. Throws a `TypeError` naming the option on an invalid value: options
|
|
30
|
+
* are written in `znight.config.ts`, the error surfaces when the configuration is loaded.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveOptions(
|
|
33
|
+
base: ResolvedGraphQLCompilerOptions,
|
|
34
|
+
options: GraphQLCompilerOptions,
|
|
35
|
+
): ResolvedGraphQLCompilerOptions {
|
|
36
|
+
if (typeof options !== 'object' || options === null || Array.isArray(options)) {
|
|
37
|
+
throw new TypeError('[znight] graphql.configure() expects an options object: { sdl?, number? }')
|
|
38
|
+
}
|
|
39
|
+
for (const key of Object.keys(options)) {
|
|
40
|
+
if (!KNOWN.has(key))
|
|
41
|
+
throw new TypeError(`[znight] graphql.configure(): unknown option "${key}" (options: sdl, number)`)
|
|
42
|
+
}
|
|
43
|
+
const sdl = options.sdl === undefined ? base.sdl : options.sdl
|
|
44
|
+
if (sdl !== false && (typeof sdl !== 'string' || sdl.trim() === '')) {
|
|
45
|
+
throw new TypeError(
|
|
46
|
+
'[znight] graphql.configure(): "sdl" must be a path relative to the project root (\'schema.graphql\') or false',
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
const number = options.number === undefined ? base.number : options.number
|
|
50
|
+
if (number !== 'Float' && number !== 'Int') {
|
|
51
|
+
throw new TypeError(`[znight] graphql.configure(): "number" must be 'Float' or 'Int'`)
|
|
52
|
+
}
|
|
53
|
+
return Object.freeze({ sdl, number })
|
|
54
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `scalar<T>(name, definition)` declarations of the application (RFC 0005 §5.7): `T` (a plain type
|
|
3
|
+
* reference, by identity) maps to the scalar `name` wherever it appears. The definition is an
|
|
4
|
+
* exported value the generated module imports; its `description` and `specifiedByURL` are read
|
|
5
|
+
* for the SDL when they are statically evaluable.
|
|
6
|
+
*/
|
|
7
|
+
import type { ArgInfo, Diagnostic, Location, ValidateContext } from '@znight/compiler'
|
|
8
|
+
import type { GraphQLData } from './data.ts'
|
|
9
|
+
|
|
10
|
+
import { isGraphQLName, SPECIFIED_SCALARS } from '../names.ts'
|
|
11
|
+
import { Codes } from './constants.ts'
|
|
12
|
+
import { GraphQLIds, SCALAR_ALIASES } from './ids.ts'
|
|
13
|
+
|
|
14
|
+
/** A valid `scalar<T>()` declaration. */
|
|
15
|
+
export interface ScalarDeclaration {
|
|
16
|
+
/** GraphQL name. */
|
|
17
|
+
readonly name: string
|
|
18
|
+
/** Identity of the exported constant (imported by the generated module). */
|
|
19
|
+
readonly identity: string
|
|
20
|
+
/** Identity of `T`. */
|
|
21
|
+
readonly type: string
|
|
22
|
+
readonly description: string | undefined
|
|
23
|
+
readonly specifiedByURL: string | undefined
|
|
24
|
+
/** Location of the constant's name. */
|
|
25
|
+
readonly location: Location
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Valid declarations by the identity of their type; invalid ones are reported. */
|
|
29
|
+
export function readScalars(
|
|
30
|
+
ctx: ValidateContext<GraphQLData>,
|
|
31
|
+
report: (diagnostic: Diagnostic) => void,
|
|
32
|
+
): ReadonlyMap<string, ScalarDeclaration> {
|
|
33
|
+
const byType = new Map<string, ScalarDeclaration>()
|
|
34
|
+
const byName = new Map<string, ScalarDeclaration>()
|
|
35
|
+
for (const definition of ctx.definitionsOf(GraphQLIds.scalar)) {
|
|
36
|
+
const fail = (message: string, hints: readonly string[], related: Diagnostic['related'] = []): void =>
|
|
37
|
+
report({ code: Codes.InvalidDecorator, severity: 'error', message, location: definition.call, hints, related })
|
|
38
|
+
const [nameArg, definitionArg] = definition.args
|
|
39
|
+
const name = nameArg?.evaluated === true ? nameArg.value : undefined
|
|
40
|
+
if (typeof name !== 'string') {
|
|
41
|
+
fail(`scalar() of ${definition.name} needs a static name`, [`scalar<T>('Name', { ... })`])
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
if (!isGraphQLName(name) || SPECIFIED_SCALARS.has(name)) {
|
|
45
|
+
report({
|
|
46
|
+
code: Codes.NameConflict,
|
|
47
|
+
severity: 'error',
|
|
48
|
+
message: SPECIFIED_SCALARS.has(name)
|
|
49
|
+
? `'${name}' is a specified GraphQL scalar: scalar() cannot redefine it (${definition.name})`
|
|
50
|
+
: `'${name}' is not a valid GraphQL scalar name (${definition.name})`,
|
|
51
|
+
location: nameArg?.location ?? definition.call,
|
|
52
|
+
hints: ['GraphQL names match /^[_A-Za-z][_0-9A-Za-z]*$/ and do not start with __'],
|
|
53
|
+
})
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
const type = definition.typeArgIdentities?.[0]
|
|
57
|
+
if (type === undefined) {
|
|
58
|
+
report({
|
|
59
|
+
code: Codes.InvalidType,
|
|
60
|
+
severity: 'error',
|
|
61
|
+
message: `scalar('${name}') of ${definition.name} needs a named type argument: scalar<T>('${name}', ...)`,
|
|
62
|
+
location: definition.call,
|
|
63
|
+
hints: [
|
|
64
|
+
`Name the TypeScript type of the scalar: export type ${name} = string & { readonly __brand: '${name}' }, then scalar<${name}>('${name}', ...)`,
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (SCALAR_ALIASES.has(type)) {
|
|
70
|
+
fail(
|
|
71
|
+
`scalar('${name}') of ${definition.name} cannot map ${type.slice(type.indexOf('#') + 1)}: it is a built-in scalar alias`,
|
|
72
|
+
['Declare a branded type for the scalar: export type Name = string & { readonly __brand: "Name" }'],
|
|
73
|
+
)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (!definition.exported) {
|
|
77
|
+
fail(`The scalar ${definition.name} must be exported: the generated module imports it`, [
|
|
78
|
+
`export const ${definition.name} = scalar<...>('${name}', ...)`,
|
|
79
|
+
])
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const declaration: ScalarDeclaration = {
|
|
83
|
+
name,
|
|
84
|
+
identity: definition.identity,
|
|
85
|
+
type,
|
|
86
|
+
description: staticProperty(ctx, definitionArg, 'description'),
|
|
87
|
+
specifiedByURL: staticProperty(ctx, definitionArg, 'specifiedByURL'),
|
|
88
|
+
location: definition.location,
|
|
89
|
+
}
|
|
90
|
+
const sameType = byType.get(type)
|
|
91
|
+
if (sameType !== undefined) {
|
|
92
|
+
report({
|
|
93
|
+
code: Codes.NameConflict,
|
|
94
|
+
severity: 'error',
|
|
95
|
+
message: `${definition.name} and ${sameType.identity} both map ${type.slice(type.indexOf('#') + 1)} to a scalar`,
|
|
96
|
+
location: definition.location,
|
|
97
|
+
related: [{ message: 'the other scalar', location: sameType.location }],
|
|
98
|
+
hints: ['Keep one scalar per TypeScript type'],
|
|
99
|
+
})
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
// a user scalar named like a built-in one (DateTime) only conflicts when both are used: the model reports it
|
|
103
|
+
const sameName = byName.get(name)
|
|
104
|
+
if (sameName !== undefined) {
|
|
105
|
+
report({
|
|
106
|
+
code: Codes.NameConflict,
|
|
107
|
+
severity: 'error',
|
|
108
|
+
message: `Two scalars are named '${name}': ${definition.identity} and ${sameName.identity}`,
|
|
109
|
+
location: definition.location,
|
|
110
|
+
related: [{ message: 'the other scalar', location: sameName.location }],
|
|
111
|
+
hints: ['Rename one of them'],
|
|
112
|
+
})
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
byType.set(type, declaration)
|
|
116
|
+
byName.set(name, declaration)
|
|
117
|
+
}
|
|
118
|
+
return byType
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A string property of an object literal argument, when statically evaluable. */
|
|
122
|
+
function staticProperty(ctx: ValidateContext<GraphQLData>, arg: ArgInfo | undefined, key: string): string | undefined {
|
|
123
|
+
if (arg === undefined) return undefined
|
|
124
|
+
if (arg.evaluated && typeof arg.value === 'object' && arg.value !== null && !Array.isArray(arg.value)) {
|
|
125
|
+
const value = (arg.value as Readonly<Record<string, unknown>>)[key]
|
|
126
|
+
return typeof value === 'string' ? value : undefined
|
|
127
|
+
}
|
|
128
|
+
const node = arg.node
|
|
129
|
+
if (node.type !== 'ObjectExpression') return undefined
|
|
130
|
+
for (const property of node.properties) {
|
|
131
|
+
if (property.type !== 'Property' || property.computed) continue
|
|
132
|
+
const name = property.key.type === 'Identifier' ? property.key.name : undefined
|
|
133
|
+
if (name !== key) continue
|
|
134
|
+
const result = ctx.project.evaluate(arg.location.file, property.value, { refs: 'none' })
|
|
135
|
+
return result.kind === 'value' && typeof result.value === 'string' ? result.value : undefined
|
|
136
|
+
}
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SDL view of the model (`sdl.ts` prints it), from which `schemaHash` and the SDL artifact
|
|
3
|
+
* derive.
|
|
4
|
+
*/
|
|
5
|
+
import type { SdlInputValue, SdlType } from '../sdl.ts'
|
|
6
|
+
import type { ArgModel, TypeModel } from './model.ts'
|
|
7
|
+
|
|
8
|
+
/** The SDL view of a model type. */
|
|
9
|
+
export function sdlType(model: TypeModel): SdlType {
|
|
10
|
+
const base = model.description === undefined ? {} : { description: model.description }
|
|
11
|
+
switch (model.kind) {
|
|
12
|
+
case 'object':
|
|
13
|
+
return {
|
|
14
|
+
kind: 'object',
|
|
15
|
+
name: model.name,
|
|
16
|
+
...base,
|
|
17
|
+
fields: model.fields.map(field => ({
|
|
18
|
+
name: field.name,
|
|
19
|
+
type: field.type,
|
|
20
|
+
...(field.description === undefined ? {} : { description: field.description }),
|
|
21
|
+
...(field.deprecationReason === undefined ? {} : { deprecationReason: field.deprecationReason }),
|
|
22
|
+
...(field.args.length === 0 ? {} : { args: field.args.map(sdlInputValue) }),
|
|
23
|
+
})),
|
|
24
|
+
}
|
|
25
|
+
case 'input':
|
|
26
|
+
return { kind: 'input', name: model.name, ...base, fields: model.fields.map(sdlInputValue) }
|
|
27
|
+
case 'enum':
|
|
28
|
+
return {
|
|
29
|
+
kind: 'enum',
|
|
30
|
+
name: model.name,
|
|
31
|
+
...base,
|
|
32
|
+
values: model.values.map(value => ({
|
|
33
|
+
name: value.name,
|
|
34
|
+
...(value.description === undefined ? {} : { description: value.description }),
|
|
35
|
+
...(value.deprecationReason === undefined ? {} : { deprecationReason: value.deprecationReason }),
|
|
36
|
+
})),
|
|
37
|
+
}
|
|
38
|
+
case 'union':
|
|
39
|
+
return { kind: 'union', name: model.name, ...base, members: model.members }
|
|
40
|
+
case 'scalar':
|
|
41
|
+
return {
|
|
42
|
+
kind: 'scalar',
|
|
43
|
+
name: model.name,
|
|
44
|
+
...base,
|
|
45
|
+
...(model.definition === undefined ? { builtin: true } : {}),
|
|
46
|
+
...(model.specifiedByURL === undefined ? {} : { specifiedByURL: model.specifiedByURL }),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sdlInputValue(arg: ArgModel): SdlInputValue {
|
|
52
|
+
return {
|
|
53
|
+
name: arg.name,
|
|
54
|
+
type: arg.type,
|
|
55
|
+
...(arg.description === undefined ? {} : { description: arg.description }),
|
|
56
|
+
...(arg.deprecationReason === undefined ? {} : { deprecationReason: arg.deprecationReason }),
|
|
57
|
+
...(arg.defaultValue === undefined ? {} : { defaultValue: arg.defaultValue }),
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The implementation of `GraphQLContext` (RFC 0005 §5.8): one per operation, passed to graphql-js
|
|
3
|
+
* as `contextValue` and stored in the request scope (`@znight/graphql#GraphQLContext`).
|
|
4
|
+
*
|
|
5
|
+
* Everything that is not needed by every operation is computed on first read: the request id (a
|
|
6
|
+
* UUID for direct executions), the HTTP context of an enclosing request, the signal, the state map
|
|
7
|
+
* and the loader registry.
|
|
8
|
+
*/
|
|
9
|
+
import type { Container, ScopeContext, Token } from '@znight/core'
|
|
10
|
+
import type { HttpContext } from '@znight/http'
|
|
11
|
+
import type { DocumentNode, OperationDefinitionNode } from 'graphql'
|
|
12
|
+
import type { LoaderFactory } from './loaders.ts'
|
|
13
|
+
import type {
|
|
14
|
+
BatchLoad,
|
|
15
|
+
BatchLoadClass,
|
|
16
|
+
BatchLoader,
|
|
17
|
+
GraphQLContext,
|
|
18
|
+
GraphQLOperationKind,
|
|
19
|
+
GraphQLTransport,
|
|
20
|
+
LoaderKey,
|
|
21
|
+
LoaderValue,
|
|
22
|
+
} from './types.ts'
|
|
23
|
+
|
|
24
|
+
import { randomUUID } from 'node:crypto'
|
|
25
|
+
import { ErrorCode, ZnightError } from '@znight/core'
|
|
26
|
+
import { HttpTokens } from '@znight/http'
|
|
27
|
+
|
|
28
|
+
/** What every operation context of a plugin shares. */
|
|
29
|
+
export interface ContextRuntime {
|
|
30
|
+
readonly container: Container
|
|
31
|
+
readonly loaders: LoaderFactory
|
|
32
|
+
/** The plugin's own signal (aborted by `onStop`): the signal of direct executions. */
|
|
33
|
+
readonly signal: AbortSignal
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What a request carries that the context exposes (a `GraphQLRequest`). */
|
|
37
|
+
export interface ContextRequest {
|
|
38
|
+
readonly variables?: Readonly<Record<string, unknown>> | null | undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** How the request reached the executor (`ExecuteOptions`). */
|
|
42
|
+
export interface ContextOptions {
|
|
43
|
+
readonly transport?: GraphQLTransport
|
|
44
|
+
readonly http?: HttpContext
|
|
45
|
+
readonly signal?: AbortSignal
|
|
46
|
+
readonly requestId?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const NO_VARIABLES: Readonly<Record<string, unknown>> = Object.freeze({})
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The `GraphQLContext` of one operation. Built from references the executor already holds (the
|
|
53
|
+
* operation node, the request, the options, the enclosing scope): one allocation per operation,
|
|
54
|
+
* every field read on demand.
|
|
55
|
+
*/
|
|
56
|
+
export class OperationContext implements GraphQLContext {
|
|
57
|
+
readonly document: DocumentNode
|
|
58
|
+
readonly #runtime: ContextRuntime
|
|
59
|
+
readonly #operation: OperationDefinitionNode
|
|
60
|
+
readonly #request: ContextRequest
|
|
61
|
+
readonly #options: ContextOptions
|
|
62
|
+
readonly #scope: ScopeContext | undefined
|
|
63
|
+
#requestId: string | undefined
|
|
64
|
+
#http: HttpContext | null | undefined
|
|
65
|
+
#signal: AbortSignal | undefined
|
|
66
|
+
#ended: AbortSignal | undefined
|
|
67
|
+
#state: Map<string | symbol, unknown> | undefined
|
|
68
|
+
#loaders: Map<Token, BatchLoader<unknown, unknown>> | undefined
|
|
69
|
+
|
|
70
|
+
constructor(
|
|
71
|
+
runtime: ContextRuntime,
|
|
72
|
+
document: DocumentNode,
|
|
73
|
+
operation: OperationDefinitionNode,
|
|
74
|
+
request: ContextRequest,
|
|
75
|
+
options: ContextOptions,
|
|
76
|
+
scope: ScopeContext | undefined,
|
|
77
|
+
) {
|
|
78
|
+
this.#runtime = runtime
|
|
79
|
+
this.document = document
|
|
80
|
+
this.#operation = operation
|
|
81
|
+
this.#request = request
|
|
82
|
+
this.#options = options
|
|
83
|
+
this.#scope = scope
|
|
84
|
+
this.#requestId = options.requestId
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get transport(): GraphQLTransport {
|
|
88
|
+
return this.#options.transport ?? 'direct'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
get operationType(): GraphQLOperationKind {
|
|
92
|
+
return this.#operation.operation
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
get operationName(): string | undefined {
|
|
96
|
+
return this.#operation.name?.value
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
get requestId(): string {
|
|
100
|
+
let id = this.#requestId
|
|
101
|
+
if (id === undefined) {
|
|
102
|
+
const http = this.http
|
|
103
|
+
const scoped = http === undefined ? this.#scope?.get(HttpTokens.RequestId) : undefined
|
|
104
|
+
id = http?.requestId ?? (typeof scoped === 'string' ? scoped : randomUUID())
|
|
105
|
+
this.#requestId = id
|
|
106
|
+
}
|
|
107
|
+
return id
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get http(): HttpContext | undefined {
|
|
111
|
+
let http = this.#http
|
|
112
|
+
if (http === undefined) {
|
|
113
|
+
// a controller calling plugin.execute(): the HTTP request of the enclosing scope
|
|
114
|
+
http = this.#options.http ?? this.#scope?.get(HttpTokens.Context) ?? null
|
|
115
|
+
this.#http = http
|
|
116
|
+
}
|
|
117
|
+
return http ?? undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get variables(): Readonly<Record<string, unknown>> {
|
|
121
|
+
return this.#request.variables ?? NO_VARIABLES
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get signal(): AbortSignal {
|
|
125
|
+
let signal = this.#signal
|
|
126
|
+
if (signal === undefined) {
|
|
127
|
+
// HttpContext.signal is created on first read: only read it when someone asks
|
|
128
|
+
const base = this.#options.signal ?? this.http?.signal ?? this.#runtime.signal
|
|
129
|
+
const ended = this.#ended
|
|
130
|
+
signal = ended === undefined ? base : AbortSignal.any([base, ended])
|
|
131
|
+
this.#signal = signal
|
|
132
|
+
}
|
|
133
|
+
return signal
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Ties {@link signal} to the end of a subscription: it also aborts when `ended` does (the stream
|
|
138
|
+
* ended by itself or with `return()`). Called before any resolver runs.
|
|
139
|
+
*/
|
|
140
|
+
endsWith(ended: AbortSignal): void {
|
|
141
|
+
this.#ended = ended
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get state(): Map<string | symbol, unknown> {
|
|
145
|
+
this.#state ??= new Map()
|
|
146
|
+
return this.#state
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
loader<L extends BatchLoad<unknown, unknown>>(loader: BatchLoadClass<L>): BatchLoader<LoaderKey<L>, LoaderValue<L>>
|
|
150
|
+
loader(token: Token): BatchLoader<unknown, unknown>
|
|
151
|
+
loader(key: Token | BatchLoadClass): BatchLoader<unknown, unknown> {
|
|
152
|
+
const token = typeof key === 'string' ? key : this.#tokenOf(key)
|
|
153
|
+
let loaders = this.#loaders
|
|
154
|
+
if (loaders === undefined) {
|
|
155
|
+
loaders = new Map()
|
|
156
|
+
this.#loaders = loaders
|
|
157
|
+
}
|
|
158
|
+
let loader = loaders.get(token)
|
|
159
|
+
if (loader === undefined) {
|
|
160
|
+
loader = this.#runtime.loaders.create(token)
|
|
161
|
+
loaders.set(token, loader)
|
|
162
|
+
}
|
|
163
|
+
return loader
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Starts a new execution (a subscription event): loaders and their caches are renewed. */
|
|
167
|
+
resetLoaders(): void {
|
|
168
|
+
this.#loaders = undefined
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#tokenOf(loader: BatchLoadClass): Token {
|
|
172
|
+
const token = typeof loader === 'function' ? this.#runtime.container.tokenOf(loader) : undefined
|
|
173
|
+
if (token === undefined) {
|
|
174
|
+
const name = typeof loader === 'function' ? loader.name || '<anonymous>' : String(loader)
|
|
175
|
+
throw new ZnightError(ErrorCode.MissingProvider, `GraphQLContext.loader(): ${name} is not a provider`, {
|
|
176
|
+
hints: ['Add @Injectable() to the loader class (it implements BatchLoad<K, V>)'],
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
return token
|
|
180
|
+
}
|
|
181
|
+
}
|