diff --git a/.changeset/sour-onions-serve.md b/.changeset/sour-onions-serve.md
new file mode 100644
index 00000000000..d5258a127ab
--- /dev/null
+++ b/.changeset/sour-onions-serve.md
@@ -0,0 +1,5 @@
+---
+'@keystone-next/keystone': minor
+---
+
+Added the config option `config.graphql.path` to configure the endpoint of the GraphQL API (default `'/api/graphql'`).
diff --git a/docs/pages/docs/apis/config.mdx b/docs/pages/docs/apis/config.mdx
index 88992f9b946..7e5bbefe702 100644
--- a/docs/pages/docs/apis/config.mdx
+++ b/docs/pages/docs/apis/config.mdx
@@ -287,6 +287,7 @@ Options:
These can be filtered out with `apolloConfig.formatError` if you need to process them, but do not want them returned over the GraphQL API.
- `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).
+- `path` (default: `'/api/graphql'`): The path of the GraphQL API endpoint.
- `apolloConfig` (default: `undefined`): Allows you to pass extra options into the `ApolloServer` constructor.
- `playground` (default: `process.env.NODE_ENV !== 'production'`): If truthy, will enable the GraphQL Playground for testing queries and mutations in the browser.
To configure behaviour, pass an object of [GraphQL Playground settings](https://github.com/graphql/graphql-playground#settings). See the [Apollo docs](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#constructor) for more supported options.
@@ -297,6 +298,7 @@ export default config({
graphql: {
debug: process.env.NODE_ENV !== 'production',
queryLimits: { maxTotalResults: 100 },
+ path: '/api/graphql',
apolloConfig: {
playground: process.env.NODE_ENV !== 'production',
introspection: process.env.NODE_ENV !== 'production',
diff --git a/examples-staging/basic/keystone.ts b/examples-staging/basic/keystone.ts
index 01b2395ff74..1ae053b3351 100644
--- a/examples-staging/basic/keystone.ts
+++ b/examples-staging/basic/keystone.ts
@@ -29,10 +29,6 @@ export default auth.withAuth(
provider: 'sqlite',
url: process.env.DATABASE_URL || 'file:./keystone-example.db',
},
- // NOTE -- this is not implemented, keystone currently always provides a graphql api at /api/graphql
- // graphql: {
- // path: '/api/graphql',
- // },
ui: {
// NOTE -- this is not implemented, keystone currently always provides an admin ui at /
// path: '/admin',
diff --git a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx
index 89695d0b655..0b50aa8ee90 100644
--- a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx
+++ b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.tsx
@@ -11,6 +11,7 @@ type AppConfig = {
adminMetaHash: string;
fieldViews: FieldViews;
lazyMetadataQuery: DocumentNode;
+ apiPath: string;
};
export const getApp =
diff --git a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/next-graphql.ts b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/next-graphql.ts
index 91565438128..1cf2125c87f 100644
--- a/packages/keystone/src/___internal-do-not-use-will-break-in-patch/next-graphql.ts
+++ b/packages/keystone/src/___internal-do-not-use-will-break-in-patch/next-graphql.ts
@@ -19,5 +19,5 @@ export function nextGraphQLAPIRoute(keystoneConfig: KeystoneConfig, prismaClient
connectionPromise: keystone.connect(),
});
- return apolloServer.createHandler({ path: '/api/graphql' });
+ return apolloServer.createHandler({ path: keystoneConfig.graphql?.path || '/api/graphql' });
}
diff --git a/packages/keystone/src/admin-ui/components/Navigation.tsx b/packages/keystone/src/admin-ui/components/Navigation.tsx
index 800e9d24621..aac48c67c08 100644
--- a/packages/keystone/src/admin-ui/components/Navigation.tsx
+++ b/packages/keystone/src/admin-ui/components/Navigation.tsx
@@ -61,6 +61,7 @@ export const NavItem = ({ href, children, isSelected: _isSelected }: NavItemProp
};
const AuthenticatedItemDialog = ({ item }: { item: AuthenticatedItem | undefined }) => {
+ const { apiPath } = useKeystone();
const { spacing, typography } = useTheme();
return (
- {/* FIXME: Use config.graphql.path */}
-
+
API Explorer
diff --git a/packages/keystone/src/admin-ui/context.tsx b/packages/keystone/src/admin-ui/context.tsx
index d5b1bfd86f7..35468e8da2a 100644
--- a/packages/keystone/src/admin-ui/context.tsx
+++ b/packages/keystone/src/admin-ui/context.tsx
@@ -24,6 +24,7 @@ type KeystoneContextType = {
visibleLists: VisibleLists;
createViewFieldModes: CreateViewFieldModes;
reinitContext: () => void;
+ apiPath: string;
};
const KeystoneContext = createContext(undefined);
@@ -34,6 +35,7 @@ type KeystoneProviderProps = {
adminMetaHash: string;
fieldViews: FieldViews;
lazyMetadataQuery: DocumentNode;
+ apiPath: string;
};
function InternalKeystoneProvider({
@@ -42,6 +44,7 @@ function InternalKeystoneProvider({
adminMetaHash,
children,
lazyMetadataQuery,
+ apiPath,
}: KeystoneProviderProps) {
const adminMeta = useAdminMeta(adminMetaHash, fieldViews);
const { authenticatedItem, visibleLists, createViewFieldModes, refetch } =
@@ -70,6 +73,7 @@ function InternalKeystoneProvider({
reinitContext,
visibleLists,
createViewFieldModes,
+ apiPath,
}}
>
{children}
@@ -84,10 +88,9 @@ export const KeystoneProvider = (props: KeystoneProviderProps) => {
() =>
new ApolloClient({
cache: new InMemoryCache(),
- // FIXME: Use config.graphql.path
- link: createUploadLink({ uri: '/api/graphql' }),
+ link: createUploadLink({ uri: props.apiPath }),
}),
- []
+ [props.apiPath]
);
return (
@@ -103,6 +106,7 @@ export const useKeystone = (): {
authenticatedItem: AuthenticatedItem;
visibleLists: VisibleLists;
createViewFieldModes: CreateViewFieldModes;
+ apiPath: string;
} => {
const value = useContext(KeystoneContext);
if (!value) {
@@ -124,6 +128,7 @@ export const useKeystone = (): {
authenticatedItem: value.authenticatedItem,
visibleLists: value.visibleLists,
createViewFieldModes: value.createViewFieldModes,
+ apiPath: value.apiPath,
};
};
diff --git a/packages/keystone/src/admin-ui/system/createAdminUIServer.ts b/packages/keystone/src/admin-ui/system/createAdminUIServer.ts
index a217ed48546..dfeec92dbc8 100644
--- a/packages/keystone/src/admin-ui/system/createAdminUIServer.ts
+++ b/packages/keystone/src/admin-ui/system/createAdminUIServer.ts
@@ -4,13 +4,14 @@ import type { KeystoneConfig, SessionStrategy, CreateContext } from '../../types
import { createSessionContext } from '../../session';
export const createAdminUIServer = async (
- ui: KeystoneConfig['ui'],
+ config: KeystoneConfig,
createContext: CreateContext,
dev: boolean,
projectAdminPath: string,
sessionStrategy?: SessionStrategy
) => {
/** We do this to stop webpack from bundling next inside of next */
+ const { ui, graphql } = config;
const thing = 'next';
const next = require(thing);
const app = next({ dev, dir: projectAdminPath });
@@ -20,7 +21,7 @@ export const createAdminUIServer = async (
const publicPages = ui?.publicPages ?? [];
return async (req: express.Request, res: express.Response) => {
const { pathname } = url.parse(req.url);
- if (pathname?.startsWith('/_next') || pathname === '/api/graphql') {
+ if (pathname?.startsWith('/_next') || pathname === (graphql?.path || '/api/graphql')) {
handle(req, res);
return;
}
diff --git a/packages/keystone/src/admin-ui/templates/api.ts b/packages/keystone/src/admin-ui/templates/api.ts
index 47e014bc370..f9274171114 100644
--- a/packages/keystone/src/admin-ui/templates/api.ts
+++ b/packages/keystone/src/admin-ui/templates/api.ts
@@ -18,5 +18,5 @@ export const config = {
bodyParser: false,
},
};
-export default apolloServer.createHandler({ path: '/api/graphql' });
+export default apolloServer.createHandler({ path: initializedKeystoneConfig.graphql?.path || '/api/graphql' });
`;
diff --git a/packages/keystone/src/admin-ui/templates/app.ts b/packages/keystone/src/admin-ui/templates/app.ts
index d6151f68936..5167fa4da3f 100644
--- a/packages/keystone/src/admin-ui/templates/app.ts
+++ b/packages/keystone/src/admin-ui/templates/app.ts
@@ -19,7 +19,8 @@ type AppTemplateOptions = { configFileExists: boolean; projectAdminPath: string
export const appTemplate = (
adminMetaRootVal: AdminMetaRootVal,
graphQLSchema: GraphQLSchema,
- { configFileExists, projectAdminPath }: AppTemplateOptions
+ { configFileExists, projectAdminPath }: AppTemplateOptions,
+ apiPath: string
) => {
const result = executeSync({
document: staticAdminMetaQuery,
@@ -53,7 +54,8 @@ export default getApp({
lazyMetadataQuery: ${JSON.stringify(getLazyMetadataQuery(graphQLSchema, adminMeta))},
fieldViews: [${allViews.map((_, i) => `view${i}`)}],
adminMetaHash: "${adminMetaQueryResultHash}",
- adminConfig: adminConfig
+ adminConfig: adminConfig,
+ apiPath: "${apiPath}",
});
`;
// -- TEMPLATE END
diff --git a/packages/keystone/src/admin-ui/templates/index.ts b/packages/keystone/src/admin-ui/templates/index.ts
index d6e7c7f34e5..cc2c1039c28 100644
--- a/packages/keystone/src/admin-ui/templates/index.ts
+++ b/packages/keystone/src/admin-ui/templates/index.ts
@@ -16,33 +16,49 @@ export const writeAdminFiles = (
adminMeta: AdminMetaRootVal,
configFileExists: boolean,
projectAdminPath: string
-): AdminFileToWrite[] => [
- ...['next.config.js', 'tsconfig.json'].map(
- outputPath =>
- ({ mode: 'copy', inputPath: Path.join(pkgDir, 'static', outputPath), outputPath } as const)
- ),
- { mode: 'write', src: noAccessTemplate(config.session), outputPath: 'pages/no-access.js' },
- {
- mode: 'write',
- src: appTemplate(adminMeta, graphQLSchema, { configFileExists, projectAdminPath }),
- outputPath: 'pages/_app.js',
- },
- { mode: 'write', src: homeTemplate, outputPath: 'pages/index.js' },
- ...adminMeta.lists.map(
- ({ path, key }) =>
- ({ mode: 'write', src: listTemplate(key), outputPath: `pages/${path}/index.js` } as const)
- ),
- ...adminMeta.lists.map(
- ({ path, key }) =>
- ({ mode: 'write', src: itemTemplate(key), outputPath: `pages/${path}/[id].js` } as const)
- ),
- ...(config.experimental?.enableNextJsGraphqlApiEndpoint
- ? [
- {
- mode: 'write' as const,
- src: apiTemplate,
- outputPath: 'pages/api/graphql.js',
- },
- ]
- : []),
-];
+): AdminFileToWrite[] => {
+ if (
+ config.experimental?.enableNextJsGraphqlApiEndpoint &&
+ config.graphql?.path &&
+ !config.graphql.path.startsWith('/api/')
+ ) {
+ throw new Error(
+ 'config.graphql.path must start with "/api/" when using config.experimental.enableNextJsGraphqlApiEndpoint'
+ );
+ }
+ return [
+ ...['next.config.js', 'tsconfig.json'].map(
+ outputPath =>
+ ({ mode: 'copy', inputPath: Path.join(pkgDir, 'static', outputPath), outputPath } as const)
+ ),
+ { mode: 'write', src: noAccessTemplate(config.session), outputPath: 'pages/no-access.js' },
+ {
+ mode: 'write',
+ src: appTemplate(
+ adminMeta,
+ graphQLSchema,
+ { configFileExists, projectAdminPath },
+ config.graphql?.path || '/api/graphql'
+ ),
+ outputPath: 'pages/_app.js',
+ },
+ { mode: 'write', src: homeTemplate, outputPath: 'pages/index.js' },
+ ...adminMeta.lists.map(
+ ({ path, key }) =>
+ ({ mode: 'write', src: listTemplate(key), outputPath: `pages/${path}/index.js` } as const)
+ ),
+ ...adminMeta.lists.map(
+ ({ path, key }) =>
+ ({ mode: 'write', src: itemTemplate(key), outputPath: `pages/${path}/[id].js` } as const)
+ ),
+ ...(config.experimental?.enableNextJsGraphqlApiEndpoint
+ ? [
+ {
+ mode: 'write' as const,
+ src: apiTemplate,
+ outputPath: `pages/${config.graphql?.path || '/api/graphql'}.js`,
+ },
+ ]
+ : []),
+ ];
+};
diff --git a/packages/keystone/src/lib/server/createApolloServer.ts b/packages/keystone/src/lib/server/createApolloServer.ts
index 33df36ca655..0948635c80d 100644
--- a/packages/keystone/src/lib/server/createApolloServer.ts
+++ b/packages/keystone/src/lib/server/createApolloServer.ts
@@ -61,7 +61,7 @@ const _createApolloServerConfig = ({
graphQLSchema: GraphQLSchema;
graphqlConfig?: GraphQLConfig;
}) => {
- // Playground config, is /api/graphql available?
+ // Playground config
const apolloConfig = graphqlConfig?.apolloConfig;
const apolloConfigPlayground = apolloConfig?.playground;
let playground: Config['playground'];
diff --git a/packages/keystone/src/lib/server/createExpressServer.ts b/packages/keystone/src/lib/server/createExpressServer.ts
index 922d4e92ec0..d4527de8381 100644
--- a/packages/keystone/src/lib/server/createExpressServer.ts
+++ b/packages/keystone/src/lib/server/createExpressServer.ts
@@ -33,9 +33,11 @@ const addApolloServer = ({
const maxFileSize = config.server?.maxFileSize || DEFAULT_MAX_FILE_SIZE;
server.use(graphqlUploadExpress({ maxFileSize }));
- // 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 });
+ apolloServer.applyMiddleware({
+ app: server,
+ path: config.graphql?.path || '/api/graphql',
+ cors: false,
+ });
};
export const createExpressServer = async (
@@ -75,7 +77,7 @@ export const createExpressServer = async (
} else {
if (isVerbose) console.log('✨ Preparing Admin UI Next.js app');
server.use(
- await createAdminUIServer(config.ui, createContext, dev, projectAdminPath, config.session)
+ await createAdminUIServer(config, createContext, dev, projectAdminPath, config.session)
);
}
diff --git a/packages/keystone/src/testing.ts b/packages/keystone/src/testing.ts
index 506680a98b8..e7c458ed40d 100644
--- a/packages/keystone/src/testing.ts
+++ b/packages/keystone/src/testing.ts
@@ -70,7 +70,7 @@ export async function setupTestEnv({
const graphQLRequest: GraphQLRequest = ({ query, variables = undefined, operationName }) =>
supertest(app)
- .post('/api/graphql')
+ .post(config.graphql?.path || '/api/graphql')
.send({ query, variables, operationName })
.set('Accept', 'application/json');
diff --git a/packages/keystone/src/types/config/index.ts b/packages/keystone/src/types/config/index.ts
index 1c131e8aec4..2a8b62e72ed 100644
--- a/packages/keystone/src/types/config/index.ts
+++ b/packages/keystone/src/types/config/index.ts
@@ -136,9 +136,8 @@ export type ServerConfig = {
// config.graphql
export type GraphQLConfig = {
- // FIXME: We currently hardcode `/api/graphql` in a bunch of places
- // We should be able to use config.graphql.path to set this path.
- // path?: string;
+ // The path of the GraphQL API endpoint. Default: '/api/graphql'.
+ path?: string;
queryLimits?: {
maxTotalResults?: number;
};