diff --git a/.changeset/spicy-crews-rescue.md b/.changeset/spicy-crews-rescue.md new file mode 100644 index 00000000000..bff8648b1c6 --- /dev/null +++ b/.changeset/spicy-crews-rescue.md @@ -0,0 +1,6 @@ +--- +"@keystone-next/website": patch +"@keystone-next/keystone": minor +--- + +Add extendExpressApp config option for configuring the express app that Keystone creates diff --git a/docs/pages/docs/apis/config.mdx b/docs/pages/docs/apis/config.mdx index 7e5bbefe702..1a80e925d41 100644 --- a/docs/pages/docs/apis/config.mdx +++ b/docs/pages/docs/apis/config.mdx @@ -203,6 +203,7 @@ Options: - `port` (default: `3000` ): The port your Express server will listen on. - `maxFileSize` (default: `200 * 1024 * 1024`): The maximum file size allowed for uploads. If left undefined, defaults to `200 MiB` - `healthCheck` (default: `undefined`): Allows you to configure a health check endpoint on your server. +- `extendExpressApp` (default: `undefined`): Allows you to extend the express app that Keystone creates. ```typescript export default config({ @@ -211,6 +212,7 @@ export default config({ port: 3000, maxFileSize: 200 * 1024 * 1024, healthCheck: true, + extendExpressApp: (app) => { /* ... */ }, }, /* ... */ }); @@ -228,8 +230,8 @@ config({ healthCheck: { path: '/my-health-check', data: { status: 'healthy' }, - } - } + }, + }, }) ``` @@ -245,11 +247,44 @@ config({ timestamp: Date.now(), uptime: process.uptime(), }), - } - } + }, + }, }) ``` +### extendExpressApp + +This lets you modify the express app that Keystone creates _before_ the Apollo Server and Admin UI Middleware are added to it (but after the `cors` and `healthcheck` options are applied). + +For example, you could add your own request logging middleware: + +```ts +export default config({ + server: { + extendExpressApp: (app) => { + app.use((req, res, next) => { + console.log('A request!'); + next(); + }); + }, + }, +}); +``` + +Or add a custom route handler: + +```ts +export default config({ + server: { + extendExpressApp: (app) => { + app.get('/_version', (req, res) => { + res.send('v6.0.0-rc.2'); + }); + }, + }, +}); +``` + ## session ``` diff --git a/packages/keystone/src/lib/server/createExpressServer.ts b/packages/keystone/src/lib/server/createExpressServer.ts index dd4b5b77cba..dc4e6e6de83 100644 --- a/packages/keystone/src/lib/server/createExpressServer.ts +++ b/packages/keystone/src/lib/server/createExpressServer.ts @@ -66,6 +66,10 @@ export const createExpressServer = async ( addHealthCheck({ config, server }); + if (config.server?.extendExpressApp) { + config.server?.extendExpressApp(server); + } + addApolloServer({ server, config, diff --git a/packages/keystone/src/types/config/index.ts b/packages/keystone/src/types/config/index.ts index 2a8b62e72ed..aac69c5b5bd 100644 --- a/packages/keystone/src/types/config/index.ts +++ b/packages/keystone/src/types/config/index.ts @@ -1,6 +1,7 @@ +import type { Config } from 'apollo-server-express'; import { CorsOptions } from 'cors'; +import express from 'express'; import type { GraphQLSchema } from 'graphql'; -import type { Config } from 'apollo-server-express'; import type { AssetMode, KeystoneContext } from '..'; @@ -131,6 +132,8 @@ export type ServerConfig = { maxFileSize?: number; /** Health check configuration. Set to `true` to add a basic `/_healthcheck` route, or specify the path and data explicitly */ healthCheck?: HealthCheckConfig | true; + /** Hook to extend the Express App that Keystone creates */ + extendExpressApp?: (app: express.Express) => void; }; // config.graphql diff --git a/tests/api-tests/extend-express-app.test.ts b/tests/api-tests/extend-express-app.test.ts new file mode 100644 index 00000000000..0018cc2c53a --- /dev/null +++ b/tests/api-tests/extend-express-app.test.ts @@ -0,0 +1,32 @@ +import { createSchema, list } from '@keystone-next/keystone'; +import { text } from '@keystone-next/keystone/fields'; +import { setupTestRunner } from '@keystone-next/keystone/testing'; +import supertest from 'supertest'; +import { apiTestConfig } from './utils'; + +const runner = setupTestRunner({ + config: apiTestConfig({ + lists: createSchema({ User: list({ fields: { name: text() } }) }), + server: { + extendExpressApp: app => { + app.get('/magic', (req, res) => { + res.json({ magic: true }); + }); + }, + }, + }), +}); + +test( + 'basic extension', + runner(async ({ app }) => { + const { text } = await supertest(app) + .get('/magic') + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(200); + expect(JSON.parse(text)).toEqual({ + magic: true, + }); + }) +);