Skip to content
This repository has been archived by the owner on Mar 20, 2023. It is now read-only.

Commit

Permalink
Merge pull request #16 from graphql/graphiql
Browse files Browse the repository at this point in the history
[RFC] GraphiQL when loaded in browser
  • Loading branch information
leebyron committed Sep 29, 2015
2 parents ca5de49 + a8bd662 commit 412470c
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 11 deletions.
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ var graphqlHTTP = require('express-graphql');

var app = express();

app.use('/graphql', graphqlHTTP({ schema: MyGraphQLSchema }));
app.use('/graphql', graphqlHTTP({ schema: MyGraphQLSchema, graphiql: true }));
```


Expand All @@ -33,6 +33,9 @@ The `graphqlHTTP` function accepts the following options:

* **`pretty`**: If `true`, any JSON response will be pretty-printed.

* **`graphiql`**: If `true`, may present [GraphiQL][] when loaded directly
from a browser (a useful tool for debugging and exploration).


### HTTP Usage

Expand All @@ -49,6 +52,10 @@ the parameters:
provided, a 400 error will be returned if the `query` contains multiple
named operations.

* **`raw`**: If the `graphiql` option is enabled and the `raw` parameter is
provided raw JSON will always be returned instead of GraphiQL even when
loaded from a browser.

GraphQL will first look for each parameter in the URL's query-string:

```
Expand Down Expand Up @@ -95,10 +102,12 @@ app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}));

app.use('/graphql', graphqlHTTP(request => ({
schema: MySessionAwareGraphQLSchema,
rootValue: request.session
rootValue: request.session,
graphiql: true
})));
```

[`graphql-js`]: https://github.com/graphql/graphql-js
[GraphiQL]: https://github.com/graphql/graphiql
[`multer`]: https://github.com/expressjs/multer
[`express-session`]: https://github.com/expressjs/session
171 changes: 171 additions & 0 deletions src/__tests__/http-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -881,5 +881,176 @@ describe('test harness', () => {
});

});

describe('Built-in GraphiQL support', () => {
it('does not renders GraphiQL if no opt-in', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({ schema: TestSchema }));

var response = await request(app)
.get(urlString({ query: '{test}' }))
.set('Accept', 'text/html');

expect(response.status).to.equal(200);
expect(response.type).to.equal('application/json');
expect(response.text).to.equal(
'{"data":{"test":"Hello World"}}'
);
});

it('presents GraphiQL when accepting HTML', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString({ query: '{test}' }))
.set('Accept', 'text/html');

expect(response.status).to.equal(200);
expect(response.type).to.equal('text/html');
expect(response.text).to.include('graphiql.min.js');
});

it('contains a pre-run response within GraphiQL', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString({ query: '{test}' }))
.set('Accept', 'text/html');

expect(response.status).to.equal(200);
expect(response.type).to.equal('text/html');
expect(response.text).to.include(
'response: ' + JSON.stringify(
JSON.stringify({ data: { test: 'Hello World' } }, null, 2)
)
);
});

it('GraphiQL renders provided variables', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString({
query: 'query helloWho($who: String) { test(who: $who) }',
variables: JSON.stringify({ who: 'Dolly' })
}))
.set('Accept', 'text/html');

expect(response.status).to.equal(200);
expect(response.type).to.equal('text/html');
expect(response.text).to.include(
'variables: ' + JSON.stringify(
JSON.stringify({ who: 'Dolly' }, null, 2)
)
);
});

it('GraphiQL accepts an empty query', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString())
.set('Accept', 'text/html');

expect(response.status).to.equal(200);
expect(response.type).to.equal('text/html');
expect(response.text).to.include('response: null');
});

it('returns HTML if preferred', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString({ query: '{test}' }))
.set('Accept', 'text/html,application/json');

expect(response.status).to.equal(200);
expect(response.type).to.equal('text/html');
expect(response.text).to.include('graphiql.min.js');
});

it('returns JSON if preferred', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString({ query: '{test}' }))
.set('Accept', 'application/json,text/html');

expect(response.status).to.equal(200);
expect(response.type).to.equal('application/json');
expect(response.text).to.equal(
'{"data":{"test":"Hello World"}}'
);
});

it('prefers JSON if unknown accept', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString({ query: '{test}' }))
.set('Accept', 'unknown');

expect(response.status).to.equal(200);
expect(response.type).to.equal('application/json');
expect(response.text).to.equal(
'{"data":{"test":"Hello World"}}'
);
});

it('prefers JSON if explicitly requested raw response', async () => {
var app = express();

app.use(urlString(), graphqlHTTP({
schema: TestSchema,
graphiql: true
}));

var response = await request(app)
.get(urlString({ query: '{test}', raw: '' }))
.set('Accept', 'text/html');

expect(response.status).to.equal(200);
expect(response.type).to.equal('application/json');
expect(response.text).to.equal(
'{"data":{"test":"Hello World"}}'
);
});
});
});
});
52 changes: 43 additions & 9 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import httpError from 'http-errors';
import { graphql } from 'graphql';
import { formatError } from 'graphql/error';
import { parseBody } from './parseBody';
import { renderGraphiQL } from './renderGraphiQL';
import type { Request, Response } from 'express';

/**
Expand All @@ -34,6 +35,11 @@ export type OptionsObj = {
* A boolean to configure whether the output should be pretty-printed.
*/
pretty?: ?boolean,

/**
* A boolean to optionally enable GraphiQL mode
*/
graphiql?: ?boolean,
};

type Middleware = (request: Request, response: Response) => void;
Expand All @@ -49,7 +55,7 @@ export default function graphqlHTTP(options: Options): Middleware {

return (request: Request, response: Response) => {
// Get GraphQL options given this request.
var { schema, rootValue, pretty } = getOptions(options, request);
var { schema, rootValue, pretty, graphiql } = getOptions(options, request);

// GraphQL HTTP only supports GET and POST methods.
if (request.method !== 'GET' && request.method !== 'POST') {
Expand All @@ -72,6 +78,17 @@ export default function graphqlHTTP(options: Options): Middleware {
// Get GraphQL params from the request and POST body data.
var { query, variables, operationName } = getGraphQLParams(request, data);

// If there is no query, present an empty GraphiQL if possible, otherwise
// return a 400 level error.
if (!query) {
if (graphiql && canDisplayGraphiQL(request, data)) {
return response
.set('Content-Type', 'text/html')
.send(renderGraphiQL());
}
throw httpError(400, 'Must provide query string.');
}

// Run GraphQL query.
graphql(
schema,
Expand All @@ -88,10 +105,19 @@ export default function graphqlHTTP(options: Options): Middleware {

// Report 200:Success if a data key exists,
// Otherwise 400:BadRequest if only errors exist.
response
.status(result.hasOwnProperty('data') ? 200 : 400)
.set('Content-Type', 'application/json')
.send(JSON.stringify(result, null, pretty ? 2 : 0));
response.status(result.hasOwnProperty('data') ? 200 : 400);

// If allowed to show GraphiQL, present it instead of JSON.
if (graphiql && canDisplayGraphiQL(request, data)) {
response
.set('Content-Type', 'text/html')
.send(renderGraphiQL({ query, variables, result }));
} else {
// Otherwise, present JSON directly.
response
.set('Content-Type', 'application/json')
.send(JSON.stringify(result, null, pretty ? 2 : 0));
}
});
});
};
Expand Down Expand Up @@ -120,7 +146,7 @@ function getOptions(options: Options, request: Request): OptionsObj {
}

type GraphQLParams = {
query: string;
query: ?string;
variables: ?Object;
operationName: ?string;
}
Expand All @@ -131,9 +157,6 @@ type GraphQLParams = {
function getGraphQLParams(request: Request, data: Object): GraphQLParams {
// GraphQL Query string.
var query = request.query.query || data.query;
if (!query) {
throw httpError(400, 'Must provide query string.');
}

// Parse the variables if needed.
var variables = request.query.variables || data.variables;
Expand All @@ -151,6 +174,17 @@ function getGraphQLParams(request: Request, data: Object): GraphQLParams {
return { query, variables, operationName };
}

/**
* Helper function to determine if GraphiQL can be displayed.
*/
function canDisplayGraphiQL(request: Request, data: Object): boolean {
// If `raw` exists, GraphiQL mode is not enabled.
var raw = request.query.raw !== undefined || data.raw !== undefined;
// Allowed to show GraphiQL if not requested as raw and this request
// prefers HTML over JSON.
return !raw && request.accepts([ 'json', 'html' ]) === 'html';
}

/**
* Helper for formatting errors
*/
Expand Down
Loading

0 comments on commit 412470c

Please sign in to comment.