@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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MemoryPubSub` (RFC 0005 §5.6, §8.7): the in-process `PubSub` of `GraphQLPlugin`, one RFC 0006
|
|
3
|
+
* `EventChannel` per subscriber.
|
|
4
|
+
*/
|
|
5
|
+
import type { SubscribeOptions } from './pubsub.ts'
|
|
6
|
+
|
|
7
|
+
import { EventChannel } from '@znight/http'
|
|
8
|
+
|
|
9
|
+
import { GraphQLErrorCode, GraphQLException } from './errors.ts'
|
|
10
|
+
import { PubSub } from './pubsub.ts'
|
|
11
|
+
|
|
12
|
+
/** Options of {@link MemoryPubSub}. */
|
|
13
|
+
export interface MemoryPubSubOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Events buffered per subscriber. A subscriber that falls further behind gets a final
|
|
16
|
+
* `SUBSCRIPTION_OVERFLOW` error and its subscription ends. Default 100.
|
|
17
|
+
*/
|
|
18
|
+
readonly bufferSize?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_BUFFER_SIZE = 100
|
|
22
|
+
const PUBLISHED: Promise<void> = Promise.resolve()
|
|
23
|
+
|
|
24
|
+
/** One subscriber: its channel, and whether it overflowed. */
|
|
25
|
+
class Subscriber<T> implements AsyncIterableIterator<T> {
|
|
26
|
+
readonly channel: EventChannel<T>
|
|
27
|
+
readonly #iterator: AsyncIterator<T>
|
|
28
|
+
overflowed = false
|
|
29
|
+
|
|
30
|
+
constructor(channel: EventChannel<T>) {
|
|
31
|
+
this.channel = channel
|
|
32
|
+
this.#iterator = channel[Symbol.asyncIterator]()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async next(): Promise<IteratorResult<T>> {
|
|
36
|
+
const step = await this.#iterator.next()
|
|
37
|
+
if (step.done === true && this.overflowed) {
|
|
38
|
+
this.overflowed = false
|
|
39
|
+
throw new GraphQLException('The subscriber fell behind the events: the subscription ended', {
|
|
40
|
+
code: GraphQLErrorCode.SubscriptionOverflow,
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
return step
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return(): Promise<IteratorResult<T>> {
|
|
47
|
+
this.overflowed = false
|
|
48
|
+
return this.#iterator.return?.() ?? Promise.resolve({ done: true, value: undefined })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[Symbol.asyncIterator](): AsyncIterableIterator<T> {
|
|
52
|
+
return this
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* In-process fan-out of events to subscribers: what `GraphQLPlugin` provides as `PubSub` by default.
|
|
58
|
+
* Deployments with several instances supply their own `PubSub` (`pubsub` option).
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* GraphQLPlugin.configure({ pubsub: new MemoryPubSub({ bufferSize: 500 }) })
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export class MemoryPubSub<Events extends object = Record<string, unknown>> extends PubSub<Events> {
|
|
65
|
+
readonly #bufferSize: number
|
|
66
|
+
readonly #topics = new Map<string, Set<Subscriber<unknown>>>()
|
|
67
|
+
#closed = false
|
|
68
|
+
|
|
69
|
+
constructor(options: MemoryPubSubOptions = {}) {
|
|
70
|
+
super()
|
|
71
|
+
const bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE
|
|
72
|
+
if (!(Number.isSafeInteger(bufferSize) && bufferSize >= 1)) {
|
|
73
|
+
throw new TypeError(`MemoryPubSub: bufferSize must be a positive integer (received ${String(bufferSize)})`)
|
|
74
|
+
}
|
|
75
|
+
this.#bufferSize = bufferSize
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Subscribers of every topic. */
|
|
79
|
+
get subscribers(): number {
|
|
80
|
+
let count = 0
|
|
81
|
+
for (const subscribers of this.#topics.values()) count += subscribers.size
|
|
82
|
+
return count
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
publish<K extends keyof Events & string>(topic: K, payload: Events[K]): Promise<void> {
|
|
86
|
+
const subscribers = this.#topics.get(topic)
|
|
87
|
+
if (subscribers !== undefined) {
|
|
88
|
+
for (const subscriber of subscribers) {
|
|
89
|
+
const channel = subscriber.channel
|
|
90
|
+
// `false` from an open channel is an overflow ('close'): it closes after its buffered events
|
|
91
|
+
if (!channel.closed && !channel.push(payload)) subscriber.overflowed = true
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return PUBLISHED
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
subscribe<K extends keyof Events & string>(
|
|
98
|
+
topic: K,
|
|
99
|
+
options: SubscribeOptions = {},
|
|
100
|
+
): AsyncIterableIterator<Events[K]> {
|
|
101
|
+
const channel = new EventChannel<Events[K]>({
|
|
102
|
+
capacity: this.#bufferSize,
|
|
103
|
+
overflow: 'close',
|
|
104
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
105
|
+
})
|
|
106
|
+
const subscriber = new Subscriber(channel)
|
|
107
|
+
if (this.#closed) {
|
|
108
|
+
channel.close()
|
|
109
|
+
return subscriber
|
|
110
|
+
}
|
|
111
|
+
let subscribers = this.#topics.get(topic)
|
|
112
|
+
if (subscribers === undefined) {
|
|
113
|
+
subscribers = new Set()
|
|
114
|
+
this.#topics.set(topic, subscribers)
|
|
115
|
+
}
|
|
116
|
+
const set = subscribers
|
|
117
|
+
set.add(subscriber as Subscriber<unknown>)
|
|
118
|
+
channel.onClose(() => {
|
|
119
|
+
set.delete(subscriber as Subscriber<unknown>)
|
|
120
|
+
if (set.size === 0 && this.#topics.get(topic) === set) this.#topics.delete(topic)
|
|
121
|
+
})
|
|
122
|
+
return subscriber
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Ends every subscription; later subscriptions end at once. */
|
|
126
|
+
close(): void {
|
|
127
|
+
this.#closed = true
|
|
128
|
+
for (const subscribers of [...this.#topics.values()]) {
|
|
129
|
+
for (const subscriber of [...subscribers]) subscriber.channel.close()
|
|
130
|
+
}
|
|
131
|
+
this.#topics.clear()
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/metadata.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL section of the generated metadata (`__ZNIGHT_METADATA__.extensions.graphql`, RFC 0005
|
|
3
|
+
* §7), produced by the `@znight/graphql/compiler` extension and consumed by `GraphQLPlugin`, which
|
|
4
|
+
* validates it at boot (ZNIGHT066), because metadata can be hand-written.
|
|
5
|
+
*
|
|
6
|
+
* Data, not code, except `invoke` (one arrow per resolver) and the scalar implementations imported
|
|
7
|
+
* from user code. The SDL printer (`sdl.ts`) reads the same shape without its functions.
|
|
8
|
+
*
|
|
9
|
+
* Import-free: types, type-only imports and one constant, shared by the compiler extension and the
|
|
10
|
+
* runtime.
|
|
11
|
+
*/
|
|
12
|
+
import type { Token } from '@znight/core'
|
|
13
|
+
import type { HttpRouteGroupDefinition } from '@znight/http'
|
|
14
|
+
import type { GraphQLResolveInfo, GraphQLScalarType } from 'graphql'
|
|
15
|
+
import type { GraphQLArgs, GraphQLContext, ScalarDefinition } from './types.ts'
|
|
16
|
+
|
|
17
|
+
/** Format version of `metadata.extensions.graphql` (another version is ZNIGHT066 at boot). */
|
|
18
|
+
export const GRAPHQL_METADATA_VERSION = 1
|
|
19
|
+
|
|
20
|
+
/** The schema of the application. The section is omitted when the application has no resolver. */
|
|
21
|
+
export interface GraphQLMetadata {
|
|
22
|
+
/** {@link GRAPHQL_METADATA_VERSION}. */
|
|
23
|
+
readonly version: number
|
|
24
|
+
/** First 16 hexadecimal characters of the SHA-256 of the SDL (`printSdl`): logs, caches. */
|
|
25
|
+
readonly schemaHash: string
|
|
26
|
+
readonly roots: GraphQLRootTypes
|
|
27
|
+
/** Every named type except the graphql-js specified scalars, sorted by name (code-unit order). */
|
|
28
|
+
readonly types: readonly GraphQLTypeDefinition[]
|
|
29
|
+
/**
|
|
30
|
+
* Route decorator groups used on resolvers that must be known at runtime (required, or holding
|
|
31
|
+
* several keys), by group name: the shape of `extensions.http.groups` (RFC 0001).
|
|
32
|
+
*/
|
|
33
|
+
readonly groups?: Readonly<Record<string, HttpRouteGroupDefinition>>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Names of the root operation types (`Query`, `Mutation`, `Subscription`). */
|
|
37
|
+
export interface GraphQLRootTypes {
|
|
38
|
+
readonly query: string
|
|
39
|
+
readonly mutation?: string
|
|
40
|
+
readonly subscription?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A named type of the schema. */
|
|
44
|
+
export type GraphQLTypeDefinition =
|
|
45
|
+
| GraphQLObjectTypeDefinition
|
|
46
|
+
| GraphQLInputTypeDefinition
|
|
47
|
+
| GraphQLEnumTypeDefinition
|
|
48
|
+
| GraphQLUnionTypeDefinition
|
|
49
|
+
| GraphQLBuiltinScalarDefinition
|
|
50
|
+
| GraphQLCustomScalarDefinition
|
|
51
|
+
|
|
52
|
+
/** An object type: resolver return types, their fields, the root types. */
|
|
53
|
+
export interface GraphQLObjectTypeDefinition {
|
|
54
|
+
readonly kind: 'object'
|
|
55
|
+
readonly name: string
|
|
56
|
+
readonly description?: string
|
|
57
|
+
/** Declaration order (base members first), then field resolvers by resolver token and source order. */
|
|
58
|
+
readonly fields: readonly GraphQLFieldDefinition[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** An input object type. */
|
|
62
|
+
export interface GraphQLInputTypeDefinition {
|
|
63
|
+
readonly kind: 'input'
|
|
64
|
+
readonly name: string
|
|
65
|
+
readonly description?: string
|
|
66
|
+
readonly fields: readonly GraphQLInputFieldDefinition[]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An enum: a TypeScript `enum`, or a named alias of a union of string literals. */
|
|
70
|
+
export interface GraphQLEnumTypeDefinition {
|
|
71
|
+
readonly kind: 'enum'
|
|
72
|
+
readonly name: string
|
|
73
|
+
readonly description?: string
|
|
74
|
+
/** Declaration order. */
|
|
75
|
+
readonly values: readonly GraphQLEnumValueDefinition[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A union of object types. */
|
|
79
|
+
export interface GraphQLUnionTypeDefinition {
|
|
80
|
+
readonly kind: 'union'
|
|
81
|
+
readonly name: string
|
|
82
|
+
readonly description?: string
|
|
83
|
+
/** Member type names, sorted. */
|
|
84
|
+
readonly members: readonly string[]
|
|
85
|
+
readonly discriminator: GraphQLDiscriminator
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A scalar implemented by `@znight/graphql`: `DateTime`, `BigInt` or `JSON` (`names.ts`). */
|
|
89
|
+
export interface GraphQLBuiltinScalarDefinition {
|
|
90
|
+
readonly kind: 'scalar'
|
|
91
|
+
readonly name: string
|
|
92
|
+
readonly description?: string
|
|
93
|
+
readonly builtin: true
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A scalar declared with `scalar<T>(name, definition)`: the definition is imported from user code. */
|
|
97
|
+
export interface GraphQLCustomScalarDefinition {
|
|
98
|
+
readonly kind: 'scalar'
|
|
99
|
+
readonly name: string
|
|
100
|
+
/** `ScalarDefinition<T>` of any `T` (its methods are bivariant), or a graphql-js scalar named `name`. */
|
|
101
|
+
readonly scalar: ScalarDefinition<unknown> | GraphQLScalarType
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A field of an object type. */
|
|
105
|
+
export interface GraphQLFieldDefinition {
|
|
106
|
+
readonly name: string
|
|
107
|
+
/** Type reference (`'Book!'`, `'[Book!]!'`, `'Int'`), parsed by `type-ref.ts`. */
|
|
108
|
+
readonly type: string
|
|
109
|
+
readonly description?: string
|
|
110
|
+
readonly deprecationReason?: string
|
|
111
|
+
readonly args?: readonly GraphQLArgumentDefinition[]
|
|
112
|
+
/** Absent: the graphql-js default resolver (`parent[name]`). */
|
|
113
|
+
readonly resolver?: GraphQLResolverBinding
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** An argument of a field. */
|
|
117
|
+
export interface GraphQLArgumentDefinition {
|
|
118
|
+
readonly name: string
|
|
119
|
+
readonly type: string
|
|
120
|
+
readonly description?: string
|
|
121
|
+
readonly deprecationReason?: string
|
|
122
|
+
/**
|
|
123
|
+
* Default value, as an external value: JSON-compatible, enum values by GraphQL name (passed to
|
|
124
|
+
* graphql-js as `default: { value }`). Absent: no default.
|
|
125
|
+
*/
|
|
126
|
+
readonly defaultValue?: unknown
|
|
127
|
+
/** The TypeScript type does not accept `null`: a `null` sent by the client becomes `undefined` (§5.4). */
|
|
128
|
+
readonly nullAsUndefined?: true
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** A field of an input object type. */
|
|
132
|
+
export interface GraphQLInputFieldDefinition extends GraphQLArgumentDefinition {}
|
|
133
|
+
|
|
134
|
+
/** A value of an enum. */
|
|
135
|
+
export interface GraphQLEnumValueDefinition {
|
|
136
|
+
/** GraphQL name (the member name, or the literal). */
|
|
137
|
+
readonly name: string
|
|
138
|
+
/** Internal value (the member value, or the literal), what resolvers receive and return. */
|
|
139
|
+
readonly value: string | number
|
|
140
|
+
readonly description?: string
|
|
141
|
+
readonly deprecationReason?: string
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** How the runtime tells the members of a union apart (`resolveType`). */
|
|
145
|
+
export type GraphQLDiscriminator =
|
|
146
|
+
| {
|
|
147
|
+
readonly by: 'property'
|
|
148
|
+
readonly property: string
|
|
149
|
+
/** Literal value of the property (as a string) → member type name. */
|
|
150
|
+
readonly values: Readonly<Record<string, string>>
|
|
151
|
+
}
|
|
152
|
+
| {
|
|
153
|
+
readonly by: 'class'
|
|
154
|
+
/** Member classes and their type names, subclasses first (`instanceof` order). */
|
|
155
|
+
readonly classes: readonly (readonly [cls: abstract new (...args: never[]) => unknown, name: string])[]
|
|
156
|
+
}
|
|
157
|
+
| { readonly by: 'typename' }
|
|
158
|
+
|
|
159
|
+
/** Kind of field a resolver method provides. */
|
|
160
|
+
export type GraphQLResolverOperation = 'query' | 'mutation' | 'subscription' | 'field'
|
|
161
|
+
|
|
162
|
+
/** The resolver method behind a field. */
|
|
163
|
+
export interface GraphQLResolverBinding {
|
|
164
|
+
/** Provider token of the resolver class. */
|
|
165
|
+
readonly token: Token
|
|
166
|
+
readonly method: string
|
|
167
|
+
readonly operation: GraphQLResolverOperation
|
|
168
|
+
/** Guard provider tokens: class `@UseGuard` guards, then method ones. */
|
|
169
|
+
readonly guards: readonly Token[]
|
|
170
|
+
/**
|
|
171
|
+
* Effective field metadata (`createRouteDecorator` values, method decisions over class ones, one
|
|
172
|
+
* decision per group). Empty: no metadata, so global guards skip a field resolver (§5.9).
|
|
173
|
+
*/
|
|
174
|
+
readonly meta: Readonly<Record<string, unknown>>
|
|
175
|
+
/** Informational: loader provider tokens the method injects. */
|
|
176
|
+
readonly loaders?: readonly Token[]
|
|
177
|
+
/**
|
|
178
|
+
* Calls the method: maps the graphql-js arguments to the method parameters (§7.3). Synchronous
|
|
179
|
+
* unless an argument is validated with a Standard Schema.
|
|
180
|
+
*/
|
|
181
|
+
readonly invoke: (
|
|
182
|
+
// biome-ignore lint/suspicious/noExplicitAny: generated invokers receive the concrete resolver class
|
|
183
|
+
resolver: any,
|
|
184
|
+
parent: unknown,
|
|
185
|
+
args: GraphQLArgs,
|
|
186
|
+
context: GraphQLContext,
|
|
187
|
+
info: GraphQLResolveInfo,
|
|
188
|
+
) => unknown
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
declare module '@znight/core' {
|
|
192
|
+
interface ZnightExtensionsMetadata {
|
|
193
|
+
readonly graphql?: GraphQLMetadata
|
|
194
|
+
}
|
|
195
|
+
}
|
package/src/names.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GraphQL naming rules and built-in types (RFC 0005 §5.7, §6.4).
|
|
3
|
+
*
|
|
4
|
+
* Import-free: shared by the compiler extension (naming diagnostics, ZNIGHT061) and the runtime
|
|
5
|
+
* (structural validation of `metadata.extensions.graphql` and the built-in scalars, ZNIGHT066).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Grammar of a GraphQL name (types, fields, arguments, enum values). */
|
|
9
|
+
export const GRAPHQL_NAME: RegExp = /^[_A-Za-z][_0-9A-Za-z]*$/
|
|
10
|
+
|
|
11
|
+
/** `true` when `name` is a GraphQL name that user types may take (not reserved with `__`). */
|
|
12
|
+
export function isGraphQLName(name: string): boolean {
|
|
13
|
+
return GRAPHQL_NAME.test(name) && !name.startsWith('__')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Names an enum value may not take (GraphQL spec §3.9). */
|
|
17
|
+
export const RESERVED_ENUM_VALUES: ReadonlySet<string> = new Set(['true', 'false', 'null'])
|
|
18
|
+
|
|
19
|
+
/** Names of the root operation types; user types may not take them (ZNIGHT061). */
|
|
20
|
+
export const ROOT_TYPE_NAMES: {
|
|
21
|
+
readonly query: 'Query'
|
|
22
|
+
readonly mutation: 'Mutation'
|
|
23
|
+
readonly subscription: 'Subscription'
|
|
24
|
+
} = Object.freeze({ query: 'Query', mutation: 'Mutation', subscription: 'Subscription' })
|
|
25
|
+
|
|
26
|
+
/** Scalars of the GraphQL specification: always present, never declared in the metadata nor printed. */
|
|
27
|
+
export const SPECIFIED_SCALARS: ReadonlySet<string> = new Set(['String', 'Int', 'Float', 'Boolean', 'ID'])
|
|
28
|
+
|
|
29
|
+
/** A scalar `@znight/graphql` implements itself (`kind: 'scalar', builtin: true` in the metadata). */
|
|
30
|
+
export interface BuiltinScalarInfo {
|
|
31
|
+
/** `@specifiedBy(url:)`. Absent: none. */
|
|
32
|
+
readonly specifiedByURL?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The scalars `@znight/graphql` implements (§5.7): `DateTime` (`Date`), `BigInt` (`bigint`) and
|
|
37
|
+
* `JSON` (`JSONValue`). They have no description. The runtime builds them with exactly these
|
|
38
|
+
* properties and without `valueToLiteral`, so that graphql-js `printSchema` and `printSdl()` agree.
|
|
39
|
+
*/
|
|
40
|
+
export const BUILTIN_SCALARS: ReadonlyMap<string, BuiltinScalarInfo> = new Map<string, BuiltinScalarInfo>([
|
|
41
|
+
['BigInt', {}],
|
|
42
|
+
['DateTime', { specifiedByURL: 'https://datatracker.ietf.org/doc/html/rfc3339' }],
|
|
43
|
+
['JSON', {}],
|
|
44
|
+
])
|
|
45
|
+
|
|
46
|
+
/** The default `@deprecated` reason of graphql-js: printed as a bare `@deprecated`. */
|
|
47
|
+
export const DEFAULT_DEPRECATION_REASON = 'No longer supported'
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options of `GraphQLPlugin` (RFC 0005 §5.11): their types, defaults and validation (ZNIGHT066).
|
|
3
|
+
*/
|
|
4
|
+
import type { ByteSize, GuardRef, RouteMetaEntry } from '@znight/http'
|
|
5
|
+
import type { SubscribeOptions } from './pubsub.ts'
|
|
6
|
+
import type { GraphQLErrorMapper, GraphQLErrorMapperClass, GraphQLGuard, GraphQLGuardClass } from './types.ts'
|
|
7
|
+
|
|
8
|
+
import { ErrorCode, ZnightError } from '@znight/core'
|
|
9
|
+
import { parseByteSize } from '@znight/http'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A `PubSub` of any event map, for the `pubsub` option: `PubSub<Events>` types are not assignable
|
|
13
|
+
* to each other (generic methods over `keyof Events`), so the option accepts the structure.
|
|
14
|
+
*/
|
|
15
|
+
export interface AnyPubSub {
|
|
16
|
+
publish(topic: never, payload: never): Promise<void>
|
|
17
|
+
subscribe(topic: never, options?: SubscribeOptions): AsyncIterableIterator<unknown>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A global GraphQL guard: a provider class or an instance. */
|
|
21
|
+
export type GraphQLGuardRef = GraphQLGuardClass | GraphQLGuard
|
|
22
|
+
|
|
23
|
+
/** An error mapper: a provider class or an instance. */
|
|
24
|
+
export type GraphQLErrorMapperRef = GraphQLErrorMapperClass | GraphQLErrorMapper
|
|
25
|
+
|
|
26
|
+
/** Options of the GraphQL-over-HTTP endpoint. */
|
|
27
|
+
export interface GraphQLHttpOptions {
|
|
28
|
+
/** Route metadata of the endpoint (`[Public.meta()]`), seen by HTTP guards and the route decision check. */
|
|
29
|
+
readonly meta?: readonly RouteMetaEntry[]
|
|
30
|
+
/** Guards of the endpoint (HTTP `CanActivate`), after the global `HttpPlugin` guards. */
|
|
31
|
+
readonly guards?: readonly GuardRef[]
|
|
32
|
+
/** Body limit of the endpoint. Default `'100kb'`. */
|
|
33
|
+
readonly bodyLimit?: ByteSize
|
|
34
|
+
/** Queries over `GET`. Default `true`. */
|
|
35
|
+
readonly get?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Request limits; `false` disables one. */
|
|
39
|
+
export interface GraphQLLimits {
|
|
40
|
+
/** Tokens of a document (`parse`). Default 2 000. */
|
|
41
|
+
readonly maxTokens?: number | false
|
|
42
|
+
/** Field nesting of an operation (introspection fields exempt). Default 12. */
|
|
43
|
+
readonly maxDepth?: number | false
|
|
44
|
+
/**
|
|
45
|
+
* Aliases of a document, as written and as one operation executes them (a fragment spread several
|
|
46
|
+
* times counts each time). Default 30.
|
|
47
|
+
*/
|
|
48
|
+
readonly maxAliases?: number | false
|
|
49
|
+
/** Directives of a document. Default 50. */
|
|
50
|
+
readonly maxDirectives?: number | false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Subscription options. */
|
|
54
|
+
export interface GraphQLSubscriptionOptions {
|
|
55
|
+
/** Active subscriptions cap (every transport); beyond it, 503. Default 10 000. */
|
|
56
|
+
readonly maxActive?: number
|
|
57
|
+
/** Events buffered per subscriber by the default `MemoryPubSub`. Default 100. */
|
|
58
|
+
readonly bufferSize?: number
|
|
59
|
+
/** SSE heartbeat (ms), `false` for none. Default: the `HttpPlugin` `sse` option. */
|
|
60
|
+
readonly heartbeat?: number | false
|
|
61
|
+
/** SSE maximum duration (ms). Default: none. */
|
|
62
|
+
readonly maxDuration?: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Options of `GraphQLPlugin.configure()`. */
|
|
66
|
+
export interface GraphQLPluginOptions {
|
|
67
|
+
/** Path of the endpoint, joined with the `HttpPlugin` prefix. Default `'/graphql'`. */
|
|
68
|
+
readonly path?: string
|
|
69
|
+
/** `false`: no HTTP transport (schema, `execute()` and `subscribe()` only). Default `{}`. */
|
|
70
|
+
readonly http?: GraphQLHttpOptions | false
|
|
71
|
+
/** Global GraphQL guards (classes or instances): root fields, and field resolvers with metadata. */
|
|
72
|
+
readonly guards?: readonly GraphQLGuardRef[]
|
|
73
|
+
/** Error mappers (classes or instances), tried in order. */
|
|
74
|
+
readonly errorMappers?: readonly GraphQLErrorMapperRef[]
|
|
75
|
+
/** Unmasked messages for unexpected errors (development). Default `false`. */
|
|
76
|
+
readonly exposeErrors?: boolean
|
|
77
|
+
/** Introspection; off, suggestions are hidden too. Default `NODE_ENV !== 'production'`. */
|
|
78
|
+
readonly introspection?: boolean
|
|
79
|
+
/** GraphiQL on `GET path` with `accept: text/html`. Default `NODE_ENV === 'development'`. */
|
|
80
|
+
readonly graphiql?: boolean
|
|
81
|
+
readonly limits?: GraphQLLimits
|
|
82
|
+
/** Distinct documents kept parsed and validated (LRU); 0 disables the cache. Default 1 000. */
|
|
83
|
+
readonly documentCache?: number
|
|
84
|
+
/** `false` disables subscriptions over SSE. */
|
|
85
|
+
readonly subscriptions?: GraphQLSubscriptionOptions | false
|
|
86
|
+
/** The `PubSub` provided to the container. Default: a `MemoryPubSub` (closed with the application). */
|
|
87
|
+
readonly pubsub?: AnyPubSub
|
|
88
|
+
/**
|
|
89
|
+
* How the boot check of route decisions on GraphQL fields (RFC 0001, ZNIGHT023) reports problems:
|
|
90
|
+
* `'error'` (default) fails the boot, `'warn'` logs.
|
|
91
|
+
*/
|
|
92
|
+
readonly enforcement?: 'error' | 'warn'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Options with their defaults, validated. */
|
|
96
|
+
export interface ResolvedGraphQLOptions {
|
|
97
|
+
readonly path: string
|
|
98
|
+
readonly http: Required<GraphQLHttpOptions> | false
|
|
99
|
+
readonly guards: readonly GraphQLGuardRef[]
|
|
100
|
+
readonly errorMappers: readonly GraphQLErrorMapperRef[]
|
|
101
|
+
readonly exposeErrors: boolean
|
|
102
|
+
readonly introspection: boolean
|
|
103
|
+
readonly graphiql: boolean
|
|
104
|
+
readonly limits: {
|
|
105
|
+
readonly maxTokens: number | undefined
|
|
106
|
+
readonly maxDepth: number | undefined
|
|
107
|
+
readonly maxAliases: number | undefined
|
|
108
|
+
readonly maxDirectives: number | undefined
|
|
109
|
+
}
|
|
110
|
+
readonly documentCache: number
|
|
111
|
+
readonly subscriptions:
|
|
112
|
+
| {
|
|
113
|
+
readonly maxActive: number
|
|
114
|
+
readonly bufferSize: number
|
|
115
|
+
readonly heartbeat: number | false | undefined
|
|
116
|
+
readonly maxDuration: number | undefined
|
|
117
|
+
}
|
|
118
|
+
| false
|
|
119
|
+
readonly bufferSize: number
|
|
120
|
+
readonly pubsub: AnyPubSub | undefined
|
|
121
|
+
readonly enforcement: 'error' | 'warn'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const KEYS: ReadonlySet<string> = new Set([
|
|
125
|
+
'path',
|
|
126
|
+
'http',
|
|
127
|
+
'guards',
|
|
128
|
+
'errorMappers',
|
|
129
|
+
'exposeErrors',
|
|
130
|
+
'introspection',
|
|
131
|
+
'graphiql',
|
|
132
|
+
'limits',
|
|
133
|
+
'documentCache',
|
|
134
|
+
'subscriptions',
|
|
135
|
+
'pubsub',
|
|
136
|
+
'enforcement',
|
|
137
|
+
])
|
|
138
|
+
const PATH = /^\/[A-Za-z0-9\-._~!$&'()*+,;=:@/]*$/
|
|
139
|
+
const LIMIT_DEFAULTS = { maxTokens: 2000, maxDepth: 12, maxAliases: 30, maxDirectives: 50 } as const
|
|
140
|
+
|
|
141
|
+
function show(value: unknown): string {
|
|
142
|
+
if (typeof value === 'function') return 'a function'
|
|
143
|
+
try {
|
|
144
|
+
return JSON.stringify(value) ?? String(value)
|
|
145
|
+
} catch {
|
|
146
|
+
return typeof value
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** ZNIGHT066 for an invalid option. */
|
|
151
|
+
export function invalidOption(name: string, expected: string, received: unknown): ZnightError {
|
|
152
|
+
return new ZnightError(
|
|
153
|
+
ErrorCode.InvalidGraphQLSetup,
|
|
154
|
+
`GraphQLPlugin: invalid option ${name}: expected ${expected} (received ${show(received)})`,
|
|
155
|
+
{ hints: ['See the options of GraphQLPlugin.configure() (RFC 0005 §5.11)'] },
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
160
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function boolean(name: string, value: unknown, fallback: boolean): boolean {
|
|
164
|
+
if (value === undefined) return fallback
|
|
165
|
+
if (typeof value !== 'boolean') throw invalidOption(name, 'a boolean', value)
|
|
166
|
+
return value
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function positiveInteger(name: string, value: unknown, fallback: number): number {
|
|
170
|
+
if (value === undefined) return fallback
|
|
171
|
+
if (!(Number.isSafeInteger(value) && (value as number) >= 1)) throw invalidOption(name, 'a positive integer', value)
|
|
172
|
+
return value as number
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function limit(name: keyof typeof LIMIT_DEFAULTS, value: unknown): number | undefined {
|
|
176
|
+
if (value === false) return undefined
|
|
177
|
+
return positiveInteger(`limits.${name}`, value, LIMIT_DEFAULTS[name])
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Refs given as classes or instances with `method`. */
|
|
181
|
+
function refs<T>(name: string, value: unknown, method: string): readonly T[] {
|
|
182
|
+
if (value === undefined) return []
|
|
183
|
+
if (!Array.isArray(value))
|
|
184
|
+
throw invalidOption(name, `an array of classes or objects with a ${method}() method`, value)
|
|
185
|
+
for (const ref of value) {
|
|
186
|
+
const ok =
|
|
187
|
+
typeof ref === 'function' ||
|
|
188
|
+
(ref !== null && typeof ref === 'object' && typeof (ref as Record<string, unknown>)[method] === 'function')
|
|
189
|
+
if (!ok) throw invalidOption(name, `classes or objects with a ${method}() method`, ref)
|
|
190
|
+
}
|
|
191
|
+
return value as readonly T[]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Validates the options and applies the defaults (`env`: `process.env`). */
|
|
195
|
+
export function resolveGraphQLOptions(
|
|
196
|
+
options: unknown,
|
|
197
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
198
|
+
): ResolvedGraphQLOptions {
|
|
199
|
+
if (!isRecord(options)) throw invalidOption('options', 'an object', options)
|
|
200
|
+
for (const key of Object.keys(options)) {
|
|
201
|
+
if (!KEYS.has(key)) throw invalidOption(key, `one of ${[...KEYS].join(', ')}`, key)
|
|
202
|
+
}
|
|
203
|
+
const nodeEnv = env['NODE_ENV']
|
|
204
|
+
const path = options['path'] ?? '/graphql'
|
|
205
|
+
if (typeof path !== 'string' || !PATH.test(path)) throw invalidOption('path', "a path starting with '/'", path)
|
|
206
|
+
|
|
207
|
+
const rawHttp = options['http'] ?? {}
|
|
208
|
+
let http: ResolvedGraphQLOptions['http']
|
|
209
|
+
if (rawHttp === false) {
|
|
210
|
+
http = false
|
|
211
|
+
} else {
|
|
212
|
+
if (!isRecord(rawHttp)) throw invalidOption('http', 'false or { meta?, guards?, bodyLimit?, get? }', rawHttp)
|
|
213
|
+
const meta = rawHttp['meta'] ?? []
|
|
214
|
+
if (
|
|
215
|
+
!Array.isArray(meta) ||
|
|
216
|
+
!meta.every(entry => isRecord(entry) && typeof entry['key'] === 'string' && entry['key'] !== '')
|
|
217
|
+
) {
|
|
218
|
+
throw invalidOption('http.meta', 'an array of route metadata entries (Decorator.meta())', meta)
|
|
219
|
+
}
|
|
220
|
+
const bodyLimit = rawHttp['bodyLimit'] ?? '100kb'
|
|
221
|
+
if (parseByteSize(bodyLimit) === undefined)
|
|
222
|
+
throw invalidOption('http.bodyLimit', "a byte size ('100kb', 1048576)", bodyLimit)
|
|
223
|
+
http = {
|
|
224
|
+
meta: meta as readonly RouteMetaEntry[],
|
|
225
|
+
guards: refs<GuardRef>('http.guards', rawHttp['guards'], 'canActivate'),
|
|
226
|
+
bodyLimit: bodyLimit as ByteSize,
|
|
227
|
+
get: boolean('http.get', rawHttp['get'], true),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const rawLimits = options['limits'] ?? {}
|
|
232
|
+
if (!isRecord(rawLimits))
|
|
233
|
+
throw invalidOption('limits', '{ maxTokens?, maxDepth?, maxAliases?, maxDirectives? }', rawLimits)
|
|
234
|
+
for (const key of Object.keys(rawLimits)) {
|
|
235
|
+
if (!(key in LIMIT_DEFAULTS))
|
|
236
|
+
throw invalidOption(`limits.${key}`, 'maxTokens, maxDepth, maxAliases or maxDirectives', key)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const documentCache = options['documentCache'] ?? 1000
|
|
240
|
+
if (!(Number.isSafeInteger(documentCache) && (documentCache as number) >= 0)) {
|
|
241
|
+
throw invalidOption('documentCache', 'a non-negative integer', documentCache)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const rawSubscriptions = options['subscriptions'] ?? {}
|
|
245
|
+
let subscriptions: ResolvedGraphQLOptions['subscriptions']
|
|
246
|
+
let bufferSize = 100
|
|
247
|
+
if (rawSubscriptions === false) {
|
|
248
|
+
subscriptions = false
|
|
249
|
+
} else {
|
|
250
|
+
if (!isRecord(rawSubscriptions)) {
|
|
251
|
+
throw invalidOption(
|
|
252
|
+
'subscriptions',
|
|
253
|
+
'false or { maxActive?, bufferSize?, heartbeat?, maxDuration? }',
|
|
254
|
+
rawSubscriptions,
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
bufferSize = positiveInteger('subscriptions.bufferSize', rawSubscriptions['bufferSize'], 100)
|
|
258
|
+
const heartbeat = rawSubscriptions['heartbeat']
|
|
259
|
+
if (heartbeat !== undefined && heartbeat !== false && !(typeof heartbeat === 'number' && heartbeat > 0)) {
|
|
260
|
+
throw invalidOption('subscriptions.heartbeat', 'a positive number of milliseconds or false', heartbeat)
|
|
261
|
+
}
|
|
262
|
+
const maxDuration = rawSubscriptions['maxDuration']
|
|
263
|
+
if (maxDuration !== undefined && !(typeof maxDuration === 'number' && maxDuration > 0)) {
|
|
264
|
+
throw invalidOption('subscriptions.maxDuration', 'a positive number of milliseconds', maxDuration)
|
|
265
|
+
}
|
|
266
|
+
subscriptions = {
|
|
267
|
+
maxActive: positiveInteger('subscriptions.maxActive', rawSubscriptions['maxActive'], 10_000),
|
|
268
|
+
bufferSize,
|
|
269
|
+
heartbeat: heartbeat as number | false | undefined,
|
|
270
|
+
maxDuration: maxDuration as number | undefined,
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const pubsub = options['pubsub']
|
|
275
|
+
if (
|
|
276
|
+
pubsub !== undefined &&
|
|
277
|
+
!(
|
|
278
|
+
isRecord(pubsub) &&
|
|
279
|
+
typeof (pubsub as { publish?: unknown }).publish === 'function' &&
|
|
280
|
+
typeof (pubsub as { subscribe?: unknown }).subscribe === 'function'
|
|
281
|
+
)
|
|
282
|
+
) {
|
|
283
|
+
throw invalidOption('pubsub', 'a PubSub (publish() and subscribe() methods)', pubsub)
|
|
284
|
+
}
|
|
285
|
+
const enforcement = options['enforcement'] ?? 'error'
|
|
286
|
+
if (enforcement !== 'error' && enforcement !== 'warn')
|
|
287
|
+
throw invalidOption('enforcement', "'error' or 'warn'", enforcement)
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
path,
|
|
291
|
+
http,
|
|
292
|
+
guards: refs<GraphQLGuardRef>('guards', options['guards'], 'canResolve'),
|
|
293
|
+
errorMappers: refs<GraphQLErrorMapperRef>('errorMappers', options['errorMappers'], 'map'),
|
|
294
|
+
exposeErrors: boolean('exposeErrors', options['exposeErrors'], false),
|
|
295
|
+
introspection: boolean('introspection', options['introspection'], nodeEnv !== 'production'),
|
|
296
|
+
graphiql: boolean('graphiql', options['graphiql'], nodeEnv === 'development'),
|
|
297
|
+
limits: {
|
|
298
|
+
maxTokens: limit('maxTokens', rawLimits['maxTokens']),
|
|
299
|
+
maxDepth: limit('maxDepth', rawLimits['maxDepth']),
|
|
300
|
+
maxAliases: limit('maxAliases', rawLimits['maxAliases']),
|
|
301
|
+
maxDirectives: limit('maxDirectives', rawLimits['maxDirectives']),
|
|
302
|
+
},
|
|
303
|
+
documentCache: documentCache as number,
|
|
304
|
+
subscriptions,
|
|
305
|
+
bufferSize,
|
|
306
|
+
pubsub: pubsub as AnyPubSub | undefined,
|
|
307
|
+
enforcement,
|
|
308
|
+
}
|
|
309
|
+
}
|