From 080265e636aed08e99344a87afdc4d9342e0393a Mon Sep 17 00:00:00 2001 From: mondaychen Date: Thu, 15 Nov 2018 04:31:20 -0500 Subject: [PATCH 1/6] Add public property graphqlPath to ApolloServer instance --- packages/apollo-server-core/src/ApolloServer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 7b715ba005b..dfb0e39d466 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -91,6 +91,7 @@ function getEngineServiceId(engine: Config['engine']): string | undefined { export class ApolloServerBase { public subscriptionsPath?: string; public graphqlPath: string = '/graphql'; + public playgroundPath: string = this.graphqlPath; public requestOptions: Partial> = Object.create(null); private context?: Context | ContextFunction; From 7068b612e1781a29f15c2e136d3602f8f22b5898 Mon Sep 17 00:00:00 2001 From: mondaychen Date: Thu, 15 Nov 2018 04:32:54 -0500 Subject: [PATCH 2/6] Implement playgroundPath support for express --- .../apollo-server-express/src/ApolloServer.ts | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/apollo-server-express/src/ApolloServer.ts b/packages/apollo-server-express/src/ApolloServer.ts index e08d2454433..ba119f69d67 100644 --- a/packages/apollo-server-express/src/ApolloServer.ts +++ b/packages/apollo-server-express/src/ApolloServer.ts @@ -29,6 +29,7 @@ export interface ServerRegistration { // users). app: express.Application; path?: string; + playgroundPath?: string; cors?: corsMiddleware.CorsOptions | boolean; bodyParserConfig?: OptionsJson | boolean; onHealthCheck?: (req: express.Request) => Promise; @@ -90,12 +91,14 @@ export class ApolloServer extends ApolloServerBase { public applyMiddleware({ app, path, + playgroundPath, cors, bodyParserConfig, disableHealthCheck, onHealthCheck, }: ServerRegistration) { if (!path) path = '/graphql'; + if (!playgroundPath) playgroundPath = path; // Despite the fact that this `applyMiddleware` function is `async` in // other integrations (e.g. Hapi), currently it is not for Express (@here). @@ -140,6 +143,7 @@ export class ApolloServer extends ApolloServerBase { // XXX multiple paths? this.graphqlPath = path; + this.playgroundPath = playgroundPath; // Note that we don't just pass all of these handlers to a single app.use call // for 'connect' compatibility. @@ -159,12 +163,26 @@ export class ApolloServer extends ApolloServerBase { app.use(path, uploadsMiddleware); } + const playgroundRenderPageOptions: + | PlaygroundRenderPageOptions + | undefined = this.playgroundOptions + ? { + endpoint: path, + subscriptionEndpoint: this.subscriptionsPath, + ...this.playgroundOptions, + } + : undefined; + // Note: if you enable playground in production and expect to be able to see your // schema, you'll need to manually specify `introspection: true` in the // ApolloServer constructor; by default, the introspection query is only // enabled in dev. app.use(path, (req, res, next) => { - if (this.playgroundOptions && req.method === 'GET') { + if ( + playgroundRenderPageOptions && + path === playgroundPath && + req.method === 'GET' + ) { // perform more expensive content-type check only if necessary // XXX We could potentially move this logic into the GuiOptions lambda, // but I don't think it needs any overriding @@ -176,11 +194,6 @@ export class ApolloServer extends ApolloServerBase { ) === 'text/html'; if (prefersHTML) { - const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { - endpoint: path, - subscriptionEndpoint: this.subscriptionsPath, - ...this.playgroundOptions, - }; res.setHeader('Content-Type', 'text/html'); const playground = renderPlaygroundPage(playgroundRenderPageOptions); res.write(playground); @@ -192,6 +205,15 @@ export class ApolloServer extends ApolloServerBase { return this.createGraphQLServerOptions(req, res); })(req, res, next); }); + + if (playgroundRenderPageOptions && path !== playgroundPath) { + app.use(playgroundPath, (_req, res) => { + res.setHeader('Content-Type', 'text/html'); + const playground = renderPlaygroundPage(playgroundRenderPageOptions); + res.write(playground); + res.end(); + }); + } } } From f70f0dce307d05395ce7a4e1bbc931ecaa6e34eb Mon Sep 17 00:00:00 2001 From: mondaychen Date: Thu, 15 Nov 2018 11:36:22 -0500 Subject: [PATCH 3/6] Implement playgroundPath support for hapi --- .../apollo-server-hapi/src/ApolloServer.ts | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/packages/apollo-server-hapi/src/ApolloServer.ts b/packages/apollo-server-hapi/src/ApolloServer.ts index 5bc2ca8adb7..4ae4a9a2f09 100644 --- a/packages/apollo-server-hapi/src/ApolloServer.ts +++ b/packages/apollo-server-hapi/src/ApolloServer.ts @@ -49,6 +49,7 @@ export class ApolloServer extends ApolloServerBase { app, cors, path, + playgroundPath, route, disableHealthCheck, onHealthCheck, @@ -56,6 +57,17 @@ export class ApolloServer extends ApolloServerBase { await this.willStart(); if (!path) path = '/graphql'; + if (!playgroundPath) playgroundPath = path; + + const playgroundRenderPageOptions: + | PlaygroundRenderPageOptions + | undefined = this.playgroundOptions + ? { + endpoint: path, + subscriptionEndpoint: this.subscriptionsPath, + ...this.playgroundOptions, + } + : undefined; await app.ext({ type: 'onRequest', @@ -68,7 +80,11 @@ export class ApolloServer extends ApolloServerBase { await handleFileUploads(this.uploadsConfig)(request); } - if (this.playgroundOptions && request.method === 'get') { + if ( + playgroundRenderPageOptions && + path === playgroundPath && + request.method === 'get' + ) { // perform more expensive content-type check only if necessary const accept = parseAll(request.headers); const types = accept.mediaTypes as string[]; @@ -78,13 +94,6 @@ export class ApolloServer extends ApolloServerBase { ) === 'text/html'; if (prefersHTML) { - const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { - endpoint: path, - subscriptionEndpoint: this.subscriptionsPath, - version: this.playgroundVersion, - ...this.playgroundOptions, - }; - return h .response(renderPlaygroundPage(playgroundRenderPageOptions)) .type('text/html') @@ -95,6 +104,18 @@ export class ApolloServer extends ApolloServerBase { }.bind(this), }); + if (playgroundRenderPageOptions && path !== playgroundPath) { + await app.route({ + method: '*', + path: playgroundPath, + handler: async function(_request, h) { + return h + .response(renderPlaygroundPage(playgroundRenderPageOptions)) + .type('text/html'); + }, + }); + } + if (!disableHealthCheck) { await app.route({ method: '*', @@ -135,12 +156,14 @@ export class ApolloServer extends ApolloServerBase { }); this.graphqlPath = path; + this.playgroundPath = playgroundPath; } } export interface ServerRegistration { app?: hapi.Server; path?: string; + playgroundPath?: string; cors?: boolean | hapi.RouteOptionsCors; route?: hapi.RouteOptions; onHealthCheck?: (request: hapi.Request) => Promise; From ba26ec5e56a594cfa97906f90db25a945355474c Mon Sep 17 00:00:00 2001 From: mondaychen Date: Thu, 15 Nov 2018 12:04:47 -0500 Subject: [PATCH 4/6] Implement playgroundPath support for koa --- .../apollo-server-koa/src/ApolloServer.ts | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/apollo-server-koa/src/ApolloServer.ts b/packages/apollo-server-koa/src/ApolloServer.ts index ef5f6aabdc4..723cef7fcba 100644 --- a/packages/apollo-server-koa/src/ApolloServer.ts +++ b/packages/apollo-server-koa/src/ApolloServer.ts @@ -20,6 +20,7 @@ import { GraphQLOptions, FileUploadOptions } from 'apollo-server-core'; export interface ServerRegistration { app: Koa; path?: string; + playgroundPath?: string; cors?: corsMiddleware.Options | boolean; bodyParserConfig?: bodyParser.Options | boolean; onHealthCheck?: (ctx: Koa.Context) => Promise; @@ -81,12 +82,14 @@ export class ApolloServer extends ApolloServerBase { public applyMiddleware({ app, path, + playgroundPath, cors, bodyParserConfig, disableHealthCheck, onHealthCheck, }: ServerRegistration) { if (!path) path = '/graphql'; + if (!playgroundPath) playgroundPath = path; // Despite the fact that this `applyMiddleware` function is `async` in // other integrations (e.g. Hapi), currently it is not for Koa (@here). @@ -139,6 +142,7 @@ export class ApolloServer extends ApolloServerBase { } this.graphqlPath = path; + this.playgroundPath = playgroundPath; if (cors === true) { app.use(middlewareFromPath(path, corsMiddleware())); @@ -156,9 +160,23 @@ export class ApolloServer extends ApolloServerBase { app.use(middlewareFromPath(path, uploadsMiddleware)); } + const playgroundRenderPageOptions: + | PlaygroundRenderPageOptions + | undefined = this.playgroundOptions + ? { + endpoint: path, + subscriptionEndpoint: this.subscriptionsPath, + ...this.playgroundOptions, + } + : undefined; + app.use( middlewareFromPath(path, (ctx: Koa.Context, next: Function) => { - if (this.playgroundOptions && ctx.request.method === 'GET') { + if ( + playgroundRenderPageOptions && + path === playgroundPath && + ctx.request.method === 'GET' + ) { // perform more expensive content-type check only if necessary const accept = accepts(ctx.req); const types = accept.types() as string[]; @@ -168,11 +186,6 @@ export class ApolloServer extends ApolloServerBase { ) === 'text/html'; if (prefersHTML) { - const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { - endpoint: path, - subscriptionEndpoint: this.subscriptionsPath, - ...this.playgroundOptions, - }; ctx.set('Content-Type', 'text/html'); const playground = renderPlaygroundPage( playgroundRenderPageOptions, @@ -186,6 +199,16 @@ export class ApolloServer extends ApolloServerBase { })(ctx, next); }), ); + + if (playgroundRenderPageOptions && path !== playgroundPath) { + app.use( + middlewareFromPath(playgroundPath, (ctx: Koa.Context) => { + ctx.set('Content-Type', 'text/html'); + const playground = renderPlaygroundPage(playgroundRenderPageOptions); + ctx.body = playground; + }), + ); + } } } From b28e618c605d0aafbd35dde37ee81304dab4b30c Mon Sep 17 00:00:00 2001 From: mondaychen Date: Thu, 15 Nov 2018 16:12:47 -0500 Subject: [PATCH 5/6] Add tests for playgroundPath --- .../src/__tests__/ApolloServer.test.ts | 41 ++++++++++ .../src/__tests__/ApolloServer.test.ts | 76 ++++++++++++++----- .../src/ApolloServer.ts | 8 ++ .../src/__tests__/ApolloServer.test.ts | 42 ++++++++++ 4 files changed, 150 insertions(+), 17 deletions(-) diff --git a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts index ce385c50bd4..5ba9962256e 100644 --- a/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-express/src/__tests__/ApolloServer.test.ts @@ -181,6 +181,47 @@ describe('apollo-server-express', () => { }); }); + it('can allow custom path for playground and GraphQL API', async () => { + const { url: uri, playgroundUrl } = await createServer( + { + typeDefs, + resolvers, + }, + { + path: '/gq', + playgroundPath: '/playground', + }, + ); + expect(uri).not.toEqual(playgroundUrl); + + const apolloFetch = createApolloFetch({ uri }); + const result = await apolloFetch({ query: '{hello}' }); + + expect(result.data).toEqual({ hello: 'hi' }); + + return new Promise((resolve, reject) => { + request( + { + url: playgroundUrl, + method: 'GET', + headers: { + accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', + }, + }, + (error, response, body) => { + if (error) { + reject(error); + } else { + expect(body).toMatch('GraphQLPlayground'); + expect(response.statusCode).toEqual(200); + resolve(); + } + }, + ); + }); + }); + const playgroundPartialOptionsTest = async () => { const defaultQuery = 'query { foo { bar } }'; const endpoint = '/fumanchupacabra'; diff --git a/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts index 60cf32e1206..d487949a64b 100644 --- a/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-hapi/src/__tests__/ApolloServer.test.ts @@ -74,7 +74,7 @@ const port = 5555; typeDefs, resolvers, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app }); await app.start(); @@ -108,7 +108,7 @@ const port = 5555; resolvers, introspection: false, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app }); await app.start(); @@ -156,7 +156,7 @@ const port = 5555; typeDefs, resolvers, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app }); await app.start(); @@ -188,14 +188,58 @@ const port = 5555; }); }); - it('accepts cors configuration', async () => { + it('can allow custom path for playground and GraphQL API', async () => { server = new ApolloServer({ typeDefs, resolvers, }); - app = new Server({ - port, + app = new Server({ port, host: 'localhost' }); + + await server.applyMiddleware({ + app, + path: '/gq', + playgroundPath: '/playground', + }); + await app.start(); + + httpServer = app.listener; + const uri = app.info.uri + '/gq'; + const playgroundUrl = app.info.uri + '/playground'; + + const apolloFetch = createApolloFetch({ uri }); + const result = await apolloFetch({ query: '{hello}' }); + + expect(result.data).toEqual({ hello: 'hi' }); + + return new Promise((resolve, reject) => { + request( + { + url: playgroundUrl, + method: 'GET', + headers: { + accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', + }, + }, + (error, response, body) => { + if (error) { + reject(error); + } else { + expect(body).toMatch('GraphQLPlayground'); + expect(response.statusCode).toEqual(200); + resolve(); + } + }, + ); + }); + }); + + it('accepts cors configuration', async () => { + server = new ApolloServer({ + typeDefs, + resolvers, }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, @@ -233,9 +277,7 @@ const port = 5555; typeDefs, resolvers, }); - app = new Server({ - port, - }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, @@ -284,7 +326,7 @@ const port = 5555; resolvers, context, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app }); await app.start(); @@ -309,7 +351,7 @@ const port = 5555; typeDefs, resolvers, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app }); await app.start(); @@ -341,7 +383,7 @@ const port = 5555; typeDefs, resolvers, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, @@ -379,7 +421,7 @@ const port = 5555; resolvers, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, @@ -519,7 +561,7 @@ const port = 5555; }, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, @@ -564,7 +606,7 @@ const port = 5555; }, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, @@ -611,7 +653,7 @@ const port = 5555; }, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, @@ -655,7 +697,7 @@ const port = 5555; }, }); - app = new Server({ port }); + app = new Server({ port, host: 'localhost' }); await server.applyMiddleware({ app, diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 4cd7156b3b5..3c07a992601 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -65,6 +65,13 @@ export function createServerInfo( pathname: server.graphqlPath, }); + serverInfo.playgroundUrl = require('url').format({ + protocol: 'http', + hostname: hostForUrl, + port: serverInfo.port, + pathname: server.playgroundPath, + }); + return serverInfo; } @@ -104,6 +111,7 @@ export interface ServerInfo { address: string; family: string; url: string; + playgroundUrl: string; port: number | string; server: AS; httpServer: http.Server; diff --git a/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts b/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts index 25ed0197002..636718254a7 100644 --- a/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts +++ b/packages/apollo-server-koa/src/__tests__/ApolloServer.test.ts @@ -178,6 +178,48 @@ describe('apollo-server-koa', () => { }); }); + it('can allow custom path for playground and GraphQL API', async () => { + const { url: uri, playgroundUrl } = await createServer( + { + typeDefs, + resolvers, + }, + { + path: '/gq', + playgroundPath: '/playground', + }, + ); + expect(uri).not.toEqual(playgroundUrl); + + const apolloFetch = createApolloFetch({ uri }); + const result = await apolloFetch({ query: '{hello}' }); + + expect(result.data).toEqual({ hello: 'hi' }); + expect(result.errors).toBeUndefined(); + + return new Promise((resolve, reject) => { + request( + { + url: playgroundUrl, + method: 'GET', + headers: { + accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', + }, + }, + (error, response, body) => { + if (error) { + reject(error); + } else { + expect(body).toMatch('GraphQLPlayground'); + expect(response.statusCode).toEqual(200); + resolve(); + } + }, + ); + }); + }); + it('accepts cors configuration', async () => { const { url: uri } = await createServer( { From 76a45271853e8d40cf53e19239ae057b156398d5 Mon Sep 17 00:00:00 2001 From: mondaychen Date: Thu, 15 Nov 2018 16:24:07 -0500 Subject: [PATCH 6/6] Add Changelog update --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 152f0ddd3da..b49c3615003 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ client reference ID, Apollo Server will now default to the values present in the of the request (`apollographql-client-name`, `apollographql-client-reference-id` and `apollographql-client-version` respectively). As a last resort, when those headers are not set, the query extensions' `clientInfo` values will be used. [PR #1960](https://github.com/apollographql/apollo-server/pull/1960) +- Add an optional parameter `playgroundPath` to options of `ApolloServer.applyMiddleware`, which allows a custom playground path that can be different from GraphQL API path. [PR #1974](https://github.com/apollographql/apollo-server/pull/1974) [Issue #1908](https://github.com/apollographql/apollo-server/issues/1908) ### v2.2.2