@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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1062 -2
  3. package/dist/compiler.d.mts +93 -0
  4. package/dist/compiler.mjs +2832 -0
  5. package/dist/compiler.mjs.map +1 -0
  6. package/dist/index.d.mts +542 -0
  7. package/dist/index.mjs +128 -0
  8. package/dist/index.mjs.map +1 -0
  9. package/dist/names-BlzDM5cF.mjs +111 -0
  10. package/dist/names-BlzDM5cF.mjs.map +1 -0
  11. package/dist/plugin-BdBxr5NQ.mjs +3222 -0
  12. package/dist/plugin-BdBxr5NQ.mjs.map +1 -0
  13. package/dist/testing.d.mts +101 -0
  14. package/dist/testing.mjs +306 -0
  15. package/dist/testing.mjs.map +1 -0
  16. package/dist/types-CgbV9LjD.d.mts +234 -0
  17. package/package.json +95 -4
  18. package/src/compiler/analyze.ts +608 -0
  19. package/src/compiler/builder.ts +1640 -0
  20. package/src/compiler/constants.ts +74 -0
  21. package/src/compiler/data.ts +73 -0
  22. package/src/compiler/defaults.ts +92 -0
  23. package/src/compiler/discriminators.ts +53 -0
  24. package/src/compiler/docs.ts +55 -0
  25. package/src/compiler/generate.ts +405 -0
  26. package/src/compiler/ids.ts +128 -0
  27. package/src/compiler/index.ts +93 -0
  28. package/src/compiler/locate.ts +44 -0
  29. package/src/compiler/model.ts +122 -0
  30. package/src/compiler/options.ts +54 -0
  31. package/src/compiler/scalars.ts +138 -0
  32. package/src/compiler/sdl-view.ts +59 -0
  33. package/src/context.ts +181 -0
  34. package/src/decisions.ts +169 -0
  35. package/src/decorators.ts +196 -0
  36. package/src/errors.ts +89 -0
  37. package/src/execution.ts +1064 -0
  38. package/src/format.ts +285 -0
  39. package/src/index.ts +84 -0
  40. package/src/limits.ts +177 -0
  41. package/src/loaders.ts +149 -0
  42. package/src/memory-pubsub.ts +133 -0
  43. package/src/metadata.ts +195 -0
  44. package/src/names.ts +47 -0
  45. package/src/options.ts +309 -0
  46. package/src/pipeline.ts +277 -0
  47. package/src/plugin.ts +557 -0
  48. package/src/pubsub.ts +36 -0
  49. package/src/scalars.ts +131 -0
  50. package/src/schema-hash.ts +18 -0
  51. package/src/schema.ts +557 -0
  52. package/src/sdl.ts +515 -0
  53. package/src/testing.ts +454 -0
  54. package/src/tokens.ts +22 -0
  55. package/src/transport/abort.ts +80 -0
  56. package/src/transport/accept.ts +155 -0
  57. package/src/transport/graphiql.ts +106 -0
  58. package/src/transport/http.ts +373 -0
  59. package/src/transport/sse.ts +45 -0
  60. package/src/type-ref.ts +89 -0
  61. package/src/types.ts +190 -0
  62. 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
+ }
@@ -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
+ }