@znight/graphql 0.2.2 → 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/README.md CHANGED
@@ -44,7 +44,8 @@ npm install -D @znight/compiler vite
44
44
 
45
45
  Runs on Node.js >= 24 and Bun >= 1.3 (ESM only). `@znight/http` and `graphql` 17 are required peer
46
46
  dependencies, `dataloader` is a dependency, `@znight/testing` an optional peer (for
47
- `@znight/graphql/testing`).
47
+ `@znight/graphql/testing`), `graphql-yoga` 6 an optional peer (for the Yoga driver,
48
+ `@znight/graphql/yoga`).
48
49
 
49
50
  ## Contents
50
51
 
@@ -61,6 +62,7 @@ dependencies, `dataloader` is a dependency, `@znight/testing` an optional peer (
61
62
  - [Errors as data](#errors-as-data)
62
63
  - [Plugin options](#plugin-options)
63
64
  - [GraphQL over HTTP and SSE](#graphql-over-http-and-sse)
65
+ - [Drivers and the engine](#drivers-and-the-engine)
64
66
  - [Security defaults and limits](#security-defaults-and-limits)
65
67
  - [Testing](#testing)
66
68
  - [The generated SDL and the compiler options](#the-generated-sdl-and-the-compiler-options)
@@ -521,13 +523,14 @@ Built-in scalars appear in the schema only when used: `String`, `Float`, `Int`,
521
523
  | Member | What |
522
524
  | --------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
523
525
  | `requestId` | Id of the request carrying the operation (`HttpContext.requestId` over HTTP, a UUID or `requestId` option otherwise) |
524
- | `transport` | `'http'`, `'sse'` or `'direct'` (`plugin.execute()` / `subscribe()`) |
526
+ | `transport` | `'http'`, `'sse'` or `'direct'` (`plugin.execute()` / `subscribe()`); `'ws'` is reserved for `graphql-ws` |
525
527
  | `http` | The `HttpContext` of the request, `undefined` for direct executions outside a request |
526
528
  | `operationType` / `operationName` | `'query'`, `'mutation'` or `'subscription'`; the selected operation name |
527
529
  | `document` / `variables` | The parsed document (shared by the document cache: never mutate it) and the coerced variables |
528
530
  | `signal` | Over HTTP, the request's `ctx.signal`: client gone, SSE stream closed; passed to graphql-js as `abortSignal` |
529
531
  | `state` | A `Map` for per-operation storage |
530
532
  | `loader(Class \| token)` | The DataLoader of the current execution for a loader |
533
+ | `serverContext` | With a [driver](#drivers-and-the-engine), the context its server gave the engine; `undefined` otherwise |
531
534
 
532
535
  - Resolvers receive it with `@Ctx()`; request-scoped providers inject `GraphQLContext`, singletons
533
536
  `Lazy<GraphQLContext>`, exactly like `HttpContext`.
@@ -786,12 +789,13 @@ ZNIGHT066.
786
789
  | `resultMappers` | `[]` | `GraphQLResultMapper` classes or instances, tried in order: errors as data on the fields whose union holds one of their `types` ([Errors as data](#errors-as-data)) |
787
790
  | `exposeErrors` | `false` | Unmasked messages for unexpected errors (development) |
788
791
  | `introspection` | `NODE_ENV !== 'production'` | Off: graphql-js `NoSchemaIntrospectionCustomRule`, and suggestions are hidden |
789
- | `graphiql` | `NODE_ENV === 'development'` | `GET path` with `accept: text/html` serves GraphiQL |
792
+ | `graphiql` | `NODE_ENV === 'development'` | `GET path` with `accept: text/html` serves GraphiQL; with a `driver`, the driver's IDE |
790
793
  | `limits` | `{ maxTokens: 2000, maxDepth: 12, maxAliases: 30, maxDirectives: 50 }` | Request limits (introspection fields exempt from `maxDepth`); `false` disables one |
791
794
  | `documentCache` | `1000` | Distinct documents kept parsed and validated (LRU); `0` disables |
792
795
  | `subscriptions` | `{ maxActive: 10_000, bufferSize: 100 }` | `false` disables subscriptions over SSE. `heartbeat` (ms or `false`) and `maxDuration` (ms) are passed to `HttpResponse.sse()` (default: the `HttpPlugin` `sse` options) |
793
796
  | `pubsub` | a `MemoryPubSub` | The `PubSub` provided to the container |
794
797
  | `enforcement` | `'error'` | How the boot check of field decisions reports a problem: `'error'` fails the boot (ZNIGHT023), `'warn'` logs |
798
+ | `driver` | the built-in transport | The GraphQL server serving the schema ([Drivers and the engine](#drivers-and-the-engine)); `heartbeat` and `maxDuration` are refused with one (ZNIGHT066) |
795
799
 
796
800
  `HttpPlugin` must be registered unless `http: false` (ZNIGHT066). The plugin instance exposes:
797
801
 
@@ -803,14 +807,15 @@ ZNIGHT066.
803
807
  | `subscribe(request, options?)` | A stream of results, one per event, until the source ends, `options.signal` aborts, `return()` is called or the application stops |
804
808
  | `pubsub` | The `PubSub` of the application |
805
809
  | `activeSubscriptions` | Active subscriptions, every transport |
806
- | `initRoots()` / `httpRoutes()` | The `PluginInitRoots` contract (`TestApp`) and the `PluginHttpRoutes` contract of `@znight/http` |
810
+ | `engine` | The `GraphQLEngine` drivers call (after bootstrap): [Drivers and the engine](#drivers-and-the-engine) |
811
+ | `initRoots()` / `httpRoutes()` | The `PluginInitRoots` contract (`TestApp`) and the `PluginHttpRoutes` contract of `@znight/http`, `onHttpDrain()` included |
807
812
 
808
813
  `request` is `{ query, operationName?, variables?, extensions? }`; `options` is `{ signal?,
809
814
  requestId? }`. A direct execution has no HTTP guards of its own: inside a controller, it runs in the
810
815
  route's request scope, after the route's guards. With `http: false`, a controller can hand its
811
- requests to another GraphQL server that delegates the execution to `execute()` and `subscribe()`:
812
- the guide's [Serve the schema with GraphQL Yoga](../../docs/guide/graphql.md#yoga) recipe keeps
813
- guards, loaders, the request scope and error formatting.
816
+ requests to another GraphQL server that delegates the execution to `execute()` and `subscribe()`;
817
+ a [driver](#drivers-and-the-engine) does it without the copying, on the parsed document (GraphQL
818
+ Yoga: [`yogaDriver()`](#graphql-yoga)).
814
819
 
815
820
  ```ts
816
821
  const plugin = app.getPlugin<GraphQLPlugin>('graphql')
@@ -818,10 +823,13 @@ const result = await plugin?.execute({ query: '{ books { total } }' })
818
823
  ```
819
824
 
820
825
  `onStart` logs `GraphQL endpoint ready` with the path, the number of types and the schema hash
821
- (`GraphQL schema ready` with `http: false`). On shutdown, `onStop` ends the direct subscriptions;
822
- `HttpPlugin` drains the requests in flight and closes the SSE streams (their `ctx.signal` aborts with
823
- `'shutdown'`, then the stream ends with a jittered `retry` and **no** `complete` event, so clients
824
- reconnect to another instance). Plugin order does not matter.
826
+ (`GraphQL schema ready` with `http: false`). On shutdown, `onStop` ends the direct subscriptions
827
+ and those of the engine; `HttpPlugin` drains the requests in flight and closes the SSE streams
828
+ (their `ctx.signal` aborts with `'shutdown'`, then the stream ends with a jittered `retry` and **no**
829
+ `complete` event, so clients reconnect to another instance). When `HttpPlugin` starts draining, it
830
+ calls `GraphQLPlugin.onHttpDrain()`, which ends the engine's subscriptions too (a driver serves them
831
+ as streams `HttpPlugin` does not close) and refuses new ones (503). A route-mounted driver stops in
832
+ `onDispose`, once the drain is over: requests in flight are served. Plugin order does not matter.
825
833
 
826
834
  ## GraphQL over HTTP and SSE
827
835
 
@@ -863,6 +871,201 @@ Adapters need nothing specific. With Express, do not mount `express.json()` befo
863
871
  (the body would be consumed). In tests, `TestApp.inject()` runs queries and mutations through every
864
872
  adapter in memory and `injectStream()` / `injectSse()` stream subscriptions.
865
873
 
874
+ ## Drivers and the engine
875
+
876
+ The built-in GraphQL-over-HTTP and SSE endpoint is the default and the fastest path. A **driver**
877
+ serves the same schema with another GraphQL server, for its protocol features and its plugins
878
+ (RFC 0009): Znight stays the engine, the driver owns the transport.
879
+
880
+ ```ts
881
+ import { yogaDriver } from '@znight/graphql/yoga'
882
+
883
+ GraphQLPlugin.configure({ guards: [AccessGuard], driver: yogaDriver() })
884
+ ```
885
+
886
+ Resolvers, guards, loaders, result mappers, request-scoped providers and tests do not change: the
887
+ server calls `plugin.engine`, which runs the compiled schema with everything this README describes.
888
+ **GraphQL Yoga** is the first driver ([`@znight/graphql/yoga`](#graphql-yoga)); **Apollo Server**
889
+ follows once it accepts graphql-js 17. Any other graphql-js-shaped server takes the engine as it
890
+ is: a driver is a small object, checked with the [conformance suite](#driver-conformance).
891
+
892
+ **The engine.** `plugin.engine` (after bootstrap, and the first argument of `driver.start()`):
893
+
894
+ | Member | What |
895
+ | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
896
+ | `schema` | The executable schema; its resolvers are Znight's pipelines |
897
+ | `settings` | The resolved options a server needs: `path` (prefix included), `introspection`, `ide` (`graphiql`), `exposeErrors`, `maxTokens`, `subscriptions` (`false` or `{ maxActive }`), `http` (`false` or `{ get, bodyLimit }`) |
898
+ | `parse(source)` | graphql-js `parse` through the document cache, with `maxTokens` and the depth, aliases and directives limits; throws a `GraphQLError` (`GRAPHQL_PARSE_FAILED`, `QUERY_TOO_COMPLEX`) |
899
+ | `validate(schema, doc, rules?)` | graphql-js `validate` with `rules` (default `specifiedRules`) plus the engine's, suggestions hidden without introspection; a valid document is cached (default rules), its errors carry `GRAPHQL_VALIDATION_FAILED` |
900
+ | `rules` | The engine's own rules (the limits, `NoSchemaIntrospectionCustomRule` without introspection), for servers that validate themselves |
901
+ | `execute(args, options?)` | A validated query or mutation, in the current request scope (or one of its own): the `GraphQLContext` is the `contextValue`, `args.contextValue` its `serverContext`; errors mapped and masked, as `GraphQLError` instances |
902
+ | `subscribe(args, options?)` | A validated subscription: a stream of results (loaders renewed per event), or a single error result; the `maxActive` cap is shared with every transport, and the plugin ends the stream when the application stops |
903
+
904
+ `options` is `{ transport?: 'http' | 'sse' | 'ws', signal?, requestId? }`; the signal defaults to
905
+ `args.abortSignal`, then to the HTTP request's. The functions are bound and have graphql-js's
906
+ signatures, so they fit `graphql-http`, `graphql-sse`, `graphql-ws` and Envelop's `useEngine` as
907
+ they are. Every operation produces one `graphql:operation` event with the driver's name, and
908
+ documents rejected by `parse` or `validate` are observed too.
909
+
910
+ **The driver contract.** A `GraphQLDriver` has a `name` (`builtin` is reserved), optional
911
+ `routes()`, `start(engine, context)` and an optional `stop()`:
912
+
913
+ ```ts
914
+ import type { HttpContext } from '@znight/http'
915
+ import type { GraphQLDriver } from '@znight/graphql'
916
+
917
+ import { HttpResponse } from '@znight/http'
918
+ import { createHandler, type Handler } from 'graphql-http'
919
+
920
+ /** graphql-http's handler on the engine: GraphQL over HTTP served by the reference implementation. */
921
+ export function graphqlHttpDriver(): GraphQLDriver {
922
+ let handler: Handler<HttpContext> | undefined
923
+ return {
924
+ name: 'graphql-http',
925
+ routes: ({ path }) => {
926
+ const handle = async (ctx: HttpContext) => {
927
+ const body = ctx.method === 'POST' ? new TextDecoder().decode(await ctx.rawBody()) : null
928
+ const [text, init] = await (handler as Handler<HttpContext>)({
929
+ method: ctx.method,
930
+ url: ctx.url,
931
+ headers: { get: name => ctx.header(name.toLowerCase()) ?? null },
932
+ body,
933
+ raw: ctx,
934
+ context: undefined,
935
+ })
936
+ return new HttpResponse(text ?? undefined, { status: init.status, headers: init.headers ?? {} })
937
+ }
938
+ return [
939
+ { method: 'GET', path, name: 'graphql', handler: handle },
940
+ { method: 'POST', path, name: 'graphql', handler: handle },
941
+ ]
942
+ },
943
+ start(engine) {
944
+ handler = createHandler<HttpContext>({
945
+ schema: engine.schema,
946
+ parse: engine.parse,
947
+ validate: engine.validate,
948
+ execute: args => engine.execute(args),
949
+ })
950
+ },
951
+ }
952
+ }
953
+ ```
954
+
955
+ - **Routes.** A route-mounted driver returns its routes: `GraphQLPlugin.httpRoutes()` gives them to
956
+ `HttpPlugin` with the `http` option's `meta` and `guards`, and its `bodyLimit` on the routes that
957
+ are not `GET`. They run in the HTTP pipeline like the built-in endpoint (request id, request
958
+ scope, `http:request` hooks, global and endpoint guards, route decisions, CORS, security headers,
959
+ drain), so the engine finds the request scope: `GraphQLContext.http` is the request, and HTTP
960
+ guards and GraphQL guards see the same request-scoped providers. `routes()` is called once,
961
+ possibly before `start()`. A driver without routes is server-mounted: it attaches itself to
962
+ `context.http.adapter` in `start()` and runs outside the pipeline.
963
+ - **Lifecycle.** `onBootstrap` builds the engine, then awaits `start(engine, { logger, container,
964
+ http })`; a failure fails the boot (ZNIGHT066, the original error as `cause`). The engine's
965
+ subscriptions end when `HttpPlugin` starts draining (`onHttpDrain()`, whatever the plugin order)
966
+ and in `onStop`; new ones are then refused (503). `stop()` is awaited in `onDispose` for a
967
+ route-mounted driver, after `HttpPlugin` drained its requests, and in `onStop` for a
968
+ server-mounted one (`onDispose` does, for an application that never started).
969
+ - **Options.** The engine options (`guards`, `errorMappers`, `resultMappers`, `exposeErrors`,
970
+ `introspection`, `limits`, `documentCache`, `subscriptions: false`, `maxActive`, `bufferSize`,
971
+ `pubsub`, `enforcement`) apply to every driver. `path`, `http` and `graphiql` are read by the
972
+ driver through its routes and `engine.settings`. `subscriptions.heartbeat` and `maxDuration`
973
+ configure the built-in SSE transport only: with a driver they are ZNIGHT066 (use the driver's
974
+ keep-alives). A driver with routes needs `HttpPlugin` and refuses `http: false` (ZNIGHT066).
975
+ - **Errors.** The engine maps and masks errors once; a driver keeps the engine's errors as they
976
+ are. Request errors raised by the server itself (malformed JSON, a persisted query not found) are
977
+ the server's; an unexpected error thrown outside the engine is masked like the engine masks one.
978
+ - **Cost.** Without `driver`, the plugin runs the built-in transport's handlers, which call the
979
+ internal executor directly: an application without driver pays nothing for the abstraction.
980
+
981
+ `executeOperation()` and `subscribeOperation()` of `@znight/graphql/testing` send their requests to
982
+ the driver's routes like to the built-in endpoint. The repository's tests run the `graphql-http`
983
+ audits against the driver above and the Yoga driver on the four adapters, on Node.js and Bun.
984
+
985
+ ### GraphQL Yoga
986
+
987
+ `@znight/graphql/yoga` serves the schema with [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
988
+ 6 (`graphql-yoga`, an optional peer dependency loaded by this entry only):
989
+
990
+ ```ts
991
+ import { GraphQLPlugin } from '@znight/graphql'
992
+ import { yogaDriver } from '@znight/graphql/yoga'
993
+
994
+ GraphQLPlugin.configure({ driver: yogaDriver({ plugins: [usePersistedOperations({ getPersistedOperation })] }) })
995
+ ```
996
+
997
+ | Export | What |
998
+ | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
999
+ | `yogaDriver(options?)` | The driver (`name: 'yoga'`); `driver.yoga` is the Yoga server once started |
1000
+ | `YogaDriverOptions` | Yoga's options without those Znight owns (below); `graphiql` takes GraphiQL's options |
1001
+ | `yogaContext(ctx)` | Yoga's context of an operation (`request`, `params`, what plugins added, `znight`: the `HttpContext`), ZNIGHT066 otherwise |
1002
+ | `YogaContext`, `YogaServerContext` | Their types |
1003
+
1004
+ - **The engine is Yoga's engine.** The driver installs Envelop's `useEngine` with the engine's
1005
+ `parse`, `validate`, `execute` and `subscribe`, before the application's plugins: one document
1006
+ cache (Yoga's `parserAndValidationCache` is off), the engine's limits and introspection rule,
1007
+ rejected documents observed by the hook, one `graphql:operation` event per operation with
1008
+ `driver: 'yoga'` (`transport: 'sse'` for subscriptions). Plugins wrapping the execution
1009
+ (`useResponseCache`) call the engine; an operation executed by a plugin that replaces it
1010
+ (`useGraphQlJit`) fails with ZNIGHT066, logged once: its resolvers would not get the
1011
+ `GraphQLContext`.
1012
+ - **Owned options**, refused with ZNIGHT066 (a type error first): `schema`, `context`,
1013
+ `graphqlEndpoint` (`path`), `maskedErrors` (the engine masks once, see below),
1014
+ `cors` (`HttpPlugin`'s), `maxRequestBodySize` (`http.bodyLimit`), `parserAndValidationCache`,
1015
+ `requestId` (Yoga uses `HttpContext.requestId`), `healthCheckEndpoint`, `landingPage`,
1016
+ `fetchAPI` (the runtime's own), `disposeOnProcessTerminate` (the application stops Yoga),
1017
+ `batching` (one operation per request: the operations of a batch would share the request scope,
1018
+ where each one's `GraphQLContext` would replace the other's) and `multipart` (JSON bodies only,
1019
+ below). Yoga's `logging` is off unless set.
1020
+ - **Routes.** `GET` and `POST` on `path`. `GET` serves Yoga's GraphiQL with `graphiql` on, and
1021
+ queries with `http.get` on (405 with `allow: POST` otherwise); a mutation over `GET` is Yoga's 405
1022
+ (`allow: POST`). A `POST` body is `application/json` only, like the built-in transport: any other
1023
+ content type (a form, `multipart/form-data`, `text/plain`, `application/graphql`) is the built-in
1024
+ transport's 415, before Yoga parses it, so a cross-site page cannot run a mutation with the
1025
+ user's cookies (no CSRF: a JSON body always needs a CORS preflight).
1026
+ - **Errors.** The engine's errors (masked already) and `GraphQLError`s thrown on purpose (a plugin's
1027
+ rate limit, Yoga's request errors) are sent as they are. Any other error thrown outside the engine
1028
+ (an Envelop plugin's hook, Yoga itself) is logged once by Znight and masked like the engine masks
1029
+ one: `Unexpected error.` with code `INTERNAL_SERVER_ERROR` (its message with `exposeErrors`), a
1030
+ 500 for a request error.
1031
+ - **The Fetch bridge.** On `honoAdapter()` the native Web `Request` is handed to Yoga as is (a `GET`,
1032
+ or a body with a `content-length` within the limit); otherwise a `Request` is built from
1033
+ `HttpContext` with the body read by `ctx.rawBody()`, so a body past `http.bodyLimit` is
1034
+ `HttpPlugin`'s 413 on every adapter. Yoga's `Response` is sent as is by `HttpPlugin`.
1035
+ - **Subscriptions** use Yoga's SSE; the engine caps them (`subscriptions.maxActive`) and ends them
1036
+ when `HttpPlugin` starts draining, whatever the plugin order, when Yoga sends `complete` (not the
1037
+ built-in transport's `retry` hint); a subscription sent during the drain gets a
1038
+ `SERVICE_UNAVAILABLE` error. Queries in flight are served until the drain is over; `stop()` then
1039
+ awaits `yoga.dispose()` (the plugins' `onDispose`), and a request reaching the stopped driver gets
1040
+ a 503.
1041
+ - **Cost.** Yoga's request parsing, its plugin chain and the Web objects cost about 13 to 18 µs per
1042
+ request on Bun and 27 to 45 µs on Node.js in the in-process benchmark (`test/bench/http.bench.ts`),
1043
+ against about 1.6 µs and 5.5 µs for the built-in transport.
1044
+
1045
+ ### Driver conformance
1046
+
1047
+ `describeDriverConformance()` of `@znight/graphql/testing` describes the scenarios every driver
1048
+ passes (RFC 0009 §12), with the test framework's own functions (`bun:test`, Vitest, Jest,
1049
+ `node:test`) and `node:assert`: queries, mutations, a field guard reading a request-scoped
1050
+ provider, loaders batching, request-scoped providers per request, result mappers, masking,
1051
+ introspection off, limits, HTTP guards on the endpoint, the hook, `POST` bodies other than JSON
1052
+ (415, never executed) and mutations over `GET` (405), and over SSE (unless `subscriptions: false`)
1053
+ events, completion, client abort, `maxActive` and shutdown in both plugin orders.
1054
+
1055
+ ```ts
1056
+ import { describeDriverConformance } from '@znight/graphql/testing'
1057
+ import { yogaDriver } from '@znight/graphql/yoga'
1058
+ import { expressAdapter } from '@znight/http/express'
1059
+ import { honoAdapter } from '@znight/http/hono'
1060
+ import { afterAll, beforeAll, describe, it } from 'bun:test'
1061
+
1062
+ describeDriverConformance({
1063
+ driver: () => yogaDriver(), // a new driver per application; () => undefined for the built-in transport
1064
+ test: { describe, it, beforeAll, afterAll },
1065
+ adapters: { express: () => expressAdapter(), hono: () => honoAdapter() },
1066
+ })
1067
+ ```
1068
+
866
1069
  ## Security defaults and limits
867
1070
 
868
1071
  - **Introspection** off in production (`NODE_ENV === 'production'`), and suggestions hidden with it
@@ -1141,8 +1344,9 @@ Boot and execution are those of code-first.
1141
1344
  ## Observability
1142
1345
 
1143
1346
  Every operation goes through the `graphql:operation` hook of `@znight/core` (`GraphQLOperationEvent`:
1144
- `operationType`, `operationName`, `documentId` (SHA-256 of the query text), `transport`,
1145
- `requestId`, `context`, `startTime`, then `outcome` (`success`, `partial`, `error`, `rejected`) and
1347
+ `operationType`, `operationName`, `documentId` (SHA-256 of the query text), `transport`, `driver`
1348
+ (`builtin`, or the name of the [driver](#drivers-and-the-engine)), `requestId`, `context`,
1349
+ `startTime`, then `outcome` (`success`, `partial`, `error`, `rejected`) and
1146
1350
  `errors` with their codes). A subscription produces one event for its whole life. The event is built
1147
1351
  only when the hook has wrappers or listeners, so it costs nothing otherwise.
1148
1352
 
@@ -1186,8 +1390,9 @@ on Node.js, use `node --conditions=@znight/source` instead of `bun`):
1186
1390
  `GraphQLPlugin.execute()` over graphql-js `execute()` for a cached 50-field query (target: under
1187
1391
  10 %; measured 0–9 % on Bun 1.4, 1–7 % on Node.js 24).
1188
1392
  - `bun --conditions=@znight/source test/bench/http.bench.ts`: the endpoint through the `HttpPlugin`
1189
- pipeline in process (`GET` and `POST`, both JSON media types), next to bare graphql-js and to a
1190
- controller route answering a constant. `ABORT_REASON=1` removes the cost graphql-js 17.0 adds to
1393
+ pipeline in process (`GET` and `POST`, both JSON media types), next to bare graphql-js, to a
1394
+ controller route answering a constant, and to `GraphQLEngine.execute()` (what a driver calls: on
1395
+ a cached `{ hello }`, within 2 % of `plugin.execute()`). `ABORT_REASON=1` removes the cost graphql-js 17.0 adds to
1191
1396
  every execution (a `DOMException` built when it aborts its own `AbortController`, about 3 µs on
1192
1397
  Node.js): compare with other servers at the same graphql-js version.
1193
1398
 
@@ -1341,6 +1546,8 @@ are emitted by schema-first only: code-first sections, hashes and artifacts are
1341
1546
  custom internal enum values (an enum value is its SDL name).
1342
1547
  - **Subscriptions over SSE only.** No `graphql-ws` (WebSocket), no SSE single connection mode, no
1343
1548
  multipart subscriptions.
1549
+ - **Drivers**: the built-in transport and GraphQL Yoga (`@znight/graphql/yoga`); Apollo Server once it
1550
+ accepts graphql-js 17. Yoga plugins that replace the execution (`useGraphQlJit`) are refused.
1344
1551
  - No `@defer` / `@stream`, no persisted or trusted documents, no query cost analysis, no response
1345
1552
  cache, no federation or schema stitching, no file uploads (use an HTTP upload route), no operation
1346
1553
  batching.
@@ -1,6 +1,6 @@
1
- import { Token } from "@znight/core";
2
- import { GuardEnforcement, HttpContext, RouteMetaCarrier } from "@znight/http";
3
- import { ConstValueNode, DocumentNode, GraphQLResolveInfo } from "graphql";
1
+ import { Container, CoreLogger, Token } from "@znight/core";
2
+ import { ByteSize, GuardEnforcement, HttpContext, HttpPlugin, PluginRoute, RouteMetaCarrier } from "@znight/http";
3
+ import { ConstValueNode, DocumentNode, ExecutionArgs, ExecutionResult, FormattedExecutionResult, GraphQLError, GraphQLFormattedError, GraphQLResolveInfo, GraphQLSchema, Source, ValidationRule } from "graphql";
4
4
  /**
5
5
  * GraphQL errors shown to clients (RFC 0005 §5.10, §8.8): `GraphQLException` and the
6
6
  * `extensions.code` values the plugin produces (Apollo conventions, which clients already know).
@@ -70,8 +70,11 @@ declare class GraphQLException extends Error {
70
70
  type GraphQLArgs = Readonly<Record<string, unknown>>;
71
71
  /** Kind of a GraphQL operation. */
72
72
  type GraphQLOperationKind = "query" | "mutation" | "subscription";
73
- /** How an operation reached the server: GraphQL over HTTP, GraphQL over SSE, or `plugin.execute()`. */
74
- type GraphQLTransport = "http" | "sse" | "direct";
73
+ /**
74
+ * How an operation reached the server: GraphQL over HTTP, GraphQL over SSE, GraphQL over WebSocket
75
+ * (`ws`, reserved for `graphql-ws`), or `plugin.execute()` / `plugin.subscribe()` (`direct`).
76
+ */
77
+ type GraphQLTransport = "http" | "sse" | "ws" | "direct";
75
78
  /**
76
79
  * Options of a loader (§5.5), read once per loader class: the `options` property of a
77
80
  * {@link BatchLoad} provider. They are DataLoader's, except `batchScheduleFn` (Znight batches per
@@ -160,6 +163,13 @@ interface GraphQLContext {
160
163
  readonly signal: AbortSignal;
161
164
  /** Per-operation storage. */
162
165
  readonly state: Map<string | symbol, unknown>;
166
+ /**
167
+ * The context of the GraphQL server that received the operation through a driver (RFC 0009): the
168
+ * `contextValue` it passed to `GraphQLEngine.execute()` (Yoga's initial context, Apollo's context
169
+ * value). `undefined` for the built-in transport and direct calls. Resolvers do not need it: the
170
+ * request is `http`, the user a request-scoped provider.
171
+ */
172
+ readonly serverContext: unknown;
163
173
  /** The DataLoader of the current execution for a loader class (`ctx.loader(AuthorsById)`). */
164
174
  loader<L extends BatchLoad<unknown, unknown>>(loader: BatchLoadClass<L>): BatchLoader<LoaderKey<L>, LoaderValue<L>>;
165
175
  /**
@@ -262,5 +272,172 @@ interface ScalarDefinition<T> {
262
272
  /** Literal of the document → internal value. Default: `parseValue` of the literal's JSON value. */
263
273
  parseLiteral?(literal: ConstValueNode): T;
264
274
  }
265
- export { GraphQLExceptionOptions as C, GraphQLException as S, LoaderValue as _, GraphQLContext as a, GraphQLErrorCode as b, GraphQLGuard as c, GraphQLResultMapper as d, GraphQLResultMapperClass as f, LoaderOptions as g, LoaderKey as h, GraphQLArgs as i, GraphQLGuardClass as l, GraphQLTransport as m, BatchLoadClass as n, GraphQLErrorMapper as o, GraphQLResultTypes as p, BatchLoader as r, GraphQLErrorMapperClass as s, BatchLoad as t, GraphQLOperationKind as u, ResolverContext as v, GraphQLErrorCodeValue as x, ScalarDefinition as y };
266
- //# sourceMappingURL=types-DQ713UgI.d.mts.map
275
+ /** Options of {@link orderByKeys}. */
276
+ interface OrderByKeysOptions {
277
+ /**
278
+ * What a key without value gets: `'error'` (default) a `NOT_FOUND` `GraphQLException` failing that
279
+ * key only, `'null'` the value `null`.
280
+ */
281
+ readonly missing?: "error" | "null";
282
+ }
283
+ /**
284
+ * Reorders `values` (rows in any order, some keys possibly missing) to match `keys`, what a
285
+ * `BatchLoad.load()` must return. Keys are compared with `Map` semantics (`===`, `NaN` equal).
286
+ *
287
+ * ```ts
288
+ * async load(ids: readonly ID[]): Promise<readonly (Author | Error)[]> {
289
+ * return orderByKeys(ids, await this.authors.findMany(ids), author => author.id)
290
+ * }
291
+ * ```
292
+ */
293
+ declare function orderByKeys<K, V>(keys: readonly K[], values: Iterable<V>, keyOf: (value: V) => K, options?: OrderByKeysOptions & {
294
+ readonly missing?: "error";
295
+ }): (V | Error)[];
296
+ declare function orderByKeys<K, V>(keys: readonly K[], values: Iterable<V>, keyOf: (value: V) => K, options: OrderByKeysOptions & {
297
+ readonly missing: "null";
298
+ }): (V | null)[];
299
+ /** A GraphQL request: the body of a GraphQL-over-HTTP request, or the argument of `plugin.execute()`. */
300
+ interface GraphQLRequest {
301
+ readonly query: string;
302
+ readonly operationName?: string | null | undefined;
303
+ readonly variables?: Readonly<Record<string, unknown>> | null | undefined;
304
+ /** Request extensions (ignored by the executor; available to transports). */
305
+ readonly extensions?: Readonly<Record<string, unknown>> | null | undefined;
306
+ }
307
+ /** The results of a subscription (one per event), or the single result of a query sent as a stream. */
308
+ interface GraphQLResultStream extends AsyncIterableIterator<FormattedExecutionResult> {
309
+ /** Ends the stream: the source event stream is returned (unsubscribed). */
310
+ return(): Promise<IteratorResult<FormattedExecutionResult>>;
311
+ }
312
+ /** A value of type `T` or a promise of it (graphql-js `PromiseOrValue`). */
313
+ type PromiseOrValue<T> = T | Promise<T>;
314
+ /** Options of {@link GraphQLEngine.execute} and {@link GraphQLEngine.subscribe}. */
315
+ interface EngineExecuteOptions {
316
+ /**
317
+ * The protocol that carried the operation (`GraphQLOperationEvent.transport`,
318
+ * `GraphQLContext.transport`): `'http'` (default), `'sse'` for an event stream, `'ws'` for
319
+ * `graphql-ws`.
320
+ */
321
+ readonly transport?: "http" | "sse" | "ws";
322
+ /**
323
+ * Cancels the operation (graphql-js `abortSignal`, `GraphQLContext.signal`). Default:
324
+ * `ExecutionArgs.abortSignal`, else the signal of the HTTP request of the current scope.
325
+ */
326
+ readonly signal?: AbortSignal;
327
+ /** `GraphQLContext.requestId`. Default: the request id of the HTTP request of the scope. */
328
+ readonly requestId?: string;
329
+ }
330
+ /** The resolved options a GraphQL server needs to configure itself (read-only). */
331
+ interface GraphQLEngineSettings {
332
+ /** The endpoint path, the `HttpPlugin` prefix included (`/api/graphql`). */
333
+ readonly path: string;
334
+ readonly introspection: boolean;
335
+ /** The `graphiql` option: serve the server's IDE. */
336
+ readonly ide: boolean;
337
+ readonly exposeErrors: boolean;
338
+ /** `limits.maxTokens`, for servers that parse documents themselves. */
339
+ readonly maxTokens: number | undefined;
340
+ readonly subscriptions: false | {
341
+ readonly maxActive: number;
342
+ };
343
+ readonly http: false | {
344
+ readonly get: boolean;
345
+ readonly bodyLimit: ByteSize;
346
+ };
347
+ }
348
+ /**
349
+ * The engine of a bootstrapped `GraphQLPlugin` (`plugin.engine`, and the first argument of
350
+ * `GraphQLDriver.start()`). Its functions are bound: pass them around as they are.
351
+ *
352
+ * ```ts
353
+ * const handler = createHandler({
354
+ * schema: engine.schema,
355
+ * parse: engine.parse,
356
+ * validate: engine.validate,
357
+ * execute: args => engine.execute(args),
358
+ * })
359
+ * ```
360
+ */
361
+ interface GraphQLEngine {
362
+ /** The executable schema: its resolvers are Znight's pipelines. */
363
+ readonly schema: GraphQLSchema;
364
+ readonly settings: GraphQLEngineSettings;
365
+ /**
366
+ * graphql-js `parse` through the document cache: `maxTokens`, then the depth, aliases and
367
+ * directives limits. Throws a `GraphQLError` (`GRAPHQL_PARSE_FAILED`, `QUERY_TOO_COMPLEX`), like
368
+ * graphql-js. A text the engine validated returns the same `DocumentNode`.
369
+ */
370
+ parse(source: string | Source): DocumentNode;
371
+ /**
372
+ * graphql-js `validate` with `rules` (default: graphql-js `specifiedRules`), to which the engine
373
+ * adds its own ({@link rules}), with `hideSuggestions` when introspection is off. With the
374
+ * default rules on {@link schema}, a document is validated once (then cached).
375
+ */
376
+ validate(schema: GraphQLSchema, document: DocumentNode, rules?: readonly ValidationRule[]): readonly GraphQLError[];
377
+ /** The engine's own rules (limits, introspection), for servers that validate documents themselves. */
378
+ readonly rules: readonly ValidationRule[];
379
+ /**
380
+ * Executes a validated query or mutation in the current request scope (or a scope of its own),
381
+ * with the operation's `GraphQLContext` as `contextValue` (`args.contextValue` becomes
382
+ * `GraphQLContext.serverContext`). Errors are mapped and masked, as `GraphQLError` instances.
383
+ */
384
+ execute(args: ExecutionArgs, options?: EngineExecuteOptions): PromiseOrValue<ExecutionResult>;
385
+ /**
386
+ * Starts a validated subscription: a stream of results, or a single error result (rejected, past
387
+ * `subscriptions.maxActive`, a source that could not start). The plugin ends the stream when the
388
+ * application stops.
389
+ */
390
+ subscribe(args: ExecutionArgs, options?: EngineExecuteOptions): PromiseOrValue<AsyncGenerator<ExecutionResult, void, void> | ExecutionResult>;
391
+ }
392
+ /** What {@link GraphQLDriver.routes} receives. */
393
+ interface GraphQLDriverRoutesContext {
394
+ /** The `path` option: the endpoint path, relative to the `HttpPlugin` prefix (`/graphql`). */
395
+ readonly path: string;
396
+ /** The absolute endpoint path, the prefix included (`/api/graphql`). */
397
+ readonly endpoint: string;
398
+ }
399
+ /**
400
+ * A route of a driver: a plugin route of `HttpPlugin` without `meta`, `guards` and `bodyLimit`,
401
+ * which `GraphQLPlugin` adds from its `http` option (`bodyLimit` on the routes that are not `GET`).
402
+ */
403
+ type GraphQLDriverRoute = Pick<PluginRoute, "method" | "path" | "name" | "handler">;
404
+ /** What {@link GraphQLDriver.start} receives besides the engine. */
405
+ interface GraphQLDriverContext {
406
+ /** The application logger. */
407
+ readonly logger: CoreLogger;
408
+ readonly container: Container;
409
+ /** The `HttpPlugin` of the application, when registered (its adapter, for server-mounted drivers). */
410
+ readonly http: HttpPlugin | undefined;
411
+ }
412
+ /**
413
+ * A GraphQL driver, given to `GraphQLPlugin.configure({ driver })`. A route-mounted driver returns
414
+ * its routes, which run in `HttpPlugin`'s pipeline (request scope, hooks, guards, CORS, drain) and
415
+ * call the engine from there; a server-mounted one has no routes and attaches itself to the
416
+ * adapter in `start()`. One driver serves one application at a time.
417
+ */
418
+ interface GraphQLDriver {
419
+ /**
420
+ * The driver's name (`yoga`, `apollo`...): `GraphQLOperationEvent.driver`, logs and errors.
421
+ * `builtin` is reserved for the built-in transport.
422
+ */
423
+ readonly name: string;
424
+ /**
425
+ * The routes `HttpPlugin` serves for the driver, with the `http` option's meta, guards and body
426
+ * limit applied by the plugin. Called once per application, possibly before {@link start}: the
427
+ * handlers MUST NOT use the engine before the first request. Not called with `http: false`.
428
+ */
429
+ routes?(context: GraphQLDriverRoutesContext): readonly GraphQLDriverRoute[];
430
+ /**
431
+ * Called in `GraphQLPlugin.onBootstrap`, once the engine is built: create the server. A failure
432
+ * fails the boot (ZNIGHT066, the original error as `cause`).
433
+ */
434
+ start(engine: GraphQLEngine, context: GraphQLDriverContext): void | Promise<void>;
435
+ /**
436
+ * Stops the server, after the engine ended its subscriptions: in `GraphQLPlugin.onDispose` for a
437
+ * route-mounted driver (once `HttpPlugin` drained its requests, which the driver keeps serving),
438
+ * in `GraphQLPlugin.onStop` for a server-mounted one (drain and stop the server).
439
+ */
440
+ stop?(): void | Promise<void>;
441
+ }
442
+ export { ScalarDefinition as A, GraphQLResultMapperClass as C, LoaderOptions as D, LoaderKey as E, GraphQLErrorCodeValue as M, GraphQLException as N, LoaderValue as O, GraphQLExceptionOptions as P, GraphQLResultMapper as S, GraphQLTransport as T, GraphQLErrorMapper as _, EngineExecuteOptions as a, GraphQLGuardClass as b, GraphQLRequest as c, orderByKeys as d, BatchLoad as f, GraphQLContext as g, GraphQLArgs as h, GraphQLDriverRoutesContext as i, GraphQLErrorCode as j, ResolverContext as k, GraphQLResultStream as l, BatchLoader as m, GraphQLDriverContext as n, GraphQLEngine as o, BatchLoadClass as p, GraphQLDriverRoute as r, GraphQLEngineSettings as s, GraphQLDriver as t, OrderByKeysOptions as u, GraphQLErrorMapperClass as v, GraphQLResultTypes as w, GraphQLOperationKind as x, GraphQLGuard as y };
443
+ //# sourceMappingURL=driver-Cnze8Oyr.d.mts.map