@znight/graphql 0.2.1 → 0.3.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/src/context.ts CHANGED
@@ -44,6 +44,10 @@ export interface ContextOptions {
44
44
  readonly http?: HttpContext
45
45
  readonly signal?: AbortSignal
46
46
  readonly requestId?: string
47
+ /** The driver that received the operation (RFC 0009); `'builtin'` when absent. */
48
+ readonly driver?: string
49
+ /** The driver's context (`ExecutionArgs.contextValue` given to the engine). */
50
+ readonly serverContext?: unknown
47
51
  }
48
52
 
49
53
  const NO_VARIABLES: Readonly<Record<string, unknown>> = Object.freeze({})
@@ -88,6 +92,15 @@ export class OperationContext implements GraphQLContext {
88
92
  return this.#options.transport ?? 'direct'
89
93
  }
90
94
 
95
+ /** The driver that received the operation: `'builtin'` for the built-in transport and direct calls. */
96
+ get driver(): string {
97
+ return this.#options.driver ?? 'builtin'
98
+ }
99
+
100
+ get serverContext(): unknown {
101
+ return this.#options.serverContext
102
+ }
103
+
91
104
  get operationType(): GraphQLOperationKind {
92
105
  return this.#operation.operation
93
106
  }
package/src/driver.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The GraphQL driver contract (RFC 0009 §4.3): a GraphQL server (GraphQL Yoga, Apollo Server, a
3
+ * third party's) serving the schema of `GraphQLPlugin` on top of its {@link GraphQLEngine}. Znight
4
+ * owns the schema, the execution and the lifecycle; the driver owns the protocol, its plugins and
5
+ * its subscription transport. Types only.
6
+ */
7
+ import type { Container, CoreLogger } from '@znight/core'
8
+ import type { HttpPlugin, PluginRoute } from '@znight/http'
9
+ import type { GraphQLEngine } from './engine.ts'
10
+
11
+ /** What {@link GraphQLDriver.routes} receives. */
12
+ export interface GraphQLDriverRoutesContext {
13
+ /** The `path` option: the endpoint path, relative to the `HttpPlugin` prefix (`/graphql`). */
14
+ readonly path: string
15
+ /** The absolute endpoint path, the prefix included (`/api/graphql`). */
16
+ readonly endpoint: string
17
+ }
18
+
19
+ /**
20
+ * A route of a driver: a plugin route of `HttpPlugin` without `meta`, `guards` and `bodyLimit`,
21
+ * which `GraphQLPlugin` adds from its `http` option (`bodyLimit` on the routes that are not `GET`).
22
+ */
23
+ export type GraphQLDriverRoute = Pick<PluginRoute, 'method' | 'path' | 'name' | 'handler'>
24
+
25
+ /** What {@link GraphQLDriver.start} receives besides the engine. */
26
+ export interface GraphQLDriverContext {
27
+ /** The application logger. */
28
+ readonly logger: CoreLogger
29
+ readonly container: Container
30
+ /** The `HttpPlugin` of the application, when registered (its adapter, for server-mounted drivers). */
31
+ readonly http: HttpPlugin | undefined
32
+ }
33
+
34
+ /**
35
+ * A GraphQL driver, given to `GraphQLPlugin.configure({ driver })`. A route-mounted driver returns
36
+ * its routes, which run in `HttpPlugin`'s pipeline (request scope, hooks, guards, CORS, drain) and
37
+ * call the engine from there; a server-mounted one has no routes and attaches itself to the
38
+ * adapter in `start()`. One driver serves one application at a time.
39
+ */
40
+ export interface GraphQLDriver {
41
+ /**
42
+ * The driver's name (`yoga`, `apollo`...): `GraphQLOperationEvent.driver`, logs and errors.
43
+ * `builtin` is reserved for the built-in transport.
44
+ */
45
+ readonly name: string
46
+ /**
47
+ * The routes `HttpPlugin` serves for the driver, with the `http` option's meta, guards and body
48
+ * limit applied by the plugin. Called once per application, possibly before {@link start}: the
49
+ * handlers MUST NOT use the engine before the first request. Not called with `http: false`.
50
+ */
51
+ routes?(context: GraphQLDriverRoutesContext): readonly GraphQLDriverRoute[]
52
+ /**
53
+ * Called in `GraphQLPlugin.onBootstrap`, once the engine is built: create the server. A failure
54
+ * fails the boot (ZNIGHT066, the original error as `cause`).
55
+ */
56
+ start(engine: GraphQLEngine, context: GraphQLDriverContext): void | Promise<void>
57
+ /**
58
+ * Stops the server, after the engine ended its subscriptions: in `GraphQLPlugin.onDispose` for a
59
+ * route-mounted driver (once `HttpPlugin` drained its requests, which the driver keeps serving),
60
+ * in `GraphQLPlugin.onStop` for a server-mounted one (drain and stop the server).
61
+ */
62
+ stop?(): void | Promise<void>
63
+ }
package/src/engine.ts ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The GraphQL engine (RFC 0009 §4.2): what a GraphQL server (a driver) calls to run Znight's
3
+ * schema. Its functions have the shape of graphql-js's (`parse`, `validate`, `execute`,
4
+ * `subscribe`), so they fit Envelop's `useEngine`, `graphql-ws`, `graphql-sse` and `graphql-http`
5
+ * handlers as they are, and they keep everything `GraphQLPlugin` guarantees: the document cache and
6
+ * limits, the request scope and `GraphQLContext`, guards, loaders, result mappers, error mapping
7
+ * and masking, the subscriptions cap and the `graphql:operation` hook.
8
+ *
9
+ * A layer over the internal `GraphQLExecutor`: the built-in transport keeps calling the executor
10
+ * directly, so the engine costs nothing to an application without driver.
11
+ */
12
+ import type { ByteSize } from '@znight/http'
13
+ import type {
14
+ DocumentNode,
15
+ ExecutionArgs,
16
+ ExecutionResult,
17
+ GraphQLError,
18
+ GraphQLSchema,
19
+ Source,
20
+ ValidationRule,
21
+ } from 'graphql'
22
+ import type { EngineStream, ExecuteOptions, GraphQLExecutor } from './execution.ts'
23
+
24
+ /** A value of type `T` or a promise of it (graphql-js `PromiseOrValue`). */
25
+ type PromiseOrValue<T> = T | Promise<T>
26
+
27
+ /** Options of {@link GraphQLEngine.execute} and {@link GraphQLEngine.subscribe}. */
28
+ export interface EngineExecuteOptions {
29
+ /**
30
+ * The protocol that carried the operation (`GraphQLOperationEvent.transport`,
31
+ * `GraphQLContext.transport`): `'http'` (default), `'sse'` for an event stream, `'ws'` for
32
+ * `graphql-ws`.
33
+ */
34
+ readonly transport?: 'http' | 'sse' | 'ws'
35
+ /**
36
+ * Cancels the operation (graphql-js `abortSignal`, `GraphQLContext.signal`). Default:
37
+ * `ExecutionArgs.abortSignal`, else the signal of the HTTP request of the current scope.
38
+ */
39
+ readonly signal?: AbortSignal
40
+ /** `GraphQLContext.requestId`. Default: the request id of the HTTP request of the scope. */
41
+ readonly requestId?: string
42
+ }
43
+
44
+ /** The resolved options a GraphQL server needs to configure itself (read-only). */
45
+ export interface GraphQLEngineSettings {
46
+ /** The endpoint path, the `HttpPlugin` prefix included (`/api/graphql`). */
47
+ readonly path: string
48
+ readonly introspection: boolean
49
+ /** The `graphiql` option: serve the server's IDE. */
50
+ readonly ide: boolean
51
+ readonly exposeErrors: boolean
52
+ /** `limits.maxTokens`, for servers that parse documents themselves. */
53
+ readonly maxTokens: number | undefined
54
+ readonly subscriptions: false | { readonly maxActive: number }
55
+ readonly http: false | { readonly get: boolean; readonly bodyLimit: ByteSize }
56
+ }
57
+
58
+ /**
59
+ * The engine of a bootstrapped `GraphQLPlugin` (`plugin.engine`, and the first argument of
60
+ * `GraphQLDriver.start()`). Its functions are bound: pass them around as they are.
61
+ *
62
+ * ```ts
63
+ * const handler = createHandler({
64
+ * schema: engine.schema,
65
+ * parse: engine.parse,
66
+ * validate: engine.validate,
67
+ * execute: args => engine.execute(args),
68
+ * })
69
+ * ```
70
+ */
71
+ export interface GraphQLEngine {
72
+ /** The executable schema: its resolvers are Znight's pipelines. */
73
+ readonly schema: GraphQLSchema
74
+ readonly settings: GraphQLEngineSettings
75
+ /**
76
+ * graphql-js `parse` through the document cache: `maxTokens`, then the depth, aliases and
77
+ * directives limits. Throws a `GraphQLError` (`GRAPHQL_PARSE_FAILED`, `QUERY_TOO_COMPLEX`), like
78
+ * graphql-js. A text the engine validated returns the same `DocumentNode`.
79
+ */
80
+ parse(source: string | Source): DocumentNode
81
+ /**
82
+ * graphql-js `validate` with `rules` (default: graphql-js `specifiedRules`), to which the engine
83
+ * adds its own ({@link rules}), with `hideSuggestions` when introspection is off. With the
84
+ * default rules on {@link schema}, a document is validated once (then cached).
85
+ */
86
+ validate(schema: GraphQLSchema, document: DocumentNode, rules?: readonly ValidationRule[]): readonly GraphQLError[]
87
+ /** The engine's own rules (limits, introspection), for servers that validate documents themselves. */
88
+ readonly rules: readonly ValidationRule[]
89
+ /**
90
+ * Executes a validated query or mutation in the current request scope (or a scope of its own),
91
+ * with the operation's `GraphQLContext` as `contextValue` (`args.contextValue` becomes
92
+ * `GraphQLContext.serverContext`). Errors are mapped and masked, as `GraphQLError` instances.
93
+ */
94
+ execute(args: ExecutionArgs, options?: EngineExecuteOptions): PromiseOrValue<ExecutionResult>
95
+ /**
96
+ * Starts a validated subscription: a stream of results, or a single error result (rejected, past
97
+ * `subscriptions.maxActive`, a source that could not start). The plugin ends the stream when the
98
+ * application stops.
99
+ */
100
+ subscribe(
101
+ args: ExecutionArgs,
102
+ options?: EngineExecuteOptions,
103
+ ): PromiseOrValue<AsyncGenerator<ExecutionResult, void, void> | ExecutionResult>
104
+ }
105
+
106
+ /** The engine of a plugin: bound functions over its executor. */
107
+ export class Engine implements GraphQLEngine {
108
+ readonly schema: GraphQLSchema
109
+ readonly settings: GraphQLEngineSettings
110
+ readonly rules: readonly ValidationRule[]
111
+ readonly parse: (source: string | Source) => DocumentNode
112
+ readonly validate: (
113
+ schema: GraphQLSchema,
114
+ document: DocumentNode,
115
+ rules?: readonly ValidationRule[],
116
+ ) => readonly GraphQLError[]
117
+ readonly execute: (args: ExecutionArgs, options?: EngineExecuteOptions) => PromiseOrValue<ExecutionResult>
118
+ readonly subscribe: (args: ExecutionArgs, options?: EngineExecuteOptions) => Promise<EngineStream | ExecutionResult>
119
+
120
+ constructor(executor: GraphQLExecutor, settings: GraphQLEngineSettings, driver: string) {
121
+ this.schema = executor.schema
122
+ this.settings = Object.freeze({ ...settings })
123
+ this.rules = executor.engineRules
124
+ // extra arguments of graphql-js (parse options, validation options) are not used: the engine's apply
125
+ this.parse = source => executor.parseDocument(source, driver)
126
+ this.validate = (schema, document, rules) => executor.validateDocument(schema, document, rules, driver)
127
+ this.execute = (args, options) => executor.executeDocument(args, executeOptions(args, options, driver))
128
+ this.subscribe = (args, options) => executor.subscribeDocument(args, executeOptions(args, options, driver))
129
+ }
130
+ }
131
+
132
+ function executeOptions(
133
+ args: ExecutionArgs,
134
+ options: EngineExecuteOptions | undefined,
135
+ driver: string,
136
+ ): ExecuteOptions {
137
+ const signal = options?.signal ?? args?.abortSignal ?? undefined
138
+ const requestId = options?.requestId
139
+ return {
140
+ transport: options?.transport ?? 'http',
141
+ driver,
142
+ serverContext: args?.contextValue,
143
+ ...(signal === undefined ? {} : { signal }),
144
+ ...(requestId === undefined ? {} : { requestId }),
145
+ }
146
+ }