allow GraphiQLOptions to be a function and refactor rendering path to reduce duplication

This commit is contained in:
Daniel Rinehart 2017-06-13 12:46:06 -04:00
parent 6d8047f5dd
commit 0d5add048e
15 changed files with 194 additions and 136 deletions

View file

@ -53,6 +53,10 @@ export function graphqlExpress(options: GraphQLOptions | ExpressGraphQLOptionsFu
}; };
} }
export interface ExpressGraphiQLOptionsFunction {
(req?: express.Request): GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData>;
}
/* This middleware returns the html for the GraphiQL interactive query UI /* This middleware returns the html for the GraphiQL interactive query UI
* *
* GraphiQLData arguments * GraphiQLData arguments
@ -64,22 +68,13 @@ export function graphqlExpress(options: GraphQLOptions | ExpressGraphQLOptionsFu
* - (optional) result: the result of the query to pre-fill in the GraphiQL UI * - (optional) result: the result of the query to pre-fill in the GraphiQL UI
*/ */
export function graphiqlExpress(options: GraphiQL.GraphiQLData) { export function graphiqlExpress(options: GraphiQL.GraphiQLData | ExpressGraphiQLOptionsFunction) {
return (req: express.Request, res: express.Response) => { return (req: express.Request, res: express.Response, next) => {
const q = req.url && url.parse(req.url, true).query || {}; const query = req.url && url.parse(req.url, true).query;
const query = q.query || ''; GraphiQL.resolveGraphiQLString(query, options, req).then(graphiqlString => {
const operationName = q.operationName || ''; res.setHeader('Content-Type', 'text/html');
res.write(graphiqlString);
const graphiQLString = GraphiQL.renderGraphiQL({ res.end();
endpointURL: options.endpointURL, }, error => next(error));
subscriptionsEndpoint: options.subscriptionsEndpoint,
query: query || options.query,
variables: q.variables && JSON.parse(q.variables) || options.variables,
operationName: operationName || options.operationName,
passHeader: options.passHeader,
});
res.setHeader('Content-Type', 'text/html');
res.write(graphiQLString);
res.end();
}; };
} }

View file

@ -1,6 +1,7 @@
export { export {
ExpressGraphQLOptionsFunction, ExpressGraphQLOptionsFunction,
ExpressHandler, ExpressHandler,
ExpressGraphiQLOptionsFunction,
graphqlExpress, graphqlExpress,
graphiqlExpress, graphiqlExpress,
} from './expressApollo'; } from './expressApollo';

View file

@ -70,38 +70,38 @@ graphqlHapi.attributes = {
version: '0.0.1', version: '0.0.1',
}; };
export interface GraphiQLPluginOptions { export interface HapiGraphiQLOptionsFunction {
path: string; (req?: Request): GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData>;
route?: any;
graphiqlOptions: GraphiQL.GraphiQLData;
} }
const graphiqlHapi: IRegister = function(server: Server, options: GraphiQLPluginOptions, next) { export interface HapiGraphiQLPluginOptions {
server.method('getGraphiQLParams', getGraphiQLParams); path: string;
server.method('renderGraphiQL', renderGraphiQL); route?: any;
graphiqlOptions: GraphiQL.GraphiQLData | HapiGraphiQLOptionsFunction;
}
const config = Object.assign(options.route || {}, { const graphiqlHapi: IRegister = function(server: Server, options: HapiGraphiQLPluginOptions, next) {
plugins: { if (!options || !options.graphiqlOptions) {
graphiql: options.graphiqlOptions, throw new Error('Apollo Server GraphiQL requires options.');
}, }
pre: [{
assign: 'graphiqlParams', if (arguments.length !== 3) {
method: 'getGraphiQLParams', throw new Error(`Apollo Server GraphiQL expects exactly 3 argument, got ${arguments.length}`);
}, { }
assign: 'graphiQLString',
method: 'renderGraphiQL(route, pre.graphiqlParams)',
}],
});
server.route({ server.route({
method: 'GET', method: 'GET',
path: options.path || '/graphql', path: options.path || '/graphiql',
config, config: options.route || {},
handler: (request, reply) => { handler: (request, reply) => {
reply(request.pre['graphiQLString']).header('Content-Type', 'text/html'); const query = request.query;
GraphiQL.resolveGraphiQLString(query, options.graphiqlOptions, request).then(graphiqlString => {
reply(graphiqlString).header('Content-Type', 'text/html');
}, error => reply(error));
}, },
}); });
next();
return next();
}; };
graphiqlHapi.attributes = { graphiqlHapi.attributes = {
@ -109,25 +109,4 @@ graphiqlHapi.attributes = {
version: '0.0.1', version: '0.0.1',
}; };
function getGraphiQLParams(request, reply) {
const q = request.query || {};
const query = q.query || '';
const variables = q.variables;
const operationName = q.operationName || '';
reply({ query, variables, operationName});
}
function renderGraphiQL(route, graphiqlParams: any, reply) {
const graphiqlOptions = route.settings.plugins['graphiql'];
const graphiQLString = GraphiQL.renderGraphiQL({
endpointURL: graphiqlOptions.endpointURL,
subscriptionsEndpoint: graphiqlOptions.subscriptionsEndpoint,
query: graphiqlParams.query || graphiqlOptions.query,
variables: graphiqlParams.variables && JSON.parse(graphiqlParams.variables) || graphiqlOptions.variables,
operationName: graphiqlParams.operationName || graphiqlOptions.operationName,
passHeader: graphiqlOptions.passHeader,
});
reply(graphiQLString);
}
export { graphqlHapi, graphiqlHapi }; export { graphqlHapi, graphiqlHapi };

View file

@ -1,5 +1,6 @@
export { IRegister, export { IRegister,
HapiOptionsFunction, HapiOptionsFunction,
HapiPluginOptions, HapiPluginOptions,
GraphiQLPluginOptions, HapiGraphiQLOptionsFunction,
HapiGraphiQLPluginOptions,
graphqlHapi, graphiqlHapi } from './hapiApollo'; graphqlHapi, graphiqlHapi } from './hapiApollo';

View file

@ -120,7 +120,7 @@ export const schema = new GraphQLSchema({
export interface CreateAppOptions { export interface CreateAppOptions {
excludeParser?: boolean; excludeParser?: boolean;
graphqlOptions?: GraphQLOptions | {(): GraphQLOptions | Promise<{}>}; graphqlOptions?: GraphQLOptions | {(): GraphQLOptions | Promise<{}>};
graphiqlOptions?: GraphiQL.GraphiQLData; graphiqlOptions?: GraphiQL.GraphiQLData | {(): GraphiQL.GraphiQLData | Promise<{}>};
} }
export interface CreateAppFunc { export interface CreateAppFunc {
@ -630,7 +630,7 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
app = createApp({graphqlOptions: { app = createApp({graphqlOptions: {
schema, schema,
formatError: (err) => { formatError: (err) => {
throw new Error('I should be catched'); throw new Error('I should be caught');
}, },
}}); }});
const req = request(app) const req = request(app)
@ -727,6 +727,32 @@ export default (createApp: CreateAppFunc, destroyApp?: DestroyAppFunc) => {
}); });
}); });
it('allows options to be a function', () => {
app = createApp({graphiqlOptions: () => ({
endpointURL: '/graphql',
})});
const req = request(app)
.get('/graphiql')
.set('Accept', 'text/html');
return req.then((response) => {
expect(response.status).to.equal(200);
});
});
it('handles options function errors', () => {
app = createApp({graphiqlOptions: () => {
throw new Error('I should be caught');
}});
const req = request(app)
.get('/graphiql')
.set('Accept', 'text/html');
return req.then((response) => {
expect(response.status).to.equal(500);
});
});
it('presents options variables', () => { it('presents options variables', () => {
app = createApp({graphiqlOptions: { app = createApp({graphiqlOptions: {
endpointURL: '/graphql', endpointURL: '/graphql',

View file

@ -1 +1,7 @@
export { KoaGraphQLOptionsFunction, KoaHandler, graphqlKoa, graphiqlKoa } from './koaApollo'; export {
KoaGraphQLOptionsFunction,
KoaHandler,
KoaGraphiQLOptionsFunction,
graphqlKoa,
graphiqlKoa,
} from './koaApollo';

View file

@ -44,22 +44,16 @@ export function graphqlKoa(options: GraphQLOptions | KoaGraphQLOptionsFunction):
}; };
} }
export function graphiqlKoa(options: GraphiQL.GraphiQLData) { export interface KoaGraphiQLOptionsFunction {
(ctx: koa.Context): GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData>;
}
export function graphiqlKoa(options: GraphiQL.GraphiQLData | KoaGraphiQLOptionsFunction) {
return (ctx: koa.Context) => { return (ctx: koa.Context) => {
const query = ctx.request.query;
const q = ctx.request.query || {}; GraphiQL.resolveGraphiQLString(query, options, ctx).then(graphiqlString => {
const query = q.query || ''; ctx.set('Content-Type', 'text/html');
const operationName = q.operationName || ''; ctx.body = graphiqlString;
}, error => ctx.throw(error.message, 500));
const graphiQLString = GraphiQL.renderGraphiQL({
endpointURL: options.endpointURL,
subscriptionsEndpoint: options.subscriptionsEndpoint,
query: query || options.query,
variables: q.variables && JSON.parse(q.variables) || options.variables,
operationName: operationName || options.operationName,
passHeader: options.passHeader,
});
ctx.set('Content-Type', 'text/html');
ctx.body = graphiQLString;
}; };
} }

View file

@ -1,6 +1,8 @@
export { export {
LambdaHandler, LambdaHandler,
IHeaders, IHeaders,
LambdaGraphQLOptionsFunction,
LambdaGraphiQLOptionsFunction,
graphqlLambda, graphqlLambda,
graphiqlLambda, graphiqlLambda,
} from './lambdaApollo'; } from './lambdaApollo';

View file

@ -67,6 +67,10 @@ export function graphqlLambda( options: GraphQLOptions | LambdaGraphQLOptionsFun
}; };
} }
export interface LambdaGraphiQLOptionsFunction {
(event: any, context: lambda.Context): GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData>;
}
/* This Lambda Function Handler returns the html for the GraphiQL interactive query UI /* This Lambda Function Handler returns the html for the GraphiQL interactive query UI
* *
* GraphiQLData arguments * GraphiQLData arguments
@ -78,30 +82,25 @@ export function graphqlLambda( options: GraphQLOptions | LambdaGraphQLOptionsFun
* - (optional) result: the result of the query to pre-fill in the GraphiQL UI * - (optional) result: the result of the query to pre-fill in the GraphiQL UI
*/ */
export function graphiqlLambda(options: GraphiQL.GraphiQLData) { export function graphiqlLambda(options: GraphiQL.GraphiQLData | LambdaGraphiQLOptionsFunction) {
return (event, lambdaContext: lambda.Context, callback: lambda.Callback) => { return (event, lambdaContext: lambda.Context, callback: lambda.Callback) => {
const q = event.queryStringParameters || {}; const query = event.queryStringParameters;
const query = q.query || ''; GraphiQL.resolveGraphiQLString(query, options, event, lambdaContext).then(graphiqlString => {
const variables = q.variables || '{}'; callback(
const operationName = q.operationName || ''; null,
{
const graphiQLString = GraphiQL.renderGraphiQL({ 'statusCode': 200,
endpointURL: options.endpointURL, 'headers': {
subscriptionsEndpoint: options.subscriptionsEndpoint, 'Content-Type': 'text/html',
query: query || options.query, },
variables: q.variables && JSON.parse(variables) || options.variables, 'body': graphiqlString,
operationName: operationName || options.operationName,
passHeader: options.passHeader,
});
callback(
null,
{
'statusCode': 200,
'headers': {
'Content-Type': 'text/html',
}, },
'body': graphiQLString, );
}, }, error => {
); callback(null, {
statusCode: 500,
body: error.message,
});
});
}; };
} }

View file

@ -1,2 +1,6 @@
export { MicroGraphQLOptionsFunction, export {
microGraphql, microGraphiql } from './microApollo'; MicroGraphQLOptionsFunction,
MicroGraphiQLOptionsFunction,
microGraphql,
microGraphiql,
} from './microApollo';

View file

@ -55,21 +55,21 @@ export function microGraphql(options: GraphQLOptions | MicroGraphQLOptionsFuncti
}; };
} }
export function microGraphiql(options: GraphiQL.GraphiQLData) { export interface MicroGraphiQLOptionsFunction {
return (req: IncomingMessage, res: ServerResponse) => { (req?: IncomingMessage): GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData>;
const q = req.url && url.parse(req.url, true).query || {}; }
const query = q.query || '';
const operationName = q.operationName || '';
const graphiQLString = GraphiQL.renderGraphiQL({ export function microGraphiql(options: GraphiQL.GraphiQLData | MicroGraphiQLOptionsFunction) {
endpointURL: options.endpointURL, return (req: IncomingMessage, res: ServerResponse) => {
query: query || options.query, const query = req.url && url.parse(req.url, true).query || {};
variables: q.variables && JSON.parse(q.variables) || options.variables, GraphiQL.resolveGraphiQLString(query, options, req).then(graphiqlString => {
operationName: operationName || options.operationName, res.setHeader('Content-Type', 'text/html');
passHeader: options.passHeader, res.write(graphiqlString);
res.end();
}, error => {
res.statusCode = 500;
res.write(error.message);
res.end();
}); });
res.setHeader('Content-Type', 'text/html');
res.write(graphiQLString);
res.end();
}; };
} }

View file

@ -1 +1,2 @@
export { GraphiQLData, renderGraphiQL } from './renderGraphiQL'; export { GraphiQLData, renderGraphiQL } from './renderGraphiQL';
export { resolveGraphiQLString } from './resolveGraphiQLString';

View file

@ -0,0 +1,49 @@
import { GraphiQLData, renderGraphiQL } from './renderGraphiQL';
export type GraphiQLParams = {
query?: string,
variables?: string,
operationName?: string,
};
function isOptionsFunction(arg: GraphiQLData | Function): arg is Function {
return typeof arg === 'function';
}
async function resolveGraphiQLOptions(options: GraphiQLData | Function, ...args): Promise<GraphiQLData> {
if (isOptionsFunction(options)) {
try {
return await options(...args);
} catch (e) {
throw new Error(`Invalid options provided for GraphiQL: ${e.message}`);
}
} else {
return options;
}
}
function createGraphiQLParams(query: any = {}): GraphiQLParams {
return {
query: query.query || '',
variables: query.variables,
operationName: query.operationName || '',
};
}
function createGraphiQLData(params: GraphiQLParams, options: GraphiQLData): GraphiQLData {
return {
endpointURL: options.endpointURL,
subscriptionsEndpoint: options.subscriptionsEndpoint,
query: params.query || options.query,
variables: params.variables && JSON.parse(params.variables) || options.variables,
operationName: params.operationName || options.operationName,
passHeader: options.passHeader,
};
}
export async function resolveGraphiQLString(query: any = {}, options: GraphiQLData | Function, ...args): Promise<string> {
const graphiqlParams = createGraphiQLParams(query);
const graphiqlOptions = await resolveGraphiQLOptions(options, ...args);
const graphiqlData = createGraphiQLData(graphiqlParams, graphiqlOptions);
return renderGraphiQL(graphiqlData);
}

View file

@ -1,6 +1,7 @@
export { export {
RestifyGraphQLOptionsFunction, RestifyGraphQLOptionsFunction,
RestifyHandler, RestifyHandler,
RestifyGraphiQLOptionsFunction,
graphqlRestify, graphqlRestify,
graphiqlRestify, graphiqlRestify,
} from './restifyApollo'; } from './restifyApollo';

View file

@ -54,6 +54,10 @@ export function graphqlRestify(options: GraphQLOptions | RestifyGraphQLOptionsFu
}; };
} }
export interface RestifyGraphiQLOptionsFunction {
(req?: restify.Request): GraphiQL.GraphiQLData | Promise<GraphiQL.GraphiQLData>;
}
/* This middleware returns the html for the GraphiQL interactive query UI /* This middleware returns the html for the GraphiQL interactive query UI
* *
* GraphiQLData arguments * GraphiQLData arguments
@ -65,23 +69,19 @@ export function graphqlRestify(options: GraphQLOptions | RestifyGraphQLOptionsFu
* - (optional) result: the result of the query to pre-fill in the GraphiQL UI * - (optional) result: the result of the query to pre-fill in the GraphiQL UI
*/ */
export function graphiqlRestify(options: GraphiQL.GraphiQLData) { export function graphiqlRestify(options: GraphiQL.GraphiQLData | RestifyGraphiQLOptionsFunction) {
return (req: restify.Request, res: restify.Response, next: restify.Next) => { return (req: restify.Request, res: restify.Response, next: restify.Next) => {
const q = req.url && url.parse(req.url, true).query || {}; const query = req.url && url.parse(req.url, true).query || {};
const query = q.query || ''; GraphiQL.resolveGraphiQLString(query, options, req).then(graphiqlString => {
const operationName = q.operationName || ''; res.setHeader('Content-Type', 'text/html');
res.write(graphiqlString);
const graphiQLString = GraphiQL.renderGraphiQL({ res.end();
endpointURL: options.endpointURL, next();
subscriptionsEndpoint: options.subscriptionsEndpoint, }, error => {
query: query || options.query, res.statusCode = 500;
variables: q.variables && JSON.parse(q.variables) || options.variables, res.write(error.message);
operationName: operationName || options.operationName, res.end();
passHeader: options.passHeader, next(false);
}); });
res.setHeader('Content-Type', 'text/html');
res.write(graphiQLString);
res.end();
next();
}; };
} }