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

feat: support for GET method for existing servers #180

Merged
merged 13 commits into from
Jan 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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