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

Move gui option into constructor and use playground #1297

Merged
merged 9 commits into from
Jul 11, 2018
4 changes: 4 additions & 0 deletions docs/source/api/apollo-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ new ApolloServer({

Enables and disables schema introspection

* `playground`: <`Boolean`> | <`Object`>

Enables and disables playground and allows configuration of GraphQL Playground. The options can be found on GraphQL Playground's [documentation](https://github.com/prismagraphql/graphql-playground/#usage)

* `debug`: <`Boolean`>

Enables and disables development mode helpers. Defaults to `true`
Expand Down
51 changes: 29 additions & 22 deletions docs/source/features/playground.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,51 @@ title: GraphQL Playground
description: Visually exploring a Apollo Server
---

[GraphQL Playground](https://github.com/prismagraphql/graphql-playground) is a graphical interactive in-browser GraphQL IDE, created by [Prisma](https://www.prisma.io/), based on [GraphiQL](https://github.com/graphql/graphiql). In development, Apollo Server collocates a GraphQL Playground instance with the GraphQL path. When a browser sends a request to Apollo Server, it receives the GraphQL Playground gui. When `NODE_ENV` is set to production, introspection and Playground are disabled as a production best practice.
[GraphQL Playground](https://github.com/prismagraphql/graphql-playground) is a graphical interactive in-browser GraphQL IDE, created by [Prisma](https://www.prisma.io/), based on [GraphiQL](https://github.com/graphql/graphiql). In development, Apollo Server collocates a GraphQL Playground instance with the GraphQL path. When a browser sends a request to Apollo Server, it receives GraphQL Playground. When `NODE_ENV` is set to production, introspection and Playground are disabled as a production best practice.

<div align="center">
![GraphQL Playground](../images/playground.png)
</div>

## Enabling Playground in Production

To enable Playground in production, an integration package must be installed to provide more control over the middlewares used. The following example uses the express integration:

```bash
npm install --save apollo-server-express@rc graphql
## Configuring Playground

The Apollo Server constructor contains the ability to configure GraphQL Playground with the `playground` configuration option. The options can be found on GraphQL Playground's [documentation](https://github.com/prismagraphql/graphql-playground/#usage)

```js
new ApolloServer({
typeDefs,
resolvers,
playground: {
settings: {
'editor.theme': 'light',
},
tabs: [
{
endpoint,
query: defaultQuery,
},
],
},
});
```

Introspection and the gui can be enabled explicitly in the following manner.
## Enabling Playground in Production

To enable Playground in production, introspection and the playground can be enabled explicitly in the following manner.

```js line=8,16
const { ApolloServer, gql } = require('apollo-server-express');
const express = require('express');
```js line=7-8
const { ApolloServer } = require('apollo-server');
const { typeDefs, resolvers } = require('./schema');

const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
playground: true,
});

const app = express();

// gui accepts a Playground configuration
server.applyMiddleware({
app,
gui: true,
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
);
```

> Note: when using apollo-server-express, you can remove apollo-server from your package.json
16 changes: 11 additions & 5 deletions packages/apollo-server-core/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ import { FormatErrorExtension } from './formatters';

import { gql } from './index';

import {
createPlaygroundOptions,
PlaygroundRenderPageOptions,
} from './playground';

const NoIntrospection = (context: ValidationContext) => ({
Field(node: FieldDefinitionNode) {
if (node.name.value === '__schema' || node.name.value === '__type') {
Expand All @@ -64,14 +69,12 @@ export class ApolloServerBase {
protected subscriptionServerOptions?: SubscriptionServerOptions;
protected uploadsConfig?: FileUploadOptions;

// This specifies the version of GraphQL Playground that will be served
// from graphql-playground-html, and is passed to renderPlaygroundPage
// by the integration subclasses
protected playgroundVersion = '1.7.2';

// set by installSubscriptionHandlers.
private subscriptionServer?: SubscriptionServer;

// the default version is specified in playground.ts
protected playgroundOptions?: PlaygroundRenderPageOptions;

// The constructor should be universal across all environments. All environment specific behavior should be set by adding or overriding methods
constructor(config: Config) {
if (!config) throw new Error('ApolloServer requires options.');
Expand All @@ -87,6 +90,7 @@ export class ApolloServerBase {
engine,
subscriptions,
uploads,
playground,
...requestOptions
} = config;

Expand Down Expand Up @@ -235,6 +239,8 @@ export class ApolloServerBase {
);
}
}

this.playgroundOptions = createPlaygroundOptions(playground);
}

// used by integrations to synchronize the path with subscriptions, some
Expand Down
2 changes: 2 additions & 0 deletions packages/apollo-server-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export {

export { convertNodeHttpToRequest } from './nodeHttpToRequest';

export { createPlaygroundOptions } from './playground';

// ApolloServer Base class
export { ApolloServerBase } from './ApolloServer';
export * from './types';
Expand Down
52 changes: 52 additions & 0 deletions packages/apollo-server-core/src/playground.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
RenderPageOptions as PlaygroundRenderPageOptions,
Theme,
} from '@apollographql/graphql-playground-html/dist/render-playground-page';
export {
RenderPageOptions as PlaygroundRenderPageOptions,
} from '@apollographql/graphql-playground-html/dist/render-playground-page';

// This specifies the version of GraphQL Playground that will be served
// from graphql-playground-html, and is passed to renderPlaygroundPage
// by the integration subclasses
const playgroundVersion = '1.7.2';

export type PlaygroundConfig = Partial<PlaygroundRenderPageOptions> | boolean;

export const defaultPlaygroundOptions = {
version: playgroundVersion,
settings: {
'general.betaUpdates': false,
'editor.theme': 'dark' as Theme,
'editor.reuseHeaders': true,
'tracing.hideTracingResponse': true,
'editor.fontSize': 14,
'editor.fontFamily': `'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace`,
'request.credentials': 'omit',
},
};

export function createPlaygroundOptions(
playground: PlaygroundConfig = {},
): PlaygroundRenderPageOptions | undefined {
const isDev = process.env.NODE_ENV !== 'production';
const enabled: boolean = typeof playground === 'boolean' ? playground : isDev;

if (!enabled) {
return undefined;
}

const playgroundOverrides =
typeof playground === 'boolean' ? {} : playground || {};

const playgroundOptions: PlaygroundRenderPageOptions = {
...defaultPlaygroundOptions,
...playgroundOverrides,
settings: {
...defaultPlaygroundOptions.settings,
...playgroundOverrides.settings,
},
};

return playgroundOptions;
}
13 changes: 6 additions & 7 deletions packages/apollo-server-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { SchemaDirectiveVisitor, IResolvers, IMocks } from 'graphql-tools';
import { ConnectionContext } from 'subscriptions-transport-ws';
import * as WebSocket from 'ws';
import { GraphQLExtension } from 'graphql-extensions';
import { EngineReportingOptions } from 'apollo-engine-reporting';
export { GraphQLExtension } from 'graphql-extensions';

import { EngineReportingOptions } from 'apollo-engine-reporting';

import { PlaygroundConfig } from './playground';
export { PlaygroundConfig, PlaygroundRenderPageOptions } from './playground';

import {
GraphQLServerOptions as GraphQLOptions,
PersistedQueryOptions,
Expand Down Expand Up @@ -59,6 +63,7 @@ export interface Config
subscriptions?: Partial<SubscriptionServerOptions> | string | false;
//https://github.com/jaydenseric/apollo-upload-server#options
uploads?: boolean | FileUploadOptions;
playground?: PlaygroundConfig;
}

export interface FileUploadOptions {
Expand All @@ -69,9 +74,3 @@ export interface FileUploadOptions {
//Max allowed number of files (default: Infinity).
maxFiles?: number;
}

export interface MiddlewareOptions {
path?: string;
gui?: boolean;
subscriptions?: boolean;
}
99 changes: 97 additions & 2 deletions packages/apollo-server-express/src/ApolloServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe('apollo-server-express', () => {
// XXX Unclear why this would be something somebody would want (vs enabling
// introspection without graphql-playground, which seems reasonable, eg you
// have your own graphql-playground setup with a custom link)
it('can enable gui separately from introspection during production', async () => {
it('can enable playground separately from introspection during production', async () => {
const INTROSPECTION_QUERY = `
{
__schema {
Expand Down Expand Up @@ -146,7 +146,7 @@ describe('apollo-server-express', () => {
});
});

it('renders GraphQL playground when browser requests', async () => {
it('renders GraphQL playground by default when browser requests', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;

Expand Down Expand Up @@ -179,6 +179,101 @@ describe('apollo-server-express', () => {
});
});

it('accepts partial GraphQL Playground Options', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;

const defaultQuery = 'query { foo { bar } }';
const endpoint = '/fumanchupacabra';
const { url } = await createServer(
{
typeDefs,
resolvers,
playground: {
// https://github.com/apollographql/graphql-playground/blob/0e452d2005fcd26f10fbdcc4eed3b2e2af935e3a/packages/graphql-playground-html/src/render-playground-page.ts#L16-L24
// must be made partial
settings: {
'editor.theme': 'light',
} as any,
tabs: [
{
query: defaultQuery,
},
{
endpoint,
} as any,
],
},
},
{},
);

return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
Folo: 'bar',
},
},
(error, response, body) => {
process.env.NODE_ENV = nodeEnv;
if (error) {
reject(error);
} else {
console.log('body', body);
expect(body).to.contain('GraphQLPlayground');
expect(body).to.contain(`"editor.theme": "light"`);
expect(body).to.contain(defaultQuery);
expect(body).to.contain(endpoint);
expect(response.statusCode).to.equal(200);
resolve();
}
},
);
});
});

it('accepts playground options as a boolean', async () => {
const nodeEnv = process.env.NODE_ENV;
delete process.env.NODE_ENV;

const { url } = await createServer(
{
typeDefs,
resolvers,
playground: false,
},
{},
);

return new Promise<http.Server>((resolve, reject) => {
request(
{
url,
method: 'GET',
headers: {
accept:
'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
},
},
(error, response, body) => {
process.env.NODE_ENV = nodeEnv;
if (error) {
reject(error);
} else {
expect(body).not.to.contain('GraphQLPlayground');
expect(response.statusCode).not.to.equal(200);
resolve();
}
},
);
});
});

it('accepts cors configuration', async () => {
const { url: uri } = await createServer(
{
Expand Down
Loading