diff --git a/docs/source/config.json b/docs/source/config.json index f394daff390..85cb1d26f93 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -1,22 +1,27 @@ { "title": "Apollo Server", "version": "v4 (Alpha)", - "algoliaFilters": [ - "docset:server", - [ - "docset:react", - "docset:federation" - ] - ], + "algoliaFilters": ["docset:server", ["docset:react", "docset:federation"]], "sidebar": { "Introduction": "/", "Get started": "/getting-started", - "New in v4": { - "Migrating to Apollo Server 4": "/migration", - "Changelog": "https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md" - }, - "Server frameworks": { - "Building integrations": "/integrations/building-integrations" - } + "New in v4": { + "Migrating to Apollo Server 4": "/migration", + "Changelog": "https://github.com/apollographql/apollo-server/blob/main/CHANGELOG.md" + }, + "Defining a Schema": { + "Schema basics": "/schema/schema", + "Unions and interfaces": "/schema/unions-interfaces", + "Custom scalars": "/schema/custom-scalars", + "Directives": "/schema/directives" + }, + "Fetching Data": { + "Resolvers": "/data/resolvers", + "Error handling": "/data/errors", + "Subscriptions": "/data/subscriptions" + }, + "Server frameworks": { + "Building integrations": "/integrations/building-integrations" + } } } diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx index ca0ef2edc32..09b2ba3b2f2 100644 --- a/docs/source/data/data-sources.mdx +++ b/docs/source/data/data-sources.mdx @@ -3,6 +3,8 @@ title: Data sources description: Manage connections to databases and REST APIs --- + + **Data sources** are classes that Apollo Server can use to encapsulate fetching data from a particular source, such as a database or a REST API. These classes help handle caching, deduplication, and errors while resolving operations. Your server can use any number of different data sources. You don't _have_ to use data sources to fetch data, but they're strongly recommended. @@ -22,8 +24,6 @@ flowchart LR; class restAPI,sql secondary; ``` - - ## Open-source implementations All data source implementations extend the generic [`DataSource` abstract class](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-datasource/src/index.ts), which is included in the `apollo-datasource` package. Subclasses of a `DataSource` should define whatever logic is required to communicate with a particular store or API. @@ -45,7 +45,6 @@ If none of these implementations applies to your use case, you can create your o > Apollo does not provide official support for community-maintained libraries. We cannot guarantee that community-maintained libraries adhere to best practices, or that they will continue to be maintained. - ## Adding data sources to Apollo Server You provide your `DataSource` subclasses to the `ApolloServer` constructor, like so: @@ -55,10 +54,8 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, - cache: "bounded", - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], + cache: 'bounded', + plugins: [ApolloServerPluginLandingPageLocalDefault({ embed: true })], dataSources: () => { return { moviesAPI: new MoviesAPI(), @@ -68,9 +65,9 @@ const server = new ApolloServer({ }); ``` -* As shown, the `dataSources` option is a _function_. This function returns an _object_ containing instances of your `DataSource` subclasses (in this case, `MoviesAPI` and `PersonalizationAPI`). -* Apollo Server calls this function for _every incoming operation_. It automatically assigns the returned object to the `dataSources` field of [the `context` object](./resolvers/#the-context-argument) that's passed between your server's resolvers. -* Also as shown, **the function should create a new instance of each data source for each operation.** If multiple operations share a single data source instance, you might accidentally combine results from multiple operations. +- As shown, the `dataSources` option is a _function_. This function returns an _object_ containing instances of your `DataSource` subclasses (in this case, `MoviesAPI` and `PersonalizationAPI`). +- Apollo Server calls this function for _every incoming operation_. It automatically assigns the returned object to the `dataSources` field of [the `context` object](./resolvers/#the-context-argument) that's passed between your server's resolvers. +- Also as shown, **the function should create a new instance of each data source for each operation.** If multiple operations share a single data source instance, you might accidentally combine results from multiple operations. Your resolvers can now access your data sources from the shared `context` object and use them to fetch data: @@ -98,7 +95,7 @@ When you initialize Apollo Server, you can provide its constructor a _different_ ### Using an external cache backend - When running multiple instances of your server, you should use a shared cache backend. This enables one server instance to use the cached result from _another_ instance. +When running multiple instances of your server, you should use a shared cache backend. This enables one server instance to use the cached result from _another_ instance. Apollo Server supports using [Memcached](https://memcached.org/), [Redis](https://redis.io/), or other cache backends via the [`keyv`](https://www.npmjs.com/package/keyv) package. For examples, see [Configuring external caching](../performance/cache-backends#configuring-external-caching). @@ -165,7 +162,7 @@ class MoviesAPI extends RESTDataSource { // GET async getMovie(id) { return this.get( - `movies/${encodeURIComponent(id)}` // path + `movies/${encodeURIComponent(id)}`, // path ); } @@ -207,8 +204,9 @@ class MoviesAPI extends RESTDataSource { > Note the use of [`encodeURIComponent`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent). This is a standard JavaScript function that encodes special characters in a URI, preventing a possible injection attack vector. For a simple example, suppose our REST endpoint responded to the following URLs: -* DELETE `/movies/:id` -* DELETE `/movies/:id/characters` + +- DELETE `/movies/:id` +- DELETE `/movies/:id/characters` A "malicious" client could provide an `:id` of `1/characters` to target the delete `characters` endpoint when it was the singular `movie` endpoint that we were trying to delete. URI encoding prevents this kind of injection by transforming the `/` into `%2F`. This can then be correctly decoded and interpreted by the server and won't be treated as a path segment. @@ -218,8 +216,8 @@ For all HTTP convenience methods, the **first parameter** is the relative path o The **second parameter** depends on the HTTP method: -* For HTTP methods with a request body (`post`, `put`, `patch`), the second parameter _is_ the request body. -* For HTTP methods _without_ a request body, the second parameter is an object with keys and values corresponding to the request's query parameters. +- For HTTP methods with a request body (`post`, `put`, `patch`), the second parameter _is_ the request body. +- For HTTP methods _without_ a request body, the second parameter is an object with keys and values corresponding to the request's query parameters. For all methods, the **third parameter** is an `init` object that enables you to provide additional options (such as headers and referrers) to the `fetch` API that's used to send the request. For details, [see MDN's fetch docs](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters). @@ -229,7 +227,6 @@ For all methods, the **third parameter** is an `init` object that enables you to Data sources also have access to the GraphQL operation context, which is useful for storing a user token or other relevant information. - #### Setting a header ```js @@ -288,9 +285,9 @@ DataLoader is great for its intended use case, but itโ€™s less helpful when load When layering GraphQL over REST APIs, it's most helpful to have a resource cache that: -* Saves data across multiple GraphQL requests -* Can be shared across multiple GraphQL servers -* Provides cache management features like expiry and invalidation that use standard HTTP cache control headers +- Saves data across multiple GraphQL requests +- Can be shared across multiple GraphQL servers +- Provides cache management features like expiry and invalidation that use standard HTTP cache control headers #### Batching with REST APIs diff --git a/docs/source/data/errors.mdx b/docs/source/data/errors.mdx index f9b525d2412..4e3f02ae5cb 100644 --- a/docs/source/data/errors.mdx +++ b/docs/source/data/errors.mdx @@ -5,33 +5,30 @@ description: Making errors actionable on the client and server -Whenever Apollo Server encounters errors while processing a GraphQL operation, its response to the client includes an `errors` array that contains each error that occurred. Each error in the array has an `extensions` field that provides additional useful information, including an error `code` and (while in development mode) an `exception.stacktrace`. +Whenever Apollo Server encounters errors while processing a GraphQL operation, its response to the client includes an `errors` array containing each error that occurred. Each error in the array has an `extensions` field that provides additional useful information, including an error `code` and (while in development mode) a `stacktrace`. Here's an example error response caused by misspelling the `__typename` field in a query: - ```json { - "errors":[ + "errors": [ { - "message":"Cannot query field \"__typenam\" on type \"Query\".", - "locations":[ + "message": "Cannot query field \"__typenam\" on type \"Query\".", + "locations": [ { - "line":1, - "column":2 + "line": 1, + "column": 2 } ], - "extensions":{ - "code":"GRAPHQL_VALIDATION_FAILED", - "exception":{ - "stacktrace":[ - "GraphQLError: Cannot query field \"__typenam\" on type \"Query\".", - " at Object.Field (/my_project/node_modules/graphql/validation/rules/FieldsOnCorrectTypeRule.js:48:31)", - " ...additional lines...", - ] - } + "extensions": { + "code": "GRAPHQL_VALIDATION_FAILED", + "stacktrace": [ + "GraphQLError: Cannot query field \"__typenam\" on type \"Query\".", + " at Object.Field (/my_project/node_modules/graphql/validation/rules/FieldsOnCorrectTypeRule.js:48:31)", + " ...additional lines..." + ] } } ] @@ -40,16 +37,28 @@ Here's an example error response caused by misspelling the `__typename` field in -To help with debugging, Apollo Server defines error subclasses that represent different types of errors that can occur while handling a GraphQL operation (such as `SyntaxError` and `ValidationError`). These subclasses each return a different [error code](#error-codes), which enables requesting clients to respond differently to different error types. +To help with debugging, Apollo Server provides an `ApolloServerErrorCode` enum, which you can use to check if your error is one of the [different types produced by Apollo Server](#built-in-error-codes). -These built-in error subclasses inherit from the generic `ApolloError` class, and they're all defined in [the `apollo-server-errors` package](https://github.com/apollographql/apollo-server/blob/main/packages/apollo-server-errors/src/index.ts). You can also [create your own custom errors and codes](#custom-errors). +You can check an error's `code` to determine why an error occurred and also add logic to respond to different types of errors, like so: -## Error codes +```ts +import { ApolloServerErrorCode } from '@apollo/server/errors'; + +if (error.extensions?.code === ApolloServerErrorCode.GRAPHQL_PARSE_FAILED) { + // respond to the syntax error +} else if (error.extensions?.code === "MY_CUSTOM_CODE") { + // do something else +} +``` + +Apollo Server's variety of error codes enables requesting clients to respond differently to different error types. You can also [create your own custom errors and codes](#custom-errors). + +## Built-in error codes - + @@ -61,11 +70,11 @@ These built-in error subclasses inherit from the generic `ApolloError` class, an ###### `GRAPHQL_PARSE_FAILED` -`SyntaxError` @@ -74,11 +83,11 @@ The GraphQL operation string contains a syntax error. ###### `GRAPHQL_VALIDATION_FAILED` -`ValidationError` @@ -87,63 +96,65 @@ The GraphQL operation is not valid against the server's schema. ###### `BAD_USER_INPUT` -`UserInputError` @@ -152,38 +163,57 @@ A client sent the hash of a query string to execute via [automatic persisted que ###### `INTERNAL_SERVER_ERROR` -None
Code /
Subclass
Code Description
The GraphQL operation string contains a syntax error. +
The GraphQL operation is not valid against the server's schema. +
The GraphQL operation includes an invalid value for a field argument. +
-###### `UNAUTHENTICATED` +###### `PERSISTED_QUERY_NOT_FOUND` -`AuthenticationError` -The server failed to authenticate with a required data source, such as a REST API. +A client sent the hash of a query string to execute via [automatic persisted queries](/apollo-server/performance/apq/), but the query was not in the APQ cache. +
-###### `FORBIDDEN` +###### `PERSISTED_QUERY_NOT_SUPPORTED` -`ForbiddenError` -The server was unauthorized to access a required data source, such as a REST API. +A client sent the hash of a query string to execute via [automatic persisted queries](/apollo-server/performance/apq/), but the server has disabled APQ. +
-###### `PERSISTED_QUERY_NOT_FOUND` +###### `OPERATION_RESOLUTION_FAILURE` -`PersistedQueryNotFoundError` -A client sent the hash of a query string to execute via [automatic persisted queries](../performance/apq/), but the query was not in the APQ cache. +The request was parsed successfully and is valid against the server's schema, but the server couldn't resolve which operation to run. + +This occurs when a request containing multiple named operations doesn't specify which operation to run (i.e.,`operationName`), or if the named operation isn't included in the request. +
-###### `PERSISTED_QUERY_NOT_SUPPORTED` +###### `BAD_REQUEST` -`PersistedQueryNotSupportedError` -A client sent the hash of a query string to execute via [automatic persisted queries](../performance/apq/), but the server has disabled APQ. +An error occurred before your server could attempt to parse the given GraphQL operation. +
An unspecified error occurred. -This is the default error code returned by any `ApolloError` instance that doesn't specify a different code. +When Apollo Server formats an error in a response, it sets the code extension to this value if no other code is set. +
+## Custom errors + +You can create a custom errors and codes using the `graphql` package's `GraphQLError` class, like so: + +```ts +import { GraphQLError } from 'graphql'; + +throw new GraphQLError(message, { + extensions: { code: 'YOUR_ERROR_CODE', myCustomExtensions }, +}); +``` + +Custom errors can provide additional context, enabling your clients to understand _why_ an error is happening. We recommend making clear errors for common cases, for example, when a user isn't logged in (`UNAUTHENTICATED`), or someone is forbidden from performing an action: + +```ts +import { GraphQLError } from 'graphql'; + +throw new GraphQLError('You are not authorized to perform this action.', { + extensions: { + code: 'FORBIDDEN', + }, +}); +``` ## Throwing errors -Apollo Server throws errors of most built-in types automatically when applicable. For example, it throws a `ValidationError` whenever an incoming operation isn't valid against the server's schema. +Apollo Server throws [errors](#built-in-error-codes) automatically when applicable. For example, it throws a `GRAPHQL_VALIDATION_FAILED` error whenever an incoming operation isn't valid against the server's schema. Your resolvers can also throw errors in situations where Apollo Server doesn't do so automatically. -For example, this resolver throws a [`UserInputError`](#bad_user_input) if the integer value provided for a user's ID is less than `1`: +For example, this resolver throws a [custom error](#custom-errors) if the integer value provided for a user's ID is less than `1`: -```js {20-25} -const { - ApolloServer, - gql, - UserInputError -} = require('apollo-server'); +```ts +import { GraphQLError } from 'graphql'; -const typeDefs = gql` +const typeDefs = `#graphql type Query { userWithID(id: ID!): User } @@ -196,10 +226,15 @@ const typeDefs = gql` const resolvers = { Query: { - userWithID: (parent, args, context) => { + userWithID: (_, args) => { + // highlight-start if (args.id < 1) { - throw new UserInputError('Invalid argument value'); - } + throw new GraphQLError('Invalid argument value', { + extensions: { + code: 'BAD_USER_INPUT', + }, + }); + } // highlight-end // ...fetch correct user... }, }, @@ -208,24 +243,20 @@ const resolvers = { -If a resolver throws an error that is _not_ an `ApolloError` instance, that error is converted to a generic `ApolloError` with an `extensions` field that includes a `stacktrace` and `code` (specifically [`INTERNAL_SERVER_ERROR`](#internal_server_error)), along with other relevant error details. +If a resolver throws a generic error that is _not_ a `GraphQLError` instance, that error is still thrown with an `extensions` field that includes a `stacktrace` and `code` (specifically [`INTERNAL_SERVER_ERROR`](#internal_server_error)), along with any other relevant error details. ### Including custom error details -Whenever you throw an `ApolloError`, you can add arbitrary fields to the error's `extensions` object to provide additional context to the client. You specify these fields in an object you provide to the error's constructor. +Whenever you throw a `GraphQLError`, you can add arbitrary fields to the error's `extensions` object to provide additional context to the client. You specify these fields in an object you provide to the error's constructor. This example builds on the one above by adding the name of the GraphQL argument that was invalid: -```js {22-24} -const { - ApolloServer, - gql, - UserInputError -} = require('apollo-server'); +```ts +import { GraphQLError } from 'graphql'; -const typeDefs = gql` +const typeDefs = `#graphql type Query { userWithID(id: ID!): User } @@ -238,10 +269,15 @@ const typeDefs = gql` const resolvers = { Query: { - userWithID: (parent, args, context) => { + userWithID: (_, args) => { if (args.id < 1) { - throw new UserInputError('Invalid argument value', { - argumentName: 'id' + throw new GraphQLError('Invalid argument value', { + // highlight-start + extensions: { + code: 'BAD_USER_INPUT', + argumentName: 'id', + }, + // highlight-end }); } // ...fetch correct user... @@ -256,7 +292,7 @@ This results in a response like the following: -```json {15} +```json { "errors": [ { @@ -267,19 +303,17 @@ This results in a response like the following: "column": 3 } ], - "path": [ - "userWithID" - ], + "path": ["userWithID"], "extensions": { - "argumentName": "id", "code": "BAD_USER_INPUT", - "exception": { - "stacktrace": [ - "UserInputError: Invalid argument value", - " at userWithID (/my-project/index.js:25:13)", - " ...more lines...", - ] - } + // highlight-start + "argumentName": "id", + // highlight-end + "stacktrace": [ + "GraphQLError: Invalid argument value", + " at userWithID (/my-project/index.js:25:13)", + " ...more lines..." + ] } } ] @@ -288,43 +322,17 @@ This results in a response like the following: -## Custom errors - -You can create a custom error by defining your own subclass of `ApolloError`, or by initializing an `ApolloError` object directly: - -#### Subclass with custom error code - -```ts -import { ApolloError } from 'apollo-server-errors'; - -export class MyError extends ApolloError { - constructor(message: string) { - super(message, 'MY_ERROR_CODE'); - - Object.defineProperty(this, 'name', { value: 'MyError' }); - } -} - -throw new MyError('My error message') -``` - -#### Direct initialization - -```ts -import { ApolloError } from 'apollo-server-errors'; - -throw new ApolloError('My error message', 'MY_ERROR_CODE', myCustomExtensions); -``` - ## Omitting or including `stacktrace` -The `exception.stacktrace` error field is useful while developing and debugging your server, but you probably don't want to expose it to clients in production. +The `stacktrace` error field is useful while developing and debugging your server, but you probably don't want to expose it to clients in production. + +By default, Apollo Server _omits_ the `stacktrace` field if the `NODE_ENV` environment variable is set to either `production` or `test`. -By default, Apollo Server _omits_ the `exception.stacktrace` field if the `NODE_ENV` environment variable is set to either `production` or `test`. + -You can override this default behavior by passing the `debug` option to [the constructor of `ApolloServer`](../api/apollo-server/#constructor). If `debug` is `true`, `exception.stacktrace` is always included. If it's `false`, `exception.stacktrace` is always omitted. +You can override this default behavior by passing the `includeStacktraceInErrorResponses` option to the constructor of `ApolloServer`. If `includeStacktraceInErrorResponses` is `true`, `stacktrace` is always included. If it's `false`, `stacktrace` is always omitted. -Note that when `exception.stacktrace` is omitted, it's also unavailable to your application. To log error `stacktrace`s without including them in responses to clients, see [Masking and logging errors](#masking-and-logging-errors). +Note that when `stacktrace` is omitted, it's also unavailable to your application. To log error `stacktrace`s without including them in responses to clients, see [Masking and logging errors](#masking-and-logging-errors). ## Masking and logging errors @@ -332,168 +340,258 @@ You can edit Apollo Server error details before they're passed to a client or re ### For client responses -The `ApolloServer` constructor accepts a `formatError` function that is run on each error before it's passed back to the client. You can use this function to mask particular errors, as well as for logging. +The `ApolloServer` constructor accepts a `formatError` hook that is run on each error before it's passed back to the client. You can use this function to log or mask particular errors. + +The `formatError` hook receives two arguments: the first is the error formatted as a JSON object (to be spent with the response), and the second is the original error. > The `formatError` function does _not_ modify errors that are sent to Apollo Studio as part of usage reporting. See [For Apollo Studio reporting](#for-apollo-studio-reporting). -This example returns a more generic error whenever the original error's message begins with `Database Error: `: +The below example returns a user-friendly message whenever Apollo Server throws a `GRAPHQL_VALIDATION_FAILED` error: + +```ts +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { ApolloServerErrorCode } from '@apollo/server/errors'; -```js {4-12} const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: "bounded", - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], - formatError: (err) => { - // Don't give the specific errors to the client. - if (err.message.startsWith('Database Error: ')) { - return new Error('Internal server error'); + // highlight-start + formatError: (formattedError, error) => { + // Return a different error message + if ( + formattedError.extensions.code === + ApolloServerErrorCode.GRAPHQL_VALIDATION_FAILED + ) { + return { + ...formattedError, + message: "Your query doesn't match the schema. Try double-checking it!", + }; } - // Otherwise return the original error. The error can also + // Otherwise return the formatted error. This error can also // be manipulated in other ways, as long as it's returned. - return err; + return formattedError; }, + // highlight-end }); -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`); -}); +const { url } = await startStandaloneServer(server); +console.log(`๐Ÿš€ Server listening at: ${url}`); ``` -The error instance received by `formatError` (a `GraphQLError`) contains an `originalError` property, which represents the original error thrown in the resolver. You can use this property to obtain the `instanceof` the error class, such as `AuthenticationError` or `ValidationError`: +As another example, here we return a more generic error whenever the original error's message begins with `Database Error: `: -```js - formatError(err) { - if (err.originalError instanceof AuthenticationError) { - return new Error('Different authentication error message!'); +```ts +formatError: (formattedError, error) => { + // highlight-start + if (formattedError.message.startsWith('Database Error: ')) { + return { message: 'Internal server error' }; + } + // highlight-end + + // Otherwise return the formatted error. + return formattedError; +}, +``` + +If you want to access the originally thrown error (without the JSON formatting), you can use `formatError`'s second argument. + +For example, if you are using a database package in your app and you'd like to do something when your server throws a specific type of database error: + +```ts + formatError: (formattedError, error) => { + if (error instanceof CustomDBError) { + // do something specific } }, ``` -> To make context-specific adjustments to the error received by `formatError` (such as localization or personalization), consider [creating a plugin](../integrations/plugins/) that uses the [`didEncounterErrors` lifecycle event](../integrations/plugins-event-reference/#didencountererrors) to attach additional properties to the error. These properties can be accessed from `formatError`. +Note, if a _resolver_ throws the error, a `GraphQLError` is wrapped around the initially thrown error. This `GraphQLError` neatly formats the error and contains useful fields, such as the `path` where the error occurred. + +In you want to remove the outer `GraphQLError` to access the originally thrown error you can use `unwrapResolverError` from `@apollo/server/errors`. The `unwrapResolverError` function can remove the `GraphQLError` wrapping from a resolver error or return the error unaltered if it isn't from a resolver. + +So, we can rewrite the above code snippet to work for errors thrown in and outside of resolvers, like so: + +```ts {7-9} +import { unwrapResolverError } from '@apollo/server/errors'; + +new ApolloServer({ + formatError: (formattedError, error) => { + // unwrapResolverError removes the outer GraphQLError wrapping from + // errors thrown in resolvers, enabling us to check the instance of + // the original error + if (unwrapResolverError(error) instanceof CustomDBError) { + return { message: 'Internal server error' }; + } + }, +}); +``` + +> To make context-specific adjustments to the error received by `formatError` (such as localization or personalization), consider [creating a plugin](/apollo-server/integrations/plugins/) that uses the [`didEncounterErrors` lifecycle event](/apollo-server/integrations/plugins-event-reference/#didencountererrors) to attach additional properties to the error. These properties can be accessed from `formatError`. ### For Apollo Studio reporting -You can use Apollo Studio to analyze your server's error rates. If you connect Apollo Server to Studio, all errors are sent to Studio by default. If you _don't_ want certain error information to be sent to Studio (either because the error is unimportant or because certain information is confidential), you can modify or redact errors entirely before they're transmitted. +> **New in Apollo Server 4:** error details are [_not_ included in traces by default](../migration#usage-reporting-and-inline-trace-plugins-mask-errors-by-default). Instead, `` replaces each error's message, and the `maskedBy` error extension replaces all other extensions. The `maskedBy` extension includes the name of the plugin that performed the masking (`ApolloServerPluginUsageReporting` or `ApolloServerPluginInlineTrace`). -To accomplish this, you can provide a `rewriteError` function to the [usage reporting plugin](../api/plugin/usage-reporting/). +You can use Apollo Studio to analyze your server's error rates. By default, the operations sent to Studio as detailed traces _don't_ contain error details. -> The usage reporting plugin is installed automatically with its default configuration if you provide an Apollo API key to Apollo Server. To define a custom `rewriteError` function, you need to install the plugin explicitly with a custom configuration, as shown in examples below. +If you _do_ want error information sent to Studio, you can send every error, or you can modify or redact specific errors before they're transmitted. -Your `rewriteError` function is called for each error (a `GraphQLError` or an `ApolloError`) to be reported to Studio. The error is provided as the function's first argument. The function can either: +To send all errors to Studio you can pass `{ unmodified: true }` to `sendErrorsInTraces`, like so: -* Return a modified form of the error (e.g., by changing the `err.message` to remove potentially sensitive information) -* Return `null` to prevent the error from being reported entirely +```ts {7} +new ApolloServer({ + // etc. + plugins: [ + ApolloServerPluginUsageReporting({ + // If you pass unmodified: true to the usage reporting + // plugin, Apollo Studio receives ALL error details + sendErrorsInTraces: { unmodified: true }, + }), + ], +}); +``` ->**For federated graphs**, instead define `rewriteError` in each subgraph's [inline trace plugin](../api/plugin/inline-trace/#rewriteerror). Do not define it in the gateway. +If you want to report specific errors or modify an error before reporting it, you can pass a function to the `sendErrorsInTraces.transform` option, like so: + +```ts {4-6} +new ApolloServer({ + // etc. + plugins: [ + ApolloServerPluginUsageReporting({ + sendErrorsInTraces: { + transform: (err) => { + if (err.extensions.code === 'MY_CUSTOM_CODE') { + // returning null will skip reporting this error + return null; + } + + // All other errors are reported. + return err; + }, + }, + }), + ], +}); +``` + +> The usage reporting plugin is installed automatically with its default configuration if you provide an Apollo API key to Apollo Server. To customize the usage reporting plugin's behavior, you need to install it explicitly with a custom configuration, as shown in the examples below. + +The function you pass to `transform` is called for each error (`GraphQLError`) to be reported to Studio. The error is provided as the function's first argument. The function can either: + +- Return a modified form of the error (e.g., by changing the `err.message` to remove potentially sensitive information) +- Return `null` to prevent the error from being reported entirely + +[As mentioned above](#for-client-responses), you can use the `unwrapResolverError` (from `@apollo/server/errors`) to remove the `GraphQLError` wrapping an original error. + + + +> **For federated graphs**, define your `transform` function in each subgraph's [inline trace plugin](./migration/rewriteEerror-plugin-option) to rewrite field errors. If you want to transform your gateway's parsing or validation errors, you can define your `transform` function in your gateway. #### Example: Ignoring common low-severity errors -Let's say our server is `throw`ing an `AuthenticationError` whenever an incorrect password is provided. We can avoid reporting these errors to Apollo Studio by defining `rewriteError`, like so: +Let's say our server is `throw`ing an `UNAUTHENTICATED` error whenever a user enters an incorrect password. We can avoid reporting these errors to Apollo Studio by defining a `transform` function, like so: -```js {7-17} -const { ApolloServer, AuthenticationError } = require("apollo-server"); -const { - ApolloServerPluginUsageReporting, - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); +```ts +import { ApolloServer } from '@apollo/server'; +import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting'; const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: "bounded", plugins: [ + // highlight-start ApolloServerPluginUsageReporting({ - rewriteError(err) { - // Return `null` to avoid reporting `AuthenticationError`s - if (err instanceof AuthenticationError) { - return null; - } - - // All other errors will be reported. - return err; - } + sendErrorsInTraces: { + transform: (err) => { + // Return `null` to avoid reporting `UNAUTHENTICATED` errors + if (err.extensions.code === 'UNAUTHENTICATED') { + return null; + } + + // All other errors will be reported. + return err; + }, + }, }), - ApolloServerPluginLandingPageLocalDefault({ embed: true }), + // highlight-end ], }); ``` -This example configuration ensures that any `AuthenticationError` that's thrown within a resolver is only reported to the client, and never sent to Apollo Studio. All other errors are transmitted to Studio normally. +This example configuration ensures that any `UNAUTHENTICATED` error that's thrown within a resolver is only reported to the client, and never sent to Apollo Studio. All other errors are transmitted to Studio normally. #### Example: Filtering errors based on other properties -When generating an error (e.g., `new ApolloError("Failure!")`), the error's `message` is the most common property (in this case it's `Failure!`). However, any number of properties can be attached to the error (such as a `code` property). +When generating an error (e.g., `new GraphQLError("Failure!")`), the error's `message` is the most common extension (in this case it's `Failure!`). However, any number of extensions can be attached to the error (such as a `code` extension). -We can check these properties when determining whether an error should be reported to Apollo Studio using the `rewriteError` function as follows: +We can check these extensions when determining whether an error should be reported to Apollo Studio using the `transform` function as follows: + +```ts +import { ApolloServer } from '@apollo/server'; +import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting'; -```js {7-17} -const { ApolloServer } = require("apollo-server"); -const { - ApolloServerPluginUsageReporting, - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: "bounded", plugins: [ ApolloServerPluginUsageReporting({ - rewriteError(err) { - // Using a more stable, known error property (e.g. `err.code`) would be - // more defensive, however checking the `message` might serve most needs! - if (err.message && err.message.startsWith("Known error message")) { - return null; - } - - // All other errors should still be reported! - return err; - } + sendErrorsInTraces: { + transform: (err) => { + // Using a more stable, known error extension (e.g. `err.code`) would be + // more defensive, however checking the `message` might serve most needs! + // highlight-start + if (err.message && err.message.startsWith('Known error message')) { + return null; + } + // highlight-end + + // All other errors should still be reported! + return err; + }, + }, }), - ApolloServerPluginLandingPageLocalDefault({ embed: true }), ], }); ``` This example configuration ensures that any error that starts with `Known error message` is not transmitted to Apollo Studio, but all other errors are sent as normal. -#### Example: Redacting the error message +#### Example: Redacting information from an error message + +[As mentioned above](#for-apollo-studio-reporting), by default, the operations sent to Studio as detailed traces don't contain error details. -If it is necessary to change an error prior to reporting it to Apollo Studio (for example, if there is personally identifiable information in the error `message`), the `rewriteError` function can also help. +If you _do_ want to send an error's details to Apollo Studio, but need to redact some information first, the `transform` function can help. -Consider an example where the error contains a piece of information like an API key: +For example, if there is personally identifiable information in the error `message`, like an API key: ```js -throw new ApolloError("The x-api-key:12345 doesn't have sufficient privileges."); +import { GraphQLError } from 'graphql'; + +throw new GraphQLError( + "The x-api-key:12345 doesn't have sufficient privileges.", +); ``` -The `rewriteError` function can ensure that such information is not sent to Apollo Studio and potentially revealed outside its intended scope: +The `transform` function can ensure that such information is not sent to Apollo Studio and potentially revealed outside its intended scope: + +```ts +import { ApolloServer } from '@apollo/server'; +import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting'; -```js {7-13} -const { ApolloServer } = require("apollo-server"); -const { - ApolloServerPluginUsageReporting, - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: "bounded", plugins: [ ApolloServerPluginUsageReporting({ - rewriteError(err) { - // Make sure that a specific pattern is removed from all error messages. - err.message = err.message.replace(/x-api-key:[A-Z0-9-]+/, "REDACTED"); - return err; - } + sendErrorsInTraces: { + transform: (err) => { + // Make sure that a specific pattern is removed from all error messages. + err.message = err.message.replace(/x-api-key:[A-Z0-9-]+/, 'REDACTED'); + return err; + }, + }, }), - ApolloServerPluginLandingPageLocalDefault({ embed: true }), ], }); ``` @@ -506,35 +604,29 @@ The REDACTED doesn't have sufficient privileges. ## Returning HTTP status codes -GraphQL, by design, does not use the same conventions from REST to communicate via HTTP verbs and status codes. Client information should be contained in the schema or as part of the standard response `errors` field. We recommend using the included [Error Codes](#error-codes) or [Custom Errors](#custom-errors) for error consistency rather than directly modifying the HTTP response. +GraphQL, by design, does not use the same conventions from REST to communicate via HTTP verbs and status codes. Client information should be contained in the schema or as part of the standard response `errors` field. We recommend using the included [Error Codes](#built-in-error-codes) or [Custom Errors](#custom-errors) for error consistency rather than directly modifying the HTTP response. -You can set custom fields on your HTTP response by using a [plugin](../integrations/plugins). Be aware that GraphQL client libraries may not treat all response status codes the same, and so it will be up to your team to decide what patterns to use. +You can set custom fields on your HTTP response by using a [plugin](/apollo-server/integrations/plugins). Be aware that GraphQL client libraries may not treat all response status codes the same, and so it will be up to your team to decide what patterns to use. As an example, here is how you could set a custom response header and status code based on a GraphQL error: - -```js +```ts const setHttpPlugin = { async requestDidStart() { return { async willSendResponse({ response }) { - response.http.headers.set('Custom-Header', 'hello'); - if (response?.errors?.[0]?.message === 'teapot') { + response.http.headers.set('custom-header', 'hello'); + if (response?.result?.errors?.[0]?.message === 'teapot') { response.http.status = 418; } - } + }, }; - } + }, }; const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - setHttpPlugin, - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], + plugins: [setHttpPlugin], }); ``` diff --git a/docs/source/data/file-uploads.mdx b/docs/source/data/file-uploads.mdx deleted file mode 100644 index a89e394ffd1..00000000000 --- a/docs/source/data/file-uploads.mdx +++ /dev/null @@ -1,212 +0,0 @@ ---- -title: File uploads -description: Enabling file uploads in Apollo Server ---- - -There are a few ways to accomplish file uploads in Apollo Server. Our [blog post](https://www.apollographql.com/blog/backend/file-uploads/file-upload-best-practices/) on file uploads goes in depth on the approaches we've taken, outlining the pros and cons of each. - -Apollo recommends handling the file upload itself out-of-band of your GraphQL server for the sake of simplicity and security. This "signed URL" approach allows your client to retrieve a URL from your GraphQL server (via S3 or other storage service) and upload the file to the URL rather than to your GraphQL server. The blog post mentioned above goes into detail on how to set this up. - -Another common approach involves adding file upload support directly to Apollo Server via the third-party [`graphql-upload`](https://npm.im/graphql-upload) library. This package provides support for the `multipart/form-data` content-type. **Note: this approach is vulnerable to a [CSRF mutation attack](https://github.com/apollographql/apollo-server/security/advisories/GHSA-2p3c-p3qw-69r4) unless Apollo Server is explicitly configured with `csrfPrevention: true`.** In the examples below, we demonstrate configuring CSRF prevention in order to prevent this vulnerability by always requiring a "preflight" OPTIONS request before executing an operation. When configuring your file upload client, you will need to send a non-empty `Apollo-Require-Preflight` header or Apollo Server will block the request. For example, if you use the `apollo-upload-client` package with Apollo Client Web, pass headers: `{'Apollo-Require-Preflight': 'true'}` to `createUploadLink`. - -**New in Apollo Server 3:** Apollo Server 3 does not contain a built-in integration with `graphql-upload` like in Apollo Server 2. Instead, the instructions below show how to integrate it yourself. You cannot do this with the "batteries-included" `apollo-server` library; you must use a web framework integration such as `apollo-server-express` instead. This page shows how to integrate `graphql-upload` with Express and Fastify. To implement similar functionality with another Node.js HTTP framework (e.g., Koa), see the [`graphql-upload` documentation](https://github.com/jaydenseric/graphql-upload) for more information. Some integrations might need to use `graphql-upload`'s `processRequest` directly. - -### Integrating with Express - - - -```ts title="index.ts" -const express = require('express'); -const { ApolloServer, gql } = require('apollo-server-express'); -const { - GraphQLUpload, - graphqlUploadExpress, // A Koa implementation is also exported. -} = require('graphql-upload'); -const { finished } = require('stream/promises'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); - -const typeDefs = gql` - # The implementation for this scalar is provided by the - # 'GraphQLUpload' export from the 'graphql-upload' package - # in the resolver map below. - scalar Upload - - type File { - filename: String! - mimetype: String! - encoding: String! - } - - type Query { - # This is only here to satisfy the requirement that at least one - # field be present within the 'Query' type. This example does not - # demonstrate how to fetch uploads back. - otherFields: Boolean! - } - - type Mutation { - # Multiple uploads are supported. See graphql-upload docs for details. - singleUpload(file: Upload!): File! - } -`; - -const resolvers = { - // This maps the `Upload` scalar to the implementation provided - // by the `graphql-upload` package. - Upload: GraphQLUpload, - - Mutation: { - singleUpload: async (parent, { file }) => { - const { createReadStream, filename, mimetype, encoding } = await file; - - // Invoking the `createReadStream` will return a Readable Stream. - // See https://nodejs.org/api/stream.html#stream_readable_streams - const stream = createReadStream(); - - // This is purely for demonstration purposes and will overwrite the - // local-file-output.txt in the current working directory on EACH upload. - const out = require('fs').createWriteStream('local-file-output.txt'); - stream.pipe(out); - await finished(out); - - return { filename, mimetype, encoding }; - }, - }, -}; - -async function startServer() { - const server = new ApolloServer({ - typeDefs, - resolvers, - // Using graphql-upload without CSRF prevention is very insecure. - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], - }); - await server.start(); - - const app = express(); - - // This middleware should be added before calling `applyMiddleware`. - app.use(graphqlUploadExpress()); - - server.applyMiddleware({ app }); - - await new Promise(r => app.listen({ port: 4000 }, r)); - - console.log(`๐Ÿš€ Server ready at http://localhost:4000${server.graphqlPath}`); -} - -startServer(); -``` - - - -### Integrating with Fastify - -```js -const { ApolloServer, gql } = require('apollo-server-fastify'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); -const { GraphQLUpload, processRequest } = require('graphql-upload'); -const { finished } = require('stream/promises'); - -const typeDefs = gql` - # The implementation for this scalar is provided by the - # 'GraphQLUpload' export from the 'graphql-upload' package - # in the resolver map below. - scalar Upload - - type File { - filename: String! - mimetype: String! - encoding: String! - } - - type Query { - # This is only here to satisfy the requirement that at least one - # field be present within the 'Query' type. This example does not - # demonstrate how to fetch uploads back. - otherFields: Boolean! - } - - type Mutation { - # Multiple uploads are supported. See graphql-upload docs for details. - singleUpload(file: Upload!): File! - } -`; - -const resolvers = { - // This maps the `Upload` scalar to the implementation provided - // by the `graphql-upload` package. - Upload: GraphQLUpload, - - Mutation: { - singleUpload: async (parent, { file }) => { - const { createReadStream, filename, mimetype, encoding } = await file; - - // Invoking the `createReadStream` will return a Readable Stream. - // See https://nodejs.org/api/stream.html#stream_readable_streams - const stream = createReadStream(); - - // This is purely for demonstration purposes and will overwrite the - // local-file-output.txt in the current working directory on EACH upload. - const out = require('fs').createWriteStream('local-file-output.txt'); - stream.pipe(out); - await finished(out); - - return { filename, mimetype, encoding }; - }, - }, -}; - -const app = require('fastify')({ - logger: true -}); - -const start = async () => { - try { - // Handle all requests that have the `Content-Type` header set as multipart - app.addContentTypeParser('multipart', (request, payload, done) => { - request.isMultipart = true; - done(); - }); - - // Format the request body to follow graphql-upload's - app.addHook('preValidation', async function (request, reply) { - if (!request.isMultipart) { - return; - } - - request.body = await processRequest(request.raw, reply.raw); - }); - - const server = new ApolloServer({ - typeDefs, - resolvers, - // Using graphql-upload without CSRF prevention is very insecure. - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], - }); - - // Start Apollo Server - await server.start(); - - app.register(server.createHandler()); - await app.listen(3000); - } catch (err) { - app.log.error(err); - process.exit(1); - } -}; - -start(); -``` diff --git a/docs/source/data/resolvers.mdx b/docs/source/data/resolvers.mdx index c6e602ded7d..c6bb537061d 100644 --- a/docs/source/data/resolvers.mdx +++ b/docs/source/data/resolvers.mdx @@ -34,16 +34,16 @@ const resolvers = { }, numberSeven() { return 7; - } - } + }, + }, }; ``` #### As this example shows: -* You define all of your server's resolvers in a single JavaScript object (named `resolvers` above). This object is called the **resolver map**. -* The resolver map has top-level fields that correspond to your schema's types (such as `Query` above). -* Each resolver function belongs to whichever type its corresponding field belongs to. +- You define all of your server's resolvers in a single JavaScript object (named `resolvers` above). This object is called the **resolver map**. +- The resolver map has top-level fields that correspond to your schema's types (such as `Query` above). +- Each resolver function belongs to whichever type its corresponding field belongs to. ### Handling arguments @@ -68,34 +68,38 @@ To achieve this, our server needs access to user data. For this contrived exampl const users = [ { id: '1', - name: 'Elizabeth Bennet' + name: 'Elizabeth Bennet', }, { id: '2', - name: 'Fitzwilliam Darcy' - } + name: 'Fitzwilliam Darcy', + }, ]; ``` + + Now we can define a resolver for the `user` field, like so: ```js const resolvers = { Query: { user(parent, args, context, info) { - return users.find(user => user.id === args.id); - } - } -} + return users.find((user) => user.id === args.id); + }, + }, +}; ``` #### As this example shows: -* A resolver can optionally accept four positional arguments: `(parent, args, context, info)`. - * [Learn more about these arguments.](#resolver-arguments) -* The `args` argument is an object that contains all _GraphQL_ arguments that were provided for the field by the GraphQL operation. +- A resolver can optionally accept four positional arguments: `(parent, args, context, info)`. + - [Learn more about these arguments.](#resolver-arguments) +- The `args` argument is an object that contains all _GraphQL_ arguments that were provided for the field by the GraphQL operation. > Notice that this example _doesn't_ define resolvers for `User` fields (`id` and `name`). That's because the [default resolver](#default-resolvers) that Apollo Server creates for these fields does the right thing: it obtains the value directly from the object returned by the `user` resolver. @@ -108,10 +112,8 @@ The following example defines a hardcoded data set, a schema, and a resolver map ```javascript -const { ApolloServer, gql } = require('apollo-server'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; // Hardcoded data store const books = [ @@ -126,7 +128,7 @@ const books = [ ]; // Schema definition -const typeDefs = gql` +const typeDefs = `#graphql type Book { title: String author: String @@ -142,7 +144,7 @@ const resolvers = { Query: { books() { return books; - } + }, }, }; @@ -151,17 +153,12 @@ const resolvers = { const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], }); // Launch the server -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`); -}); +const { url } = await startStandaloneServer(server); + +console.log(`๐Ÿš€ Server listening at: ${url}`); ``` @@ -247,17 +244,15 @@ Here's a code sample that can resolve the query above with this resolver chain: ```javascript -const { ApolloServer, gql } = require('apollo-server'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; const libraries = [ { - branch: 'downtown' + branch: 'downtown', }, { - branch: 'riverside' + branch: 'riverside', }, ]; @@ -266,19 +261,18 @@ const books = [ { title: 'The Awakening', author: 'Kate Chopin', - branch: 'riverside' + branch: 'riverside', }, { title: 'City of Glass', author: 'Paul Auster', - branch: 'downtown' + branch: 'downtown', }, ]; // Schema definition -const typeDefs = gql` - -# A library has a branch and books +const typeDefs = `#graphql + # A library has a branch and books type Library { branch: String! books: [Book!] @@ -305,30 +299,27 @@ const typeDefs = gql` const resolvers = { Query: { libraries() { - // Return our hardcoded array of libraries return libraries; - } + }, }, Library: { books(parent) { - // Filter the hardcoded array of books to only include // books that are located at the correct branch - return books.filter(book => book.branch === parent.branch); - } + return books.filter((book) => book.branch === parent.branch); + }, }, Book: { - // The parent resolver (Library.books) returns an object with the // author's name in the "author" field. Return a JSON object containing // the name, because this field expects an object. author(parent) { return { - name: parent.author + name: parent.author, }; - } - } + }, + }, // Because Book.author returns an object with a "name" field, // Apollo Server's default resolver for Author.name will work. @@ -340,17 +331,12 @@ const resolvers = { const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], }); // Launch the server -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`); -}); +const { url } = await startStandaloneServer(server); + +console.log(`๐Ÿš€ Server listening at: ${url}`); ``` @@ -397,40 +383,69 @@ Resolver functions are passed four arguments: `parent`, `args`, `context`, and ` ### The `context` argument -The `context` argument is useful for passing things that any resolver might need, like [authentication scope](https://blog.apollographql.com/authorization-in-graphql-452b1c402a9), database connections, and custom fetch functions. If you're using [dataloaders to batch requests](./data-sources/#using-with-dataloader) across resolvers, you can attach them to the `context` as well. +> โš ๏ธ The Apollo Server 4 alpha doesn't currently support using [`RESTDataSource`](./migration#datasources), a class commonly used to fetch data from a database or a REST API. This feature is in active development, so check back frequently for updates. + +The `context` argument is useful for passing things that any resolver might need, like [authentication scope](https://blog.apollographql.com/authorization-in-graphql-452b1c402a9), database connections, and custom fetch functions. + + **Resolvers should never destructively modify the `context` argument.** This ensures consistency across all resolvers and prevents unexpected errors. -To provide an initial `context` to your resolvers, add a `context` initialization function to the `ApolloServer` constructor. This function is called with every request, so you can customize the context based on each request's details (such as HTTP headers). +To provide an initial `context` to your resolvers, add an asynchronous `context` initialization function to your integration function (e.g., `expressMiddleware` or `startStandaloneServer`). This function is called _with every request_, so you can customize the context based on each request's details (such as HTTP headers). -```js + -// Constructor -const server = new ApolloServer({ + + +```ts +import { GraphQLError } from 'graphql'; + +const resolvers = { + Query: { + // Example resolver + adminExample: (parent, args, context, info) => { + if (context.authScope !== ADMIN) { + throw new GraphQLError('not admin!', { + extensions: { code: 'UNAUTHENTICATED' } + }); + } + }, + }, +}; + +interface MyContext { // Context typing + authScope?: String; +} + +const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - context: ({ req }) => ({ - authScope: getScope(req.headers.authorization) - }), - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], }); -// Example resolver -(parent, args, context, info) => { - if(context.authScope !== ADMIN) throw new AuthenticationError('not admin'); - // Proceed -} +const { url } = await startStandaloneServer(server, { + // A named context function is required if you are not + // using ApolloServer + context: async ({req, res}) => ({ + authScope: getScope(req.headers.authorization), + }) +}); + ``` -> This example assumes you're using either the `apollo-server` or `apollo-server-express` package, both of which use Express. The fields of the object passed to your `context` function might differ if you're using middleware besides Express. [See the API reference for details.](../api/apollo-server/#middleware-specific-context-fields) -> ->For more information on middleware in general, see [Choosing an Apollo Server package](../integrations/middleware/). + + +> This example assumes you're using either the `startStandaloneServer` or `expressMiddleware`, both of which use Express under the hood. The fields of the object passed to your `context` function might differ if you're using a different integration. + +The `context` function should be *asynchronous* and return an object, which is then accessible to your server's resolvers and plugins (via the `contextValue` field). -Context initialization can be asynchronous, allowing database connections and other operations to complete: + + +Because the `context` initialization function is asynchronous, you can use it to establish database connections and wait for other operations to complete: ```js context: async () => ({ @@ -452,8 +467,7 @@ A resolver function's return value is treated differently by Apollo Server depen | Scalar / object |

A resolver can return a single value or an object, as shown in [Defining a resolver](#defining-a-resolver). This return value is passed down to any nested resolvers via the `parent` argument.

| | `Array` |

Return an array if and only if your schema indicates that the resolver's associated field contains a list.

After you return an array, Apollo Server executes nested resolvers for each item in the array.

| | `null` / `undefined` |

Indicates that the value for the field could not be found.

If your schema indicates that this resolver's field is nullable, then the operation result has a `null` value at the field's position.

If this resolver's field is _not_ nullable, Apollo Server sets the field's _parent_ to `null`. If necessary, this process continues up the resolver chain until it reaches a field that _is_ nullable. This ensures that a response never includes a `null` value for a non-nullable field. When this happens, the response's `errors` property will be populated with relevant errors concerning the nullability of that field.

| -| [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) |

Resolvers often perform asynchronous actions, such as fetching from a database or back-end API. To support this, a resolver can return a promise that resolves to any other supported return type.

| - +| [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises) |

Resolvers can be asynchronous and perform async actions, such as fetching from a database or back-end API. To support this, a resolver can return a promise that resolves to any other supported return type.

| ## Default resolvers diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index 79e40294912..8477c78795c 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -3,15 +3,15 @@ title: Subscriptions in Apollo Server description: Persistent GraphQL read operations --- -> **Apollo Server 3 removes built-in support for subscriptions.** You can reenable support as [described below](#enabling-subscriptions). +>**Apollo Server does not provide built-in support for subscriptions.** You can enable support for subscriptions as [described below](#enabling-subscriptions). > > **Subscriptions are not currently supported in [Apollo Federation](/federation/).** > -> This article has been updated to use the `graphql-ws` library to add support for subscriptions to Apollo Server. We no longer recommend using the previously documented `subscriptions-transport-ws`, because this library is not actively maintained. For more information about the differences between the two libraries, see [Switching from `subscriptions-transport-ws`](#switching-from-subscriptions-transport-ws). +> This article uses the `graphql-ws` library to add support for subscriptions to Apollo Server 4. We no longer recommend using the previously documented `subscriptions-transport-ws`, because this library is not actively maintained. For more information about the differences between the two libraries, see [Switching from `subscriptions-transport-ws`](#switching-from-subscriptions-transport-ws). **Subscriptions** are long-lasting GraphQL read operations that can update their result whenever a particular server-side event occurs. Most commonly, updated results are _pushed_ from the server to subscribing clients. For example, a chat application's server might use a subscription to push newly received messages to all clients in a particular chat room. -Because subscription updates are usually pushed by the server (instead of polled by the client), they usually use [the WebSocket protocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) instead of HTTP. +Because subscription updates are usually pushed by the server (instead of polled by the client), they generally use [the WebSocket protocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) instead of HTTP. > **Important:** Compared to queries and mutations, subscriptions are significantly more complex to implement. Before you begin, [confirm that your use case requires subscriptions](/react/data/subscriptions/#when-to-use-subscriptions). @@ -42,102 +42,94 @@ subscription PostFeed { ## Enabling subscriptions -> Beginning in Apollo Server 3, subscriptions are **not** supported by the "batteries-included" `apollo-server` package. To enable subscriptions, you must first [swap to the `apollo-server-express` package](../integrations/middleware/#swapping-out-apollo-server) (or any other Apollo Server integration package that supports subscriptions). + + +> Subscriptions are **not** supported by Apollo Server 4's `startStandaloneServer` function. To enable subscriptions, you must first [swap to using the `expressMiddleware` function](./migration#migrate-from-apollo-server-express) (or any other Apollo Server integration package that supports subscriptions). > -> The following steps assume you've already swapped to `apollo-server-express`. +> The following steps assume you've already swapped to `expressMiddleware`. To run both an Express app _and_ a separate WebSocket server for subscriptions, we'll create an `http.Server` instance that effectively wraps the two and becomes our new `listen`er. -1. Install `graphql-ws`, `ws`, `@graphql-tools/schema`, and `apollo-server-core`: - ```bash - npm install graphql-ws ws @graphql-tools/schema apollo-server-core - ``` +1. Install `graphql-ws`, `ws`, and `@graphql-tools/schema`: + + ```bash + npm install graphql-ws ws @graphql-tools/schema + ``` 2. Add the following imports to the file where you initialize your `ApolloServer` instance (we'll use these in later steps): - ```ts title="index.ts" - import { createServer } from 'http'; - import { - ApolloServerPluginDrainHttpServer, - ApolloServerPluginLandingPageLocalDefault, - } from "apollo-server-core"; - import { makeExecutableSchema } from '@graphql-tools/schema'; - import { WebSocketServer } from 'ws'; - import { useServer } from 'graphql-ws/lib/use/ws'; - ``` + ```ts title="index.ts" + import { createServer } from 'http'; + import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; + import { makeExecutableSchema } from '@graphql-tools/schema'; + import { WebSocketServer } from 'ws'; + import { useServer } from 'graphql-ws/lib/use/ws'; + ``` 3. Next, in order to set up both the HTTP and subscription servers, we need to first create an `http.Server`. Do this by passing your Express `app` to the `createServer` function, which we imported from the `http` module: - ```ts title="index.ts" - // This `app` is the returned value from `express()`. - const httpServer = createServer(app); - ``` + ```ts title="index.ts" + // This `app` is the returned value from `express()`. + const httpServer = createServer(app); + ``` 4. Create an instance of `GraphQLSchema` (if you haven't already). - > If you already pass the `schema` option to the `ApolloServer` constructor (instead of `typeDefs` and `resolvers`), you can skip this step. + > If you already pass the `schema` option to the `ApolloServer` constructor (instead of `typeDefs` and `resolvers`), you can skip this step. - The subscription server (which we'll instantiate next) doesn't take `typeDefs` and `resolvers` options. Instead, it takes an executable `GraphQLSchema`. We can pass this `schema` object to both the subscription server and `ApolloServer`. This way, we make sure that the same schema is being used in both places. + The subscription server (which we'll instantiate next) doesn't take `typeDefs` and `resolvers` options. Instead, it takes an executable `GraphQLSchema`. We can pass this `schema` object to both the subscription server and `ApolloServer`. This way, we make sure that the same schema is being used in both places. - ```ts title="index.ts" - const schema = makeExecutableSchema({ typeDefs, resolvers }); - // ... - const server = new ApolloServer({ - schema, - csrfPrevention: true, - cache: "bounded", - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], - }); - ``` + ```ts title="index.ts" + const schema = makeExecutableSchema({ typeDefs, resolvers }); + // ... + const server = new ApolloServer({ + schema, + }); + ``` 5. Create a `WebSocketServer` to use as your subscription server. - ```ts title="index.ts" - // Creating the WebSocket server - const wsServer = new WebSocketServer({ - // This is the `httpServer` we created in a previous step. - server: httpServer, - // Pass a different path here if your ApolloServer serves at - // a different path. - path: '/graphql', - }); - - // Hand in the schema we just created and have the - // WebSocketServer start listening. - const serverCleanup = useServer({ schema }, wsServer); - ``` - -6. Add [plugins](../integrations/plugins) to your `ApolloServer` constructor to shutdown both the HTTP server and the `WebSocketServer`: - - ```ts title="index.ts" - const server = new ApolloServer({ - schema, - csrfPrevention: true, - cache: "bounded", - plugins: [ - // Proper shutdown for the HTTP server. - ApolloServerPluginDrainHttpServer({ httpServer }), - - // Proper shutdown for the WebSocket server. - { - async serverWillStart() { - return { - async drainServer() { - await serverCleanup.dispose(); - }, - }; - }, - }, - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], - }); - ``` + ```ts title="index.ts" + // Creating the WebSocket server + const wsServer = new WebSocketServer({ + // This is the `httpServer` we created in a previous step. + server: httpServer, + // Pass a different path here if app.use + // serves expressMiddleware at a different path + path: '/graphql', + }); + + // Hand in the schema we just created and have the + // WebSocketServer start listening. + const serverCleanup = useServer({ schema }, wsServer); + ``` + +6. Add [plugins](/apollo-server/integrations/plugins) to your `ApolloServer` constructor to shutdown both the HTTP server and the `WebSocketServer`: + + ```ts title="index.ts" + const server = new ApolloServer({ + schema, + plugins: [ + // Proper shutdown for the HTTP server. + ApolloServerPluginDrainHttpServer({ httpServer }), + + // Proper shutdown for the WebSocket server. + { + async serverWillStart() { + return { + async drainServer() { + await serverCleanup.dispose(); + }, + }; + }, + }, + ], + }); + ``` 7. Finally, ensure you are `listen`ing to your `httpServer`. - Most Express applications call `app.listen(...)`, but for your setup change this to `httpServer.listen(...)` using the same arguments. This way, the server starts listening on the HTTP and WebSocket transports simultaneously. + Most Express applications call `app.listen(...)`, but for your setup change this to `httpServer.listen(...)` using the same arguments. This way, the server starts listening on the HTTP and WebSocket transports simultaneously. A completed example of setting up subscriptions is shown below: @@ -146,13 +138,16 @@ A completed example of setting up subscriptions is shown below: ```ts title="index.ts" -import { ApolloServer } from 'apollo-server-express'; +import { ApolloServer } from '@apollo/server'; +import { expressMiddleware } from '@apollo/server/express4'; +import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; import { createServer } from 'http'; import express from 'express'; -import { ApolloServerPluginDrainHttpServer } from "apollo-server-core"; import { makeExecutableSchema } from '@graphql-tools/schema'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; +import bodyParser from 'body-parser'; +import cors from 'cors'; import resolvers from './resolvers'; import typeDefs from './typeDefs'; @@ -176,8 +171,6 @@ const serverCleanup = useServer({ schema }, wsServer); // Set up ApolloServer. const server = new ApolloServer({ schema, - csrfPrevention: true, - cache: "bounded", plugins: [ // Proper shutdown for the HTTP server. ApolloServerPluginDrainHttpServer({ httpServer }), @@ -192,18 +185,21 @@ const server = new ApolloServer({ }; }, }, - ApolloServerPluginLandingPageLocalDefault({ embed: true }), ], }); + await server.start(); -server.applyMiddleware({ app }); +app.use( + '/graphql', + cors(), + bodyParser.json(), + expressMiddleware(server), +); const PORT = 4000; // Now that our HTTP server is fully set up, we can listen to it. httpServer.listen(PORT, () => { - console.log( - `Server is now running on http://localhost:${PORT}${server.graphqlPath}`, - ); + console.log(`Server is now running on http://localhost:${PORT}/graphql`); }); ``` @@ -223,7 +219,7 @@ const resolvers = { hello: { // Example using an async generator subscribe: async function* () { - for await (const word of ["Hello", "Bonjour", "Ciao"]) { + for await (const word of ['Hello', 'Bonjour', 'Ciao']) { yield { hello: word }; } }, @@ -243,7 +239,7 @@ The `subscribe` function **must** return an object of type `AsyncIterator`, a st > The `PubSub` class is **not** recommended for production environments, because it's an in-memory event system that only supports a single server instance. After you get subscriptions working in development, we strongly recommend switching it out for a different subclass of the abstract [`PubSubEngine` class](https://github.com/apollographql/graphql-subscriptions/blob/master/src/pubsub-engine.ts). Recommended subclasses are listed in [Production `PubSub` libraries](#production-pubsub-libraries). -Apollo Server uses a **publish-subscribe** (**pub/sub**) model to track events that update active subscriptions. The [`graphql-subscriptions` library](https://github.com/apollographql/graphql-subscriptions) provides the `PubSub` class as a basic in-memory event bus to help you get started: +You can use the **publish-subscribe** (**pub/sub**) model to track events that update active subscriptions. The [`graphql-subscriptions` library](https://github.com/apollographql/graphql-subscriptions) provides the `PubSub` class as a basic in-memory event bus to help you get started: To use the `graphql-subscriptions` package, first install it like so: @@ -252,6 +248,7 @@ npm install graphql-subscriptions ``` A `PubSub` instance enables your server code to both `publish` events to a particular label and listen for events associated with a particular label. We can create a `PubSub` instance like so: + ```js import { PubSub } from 'graphql-subscriptions'; @@ -266,15 +263,15 @@ You can publish an event using the `publish` method of a `PubSub` instance: pubsub.publish('POST_CREATED', { postCreated: { author: 'Ali Baba', - comment: 'Open sesame' - } + comment: 'Open sesame', + }, }); ``` -* The first parameter is the name of the event label you're publishing to, as a string. - * _You don't need to register a label name before publishing to it._ -* The second parameter is the payload associated with the event. - * _The payload should include whatever data is necessary for your resolvers to populate the associated `Subscription` field and its subfields._ +- The first parameter is the name of the event label you're publishing to, as a string. + - _You don't need to register a label name before publishing to it._ +- The second parameter is the payload associated with the event. + - _The payload should include whatever data is necessary for your resolvers to populate the associated `Subscription` field and its subfields._ When working with GraphQL subscriptions, you `publish` an event whenever a subscription's return value should be updated. One common cause of such an update is a mutation, but _any_ back-end logic might result in changes that should be `publish`ed. @@ -352,7 +349,7 @@ Sometimes, a client should only receive updated subscription data if that data m Let's say our server provides a `commentAdded` subscription, which should notify clients whenever a comment is added to a specified code repository. A client can execute a subscription that looks like this: ```graphql -subscription($repoName: String!){ +subscription ($repoName: String!) { commentAdded(repoFullName: $repoName) { id content @@ -377,31 +374,35 @@ const resolvers = { (payload, variables) => { // Only push an update if the comment is on // the correct repository for this operation - return (payload.commentAdded.repository_name === variables.repoFullName); + return ( + payload.commentAdded.repository_name === variables.repoFullName + ); }, ), - } + }, }, - // ...other resolvers... + // ...other resolvers... }; ``` The `withFilter` function takes two parameters: -* The first parameter is exactly the function you would use for `subscribe` if you _weren't_ applying a filter. -* The second parameter is a **filter function** that returns `true` if a subscription update _should_ be sent to a particular client, and `false` otherwise (`Promise` is also allowed). This function takes two parameters of its own: - * `payload` is the payload of the event that was published. - * `variables` is an object containing all arguments the client provided when initiating their subscription. +- The first parameter is exactly the function you would use for `subscribe` if you _weren't_ applying a filter. +- The second parameter is a **filter function** that returns `true` if a subscription update _should_ be sent to a particular client, and `false` otherwise (`Promise` is also allowed). This function takes two parameters of its own: + - `payload` is the payload of the event that was published. + - `variables` is an object containing all arguments the client provided when initiating their subscription. Use `withFilter` to make sure clients get exactly the subscription updates they want (and are allowed to receive). - ## Basic runnable example -An example server is available on [GitHub](https://github.com/apollographql/docs-examples/blob/main/apollo-server/v3/subscriptions-graphql-ws/src/index.ts) and CodeSandbox: +An example server is available on [GitHub](https://github.com/apollographql/docs-examples/blob/main/apollo-server/v4/subscriptions-graphql-ws/src/index.ts) and CodeSandbox: - - Edit server-subscriptions-as3 + + Edit server-subscriptions-as4 The server exposes one subscription (`numberIncremented`) that returns an integer that's incremented on the server every second. Here's an example subscription that you can run against your server: @@ -431,9 +432,9 @@ useServer( // Our GraphQL schema. schema, // Adding a context property lets you add data to your GraphQL operation context - context: (ctx, msg, args) => { - // You can define your own function for setting a dynamic context - // or provide a static value + context: (ctx, msg, args) => { + // You can define your own function for setting a dynamic context + // or provide a static value return getDynamicContext(ctx, msg, args); }, }, @@ -450,8 +451,8 @@ Below is an example of the common use case of extracting an authentication token ```ts const getDynamicContext = async (ctx, msg, args) => { // ctx is the graphql-ws Context where connectionParams live - if (ctx.connectionParams.authentication) { - const currentUser = await findUser(connectionParams.authentication); + if (ctx.connectionParams.authentication) { + const currentUser = await findUser(ctx.connectionParams.authentication); return { currentUser }; } // Otherwise let our resolvers know we don't have a current user @@ -462,15 +463,16 @@ useServer( { schema, // Adding a context property lets you add data to your GraphQL operation context - context: (ctx, msg, args) => { - // Returning an object will add that information to our - // GraphQL context, which all of our resolvers have access to. + context: (ctx, msg, args) => { + // Returning an object will add that information to our + // GraphQL context, which all of our resolvers have access to. return getDynamicContext(ctx, msg, args); }, }, wsServer, ); ``` + Putting it all together, if the `useServer.context` function returns an _object_, that object is passed to the GraphQL operation `context`, which is available to your resolvers. Note that the `context` option is called once per `subscription` request, not once per event emission. This means that in the above example, every time a client sends a `subscription` operation, we check their authentication token. @@ -502,11 +504,12 @@ useServer( wsServer, ); ``` + For more information and examples of using `onConnect` and `onDisconnect`, see the [`graphql-ws` documentation](https://github.com/enisdenjo/graphql-ws#ws-auth-handling). ### Example: Authentication with Apollo Client ->If you plan to use subscriptions with Apollo Client, ensure **[both your client and server subscription protocols are consistent](#switching-from-subscriptions-transport-ws)** for the subscription library you're using (this example uses the `graphql-ws` library). +> If you plan to use subscriptions with Apollo Client, ensure **[both your client and server subscription protocols are consistent](#switching-from-subscriptions-transport-ws)** for the subscription library you're using (this example uses the `graphql-ws` library). In Apollo Client, the `GraphQLWsLink` constructor supports adding information to `connectionParams` ([example](/react/data/subscriptions/#5-authenticate-over-websocket-optional)). Those `connectionParams` are sent to your server when it connects, allowing you to extract information from the client request by accessing `Context.connectionParams`. @@ -516,12 +519,14 @@ Let's suppose we create our subscription client like so: import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; -const wsLink = new GraphQLWsLink(createClient({ - url: 'ws://localhost:4000/subscriptions', - connectionParams: { - authentication: user.authToken, - }, -})); +const wsLink = new GraphQLWsLink( + createClient({ + url: 'ws://localhost:4000/subscriptions', + connectionParams: { + authentication: user.authToken, + }, + }), +); ``` The `connectionParams` argument (which contains the information provided by the client) is passed to your server, enabling you to validate the user's credentials. @@ -536,8 +541,8 @@ const findUser = async (authToken) => { }; const getDynamicContext = async (ctx, msg, args) => { - if (ctx.connectionParams.authentication) { - const currentUser = await findUser(connectionParams.authentication); + if (ctx.connectionParams.authentication) { + const currentUser = await findUser(ctx.connectionParams.authentication); return { currentUser }; } // Let the resolvers know we don't have a current user so they can @@ -550,8 +555,8 @@ useServer( { // Our GraphQL schema. schema, - context: (ctx, msg, args) => { - // This will be run every time the client sends a subscription request + context: (ctx, msg, args) => { + // This will be run every time the client sends a subscription request return getDynamicContext(ctx, msg, args); }, }, @@ -581,20 +586,19 @@ The following are community-created `PubSub` libraries for popular event-publish - [Azure SignalR Service](https://github.com/aaronpowell/graphql-azure-subscriptions) - [Azure ServiceBus](https://github.com/abdomohamed/graphql-azure-servicebus-subscriptions) - If none of these libraries fits your use case, you can also create your own `PubSubEngine` subclass. If you create a new open-source library, click **Edit on GitHub** to let us know about it! ## Switching from `subscriptions-transport-ws` ->If you use subscriptions with Apollo Client you must ensure **[both your client and server subscription protocols are consistent](/react/data/subscriptions/#choosing-a-subscription-library)** for the subscription library you're using. +> If you use subscriptions with Apollo Client you must ensure **[both your client and server subscription protocols are consistent](/react/data/subscriptions/#choosing-a-subscription-library)** for the subscription library you're using. This article previously demonstrated using the `subscriptions-transport-ws` library to set up subscriptions. However, this library is no longer actively maintained. You can still use it with Apollo Server, but we strongly recommend using [`graphql-ws`](https://www.npmjs.com/package/graphql-ws) instead. -For more information on using Apollo Server with `subscriptions-transport-ws`, you can view the [previous version of this article](https://github.com/apollographql/apollo-server/blob/4b82967c10a69aa104848a8c9d15c591b528918c/docs/source/data/subscriptions.mdx). +For details on how to switch from `subscriptions-transport-ws` to `graphql-ws`, follow the steps in the [Apollo Server 3 docs](/apollo-server/v3/data/subscriptions/#switching-to-graphql-ws). ### Updating subscription clients -If you intend to [switch from `subscriptions-transport-ws` to `graphql-ws`](#switching-to-graphql-ws) you will need to update the following clients: +If you intend to [switch from `subscriptions-transport-ws` to `graphql-ws`](/apollo-server/v3/data/subscriptions/#switching-to-graphql-ws) you will need to update the following clients: @@ -622,6 +626,7 @@ If you intend to [switch from `subscriptions-transport-ws` to `graphql-ws`](#swi @@ -633,7 +638,6 @@ If you intend to [switch from `subscriptions-transport-ws` to `graphql-ws`](#swi - - - -
[`subscriptions-transport-ws`](/studio/explorer/additional-features/#subscription-support) +
Use [`GraphQLWsLink`](/react/api/link/apollo-link-subscriptions/)
@@ -644,11 +648,11 @@ Use [`GraphQLWsLink`](/react/api/link/apollo-link-subscriptions/)
Use [`WebSocketLink`](/react/api/link/apollo-link-ws/) +
@@ -656,7 +660,6 @@ Use [`WebSocketLink`](/react/api/link/apollo-link-ws/) [`graphql_transport_ws`](/ios/api/ApolloWebSocket/enums/WebSocket.WSProtocol/#graphql_transport_ws)
@@ -667,11 +670,11 @@ Use [`WebSocketLink`](/react/api/link/apollo-link-ws/)
[`graphql_ws`](/ios/api/ApolloWebSocket/enums/WebSocket.WSProtocol/#graphql_ws) +
@@ -689,147 +692,10 @@ Use [`WebSocketLink`](/react/api/link/apollo-link-ws/) [`SubscriptionWsProtocol`](/kotlin/essentials/subscriptions/#customizing-your-websocket-protocol) +
- -### Switching to `graphql-ws` - -> The following steps assume you've already [swapped to the `apollo-server-express` package](../integrations/middleware/#swapping-out-apollo-server). - -To switch from `subscriptions-transport-ws` to `graphql-ws`, you can follow the steps below. - -1. Install `graphql-ws` and `ws` into your app: - ```bash - npm install graphql-ws ws - ``` - -2. Add the following imports to the file where you initialize your HTTP and subscription servers: - - ```ts title="index.ts" - import { WebSocketServer } from 'ws'; - import { useServer } from 'graphql-ws/lib/use/ws'; - ``` - -3. Instantiate a `WebSocketServer` to use as your new subscription server. This server will replace the `SubscriptionServer` from `subscriptions-transport-ws`. - - - -```ts {2} title="index.ts" - // Creating the WebSocket subscription server - const wsServer = new WebSocketServer({ - // This is the `httpServer` returned by createServer(app); - server: httpServer, - // Pass a different path here if your ApolloServer serves at - // a different path. - path: "/graphql", - }); - - // Passing in an instance of a GraphQLSchema and - // telling the WebSocketServer to start listening - const serverCleanup = useServer({ schema }, wsServer); -``` - -```ts title="Old Subscription Server" - const subscriptionServer = SubscriptionServer.create( - { - schema, - // These are imported from `graphql`. - execute, - subscribe, - }, - { - server: httpServer, - path: "/graphql", - }, - ); -``` - - - -If your `SubscriptionServer` extracts information from a subscription to use in your GraphQL operation [`context`](./resolvers/#the-context-argument), you can configure your new WebSocket server to do the same. - -For example, if your `SubscriptionServer` checks a client's authentication token and adds the current user to the GraphQL operation context: - -```ts title="Old Subscription Server" - const subscriptionServer = SubscriptionServer.create( - { - schema, - execute, - subscribe, - // This will check authentication every time a - // client first connects to the subscription server. - async onConnect(connectionParams) { - if (connectionParams.authentication) { - const currentUser = await findUser(connectionParams.authentication); - return { currentUser }; - } - throw new Error("Missing auth token!"); - }, - }, - { - server: httpServer, - path: "/graphql", - }, - ); -``` - -You can replicate this behavior by providing a `context` option to the first argument passed to the `useServer` function. - -In the example below, we check a client's authentication every time they send a `subscription` operation. If that user exists, we then add the current user to our GraphQL operation context: - -```ts title="New Subscription Server" -const getDynamicContext = async (ctx, msg, args) => { - // ctx is the `graphql-ws` Context where connectionParams live - if (ctx.connectionParams.authentication) { - const currentUser = await findUser(connectionParams.authentication); - return { currentUser }; - } - return { currentUser: null }; -}; - -useServer( - { - schema, - // Adding a context property lets you add data to your GraphQL operation context. - context: (ctx, msg, args) => { - // Returning an object here will add that information to our - // GraphQL context, which all of our resolvers have access to. - return getDynamicContext(ctx, msg, args); - }, - }, - wsServer, -); -``` - -See [Operation Context](#operation-context) for more examples of setting GraphQL operation `context`. - -4. Add a [plugin](../integrations/plugins) to your `ApolloServer` constructor to properly shutdown your new `WebSocketServer`: - -```ts title="index.ts" - const server = new ApolloServer({ - schema, - plugins: [ - // Proper shutdown for the WebSocket server. - { - async serverWillStart() { - return { - async drainServer() { - await serverCleanup.dispose(); - }, - }; - }, - }, - ], - }); -``` - -5. Ensure you are `listen`ing to your `httpServer` call. You can now remove the code related to your `SubscriptionServer`, start your HTTP server, and test that everything works. - -If everything is running smoothly, you can (and should) uninstall `subscriptions-transport-ws`. The final step is to [update all your subscription clients](#updating-subscription-clients) to ensure they use the same subscription protocol. - - - diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx index 91bbf89fe5d..452908fc30c 100644 --- a/docs/source/migration.mdx +++ b/docs/source/migration.mdx @@ -929,7 +929,7 @@ ApolloServerPluginUsageReporting({ In Apollo Server 3, you can specify a function to rewrite errors before sending them to Apollo's server via the `rewriteError` option to `ApolloServerPluginUsageReporting` (for monoliths) and `ApolloServerPluginInlineTrace` (for subgraphs). -In Apollo Server 4, you specify the same function as the `transform` option on the `sendErrorsInTrace` option to `ApolloServerPluginUsageReporting` and the `includeErrors` option to `ApolloServerPluginInlineTrace`. +In Apollo Server 4, you specify the same function as the `transform` option on the `sendErrorsInTraces` option to `ApolloServerPluginUsageReporting` and the `includeErrors` option to `ApolloServerPluginInlineTrace`. (Additionally, the [default behavior has changed to mask errors](#usage-reporting-and-inline-trace-plugins-mask-errors-by-default).) @@ -955,7 +955,7 @@ you can now write: // monoliths new ApolloServer({ plugins: [ApolloServerPluginUsageReporting({ - sendErrorsInTrace: { transform: rewriteError }, + sendErrorsInTraces: { transform: rewriteError }, })], // ... }) @@ -1592,7 +1592,7 @@ To restore the Apollo Server 3 behavior, you can pass `{ unmodified: true }` to // monoliths new ApolloServer({ plugins: [ApolloServerPluginUsageReporting({ - sendErrorsInTrace: { unmodified: true }, + sendErrorsInTraces: { unmodified: true }, })], // ... }) @@ -1606,7 +1606,7 @@ new ApolloServer({ }) ``` -(As [described above](#rewriteerror-plugin-option), the `rewriteError` option has been replaced by a `transform` option on `sendErrorsInTrace` or `includeErrors`.) +(As [described above](#rewriteerror-plugin-option), the `rewriteError` option has been replaced by a `transform` option on `sendErrorsInTraces` or `includeErrors`.) ## Renamed packages diff --git a/docs/source/schema/creating-directives.mdx b/docs/source/schema/creating-directives.mdx index 7411773621c..414a0c67032 100644 --- a/docs/source/schema/creating-directives.mdx +++ b/docs/source/schema/creating-directives.mdx @@ -19,8 +19,11 @@ type Query { > See an example implementation of this directive on CodeSandbox: > -> -> Edit upper-case-directive +> +> alt="Edit upper-case-directive" +> src="https://codesandbox.io/static/img/play-codesandbox.svg" +> /> > When you start up your app, you can use directives to transform your executable schema's behavior before you provide that schema to Apollo Server. For example, you can modify the resolver function for any decorated field (for the schema above, it could transform the `hello` resolver's original result to uppercase). @@ -35,9 +38,9 @@ directive @deprecated( ) on FIELD_DEFINITION | ENUM_VALUE ``` -* This is the definition for the `@deprecated` directive in the [GraphQL spec](https://spec.graphql.org/June2018/#sec--deprecated). -* The directive takes one optional argument (`reason`) with a default value (`"No longer supported"`). -* The directive can decorate any number of `FIELD_DEFINITION`s and `ENUM_VALUE`s in your schema. +- This is the definition for the `@deprecated` directive in the [GraphQL spec](https://spec.graphql.org/June2018/#sec--deprecated). +- The directive takes one optional argument (`reason`) with a default value (`"No longer supported"`). +- The directive can decorate any number of `FIELD_DEFINITION`s and `ENUM_VALUE`s in your schema. ### Supported locations @@ -60,6 +63,7 @@ The table below lists all available locations in a GraphQL schema. Your directiv ###### `SCALAR` `SCALAR_TYPE` + @@ -74,6 +78,7 @@ The definition of a [custom scalar](./custom-scalars/) ###### `OBJECT` `OBJECT_TYPE` + @@ -88,6 +93,7 @@ The definition of an [object type](./schema/#object-types/) ###### `FIELD_DEFINITION` `OBJECT_FIELD` + @@ -102,6 +108,7 @@ The definition of a field within any defined type _except_ an [input type](./sch ###### `ARGUMENT_DEFINITION` `ARGUMENT` + @@ -116,6 +123,7 @@ The definition of a field argument ###### `INTERFACE` `INTERFACE_TYPE` + @@ -130,6 +138,7 @@ The definition of an [interface](unions-interfaces/#interface-type) ###### `UNION` `UNION_TYPE` + @@ -144,6 +153,7 @@ The definition of a [union](./unions-interfaces/#union-type) ###### `ENUM` `ENUM_TYPE` + @@ -158,6 +168,7 @@ The definition of an [enum](./schema/#enum-types) ###### `ENUM_VALUE` `ENUM_VALUE` + @@ -172,6 +183,7 @@ The definition of one value within an [enum](./schema/#enum-types) ###### `INPUT_OBJECT` `INPUT_OBJECT_TYPE` + @@ -186,6 +198,7 @@ The definition of an [input type](./schema/#input-types) ###### `INPUT_FIELD_DEFINITION` `INPUT_OBJECT_FIELD` + @@ -194,13 +207,13 @@ The definition of a field within an [input type](./schema/#input-types) - ###### `SCHEMA` `ROOT_OBJECT` + @@ -213,7 +226,7 @@ The top-level `schema` object declaration with `query`, `mutation`, and/or `subs ## Implementing -> **Important:** Apollo Server 3 does not provide built-in support for custom directives. To enable this support, you need to install certain `@graphql-tools` libraries. +> **Important:** Apollo Server 4 does not provide built-in support for custom directives. To enable this support, you need to install certain `@graphql-tools` libraries. > > This article uses `@graphql-tools` version 8. Previous versions use a different API for custom directives. If you're using an earlier version of `@graphql-tools`, see the [Apollo Server v2 docs](/apollo-server/v2/schema/creating-directives). @@ -235,15 +248,18 @@ To define what Apollo Server does when it encounters your directive, you can cre For example, here's a possible transformer function for the default `@deprecated` directive: -```js title="directives.js" -const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils'); +```ts title="directives.ts" +import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils'; function deprecatedDirectiveTransformer(schema, directiveName) { - return mapSchema(schema, { - + return mapSchema(schema, { // Executes once for each object field definition in the schema [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - const deprecatedDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; + const deprecatedDirective = getDirective( + schema, + fieldConfig, + directiveName, + )?.[0]; if (deprecatedDirective) { fieldConfig.deprecationReason = deprecatedDirective['reason']; return fieldConfig; @@ -252,14 +268,18 @@ function deprecatedDirectiveTransformer(schema, directiveName) { // Executes once for each enum value definition in the schema [MapperKind.ENUM_VALUE]: (enumValueConfig) => { - const deprecatedDirective = getDirective(schema, enumValueConfig, directiveName)?.[0]; + const deprecatedDirective = getDirective( + schema, + enumValueConfig, + directiveName, + )?.[0]; if (deprecatedDirective) { enumValueConfig.deprecationReason = deprecatedDirective['reason']; return enumValueConfig; } - } + }, }); -}; +} ``` As shown, the second parameter you pass `mapSchema` is an object with keys that represent one or more locations in your schema. The `MapperKind` enum value for each supported location is listed in [the table above](#supported-locations). @@ -272,17 +292,17 @@ This example defines an `@uppercase` directive for this purpose: -```js title="index.js" -const { ApolloServer, gql } = require('apollo-server'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); -const { makeExecutableSchema } = require('@graphql-tools/schema'); -const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils'); -const { defaultFieldResolver } = require('graphql'); + + +```ts title="index.ts" +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils'; +import { defaultFieldResolver } from 'graphql'; // Our GraphQL schema -const typeDefs = gql` +const typeDefs = `#graphql directive @upper on FIELD_DEFINITION type Query { @@ -295,8 +315,8 @@ const resolvers = { Query: { hello() { return 'Hello World!'; - } - } + }, + }, }; // This function takes in a schema and adds upper-casing logic @@ -304,57 +324,53 @@ const resolvers = { // the specified name (we're using `upper`) function upperDirectiveTransformer(schema, directiveName) { return mapSchema(schema, { - // Executes once for each object field in the schema [MapperKind.OBJECT_FIELD]: (fieldConfig) => { - // Check whether this field has the specified directive - const upperDirective = getDirective(schema, fieldConfig, directiveName)?.[0]; + const upperDirective = getDirective( + schema, + fieldConfig, + directiveName, + )?.[0]; if (upperDirective) { - // Get this field's original resolver const { resolve = defaultFieldResolver } = fieldConfig; // Replace the original resolver with a function that *first* calls // the original resolver, then converts its result to upper case - fieldConfig.resolve = async function (source, args, context, info) { + fieldConfig.resolve = async function (source, args, contextValue, info) { const result = await resolve(source, args, context, info); if (typeof result === 'string') { return result.toUpperCase(); } return result; - } + }; return fieldConfig; } - } + }, }); } // Create the base executable schema let schema = makeExecutableSchema({ typeDefs, - resolvers + resolvers, }); // Transform the schema by applying directive logic schema = upperDirectiveTransformer(schema, 'upper'); // Provide the schema to the ApolloServer constructor -const server = new ApolloServer({ - schema, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], -}); +const server = new ApolloServer({ schema }); -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`); -}); +const { url } = await startStandaloneServer(server); + +console.log(`๐Ÿš€ Server listening at: ${url}`); ``` + + This code _replaces_ the resolver of an `@uppercase` field with a new function. This new function first calls the _original_ resolver, then transforms its result to uppercase (assuming it's a string) before returning it. diff --git a/docs/source/schema/custom-scalars.md b/docs/source/schema/custom-scalars.mdx similarity index 72% rename from docs/source/schema/custom-scalars.md rename to docs/source/schema/custom-scalars.mdx index 4deff68263f..e25c6d5d144 100644 --- a/docs/source/schema/custom-scalars.md +++ b/docs/source/schema/custom-scalars.mdx @@ -20,10 +20,10 @@ However, Apollo Server still needs to know how to interact with values of this n After you define a custom scalar type, you need to define how Apollo Server interacts with it. In particular, you need to define: -* How the scalar's value is represented in your backend - * _This is often the representation used by the driver for your backing data store._ -* How the value's back-end representation is **serialized** to a JSON-compatible type -* How the JSON-compatible representation is **deserialized** to the back-end representation +- How the scalar's value is represented in your backend + - _This is often the representation used by the driver for your backing data store._ +- How the value's back-end representation is **serialized** to a JSON-compatible type +- How the JSON-compatible representation is **deserialized** to the back-end representation You define these interactions in an instance of the [`GraphQLScalarType`](http://graphql.org/graphql-js/type/#graphqlscalartype) class. @@ -31,34 +31,42 @@ You define these interactions in an instance of the [`GraphQLScalarType`](http:/ ## Example: The `Date` scalar + + The following `GraphQLScalarType` object defines interactions for a custom scalar that represents a date (this is one of the most commonly implemented custom scalars). It assumes that our backend represents a date with the `Date` JavaScript object. -```js -const { GraphQLScalarType, Kind } = require('graphql'); + + +```ts +import { GraphQLScalarType, Kind } from 'graphql'; const dateScalar = new GraphQLScalarType({ name: 'Date', description: 'Date custom scalar type', - serialize(value) { + serialize(value: Date) { return value.getTime(); // Convert outgoing Date to integer for JSON }, - parseValue(value) { + parseValue(value: number) { return new Date(value); // Convert incoming integer to Date }, parseLiteral(ast) { if (ast.kind === Kind.INT) { - return new Date(parseInt(ast.value, 10)); // Convert hard-coded AST string to integer and then to Date + // Convert hard-coded AST string to integer and then to Date + return new Date(parseInt(ast.value, 10)); } - return null; // Invalid hard-coded value (not an integer) + // Invalid hard-coded value (not an integer) + return null; }, }); ``` + + This initialization defines the following methods: -* `serialize` -* `parseValue` -* `parseLiteral` +- `serialize` +- `parseValue` +- `parseLiteral` Together, these methods describe how Apollo Server interacts with the scalar in every scenario. @@ -86,14 +94,14 @@ In [the example above](#example-the-date-scalar), `parseLiteral` converts the AS After you define your `GraphQLScalarType` instance, you include it in the same [resolver map](../data/resolvers/#defining-a-resolver) that contains resolvers for your schema's other types and fields: -```js {21-24} -const { ApolloServer, gql } = require('apollo-server'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); -const { GraphQLScalarType, Kind } = require('graphql'); + -const typeDefs = gql` +```ts {6, 22-25} +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { GraphQLScalarType, Kind } from 'graphql'; + +const typeDefs = `#graphql scalar Date type Event { @@ -111,34 +119,35 @@ const dateScalar = new GraphQLScalarType({ }); const resolvers = { - Date: dateScalar + Date: dateScalar, // ...other resolver definitions... }; const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], }); + +const { url } = await startStandaloneServer(server); + +console.log(`๐Ÿš€ Server listening at: ${url}`); ``` + + ## Example: Restricting integers to odd values In this example, we create a custom scalar called `Odd` that can only contain odd integers: -```js title="index.js" -const { ApolloServer, gql, UserInputError } = require('apollo-server'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); -const { GraphQLScalarType, Kind } = require('graphql'); + + +```ts title="index.ts" +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { GraphQLScalarType, Kind, GraphQLError } from 'graphql'; // Basic schema -const typeDefs = gql` +const typeDefs = `#graphql scalar Odd type Query { @@ -148,11 +157,13 @@ const typeDefs = gql` `; // Validation function for checking "oddness" -function oddValue(value) { +function oddValue(value: number) { if (typeof value === 'number' && Number.isInteger(value) && value % 2 !== 0) { return value; } - throw new UserInputError('Provided value is not an odd integer'); + throw new GraphQLError('Provided value is not an odd integer', { + extensions: { code: 'BAD_USER_INPUT' }, + }); } const resolvers = { @@ -165,31 +176,30 @@ const resolvers = { if (ast.kind === Kind.INT) { return oddValue(parseInt(ast.value, 10)); } - throw new UserInputError('Provided value is not an odd integer'); + throw new GraphQLError('Provided value is not an odd integer', { + extensions: { code: 'BAD_USER_INPUT' }, + }); }, }), Query: { - echoOdd(_, {odd}) { + echoOdd(_, { odd }) { return odd; - } - } + }, + }, }; const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], }); -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`) -}); +const { url } = await startStandaloneServer(server); + +console.log(`๐Ÿš€ Server listening at: ${url}`); ``` + + ## Importing a third-party custom scalar If another library defines a custom scalar, you can import it and use it just like any other symbol. @@ -202,16 +212,14 @@ First, install the library: $ npm install graphql-type-json ``` -Then `require` the `GraphQLJSON` object and add it to the resolver map as usual: +Then import the `GraphQLJSON` object and add it to the resolver map as usual: -```js -const { ApolloServer, gql } = require('apollo-server'); -const { - ApolloServerPluginLandingPageLocalDefault, -} = require('apollo-server-core'); -const GraphQLJSON = require('graphql-type-json'); +```ts +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; +import { GraphQLJSONObject } from 'graphql-type-json'; -const typeDefs = gql` +const typeDefs = `#graphql scalar JSON type MyObject { @@ -224,21 +232,16 @@ const typeDefs = gql` `; const resolvers = { - JSON: GraphQLJSON + JSON: GraphQLJSON, // ...other resolvers... }; const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], }); -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`) -}); +const { url } = await startStandaloneServer(server); + +console.log(`๐Ÿš€ Server listening at: ${url}`); ``` diff --git a/docs/source/schema/directives.md b/docs/source/schema/directives.md index 8336da6fb0e..53048507190 100644 --- a/docs/source/schema/directives.md +++ b/docs/source/schema/directives.md @@ -5,7 +5,7 @@ description: Configure GraphQL types, fields, and arguments > Looking for Apollo Federation directives? See [Federation-specific GraphQL directives](/federation/federated-types/federated-directives/). -A **directive** decorates part of a GraphQL schema or operation with additional configuration. Tools like Apollo Server (and [Apollo Client](https://www.apollographql.com/docs/react/local-state/managing-state-with-field-policies/#querying)) can read a GraphQL document's directives and perform custom logic as appropriate. +A **directive** decorates part of a GraphQL schema or operation with additional configuration. Tools like Apollo Server (and [Apollo Client](/react/local-state/managing-state-with-field-policies/#querying)) can read a GraphQL document's directives and perform custom logic as appropriate. Directives are preceded by the `@` character, like so: @@ -18,8 +18,8 @@ type ExampleType { This example shows the `@deprecated` directive, which is a [default directive](#default-directives) (i.e., it's part of the [GraphQL specification](http://spec.graphql.org/June2018/#sec--deprecated)). It demonstrates the following about directives: -* Directives can take arguments of their own (`reason` in this case). -* Directives appear _after_ the declaration of what they decorate (the `oldField` field in this case) +- Directives can take arguments of their own (`reason` in this case). +- Directives appear _after_ the declaration of what they decorate (the `oldField` field in this case) ## Valid locations @@ -39,7 +39,7 @@ This indicates that `@deprecated` can decorate any of the four listed locations. # ARGUMENT_DEFINITION # Note: @deprecated arguments _must_ be optional. directive @withDeprecatedArgs( - deprecatedArg: String @deprecated(reason: "Use `newArg`"), + deprecatedArg: String @deprecated(reason: "Use `newArg`") newArg: String ) on FIELD diff --git a/docs/source/schema/schema.md b/docs/source/schema/schema.md index ef049ddb4e9..c71a1b07dfb 100644 --- a/docs/source/schema/schema.md +++ b/docs/source/schema/schema.md @@ -37,7 +37,7 @@ _Most_ of the schema types you define have one or more **fields**: ```graphql # This Book type has two fields: title and author type Book { - title: String # returns a String + title: String # returns a String author: Author # returns an Author } ``` @@ -78,22 +78,21 @@ type Author { } ``` -* If `!` appears _inside_ the square brackets, the returned list can't include _items_ that are `null`. -* If `!` appears _outside_ the square brackets, _the list itself_ can't be `null`. -* In _any_ case, it's valid for a list field to return an _empty_ list. - +- If `!` appears _inside_ the square brackets, the returned list can't include _items_ that are `null`. +- If `!` appears _outside_ the square brackets, _the list itself_ can't be `null`. +- In _any_ case, it's valid for a list field to return an _empty_ list. ## Supported types Every type definition in a GraphQL schema belongs to one of the following categories: -* [Scalar](#scalar-types) -* [Object](#object-types) - * This includes the three special **root operation types**: [`Query`](#the-query-type), [`Mutation`](#the-mutation-type), and [`Subscription`](#the-subscription-type). -* [Input](#input-types) -* [Enum](#enum-types) -* [Union](#union-and-interface-types) -* [Interface](#union-and-interface-types) +- [Scalar](#scalar-types) +- [Object](#object-types) + - This includes the three special **root operation types**: [`Query`](#the-query-type), [`Mutation`](#the-mutation-type), and [`Subscription`](#the-subscription-type). +- [Input](#input-types) +- [Enum](#enum-types) +- [Union](#union-and-interface-types) +- [Interface](#union-and-interface-types) Each of these is described below. @@ -103,11 +102,11 @@ Scalar types are similar to primitive types in your favorite programming languag GraphQL's default scalar types are: -* `Int`: A signed 32โ€bit integer -* `Float`: A signed double-precision floating-point value -* `String`: A UTFโ€8 character sequence -* `Boolean`: `true` or `false` -* `ID` (serialized as a `String`): A unique identifier that's often used to refetch an object or as the key for a cache. Although it's serialized as a `String`, an `ID` is not intended to be humanโ€readable. +- `Int`: A signed 32โ€bit integer +- `Float`: A signed double-precision floating-point value +- `String`: A UTFโ€8 character sequence +- `Boolean`: `true` or `false` +- `ID` (serialized as a `String`): A unique identifier that's often used to refetch an object or as the key for a cache. Although it's serialized as a `String`, an `ID` is not intended to be humanโ€readable. These primitive types cover the majority of use cases. For more specific use cases, you can create [custom scalar types](./custom-scalars/). @@ -369,20 +368,20 @@ Sometimes, a backend forces a different value for an enum internally than in the The following example uses color hex codes for each `AllowedColor`'s internal value: -```js +```ts const resolvers = { AllowedColor: { RED: '#f00', GREEN: '#0f0', BLUE: '#00f', - } + }, // ...other resolver definitions... }; ``` These internal values don't change the public API at all. Apollo Server resolvers accept these values instead of the schema values, as shown: -```js +```ts const resolvers = { AllowedColor: { RED: '#f00', @@ -394,7 +393,7 @@ const resolvers = { avatar: (parent, args) => { // args.borderColor is '#f00', '#0f0', or '#00f' }, - } + }, }; ``` @@ -408,10 +407,10 @@ As your organization grows and evolves, your graph grows and evolves with it. Ne Most _additive_ changes to a schema are safe and backward compatible. However, changes that remove or alter _existing_ behavior might be _breaking_ changes for one or more of your existing clients. All of the following schema changes are potentially breaking changes: -* Removing a type or field -* Renaming a type or field -* Adding [nullability](#field-nullability) to a field -* Removing a field's arguments +- Removing a type or field +- Renaming a type or field +- Adding [nullability](#field-nullability) to a field +- Removing a field's arguments A graph management tool such as [Apollo Studio](https://studio.apollographql.com/) helps you understand whether a potential schema change will impact any of your active clients. Studio also provides field-level performance metrics, schema history tracking, and advanced security via operation safelisting. @@ -437,16 +436,16 @@ type MyObjectType { } ``` -A well documented schema helps provide an enhanced development experience, because GraphQL development tools (such as [Apollo Explorer](https://www.apollographql.com/docs/studio/explorer/)) auto-complete field names along with descriptions when they're provided. Furthermore, [Apollo Studio](https://studio.apollographql.com/) displays descriptions alongside field usage and performance details when using its metrics reporting and client awareness features. +A well documented schema helps provide an enhanced development experience, because GraphQL development tools (such as [the Apollo Studio Explorer](https://www.apollographql.com/docs/studio/explorer/)) auto-complete field names along with descriptions when they're provided. Furthermore, [Apollo Studio](https://studio.apollographql.com/) displays descriptions alongside field usage and performance details when using its metrics reporting and client awareness features. ## Naming conventions The GraphQL specification is flexible and doesn't impose specific naming guidelines. However, it's helpful to establish a set of conventions to ensure consistency across your organization. We recommend the following: -* **Field names** should use `camelCase`. Many GraphQL clients are written in JavaScript, Java, Kotlin, or Swift, all of which recommend `camelCase` for variable names. -* **Type names** should use `PascalCase`. This matches how classes are defined in the languages mentioned above. -* **Enum names** should use `PascalCase`. -* **Enum values** should use `ALL_CAPS`, because they are similar to constants. +- **Field names** should use `camelCase`. Many GraphQL clients are written in JavaScript, Java, Kotlin, or Swift, all of which recommend `camelCase` for variable names. +- **Type names** should use `PascalCase`. This matches how classes are defined in the languages mentioned above. +- **Enum names** should use `PascalCase`. +- **Enum values** should use `ALL_CAPS`, because they are similar to constants. These conventions help ensure that most clients don't need to define extra logic to transform the results returned by your server. @@ -527,7 +526,7 @@ A client could then execute a mutation against the schema with the following str ```graphql mutation updateMyUser { - updateUserEmail(id: 1, email: "jane@example.com"){ + updateUserEmail(id: 1, email: "jane@example.com") { id name email @@ -601,10 +600,10 @@ Our `updateUserEmail` mutation would specify `UpdateUserEmailMutationResponse` a Letโ€™s break this down field by field: -* `code` is a string that represents the status of the data transfer. Think of it like an HTTP status code. -* `success` is a boolean that indicates whether the mutation was successful. This allows a coarse check by the client to know if there were failures. -* `message` is a human-readable string that describes the result of the mutation. It is intended to be used in the UI of the product. -* `user` is added by the implementing type `UpdateUserEmailMutationResponse` to return the newly updated user to the client. +- `code` is a string that represents the status of the data transfer. Think of it like an HTTP status code. +- `success` is a boolean that indicates whether the mutation was successful. This allows a coarse check by the client to know if there were failures. +- `message` is a human-readable string that describes the result of the mutation. It is intended to be used in the UI of the product. +- `user` is added by the implementing type `UpdateUserEmailMutationResponse` to return the newly updated user to the client. If a mutation modifies _multiple_ types (like our earlier example of "liking" a blog post), its implementing type can include a separate field for each type that's modified: diff --git a/docs/source/schema/unions-interfaces.md b/docs/source/schema/unions-interfaces.md index e92c3eb507b..5a844227bdf 100644 --- a/docs/source/schema/unions-interfaces.md +++ b/docs/source/schema/unions-interfaces.md @@ -91,10 +91,8 @@ Here's a valid result for the above query: } ``` - For more information, see [Using fragments with unions and interfaces](https://www.apollographql.com/docs/react/data/fragments/#using-fragments-with-unions-and-interfaces). - ### Resolving a union > Before reading this section, [learn about resolvers](../data/resolvers/). @@ -103,12 +101,12 @@ To fully resolve a union, Apollo Server needs to specify _which_ of the union's The `__resolveType` function is responsible for determining an object's corresponding GraphQL type and returning the name of that type as a string. It can use any logic to do so, such as: -* Checking for the presence or absence of fields that are unique to a particular type in the union -* Using `instanceof`, if the _JavaScript_ object's type is related to its _GraphQL_ object type +- Checking for the presence or absence of fields that are unique to a particular type in the union +- Using `instanceof`, if the _JavaScript_ object's type is related to its _GraphQL_ object type Here's a basic `__resolveType` function for the `SearchResult` union defined above: -```js {3-13} +```ts {3-13} const resolvers = { SearchResult: { __resolveType(obj, context, info){ @@ -131,16 +129,11 @@ const resolvers = { const server = new ApolloServer({ typeDefs, resolvers, - csrfPrevention: true, - cache: 'bounded', - plugins: [ - ApolloServerPluginLandingPageLocalDefault({ embed: true }), - ], }); -server.listen().then(({ url }) => { - console.log(`๐Ÿš€ Server ready at ${url}`) -}); +const { url } = await startStandaloneServer(server); + +console.log(`๐Ÿš€ Server ready at: ${url}`); ``` > If a `__resolveType` function returns any value that _isn't_ the name of a valid type, the associated operation produces a GraphQL error. @@ -227,7 +220,8 @@ query GetBooks { __typename title ... on Textbook { - courses { # Only present in Textbook + courses { + # Only present in Textbook name } } @@ -260,9 +254,7 @@ Here's a valid result for the above query: { "__typename": "ColoringBook", "title": "Oops All Water", - "colors": [ - "Blue" - ] + "colors": ["Blue"] } ] } @@ -271,7 +263,6 @@ Here's a valid result for the above query: For more information, see [Using fragments with unions and interfaces](https://www.apollographql.com/docs/react/data/fragments/#using-fragments-with-unions-and-interfaces). - ### Resolving an interface > Before reading this section, [learn about resolvers](../data/resolvers/). @@ -280,7 +271,7 @@ For more information, see [Using fragments with unions and interfaces](https://w Here's an example `__resolveType` function for the `Book` interface defined above: -```js {3-13} +```ts {3-13} const resolvers = { Book: { __resolveType(book, context, info){