From 7e9108e86bebbba82cd16b658e1b7f40e347f4eb Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Tue, 19 May 2026 01:14:19 +0300 Subject: [PATCH] docs: expand website guides --- website/pages/docs/_meta.ts | 29 +- website/pages/docs/abort-signals.mdx | 277 ++++++++ .../pages/docs/advanced-custom-scalars.mdx | 42 +- .../docs/advanced-execution-pipelines.mdx | 186 ++++++ website/pages/docs/custom-scalars.mdx | 71 +- website/pages/docs/defer-stream.mdx | 186 +++++- .../pages/docs/directives-on-directives.mdx | 130 ++++ website/pages/docs/execution-hooks.mdx | 192 ++++++ .../experimental-specification-features.mdx | 55 ++ website/pages/docs/fragment-arguments.mdx | 140 ++++ website/pages/docs/getting-started.mdx | 24 +- website/pages/docs/graphql-clients.mdx | 55 +- website/pages/docs/graphql-harness.mdx | 154 +++++ website/pages/docs/index.mdx | 27 +- .../pages/docs/mutations-and-input-types.mdx | 35 + website/pages/docs/oneof-input-objects.mdx | 22 +- website/pages/docs/passing-arguments.mdx | 47 ++ website/pages/docs/schema-coordinates.mdx | 128 ++++ website/pages/docs/schema-evolution.mdx | 177 +++++ website/pages/docs/subscriptions.mdx | 39 +- website/pages/upgrade-guides/v16-v17.mdx | 609 ++++++++++++++---- website/vercel.json | 5 + 22 files changed, 2432 insertions(+), 198 deletions(-) create mode 100644 website/pages/docs/abort-signals.mdx create mode 100644 website/pages/docs/advanced-execution-pipelines.mdx create mode 100644 website/pages/docs/directives-on-directives.mdx create mode 100644 website/pages/docs/execution-hooks.mdx create mode 100644 website/pages/docs/experimental-specification-features.mdx create mode 100644 website/pages/docs/fragment-arguments.mdx create mode 100644 website/pages/docs/graphql-harness.mdx create mode 100644 website/pages/docs/schema-coordinates.mdx create mode 100644 website/pages/docs/schema-evolution.mdx diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts index cabe4d07e8..dffed77ed8 100644 --- a/website/pages/docs/_meta.ts +++ b/website/pages/docs/_meta.ts @@ -23,14 +23,31 @@ const meta = { nullability: '', 'abstract-types': '', 'custom-scalars': '', + 'constructing-types': '', + 'oneof-input-objects': '', + 'schema-coordinates': '', + 'schema-evolution': '', + subscriptions: '', '-- 3': { type: 'separator', - title: 'Advanced Guides', + title: 'Experimental Specification Features', }, - 'constructing-types': '', - 'oneof-input-objects': '', + 'experimental-specification-features': '', 'defer-stream': '', - subscriptions: '', + 'fragment-arguments': '', + 'directives-on-directives': '', + '-- 4': { + type: 'separator', + title: 'GraphQL.js Runtime Features', + }, + 'graphql-harness': '', + 'advanced-execution-pipelines': '', + 'abort-signals': '', + 'execution-hooks': '', + '-- 5': { + type: 'separator', + title: 'Advanced Guides', + }, 'type-generation': '', 'cursor-based-pagination': '', 'advanced-custom-scalars': '', @@ -41,7 +58,7 @@ const meta = { 'graphql-errors': '', 'using-directives': '', 'authorization-strategies': '', - '-- 4': { + '-- 6': { type: 'separator', title: 'Testing', }, @@ -50,7 +67,7 @@ const meta = { 'testing-operations': '', 'testing-resolvers': '', 'testing-best-practices': '', - '-- 5': { + '-- 7': { type: 'separator', title: 'Production & Scaling', }, diff --git a/website/pages/docs/abort-signals.mdx b/website/pages/docs/abort-signals.mdx new file mode 100644 index 0000000000..5238a0871e --- /dev/null +++ b/website/pages/docs/abort-signals.mdx @@ -0,0 +1,277 @@ +--- +title: Handling Abort Signals +sidebarTitle: Abort Signals +--- + +import { Callout } from 'nextra/components'; + +# Handling Abort Signals + + + Abort signal support is available in GraphQL.js v17 and newer. It is a + GraphQL.js runtime API, not GraphQL syntax or a transport protocol. + + +## What the specification says + +The GraphQL specification mentions cancellation in a narrow execution case. +During ordinary execution, when a non-null execution error propagates to a +parent response position, sibling response positions that have not executed or +yielded a value +[may be cancelled](https://spec.graphql.org/draft/#sec-Errors-and-Non-Null-Types) +to avoid unnecessary work. The +[conformance appendix](https://spec.graphql.org/draft/#sec-Appendix-Conformance) +makes lowercase key words in normative portions of the specification carry +their RFC 2119 meaning, so this is a normative `MAY`. It permits cancellation; +it does not require every implementation to cancel work in that case. + +The subscription algorithms also describe cancellation of response streams and +source streams. Those algorithms are still about GraphQL execution and stream +lifecycle, not about a user or host cancelling a request that is already in +flight. + +The specification does not define a cancellation primitive or a transport +cancellation protocol. + +## What GraphQL.js v17 adds + +GraphQL.js v17 exposes cancellation for two related situations: + +- Internally, GraphQL.js can signal work that no longer contributes to the + returned result, such as work from response positions cancelled under the + specification's execution rules. +- Externally, a host can pass `abortSignal` to `execute()`, `subscribe()`, + `graphql()`, or `experimentalExecuteIncrementally()` to cancel an issued + request while it is in flight. + +Both cases matter because long-running GraphQL operations often start other +asynchronous work: database queries, HTTP requests, async iterators, loaders, +and subscription streams. In v16, GraphQL.js had no standard way to ask that +work to stop. In v17, GraphQL.js uses `AbortSignal` as its JavaScript runtime +API for cancellation. + +GraphQL.js v17 uses the same resolver-scoped signal for internal cancellation +and external request cancellation. Resolvers read that shared signal with +`info.getAbortSignal()` and pass it to downstream APIs that support +cancellation. + +Abort signals are cooperative. They let GraphQL.js and resolvers pass a +cancellation request to downstream work, but JavaScript cannot force an +arbitrary promise, database driver, HTTP client, or async iterator to stop. The +downstream API has to accept the signal and honor it. + +GraphQL.js does not currently provide fine-grained per-field or per-branch +abort signals. Resolvers in an operation share one resolver `AbortSignal`. For +internally cancelled portions of an operation, GraphQL.js aborts that shared +signal when the result that will actually be returned has finished, notifying +pending resolver work together. An external abort also aborts the same shared +resolver signal. This coarseness may change in future versions. + +## Using the signal in resolvers + +Resolvers obtain the abort signal via `info.getAbortSignal()`. Pass that +signal to downstream APIs that support cancellation. + +```js +const Query = new GraphQLObjectType({ + name: 'Query', + fields: { + user: { + type: User, + args: { id: { type: new GraphQLNonNull(GraphQLID) } }, + async resolve(_source, args, _context, info) { + const abortSignal = info.getAbortSignal(); + + const response = await fetch(`https://users.example/${args.id}`, { + signal: abortSignal, + }); + + return response.json(); + }, + }, + }, +}); +``` + +As noted, this signal can be triggered by GraphQL.js itself. For example, +suppose a query selects `user { profile recommendations }`, `profile` is +non-null, and the `recommendations` resolver starts a slow downstream request. +If `profile` throws and the error bubbles so that `user` becomes `null`, the +`recommendations` result can no longer appear in the response. GraphQL.js does +not currently create a separate signal for that one sibling branch. Instead, +when the response that will actually be returned has finished, GraphQL.js +aborts the shared resolver signal, so any still-pending resolver work that +honors the signal can stop together. + +For work that does not accept `AbortSignal`, check the signal before starting +and again between expensive steps. This is useful even for synchronous chunks: +JavaScript cannot interrupt code that is already running, but the next check can +avoid starting more work. + +```js +async function loadReport(info) { + const abortSignal = info.getAbortSignal(); + + if (abortSignal?.aborted) { + throw abortSignal.reason; + } + + const rows = await loadReportRows(); + + if (abortSignal?.aborted) { + throw abortSignal.reason; + } + + const totals = calculateTotals(rows); + + if (abortSignal?.aborted) { + throw abortSignal.reason; + } + + return formatReport(totals); +} +``` + +If the API exposes its own cancellation method, connect the abort event to that +method: + +```js +async function loadReportFromJob(info) { + const abortSignal = info.getAbortSignal(); + + if (abortSignal?.aborted) { + throw abortSignal.reason; + } + + const job = startReportJob(); + abortSignal?.addEventListener( + 'abort', + () => { + job.cancel(); + }, + { once: true }, + ); + + return job.result; +} +``` + +Here `startReportJob()` represents an async API that does not accept +`AbortSignal` directly, but does return an object with a `result` promise and a +`cancel()` method. + +## Passing an external signal to execution + +GraphQL.js also lets a host cancel an issued request while it is in flight. +Pass `abortSignal` to `graphql()`, `execute()`, `subscribe()`, or +`experimentalExecuteIncrementally()`. If that external signal is aborted, +GraphQL.js aborts the same resolver signal exposed through +`info.getAbortSignal()`. + +```js +import { execute, parse } from 'graphql'; + +const controller = new AbortController(); +const document = parse(` + query User($id: ID!) { + user(id: $id) { + id + name + } + } +`); + +const resultPromise = execute({ + schema, + document, + variableValues: { id: '123' }, + abortSignal: controller.signal, +}); + +setTimeout(() => { + controller.abort(new Error('Request timed out')); +}, 500); + +const result = await resultPromise; +``` + +If the signal is aborted before execution finishes, asynchronous execution +rejects. The abort reason becomes the rejection cause when possible. + +## Wiring HTTP request life cycles + +Most servers already know when a request is no longer useful: the client +disconnects, a gateway timeout fires, or a framework cancellation token is +triggered. Bridge that lifecycle to GraphQL.js so resolver cancellation follows +request cancellation. + +```js +const controller = new AbortController(); + +req.on('close', () => { + controller.abort(new Error('Client disconnected')); +}); + +const result = await execute({ + schema, + document, + variableValues, + contextValue, + abortSignal: controller.signal, +}); +``` + +This helps avoid expensive resolver work after the client is already gone. + +## Handling aborted execution + +GraphQL.js rejects with `AbortedGraphQLExecutionError` when execution is +aborted after it has started. The error exposes the best partial result +GraphQL.js can still produce. + +```js +import { AbortedGraphQLExecutionError, execute } from 'graphql'; + +try { + const result = await execute({ + schema, + document, + abortSignal, + }); + return result; +} catch (error) { + if (error instanceof AbortedGraphQLExecutionError) { + const partialResult = await error.abortedResult; + logger.info({ partialResult }, 'GraphQL execution aborted'); + } + + throw error; +} +``` + +`abortedResult` may be either a result or a promise for a result. For +incremental delivery, it may contain the initial incremental result if that was +already available. + +## Observing async cleanup + +An abort can stop GraphQL.js from producing more response data before every +tracked async task has settled. Cleanup can continue after the response +boundary, especially when resolvers use async iterators or when downstream +libraries perform their own shutdown work. + +Use the experimental `asyncWorkFinished` execution hook when a host needs to +observe that boundary. See [Execution Hooks](/docs/execution-hooks) for +examples of logging cleanup completion, delaying response delivery until +tracked work settles, and tracking resolver-started async work. + +## Practical guidance + +- Treat abort as cooperative cancellation. A resolver or downstream client that + ignores the signal may keep doing work outside GraphQL.js. +- Pass the signal to downstream clients early, before starting expensive work. +- Avoid swallowing abort errors in resolvers. Let GraphQL.js stop the operation. +- Keep request timeouts at the server or transport layer, and connect those + timeouts to an `AbortController`. +- Do not expose abort signals in the GraphQL schema. They are a JavaScript + runtime concern, not client query syntax. diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx index b71aa450fc..930284b283 100644 --- a/website/pages/docs/advanced-custom-scalars.mdx +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -2,11 +2,39 @@ title: Best Practices for Custom Scalars --- +import { Callout } from 'nextra/components'; + # Custom Scalars: Best Practices and Testing Custom scalars must behave predictably and clearly. To maintain a consistent, reliable schema, follow these best practices. + + GraphQL.js v17 renames the scalar hooks to the coercion terms used by the + specification. The v16 names still work in v17, but are deprecated for + removal in v18. + + +### Map v16 names to v17 names + +| v16 name | v17 name | Purpose | +| --- | --- | --- | +| `serialize` | `coerceOutputValue` | Convert resolver values into response values. | +| `parseValue` | `coerceInputValue` | Convert variable values into internal values. | +| `parseLiteral` | `coerceInputLiteral` | Convert constant GraphQL literals into internal values. | +| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. | + +If your scalar supports both v16 and v17, keep the v16 method names for now. +When your minimum version is v17, implement the v17 names and add +`valueToLiteral()` if tooling needs to print defaults or external values for +that scalar. + +In v16, `parseLiteral(ast, variables)` could receive variable values directly. +In v17, `coerceInputLiteral()` receives a constant literal. During execution, +GraphQL.js calls `replaceVariables()` before scalar literal coercion. If +tooling calls `coerceInputLiteral()` directly outside execution, it must call +`replaceVariables()` first. + ### Document expected formats and validation Provide a clear description of the scalar's accepted input and output formats. For example, a @@ -14,11 +42,12 @@ Provide a clear description of the scalar's accepted input and output formats. F Clear descriptions help clients understand valid input and reduce mistakes. -### Validate consistently across `parseValue` and `parseLiteral` +### Validate consistently across value and literal coercion Clients can send values either through variables or inline literals. -Your `parseValue` and `parseLiteral` functions should apply the same validation logic in -both cases. +Your value and literal coercion functions should apply the same validation +logic in both cases. In v16 those functions are `parseValue` and +`parseLiteral`; in v17 they are `coerceInputValue` and `coerceInputLiteral`. Use a shared helper to avoid duplication: @@ -32,7 +61,7 @@ function parseDate(value) { } ``` -Both `parseValue` and `parseLiteral` should call this function. +Both input coercion paths should call this function. ### Return clear errors @@ -69,8 +98,9 @@ Tests should cover three areas: coercion functions, schema integration, and erro ### Unit test serialization and parsing -Write unit tests for each function: `serialize`, `parseValue`, and `parseLiteral`. -Test with both valid and invalid inputs. +Write unit tests for each function: `serialize`, `parseValue`, and +`parseLiteral` in v16, or `coerceOutputValue`, `coerceInputValue`, and +`coerceInputLiteral` in v17. Test with both valid and invalid inputs. ```js describe('DateTime scalar', () => { diff --git a/website/pages/docs/advanced-execution-pipelines.mdx b/website/pages/docs/advanced-execution-pipelines.mdx new file mode 100644 index 0000000000..85a82059e5 --- /dev/null +++ b/website/pages/docs/advanced-execution-pipelines.mdx @@ -0,0 +1,186 @@ +--- +title: Advanced Execution Pipelines +sidebarTitle: Advanced Execution Pipelines +--- + +import { Callout } from 'nextra/components'; + +# Advanced Execution Pipelines + + + The APIs on this page are low-level GraphQL.js v17 execution APIs. Most + servers should call `execute()`, `subscribe()`, or a framework abstraction. + + +GraphQL.js v17 exposes validated execution argument objects so hosts can split +the execution step without rebuilding GraphQL.js internals. These helpers are +for code that already has a parsed document and has decided to execute it. A +complete request pipeline still needs to parse, validate, authorize, execute, +and serialize results in the places that make sense for the host. + +The central types are `ValidatedExecutionArgs` and +`ValidatedSubscriptionArgs`. The validator functions return one of those +objects, or a list of `GraphQLError` values that can be returned as the +operation result. They validate and normalize execution arguments; they do not +replace operation validation with `validate()`. + +## Build a custom `execute()` + +`validateExecutionArgs()` checks the schema, selects the operation, coerces +variables, prepares fragment information, and fills in default resolvers and +execution options. `executeRootSelectionSet()` then runs the same +single-result root selection set executor used by `execute()`. + +```js +import { executeRootSelectionSet, validateExecutionArgs } from 'graphql'; + +function executeWithTiming(args) { + const startedAt = Date.now(); + const validatedArgs = validateExecutionArgs(args); + + if (!('schema' in validatedArgs)) { + return { errors: validatedArgs }; + } + + const result = executeRootSelectionSet(validatedArgs); + const addTiming = (executionResult) => { + return { + ...executionResult, + extensions: { + ...executionResult.extensions, + durationMs: Date.now() - startedAt, + }, + }; + }; + + return typeof result.then === 'function' ? result.then(addTiming) : addTiming(result); +} +``` + +The native `execute()` and `subscribe()` functions preserve synchronous results +when execution completes synchronously. They do not wrap every result in a +promise. Custom wrappers that want the same behavior need to handle both the +synchronous and asynchronous paths, which is why the example returns either an +`ExecutionResult` or a promise for one. + +For incremental delivery, use +`experimentalExecuteRootSelectionSet(validatedArgs)` instead. It accepts the +same `ValidatedExecutionArgs` object and returns either an `ExecutionResult` or +the experimental incremental result shape. + +## Build a custom `subscribe()` + +`validateSubscriptionArgs()` performs the same base validation and also asserts +that the selected operation is a subscription. `createSourceEventStream()` +expects the resulting `ValidatedSubscriptionArgs` object and resolves the +source event stream from the root subscription field. + +```js +import { + createSourceEventStream, + mapSourceToResponseEvent, + validateSubscriptionArgs, +} from 'graphql'; + +function subscribeWithHostPipeline(args) { + const validatedArgs = validateSubscriptionArgs(args); + + if (!('schema' in validatedArgs)) { + return { errors: validatedArgs }; + } + + const source = createSourceEventStream(validatedArgs); + const mapSource = (sourceEventStream) => { + if ( + sourceEventStream == null || + typeof sourceEventStream[Symbol.asyncIterator] !== 'function' + ) { + return sourceEventStream; + } + + return mapSourceToResponseEvent(validatedArgs, sourceEventStream); + }; + + return typeof source.then === 'function' ? source.then(mapSource) : mapSource(source); +} +``` + +Subscription execution has two phases: + +1. Create the source event stream. +2. Execute the subscription selection set once for each source event. + +`subscribe()` performs both phases for you. The lower-level helpers are useful +when a host owns one phase directly, for example when source events are +produced by a broker process and response execution happens in a separate +worker. + +`mapSourceToResponseEvent()` maps each source event into an `ExecutionResult`. +With two arguments, it uses the default subscription event executor. + +## Customize per-event execution + +The third argument to `mapSourceToResponseEvent()` is a +`RootSelectionSetExecutor`. That function receives the validated subscription +arguments for one source event. It is responsible for executing the +subscription selection set for that event and returning an `ExecutionResult`, +or a promise for one. + +By default, `mapSourceToResponseEvent()` uses `executeSubscriptionEvent()` as +that executor. Pass a custom executor when each event needs extra context, +instrumentation, or execution policy. + +```js +import { + createSourceEventStream, + executeSubscriptionEvent, + mapSourceToResponseEvent, + validateSubscriptionArgs, +} from 'graphql'; + +const validatedArgs = validateSubscriptionArgs({ + schema, + document, + contextValue, + variableValues, + operationName, +}); + +if (!('schema' in validatedArgs)) { + return { errors: validatedArgs }; +} + +const source = await createSourceEventStream(validatedArgs); + +if (source == null || typeof source[Symbol.asyncIterator] !== 'function') { + return source; +} + +const stream = mapSourceToResponseEvent( + validatedArgs, + source, + (validatedEventArgs) => + executeSubscriptionEvent({ + ...validatedEventArgs, + contextValue: { + requestContext: validatedEventArgs.contextValue, + sourceEventStartedAt: Date.now(), + }, + }), +); +``` + +## Choosing the right API + +- Use `execute()` or `subscribe()` when the built-in execution behavior is + enough. +- Use [GraphQL Harness](/docs/graphql-harness) when you want to replace the + parse, validate, execute, or subscribe phase used by `graphql()`. +- Use the helpers on this page when you are building a custom execution or + subscription function and need the same validated argument objects that + GraphQL.js uses internally. + +These helpers do not replace request parsing, validation, transport framing, or +response serialization. They give hosts precise control over the execution +boundary while keeping GraphQL.js's argument normalization, variable coercion, +fragment handling, and default resolver behavior intact. diff --git a/website/pages/docs/custom-scalars.mdx b/website/pages/docs/custom-scalars.mdx index 043a729b29..d50c8fbf6c 100644 --- a/website/pages/docs/custom-scalars.mdx +++ b/website/pages/docs/custom-scalars.mdx @@ -2,6 +2,8 @@ title: Using Custom Scalars --- +import { Callout } from 'nextra/components'; + # Custom Scalars: When and How to Use Them In GraphQL, scalar types represent primitive data like strings, numbers, and booleans. @@ -21,6 +23,14 @@ Here’s a simple example of a custom scalar that handles date-time strings: ```js import { GraphQLScalarType, Kind } from 'graphql'; +function parseDate(value) { + const date = new Date(value); + if (isNaN(date.getTime())) { + throw new TypeError(`DateTime cannot represent an invalid date: ${value}`); + } + return date; +} + const DateTime = new GraphQLScalarType({ name: 'DateTime', description: 'An ISO-8601 encoded UTC date string.', @@ -126,6 +136,65 @@ const DateTime = new GraphQLScalarType({ These functions give you full control over validation and data flow. +## v17 scalar method names + + + GraphQL.js v16 uses `serialize`, `parseValue`, and `parseLiteral`. GraphQL.js + v17 keeps those names as deprecated aliases and introduces names that match + the GraphQL specification's coercion terminology. + + +When writing code that only needs to run on v17 and newer, prefer: + +- `coerceOutputValue`: Result coercion for values returned by resolvers. +- `coerceInputValue`: Input coercion for variable values. +- `coerceInputLiteral`: Input coercion for constant GraphQL literals. +- `valueToLiteral`: Conversion from an external input value to a GraphQL + literal. + +During execution, GraphQL.js replaces variables before calling +`coerceInputLiteral()`. If tooling or application code calls scalar literal +coercion directly outside execution, it must call `replaceVariables()` itself +before passing the literal to `coerceInputLiteral()`. + +```js +import { GraphQLScalarType, Kind } from 'graphql'; + +const DateTime = new GraphQLScalarType({ + name: 'DateTime', + description: 'An ISO-8601 encoded UTC date string.', + + coerceOutputValue(value) { + if (!(value instanceof Date)) { + throw new TypeError('DateTime can only serialize Date instances'); + } + return value.toISOString(); + }, + + coerceInputValue(value) { + return parseDate(value); + }, + + coerceInputLiteral(ast) { + if (ast.kind !== Kind.STRING) { + throw new TypeError( + `DateTime can only parse string values, but got: ${ast.kind}`, + ); + } + return parseDate(ast.value); + }, + + valueToLiteral(value) { + if (typeof value === 'string') { + return { kind: Kind.STRING, value, block: false }; + } + }, +}); +``` + +If you need one scalar definition that supports both v16 and v17, keep the +v16 method names until your minimum GraphQL.js version is v17. + ## Learn more -- [Custom Scalars: Best Practices and Testing](./advanced-custom-scalars): Dive deeper into validation, testing, and building production-grade custom scalars. \ No newline at end of file +- [Custom Scalars: Best Practices and Testing](./advanced-custom-scalars): Dive deeper into validation, testing, and building production-grade custom scalars. diff --git a/website/pages/docs/defer-stream.mdx b/website/pages/docs/defer-stream.mdx index d52296f0a5..5252fa26a5 100644 --- a/website/pages/docs/defer-stream.mdx +++ b/website/pages/docs/defer-stream.mdx @@ -1,23 +1,35 @@ --- -title: Enabling Defer & Stream +title: Enabling Defer and Stream +sidebarTitle: Defer and Stream --- +import { Callout } from 'nextra/components'; + # Enabling Defer and Stream -import { Callout } from 'nextra/components' - - - These exports are only available in v17 and beyond. + + `@defer`, `@stream`, and `experimentalExecuteIncrementally()` are available + in GraphQL.js v17 and newer. Incremental delivery is pending GraphQL + specification work. -The `@defer` and `@stream` directives are not enabled by default. -In order to use these directives, you must add them to your GraphQL Schema and -use the `experimentalExecuteIncrementally` function instead of `execute`. +`@defer` and `@stream` allow a GraphQL operation to produce an initial result +and later incremental payloads. This is useful when part of a response is slow, +large, or naturally delivered over time. + +GraphQL.js keeps this feature explicit. You must add the directives to the +schema and use the experimental executor. + +## Add the directives to your schema + +If the `directives` option is passed to `GraphQLSchema`, the default directive +list is replaced. Include `specifiedDirectives` when adding experimental +directives. ```js import { - GraphQLSchema, GraphQLDeferDirective, + GraphQLSchema, GraphQLStreamDirective, specifiedDirectives, } from 'graphql'; @@ -30,11 +42,163 @@ const schema = new GraphQLSchema({ GraphQLStreamDirective, ], }); +``` + +Once a schema includes `@defer` or `@stream`, execute operations against that +schema with `experimentalExecuteIncrementally()`. `execute()` is the +single-result executor and will reject schemas that contain the experimental +incremental directives. + +## Execute incrementally + +```js +import { experimentalExecuteIncrementally, parse } from 'graphql'; + +const document = parse(` + query ProductPage { + product(id: "abc") { + id + name + ...Reviews @defer(label: "reviews") + } + } + + fragment Reviews on Product { + reviews { + body + rating + } + } +`); + +const result = await experimentalExecuteIncrementally({ + schema, + document, +}); +``` + +The result is either a normal `ExecutionResult` or an incremental result object. + +```js +if ('initialResult' in result) { + sendInitialPayload(result.initialResult); + + for await (const subsequentResult of result.subsequentResults) { + sendIncrementalPayload(subsequentResult); + } +} else { + sendSinglePayload(result); +} +``` + +GraphQL.js produces the execution results; your server transport is responsible +for serializing and delivering them to the client. + +## Transport framing guidance + +`experimentalExecuteIncrementally()` gives you result objects, not a wire +protocol. Pick a transport framing format that your clients already support and +test it end to end. + +- HTTP multipart responses for clients that support incremental patches. +- Server-sent events when your stack already uses event streams. +- WebSocket message streams for subscription-like transports. + +Keep transport concerns separate from execution concerns: validate operation +behavior first, then validate framing and client reassembly separately. + +## `@defer` -const result = experimentalExecuteIncrementally({ +`@defer` can be applied to fragment spreads and inline fragments. It defers the +fragment when `if` is `true` or omitted. + +```graphql +query ProductPage($includeReviews: Boolean! = true) { + product(id: "abc") { + id + name + ...Reviews @defer(if: $includeReviews, label: "reviews") + } +} +``` + +The `label` argument is optional, but labels must be unique for active +`@defer` and `@stream` usages in the operation. + +## `@stream` + +`@stream` can be applied to list fields. It sends `initialCount` items in the +initial result and streams later items in subsequent payloads. + +```graphql +query Feed { + feed(first: 100) @stream(initialCount: 10, label: "feed") { + id + title + } +} +``` + +`initialCount` is non-null and defaults to `0`. + +Resolvers may return normal iterables, promises, or async iterables for list +fields. Async iterables are especially useful with `@stream` because the +executor can complete list items as they become available. + +## Early execution + +`enableEarlyExecution` allows deferred work to begin before all non-deferred +work has completed. + +```js +const result = await experimentalExecuteIncrementally({ schema, document, + enableEarlyExecution: true, }); ``` -If the `directives` option is passed to `GraphQLSchema`, the default directives will not be included. `specifiedDirectives` must be passed to ensure all standard directives are added in addition to `defer` & `stream`. +This can reduce total latency for expensive deferred sections, but it can also +increase concurrent work. Measure before enabling it broadly. + +## Validation limits + +GraphQL.js validates the current incremental delivery rules: + +- `@defer` and `@stream` are not supported on subscription operations. +- `@stream` must be used on list fields. +- `@stream(initialCount:)` must be non-null. +- Active `@defer` and `@stream` labels must be unique. +- Root field usage must follow the current proposal rules. +- Multiple active `@stream` instances cannot target the same field instance. + +Those checks are exposed through the validation rules +`DeferStreamDirectiveLabelRule`, `DeferStreamDirectiveOnRootFieldRule`, +`DeferStreamDirectiveOnValidOperationsRule`, and +`StreamDirectiveOnListFieldRule`. + +If a fragment is shared between query and subscription operations, use the +directive `if` argument to disable incremental behavior in the subscription. + +```graphql +subscription Events($incremental: Boolean! = false) { + event { + ...EventFields @defer(if: $incremental) + } +} +``` + +## Cancellation + +Incremental execution accepts `abortSignal`. Aborting stops new payload +production and attempts to close async iterators. + +```js +const controller = new AbortController(); + +const result = await experimentalExecuteIncrementally({ + schema, + document, + abortSignal: controller.signal, +}); +``` diff --git a/website/pages/docs/directives-on-directives.mdx b/website/pages/docs/directives-on-directives.mdx new file mode 100644 index 0000000000..6407175531 --- /dev/null +++ b/website/pages/docs/directives-on-directives.mdx @@ -0,0 +1,130 @@ +--- +title: Directives on Directive Definitions +sidebarTitle: Directives on Directives +--- + +import { Callout } from 'nextra/components'; + +# Directives on Directive Definitions + + + GraphQL.js supports directives applied to directive definitions, directive + extensions, and directive deprecation metadata through experimental APIs. + + +When the experiment is enabled, GraphQL directives can be applied to directive +definitions. This is the SDL shape introduced by the directives-on-directives +proposal: + +```graphql +directive @tag(name: String!) on DIRECTIVE_DEFINITION + +directive @cacheControl(maxAge: Int) @tag(name: "runtime") on FIELD_DEFINITION +``` + +The directive location is `DIRECTIVE_DEFINITION`. + +```js +import { DirectiveLocation } from 'graphql'; + +DirectiveLocation.DIRECTIVE_DEFINITION; +``` + +## Parser surface + +Directive definition directives are represented on the AST: + +- `DirectiveDefinitionNode.directives` +- `DirectiveExtensionNode.directives` +- `Kind.DIRECTIVE_EXTENSION` + +Parsing this syntax is controlled by +`experimentalDirectivesOnDirectiveDefinitions`. + +```js +import { parse } from 'graphql'; + +const document = parse(source, { + experimentalDirectivesOnDirectiveDefinitions: true, +}); +``` + +Directive extensions use the same option: + +```graphql +extend directive @cacheControl @tag(name: "performance") +``` + +## Runtime schema surface + +GraphQL.js does not add a generic `GraphQLDirective.directives` property. The +applied directives remain available through the AST nodes: + +- `GraphQLDirective.astNode?.directives` +- `GraphQLDirective.extensionASTNodes` + +GraphQL.js does derive directive deprecation metadata from those AST nodes. +`GraphQLDirective` includes: + +- `deprecationReason` +- `extensionASTNodes` + +```js +const directive = schema.getDirective('cacheControl'); + +directive.deprecationReason; +directive.astNode?.directives; +directive.extensionASTNodes; +``` + +## Deprecating custom directives + +`@deprecated` can be used on directive definitions. The built-in +`GraphQLDeprecatedDirective` includes `DIRECTIVE_DEFINITION` in its locations. + +```graphql +directive @oldAuth @deprecated(reason: "Use @auth instead") on FIELD_DEFINITION +``` + +The introspection type `__Directive` includes: + +- `isDeprecated` +- `deprecationReason` + +`__Schema.directives` accepts `includeDeprecated: Boolean! = false` when +directive deprecation support is present. + +```graphql +query DeprecatedDirectives { + __schema { + directives(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } +} +``` + +## Directive extensions + +Directive extensions can attach deprecation metadata to a directive defined in +another document: + +```graphql +directive @oldAuth on FIELD_DEFINITION + +extend directive @oldAuth @deprecated(reason: "Use @auth instead") +``` + +When a schema is extended, GraphQL.js preserves directive extension AST nodes on +`GraphQLDirective.extensionASTNodes` and uses them when computing +`deprecationReason`. + +## Validation + +`KnownDirectivesRule` understands `DIRECTIVE_DEFINITION`, so a directive applied +to a directive definition must itself be declared for that location. + +`UniqueDirectivesPerLocationRule` also treats a directive definition and its +extensions as one directive location for non-repeatable directive uniqueness. diff --git a/website/pages/docs/execution-hooks.mdx b/website/pages/docs/execution-hooks.mdx new file mode 100644 index 0000000000..f14b5c11c7 --- /dev/null +++ b/website/pages/docs/execution-hooks.mdx @@ -0,0 +1,192 @@ +--- +title: Execution Hooks and Async Cleanup +sidebarTitle: Execution Hooks +--- + +import { Callout } from 'nextra/components'; + +# Execution Hooks and Async Cleanup + + + Execution hooks are experimental in GraphQL.js v17. The current hook surface + may change before it becomes stable. + + +GraphQL execution can stop producing a result before every piece of async work +started by execution has settled. This is most visible with cancellation: +JavaScript does not have preemptive cancellation for promises. An +`AbortSignal` is cooperative, so it only helps when downstream async functions +accept the signal and honor it. + +GraphQL.js cannot force arbitrary JavaScript work to stop. What it can do is +track async work it knows about and tell the host when that tracked work has +finished. The `asyncWorkFinished` hook is that boundary. + +## `asyncWorkFinished` + +Pass hooks through `execute()`, `subscribe()`, `graphql()`, or +`experimentalExecuteIncrementally()`. + +```js +import { execute } from 'graphql'; + +await execute({ + schema, + document, + hooks: { + asyncWorkFinished({ validatedExecutionArgs }) { + logger.debug( + { + operationName: validatedExecutionArgs.operation.name?.value, + }, + 'GraphQL async work finished', + ); + }, + }, +}); +``` + +The hook fires after GraphQL.js has stopped producing payloads and all tracked +async execution work has settled. It is useful when a host needs to observe +work that continues after the response boundary, such as async iterator +cleanup, cleanup after an aborted execution, or resolver-started work that was +explicitly registered for tracking. + +## What GraphQL.js can track + +GraphQL.js tracks async work that is part of execution and work registered +through resolver info helpers. It does not automatically know about arbitrary +background work started by your application. + +Use `promiseAll()` when a resolver awaits several async branches and you want +rejected branches to be tracked consistently: + +```js +async resolve(_source, args, _context, info) { + const { promiseAll } = info.getAsyncHelpers(); + + const [user, permissions] = await promiseAll([ + loadUser(args.id), + loadPermissions(args.id), + ]); + + return { user, permissions }; +} +``` + +Use `track()` for async cleanup or side effects that a resolver starts but does +not return or await: + +```js +async resolve(_source, _args, _context, info) { + const { track } = info.getAsyncHelpers(); + const cleanup = closeResourceLater().catch(() => undefined); + + track([cleanup]); + + return 'ok'; +} +``` + +If the resolver is already awaiting or returning the work, do that normally. +Use `track()` only for work that would otherwise be invisible to GraphQL.js. + +## Logging and telemetry + +For many hosts, the hook is an observability point. It can record how long +tracked async cleanup continued after execution started or after a response was +produced. + +```js +const startedAt = Date.now(); + +await execute({ + schema, + document, + hooks: { + asyncWorkFinished({ validatedExecutionArgs }) { + metrics.record('graphql.async_work_finished', { + operationName: validatedExecutionArgs.operation.name?.value, + elapsedMs: Date.now() - startedAt, + }); + }, + }, +}); +``` + +## Waiting before returning a result + +Some hosts prefer not to return the GraphQL result to the transport until +tracked async cleanup has finished. For example, a test harness may want the +operation to leave no pending execution work before assertions run. + +```js +import { + executeRootSelectionSet, + validateExecutionArgs, +} from 'graphql'; + +async function executeAndWaitForAsyncWork(args) { + const validatedArgs = validateExecutionArgs(args); + + if (!('schema' in validatedArgs)) { + return { errors: validatedArgs }; + } + + let markAsyncWorkFinished; + const asyncWorkFinished = new Promise((resolve) => { + markAsyncWorkFinished = resolve; + }); + + const result = executeRootSelectionSet({ + ...validatedArgs, + hooks: { + ...validatedArgs.hooks, + asyncWorkFinished(info) { + validatedArgs.hooks?.asyncWorkFinished?.(info); + markAsyncWorkFinished(); + }, + }, + }); + + const executionResult = await result; + await asyncWorkFinished; + + return executionResult; +} +``` + +This pattern trades response latency for a stronger lifecycle boundary. It is +usually better for tests, controlled batch jobs, and framework internals than +for latency-sensitive HTTP handlers. + +## Host cleanup + +The hook can also release host-owned bookkeeping that should stay alive until +GraphQL.js has finished tracked async work. + +```js +await execute({ + schema, + document, + contextValue: requestContext, + hooks: { + asyncWorkFinished({ validatedExecutionArgs }) { + requestRegistry.delete(validatedExecutionArgs); + requestContext.loaderCache.clear(); + }, + }, +}); +``` + +## Aborts and incremental delivery + +Hooks are especially useful when execution is aborted or incremental delivery is +used: + +- Abort may stop payload production before cleanup is complete. +- Async iterator `return()` paths can continue after the response boundary. +- Deferred work can leave short-lived cleanup tasks after the final patch. + +Pair hooks with [Handling Abort Signals](/docs/abort-signals) for timeout and +cancellation instrumentation. diff --git a/website/pages/docs/experimental-specification-features.mdx b/website/pages/docs/experimental-specification-features.mdx new file mode 100644 index 0000000000..165f551aa2 --- /dev/null +++ b/website/pages/docs/experimental-specification-features.mdx @@ -0,0 +1,55 @@ +--- +title: Experimental Specification Features +sidebarTitle: Experimental Specification Features +--- + +# Experimental Specification Features + +GraphQL.js v17 beta supports several GraphQL specification proposals. This page +lists experimental features available in the v17 beta line; not every feature +listed here is new to v17. These features are intentionally explicit: syntax +usually requires a parser option or schema directive, and execution behavior +usually requires a specific executor. + +GraphQL.js-specific runtime APIs, such as abort signals, execution hooks, and +the harness API, are documented separately because they are not GraphQL language +features. + +## Incremental delivery + +Incremental delivery adds `@defer`, `@stream`, and execution results with +initial and subsequent payloads. + +The relevant GraphQL.js APIs are `GraphQLDeferDirective`, +`GraphQLStreamDirective`, and `experimentalExecuteIncrementally()`. +GraphQL.js does not include the directives in `specifiedDirectives`; a schema +that uses them should add them explicitly and execute matching operations with +`experimentalExecuteIncrementally()`. If you add them programmatically with the +`GraphQLSchema` `directives` config property, include `specifiedDirectives` as +well. Providing `directives` replaces the default directive list, whose default +value is `specifiedDirectives`. + +See [Defer and Stream](/docs/defer-stream). + +## Fragment arguments + +Fragment arguments add fragment-local variable definitions and fragment-spread +arguments. GraphQL.js exposes the syntax through the +`experimentalFragmentArguments` parser option. The AST surface includes +`FragmentArgumentNode`, and execution supports the resulting values. + +See [Fragment Arguments](/docs/fragment-arguments). + +## Directives on directive definitions + +The directives-on-directives proposal adds `DIRECTIVE_DEFINITION` as a +directive location and allows directive metadata to be attached to directive +definitions and directive extensions. + +GraphQL.js exposes the syntax through +`experimentalDirectivesOnDirectiveDefinitions`. The AST surface includes +`DirectiveLocation.DIRECTIVE_DEFINITION` and `DirectiveExtensionNode`. +Directive deprecation metadata is surfaced on `GraphQLDirective`, +introspection, and schema printing. + +See [Directives on Directives](/docs/directives-on-directives). diff --git a/website/pages/docs/fragment-arguments.mdx b/website/pages/docs/fragment-arguments.mdx new file mode 100644 index 0000000000..2d4c1205ec --- /dev/null +++ b/website/pages/docs/fragment-arguments.mdx @@ -0,0 +1,140 @@ +--- +title: Fragment Arguments +sidebarTitle: Fragment Arguments +--- + +import { Callout } from 'nextra/components'; + +# Fragment Arguments + + + Fragment arguments are available behind an experimental parser option in + GraphQL.js v17 and newer. They are pending GraphQL specification work. + + +GraphQL operation variables are defined at the operation level. That works for +many documents, but reusable fragments sometimes need local parameters. Fragment +arguments let a fragment define its own variables and let each fragment spread +provide values for those variables. + +The feature is also called fragment variables because the fragment-level +parameters are conceptually "variables" scoped to that fragment. They are passed +as arguments on the fragment spread, using syntax similar to field arguments. + +## Changed from legacy fragment variables + +Older GraphQL.js versions had an experimental `allowLegacyFragmentVariables` +parser option that added only syntax support for fragment definitions with +variable-like syntax. That v16 experiment was parser-only: it could parse the +syntax, but GraphQL.js did not apply fragment variable values during execution. + +`allowLegacyFragmentVariables` was removed in favor of the more complete +`experimentalFragmentArguments` feature. `experimentalFragmentArguments` +includes parser support and runtime execution support, including coercion and +default value handling through the execution layer. + +## Syntax + +```graphql +query Profile($viewerID: ID!) { + node(id: $viewerID) { + ...UserCard(size: 96) + } +} + +fragment UserCard($size: Int = 48) on User { + id + name + avatar(size: $size) +} +``` + +`$viewerID` is an operation variable. `$size` is local to `UserCard`. + +Different spreads can call the same fragment with different values: + +```graphql +query Team { + lead { + ...UserCard(size: 96) + } + members { + ...UserCard(size: 32) + } +} +``` + +## Enabling fragment arguments + +The parser rejects fragment arguments unless the experiment is enabled. + +```js +import { parse } from 'graphql'; + +const document = parse(source, { + experimentalFragmentArguments: true, +}); +``` + +`graphql()` in v17 accepts parse options too, so simple hosts can pass the same +option through the top-level API: + +```js +import { graphql } from 'graphql'; + +const result = await graphql({ + schema, + source, + experimentalFragmentArguments: true, +}); +``` + +## Runtime values and scope + +Fragment arguments are coerced using the fragment definition's variable +definitions. If a fragment argument has a default value, the default is applied +when a spread omits that argument. + +```graphql +fragment UserCard($size: Int = 48) on User { + avatar(size: $size) +} + +query { + viewer { + ...UserCard + } +} +``` + +When an operation variable and a fragment argument share a name, the fragment +argument is used inside that fragment. Avoid name reuse unless it is deliberate; +distinct names are easier for humans and tools to follow. + +Resolvers do not need a separate API for fragment arguments. By the time a +field resolver runs, GraphQL.js has applied the fragment argument values to the +field arguments, directives, and nested fragment spreads that reference them. + +## AST and tooling changes + +When enabled, GraphQL.js adds: + +- `FragmentDefinitionNode.variableDefinitions`. +- `FragmentSpreadNode.arguments`. +- `FragmentArgumentNode`. +- `Kind.FRAGMENT_ARGUMENT`. +- The `FRAGMENT_VARIABLE_DEFINITION` directive location. + +The printer and visitor understand these nodes in v17. Tooling that uses custom +visitors should include the new node kind when it needs to inspect or transform +fragment spread arguments. + +```js +import { Kind, visit } from 'graphql'; + +visit(document, { + [Kind.FRAGMENT_ARGUMENT](node) { + console.log(node.name.value); + }, +}); +``` diff --git a/website/pages/docs/getting-started.mdx b/website/pages/docs/getting-started.mdx index 0a12557526..6f7b089836 100644 --- a/website/pages/docs/getting-started.mdx +++ b/website/pages/docs/getting-started.mdx @@ -24,14 +24,32 @@ GraphQL.js v16 is the current stable release. v17 is available as an alpha for early testing and feedback. The alpha may change and should not be used in production. +## Setting Up Your Project + To create a new project and install the latest stable release (v16) in your current directory: ```sh npm2yarn -npm init +npm init -y npm install graphql --save ``` +After running these commands, you'll have: +- `package.json` - your project configuration file +- `node_modules/` - directory where npm packages are installed + +Next, you need to configure your project to support ES6 import/export syntax. +Update your `package.json` to include `"type": "module"`: + +```json +{ + "type": "module", + "name": "graphql-starter", + "version": "1.0.0", + ... +} +``` + To try the v17 alpha instead: ```sh npm2yarn @@ -40,7 +58,7 @@ npm install graphql@alpha --save ## Writing Code -To handle GraphQL queries, we need a schema that defines the `Query` type, and we need an API root with a function called a "resolver" for each API endpoint. For an API that just returns "Hello world!", we can put this code in a file named `server.js`: +To handle GraphQL queries, we need a schema that defines the `Query` type, and we need an API root with a function called a "resolver" for each API endpoint. For an API that just returns "Hello world!", create a file named `server.js` in your project root (the same directory as your `package.json`): @@ -113,4 +131,4 @@ You should see the GraphQL response printed out: Congratulations - you just executed a GraphQL query! -For practical applications, you'll probably want to run GraphQL queries from an API server, rather than executing GraphQL with a command line tool. To use GraphQL for an API server over HTTP, check out [Running an Express GraphQL Server](./running-an-express-graphql-server). \ No newline at end of file +For practical applications, you'll probably want to run GraphQL queries from an API server, rather than executing GraphQL with a command line tool. To use GraphQL for an API server over HTTP, check out [Running an Express GraphQL Server](./running-an-express-graphql-server). diff --git a/website/pages/docs/graphql-clients.mdx b/website/pages/docs/graphql-clients.mdx index e1c56e1fa7..5c34cd8068 100644 --- a/website/pages/docs/graphql-clients.mdx +++ b/website/pages/docs/graphql-clients.mdx @@ -6,13 +6,33 @@ title: GraphQL Clients Since a GraphQL API has more underlying structure than a REST API, there are more powerful clients like [Relay](https://facebook.github.io/relay/) which can automatically handle batching, caching, and other features. But you don't need a complex client to call a GraphQL server. With `graphql-http`, you can just send an HTTP POST request to the endpoint you mounted your GraphQL server on, passing the GraphQL query as the `query` field in a JSON payload. -For example, let's say we mounted a GraphQL server on http://localhost:4000/graphql as in the example code for [running an Express GraphQL server](./running-an-express-graphql-server), and we want to send the GraphQL query `{ hello }`. We can do this from the command line with `curl`. If you paste this into a terminal: +For example, let's say we mounted a GraphQL server on http://localhost:4000/graphql as in the example code for [running an Express GraphQL server](./running-an-express-graphql-server), and we want to send the GraphQL query `{ hello }`. We can do this from the command line with `curl`. + +**On Linux or macOS:** ```bash curl -X POST \ --H "Content-Type: application/json" \ --d '{"query": "{ hello }"}' \ -http://localhost:4000/graphql + -H "Content-Type: application/json" \ + -d '{"query": "{ hello }"}' \ + http://localhost:4000/graphql +``` + +**On Windows (Command Prompt):** + +```bash +curl -X POST ^ + -H "Content-Type: application/json" ^ + -d "{\"query\": \"{ hello }\"}" ^ + http://localhost:4000/graphql +``` + +**On Windows (PowerShell):** + +```powershell +curl.exe -X POST ` + -H "Content-Type: application/json" ` + -d '{"query": "{ hello }"}' ` + http://localhost:4000/graphql ``` You should see the output returned as JSON: @@ -21,9 +41,19 @@ You should see the output returned as JSON: { "data": { "hello": "Hello world!" } } ``` -If you prefer to use a graphical user interface to send a test query, you can use clients such as [GraphiQL](https://github.com/graphql/graphiql), [Insomnia](https://github.com/getinsomnia/insomnia), and [Postman](https://www.postman.com/product/graphql-client/). +If you prefer to use a graphical user interface to send a test query, use a +GraphQL client such as [GraphiQL](https://github.com/graphql/graphiql), +[Insomnia](https://github.com/getinsomnia/insomnia), or +[Postman](https://www.postman.com/product/graphql-client/). The +[Running an Express GraphQL Server](./running-an-express-graphql-server) guide +shows how to serve GraphiQL locally with +[Ruru](https://github.com/graphile/crystal/blob/main/grafast/ruru/README.md). -It's also simple to send GraphQL from the browser. Open up http://localhost:4000/graphql, open a developer console, and paste in: +## Using Fetch from the Browser + +It's also simple to send GraphQL from the browser. Open +[http://localhost:4000/graphql](http://localhost:4000/graphql), open a developer +console, and paste in the following code: ```js fetch('/graphql', { @@ -38,12 +68,21 @@ fetch('/graphql', { .then((data) => console.log('data returned:', data)); ``` -You should see the data returned, logged in the console: +You should see the GraphQL response logged in the console: ```text -data returned: Object { hello: "Hello world!" } +data returned: { data: { hello: "Hello world!" } } ``` +This works because the developer console and the `/graphql` endpoint are both +on `http://localhost:4000`, so `fetch('/graphql', ...)` is same-origin. The +browser may show an error when you visit the endpoint directly because that +navigation does not send a GraphQL operation yet, but the developer console is +still on the right origin. If you run the same code from a page on a different +origin, use the full endpoint URL and make sure the GraphQL server is configured +for CORS. Otherwise, use `curl`, a GraphQL IDE, or an HTTP client such as +Insomnia or Postman. + In this example, the query was just a hardcoded string. As your application becomes more complex, and you add GraphQL endpoints that take arguments as described in [Passing Arguments](./passing-arguments), you will want to construct GraphQL queries using variables in client code. You can do this by including a keyword prefixed with a dollar sign in the query, and passing an extra `variables` field on the payload. For example, let's say you're running the example server from [Passing Arguments](./passing-arguments) that has a schema of diff --git a/website/pages/docs/graphql-harness.mdx b/website/pages/docs/graphql-harness.mdx new file mode 100644 index 0000000000..7d44f743c2 --- /dev/null +++ b/website/pages/docs/graphql-harness.mdx @@ -0,0 +1,154 @@ +--- +title: GraphQL Harness +sidebarTitle: GraphQL Harness +--- + +import { Callout } from 'nextra/components'; + +# GraphQL Harness + + + `GraphQLHarness` is new in GraphQL.js v17. It customizes the phases used by + `graphql()` and `graphqlSync()`. + + +`graphql()` is the convenience entry point that parses, validates, and executes +a GraphQL operation. In v17, those phases are represented by a harness: + +```ts +type GraphQLHarness = { + parse: GraphQLParseFn; + validate: GraphQLValidateFn; + execute: GraphQLExecuteFn; + subscribe: GraphQLSubscribeFn; +}; +``` + +`defaultHarness` is the built-in harness used by `graphql()` and +`graphqlSync()`. + +```js +import { defaultHarness, graphql } from 'graphql'; + +const result = await graphql({ + schema, + source, + harness: defaultHarness, +}); +``` + +## Why this exists + +The harness is a host integration API modeled after +[Envelop](https://the-guild.dev/graphql/envelop), The Guild's GraphQL plugin +system. Envelop showed that many servers need to customize the same request +phases: parsing, validation, execution, subscription execution, and the +cross-cutting behavior around those phases. + +GraphQL.js remains a reference implementation, not a full plugin framework. +The harness brings the broader phase types used by that ecosystem closer to the +reference implementation so frameworks and plugin systems can share a common +shape. For example, `GraphQLParseFn` can return a `DocumentNode` or a promise +for a `DocumentNode`, even though the built-in GraphQL.js `parse()` function is +synchronous. + +For application servers, prefer Envelop or a framework built on Envelop over +using a raw `GraphQLHarness` directly. The goal is that frameworks can accept a +custom harness, and plugin systems that customize these phases can interoperate +without each framework inventing a different integration surface. + +## What can be customized + +Each harness function receives the same arguments as the corresponding +GraphQL.js phase. The difference is that a harness phase may finish immediately +or by returning a promise: + +```ts +type MaybePromise = T | Promise; + +type GraphQLParseFn = ( + source: string | Source, + options?: ParseOptions, +) => MaybePromise; + +type GraphQLValidateFn = ( + schema: GraphQLSchema, + documentAST: DocumentNode, + rules?: readonly ValidationRule[], + options?: ValidationOptions, +) => MaybePromise; + +type GraphQLExecuteFn = (args: ExecutionArgs) => MaybePromise; + +type GraphQLSubscribeFn = ( + args: ExecutionArgs, +) => MaybePromise< + ExecutionResult | AsyncGenerator +>; +``` + +Any harness phase may return synchronously or asynchronously. `graphqlSync()` +still requires every phase and resolver it reaches to complete synchronously. +`GraphQLExecuteFn` deliberately returns only `ExecutionResult`; it does not +include the experimental incremental delivery result type. + +## Cached documents + +A host that has a trusted document cache can replace the parse phase while +keeping the default validation and execution behavior. + +```js +import { defaultHarness, graphql } from 'graphql'; + +const harness = { + ...defaultHarness, + parse(source, options) { + const cached = documents.get(String(source)); + return cached ?? defaultHarness.parse(source, options); + }, +}; + +const result = await graphql({ + schema, + source, + variableValues, + operationName, + harness, +}); +``` + +## External validation + +A host can also replace validation. This is useful for persisted operation +registries that validate at build time and return stored validation results at +runtime. + +```js +import { defaultHarness, graphql } from 'graphql'; + +const harness = { + ...defaultHarness, + async validate(schema, document, rules, options) { + const cached = await registry.getValidationResult(document, schema); + return cached ?? defaultHarness.validate(schema, document, rules, options); + }, +}; + +const result = await graphql({ + schema, + source, + harness, +}); +``` + +## Relationship to incremental delivery + +`graphql()` remains a single-result operation pipeline. A harness does not make +`graphql()` return incremental delivery payloads, and the harness `execute` +function has the same single-result contract. + +Operations that use `@defer` or `@stream` should use +`experimentalExecuteIncrementally()` after parsing and validation. See +[Advanced Execution Pipelines](/docs/advanced-execution-pipelines) for the +lower-level execution APIs and [Defer and Stream](/docs/defer-stream) for the +incremental result shape. diff --git a/website/pages/docs/index.mdx b/website/pages/docs/index.mdx index 3b45c15e4b..9f47a06b32 100644 --- a/website/pages/docs/index.mdx +++ b/website/pages/docs/index.mdx @@ -3,17 +3,22 @@ title: Overview sidebarTitle: Overview --- -GraphQL.js is the official JavaScript implementation of the -[GraphQL Specification](https://spec.graphql.org/draft/). It provides the core building blocks -for constructing GraphQL servers, clients, tools, and utilities in JavaScript and TypeScript. +# GraphQL.js Documentation -This documentation site is for developers who want to: +GraphQL.js is the official JavaScript implementation of the GraphQL +specification. It provides the parser, validator, executor, type system, and +utilities used to build GraphQL servers, clients, tools, and schema workflows in +JavaScript and TypeScript. -- Understand how GraphQL works -- Build a GraphQL API using GraphQL.js -- Extend, customize, or introspect GraphQL systems -- Learn best practices for using GraphQL.js in production +## Version reference -Whether you're writing your own server, building a GraphQL clients, or creating tools -that work with GraphQL, this site guides you through core concepts, APIs, and -advanced use cases of GraphQL.js. +| Version area | Start here | +| --- | --- | +| Stable v16 API | [v16 API reference](/api-v16/graphql) | +| v17 beta API | [v17 API reference](/api-v17/graphql) | +| v16 to v17 changes | [What changed in GraphQL.js v17](/upgrade-guides/v16-v17) | +| v17 specification experiments | [Experimental Specification Features](/docs/experimental-specification-features) | +| v17 runtime features | [GraphQL Harness](/docs/graphql-harness), [Abort Signals](/docs/abort-signals), [Execution Hooks](/docs/execution-hooks) | + +The guides in this section describe GraphQL concepts and GraphQL.js behavior. +The API sections document the public exports by package module. diff --git a/website/pages/docs/mutations-and-input-types.mdx b/website/pages/docs/mutations-and-input-types.mdx index 8948c69b34..f659d2f6f9 100644 --- a/website/pages/docs/mutations-and-input-types.mdx +++ b/website/pages/docs/mutations-and-input-types.mdx @@ -201,6 +201,41 @@ Input types can't have fields that are other objects, only basic scalar types, l Naming input types with `Input` on the end is a useful convention, because you will often want both an input type and an output type that are slightly different for a single conceptual object. +Input object fields can also define defaults for omitted fields. In SDL, use +the same `= value` syntax as field arguments: + +```graphql +input MessageInput { + content: String + author: String = "Anonymous" +} +``` + +When constructing input object types in code, GraphQL.js v16 uses +`defaultValue`: + +```js +const MessageInput = new GraphQLInputObjectType({ + name: 'MessageInput', + fields: { + content: { type: GraphQLString }, + author: { type: GraphQLString, defaultValue: 'Anonymous' }, + }, +}); +``` + +`defaultValue` is the legacy way to provide an already-coerced JavaScript +value. GraphQL.js v17 also supports the new `default: { value }` shape for the +raw JavaScript input value before coercion; on the built schema, that same case +is represented as `inputField.default.value`. Keeping the raw value lets +GraphQL.js validate the default and report it through introspection without +reconstructing the GraphQL literal from an already-coerced value, which fixes a +subtle source of incorrect default values for some schemas. If you already have +a GraphQL literal instead, use `default: { literal }`. GraphQL.js now uses the +literal form internally when building a schema from SDL. Invalid defaults are +reported by schema validation instead of waiting until a query happens to use +that field. + Here's some runnable code that implements this schema, keeping the data in memory: diff --git a/website/pages/docs/oneof-input-objects.mdx b/website/pages/docs/oneof-input-objects.mdx index 516da5b538..8ac5bd7ac9 100644 --- a/website/pages/docs/oneof-input-objects.mdx +++ b/website/pages/docs/oneof-input-objects.mdx @@ -1,8 +1,8 @@ --- -title: OneOf input objects +title: OneOf Input Objects --- -# OneOf input objects +# OneOf Input Objects import { Tabs } from 'nextra/components'; @@ -55,11 +55,10 @@ const Product = new GraphQLObjectType({ const ProductLocation = new GraphQLInputObjectType({ name: 'ProductLocation', - isOneOf: true, fields: { - aisleNumber: { type: GraphQLInt }, - shelfNumber: { type: GraphQLInt }, - positionOnShelf: { type: GraphQLInt }, + aisleNumber: { type: new GraphQLNonNull(GraphQLInt) }, + shelfNumber: { type: new GraphQLNonNull(GraphQLInt) }, + positionOnShelf: { type: new GraphQLNonNull(GraphQLInt) }, }, }); @@ -79,7 +78,7 @@ const schema = new GraphQLSchema({ fields: { product: { type: Product, - args: { by: { type: ProductSpecifier } }, + args: { by: { type: new GraphQLNonNull(ProductSpecifier) } }, }, }, }), @@ -91,3 +90,12 @@ const schema = new GraphQLSchema({ It doesn't matter whether you have 2 or more inputs here, all that matters is that your user will have to specify one, and only one, for this input to be valid. The values are not limited to scalars, lists and other input object types are also allowed. + +OneOf fields themselves must be nullable and must not define defaults. Nested +input object types can still have their own required fields, as shown by +`ProductLocation`. + +In GraphQL.js v17, OneOf coercion is stricter around defaults, unknown fields, +`undefined`, and values that are present before coercion but invalid after +coercion. Schemas that accidentally rely on ambiguous OneOf inputs should fail +earlier and with clearer errors. diff --git a/website/pages/docs/passing-arguments.mdx b/website/pages/docs/passing-arguments.mdx index 45eb087cd8..87e6a3f1f1 100644 --- a/website/pages/docs/passing-arguments.mdx +++ b/website/pages/docs/passing-arguments.mdx @@ -74,6 +74,53 @@ console.log('Running a GraphQL API server at localhost:4000/graphql'); The exclamation point in `Int!` indicates that `numDice` can't be null, which means we can skip a bit of validation logic to make our server code simpler. We can let `numSides` be null and assume that by default a die has 6 sides. +## Default argument values + +When an argument has a natural default, define that default in the schema +instead of filling it in inside every resolver. In SDL, add `= value` after the +argument type: + +```graphql +type Query { + rollDice(numDice: Int!, numSides: Int = 6): [Int] +} +``` + +When constructing a schema in code, GraphQL.js v16 uses `defaultValue`: + +```js +const rollDiceField = { + type: new GraphQLList(GraphQLFloat), + args: { + numDice: { type: new GraphQLNonNull(GraphQLInt) }, + numSides: { type: GraphQLInt, defaultValue: 6 }, + }, +}; +``` + +GraphQL.js v17 also supports the more explicit `default` shape for new code: + +```js +const rollDiceField = { + type: new GraphQLList(GraphQLFloat), + args: { + numDice: { type: new GraphQLNonNull(GraphQLInt) }, + numSides: { type: GraphQLInt, default: { value: 6 } }, + }, +}; +``` + +`defaultValue` is the legacy way to provide an already-coerced JavaScript +value. For new code in v17, use `default: { value }` with the raw JavaScript +input value before coercion; on the built schema, that same case is represented +as `argument.default.value`. Keeping the raw value lets GraphQL.js validate the +default and report it through introspection without reconstructing the GraphQL +literal from an already-coerced value, which fixes a subtle source of incorrect +default values for some schemas. If you already have a GraphQL +literal instead, use `default: { literal }`. GraphQL.js now uses the literal +form internally when building a schema from SDL. `defaultValue` still works in +v17, but is deprecated for removal in v18. + So far, our resolver functions took no arguments. When a resolver takes arguments, they are passed as one "args" object, as the first argument to the function. So rollDice could be implemented as: ```js diff --git a/website/pages/docs/schema-coordinates.mdx b/website/pages/docs/schema-coordinates.mdx new file mode 100644 index 0000000000..7a9042b94c --- /dev/null +++ b/website/pages/docs/schema-coordinates.mdx @@ -0,0 +1,128 @@ +--- +title: Schema Coordinates +sidebarTitle: Schema Coordinates +--- + +import { Callout } from 'nextra/components'; + +# Schema Coordinates + + + Schema coordinate helpers are available in GraphQL.js v17 and newer. They + implement the GraphQL schema-coordinate grammar and resolution semantics, + which have now been merged into the specification work. + + +A schema coordinate is a compact string that identifies a schema element. It is +useful when logs, registries, schema checks, documentation tools, or policy +systems need to refer to the same field, argument, directive, or enum value +without embedding a whole schema document. + +Examples: + +```text +Business +Business.name +Query.searchBusiness(criteria:) +SearchCriteria.filter +SearchFilter.OPEN_NOW +@private +@private(scope:) +``` + +## Resolving coordinates + +Use `resolveSchemaCoordinate(schema, coordinate)` when you want to resolve a +coordinate string directly against a schema: + +```js +import { buildSchema, resolveSchemaCoordinate } from 'graphql'; + +const schema = buildSchema(` + type Query { + searchBusiness(criteria: SearchCriteria!): [Business] + } + + input SearchCriteria { + name: String + filter: SearchFilter + } + + enum SearchFilter { + OPEN_NOW + DELIVERS_TAKEOUT + } + + type Business { + id: ID + name: String + } +`); + +const resolved = resolveSchemaCoordinate( + schema, + 'Query.searchBusiness(criteria:)', +); + +if (resolved?.kind === 'FieldArgument') { + console.log(resolved.fieldArgument.type.toString()); +} +``` + +The result is a discriminated object. Depending on the coordinate, the `kind` +can be `NamedType`, `Field`, `InputField`, `EnumValue`, `FieldArgument`, +`Directive`, or `DirectiveArgument`. + +If the final element does not exist, GraphQL.js returns `undefined`. If the +coordinate refers through a containing element that cannot exist, GraphQL.js +throws. For example, `Business.unknown` returns `undefined`, but +`Unknown.field` throws because `Unknown` is not a type in the schema. + +## Parsing coordinates + +Use `parseSchemaCoordinate()` when tooling needs the AST form before resolving: + +```js +import { parseSchemaCoordinate } from 'graphql'; + +const coordinateNode = parseSchemaCoordinate('@private(scope:)'); +``` + +GraphQL.js exposes coordinate AST node types and kinds: + +- `TypeCoordinateNode` +- `MemberCoordinateNode` +- `ArgumentCoordinateNode` +- `DirectiveCoordinateNode` +- `DirectiveArgumentCoordinateNode` +- `isSchemaCoordinateNode()` + +The coordinate parser uses a restricted lexer. It accepts coordinate syntax +only; it is not the same as parsing an executable GraphQL document or SDL +document. + +## Meta fields and introspection + +GraphQL.js can resolve meta fields and introspection schema elements: + +```js +resolveSchemaCoordinate(schema, 'Business.__typename'); +resolveSchemaCoordinate(schema, '__Directive.name'); +resolveSchemaCoordinate(schema, '__DirectiveLocation.INLINE_FRAGMENT'); +``` + +Meta-field resolution is implementation-defined rather than required for every +GraphQL server. Treat it as GraphQL.js behavior when building tooling that must +work across implementations. + +## Common uses + +- Store schema change approvals against coordinates such as + `Query.searchBusiness(criteria:)`. +- Attach ownership metadata to fields, directives, or enum values. +- Connect validation errors, usage metrics, and schema registry entries. +- Build documentation links without relying on display text. + +Schema coordinates identify schema elements; they do not describe executable +operation paths. For operation-specific paths, use GraphQL response paths such +as `["viewer", "name"]`. diff --git a/website/pages/docs/schema-evolution.mdx b/website/pages/docs/schema-evolution.mdx new file mode 100644 index 0000000000..5212f4a1d7 --- /dev/null +++ b/website/pages/docs/schema-evolution.mdx @@ -0,0 +1,177 @@ +--- +title: Schema Evolution +sidebarTitle: Schema Evolution +--- + +# Schema Evolution + +GraphQL schemas tend to evolve continuously. Fields are added, arguments are +introduced, enum values are deprecated, and object types move through product +life cycles. GraphQL.js provides schema comparison helpers so teams can make +those changes intentionally. + +Use these helpers in CI, release tooling, schema registries, or local migration +scripts. They compare two `GraphQLSchema` instances, not two SDL strings, so +you can build the schemas however your project normally does. + +## Comparing schemas in v16 + +GraphQL.js v16 exports two comparison helpers: + +```js +import { + buildSchema, + findBreakingChanges, + findDangerousChanges, +} from 'graphql'; + +const oldSchema = buildSchema(` + type Query { + product(id: ID!): Product + } + + type Product { + id: ID! + name: String + } +`); + +const newSchema = buildSchema(` + type Query { + product(id: ID!): Product + } + + type Product { + id: ID! + title: String + } +`); + +const breaking = findBreakingChanges(oldSchema, newSchema); +const dangerous = findDangerousChanges(oldSchema, newSchema); +``` + +`findBreakingChanges()` reports changes that can make existing operations fail, +such as removing a field, removing a type, removing an enum value, or adding a +required argument. + +`findDangerousChanges()` reports changes that are not always breaking but can +change client behavior, such as adding an enum value or adding an optional +argument. + +## Comparing schemas in v17 + +GraphQL.js v17 adds `findSchemaChanges()`. + +```js +import { findSchemaChanges } from 'graphql'; + +const changes = findSchemaChanges(oldSchema, newSchema); + +for (const change of changes) { + console.log(change.type, change.description); +} +``` + +`findSchemaChanges()` returns breaking, dangerous, and safe changes from one +call. The older `findBreakingChanges()` and `findDangerousChanges()` helpers +remain in v17, but they are deprecated for removal in v18. + +## Change Categories + +Breaking changes are changes that can make a previously valid operation invalid +or change the response shape in a way clients cannot safely ignore. Examples +include: + +- Removing a type, field, directive, argument, enum value, or union member. +- Adding a required argument or required input field. +- Changing a field or argument to an incompatible type. +- Removing an implemented interface from an object or interface. + +Dangerous changes may be safe for many clients, but they deserve review because +some clients can observe them. Examples include: + +- Adding an enum value. +- Adding a member to a union. +- Adding an optional argument or input field. +- Changing or removing a default value. + +Safe changes are additions or metadata changes that should not break existing +operations. In v17, `findSchemaChanges()` can report examples such as: + +- Adding a type, field, or directive. +- Adding an optional directive argument. +- Adding a directive location. +- Changing a description. +- Widening an argument or field type in a safe direction. + +## CI Gate Example + +This example fails a build on breaking changes and prints dangerous changes for +review. + +```js +import { + BreakingChangeType, + buildSchema, + findSchemaChanges, +} from 'graphql'; +import { readFile } from 'node:fs/promises'; + +const oldSchema = buildSchema(await readFile('schema-old.graphql', 'utf8')); +const newSchema = buildSchema(await readFile('schema-new.graphql', 'utf8')); + +const changes = findSchemaChanges(oldSchema, newSchema); +const breakingTypes = new Set(Object.values(BreakingChangeType)); +const breaking = changes.filter((change) => breakingTypes.has(change.type)); + +for (const change of changes) { + console.log(`${change.type}: ${change.description}`); +} + +if (breaking.length > 0) { + process.exitCode = 1; +} +``` + +In production tooling, prefer checking the exported change type objects instead +of matching strings. That lets TypeScript track the known set of change +categories. + +## Deprecate Before Removing + +The safest way to remove a field, enum value, argument, input field, or +directive is to deprecate it first, publish that deprecation, and wait until +usage is gone. + +```graphql +type Product { + name: String @deprecated(reason: "Use title.") + title: String +} +``` + +You can combine GraphQL.js schema comparison with operation analytics or a +schema registry: + +1. Add the replacement API. +2. Mark the old API as deprecated with a useful reason. +3. Monitor whether operations still use the deprecated API. +4. Remove the old API only after clients have migrated. +5. Let `findBreakingChanges()` or `findSchemaChanges()` confirm the removal is + intentional. + +## Working With Printed Schemas + +Schema comparison is most useful when the schemas are deterministic. If your +schema is constructed programmatically, print and sort it before storing a +baseline. + +```js +import { lexicographicSortSchema, printSchema } from 'graphql'; + +const sdl = printSchema(lexicographicSortSchema(schema)); +``` + +Use the sorted printed schema for human review, and use the actual +`GraphQLSchema` objects for `findBreakingChanges()` or `findSchemaChanges()`. diff --git a/website/pages/docs/subscriptions.mdx b/website/pages/docs/subscriptions.mdx index 7afeefed1b..fbe2261ddf 100644 --- a/website/pages/docs/subscriptions.mdx +++ b/website/pages/docs/subscriptions.mdx @@ -16,8 +16,9 @@ GraphQL.js implements the subscription execution algorithm, but it's up to you t ## How execution works -The core of subscription execution in GraphQL.js is the `subscribe` function. It works similarly to `graphql()`, but returns an `AsyncIterable` of execution results -instead of a single response: +The core of subscription execution in GraphQL.js is the `subscribe` function. +It starts the subscription and, when successful, returns an async iterable of +execution results instead of a single response: ```js import { subscribe, parse } from 'graphql'; @@ -29,14 +30,40 @@ const document = parse(` } `); -const iterator = await subscribe({ schema, document }); +const result = await subscribe({ schema, document }); -for await (const result of iterator) { - console.log(result); +if (isAsyncIterableObject(result)) { + for await (const payload of result) { + console.log(payload); + } +} else { + console.error(result.errors); +} + +function isAsyncIterableObject(value) { + return value != null && typeof value[Symbol.asyncIterator] === 'function'; } ``` -Each time your application publishes a new `messageSent` event, the iterator emits a new result. It's up to your transport layer to manage the connection and forward these updates to the client. +Each time your application publishes a new `messageSent` event, the iterator +emits a new result. It is up to your transport layer to manage the connection +and forward these updates to the client. + +## Why `subscribe()` is separate from `execute()` + +GraphQL.js keeps `execute()` and `subscribe()` as separate entry points for +historical and behavioral reasons. `execute()` is the older API for running one +execution and producing a single result. Subscriptions were added as a separate +GraphQL algorithm because starting a subscription has to resolve a source event +stream before executing the selection set for each event. + +The return type reflects that split. `execute()` returns one `ExecutionResult`, +or a promise for one. `subscribe()` returns an `ExecutionResult` when the +subscription cannot start, or an async stream of `ExecutionResult` values when +it can. In GraphQL.js v17, that return is a `PromiseOrValue`. It contains +either an `ExecutionResult` or an +`AsyncGenerator`, and `await subscribe(args)` still +works whether the implementation completes synchronously or asynchronously. ## When to use subscriptions diff --git a/website/pages/upgrade-guides/v16-v17.mdx b/website/pages/upgrade-guides/v16-v17.mdx index 29e44d2338..9cc129b875 100644 --- a/website/pages/upgrade-guides/v16-v17.mdx +++ b/website/pages/upgrade-guides/v16-v17.mdx @@ -1,221 +1,552 @@ --- -title: Upgrading from v16 to v17 +title: What changed in GraphQL.js v17 sidebarTitle: v16 to v17 --- -import { Tabs } from 'nextra/components'; -import { Callout } from 'nextra/components' - - - Currently GraphQL v17 is in alpha, this guide is based on the alpha release and is subject to change. +import { Callout } from 'nextra/components'; + +# What Changed in GraphQL.js v17 + + + GraphQL.js v17 is currently available as `17.0.0-beta.1`. This guide + describes migration-impacting changes from the v16 stable line to the v17 + beta line. -# Breaking changes +GraphQL.js v17 keeps the core programming model: build a schema, parse a +document, validate it, and execute it. Most changes make boundaries more +explicit. Stable single-result execution is separate from experimental +incremental delivery, input coercion is split from diagnostic validation, and +development checks are opt-in. Host integration features such as harnesses, +abort signals, and execution hooks are explicit GraphQL.js runtime APIs. + +## Reading the Labels + +Migration items below use these labels: -## Required Node.js versions +- **Breaking change:** v16 code may need to change before it runs on v17. +- **Behavioral tightening:** v17 validates or reports a case more precisely. +- **Deprecation:** the v16 API still works in v17, but should be migrated + before v18. +- **New stable API:** a new public API that can be adopted independently. +- **Experimental or opt-in:** available in v17, but proposal-backed or outside + the default execution path. -The v17 release drops support for end-of-life versions of Node.JS, retaining support for versions 20, 22, and 24 or above. +## Platform and Package Shape -## ESM and conditional exports +### Node.js and TypeScript -Earlier versions of GraphQL.js shipped dual builds for CommonJS and ESM, with ESM ".mjs" files sitting alongside CommonJS ".js" files. -The ESM build was accessible via tooling recognizing the `module` field in `package.json`, while the CommonJS build was accessible via -the `main` field. Unless configured carefully, this could sometimes lead to multiple copies of GraphQL.js being loaded in the same -process, i.e. the dual-package hazard. +**Breaking change.** GraphQL.js v17 requires Node.js 22, 24, 25, or 26 and +later: -v17 enables access to the ESM build via the `exports` field in `package.json` in a scheme designed to avoid the dual-package hazard as -best as possible, relying on the ability of bun and newer versions of Node.js to consistently load ESM modules via `require` by -indicating support for specific conditions. __ESM will now be served by default__, unless the requesting environment tooling __both__ -(A) supports the `node` or `require` conditions __and__ (B) does NOT support `bun`, `module`, `module-sync`. In that scenario, the -CommonJS build will be served instead. +```json +{ + "engines": { + "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" + } +} +``` -Note: ESM is not served to deno even though it supports require(esm) because deno not yet support the `module-sync` condition, nor -does it seem to provide the `deno` condition when calling `require`, see https://github.com/denoland/deno/issues/29970. +Upgrade Node.js before upgrading GraphQL.js. This separates runtime and +package-manager errors from GraphQL.js migration errors. -Deno users can access a Typescript build for deno via git://github.com/graphql/graphql-js.git#deno as well as by specifically loading -the index.mjs file, i.e. `import { graphql } from 'graphql/index.mjs'`, although this does not protect against the dual-package hazard. +**Breaking change.** The published type definitions target TypeScript 4.4 and +newer. -## Development mode no longer enabled by default and no longer dependent on NODE_ENV value +### Conditional exports -GraphQl.js development mode in v17 is disabled by default and can be enabled by the `development` condition on supporting platforms or -by explicitly enabling it within user code by calling `enableDevMode()`. Development mode may trigger permanent de-optimizations and -therefore cannot be disabled once enabled. The new `isDevModeEnabled()` function can be used to check whether development mode has -been enabled. +**Breaking change.** v17 uses package `exports` and modern package conditions +to select the right build. Use public entry points such as `graphql`, +`graphql/execution`, `graphql/language`, `graphql/type`, `graphql/utilities`, +and `graphql/validation`. -GraphQL.js development mode no longer depends on the `NODE_ENV` environment variable; build tools other than Node.js no longer need -to replace this Node.js specific code. See [Development Mode](./development-mode) for further details regarding how to enable these -checks in v17. +Deep imports into GraphQL.js internals may still work in some environments, but +they are not officially supported. Prefer the public entry points above for +application and library code. -## Default values +**Breaking change.** The deprecated `graphql/subscription` compatibility +subpath is gone. Import subscription APIs from `graphql` or +`graphql/execution`. -GraphQL schemas allow default values for input fields and arguments. Historically, GraphQL.js did not rigorously validate or coerce these -defaults during schema construction, leading to potential runtime errors or inconsistencies. For example: +```diff +- import { subscribe } from 'graphql/subscription'; ++ import { subscribe } from 'graphql/execution'; +``` -- A default value of "5" (string) for an Int-type argument would pass schema validation but fail at runtime. -- Internal serialization methods like astFromValue could produce invalid ASTs if inputs were not properly coerced. +### Development mode -With the new changes default values will be validated and coerced during schema construction. +**Breaking change.** Development mode is disabled by default and no longer +depends on `NODE_ENV`. Enable it with the `development` package condition or by +calling `enableDevMode()` during application startup. -```graphql -input ExampleInput { - value: Int = "invalid" # Now triggers a validation error +```js +import { enableDevMode, isDevModeEnabled } from 'graphql'; + +if (process.env.NODE_ENV === 'development') { + enableDevMode(); } + +console.log(isDevModeEnabled()); ``` -This goes hand-in-hand with the deprecation of `astFromValue` in favor of `valueToLiteral` or `default: { value: }`. +Development mode currently helps diagnose accidental use of multiple GraphQL.js +module instances. See [Development Mode](/docs/development-mode) for runtime +and bundler setup. + +## Request Pipeline and Harnesses + +### `graphql()` and `graphqlSync()` -```ts -// Before (deprecated) -const defaultValue = astFromValue(internalValue, type); -// After -const defaultValue = valueToLiteral(externalValue, type); +**New stable API.** The high-level `graphql()` and `graphqlSync()` APIs still +use the object-argument form from v16. In v17, that object can also carry +parser options, validation options, execution options, `hideSuggestions`, +`abortSignal`, hooks, and a custom harness. + +```js +const result = await graphql({ + schema, + source, + variableValues, + operationName: 'Viewer', + hideSuggestions: true, + abortSignal, +}); ``` -If you want to continue using the old behavior, you can use `defaultValue` in your schema definitions. The new -behavior will be exposed as `default: { literal: }`. +This does not make `graphql()` a framework. It remains the convenience API for +a single-result "parse, validate, execute" request. The new arguments let +simple hosts use common v17 options without rebuilding the whole pipeline. + +### GraphQL Harness + +**New stable API.** `GraphQLHarness` lets hosts replace the parse, validate, +execute, and subscribe phases used by `graphql()` and `graphqlSync()`. -## GraphQLError constructor arguments +The harness is modeled after Envelop-style plugin pipelines. It brings the +broader phase types used by that ecosystem closer to the reference +implementation, so frameworks can accept a custom harness and plugin systems +can interoperate around a shared request-pipeline shape. -The `GraphQLError` constructor now only accepts a message and options object as arguments. Previously, it also accepted positional arguments. +For example, `GraphQLParseFn` can return either a parsed `DocumentNode` or a +promise for one, even though the built-in `parse()` function is synchronous. +The harness `execute` function follows `execute()` and does not include the +experimental incremental delivery return type. + +Use [GraphQL Harness](/docs/graphql-harness) for examples, and call +`experimentalExecuteIncrementally()` directly when an operation may use +`@defer` or `@stream`. + +## Execution and Incremental Delivery + +### Single-result execution + +**Breaking change.** `execute()` is now the stable single-result executor. It +does not support incremental delivery. If a schema or operation opts into +`@defer` or `@stream`, execute it with `experimentalExecuteIncrementally()` +instead. ```diff -- new GraphQLError('message', nodes, source, positions, path, originalError, extensions); -+ new GraphQLError('message', { nodes, source, positions, path, originalError, extensions }); +- import { execute } from 'graphql'; ++ import { experimentalExecuteIncrementally } from 'graphql'; ``` -## `createSourceEventStream` arguments +This keeps the `execute()` contract simple: callers receive one +`ExecutionResult`, or a promise for one. -The `createSourceEventStream` function now only accepts an object as an argument. Previously, it also accepted positional arguments. +**Breaking change.** Incremental execution no longer uses the old +`singleResult` discriminator. Remove branches that check for `singleResult`. -```diff -- createSourceEventStream(schema, document, rootValue, contextValue, variableValues, operationName); -+ createSourceEventStream({ schema, document, rootValue, contextValue, variableValues, operationName }); +### Incremental delivery + +**Experimental or opt-in.** `experimentalExecuteIncrementally()` returns either +a normal `ExecutionResult` or an object with `initialResult` and an async +iterator of `subsequentResults`. + +```js +const result = await experimentalExecuteIncrementally({ schema, document }); + +if ('initialResult' in result) { + send(result.initialResult); + + for await (const subsequentResult of result.subsequentResults) { + send(subsequentResult); + } +} else { + send(result); +} ``` -## `execute` will error for incremental delivery +**Experimental or opt-in.** `legacyExecuteIncrementally()` remains available +for hosts that still need the older incremental delivery payload shape. The +legacy shape identifies deferred and streamed payloads with fields such as +`path` and optional `label`, and can duplicate field data across payloads. The +current experimental shape registers pending work by `id` and reports +completion with `completed` entries. + +Schema setup, directive validation, result shapes, and transport guidance are +covered in [Defer and Stream](/docs/defer-stream). + +### Resolver return values + +**New stable API.** List fields can resolve to async iterables. This is useful +for values that naturally arrive over time and is especially relevant when a +host opts into `@stream`. + +### Custom execution helpers + +**New stable API.** v17 exposes the helper boundary used by `execute()` itself. +`validateExecutionArgs()` validates and normalizes `ExecutionArgs`, including +schema checks, operation selection, variable coercion, fragment information, +default resolvers, and execution options. It returns either +`ValidatedExecutionArgs` or a list of `GraphQLError` values. -The `execute` function will now throw an error if it sees a `@defer` or `@stream` directive. Use `experimentalExecuteIncrementally` instead. -If you know you are dealing with incremental delivery requests, you can replace the import. +Then use the root selection set helper that matches the executor you are +building: + +- `executeRootSelectionSet()` for stable single-result execution. +- `experimentalExecuteRootSelectionSet()` for current incremental delivery. +- `legacyExecuteRootSelectionSet()` for the legacy incremental payload shape. + +These helpers do not replace operation validation with `validate()`. They are +for hosts that already parsed and validated a document and need a custom +execution function. + +The native `execute()` and `subscribe()` functions preserve synchronous +results when execution completes synchronously; they do not wrap every result +in a promise. Custom wrappers that want the same behavior need to handle both +sync and async paths. See +[Advanced Execution Pipelines](/docs/advanced-execution-pipelines) for complete +examples. + +## Subscriptions and Source Event Streams + +### Subscription return type + +**Breaking change.** `subscribe()` returns a `PromiseOrValue` in v17 instead +of always returning a promise. Existing `await subscribe(args)` code continues +to work, but TypeScript code that assumed a promise return type must be updated +to handle the synchronous path. + +**Breaking change.** `subscribe()` does not support incremental delivery. If a +fragment is shared between queries and subscriptions, use the `if` argument on +`@defer` or `@stream` to disable incremental behavior in subscription +operations. + +### Lower-level subscription helpers + +**Breaking change.** `createSourceEventStream()` now accepts only +`ValidatedSubscriptionArgs`. If you call it directly, call +`validateSubscriptionArgs()` first and handle validation errors before passing +the result onward. Use `subscribe()` when you want GraphQL.js to run the full +subscription pipeline. + +`validateSubscriptionArgs()` builds on `validateExecutionArgs()` and additionally +asserts that the selected operation is a subscription. + +**New stable API.** `mapSourceToResponseEvent()` maps a subscription source +stream to execution results. Its third argument is a `RootSelectionSetExecutor`, +which customizes how each source event is executed after the source stream has +already been created. If you omit it, GraphQL.js uses the default +subscription-event executor. + +See [Advanced Execution Pipelines](/docs/advanced-execution-pipelines) for +complete helper examples. + +## Abort Signals + +**Experimental or opt-in.** GraphQL.js v17 adds `AbortSignal` support as its +JavaScript runtime API for cancellation. + +The GraphQL specification discusses cancellation in narrower execution cases. +During non-null error propagation, sibling response positions that have not +executed or yielded a value +[may be cancelled](https://spec.graphql.org/draft/#sec-Errors-and-Non-Null-Types) +to avoid unnecessary work; in the specification's +[normative sections](https://spec.graphql.org/draft/#sec-Appendix-Conformance), +lowercase `may` has RFC 2119 `MAY` force. The subscription algorithms also +describe cancelling response and source streams. The specification does not +define a user or host cancelling an already issued query or mutation request. + +GraphQL.js also accepts an external `abortSignal` on `graphql()`, `execute()`, +`subscribe()`, and `experimentalExecuteIncrementally()`. Resolvers read the +resolver-scoped signal with `info.getAbortSignal()` and pass it to downstream +APIs that support cancellation. + +At this time, GraphQL.js does not expose fine-grained per-field cancellation. +Resolvers in an operation share one signal. For internally cancelled portions +of an operation, GraphQL.js aborts that shared resolver signal when the result +that will actually be returned has finished, notifying still-pending resolver +work together. This may change in future versions. + +See [Abort Signals](/docs/abort-signals) for resolver examples, HTTP request +lifecycle wiring, and aborted execution handling. + +## Execution Hooks + +**Experimental or opt-in.** Execution hooks are separate from abort signals. +They let a host observe execution lifecycle boundaries; they do not force +JavaScript work to stop. + +The first hook is `asyncWorkFinished`. It fires after GraphQL.js has stopped +producing payloads and all tracked async execution work has settled. This is +useful for cleanup, logging, telemetry, tests that need a strong lifecycle +boundary, or hosts that choose to delay returning a result until tracked async +work is finished. + +Resolvers can use `info.getAsyncHelpers()` to make additional work visible to +that tracking. See [Execution Hooks](/docs/execution-hooks) for examples. + +## Input Coercion, Defaults, and Custom Scalars + +### Default values + +**Behavioral tightening.** Argument, input-field, and directive-argument +defaults are now validated as part of schema validation performed by +`validateSchema()`. Invalid defaults that v16 could leave latent now make the +schema invalid with a targeted validation error. + +**Deprecation.** `defaultValue` is the legacy programmatic default format. It +represents an already-coerced JavaScript value and remains available as a +migration bridge. + +**New stable API.** Prefer the new `default` model for programmatic schemas. +Use `default: { value }` when you have the raw JavaScript input value before +coercion. Use `default: { literal }` when you have the GraphQL literal. When +GraphQL.js builds a schema from SDL, it now uses the literal form internally. + +```js +const field = { + type: GraphQLString, + args: { + format: { + type: GraphQLString, + default: { value: 'short' }, + }, + }, +}; +``` + +This fixes subtle cases where introspection could report default values +incorrectly from an already-coerced internal value. + +See [Passing Arguments](/docs/passing-arguments) and +[Mutations and Input Types](/docs/mutations-and-input-types). + +### Coercion and validation helpers + +**Behavioral change.** `coerceInputValue()` and `coerceInputLiteral()` now +return `undefined` when coercion fails. They are optimized for callers that +want either a coerced value or failure. + +**New stable API.** Use `validateInputValue()` and `validateInputLiteral()` when +you need diagnostic errors. Use `valueToLiteral()` when converting an external +JavaScript input value into a GraphQL literal. + +**New stable API.** `replaceVariables()` replaces variables inside complex +scalar literals. GraphQL.js calls it automatically during execution. If you use +literal coercion helpers directly outside execution, call `replaceVariables()` +yourself before coercing literals that may contain variables. + +### Custom scalar method names + +**Deprecation.** v17 introduces scalar method names that match the GraphQL +coercion model: + +| v16 name | v17 name | Purpose | +| --- | --- | --- | +| `serialize` | `coerceOutputValue` | Convert resolver values into response values. | +| `parseValue` | `coerceInputValue` | Convert variable values into internal values. | +| `parseLiteral` | `coerceInputLiteral` | Convert GraphQL literals into internal values. | +| `astFromValue()` | `valueToLiteral()` | Convert external input values into GraphQL literals. | + +The v16 scalar method names still work in v17 and are deprecated for removal in +v18. See [Advanced Custom Scalars](/docs/advanced-custom-scalars). + +## Variable Values and Resolver Info + +**Breaking change.** `getVariableValues()` now returns `{ variableValues }` on +success. That value contains source information and coerced runtime values. ```diff -- import { execute } from 'graphql'; -+ import { experimentalExecuteIncrementally as execute } from 'graphql'; +- const { coerced } = getVariableValues(schema, variableDefinitions, inputs); ++ const { variableValues } = getVariableValues(schema, variableDefinitions, inputs); ++ const coerced = variableValues.coerced; ``` -## Remove incremental delivery support from `subscribe` +This matters because v17 supports fragment-local variables and more precise +default handling. Passing only the coerced object loses information about where +values came from. + +**Breaking change.** `info.variableValues` follows the same model. Use +`info.variableValues.coerced` for runtime values inside resolvers. + +**Behavioral change.** Resolver argument maps and variable maps may use +null-prototype objects. Use `Object.hasOwn(obj, key)` or direct property access +instead of methods inherited from `Object.prototype`. + +**New stable API.** `GraphQLResolveInfo` adds `getAbortSignal()` and +`getAsyncHelpers()` for the abort-signal and execution-hook APIs. + +## Schema, Type System, Directives, and Introspection -In case you have fragments that you use with `defer/stream` that end up in a subscription, -use the `if` argument of the directive to disable it in your subscription operation +### Schema validation -## `subscribe` return type +**Behavioral tightening.** `GraphQLSchema.toConfig().assumeValid` now preserves +the original `assumeValid` setting instead of changing after schema validation +has run. -The `subscribe` function can now also return a non-Promise value, previously this was only a Promise. -This shouldn't change a lot as `await value` will still work as expected. This could lead to -some typing inconsistencies though. +**Behavioral tightening.** Schema validation reports duplicate use of the same +object type for more than one operation root. -## Remove `singleResult` from incremental results +**Behavioral tightening.** Directive argument defaults are validated as part of +schema validation. -You can remove branches that check for `singleResult` in your code, as it is no longer used. +**Behavioral tightening.** The built-in `@deprecated(reason:)` argument is now +non-null. In v16 the argument type was nullable `String`, so a directive use +could explicitly pass `reason: null`. In v17, omit `reason` to use the default +deprecation reason; explicit `null` is no longer valid. -## Node support +### Programmatic schema APIs -Dropped support for Node 14 (subject to change) +**New stable API.** v17 exposes public schema element types and assertions for +programmatic schema work, including `GraphQLField`, `GraphQLArgument`, +`GraphQLInputField`, `GraphQLEnumValue`, `assertField()`, `assertArgument()`, +`assertInputField()`, and `assertEnumValue()`. -## Removed `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types +**New stable API.** `GraphQLSchema.getField(parentType, fieldName)` resolves +ordinary fields and GraphQL meta fields such as `__typename`, `__schema`, and +`__type`. -We have removed the `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types, -use `Kind`, `TokenKind` and `DirectiveLocation` instead. https://github.com/graphql/graphql-js/pull/3579 +**New stable API.** Custom `extensions` maps on schema elements support symbol +keys as well as string keys. -## Removed `graphql/subscription` module +**New stable API.** `printDirective()` prints a directive definition without +printing an entire schema. -use `graphql/execution` instead for subscriptions, all execution related exports have been -unified there. +**Breaking change.** If you imported `GraphQLInterfaceTypeNormalizedConfig` +from a public entry point, replace that import with +`ReturnType`. -## Removed `GraphQLInterfaceTypeNormalizedConfig` export +## Language, AST, Parser, Printer, and Visitor APIs -Use `ReturnType` if you really need this +### AST constants and visitors -## Empty AST collections will be undefined +**Breaking change.** The v16 alias types `KindEnum`, `TokenKindEnum`, and +`DirectiveLocationEnum` are gone. Use `Kind`, `TokenKind`, and +`DirectiveLocation`, which are const objects with matching union types. -Empty AST collections will be presented by `undefined` rather than an empty array. +```diff +- import type { KindEnum } from 'graphql'; ++ import type { Kind } from 'graphql'; +``` -## `Info.variableValues` +**Breaking change.** `getVisitFn()` is gone. Use +`getEnterLeaveForKind()` instead. -The shape of `Info.variableValues` has changed to be an object containing -`sources` and `coerced` as keys. +```js +const { enter, leave } = getEnterLeaveForKind(visitor, Kind.FIELD); +``` -A Source contains the `signature` and provided `value` pre-coercion for the -variable. A `signature` is an object containing the `name`, `input-type` and -`defaultValue` for the variable. +**Behavioral change.** Empty AST collections may be omitted as `undefined`. +Code that reads properties such as `arguments`, `directives`, +`variableDefinitions`, `interfaces`, `fields`, `types`, or `operationTypes` +should treat them as optional. -## Stream directive can't be on multiple instances of the same field +**New stable API.** `isSubscriptionOperationDefinitionNode()` narrows +subscription operation nodes. -The `@stream` directive can't be on multiple instances of the same field, -this won't pass `validate` anymore. +### Fragment arguments -See https://github.com/graphql/graphql-js/pull/4342 +**Experimental or opt-in.** v17 adds proposal-backed fragment arguments behind +the `experimentalFragmentArguments` parser option. Older GraphQL.js versions +had an experimental `allowLegacyFragmentVariables` option, but that was +parser-only and did not work at runtime. -## Stream initialCount becomes non-nullable +When enabled, fragment definitions can declare variables and fragment spreads +can pass arguments. See [Fragment Arguments](/docs/fragment-arguments). -The `initialCount` argument of the `@stream` directive is now non-nullable. +## Validation -See https://github.com/graphql/graphql-js/pull/4322 +**Breaking change.** Passing a custom `TypeInfo` instance as a fifth argument +to `validate()` was removed. If you need a custom traversal with custom type +tracking, compose your visitor with `visitWithTypeInfo()`. -## GraphQLSchemas converted to configuration may no longer be assumed valid +```diff +- const errors = validate(schema, document, rules, options, customTypeInfo); ++ const errors = validate(schema, document, rules, options); +``` -The `assumeValid` config property exported by the `GraphQLSchema.toConfig()` method now passes through the original -flag passed on creation of the `GraphQLSchema`. -Previously, the `assumeValid` property would be to `true` if validation had been run, potentially concealing the original intent. +**New stable API.** `hideSuggestions` removes "Did you mean ..." suggestion +text from diagnostics. This is useful for public APIs that do not want to leak +schema shape through error messages. -See https://github.com/graphql/graphql-js/pull/4244 and https://github.com/graphql/graphql-js/issues/3448 +**New stable API.** `KnownOperationTypesRule` validates that an operation's +root type exists in the schema. For example, a `mutation` operation is invalid +when the schema has no mutation root type. -## `coerceInputValue` returns `undefined` on error +The defer/stream validation rules are documented with the experimental +incremental delivery feature rather than repeated here. See +[Defer and Stream](/docs/defer-stream). -`coerceInputValue` now aborts early when an error occurs, to optimize execution speed on the happy path. -Use the `validateInputValue` helper to retrieve the actual errors. +## Utilities and Error Handling -## Removals +### Schema Change utilities -- Removed deprecated `getOperationType` function, use `getRootType` on the `GraphQLSchema` instance instead -- Removed deprecated `getVisitFn` function, use `getEnterLeaveForKind` instead -- Removed deprecated `printError` and `formatError` utilities, you can use `toString` or `toJSON` on the error as a replacement -- Removed deprecated `assertValidName` and `isValidNameError` utilities, use `assertName` instead -- Removed deprecated `assertValidExecutionArguments` function, use `assertValidSchema` instead -- Removed deprecated `getFieldDefFn` from `TypeInfo` -- Removed deprecated `TypeInfo` from `validate` https://github.com/graphql/graphql-js/pull/4187 +**Deprecation.** `findBreakingChanges()` and `findDangerousChanges()` still +exist in v17 and are deprecated for removal in v18. Use +`findSchemaChanges()` for new schema registry and CI tooling. -## Deprecations +**New stable API.** `findSchemaChanges()` reports breaking, dangerous, and safe +changes from one call. Code that switches to it should handle all three +categories; safe changes use the `SafeChangeType` and `SafeChange` shapes. -- Deprecated `astFromValue` use `valueToLiteral` instead, when leveraging `valueToLiteral` ensure - that you are working with externally provided values i.e. the SDL provided defaultValue to a variable. -- Deprecated `valueFromAST` use `coerceInputLiteral` instead -- Deprecated `findBreakingChanges()` and `findDangerousChanges()`. Use `findSchemaChanges()` instead, which can also be used to find safe changes. -- Deprecated `serialize`. `parseValue`, and `parseLiteral` properties on scalar type configuration. Use `coerceOutputValue`, `coerceInputValue`, and `coerceInputLiteral` instead. +See [Schema Evolution](/docs/schema-evolution) for schema comparison examples. -## Experimental Features +### Removed helpers -### Experimental Support for Incremental Delivery +**Breaking change.** The following deprecated helpers were removed: -- [Spec PR](https://github.com/graphql/graphql-spec/pull/1110) / [RFC](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md) -- enabled only when using `experimentalExecuteIncrementally()`, use of a schema or operation with `@defer`/`@stream` directives within `execute()` will now throw. -- enable early execution with the new `enableEarlyExecution` configuration option for `experimentalExecuteIncrementally()`. +- `assertValidName()` and `isValidNameError()`; use `assertName()`. +- `assertValidExecutionArguments()`; use `assertValidSchema()` for schema + validation and `validateExecutionArgs()` for execution argument validation. +- `getOperationRootType()`; use `schema.getRootType(operation)`. +- `getFieldDefFn` from `TypeInfo`. +- `printError()` and `formatError()`; use `error.toString()` or + `error.toJSON()`. -### Experimental Support for Fragment Arguments +### `GraphQLError` -- [Spec PR](https://github.com/graphql/graphql-spec/pull/1081) -- enable with the new `experimentalFragmentArguments` configuration option for `parse()`. -- new experimental `Kind.FRAGMENT_ARGUMENT` for visiting -- new experimental `TypeInfo` methods and options for handling fragment arguments. -- coerce AST via new function `coerceInputLiteral()` with experimental fragment variables argument (as opposed to deprecated `valueFromAST()` function). +**Breaking change.** The positional `GraphQLError` constructor was removed. +Pass a message and an options object. -## Features +```diff +- new GraphQLError(message, nodes, source, positions, path, originalError); ++ new GraphQLError(message, { ++ nodes, ++ source, ++ positions, ++ path, ++ originalError, ++ extensions, ++ }); +``` -- Added `hideSuggestions` option to `execute`/`validate`/`subscribe`/... to hide schema-suggestions in error messages -- Added `abortSignal` option to `graphql()`, `execute()`, and `subscribe()` allows cancellation of these methods; - `info.abortSignal` can also be used in field resolvers to cancel asynchronous work that they initiate. -- `extensions` support `symbol` keys, in addition to the normal string keys. -- Added ability for resolver functions to return async iterables. -- Added `perEventExecutor` execution option to allows specifying a custom executor for subscription source stream events, which can be useful for preparing a per event execution context argument. -- Added `validateInputValue` and `validateInputLiteral` helpers to validate input values and literals, respectively. -- Added `replaceVariableValues` helper to replace variables within complex scalars uses as inputs. Internally, this allows variables embedded within complex scalars to finally use the correct default values. -- Added new `printDirective` helper. +## Practical Migration Order + +1. Update Node.js, TypeScript, and package-entry imports. +2. Compile and replace removed helpers, removed alias types, and positional + `GraphQLError` calls. +3. Run `validateSchema()` and migrate invalid or ambiguous defaults to the new + `default` model. +4. Update execution hosts: `execute()` versus + `experimentalExecuteIncrementally()`, `createSourceEventStream()` validation, + and subscription return types. +5. Migrate custom scalars to the v17 coercion method names while keeping the + v16 names if you still support v16. +6. Adopt optional host features such as development mode, harnesses, abort + signals, execution hooks, and fragment arguments only where they match your + server design. + +Run schema validation, operation validation, execution tests, and TypeScript +checks after each group. The upgrade is easiest to review when mechanical +changes are separated from behavior changes. diff --git a/website/vercel.json b/website/vercel.json index 9f11ce21b7..7914fe17eb 100644 --- a/website/vercel.json +++ b/website/vercel.json @@ -1,5 +1,10 @@ { "redirects": [ + { + "source": "/api-v16/graphql-http", + "destination": "/docs/graphql-http", + "permanent": true + }, { "source": "/api", "destination": "/api-v16/graphql",