Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add config.graphql.path option #6458

Merged
merged 5 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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