Add support for AWS Lambda

Create an integration that will return a lambda handler for a
graphql or graphiql server. This integration requires an API
Gateway with Lambda Proxy Integration.

The test runner expects req and res objects for testing. We
have to mock these for the tests to execute properly.
This commit is contained in:
Reyad Attiyat 2016-12-14 14:51:56 -06:00 committed by Urigo
parent 7db522be9b
commit cd1c19d033
8 changed files with 245 additions and 0 deletions

View file

@ -0,0 +1,5 @@
*
!dist
!dist/**/*
dist/**/*.test.*
!package.json

View file

@ -0,0 +1,3 @@
# graphql-server-lambda
This is the AWS Lambda integration for the Apollo community GraphQL Server. [Read the docs.](http://dev.apollodata.com/tools/apollo-server/index.html)

View file

@ -0,0 +1,43 @@
{
"name": "graphql-server-lambda",
"version": "0.5.1",
"description": "Production-ready Node.js GraphQL server for AWS Lambda",
"main": "dist/index.js",
"scripts": {
"compile": "tsc",
"prepublish": "npm run compile"
},
"repository": {
"type": "git",
"url": "https://github.com/apollostack/graphql-server/tree/master/packages/graphql-server-lambda"
},
"keywords": [
"GraphQL",
"Apollo",
"Server",
"Lambda",
"Javascript"
],
"author": "Jonas Helfer <jonas@helfer.email>",
"license": "MIT",
"bugs": {
"url": "https://github.com/apollostack/graphql-server/issues"
},
"homepage": "https://github.com/apollostack/graphql-server#readme",
"dependencies": {
"graphql-server-core": "^0.5.1",
"graphql-server-module-graphiql": "^0.4.4"
},
"devDependencies": {
"@types/aws-lambda": "0.0.5",
"@types/graphql": "^0.8.6",
"graphql-server-integration-testsuite": "^0.5.1"
},
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0"
},
"typings": "dist/index.d.ts",
"typescript": {
"definition": "dist/index.d.ts"
}
}

View file

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

View file

@ -0,0 +1,64 @@
import { graphqlLambda, graphiqlLambda } from './lambdaApollo';
import testSuite, { schema as Schema, CreateAppOptions } from 'graphql-server-integration-testsuite';
import { expect } from 'chai';
import { GraphQLOptions } from 'graphql-server-core';
import 'mocha';
import * as url from 'url';
function createLambda(options: CreateAppOptions = {}) {
let handler,
callback,
event,
context;
options.graphqlOptions = options.graphqlOptions || { schema: Schema };
if (options.graphiqlOptions ) {
handler = graphiqlLambda( options.graphiqlOptions );
} else {
handler = graphqlLambda( options.graphqlOptions );
}
return function(req, res) {
let body = '';
req.on('data', function (chunk) {
body += chunk;
});
req.on('end', function() {
let urlObject = url.parse(req.url, true);
event = {
httpMethod: req.method,
body: body,
path: req.url,
queryStringParameters: urlObject.query,
};
context = {};
callback = function(error, result) {
res.statusCode = result.statusCode;
for (let key in result.headers) {
if (result.headers.hasOwnProperty(key)) {
res.setHeader(key, result.headers[key]);
}
}
res.write(result.body);
res.end();
};
handler(event, context, callback);
});
};
}
describe('lambdaApollo', () => {
it('throws error if called without schema', function(){
expect(() => graphqlLambda(undefined as GraphQLOptions)).to.throw('Apollo Server requires options.');
});
it('throws an error if called with more than one argument', function(){
expect(() => (<any>graphqlLambda)({}, {})).to.throw(
'Apollo Server expects exactly one argument, got 2');
});
});
describe('integration:Lambda', () => {
testSuite(createLambda);
});

View file

@ -0,0 +1,106 @@
import * as lambda from 'aws-lambda';
import { GraphQLOptions, runHttpQuery } from 'graphql-server-core';
import * as GraphiQL from 'graphql-server-module-graphiql';
export interface LambdaGraphQLOptionsFunction {
(event: any, context: lambda.Context): GraphQLOptions | Promise<GraphQLOptions>;
}
// Design principles:
// - there is just one way allowed: POST request with JSON body. Nothing else.
// - simple, fast and secure
//
export interface LambdaHandler {
(event: any, context: lambda.Context, callback: lambda.Callback): void;
}
export interface IHeaders {
[header: string]: string | number;
}
export function graphqlLambda( options: GraphQLOptions | LambdaGraphQLOptionsFunction ): LambdaHandler {
if (!options) {
throw new Error('Apollo Server requires options.');
}
if (arguments.length > 1) {
throw new Error(`Apollo Server expects exactly one argument, got ${arguments.length}`);
}
return async (event, lambdaContext: lambda.Context, callback: lambda.Callback) => {
let query = (event.httpMethod === 'POST') ? event.body : event.queryStringParameters,
statusCode: number = null,
gqlResponse = null,
headers: {[headerName: string]: string} = {};
if (query && typeof query === 'string') {
query = JSON.parse(query);
}
try {
gqlResponse = await runHttpQuery([event, lambdaContext], {
method: event.httpMethod,
options: options,
query: query,
});
headers['Content-Type'] = 'application/json';
statusCode = 200;
} catch (error) {
if ( 'HttpQueryError' !== error.name ) {
throw error;
}
headers = error.headers;
statusCode = error.statusCode;
gqlResponse = error.message;
} finally {
callback(
null,
{
'statusCode': statusCode,
'headers': headers,
'body': gqlResponse,
},
);
}
};
}
/* This Lambda Function Handler returns the html for the GraphiQL interactive query UI
*
* GraphiQLData arguments
*
* - endpointURL: the relative or absolute URL for the endpoint which GraphiQL will make queries to
* - (optional) query: the GraphQL query to pre-fill in the GraphiQL UI
* - (optional) variables: a JS object of variables to pre-fill in the GraphiQL UI
* - (optional) operationName: the operationName 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) {
return (event, lambdaContext: lambda.Context, callback: lambda.Callback) => {
const q = event.queryStringParameters || {};
const query = q.query || '';
const variables = q.variables || '{}';
const operationName = q.operationName || '';
const graphiQLString = GraphiQL.renderGraphiQL({
endpointURL: options.endpointURL,
query: query || options.query,
variables: q.variables && JSON.parse(variables) || options.variables,
operationName: operationName || options.operationName,
passHeader: options.passHeader,
});
callback(
null,
{
'statusCode': 200,
'headers': {
'Content-Type': 'text/html',
},
'body': graphiQLString,
},
);
};
}

View file

@ -0,0 +1,17 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": [
"node_modules/@types"
],
"types": [
"@types/node"
]
},
"exclude": [
"node_modules",
"dist"
]
}

View file

@ -7,4 +7,5 @@ require('../packages/graphql-server-express/dist/connectApollo.test');
require('../packages/graphql-server-hapi/dist/hapiApollo.test');
require('../packages/graphql-server-koa/dist/koaApollo.test');
require('../packages/graphql-server-restify/dist/restifyApollo.test');
require('../packages/graphql-server-lambda/dist/lambdaApollo.test');
require('../packages/graphql-server-express/dist/apolloServerHttp.test');