diff --git a/website/pages/docs/advanced-custom-scalars.mdx b/website/pages/docs/advanced-custom-scalars.mdx index a7e7119a56..91a068409a 100644 --- a/website/pages/docs/advanced-custom-scalars.mdx +++ b/website/pages/docs/advanced-custom-scalars.mdx @@ -100,25 +100,36 @@ describe('DateTime scalar', () => { Integrate the scalar into a schema and run real GraphQL queries to validate end-to-end behavior. ```js -const { graphql, buildSchema } = require('graphql'); +const { graphql, GraphQLSchema, GraphQLObjectType } = require('graphql'); +const { DateTimeResolver as DateTime } = require('graphql-scalars'); + +const Query = new GraphQLObjectType({ + name: 'Query', + fields: { + now: { + type: DateTime, + resolve() { + return new Date(); + }, + }, + }, +}); -const schema = buildSchema(` +/* scalar DateTime type Query { now: DateTime } -`); - -const rootValue = { - now: () => new Date('2024-01-01T00:00:00Z'), -}; +*/ +const schema = new GraphQLSchema({ + query: Query, +}); async function testQuery() { const response = await graphql({ schema, source: '{ now }', - rootValue, }); console.log(response); } @@ -181,13 +192,22 @@ If you need domain-specific behavior, you can wrap an existing scalar with custo ```js const { EmailAddressResolver } = require('graphql-scalars'); -const StrictEmail = new GraphQLScalarType({ +const StrictEmailAddress = new GraphQLScalarType({ ...EmailAddressResolver, + name: 'StrictEmailAddress', parseValue(value) { - if (!value.endsWith('@example.com')) { + const email = EmailAddressResolver.parseValue(value); + if (!email.endsWith('@example.com')) { + throw new TypeError('Only example.com emails are allowed.'); + } + return email; + }, + parseLiteral(literal, variables) { + const email = EmailAddressResolver.parseLiteral(literal, variables); + if (!email.endsWith('@example.com')) { throw new TypeError('Only example.com emails are allowed.'); } - return EmailAddressResolver.parseValue(value); + return email; }, }); ``` diff --git a/website/pages/docs/cursor-based-pagination.mdx b/website/pages/docs/cursor-based-pagination.mdx index 5b548be264..a5f628315d 100644 --- a/website/pages/docs/cursor-based-pagination.mdx +++ b/website/pages/docs/cursor-based-pagination.mdx @@ -2,6 +2,8 @@ title: Implementing Cursor-based Pagination --- +import { Callout } from "nextra/components"; + When a GraphQL API returns a list of data, pagination helps avoid fetching too much data at once. Cursor-based pagination fetches items relative to a specific point in the list, rather than using numeric offsets. @@ -18,15 +20,15 @@ that works well with clients. Cursor-based pagination typically uses a structured format that separates pagination metadata from the actual data. The most widely adopted pattern follows the -[Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm). While +[GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm). While this format originated in Relay, many GraphQL APIs use it independently because of its clarity and flexibility. This pattern wraps your list of items in a connection type, which includes the following fields: -- `edges`: A list of edge objects, each representing an item in the list. -- `node`: The actual object you want to retrieve, such as user, post, or comment. -- `cursor`: An opaque string that identifies the position of the item in the list. +- `edges`: A list of edge objects, representing for each item in the list: + - `node`: The actual object you want to retrieve, such as user, post, or comment. + - `cursor`: An opaque string that identifies the position of the item in the list. - `pageInfo`: Metadata about the list, such as whether more items are available. The following query and response show how this structure works: @@ -192,7 +194,7 @@ const usersField = { let start = 0; if (args.after) { const index = decodeCursor(args.after); - if (index != null) { + if (Number.isFinite(index)) { start = index + 1; } } @@ -243,7 +245,7 @@ async function resolveUsers(_, args) { if (args.after) { const index = decodeCursor(args.after); - if (index != null) { + if (Number.isFinite(index)) { offset = index + 1; } } @@ -279,6 +281,25 @@ an `OFFSET`. To paginate backward, you can reverse the sort order and slice the results accordingly, or use keyset pagination for improved performance on large datasets. + + +The above is just an example to aid understanding; in a production application, +for most databases it is better to use `WHERE` clauses to implement cursor +pagination rather than using `OFFSET`. Using `WHERE` can leverage indices +(indexes) to jump directly to the relevant records, whereas `OFFSET` typically +must scan over and discard that number of records. When paginating very large +datasets, `OFFSET` can become more expensive as the value grows, whereas using +`WHERE` tends to have fixed cost. Using `WHERE` can also typically handle the +addition or removal of data more gracefully. + +For example, if you were ordering a collection of users by username, you could +use the username itself as the `cursor`, thus GraphQL's `allUsers(first: 10, +after: $cursor)` could become SQL's `WHERE username > $1 LIMIT 10`. Even if +that user was deleted, you could still continue to paginate from that position +onwards. + + + ## Handling edge cases When implementing pagination, consider how your resolver should handle the following scenarios: @@ -297,7 +318,7 @@ errors. To learn more about cursor-based pagination patterns and best practices, see: -- [Relay Cursor Connections Specification](https://relay.dev/graphql/connections.htm) +- [GraphQL Cursor Connections Specification](https://relay.dev/graphql/connections.htm) - [Pagination](https://graphql.org/learn/pagination/) guide on graphql.org - [`graphql-relay-js`](https://github.com/graphql/graphql-relay-js): Utility library for building Relay-compatible GraphQL servers using GraphQL.js diff --git a/website/pages/docs/custom-scalars.mdx b/website/pages/docs/custom-scalars.mdx index b5d1959867..d724360e9b 100644 --- a/website/pages/docs/custom-scalars.mdx +++ b/website/pages/docs/custom-scalars.mdx @@ -80,6 +80,10 @@ providing a name, description, and three functions: - `serialize`: How the server sends internal values to clients. - `parseValue`: How the server parses incoming variable values. - `parseLiteral`: How the server parses inline values in queries. +- `specifiedByURL` (optional): A URL specifying the behavior of your scalar; + this can be used by clients and tooling to recognize and handle common scalars + such as [date-time](https://scalars.graphql.org/andimarek/date-time.html) + independent of their name. The following example is a custom `DateTime` scalar that handles ISO-8601 encoded date strings: @@ -90,6 +94,7 @@ const { GraphQLScalarType, Kind } = require('graphql'); const DateTime = new GraphQLScalarType({ name: 'DateTime', description: 'An ISO-8601 encoded UTC date string.', + specifiedByURL: 'https://scalars.graphql.org/andimarek/date-time.html', serialize(value) { if (!(value instanceof Date)) { diff --git a/website/pages/docs/graphql-errors.mdx b/website/pages/docs/graphql-errors.mdx index 533f63bbe9..13e286f025 100644 --- a/website/pages/docs/graphql-errors.mdx +++ b/website/pages/docs/graphql-errors.mdx @@ -1,6 +1,7 @@ --- title: Understanding GraphQL.js Errors --- +import { Callout, GitHubNoteIcon } from "nextra/components"; # Understanding GraphQL.js Errors @@ -37,13 +38,22 @@ For example: 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. +- `locations` (optional): Where the error occurred in the operation document. - `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. + + +The GraphQL specification separates errors into two types: _request_ errors, and +_execution_ errors. Request errors indicate something went wrong that prevented +the GraphQL operation from executing, for example the document is invalid, and +only requires the `message` field. Execution errors indicate something went +wrong during execution, typically due to the result of calling a resolver, and +requires both the `message` and `path` fields to be present. All others fields +are optional, but recommended to help clients understand and react to errors. + + ## Creating and handling errors with `GraphQLError` @@ -81,12 +91,16 @@ Each option helps tie the error to specific parts of the GraphQL execution: 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`. +- If the thrown value is a `GraphQLError` and contains the required information +(`path`), GraphQL.js uses it as-is. +- Otherwise, GraphQL.js wraps it into a `GraphQLError`. This ensures that all errors returned to the client follow a consistent structure. +You may throw any type of error that makes sense in your application; throwing +`Error` is fine, you do not need to throw `GraphQLError`. However, ensure that +your errors do not reveal security sensitive information. + ## How errors propagate during execution Errors in GraphQL don't necessarily abort the entire operation. How an error affects the response @@ -96,6 +110,7 @@ depends on the nullability of the field where the error occurs. 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`. +If no such nullable field exists, then the operation root will be set `null` (`"data": null`). For example, consider the following schema: diff --git a/website/pages/docs/n1-dataloader.mdx b/website/pages/docs/n1-dataloader.mdx index 7a5680a0f5..57e8f351aa 100644 --- a/website/pages/docs/n1-dataloader.mdx +++ b/website/pages/docs/n1-dataloader.mdx @@ -2,7 +2,7 @@ title: Solving the N+1 Problem with `DataLoader` --- -When building a server with GraphQL.js, it's common to encounter +When building your first server with GraphQL.js, it's common to encounter performance issues related to the N+1 problem: a pattern that results in many unnecessary database or service calls, especially in nested query structures. @@ -69,7 +69,8 @@ when setting up a GraphQL HTTP server such as [express-graphql](https://github.c Suppose each `Post` has an `authorId`, and you have a `getUsersByIds(ids)` function that can fetch multiple users in a single call: -```js +{/* prettier-ignore */} +```js {14-17,37} import { graphql, GraphQLObjectType, @@ -81,6 +82,15 @@ import { import DataLoader from 'dataloader'; import { getPosts, getUsersByIds } from './db.js'; +function createContext() { + return { + userLoader: new DataLoader(async (userIds) => { + const users = await getUsersByIds(userIds); + return userIds.map(id => users.find(user => user.id === id)); + }), + }; +} + const UserType = new GraphQLObjectType({ name: 'User', fields: () => ({ @@ -114,15 +124,6 @@ const QueryType = new GraphQLObjectType({ }); const schema = new GraphQLSchema({ query: QueryType }); - -function createContext() { - return { - userLoader: new DataLoader(async (userIds) => { - const users = await getUsersByIds(userIds); - return userIds.map(id => users.find(user => user.id === id)); - }), - }; -} ``` With this setup, all `.load(authorId)` calls are automatically collected and batched diff --git a/website/pages/docs/resolver-anatomy.mdx b/website/pages/docs/resolver-anatomy.mdx index 8b7195790a..6799d021a0 100644 --- a/website/pages/docs/resolver-anatomy.mdx +++ b/website/pages/docs/resolver-anatomy.mdx @@ -33,15 +33,16 @@ When GraphQL.js executes a resolver, it calls the resolver function with four arguments: ```js -function resolver(source, args, context, info) { ... } +function resolve(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`. +`source` contains the value returned by the parent object (after resolving any +lists). For root fields, it is the `rootValue` passed to GraphQL, which is often +left `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`. @@ -85,7 +86,7 @@ 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 +```js {6-8} const UserType = new GraphQLObjectType({ name: 'User', fields: {