Skip to content

Commit

Permalink
Add config.graphql.path option (#6458)
Browse files Browse the repository at this point in the history
  • Loading branch information
timleslie authored Sep 3, 2021
1 parent bf9b560 commit 944bce1
Show file tree
Hide file tree
Showing 15 changed files with 83 additions and 54 deletions.
5 changes: 5 additions & 0 deletions .changeset/sour-onions-serve.md
Original file line number Diff line number Diff line change
@@ -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'`).
2 changes: 2 additions & 0 deletions docs/pages/docs/apis/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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',
Expand Down
4 changes: 0 additions & 4 deletions examples-staging/basic/keystone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type AppConfig = {
adminMetaHash: string;
fieldViews: FieldViews;
lazyMetadataQuery: DocumentNode;
apiPath: string;
};

export const getApp =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
4 changes: 2 additions & 2 deletions packages/keystone/src/admin-ui/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
Expand Down Expand Up @@ -95,8 +96,7 @@ const AuthenticatedItemDialog = ({ item }: { item: AuthenticatedItem | undefined
)}
>
<Stack gap="medium" padding="large" dividers="between">
{/* FIXME: Use config.graphql.path */}
<PopoverLink target="_blank" href="/api/graphql">
<PopoverLink target="_blank" href={apiPath}>
API Explorer
</PopoverLink>
<PopoverLink target="_blank" href="https://github.com/keystonejs/keystone">
Expand Down
11 changes: 8 additions & 3 deletions packages/keystone/src/admin-ui/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type KeystoneContextType = {
visibleLists: VisibleLists;
createViewFieldModes: CreateViewFieldModes;
reinitContext: () => void;
apiPath: string;
};

const KeystoneContext = createContext<KeystoneContextType | undefined>(undefined);
Expand All @@ -34,6 +35,7 @@ type KeystoneProviderProps = {
adminMetaHash: string;
fieldViews: FieldViews;
lazyMetadataQuery: DocumentNode;
apiPath: string;
};

function InternalKeystoneProvider({
Expand All @@ -42,6 +44,7 @@ function InternalKeystoneProvider({
adminMetaHash,
children,
lazyMetadataQuery,
apiPath,
}: KeystoneProviderProps) {
const adminMeta = useAdminMeta(adminMetaHash, fieldViews);
const { authenticatedItem, visibleLists, createViewFieldModes, refetch } =
Expand Down Expand Up @@ -70,6 +73,7 @@ function InternalKeystoneProvider({
reinitContext,
visibleLists,
createViewFieldModes,
apiPath,
}}
>
{children}
Expand All @@ -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 (
Expand All @@ -103,6 +106,7 @@ export const useKeystone = (): {
authenticatedItem: AuthenticatedItem;
visibleLists: VisibleLists;
createViewFieldModes: CreateViewFieldModes;
apiPath: string;
} => {
const value = useContext(KeystoneContext);
if (!value) {
Expand All @@ -124,6 +128,7 @@ export const useKeystone = (): {
authenticatedItem: value.authenticatedItem,
visibleLists: value.visibleLists,
createViewFieldModes: value.createViewFieldModes,
apiPath: value.apiPath,
};
};

Expand Down
5 changes: 3 additions & 2 deletions packages/keystone/src/admin-ui/system/createAdminUIServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>
) => {
/** 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 });
Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/keystone/src/admin-ui/templates/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
`;
6 changes: 4 additions & 2 deletions packages/keystone/src/admin-ui/templates/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
76 changes: 46 additions & 30 deletions packages/keystone/src/admin-ui/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
},
]
: []),
];
};
2 changes: 1 addition & 1 deletion packages/keystone/src/lib/server/createApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
10 changes: 6 additions & 4 deletions packages/keystone/src/lib/server/createExpressServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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)
);
}

Expand Down
2 changes: 1 addition & 1 deletion packages/keystone/src/testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
5 changes: 2 additions & 3 deletions packages/keystone/src/types/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down

0 comments on commit 944bce1

Please sign in to comment.