From 814db561fab9c0afa39440a9a2c5399e4676cda4 Mon Sep 17 00:00:00 2001 From: Ulrik Strid Date: Tue, 15 Aug 2017 09:59:29 +0200 Subject: [PATCH] Azure Functions bindings (#503) --- README.md | 1 + .../apollo-server-azure-functions/.npmignore | 5 + .../apollo-server-azure-functions/README.md | 49 +++++++ .../package.json | 43 ++++++ .../src/azureFunctionsApollo.test.ts | 89 +++++++++++++ .../src/azureFunctionsApollo.ts | 126 ++++++++++++++++++ .../src/index.ts | 8 ++ .../tsconfig.json | 17 +++ test/tests.js | 1 + 9 files changed, 339 insertions(+) create mode 100755 packages/apollo-server-azure-functions/.npmignore create mode 100755 packages/apollo-server-azure-functions/README.md create mode 100644 packages/apollo-server-azure-functions/package.json create mode 100755 packages/apollo-server-azure-functions/src/azureFunctionsApollo.test.ts create mode 100755 packages/apollo-server-azure-functions/src/azureFunctionsApollo.ts create mode 100755 packages/apollo-server-azure-functions/src/index.ts create mode 100644 packages/apollo-server-azure-functions/tsconfig.json diff --git a/README.md b/README.md index fbf6622a65d..fa451721e01 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ where `` is one of the following: - `restify` - `lambda` - `micro` + - `azure-functions` ### Express diff --git a/packages/apollo-server-azure-functions/.npmignore b/packages/apollo-server-azure-functions/.npmignore new file mode 100755 index 00000000000..063364e2b5d --- /dev/null +++ b/packages/apollo-server-azure-functions/.npmignore @@ -0,0 +1,5 @@ +* +!dist +!dist/**/* +dist/**/*.test.* +!package.json diff --git a/packages/apollo-server-azure-functions/README.md b/packages/apollo-server-azure-functions/README.md new file mode 100755 index 00000000000..11af14443e0 --- /dev/null +++ b/packages/apollo-server-azure-functions/README.md @@ -0,0 +1,49 @@ +# graphql-server-lambda + +This is the Azure Functions integration for the Apollo community GraphQL Server. [Read the docs.](http://dev.apollodata.com/tools/apollo-server/index.html) + + +## Example: + +```js +const server = require("apollo-server-azure-functions"); +const graphqlTools = require("graphql-tools"); + +const typeDefs = ` + type Random { + id: Int! + rand: String + } + + type Query { + rands: [Random] + rand(id: Int!): Random + } +`; + +const rands = [{ id: 1, rand: "random" }, { id: 2, rand: "modnar" }]; + +const resolvers = { + Query: { + rands: () => rands, + rand: (_, { id }) => rands.find(rand => rand.id === id) + } +}; + +const schema = graphqlTools.makeExecutableSchema({ + typeDefs, + resolvers +}); + +module.exports = function run(context, request) { + if (request.method === "POST") { + server.graphqlAzureFunctions({ + endpointURL: '/api/graphql' + })(context, request); + } else if (request.method === "GET") { + return server.graphiqlAzureFunctions({ + endpointURL: '/api/graphql' + })(context, request); + } +}; +``` diff --git a/packages/apollo-server-azure-functions/package.json b/packages/apollo-server-azure-functions/package.json new file mode 100644 index 00000000000..ea0bb1f6370 --- /dev/null +++ b/packages/apollo-server-azure-functions/package.json @@ -0,0 +1,43 @@ +{ + "name": "apollo-server-azure-functions", + "version": "1.1.0", + "description": "Node.js GraphQl server for Azure Functions", + "main": "dist/index.js", + "scripts": { + "compile": "tsc", + "prepublish": "npm run compile" + }, + "repository": { + "type": "git", + "url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-azure-functions" + }, + "keywords": [ + "GraphQL", + "Apollo", + "Server", + "Azure", + "Functions" + ], + "author": "Ulrik Strid ", + "license": "MIT", + "bugs": { + "url": "https://github.com/apollographql/apollo-server/issues" + }, + "homepage": "https://github.com/apollographql/apollo-server#readme", + "dependencies": { + "apollo-server-core": "^1.0.2", + "apollo-server-module-graphiql": "^1.0.5" + }, + "devDependencies": { + "azure-functions-typescript": "0.0.1", + "@types/graphql": "^0.10.1", + "apollo-server-integration-testsuite": "^1.0.5" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.1" + }, + "typings": "dist/index.d.ts", + "typescript": { + "definition": "dist/index.d.ts" + } +} diff --git a/packages/apollo-server-azure-functions/src/azureFunctionsApollo.test.ts b/packages/apollo-server-azure-functions/src/azureFunctionsApollo.test.ts new file mode 100755 index 00000000000..d284d66fb89 --- /dev/null +++ b/packages/apollo-server-azure-functions/src/azureFunctionsApollo.test.ts @@ -0,0 +1,89 @@ +import { + AzureFunctionsHandler, + graphqlAzureFunctions, + graphiqlAzureFunctions, +} from './azureFunctionsApollo'; +import testSuite, { + schema as Schema, + CreateAppOptions, +} from 'apollo-server-integration-testsuite'; +import { expect } from 'chai'; +import { GraphQLOptions } from 'apollo-server-core'; +import 'mocha'; +import * as url from 'url'; + +function createFunction(options: CreateAppOptions = {}) { + let route, callback, context; + let handler: AzureFunctionsHandler; + + options.graphqlOptions = options.graphqlOptions || { schema: Schema }; + if (options.graphiqlOptions) { + route = '/graphiql'; + handler = graphiqlAzureFunctions(options.graphiqlOptions); + } else { + route = '/graphql'; + handler = graphqlAzureFunctions(options.graphqlOptions); + } + + return function(req, res) { + if (!req.url.startsWith(route)) { + res.statusCode = 404; + res.end(); + return; + } + + let body = ''; + req.on('data', function(chunk) { + body += chunk; + }); + req.on('end', function() { + let urlObject = url.parse(req.url, true); + const request = { + method: req.method, + originalUrl: req.url, + query: urlObject.query, + headers: req.headers, + body: body, + rawbody: body, + }; + + context = { + done: function(error, result) { + res.statusCode = result.status; + for (let key in result.headers) { + if (result.headers.hasOwnProperty(key)) { + res.setHeader(key, result.headers[key]); + } + } + + if (error) { + res.error = error; + } + + res.write(result.body); + res.end(); + }, + }; + + handler(context, request); + }); + }; +} + +describe('azureFunctionsApollo', () => { + it('throws error if called without schema', function() { + expect(() => graphqlAzureFunctions(undefined as GraphQLOptions)).to.throw( + 'Apollo Server requires options.', + ); + }); + + it('throws an error if called with more than one argument', function() { + expect(() => (graphqlAzureFunctions)({}, {})).to.throw( + 'Apollo Server expects exactly one argument, got 2', + ); + }); +}); + +describe('integration:Azure Functions', () => { + testSuite(createFunction); +}); diff --git a/packages/apollo-server-azure-functions/src/azureFunctionsApollo.ts b/packages/apollo-server-azure-functions/src/azureFunctionsApollo.ts new file mode 100755 index 00000000000..e823a07d4df --- /dev/null +++ b/packages/apollo-server-azure-functions/src/azureFunctionsApollo.ts @@ -0,0 +1,126 @@ +import { + IHttpContext, + IFunctionRequest, + HttpStatusCodes, +} from 'azure-functions-typescript'; +import { GraphQLOptions, runHttpQuery } from 'apollo-server-core'; +import * as GraphiQL from 'apollo-server-module-graphiql'; + +export interface AzureFunctionsGraphQLOptionsFunction { + (context: IHttpContext): GraphQLOptions | Promise; +} + +export interface AzureFunctionsHandler { + (context: IHttpContext, request: IFunctionRequest): void; +} + +export interface IHeaders { + 'content-type'?: string; + 'content-length'?: HttpStatusCodes | number; + 'content-disposition'?: string; + 'content-encoding'?: string; + 'content-language'?: string; + 'content-range'?: string; + 'content-location'?: string; + 'content-md5'?: Buffer; + 'expires'?: Date; + 'last-modified'?: Date; + [header: string]: any; +} + +export interface AzureFunctionsGraphiQLOptionsFunction { + (context: IHttpContext, request: IFunctionRequest): + | GraphiQL.GraphiQLData + | Promise; +} + +export function graphqlAzureFunctions( + options: GraphQLOptions | AzureFunctionsGraphQLOptionsFunction, +): AzureFunctionsHandler { + 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 (httpContext: IHttpContext, request: IFunctionRequest) => { + const queryRequest = { + method: request.method, + options: options, + query: request.method === 'POST' ? request.body : request.query, + }; + + if (queryRequest.query && typeof queryRequest.query === 'string') { + queryRequest.query = JSON.parse(queryRequest.query); + } + + return runHttpQuery([httpContext, request], queryRequest) + .then(gqlResponse => { + const result = { + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: gqlResponse, + }; + + httpContext.res = result; + + httpContext.done(null, result); + }) + .catch(error => { + const result = { + status: error.statusCode, + headers: error.headers, + body: error.message, + }; + + httpContext.res = result; + + httpContext.done(null, result); + }); + }; +} + +/* This Azure Functions 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 graphiqlAzureFunctions( + options: GraphiQL.GraphiQLData | AzureFunctionsGraphiQLOptionsFunction, +) { + return (httpContext: IHttpContext, request: IFunctionRequest) => { + const query = request.query; + + GraphiQL.resolveGraphiQLString(query, options, httpContext, request).then( + graphiqlString => { + httpContext.res = { + status: 200, + headers: { + 'Content-Type': 'text/html', + }, + body: graphiqlString, + }; + + httpContext.done(null, httpContext.res); + }, + error => { + httpContext.res = { + status: 500, + body: error.message, + }; + + httpContext.done(null, httpContext.res); + }, + ); + }; +} diff --git a/packages/apollo-server-azure-functions/src/index.ts b/packages/apollo-server-azure-functions/src/index.ts new file mode 100755 index 00000000000..71c5b6da461 --- /dev/null +++ b/packages/apollo-server-azure-functions/src/index.ts @@ -0,0 +1,8 @@ +export { + AzureFunctionsHandler, + IHeaders, + AzureFunctionsGraphQLOptionsFunction, + AzureFunctionsGraphiQLOptionsFunction, + graphqlAzureFunctions, + graphiqlAzureFunctions, +} from './azureFunctionsApollo'; diff --git a/packages/apollo-server-azure-functions/tsconfig.json b/packages/apollo-server-azure-functions/tsconfig.json new file mode 100644 index 00000000000..dcab1f88b17 --- /dev/null +++ b/packages/apollo-server-azure-functions/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "typeRoots": [ + "node_modules/@types" + ], + "types": [ + "@types/node" + ] + }, + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/test/tests.js b/test/tests.js index 4bb8811e001..ff56d36c62a 100644 --- a/test/tests.js +++ b/test/tests.js @@ -10,6 +10,7 @@ require('../packages/apollo-server-hapi/dist/hapiApollo.test'); (NODE_MAJOR_VERSION >= 6) && require('../packages/apollo-server-micro/dist/microApollo.test'); (NODE_MAJOR_VERSION >= 7) && require('../packages/apollo-server-koa/dist/koaApollo.test'); require('../packages/apollo-server-lambda/dist/lambdaApollo.test'); +require('../packages/apollo-server-azure-functions/dist/azureFunctionsApollo.test'); require('../packages/apollo-server-express/dist/apolloServerHttp.test'); // XXX: Running restify last as it breaks http.