Skip to content

Commit

Permalink
Azure Functions bindings (#503)
Browse files Browse the repository at this point in the history
  • Loading branch information
ulrikstrid authored and martijnwalraven committed Aug 15, 2017
1 parent a750507 commit 814db56
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ where `<variant>` is one of the following:
- `restify`
- `lambda`
- `micro`
- `azure-functions`

### Express

Expand Down
5 changes: 5 additions & 0 deletions packages/apollo-server-azure-functions/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*
!dist
!dist/**/*
dist/**/*.test.*
!package.json
49 changes: 49 additions & 0 deletions packages/apollo-server-azure-functions/README.md
Original file line number Diff line number Diff line change
@@ -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);
}
};
```
43 changes: 43 additions & 0 deletions packages/apollo-server-azure-functions/package.json
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>",
"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"
}
}
Original file line number Diff line number Diff line change
@@ -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(() => (<any>graphqlAzureFunctions)({}, {})).to.throw(
'Apollo Server expects exactly one argument, got 2',
);
});
});

describe('integration:Azure Functions', () => {
testSuite(createFunction);
});
126 changes: 126 additions & 0 deletions packages/apollo-server-azure-functions/src/azureFunctionsApollo.ts
Original file line number Diff line number Diff line change
@@ -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<GraphQLOptions>;
}

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<GraphiQL.GraphiQLData>;
}

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);
},
);
};
}
8 changes: 8 additions & 0 deletions packages/apollo-server-azure-functions/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
AzureFunctionsHandler,
IHeaders,
AzureFunctionsGraphQLOptionsFunction,
AzureFunctionsGraphiQLOptionsFunction,
graphqlAzureFunctions,
graphiqlAzureFunctions,
} from './azureFunctionsApollo';
17 changes: 17 additions & 0 deletions packages/apollo-server-azure-functions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"typeRoots": [
"node_modules/@types"
],
"types": [
"@types/node"
]
},
"exclude": [
"node_modules",
"dist"
]
}
1 change: 1 addition & 0 deletions test/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 814db56

Please sign in to comment.