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

Support playgroundPath #1974

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GraphQLOptions<any>> = Object.create(null);

private context?: Context | ContextFunction;
Expand Down
34 changes: 28 additions & 6 deletions packages/apollo-server-express/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>;
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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();
});
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions packages/apollo-server-express/src/__tests__/ApolloServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<http.Server>((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';
Expand Down
39 changes: 31 additions & 8 deletions packages/apollo-server-hapi/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,25 @@ export class ApolloServer extends ApolloServerBase {
app,
cors,
path,
playgroundPath,
route,
disableHealthCheck,
onHealthCheck,
}: ServerRegistration) {
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',
Expand All @@ -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[];
Expand All @@ -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')
Expand All @@ -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: '*',
Expand Down Expand Up @@ -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<any>;
Expand Down