diff --git a/README.md b/README.md index 8908c605f5..63d3cf68b7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![GraphQLConf 2024 Banner: September 10-12, San Francisco. Hosted by the GraphQL Foundation](https://github.com/user-attachments/assets/2d048502-e5b2-4e9d-a02a-50b841824de6)](https://graphql.org/conf/2024/?utm_source=github&utm_medium=graphql_js&utm_campaign=readme) +[![GraphQLConf 2025 Banner: September 08-10, Amsterdam. Hosted by the GraphQL Foundation](./assets/graphql-conf-2025.png)](https://graphql.org/conf/2025/?utm_source=github&utm_medium=graphql_js&utm_campaign=readme) # GraphQL.js diff --git a/assets/graphql-conf-2025.png b/assets/graphql-conf-2025.png new file mode 100644 index 0000000000..d2c7ec22b0 Binary files /dev/null and b/assets/graphql-conf-2025.png differ diff --git a/benchmark/fixtures.js b/benchmark/fixtures.js index b93f075534..7625a5f937 100644 --- a/benchmark/fixtures.js +++ b/benchmark/fixtures.js @@ -8,3 +8,8 @@ export const bigSchemaSDL = fs.readFileSync( export const bigSchemaIntrospectionResult = JSON.parse( fs.readFileSync(new URL('github-schema.json', import.meta.url), 'utf8'), ); + +export const bigDocumentSDL = fs.readFileSync( + new URL('kitchen-sink.graphql', import.meta.url), + 'utf8', +); diff --git a/benchmark/kitchen-sink.graphql b/benchmark/kitchen-sink.graphql new file mode 100644 index 0000000000..8d9c6ab341 --- /dev/null +++ b/benchmark/kitchen-sink.graphql @@ -0,0 +1,65 @@ +query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { + whoever123is: node(id: [123, 456]) { + id + ... on User @onInlineFragment { + field2 { + id + alias: field1(first: 10, after: $foo) @include(if: $foo) { + id + ...frag @onFragmentSpread + } + } + } + ... @skip(unless: $foo) { + id + } + ... { + id + } + } +} + +mutation likeStory @onMutation { + like(story: 123) @onField { + story { + id @onField + } + } +} + +subscription StoryLikeSubscription( + $input: StoryLikeSubscribeInput @onVariableDefinition +) @onSubscription { + storyLikeSubscribe(input: $input) { + story { + likers { + count + } + likeSentence { + text + } + } + } +} + +fragment frag on Friend @onFragmentDefinition { + foo( + size: $size + bar: $b + obj: { + key: "value" + block: """ + block string uses \""" + """ + } + ) +} + +{ + unnamed(truthy: true, falsy: false, nullish: null) + query +} + +query { + __typename +} diff --git a/benchmark/printer-benchmark.js b/benchmark/printer-benchmark.js new file mode 100644 index 0000000000..e8da1f2b97 --- /dev/null +++ b/benchmark/printer-benchmark.js @@ -0,0 +1,14 @@ +import { parse } from 'graphql/language/parser.js'; +import { print } from 'graphql/language/printer.js'; + +import { bigDocumentSDL } from './fixtures.js'; + +const document = parse(bigDocumentSDL); + +export const benchmark = { + name: 'Print kitchen sink document', + count: 1000, + measure() { + print(document); + }, +}; diff --git a/cspell.yml b/cspell.yml index 9454626484..11fb01797d 100644 --- a/cspell.yml +++ b/cspell.yml @@ -114,6 +114,7 @@ words: - svgr - ruru - oneof + - vercel # used as href anchors - graphqlerror diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index 6e72609fec..05e1c293f9 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -10,6 +10,7 @@ import { Kind } from '../../language/kinds.js'; import { parse } from '../../language/parser.js'; import { + GraphQLInputObjectType, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, @@ -1380,4 +1381,74 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { foo: { bar: 'bar' } } }); expect(possibleTypes).to.deep.equal([fooObject]); }); + + it('uses a different number of max coercion errors', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + dummy: { type: GraphQLString }, + }, + }), + mutation: new GraphQLObjectType({ + name: 'Mutation', + fields: { + updateUser: { + type: GraphQLString, + args: { + data: { + type: new GraphQLInputObjectType({ + name: 'User', + fields: { + email: { type: new GraphQLNonNull(GraphQLString) }, + }, + }), + }, + }, + }, + }, + }), + }); + + const document = parse(` + mutation ($data: User) { + updateUser(data: $data) + } + `); + + const options = { + maxCoercionErrors: 1, + }; + + const result = executeSync({ + schema, + document, + variableValues: { + data: { + email: '', + wrongArg: 'wrong', + wrongArg2: 'wrong', + wrongArg3: 'wrong', + }, + }, + options, + }); + + // Returns at least 2 errors, one for the first 'wrongArg', and one for coercion limit + expect(result.errors).to.have.lengthOf(options.maxCoercionErrors + 1); + + expectJSON(result).toDeepEqual({ + errors: [ + { + message: + 'Variable "$data" has invalid value: Expected value of type "User" not to include unknown field "wrongArg", found: { email: "", wrongArg: "wrong", wrongArg2: "wrong", wrongArg3: "wrong" }.', + locations: [{ line: 2, column: 17 }], + }, + { + message: + 'Too many errors processing variables, error limit reached. Execution aborted.', + }, + ], + }); + }); }); diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 71cbdb6b42..539514dd37 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -200,6 +200,11 @@ export interface ExecutionArgs { enableEarlyExecution?: Maybe; hideSuggestions?: Maybe; abortSignal?: Maybe; + /** Additional execution options. */ + options?: { + /** Set the maximum number of errors allowed for coercing (defaults to 50). */ + maxCoercionErrors?: number; + }; } export interface StreamUsage { @@ -473,6 +478,7 @@ export function validateExecutionArgs( perEventExecutor, enableEarlyExecution, abortSignal, + options, } = args; if (abortSignal?.aborted) { @@ -534,10 +540,7 @@ export function validateExecutionArgs( schema, variableDefinitions, rawVariableValues ?? {}, - { - maxErrors: 50, - hideSuggestions, - }, + { maxErrors: options?.maxCoercionErrors ?? 50, hideSuggestions }, ); if (variableValuesOrErrors.errors) { diff --git a/src/language/visitor.ts b/src/language/visitor.ts index 7fbb703909..40324849cd 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -220,10 +220,7 @@ export function visit( } } } else { - node = Object.defineProperties( - {}, - Object.getOwnPropertyDescriptors(node), - ); + node = { ...node }; for (const [editKey, editValue] of edits) { node[editKey] = editValue; } diff --git a/src/utilities/__tests__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index 963474ed7e..2d9899fb77 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -122,11 +122,19 @@ describe('coerceInputValue', () => { }); describe('for GraphQLInputObject', () => { + const DeepObject = new GraphQLInputObjectType({ + name: 'DeepObject', + fields: { + foo: { type: new GraphQLNonNull(GraphQLInt) }, + bar: { type: GraphQLInt }, + }, + }); const TestInputObject = new GraphQLInputObjectType({ name: 'TestInputObject', fields: { foo: { type: new GraphQLNonNull(GraphQLInt) }, bar: { type: GraphQLInt }, + deepObject: { type: DeepObject }, }, }); @@ -153,6 +161,14 @@ describe('coerceInputValue', () => { it('invalid for an unknown field', () => { test({ foo: 123, unknownField: 123 }, TestInputObject, undefined); }); + + it('invalid for an array type', () => { + test([{ foo: 1 }, { bar: 1 }], TestInputObject, undefined); + }); + + it('invalid for an array type on a nested field', () => { + test({ foo: 1, deepObject: [1, 2, 3] }, TestInputObject, undefined); + }); }); describe('for GraphQLInputObject that isOneOf', () => { diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index d33b4273c6..3c3091db5f 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -64,8 +64,8 @@ export function coerceInputValue( } if (isInputObjectType(type)) { - if (!isObjectLike(inputValue)) { - return; // Invalid: intentionally return no value. + if (!isObjectLike(inputValue) || Array.isArray(inputValue)) { + return; } const coercedValue: any = {}; diff --git a/website/next.config.js b/website/next.config.js index 3da28a4473..0c4793d198 100644 --- a/website/next.config.js +++ b/website/next.config.js @@ -1,5 +1,9 @@ /* eslint-disable camelcase */ import path from 'node:path'; +import fs from 'node:fs'; + +const fileContents = fs.readFileSync('./vercel.json', 'utf-8'); +const vercel = JSON.parse(fileContents); import nextra from 'nextra'; @@ -29,6 +33,7 @@ export default withNextra({ }); return config; }, + redirects: async () => vercel.redirects, output: 'export', images: { loader: 'custom', diff --git a/website/pages/_meta.ts b/website/pages/_meta.ts index b56f19ce54..7bf4b6e9cd 100644 --- a/website/pages/_meta.ts +++ b/website/pages/_meta.ts @@ -1,29 +1,18 @@ const meta = { - index: '', - '-- 1': { - type: 'separator', - title: 'GraphQL.JS Tutorial', + docs: { + type: 'page', + title: 'Documentation', }, - 'getting-started': '', - 'running-an-express-graphql-server': '', - 'graphql-clients': '', - 'basic-types': '', - 'passing-arguments': '', - 'object-types': '', - 'mutations-and-input-types': '', - 'authentication-and-express-middleware': '', - '-- 2': { - type: 'separator', - title: 'Advanced Guides', - }, - 'constructing-types': '', - 'oneof-input-objects': 'OneOf input objects', - 'defer-stream': '', - '-- 3': { - type: 'separator', - title: 'FAQ', + 'upgrade-guides': { + type: 'menu', + title: 'Upgrade Guides', + items: { + 'v16-v17': { + title: 'v16 to v17', + href: '/upgrade-guides/v16-v17', + }, + }, }, - 'going-to-production': '', 'api-v16': { type: 'menu', title: 'API', diff --git a/website/pages/api-v16/error.mdx b/website/pages/api-v16/error.mdx index 1338d321de..09f5e746a6 100644 --- a/website/pages/api-v16/error.mdx +++ b/website/pages/api-v16/error.mdx @@ -56,7 +56,7 @@ class GraphQLError extends Error { source?: Source, positions?: number[], originalError?: Error, - extensions?: { [key: string]: mixed }, + extensions?: Record, ); } ``` diff --git a/website/pages/api-v16/execution.mdx b/website/pages/api-v16/execution.mdx index 2810ed183a..4723513d23 100644 --- a/website/pages/api-v16/execution.mdx +++ b/website/pages/api-v16/execution.mdx @@ -29,14 +29,28 @@ const { execute } = require('graphql'); // CommonJS ### execute ```ts -export function execute( - schema: GraphQLSchema, - documentAST: Document, - rootValue?: mixed, - contextValue?: mixed, - variableValues?: { [key: string]: mixed }, - operationName?: string, -): MaybePromise; +export function execute({ + schema, + document + rootValue, + contextValue, + variableValues, + operationName, + options, +}: ExecutionParams): MaybePromise; + +type ExecutionParams = { + schema: GraphQLSchema; + document: Document; + rootValue?: unknown; + contextValue?: unknown; + variableValues?: Record; + operationName?: string; + options?: { + /** Set the maximum number of errors allowed for coercing (defaults to 50). */ + maxCoercionErrors?: number; + } +}; type MaybePromise = Promise | T; @@ -50,6 +64,20 @@ interface ExecutionResult< } ``` +We have another approach with positional arguments, this is however deprecated and set +to be removed in v17. + +```ts +export function execute( + schema: GraphQLSchema, + documentAST: Document, + rootValue?: unknown, + contextValue?: unknown, + variableValues?: Record, + operationName?: string, +): MaybePromise; +``` + Implements the "Evaluating requests" section of the GraphQL specification. Returns a Promise that will eventually be resolved and never rejected. @@ -63,22 +91,62 @@ non-empty array if an error occurred. ### executeSync +This is a short-hand method that will call `execute` and when the response can +be returned synchronously it will be returned, when a `Promise` is returned this +method will throw an error. + +```ts +export function executeSync({ + schema, + document, + rootValue, + contextValue, + variableValues, + operationName, + options, +}: ExecutionParams): MaybePromise; + +type ExecutionParams = { + schema: GraphQLSchema; + document: Document; + rootValue?: unknown; + contextValue?: unknown; + variableValues?: Record; + operationName?: string; + options?: { + /** Set the maximum number of errors allowed for coercing (defaults to 50). */ + maxCoercionErrors?: number; + } +}; + +type MaybePromise = Promise | T; + +interface ExecutionResult< + TData = ObjMap, + TExtensions = ObjMap, +> { + errors?: ReadonlyArray; + data?: TData | null; + extensions?: TExtensions; +} +``` + +We have another approach with positional arguments, this is however deprecated and set +to be removed in v17. + ```ts export function executeSync( schema: GraphQLSchema, documentAST: Document, - rootValue?: mixed, - contextValue?: mixed, - variableValues?: { [key: string]: mixed }, + rootValue?: unknown, + contextValue?: unknown, + variableValues?: Record, operationName?: string, ): ExecutionResult; - -type ExecutionResult = { - data: Object; - errors?: GraphQLError[]; -}; ``` -This is a short-hand method that will call `execute` and when the response can -be returned synchronously it will be returned, when a `Promise` is returned this -method will throw an error. +#### Execution options + +##### maxCoercionErrors + +Set the maximum number of errors allowed for coercing variables, this implements a default limit of 50 errors. diff --git a/website/pages/docs/_meta.ts b/website/pages/docs/_meta.ts new file mode 100644 index 0000000000..36da875caa --- /dev/null +++ b/website/pages/docs/_meta.ts @@ -0,0 +1,31 @@ +const meta = { + index: '', + '-- 1': { + type: 'separator', + title: 'GraphQL.JS Tutorial', + }, + 'getting-started': '', + 'running-an-express-graphql-server': '', + 'graphql-clients': '', + 'basic-types': '', + 'passing-arguments': '', + 'object-types': '', + 'mutations-and-input-types': '', + 'authentication-and-express-middleware': '', + '-- 2': { + type: 'separator', + title: 'Advanced Guides', + }, + 'constructing-types': '', + 'oneof-input-objects': '', + 'defer-stream': '', + 'resolver-anatomy': '', + 'graphql-errors': '', + '-- 3': { + type: 'separator', + title: 'FAQ', + }, + 'going-to-production': '', +}; + +export default meta; diff --git a/website/pages/authentication-and-express-middleware.mdx b/website/pages/docs/authentication-and-express-middleware.mdx similarity index 95% rename from website/pages/authentication-and-express-middleware.mdx rename to website/pages/docs/authentication-and-express-middleware.mdx index c03f444496..4052f7ee26 100644 --- a/website/pages/authentication-and-express-middleware.mdx +++ b/website/pages/docs/authentication-and-express-middleware.mdx @@ -61,7 +61,14 @@ const { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', - fields: { ip: { type: GraphQLString } }, + fields: { + ip: { + type: GraphQLString, + resolve: (_, args, context) => { + return context.ip; + } + } + }, }), }); @@ -70,19 +77,12 @@ function loggingMiddleware(req, res, next) { next(); } -const root = { - ip(args, context) { - return context.ip; - }, -}; - const app = express(); app.use(loggingMiddleware); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, context: (req) => ({ ip: req.raw.ip, }), @@ -90,8 +90,8 @@ app.all( ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` +``` diff --git a/website/pages/basic-types.mdx b/website/pages/docs/basic-types.mdx similarity index 82% rename from website/pages/basic-types.mdx rename to website/pages/docs/basic-types.mdx index b6480a979d..90f7c7f9cf 100644 --- a/website/pages/basic-types.mdx +++ b/website/pages/docs/basic-types.mdx @@ -39,7 +39,7 @@ const root = { return Math.random(); }, rollThreeDice() { - return [1, 2, 3].map((\_) => 1 + Math.floor(Math.random() \* 6)); + return [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)); }, }; @@ -73,33 +73,27 @@ const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - quoteOfTheDay: { type: GraphQLString }, - random: { type: GraphQLFloat }, - rollThreeDice: { type: new GraphQLList(GraphQLFloat) }, + quoteOfTheDay: { + type: GraphQLString, + resolve: () => Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within' + }, + random: { + type: GraphQLFloat, + resolve: () => Math.random() + }, + rollThreeDice: { + type: new GraphQLList(GraphQLFloat), + resolve: () => [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)) + }, }, }), }); -// The root provides a resolver function for each API endpoint -const root = { - quoteOfTheDay() { - return Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within'; - }, - random() { - return Math.random(); - }, - rollThreeDice() { - return [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)); - }, -}; - const app = express(); - app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); @@ -112,4 +106,4 @@ console.log('Running a GraphQL API server at localhost:4000/graphql'); If you run this code with `node server.js` and browse to http://localhost:4000/graphql you can try out these APIs. -These examples show you how to call APIs that return different types. To send different types of data into an API, you will also need to learn about [passing arguments to a GraphQL API](/passing-arguments/). +These examples show you how to call APIs that return different types. To send different types of data into an API, you will also need to learn about [passing arguments to a GraphQL API](./passing-arguments). diff --git a/website/pages/constructing-types.mdx b/website/pages/docs/constructing-types.mdx similarity index 98% rename from website/pages/constructing-types.mdx rename to website/pages/docs/constructing-types.mdx index 2ae7b93872..2062ed4dad 100644 --- a/website/pages/constructing-types.mdx +++ b/website/pages/docs/constructing-types.mdx @@ -86,7 +86,7 @@ const userType = new graphql.GraphQLObjectType({ }, }); -// Define the Query type +// Define the Query type with inline resolver const queryType = new graphql.GraphQLObjectType({ name: 'Query', fields: { diff --git a/website/pages/defer-stream.mdx b/website/pages/docs/defer-stream.mdx similarity index 100% rename from website/pages/defer-stream.mdx rename to website/pages/docs/defer-stream.mdx diff --git a/website/pages/getting-started.mdx b/website/pages/docs/getting-started.mdx similarity index 76% rename from website/pages/getting-started.mdx rename to website/pages/docs/getting-started.mdx index c84d1509ac..f907e11692 100644 --- a/website/pages/getting-started.mdx +++ b/website/pages/docs/getting-started.mdx @@ -13,7 +13,7 @@ import { Tabs } from 'nextra/components'; Before getting started, you should have Node v6 installed, although the examples should mostly work in previous versions of Node as well. For this guide, we won't use any language features that require transpilation, but we will use some ES6 features like -[Promises](http://www.html5rocks.com/en/tutorials/es6/promises/), classes, +[Promises](http://web.dev/articles/promises/), classes, and arrow functions, so if you aren't familiar with them you might want to read up on them first. > Alternatively you can start from [this StackBlitz](https://stackblitz.com/edit/stackblitz-starters-znvgwr) - if you choose @@ -28,7 +28,7 @@ npm install graphql --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!", we can put this code in a file named `server.js`: @@ -53,42 +53,34 @@ graphql({ }).then((response) => { console.log(response); }); -}); -```` +``` ```javascript -const { graphql, GraphQLSchema, GraphQLObjectType } = require('graphql'); +const { graphql, GraphQLSchema, GraphQLObjectType, GraphQLString } = require('graphql'); // Construct a schema const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - hello: { type: GraphQLString }, + hello: { + type: GraphQLString, + resolve: () => 'Hello world!' + }, }, }), }); -// The rootValue provides a resolver function for each API endpoint -const rootValue = { - hello() { - return 'Hello world!'; - }, -}; - -// Run the GraphQL query '{ hello }' and print out the response graphql({ schema, source: '{ hello }', - rootValue, }).then((response) => { console.log(JSON.stringify(response, null, 2)); }); -```` - - +``` + If you run this with: @@ -109,4 +101,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/). +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 diff --git a/website/pages/going-to-production.mdx b/website/pages/docs/going-to-production.mdx similarity index 100% rename from website/pages/going-to-production.mdx rename to website/pages/docs/going-to-production.mdx diff --git a/website/pages/graphql-clients.mdx b/website/pages/docs/graphql-clients.mdx similarity index 91% rename from website/pages/graphql-clients.mdx rename to website/pages/docs/graphql-clients.mdx index 342193450f..552865c56a 100644 --- a/website/pages/graphql-clients.mdx +++ b/website/pages/docs/graphql-clients.mdx @@ -4,7 +4,7 @@ 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`. If you paste this into a terminal: ```bash curl -X POST \ @@ -42,9 +42,9 @@ You should see the data returned, logged in the console: data returned: Object { hello: "Hello world!" } ``` -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. +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 +For example, let's say you're running the example server from [Passing Arguments](./passing-arguments) that has a schema of ```graphql type Query { @@ -82,4 +82,4 @@ Using this syntax for variables is a good idea because it automatically prevents In general, it will take a bit more time to set up a GraphQL client like Relay, but it's worth it to get more features as your application grows. You might want to start out just using HTTP requests as the underlying transport layer, and switching to a more complex client as your application gets more complex. -At this point you can write a client and server in GraphQL for an API that receives a single string. To do more, you will want to [learn how to use the other basic data types](/basic-types/). +At this point you can write a client and server in GraphQL for an API that receives a single string. To do more, you will want to [learn how to use the other basic data types](./basic-types). diff --git a/website/pages/docs/graphql-errors.mdx b/website/pages/docs/graphql-errors.mdx new file mode 100644 index 0000000000..533f63bbe9 --- /dev/null +++ b/website/pages/docs/graphql-errors.mdx @@ -0,0 +1,188 @@ +--- +title: Understanding GraphQL.js Errors +--- + +# Understanding GraphQL.js Errors + +When executing a GraphQL operation, a server might encounter problems, such as failing to fetch +data, encountering invalid arguments, or running into unexpected internal issues. Instead of +crashing or halting execution, GraphQL.js collects these problems as structured errors +and includes them in the response. + +This guide explains how GraphQL.js represents errors internally, how errors propagate through a +query, and how you can customize error behavior. + +## How GraphQL.js represents errors in a response + +If an error occurs during execution, GraphQL.js includes it in a top-level `errors` array in the +response, alongside any successfully returned data. + +For example: + +```json +{ + "data": { + "user": null + }, + "errors": [ + { + "message": "User not found", + "locations": [{ "line": 2, "column": 3 }], + "path": ["user"] + } + ] +} +``` + +Each error object can include the following fields: + +- `message`: A human-readable description of the error. +- `locations` (optional): Where the error occurred in the operation. +- `path` (optional): The path to the field that caused the error. +- `extensions` (optional): Additional error metadata, often used for error codes, HTTP status +codes or debugging information. + +The GraphQL specification only requires the `message` field. All others are optional, but +recommended to help clients understand and react to errors. + +## Creating and handling errors with `GraphQLError` + +Internally, GraphQL.js represents errors with the `GraphQLError` class, found in the +`graphql/error` module. + +You can create a `GraphQLError` manually: + +```js +import { GraphQLError } from 'graphql'; + +throw new GraphQLError('Something went wrong'); +``` + +To provide more context about an error, you can pass additional options: + +```js +throw new GraphQLError('Invalid input', { + nodes, + source, + positions, + path, + originalError, + extensions, +}); +``` + +Each option helps tie the error to specific parts of the GraphQL execution: + +- `nodes`: The AST nodes associated with the error. +- `source` and `positions`: The source document and character offsets. +- `path`: The field path leading to the error. +- `originalError`: The underlying JavaScript error, if available. +- `extensions`: Any custom metadata you want to include. + +When a resolver throws an error: + +- If the thrown value is already a `GraphQLError`, GraphQL.js uses it as-is. +- If it is another type of error (such as a built-in `Error`), GraphQL.js wraps it into a +`GraphQLError`. + +This ensures that all errors returned to the client follow a consistent structure. + +## How errors propagate during execution + +Errors in GraphQL don't necessarily abort the entire operation. How an error affects the response +depends on the nullability of the field where the error occurs. + +- **Nullable fields**: If a resolver for a nullable field throws an error, GraphQL.js records +the error and sets the field's value to `null` in the `data` payload. +- **Non-nullable fields**: If a resolver for a non-nullable field throws an error, GraphQL.js +records the error and then sets the nearest parent nullable field to `null`. + +For example, consider the following schema: + +```graphql +type Query { + user: User +} + +type User { + id: ID! + name: String! +} +``` + +If the `name` resolver throws an error during execution: + +- Because `name` is non-nullable (`String!`), GraphQL.js can't return `null` for just that field. +- Instead, the `user` field itself becomes `null`. +- The error is recorded and included in the response. + +The result looks like: + +```json +{ + "data": { + "user": null + }, + "errors": [ + { + "message": "Failed to fetch user's name", + "path": ["user", "name"] + } + ] +} +``` + +This behavior ensures that non-nullability guarantees are respected even in the presence of errors. + +For more detailed rules, see the [GraphQL Specification on error handling](https://spec.graphql.org/October2021/#sec-Errors). + +## Customizing errors with `extensions` + +You can add additional information to errors using the `extensions` field. This is useful for +passing structured metadata like error codes, HTTP status codes, or debugging hints. + +For example: + +```js +throw new GraphQLError('Unauthorized', { + extensions: { + code: 'UNAUTHORIZED', + http: { + status: 401 + } + } +}); +``` + +Clients can inspect the `extensions` field instead of relying on parsing `message` strings. + +Common use cases for `extensions` include: + +- Assigning machine-readable error codes (`code: 'BAD_USER_INPUT'`) +- Specifying HTTP status codes +- Including internal debug information (hidden from production clients) + +Libraries like [Apollo Server](https://www.apollographql.com/docs/apollo-server/data/errors/) and +[Envelop](https://the-guild.dev/graphql/envelop/plugins/use-error-handler) offer conventions for +structured error extensions, if you want to adopt standardized patterns. + +## Best practices for error handling + +- Write clear, actionable messages. Error messages should help developers understand what went +wrong and how to fix it. +- Use error codes in extensions. Define a set of stable, documented error codes for your API +to make client-side error handling easier. +- Avoid leaking internal details. Do not expose stack traces, database errors, or other +sensitive information to clients. +- Wrap unexpected errors. Catch and wrap low-level exceptions to ensure that all errors passed +through your GraphQL server follow the `GraphQLError` structure. + +In larger servers, you might centralize error handling with a custom error formatting function +to enforce these best practices consistently. + +## Additional resources + +- [GraphQLError reference](https://graphql.org/graphql-js/error/#graphqlerror) +- [GraphQL Specification: Error handling](https://spec.graphql.org/October2021/#sec-Errors) +- [Apollo Server: Error handling](https://www.apollographql.com/docs/apollo-server/data/errors/) +- [Envelop: Error plugins](https://the-guild.dev/graphql/envelop/plugins/use-error-handler) \ No newline at end of file diff --git a/website/pages/index.mdx b/website/pages/docs/index.mdx similarity index 87% rename from website/pages/index.mdx rename to website/pages/docs/index.mdx index 4c8fb78b56..7f1e61f04d 100644 --- a/website/pages/index.mdx +++ b/website/pages/docs/index.mdx @@ -7,7 +7,7 @@ GraphQL.JS is the reference implementation to the [GraphQL Specification](https: while closely following the Specification. You can build GraphQL servers, clients, and tools with this library, it's designed so you can choose which parts you use, for example, you can build your own parser -and use the execution/validation from the library. There also a lot of useful utilities for schema-diffing, working with arguments and [many more](./utilities.mdx). +and use the execution/validation from the library. There also a lot of useful utilities for schema-diffing, working with arguments and many more. In the following chapters you'll find out more about the three critical pieces of this library @@ -15,4 +15,4 @@ In the following chapters you'll find out more about the three critical pieces o - Document validation - GraphQL Execution -You can also code along on [a tutorial](./getting-started.mdx). +You can also code along on [a tutorial](/docs/getting-started). diff --git a/website/pages/mutations-and-input-types.mdx b/website/pages/docs/mutations-and-input-types.mdx similarity index 77% rename from website/pages/mutations-and-input-types.mdx rename to website/pages/docs/mutations-and-input-types.mdx index 7b4bfa4859..065ef37a9e 100644 --- a/website/pages/mutations-and-input-types.mdx +++ b/website/pages/docs/mutations-and-input-types.mdx @@ -6,7 +6,7 @@ import { Tabs } from 'nextra/components'; If you have an API endpoint that alters data, like inserting data into a database or altering data already in a database, you should make this endpoint a `Mutation` rather than a `Query`. This is as simple as making the API endpoint part of the top-level `Mutation` type instead of the top-level `Query` type. -Let's say we have a “message of the day” server, where anyone can update the message of the day, and anyone can read the current one. The GraphQL schema for this is simply: +Let's say we have a "message of the day" server, where anyone can update the message of the day, and anyone can read the current one. The GraphQL schema for this is simply: @@ -18,7 +18,7 @@ type Mutation { type Query { getMessage: String } -```` +``` ```js @@ -42,7 +42,7 @@ const schema = new GraphQLSchema({ }, }), }); -```` +``` @@ -64,7 +64,7 @@ const root = { }; ``` -You don't need anything more than this to implement mutations. But in many cases, you will find a number of different mutations that all accept the same input parameters. A common example is that creating an object in a database and updating an object in a database often take the same parameters. To make your schema simpler, you can use “input types” for this, by using the `input` keyword instead of the `type` keyword. +You don't need anything more than this to implement mutations. But in many cases, you will find a number of different mutations that all accept the same input parameters. A common example is that creating an object in a database and updating an object in a database often take the same parameters. To make your schema simpler, you can use "input types" for this, by using the `input` keyword instead of the `type` keyword. For example, instead of a single message of the day, let's say we have many messages, indexed in a database by the `id` field, and each message has both a `content` string and an `author` string. We want a mutation API both for creating a new message and for updating an old message. We could use the schema: @@ -91,7 +91,7 @@ type Mutation { updateMessage(id: ID!, input: MessageInput): Message } -```` +``` ```js @@ -104,6 +104,9 @@ const { GraphQLNonNull, } = require('graphql'); +// Maps username to content +const fakeDatabase = {}; + const MessageInput = new GraphQLInputObjectType({ name: 'MessageInput', fields: { @@ -130,6 +133,16 @@ const schema = new GraphQLSchema({ args: { id: { type: new GraphQLNonNull(GraphQLID) }, }, + resolve: (_, { id }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + return fakeDatabase[id] ? { + id, + content: fakeDatabase[id].content, + author: fakeDatabase[id].author, + } : null; + } }, }, }), @@ -141,6 +154,16 @@ const schema = new GraphQLSchema({ args: { input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { input }) => { + // Create a random id for our "database". + const id = require('crypto').randomBytes(10).toString('hex'); + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, updateMessage: { type: Message, @@ -148,11 +171,23 @@ const schema = new GraphQLSchema({ id: { type: new GraphQLNonNull(GraphQLID) }, input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { id, input }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + // This replaces all old data, but some apps might want partial update. + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, }, }), }); -```` +``` @@ -204,46 +239,18 @@ class Message { } } -// Maps username to content -const fakeDatabase = {}; - -const root = { - getMessage({ id }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - return new Message(id, fakeDatabase[id]); - }, - createMessage({ input }) { - // Create a random id for our "database". - const id = require('crypto').randomBytes(10).toString('hex'); - - fakeDatabase[id] = input; - return new Message(id, input); - }, - updateMessage({ id, input }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - // This replaces all old data, but some apps might want partial update. - fakeDatabase[id] = input; - return new Message(id, input); - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000, () => { console.log('Running a GraphQL API server at localhost:4000/graphql'); }); -```` +``` ```js @@ -258,6 +265,18 @@ const { GraphQLNonNull, } = require('graphql'); +// If Message had any complex fields, we'd put them on this object. +class Message { + constructor(id, { content, author }) { + this.id = id; + this.content = content; + this.author = author; + } +} + +// Maps username to content +const fakeDatabase = {}; + const MessageInput = new GraphQLInputObjectType({ name: 'MessageInput', fields: { @@ -284,6 +303,16 @@ const schema = new GraphQLSchema({ args: { id: { type: new GraphQLNonNull(GraphQLID) }, }, + resolve: (_, { id }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + return fakeDatabase[id] ? { + id, + content: fakeDatabase[id].content, + author: fakeDatabase[id].author, + } : null; + } }, }, }), @@ -295,6 +324,16 @@ const schema = new GraphQLSchema({ args: { input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { input }) => { + // Create a random id for our "database". + const id = require('crypto').randomBytes(10).toString('hex'); + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, updateMessage: { type: Message, @@ -302,59 +341,34 @@ const schema = new GraphQLSchema({ id: { type: new GraphQLNonNull(GraphQLID) }, input: { type: new GraphQLNonNull(MessageInput) }, }, + resolve: (_, { id, input }) => { + if (!fakeDatabase[id]) { + throw new Error('no message exists with id ' + id); + } + // This replaces all old data, but some apps might want partial update. + fakeDatabase[id] = input; + return { + id, + content: input.content, + author: input.author, + }; + } }, }, }), }); -// If Message had any complex fields, we'd put them on this object. -class Message { - constructor(id, { content, author }) { - this.id = id; - this.content = content; - this.author = author; - } -} - -// Maps username to content -const fakeDatabase = {}; - -const root = { - getMessage({ id }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - return new Message(id, fakeDatabase[id]); - }, - createMessage({ input }) { - // Create a random id for our "database". - const id = require('crypto').randomBytes(10).toString('hex'); - - fakeDatabase[id] = input; - return new Message(id, input); - }, - updateMessage({ id, input }) { - if (!fakeDatabase[id]) { - throw new Error('no message exists with id ' + id); - } - // This replaces all old data, but some apps might want partial update. - fakeDatabase[id] = input; - return new Message(id, input); - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000, () => { console.log('Running a GraphQL API server at localhost:4000/graphql'); }); -```` +``` @@ -402,4 +416,4 @@ fetch('/graphql', { .then((data) => console.log('data returned:', data)); ``` -One particular type of mutation is operations that change users, like signing up a new user. While you can implement this using GraphQL mutations, you can reuse many existing libraries if you learn about [GraphQL with authentication and Express middleware](/authentication-and-express-middleware/). +One particular type of mutation is operations that change users, like signing up a new user. While you can implement this using GraphQL mutations, you can reuse many existing libraries if you learn about [GraphQL with authentication and Express middleware](./authentication-and-express-middleware). diff --git a/website/pages/object-types.mdx b/website/pages/docs/object-types.mdx similarity index 79% rename from website/pages/object-types.mdx rename to website/pages/docs/object-types.mdx index 366620c970..ed0cfef5da 100644 --- a/website/pages/object-types.mdx +++ b/website/pages/docs/object-types.mdx @@ -6,7 +6,7 @@ import { Tabs } from 'nextra/components'; In many cases, you don't want to return a number or a string from an API. You want to return an object that has its own complex behavior. GraphQL is a perfect fit for this. -In GraphQL schema language, the way you define a new object type is the same way we have been defining the `Query` type in our examples. Each object can have fields that return a particular type, and methods that take arguments. For example, in the [Passing Arguments](/passing-arguments/) documentation, we had a method to roll some random dice: +In GraphQL schema language, the way you define a new object type is the same way we have been defining the `Query` type in our examples. Each object can have fields that return a particular type, and methods that take arguments. For example, in the [Passing Arguments](./passing-arguments) documentation, we had a method to roll some random dice: @@ -43,7 +43,6 @@ new GraphQLObjectType({ }, }, }) - ```` @@ -60,7 +59,7 @@ type RandomDie { type Query { getDie(numSides: Int): RandomDie } -```` +``` @@ -72,17 +71,37 @@ const { GraphQLString, GraphQLList, GraphQLFloat, + GraphQLSchema, } = require('graphql'); const RandomDie = new GraphQLObjectType({ name: 'RandomDie', fields: { + numSides: { + type: new GraphQLNonNull(GraphQLInt), + resolve: function(die) { + return die.numSides; + } + }, + rollOnce: { + type: new GraphQLNonNull(GraphQLInt), + resolve: function(die) { + return 1 + Math.floor(Math.random() * die.numSides); + } + }, roll: { type: new GraphQLList(GraphQLInt), args: { numRolls: { type: new GraphQLNonNull(GraphQLInt) + }, + }, + resolve: function(die, { numRolls }) { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(1 + Math.floor(Math.random() * die.numSides)); } + return output; } } } @@ -98,13 +117,17 @@ const schema = new GraphQLSchema({ numSides: { type: GraphQLInt } + }, + resolve: (_, { numSides }) => { + return { + numSides: numSides || 6, + } } } } }) }); - -```` +``` @@ -122,7 +145,7 @@ class RandomDie { roll({ numRolls }) { const output = []; - for (const i = 0; i < numRolls; i++) { + for (let i = 0; i < numRolls; i++) { output.push(this.rollOnce()); } return output; @@ -134,7 +157,7 @@ const root = { return new RandomDie(numSides || 6); }, }; -```` +``` For fields that don't use any arguments, you can use either properties on the object or instance methods. So for the example code above, both `numSides` and `rollOnce` can actually be used to implement GraphQL fields, so that code also implements the schema of: @@ -148,9 +171,8 @@ type RandomDie { } type Query { -getDie(numSides: Int): RandomDie + getDie(numSides: Int): RandomDie } - ```` @@ -162,6 +184,7 @@ const { GraphQLString, GraphQLList, GraphQLFloat, + GraphQLSchema, } = require('graphql'); const RandomDie = new GraphQLObjectType({ @@ -169,9 +192,11 @@ const RandomDie = new GraphQLObjectType({ fields: { numSides: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => die.numSides }, rollOnce: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => 1 + Math.floor(Math.random() * die.numSides) }, roll: { type: new GraphQLList(GraphQLInt), @@ -179,6 +204,13 @@ const RandomDie = new GraphQLObjectType({ numRolls: { type: new GraphQLNonNull(GraphQLInt) }, + }, + resolve: (die, { numRolls }) => { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(1 + Math.floor(Math.random() * die.numSides)); + } + return output; } } } @@ -194,6 +226,11 @@ const schema = new GraphQLSchema({ numSides: { type: GraphQLInt } + }, + resolve: (_, { numSides }) => { + return { + numSides: numSides || 6, + } } } } @@ -233,13 +270,13 @@ class RandomDie { } rollOnce() { - return 1 + Math.floor(Math.random() \* this.numSides); + return 1 + Math.floor(Math.random() * this.numSides); } roll({ numRolls }) { const output = []; - for (const i = 0; i < numRolls; i++) { - output.push(this.rollOnce()); + for (let i = 0; i < numRolls; i++) { + output.push(this.rollOnce()); } return output; } @@ -282,9 +319,11 @@ const RandomDie = new GraphQLObjectType({ fields: { numSides: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => die.numSides }, rollOnce: { type: new GraphQLNonNull(GraphQLInt), + resolve: (die) => 1 + Math.floor(Math.random() * die.numSides) }, roll: { type: new GraphQLList(GraphQLInt), @@ -292,6 +331,13 @@ const RandomDie = new GraphQLObjectType({ numRolls: { type: new GraphQLNonNull(GraphQLInt) }, + }, + resolve: (die, { numRolls }) => { + const output = []; + for (let i = 0; i < numRolls; i++) { + output.push(1 + Math.floor(Math.random() * die.numSides)); + } + return output; } } } @@ -305,7 +351,12 @@ const schema = new GraphQLSchema({ type: RandomDie, args: { numSides: { - type: GraphQLInt + type: GraphQLInt, + } + }, + resolve: (_, { numSides }) => { + return { + numSides: numSides || 6, } } } @@ -313,44 +364,16 @@ const schema = new GraphQLSchema({ }) }); -// This class implements the RandomDie GraphQL type -class RandomDie { - constructor(numSides) { - this.numSides = numSides; - } - - rollOnce() { - return 1 + Math.floor(Math.random() * this.numSides); - } - - roll({ numRolls }) { - const output = []; - for (const i = 0; i < numRolls; i++) { - output.push(this.rollOnce()); - } - return output; - } -} - -// The root provides the top-level API endpoints -const root = { - getDie({ numSides }) { - return new RandomDie(numSides || 6); - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` - +``` @@ -369,4 +392,4 @@ If you run this code with `node server.js` and browse to http://localhost:4000/g This way of defining object types often provides advantages over a traditional REST API. Instead of doing one API request to get basic information about an object, and then multiple subsequent API requests to find out more information about that object, you can get all of that information in one API request. That saves bandwidth, makes your app run faster, and simplifies your client-side logic. -So far, every API we've looked at is designed for returning data. In order to modify stored data or handle complex input, it helps to [learn about mutations and input types](/mutations-and-input-types/). +So far, every API we've looked at is designed for returning data. In order to modify stored data or handle complex input, it helps to [learn about mutations and input types](./mutations-and-input-types). diff --git a/website/pages/oneof-input-objects.mdx b/website/pages/docs/oneof-input-objects.mdx similarity index 98% rename from website/pages/oneof-input-objects.mdx rename to website/pages/docs/oneof-input-objects.mdx index 95be65d2c2..0a968eace7 100644 --- a/website/pages/oneof-input-objects.mdx +++ b/website/pages/docs/oneof-input-objects.mdx @@ -24,7 +24,7 @@ const schema = buildSchema(` shelfNumber: Int! positionOnShelf: Int! } - + input ProductSpecifier @oneOf { id: ID name: String @@ -88,4 +88,4 @@ 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. \ No newline at end of file +The values are not limited to scalars, lists and other input object types are also allowed. diff --git a/website/pages/passing-arguments.mdx b/website/pages/docs/passing-arguments.mdx similarity index 75% rename from website/pages/passing-arguments.mdx rename to website/pages/docs/passing-arguments.mdx index 0017a69638..54d9489235 100644 --- a/website/pages/passing-arguments.mdx +++ b/website/pages/docs/passing-arguments.mdx @@ -4,7 +4,7 @@ title: Passing Arguments import { Tabs } from 'nextra/components'; -Just like a REST API, it's common to pass arguments to an endpoint in a GraphQL API. By defining the arguments in the schema language, typechecking happens automatically. Each argument must be named and have a type. For example, in the [Basic Types documentation](/basic-types/) we had an endpoint called `rollThreeDice`: +Just like a REST API, it's common to pass arguments to an endpoint in a GraphQL API. By defining the arguments in the schema language, typechecking happens automatically. Each argument must be named and have a type. For example, in the [Basic Types documentation](./basic-types) we had an endpoint called `rollThreeDice`: ```graphql type Query { @@ -12,7 +12,7 @@ type Query { } ``` -Instead of hard coding “three”, we might want a more general function that rolls `numDice` dice, each of which have `numSides` sides. We can add arguments to the GraphQL schema language like this: +Instead of hard coding "three", we might want a more general function that rolls `numDice` dice, each of which have `numSides` sides. We can add arguments to the GraphQL schema language like this: @@ -24,51 +24,67 @@ type Query { ```js +const express = require('express'); +const { createHandler } = require('graphql-http/lib/use/express'); const { GraphQLObjectType, - GraphQLNonNull, - GraphQLInt, - GraphQLString, + GraphQLSchema, GraphQLList, GraphQLFloat, + GraphQLInt, + GraphQLNonNull, } = require('graphql'); -new GraphQLObjectType({ - name: 'Query', - fields: { - rollDice: { - type: new GraphQLList(GraphQLFloat), - args: { - numDice: { - type: new GraphQLNonNull(GraphQLInt) - }, - numSides: { - type: new GraphQLNonNull(GraphQLInt) +const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + rollDice: { + type: new GraphQLList(GraphQLFloat), + args: { + numDice: { type: new GraphQLNonNull(GraphQLInt) }, + numSides: { type: GraphQLInt }, }, + resolve: (_, { numDice, numSides }) => { + const output = []; + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); + } + return output; + } }, }, - }, -}) + }), +}); -```` +const app = express(); +app.all( + '/graphql', + createHandler({ + schema: schema, + }), +); +app.listen(4000); +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. -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: +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 const root = { rollDice(args) { const output = []; - for (const i = 0; i < args.numDice; i++) { + for (let i = 0; i < args.numDice; i++) { output.push(1 + Math.floor(Math.random() * (args.numSides || 6))); } return output; }, }; -```` +``` It's convenient to use [ES6 destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) for these parameters, since you know what format they will be. So we can also write `rollDice` as @@ -76,7 +92,7 @@ It's convenient to use [ES6 destructuring assignment](https://developer.mozilla. const root = { rollDice({ numDice, numSides }) { const output = []; - for (const i = 0; i < numDice; i++) { + for (let i = 0; i < numDice; i++) { output.push(1 + Math.floor(Math.random() * (numSides || 6))); } return output; @@ -102,8 +118,8 @@ const schema = buildSchema(/_ GraphQL _/ ` type Query { rollDice(numDice: Int!, const root = { rollDice({ numDice, numSides }) { const output = []; - for (const i = 0; i < numDice; i++) { - output.push(1 + Math.floor(Math.random() \* (numSides || 6))); + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); } return output; }, @@ -119,8 +135,7 @@ app.all( ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); - -```` +``` ```js @@ -133,6 +148,7 @@ const { GraphQLString, GraphQLList, GraphQLFloat, + GraphQLSchema, } = require('graphql'); // Construct a schema, using GraphQL schema language @@ -150,34 +166,28 @@ const schema = new GraphQLSchema({ type: new GraphQLNonNull(GraphQLInt) }, }, + resolve: (_, { numDice, numSides }) => { + const output = []; + for (let i = 0; i < numDice; i++) { + output.push(1 + Math.floor(Math.random() * (numSides || 6))); + } + return output; + } }, }, }) }) -// The root provides a resolver function for each API endpoint -const root = { - rollDice({ numDice, numSides }) { - const output = []; - for (const i = 0; i < numDice; i++) { - output.push(1 + Math.floor(Math.random() * (numSides || 6))); - } - return output; - }, -}; - const app = express(); app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); app.listen(4000); console.log('Running a GraphQL API server at localhost:4000/graphql'); -```` - +``` @@ -221,4 +231,4 @@ fetch('/graphql', { Using `$dice` and `$sides` as variables in GraphQL means we don't have to worry about escaping on the client side. -With basic types and argument passing, you can implement anything you can implement in a REST API. But GraphQL supports even more powerful queries. You can replace multiple API calls with a single API call if you learn how to [define your own object types](/object-types/). +With basic types and argument passing, you can implement anything you can implement in a REST API. But GraphQL supports even more powerful queries. You can replace multiple API calls with a single API call if you learn how to [define your own object types](./object-types). diff --git a/website/pages/docs/resolver-anatomy.mdx b/website/pages/docs/resolver-anatomy.mdx new file mode 100644 index 0000000000..8b7195790a --- /dev/null +++ b/website/pages/docs/resolver-anatomy.mdx @@ -0,0 +1,138 @@ +--- +title: Anatomy of a Resolver +--- + +# Anatomy of a Resolver + +In GraphQL.js, a resolver is a function that returns the value for a +specific field in a schema. Resolvers connect a GraphQL query to the +underlying data or logic needed to fulfill it. + +This guide breaks down the anatomy of a resolver, how GraphQL.js uses +them during query execution, and best practices for writing them effectively. + +## What is a resolver? + +A resolver is responsible for returning the value for a specific field in a +GraphQL query. During execution, GraphQL.js calls a resolver for each field, +either using a custom function you provide or falling back to a default +behavior. + +If no resolver is provided, GraphQL.js tries to retrieve a property from the +parent object that matches the field name. If the property is a function, it +calls the function and uses the result. Otherwise, it returns the property +value directly. + +You can think of a resolver as a translator between the schema and the +actual data. The schema defines what can be queried, while resolvers +determine how to fetch or compute the data at runtime. + +## Resolver function signature + +When GraphQL.js executes a resolver, it calls the resolver function +with four arguments: + +```js +function resolver(source, args, context, info) { ... } +``` + +Each argument provides information that can help the resolver return the +correct value: + +- `source`: The result from the parent field's resolver. In nested fields, +`source` contains the value returned by the parent object. For root fields, +it is often `undefined`. +- `args`: An object containing the arguments passed to the field in the +query. For example, if a field is defined to accept an `id` argument, you can +access it as `args.id`. +- `context`: A shared object available to every resolver in an operation. +It is commonly used to store per-request state like authentication +information, database connections, or caching utilities. +- `info`: Information about the current execution state, including +the field name, path to the field from the root, the return type, the parent +type, and the full schema. It is mainly useful for advanced scenarios such +as query optimization or logging. + +Resolvers can use any combination of these arguments, depending on the needs +of the field they are resolving. + +## Default resolvers + +If you do not provide a resolver for a field, GraphQL.js uses a built-in +default resolver called `defaultFieldResolver`. + +The default behavior is simple: + +- It looks for a property on the `source` object that matches the name of +the field. +- If the property exists and is a function, it calls the function and uses the +result. +- Otherwise, it returns the property value directly. + +This default resolution makes it easy to build simple schemas without +writing custom resolvers for every field. For example, if your `source` object +already contains fields with matching names, GraphQL.js can resolve them +automatically. + +You can override the default behavior by specifying a `resolve` function when +defining a field in the schema. This is necessary when the field’s value +needs to be computed dynamically, fetched from an external service, or +otherwise requires custom logic. + +## Writing a custom resolver + +A custom resolver is a function you define to control exactly how a field's +value is fetched or computed. You can add a resolver by specifying a `resolve` +function when defining a field in your schema: + +```js +const UserType = new GraphQLObjectType({ + name: 'User', + fields: { + fullName: { + type: GraphQLString, + resolve(source) { + return `${source.firstName} ${source.lastName}`; + }, + }, + }, +}); +``` + +Resolvers can be synchronous or asynchronous. If a resolver returns a +Promise, GraphQL.js automatically waits for the Promise to resolve before +continuing execution: + +```js +resolve(source, args, context) { + return database.getUserById(args.id); +} +``` + +Custom resolvers are often used to implement patterns such as batching, +caching, or delegation. For example, a resolver might use a batching utility +like DataLoader to fetch multiple related records efficiently, or delegate +part of the query to another API or service. + +## Best practices + +When writing resolvers, it's important to keep them focused and maintainable: + +- Keep business logic separate. A resolver should focus on fetching or +computing the value for a field, not on implementing business rules or +complex workflows. Move business logic into separate service layers +whenever possible. +- Handle errors carefully. Resolvers should catch and handle errors +appropriately, either by throwing GraphQL errors or returning `null` values +when fields are nullable. Avoid letting unhandled errors crash the server. +- Use context effectively. Store shared per-request information, such as +authentication data or database connections, in the `context` object rather +than passing it manually between resolvers. +- Prefer batching over nested requests. For fields that trigger multiple +database or API calls, use batching strategies to minimize round trips and +improve performance. A common solution for batching in GraphQL is [dataloader](https://github.com/graphql/dataloader). +- Keep resolvers simple. Aim for resolvers to be small, composable functions +that are easy to read, test, and reuse. + +Following these practices helps keep your GraphQL server reliable, efficient, +and easy to maintain as your schema grows. \ No newline at end of file diff --git a/website/pages/running-an-express-graphql-server.mdx b/website/pages/docs/running-an-express-graphql-server.mdx similarity index 83% rename from website/pages/running-an-express-graphql-server.mdx rename to website/pages/docs/running-an-express-graphql-server.mdx index 5b8e201ec1..31020f9de2 100644 --- a/website/pages/running-an-express-graphql-server.mdx +++ b/website/pages/docs/running-an-express-graphql-server.mdx @@ -23,8 +23,8 @@ const express = require('express'); // Construct a schema, using GraphQL schema language const schema = buildSchema(`type Query { hello: String } `); -// The rootValue provides a resolver function for each API endpoint -const rootValue = { +// The root provides a resolver function for each API endpoint +const root = { hello() { return 'Hello world!'; }, @@ -45,11 +45,11 @@ app.all( app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); -```` +``` ```javascript -const { GraphQLObjectType, GraphQLSchema } = require('graphql'); +const { GraphQLObjectType, GraphQLSchema, GraphQLString } = require('graphql'); const { createHandler } = require('graphql-http/lib/use/express'); const express = require('express'); @@ -58,18 +58,14 @@ const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { - hello: { type: GraphQLString }, + hello: { + type: GraphQLString, + resolve: () => 'Hello world!' + }, }, }), }); -// The rootValue provides a resolver function for each API endpoint -const rootValue = { - hello() { - return 'Hello world!'; - }, -}; - const app = express(); // Create and use the GraphQL handler. @@ -77,16 +73,15 @@ app.all( '/graphql', createHandler({ schema: schema, - rootValue: root, }), ); // Start the server at port app.listen(4000); console.log('Running a GraphQL API server at http://localhost:4000/graphql'); -```` - +``` + You can run this GraphQL server with: @@ -95,6 +90,8 @@ You can run this GraphQL server with: node server.js ``` +At this point you will have a running GraphQL API; but you can't just visit it in your web browser to use it - you need a GraphQL client to issue GraphQL queries to the API. Let's take a look at how to add the GraphiQL (GraphQL with an `i` in the middle) integrated development environment to your server. + ## Using GraphiQL [GraphiQL](https://github.com/graphql/graphiql) is GraphQL's IDE; a great way of querying and exploring your GraphQL API. @@ -114,4 +111,4 @@ app.get('/', (_req, res) => { If you navigate to [http://localhost:4000](http://localhost:4000), you should see an interface that lets you enter queries; now you can use the GraphiQL IDE tool to issue GraphQL queries directly in the browser. -At this point you have learned how to run a GraphQL server. The next step is to learn how to [issue GraphQL queries from client code](/graphql-clients/). +At this point you have learned how to run a GraphQL server. The next step is to learn how to [issue GraphQL queries from client code](./graphql-clients). diff --git a/website/pages/upgrade-guides/v16-v17.mdx b/website/pages/upgrade-guides/v16-v17.mdx new file mode 100644 index 0000000000..00b8a27343 --- /dev/null +++ b/website/pages/upgrade-guides/v16-v17.mdx @@ -0,0 +1,187 @@ +--- +title: Upgrading from v16 to 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. + + +# Breaking changes + +## Default values + +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: + +- 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. + +With the new changes default values will be validated and coerced during schema construction. + +```graphql +input ExampleInput { + value: Int = "invalid" # Now triggers a validation error +} +``` + +This goes hand-in-hand with the deprecation of `astFromValue` in favor of `valueToLiteral` or `default: { value: }`. + +```ts +// Before (deprecated) +const defaultValue = astFromValue(internalValue, type); +// After +const defaultValue = valueToLiteral(externalValue, type); +``` + +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: }`. + +## GraphQLError constructor arguments + +The `GraphQLError` constructor now only accepts a message and options object as arguments. Previously, it also accepted positional arguments. + +```diff +- new GraphQLError('message', 'source', 'positions', 'path', 'originalError', 'extensions'); ++ new GraphQLError('message', { source, positions, path, originalError, extensions }); +``` + +## `createSourceEventStream` arguments + +The `createSourceEventStream` function now only accepts an object as an argument. Previously, it also accepted positional arguments. + +```diff +- createSourceEventStream(schema, document, rootValue, contextValue, variableValues, operationName); ++ createSourceEventStream({ schema, document, rootValue, contextValue, variableValues, operationName }); +``` + +## `execute` will error for incremental delivery + +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. + +```diff +- import { execute } from 'graphql'; ++ import { experimentalExecuteIncrementally as execute } from 'graphql'; +``` + +## Remove incremental delivery support from `subscribe` + +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 + +## `subscribe` return type + +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. + +## Remove `singleResult` from incremental results + +You can remove branches that check for `singleResult` in your code, as it is no longer used. + +## Node support + +Dropped support for Node 14 (subject to change) + +## Removed `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types + +We have removed the `TokenKindEnum`, `KindEnum` and `DirectiveLocationEnum` types, +use `Kind`, `TokenKind` and `DirectiveLocation` instead. https://github.com/graphql/graphql-js/pull/3579 + +## Removed `graphql/subscription` module + +use `graphql/execution` instead for subscriptions, all execution related exports have been +unified there. + +## Removed `GraphQLInterfaceTypeNormalizedConfig` export + +Use `ReturnType` if you really need this + +## Empty AST collections will be undefined + +Empty AST collections will be presented by `undefined` rather than an empty array. + +## `Info.variableValues` + +The shape of `Info.variableValues` has changed to be an object containing +`sources` and `coerced` as keys. + +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. + +## Stream directive can't be on multiple instances of the same field + +The `@stream` directive can't be on multiple instances of the same field, +this won't pass `validate` anymore. + +See https://github.com/graphql/graphql-js/pull/4342 + +## Stream initialCount becomes non-nullable + +The `initialCount` argument of the `@stream` directive is now non-nullable. + +See https://github.com/graphql/graphql-js/pull/4322 + +## GraphQLSchemas converted to configuration may no longer be assumed valid + +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. + +See https://github.com/graphql/graphql-js/pull/4244 and https://github.com/graphql/graphql-js/issues/3448 + +## `coerceInputValue` returns `undefined` on error + +`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. + +## Removals + +- 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 + +## Deprecations + +- 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. + +## Experimental Features + +### Experimental Support for Incremental Delivery + +- [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()`. + +### Experimental Support for Fragment Arguments + +- [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). + +## Features + +- 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; + the `abortSignal` can also be passed to 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. diff --git a/website/tailwind.config.js b/website/tailwind.config.js index 384ecc396e..6cf5041653 100644 --- a/website/tailwind.config.js +++ b/website/tailwind.config.js @@ -4,7 +4,6 @@ const config = { content: [ './pages/**/*.{ts,tsx,mdx}', './icons/**/*.{ts,tsx,mdx}', - './css/**/*.css', './theme.config.tsx', ], theme: { diff --git a/website/vercel.json b/website/vercel.json new file mode 100644 index 0000000000..9f11ce21b7 --- /dev/null +++ b/website/vercel.json @@ -0,0 +1,14 @@ +{ + "redirects": [ + { + "source": "/api", + "destination": "/api-v16/graphql", + "permanent": true + }, + { + "source": "/", + "destination": "/docs", + "permanent": true + } + ] +}