Skip to content

Commit

Permalink
chore(get-support): refactor to share http logic in core module
Browse files Browse the repository at this point in the history
Closes #186
  • Loading branch information
DxCx committed Jan 13, 2017
1 parent 5b46db4 commit 32140cb
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 401 deletions.
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'
127 changes: 127 additions & 0 deletions packages/graphql-server-core/src/runHttpQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { 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.statusCode = statusCode;
this.isGraphQLError = isGraphQLError;
this.headers = headers;
}
}

export async function runHttpQuery(handlerArguments: Array<any>, request: HttpQueryRequest): Promise<string> {
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.');
}

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 {
const query = requestParams.query;
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) {
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);
}
141 changes: 21 additions & 120 deletions packages/graphql-server-express/src/expressApollo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as express from 'express';
import * as graphql from 'graphql';
import * as url from 'url';
import { GraphQLOptions, runQuery } from 'graphql-server-core';
import { GraphQLOptions, HttpQueryError, runHttpQuery } from 'graphql-server-core';
import * as GraphiQL from 'graphql-server-module-graphiql';

export interface ExpressGraphQLOptionsFunction {
Expand All @@ -27,131 +26,33 @@ export function graphqlExpress(options: GraphQLOptions | ExpressGraphQLOptionsFu
throw new Error(`Apollo Server expects exactly one argument, got ${arguments.length}`);
}

return async (req: express.Request, res: express.Response, next) => {
let optionsObject: GraphQLOptions;
if (isOptionsFunction(options)) {
try {
optionsObject = await options(req, res);
} catch (e) {
res.statusCode = 500;
res.write(`Invalid options provided to ApolloServer: ${e.message}`);
res.end();
return (req: express.Request, res: express.Response): void => {
runHttpQuery([req, res], {
method: req.method,
options: options,
query: req.method === 'POST' ? req.body : req.query,
}).then((gqlResponse) => {
res.setHeader('Content-Type', 'application/json');
res.write(gqlResponse);
res.end();
}, (error: HttpQueryError) => {
if ( undefined === error.statusCode ) {
throw error;
}
} else {
optionsObject = options;
}

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

switch ( req.method ) {
case 'POST':
if ( !req.body ) {
res.statusCode = 500;
res.write('POST body missing. Did you forget "app.use(bodyParser.json())"?');
res.end();
return;
}

requestPayload = req.body;
break;
case 'GET':
if ( !req.query || (Object.keys(req.query).length === 0) ) {
res.statusCode = 400;
res.write('GET query missing.');
res.end();
return;
}

requestPayload = req.query;
break;

default:
res.setHeader('Allow', 'GET, POST');
res.statusCode = 405;
res.write('Apollo Server supports only GET/POST requests.');
res.end();
return;
}

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<graphql.ExecutionResult> = [];
for (let requestParams of requestPayload) {
try {
const query = requestParams.query;
const operationName = requestParams.operationName;
let variables = requestParams.variables;

if (typeof variables === 'string') {
try {
variables = JSON.parse(variables);
} catch (error) {
res.statusCode = 400;
res.write('Variables are invalid JSON.');
res.end();
return;
}
}

// 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) {
responses.push({ errors: [formatErrorFn(e)] });
if ( error.headers ) {
Object.keys(error.headers).forEach((header) => {
res.setHeader(header, error.headers[header]);
});
}
}

res.setHeader('Content-Type', 'application/json');
if (isBatch) {
res.write(JSON.stringify(responses));
res.end();
} else {
const gqlResponse = responses[0];
if (gqlResponse.errors && typeof gqlResponse.data === 'undefined') {
res.statusCode = 400;
}
res.write(JSON.stringify(gqlResponse));
res.statusCode = error.statusCode;
res.write(error.message);
res.end();
}

});
};
}

function isOptionsFunction(arg: GraphQLOptions | ExpressGraphQLOptionsFunction): arg is ExpressGraphQLOptionsFunction {
return typeof arg === 'function';
}

/* This middleware returns the html for the GraphiQL interactive query UI
*
* GraphiQLData arguments
Expand All @@ -164,7 +65,7 @@ function isOptionsFunction(arg: GraphQLOptions | ExpressGraphQLOptionsFunction):
*/

export function graphiqlExpress(options: GraphiQL.GraphiQLData) {
return (req: express.Request, res: express.Response, next) => {
return (req: express.Request, res: express.Response) => {
const q = req.url && url.parse(req.url, true).query || {};
const query = q.query || '';
const variables = q.variables || '{}';
Expand Down
8 changes: 4 additions & 4 deletions packages/graphql-server-hapi/src/hapiApollo.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as hapi from 'hapi';
import { graphqlHapi, graphiqlHapi, HapiPluginOptions } from './hapiApollo';
import { graphqlHapi, graphiqlHapi } from './hapiApollo';
import 'mocha';

import testSuite, { Schema } from 'graphql-server-integration-testsuite';
import testSuite, { Schema, CreateAppOptions } from 'graphql-server-integration-testsuite';

function createApp(createOptions: HapiPluginOptions) {
function createApp(options: CreateAppOptions) {
const server = new hapi.Server();

server.connection({
Expand All @@ -15,7 +15,7 @@ function createApp(createOptions: HapiPluginOptions) {
server.register({
register: graphqlHapi,
options: {
graphqlOptions: createOptions ? createOptions.graphqlOptions : { schema: Schema },
graphqlOptions: (options && options.graphqlOptions) || { schema: Schema },
path: '/graphql',
},
});
Expand Down
Loading

0 comments on commit 32140cb

Please sign in to comment.