Skip to content

Commit

Permalink
Merge pull request #180 from DxCx/get-support
Browse files Browse the repository at this point in the history
feat: support for GET method for existing servers
  • Loading branch information
helfer authored Jan 19, 2017
2 parents 780a9eb + 01e3010 commit 454e2a4
Show file tree
Hide file tree
Showing 14 changed files with 385 additions and 352 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

### VNEXT
* add support for HTTP GET Method ([@DxCx](https://github.com/DxCx)) on [#180](https://github.com/apollostack/graphql-server/pull/180)

### v0.5.0
* Switch graphql typings for typescript to @types/graphql [#260](https://github.com/apollostack/graphql-server/pull/260)
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const PORT = 3000;

var app = express();

// bodyParser is needed just for POST.
app.use('/graphql', bodyParser.json(), graphqlExpress({ schema: myGraphQLSchema }));

app.listen(PORT);
Expand All @@ -60,6 +61,7 @@ const PORT = 3000;

var app = connect();

// bodyParser is needed just for POST.
app.use('/graphql', bodyParser.json());
app.use('/graphql', graphqlConnect({ schema: myGraphQLSchema }));

Expand Down Expand Up @@ -108,24 +110,25 @@ server.start((err) => {
### Koa
```js
import koa from 'koa'; // koa@2
import koaBody from 'koa-bodyparser'; // koa-bodyparser@next
import koaRouter from 'koa-router'; // koa-router@next
import koaBody from 'koa-bodyparser'; // koa-bodyparser@next
import { graphqlKoa } from 'graphql-server-koa';

const app = new koa();
const router = new koaRouter();
const PORT = 3000;

// koaBody is needed just for POST.
app.use(koaBody());

router.post('/graphql', graphqlKoa({ schema: myGraphQLSchema }));
router.get('/graphql', graphqlKoa({ schema: myGraphQLSchema }));

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(PORT);
```

## Options

GraphQL Server can be configured with an options object with the the following fields:

* **schema**: the GraphQLSchema to be used
Expand Down
6 changes: 5 additions & 1 deletion packages/graphql-server-core/src/graphqlOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { LogFunction } from './runQuery';
* - (optional) debug: a boolean that will print additional debug logging if execution errors occur
*
*/
interface GraphQLServerOptions {
export interface GraphQLServerOptions {
schema: GraphQLSchema;
formatError?: Function;
rootValue?: any;
Expand All @@ -28,3 +28,7 @@ interface GraphQLServerOptions {
}

export default GraphQLServerOptions;

export function isOptionsFunction(arg: GraphQLServerOptions | Function): arg is Function {
return typeof arg === 'function';
}
3 changes: 2 additions & 1 deletion packages/graphql-server-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { runQuery, LogFunction, LogMessage, LogStep, LogAction } from './runQuery'
export { default as GraphQLOptions} from './graphqlOptions'
export { runHttpQuery, HttpQueryRequest, HttpQueryError } from './runHttpQuery';
export { default as GraphQLOptions } from './graphqlOptions'
154 changes: 154 additions & 0 deletions packages/graphql-server-core/src/runHttpQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { parse, getOperationAST, DocumentNode, formatError, ExecutionResult } from 'graphql';
import { runQuery } from './runQuery';
import { default as GraphQLOptions, isOptionsFunction } from './graphqlOptions';

export interface HttpQueryRequest {
method: string;
query: string;
options: GraphQLOptions | Function;
}

export class HttpQueryError extends Error {
public statusCode: number;
public isGraphQLError: boolean;
public headers: { [key: string]: string };

constructor (statusCode: number, message: string, isGraphQLError: boolean = false, headers?: { [key: string]: string }) {
super(message);
this.name = 'HttpQueryError';
this.statusCode = statusCode;
this.isGraphQLError = isGraphQLError;
this.headers = headers;
}
}

function isQueryOperation(query: DocumentNode, operationName: string) {
const operationAST = getOperationAST(query, operationName);
return operationAST.operation === 'query';
}

export async function runHttpQuery(handlerArguments: Array<any>, request: HttpQueryRequest): Promise<string> {
let isGetRequest: boolean = false;
let optionsObject: GraphQLOptions;
if (isOptionsFunction(request.options)) {
try {
optionsObject = await request.options(...handlerArguments);
} catch (e) {
throw new HttpQueryError(500, `Invalid options provided to ApolloServer: ${e.message}`);
}
} else {
optionsObject = request.options;
}

const formatErrorFn = optionsObject.formatError || formatError;
let requestPayload;

switch ( request.method ) {
case 'POST':
if ( !request.query ) {
throw new HttpQueryError(500, 'POST body missing. Did you forget use body-parser middleware?');
}

requestPayload = request.query;
break;
case 'GET':
if ( !request.query || (Object.keys(request.query).length === 0) ) {
throw new HttpQueryError(400, 'GET query missing.');
}

isGetRequest = true;
requestPayload = request.query;
break;

default:
throw new HttpQueryError(405, 'Apollo Server supports only GET/POST requests.', false, {
'Allow': 'GET, POST',
});
}

let isBatch = true;
// TODO: do something different here if the body is an array.
// Throw an error if body isn't either array or object.
if (!Array.isArray(requestPayload)) {
isBatch = false;
requestPayload = [requestPayload];
}

let responses: Array<ExecutionResult> = [];
for (let requestParams of requestPayload) {
try {
let query = requestParams.query;
if ( isGetRequest ) {
if (typeof query === 'string') {
// preparse the query incase of GET so we can assert the operation.
query = parse(query);
}

if ( ! isQueryOperation(query, requestParams.operationName) ) {
throw new HttpQueryError(405, `GET supports only query operation`, false, {
'Allow': 'POST',
});
}
}

const operationName = requestParams.operationName;
let variables = requestParams.variables;

if (typeof variables === 'string') {
try {
variables = JSON.parse(variables);
} catch (error) {
throw new HttpQueryError(400, 'Variables are invalid JSON.');
}
}

// Shallow clone context for queries in batches. This allows
// users to distinguish multiple queries in the batch and to
// modify the context object without interfering with each other.
let context = optionsObject.context;
if (isBatch) {
context = Object.assign({}, context || {});
}

let params = {
schema: optionsObject.schema,
query: query,
variables: variables,
context: context,
rootValue: optionsObject.rootValue,
operationName: operationName,
logFunction: optionsObject.logFunction,
validationRules: optionsObject.validationRules,
formatError: formatErrorFn,
formatResponse: optionsObject.formatResponse,
debug: optionsObject.debug,
};

if (optionsObject.formatParams) {
params = optionsObject.formatParams(params);
}

responses.push(await runQuery(params));
} catch (e) {
// Populate any HttpQueryError to our handler which should
// convert it to Http Error.
if ( e.name === 'HttpQueryError' ) {
throw e;
}

responses.push({ errors: [formatErrorFn(e)] });
}
}

if (!isBatch) {
const gqlResponse = responses[0];
if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') {
throw new HttpQueryError(400, JSON.stringify(gqlResponse), true, {
'Content-Type': 'application/json',
});
}
return JSON.stringify(gqlResponse);
}

return JSON.stringify(responses);
}
1 change: 1 addition & 0 deletions packages/graphql-server-express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"graphql-server-integration-testsuite": "^0.5.0",
"body-parser": "^1.15.2",
"connect": "^3.4.1",
"connect-query": "^0.2.0",
"express": "^4.14.0",
"multer": "^1.2.0"
},
Expand Down
6 changes: 3 additions & 3 deletions packages/graphql-server-express/src/apolloServerHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,11 @@ describe(`GraphQL-HTTP (apolloServer) tests for ${version} express`, () => {
app.use(urlString(), graphqlExpress({ schema: TestSchema }));

const response = await request(app)
.get(urlString({ query: '{test}' }));
.put(urlString({ query: '{test}' }));

expect(response.status).to.equal(405);
expect(response.headers.allow).to.equal('POST');
return expect(response.text).to.contain('Apollo Server supports only POST requests.');
expect(response.headers.allow).to.equal('GET, POST');
return expect(response.text).to.contain('Apollo Server supports only GET/POST requests.');
});
});

Expand Down
1 change: 1 addition & 0 deletions packages/graphql-server-express/src/connectApollo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function createConnectApp(options: CreateAppOptions = {}) {
if (options.graphiqlOptions ) {
app.use('/graphiql', graphiqlConnect( options.graphiqlOptions ));
}
app.use('/graphql', require('connect-query')());
app.use('/graphql', graphqlConnect( options.graphqlOptions ));
return app;
}
Expand Down
Loading

0 comments on commit 454e2a4

Please sign in to comment.