diff --git a/.changeset/tricky-fans-serve.md b/.changeset/tricky-fans-serve.md new file mode 100644 index 00000000000..298307c7844 --- /dev/null +++ b/.changeset/tricky-fans-serve.md @@ -0,0 +1,7 @@ +--- +'@keystone-next/website': minor +'@keystone-next/keystone': minor +'@keystone-next/types': minor +--- + +Added a `config.graphql.apolloConfig` option to allow developers to configure the `ApolloServer` object provided by Keystone. diff --git a/docs-next/pages/apis/config.mdx b/docs-next/pages/apis/config.mdx index 823071918f1..333dbeafb12 100644 --- a/docs-next/pages/apis/config.mdx +++ b/docs-next/pages/apis/config.mdx @@ -267,11 +267,14 @@ Options: - `queryLimits` (default: `undefined`): Allows you to limit the total number of results returned from a query to your GraphQL API. See also the per-list `graphql.queryLimits` option in the [Schema API](./schema). +- `apolloConfig` (default: `undefined`): Allows you to pass extra options into the `ApolloServer` constructor. + See the [Apollo docs](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#constructor) for supported options. ```typescript export default config({ graphql: { queryLimits: { maxTotalResults: 100 }, + apolloConfig: { /* ... */ }, }, /* ... */ }); diff --git a/packages-next/admin-ui/src/templates/api.ts b/packages-next/admin-ui/src/templates/api.ts index bebb96ba3e5..74901a3de27 100644 --- a/packages-next/admin-ui/src/templates/api.ts +++ b/packages-next/admin-ui/src/templates/api.ts @@ -16,6 +16,7 @@ const apolloServer = createApolloServerMicro({ graphQLSchema, createContext, sessionStrategy: initializedKeystoneConfig.session ? initializedKeystoneConfig.session() : undefined, + apolloConfig: config.graphql?.apolloConfig, connectionPromise: keystone.connect(), }); diff --git a/packages-next/keystone/package.json b/packages-next/keystone/package.json index ed0a3983a69..e89be70abfd 100644 --- a/packages-next/keystone/package.json +++ b/packages-next/keystone/package.json @@ -38,6 +38,7 @@ "@types/uid-safe": "^2.1.2", "apollo-server-express": "^2.21.1", "apollo-server-micro": "^2.21.1", + "apollo-server-types": "^0.6.3", "cookie": "^0.4.1", "cors": "^2.8.5", "express": "^4.17.1", diff --git a/packages-next/keystone/src/lib/createApolloServer.ts b/packages-next/keystone/src/lib/createApolloServer.ts index 11ee7e75189..122b151dc74 100644 --- a/packages-next/keystone/src/lib/createApolloServer.ts +++ b/packages-next/keystone/src/lib/createApolloServer.ts @@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { GraphQLSchema } from 'graphql'; import { ApolloServer as ApolloServerMicro } from 'apollo-server-micro'; import { ApolloServer as ApolloServerExpress } from 'apollo-server-express'; +import type { Config } from 'apollo-server-express'; // @ts-ignore import { formatError } from '@keystone-next/keystone-legacy/lib/Keystone/format-error'; @@ -12,52 +13,73 @@ export const createApolloServerMicro = ({ graphQLSchema, createContext, sessionStrategy, + apolloConfig, connectionPromise, }: { graphQLSchema: GraphQLSchema; createContext: CreateContext; sessionStrategy?: SessionStrategy; + apolloConfig?: Config; connectionPromise: Promise; }) => { - return new ApolloServerMicro({ - uploads: false, - schema: graphQLSchema, - playground: { settings: { 'request.credentials': 'same-origin' } }, - formatError, - context: async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) => { - await connectionPromise; - return createContext({ - sessionContext: sessionStrategy - ? await createSessionContext(sessionStrategy, req, res, createContext) - : undefined, - req, - }); - }, - }); + const context = async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) => { + await connectionPromise; + return createContext({ + sessionContext: sessionStrategy + ? await createSessionContext(sessionStrategy, req, res, createContext) + : undefined, + req, + }); + }; + const serverConfig = _createApolloServerConfig({ graphQLSchema, apolloConfig }); + return new ApolloServerMicro({ ...serverConfig, context }); }; export const createApolloServerExpress = ({ graphQLSchema, createContext, sessionStrategy, + apolloConfig, }: { graphQLSchema: GraphQLSchema; createContext: CreateContext; sessionStrategy?: SessionStrategy; + apolloConfig?: Config; }) => { - return new ApolloServerExpress({ + const context = async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) => + createContext({ + sessionContext: sessionStrategy + ? await createSessionContext(sessionStrategy, req, res, createContext) + : undefined, + req, + }); + const serverConfig = _createApolloServerConfig({ graphQLSchema, apolloConfig }); + return new ApolloServerExpress({ ...serverConfig, context }); +}; + +const _createApolloServerConfig = ({ + graphQLSchema, + apolloConfig, +}: { + graphQLSchema: GraphQLSchema; + apolloConfig?: Config; +}) => { + // Playground config + const pp = apolloConfig?.playground; + let playground: Config['playground']; + const settings = { 'request.credentials': 'same-origin' }; + if (typeof pp === 'boolean' && !pp) { + playground = undefined; + } else if (typeof pp === 'undefined' || typeof pp === 'boolean') { + playground = { settings }; + } else { + playground = { ...pp, settings: { ...settings, ...pp.settings } }; + } + + return { uploads: false, schema: graphQLSchema, - // FIXME: allow the dev to control where/when they get a playground - playground: { settings: { 'request.credentials': 'same-origin' } }, formatError, // TODO: this needs to be discussed - context: async ({ req, res }: { req: IncomingMessage; res: ServerResponse }) => - createContext({ - sessionContext: sessionStrategy - ? await createSessionContext(sessionStrategy, req, res, createContext) - : undefined, - req, - }), // FIXME: support for apollo studio tracing // ...(process.env.ENGINE_API_KEY || process.env.APOLLO_KEY // ? { tracing: true } @@ -68,12 +90,11 @@ export const createApolloServerExpress = ({ // // disabled. // tracing: dev, // }), - // FIXME: Support for generic custom apollo configuration - // ...apolloConfig, - }); - // FIXME: Support custom API path via config.graphql.path. - // Note: Core keystone uses '/admin/api' as the default. - // FIXME: Support for file handling configuration - // maxFileSize: 200 * 1024 * 1024, - // maxFiles: 5, + ...apolloConfig, + // Carefully inject the playground + playground, + // FIXME: Support for file handling configuration + // maxFileSize: 200 * 1024 * 1024, + // maxFiles: 5, + }; }; diff --git a/packages-next/keystone/src/lib/createExpressServer.ts b/packages-next/keystone/src/lib/createExpressServer.ts index 18584039548..8201d09ea01 100644 --- a/packages-next/keystone/src/lib/createExpressServer.ts +++ b/packages-next/keystone/src/lib/createExpressServer.ts @@ -1,3 +1,4 @@ +import type { Config } from 'apollo-server-express'; import cors, { CorsOptions } from 'cors'; import express from 'express'; import { GraphQLSchema } from 'graphql'; @@ -11,14 +12,23 @@ const addApolloServer = ({ graphQLSchema, createContext, sessionStrategy, + apolloConfig, }: { server: express.Express; graphQLSchema: GraphQLSchema; createContext: CreateContext; sessionStrategy?: SessionStrategy; + apolloConfig?: Config; }) => { - const apolloServer = createApolloServerExpress({ graphQLSchema, createContext, sessionStrategy }); + const apolloServer = createApolloServerExpress({ + graphQLSchema, + createContext, + sessionStrategy, + apolloConfig, + }); server.use(graphqlUploadExpress()); + // FIXME: Support custom API path via config.graphql.path. + // Note: Core keystone uses '/admin/api' as the default. apolloServer.applyMiddleware({ app: server, path: '/api/graphql', cors: false }); }; @@ -45,7 +55,13 @@ export const createExpressServer = async ( const sessionStrategy = config.session ? config.session() : undefined; if (isVerbose) console.log('✨ Preparing GraphQL Server'); - addApolloServer({ server, graphQLSchema, createContext, sessionStrategy }); + addApolloServer({ + server, + graphQLSchema, + createContext, + sessionStrategy, + apolloConfig: config.graphql?.apolloConfig, + }); if (config.ui?.isDisabled) { if (isVerbose) console.log('✨ Skipping Admin UI app'); diff --git a/packages-next/types/package.json b/packages-next/types/package.json index de581d0c64f..58ec49dfc52 100644 --- a/packages-next/types/package.json +++ b/packages-next/types/package.json @@ -8,6 +8,7 @@ "node": ">=10.0.0" }, "dependencies": { + "apollo-server-types": "^0.6.3", "cors": "^2.8.5", "graphql": "^15.5.0" }, diff --git a/packages-next/types/src/config/index.ts b/packages-next/types/src/config/index.ts index 05e9a62fdd5..e466280f741 100644 --- a/packages-next/types/src/config/index.ts +++ b/packages-next/types/src/config/index.ts @@ -2,6 +2,7 @@ import type { ConnectOptions } from 'mongoose'; import { CorsOptions } from 'cors'; import type { GraphQLSchema } from 'graphql'; import { IncomingMessage } from 'http'; +import type { Config } from 'apollo-server-express'; import type { ListHooks } from './hooks'; import type { ListAccessControl, FieldAccessControl } from './access-control'; @@ -121,6 +122,11 @@ export type GraphQLConfig = { queryLimits?: { maxTotalResults?: number; }; + /** + * Additional options to pass into the ApolloServer constructor. + * @see https://www.apollographql.com/docs/apollo-server/api/apollo-server/#constructor + */ + apolloConfig?: Config; }; // config.extendGraphqlSchema