From e2406e2a69cc8b6223d4831adcb4faf2e14c68c7 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 30 Sep 2018 17:44:15 +0200 Subject: [PATCH 01/11] apollo-server-azure-function first implementation. Closes #1752. Added info about azure-function and cloud-function to readme.md --- README.md | 3 + .../apollo-server-azure-function/.npmignore | 6 + .../apollo-server-azure-function/README.md | 220 ++++++++++++++++++ .../apollo-server-azure-function/package.json | 44 ++++ .../src/ApolloServer.ts | 162 +++++++++++++ .../src/__tests__/azureFunctionApollo.test.ts | 55 +++++ .../src/azureFunctionApollo.ts | 88 +++++++ .../src/azureFunctions.d.ts | 151 ++++++++++++ .../apollo-server-azure-function/src/index.ts | 24 ++ .../tsconfig.json | 9 + 10 files changed, 762 insertions(+) create mode 100644 packages/apollo-server-azure-function/.npmignore create mode 100644 packages/apollo-server-azure-function/README.md create mode 100644 packages/apollo-server-azure-function/package.json create mode 100644 packages/apollo-server-azure-function/src/ApolloServer.ts create mode 100644 packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts create mode 100644 packages/apollo-server-azure-function/src/azureFunctionApollo.ts create mode 100644 packages/apollo-server-azure-function/src/azureFunctions.d.ts create mode 100644 packages/apollo-server-azure-function/src/index.ts create mode 100644 packages/apollo-server-azure-function/tsconfig.json diff --git a/README.md b/README.md index 62097d8c152..b5566d6e685 100644 --- a/README.md +++ b/README.md @@ -86,8 +86,11 @@ Often times, Apollo Server needs to be run with a particular integration. To sta - `express` - `hapi` - `lambda` +- `azure-function` +- `cloud-function` - `cloudflare` + If a framework is not on this list and it should be supported, please open a PR. ### Express diff --git a/packages/apollo-server-azure-function/.npmignore b/packages/apollo-server-azure-function/.npmignore new file mode 100644 index 00000000000..a165046d359 --- /dev/null +++ b/packages/apollo-server-azure-function/.npmignore @@ -0,0 +1,6 @@ +* +!src/**/* +!dist/**/* +dist/**/*.test.* +!package.json +!README.md diff --git a/packages/apollo-server-azure-function/README.md b/packages/apollo-server-azure-function/README.md new file mode 100644 index 00000000000..1ed9c8d2ec7 --- /dev/null +++ b/packages/apollo-server-azure-function/README.md @@ -0,0 +1,220 @@ +--- +title: Azure Functions +description: Setting up Apollo Server with Azure Functions +--- + +This is the Azure functions integration of GraphQL Server. Apollo Server is a community-maintained open-source GraphQL server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/v2). [Read the CHANGELOG](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md). + +```sh +npm install apollo-server-azure-functions@rc graphql +``` + +## Deploying with AWS Serverless Application Model (SAM) + +To deploy the AWS Lambda function we must create a Cloudformation Template and a S3 bucket to store the artifact (zip of source code) and template. We will use the [AWS Command Line Interface](https://aws.amazon.com/cli/). + +#### 1. Write the API handlers + +In a file named `graphql.js`, place the following code: + +```js +const { ApolloServer, gql } = require('apollo-server-lambda'); + +// Construct a schema, using GraphQL schema language +const typeDefs = gql` + type Query { + hello: String + } +`; + +// Provide resolver functions for your schema fields +const resolvers = { + Query: { + hello: () => 'Hello world!', + }, +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, +}); + +exports.handler = server.createHandler(); +``` + +#### 2. Create an S3 bucket + +The bucket name must be universally unique. + +```bash +aws s3 mb s3:// +``` + +#### 3. Create the Template + +This will look for a file called graphql.js with the export `graphqlHandler`. It creates one API endpoints: + +* `/graphql` (GET and POST) + +In a file called `template.yaml`: + +```yaml +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Resources: + GraphQL: + Type: AWS::Serverless::Function + Properties: + Handler: graphql.handler + Runtime: nodejs8.10 + Events: + AnyRequest: + Type: Api + Properties: + Path: /graphql + Method: ANY +``` + +#### 4. Package source code and dependencies + +This will read and transform the template, created in previous step. Package and upload the artifact to the S3 bucket and generate another template for the deployment. + +```sh +aws cloudformation package \ + --template-file template.yaml \ + --output-template-file serverless-output.yaml \ + --s3-bucket +``` + +#### 5. Deploy the API + +The will create the Lambda Function and API Gateway for GraphQL. We use the stack-name `prod` to mean production but any stack name can be used. + +``` +aws cloudformation deploy \ + --template-file serverless-output.yaml \ + --stack-name prod \ + --capabilities CAPABILITY_IAM +``` + +## Getting request info + +To read information about the current request from the API Gateway event (HTTP headers, HTTP method, body, path, ...) or the current Lambda Context (Function Name, Function Version, awsRequestId, time remaining, ...) use the options function. This way they can be passed to your schema resolvers using the context option. + +```js +const { ApolloServer, gql } = require('apollo-server-lambda'); + +// Construct a schema, using GraphQL schema language +const typeDefs = gql` + type Query { + hello: String + } +`; + +// Provide resolver functions for your schema fields +const resolvers = { + Query: { + hello: () => 'Hello world!', + }, +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, + context: ({ event, context }) => ({ + headers: event.headers, + functionName: context.functionName, + event, + context, + }), +}); + +exports.handler = server.createHandler(); +``` + +## Modifying the Lambda Response (Enable CORS) + +To enable CORS the response HTTP headers need to be modified. To accomplish this use the `cors` option. + +```js +const { ApolloServer, gql } = require('apollo-server-lambda'); + +// Construct a schema, using GraphQL schema language +const typeDefs = gql` + type Query { + hello: String + } +`; + +// Provide resolver functions for your schema fields +const resolvers = { + Query: { + hello: () => 'Hello world!', + }, +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, +}); + +exports.handler = server.createHandler({ + cors: { + origin: '*', + credentials: true, + }, +}); +``` + +To enable CORS response for requests with credentials (cookies, http authentication) the allow origin header must equal the request origin and the allow credential header must be set to true. + +```js +const { ApolloServer, gql } = require('apollo-server-lambda'); + +// Construct a schema, using GraphQL schema language +const typeDefs = gql` + type Query { + hello: String + } +`; + +// Provide resolver functions for your schema fields +const resolvers = { + Query: { + hello: () => 'Hello world!', + }, +}; + +const server = new ApolloServer({ + typeDefs, + resolvers, +}); + +exports.handler = server.createHandler({ + cors: { + origin: true, + credentials: true, + }, +}); +``` + +### Cors Options + +The options correspond to the [express cors configuration](https://github.com/expressjs/cors#configuration-options) with the following fields(all are optional): + +* `origin`: boolean | string | string[] +* `methods`: string | string[] +* `allowedHeaders`: string | string[] +* `exposedHeaders`: string | string[] +* `credentials`: boolean +* `maxAge`: number + +## Principles + +GraphQL Server is built with the following principles in mind: + +* **By the community, for the community**: GraphQL Server's development is driven by the needs of developers +* **Simplicity**: by keeping things simple, GraphQL Server is easier to use, easier to contribute to, and more secure +* **Performance**: GraphQL Server is well-tested and production-ready - no modifications needed + +Anyone is welcome to contribute to GraphQL Server, just read [CONTRIBUTING.md](https://github.com/apollographql/apollo-server/blob/master/CONTRIBUTING.md), take a look at the [roadmap](https://github.com/apollographql/apollo-server/blob/master/ROADMAP.md) and make your first PR! diff --git a/packages/apollo-server-azure-function/package.json b/packages/apollo-server-azure-function/package.json new file mode 100644 index 00000000000..fb2fc62351e --- /dev/null +++ b/packages/apollo-server-azure-function/package.json @@ -0,0 +1,44 @@ +{ + "name": "apollo-server-azure-function", + "version": "2.1.0", + "description": "Production-ready Node.js GraphQL server for Azure Functions", + "keywords": [ + "GraphQL", + "Apollo", + "Server", + "Azure", + "Javascript" + ], + "author": "opensource@apollographql.com", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-azure-function" + }, + "homepage": "https://github.com/apollographql/apollo-server#readme", + "bugs": { + "url": "https://github.com/apollographql/apollo-server/issues" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "engines": { + "node": ">=6" + }, + "scripts": { + "clean": "rm -rf dist", + "compile": "tsc", + "prepare": "npm run clean && npm run compile" + }, + "dependencies": { + "@apollographql/graphql-playground-html": "^1.6.0", + "apollo-server-core": "file:../apollo-server-core", + "apollo-server-env": "file:../apollo-server-env", + "graphql-tools": "^3.0.4" + }, + "devDependencies": { + "apollo-server-integration-testsuite": "file:../apollo-server-integration-testsuite" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" + } +} diff --git a/packages/apollo-server-azure-function/src/ApolloServer.ts b/packages/apollo-server-azure-function/src/ApolloServer.ts new file mode 100644 index 00000000000..c7521a0b74c --- /dev/null +++ b/packages/apollo-server-azure-function/src/ApolloServer.ts @@ -0,0 +1,162 @@ +import { + HttpContext, + FunctionRequest, + FunctionResponse, +} from './azureFunctions'; +import { ApolloServerBase } from 'apollo-server-core'; +import { GraphQLOptions, Config } from 'apollo-server-core'; +import { + renderPlaygroundPage, + RenderPageOptions as PlaygroundRenderPageOptions, +} from '@apollographql/graphql-playground-html'; + +import { graphqlAzureFunction } from './azureFunctionApollo'; + +export interface CreateHandlerOptions { + cors?: { + origin?: boolean | string | string[]; + methods?: string | string[]; + allowedHeaders?: string | string[]; + exposedHeaders?: string | string[]; + credentials?: boolean; + maxAge?: number; + }; +} + +export class ApolloServer extends ApolloServerBase { + // If you feel tempted to add an option to this constructor. Please consider + // another place, since the documentation becomes much more complicated when + // the constructor is not longer shared between all integration + constructor(options: Config) { + if (process.env.ENGINE_API_KEY || options.engine) { + options.engine = { + sendReportsImmediately: true, + ...(typeof options.engine !== 'boolean' ? options.engine : {}), + }; + } + super(options); + } + + // This translates the arguments from the middleware into graphQL options It + // provides typings for the integration specific behavior, ideally this would + // be propagated with a generic to the super class + createGraphQLServerOptions( + request: FunctionRequest, + context: HttpContext, + ): Promise { + return super.graphQLServerOptions({ request, context }); + } + + public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { + const corsHeaders: FunctionResponse['headers'] = {}; + + if (cors) { + if (cors.methods) { + if (typeof cors.methods === 'string') { + corsHeaders['Access-Control-Allow-Methods'] = cors.methods; + } else if (Array.isArray(cors.methods)) { + corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(','); + } + } + + if (cors.allowedHeaders) { + if (typeof cors.allowedHeaders === 'string') { + corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders; + } else if (Array.isArray(cors.allowedHeaders)) { + corsHeaders[ + 'Access-Control-Allow-Headers' + ] = cors.allowedHeaders.join(','); + } + } + + if (cors.exposedHeaders) { + if (typeof cors.exposedHeaders === 'string') { + corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders; + } else if (Array.isArray(cors.exposedHeaders)) { + corsHeaders[ + 'Access-Control-Expose-Headers' + ] = cors.exposedHeaders.join(','); + } + } + + if (cors.credentials) { + corsHeaders['Access-Control-Allow-Credentials'] = 'true'; + } + if (cors.maxAge) { + corsHeaders['Access-Control-Max-Age'] = cors.maxAge; + } + } + + return (context: HttpContext, req: FunctionRequest) => { + if (cors && cors.origin) { + if (typeof cors.origin === 'string') { + corsHeaders['Access-Control-Allow-Origin'] = cors.origin; + } else if ( + typeof cors.origin === 'boolean' || + (Array.isArray(cors.origin) && + cors.origin.includes( + req.headers['Origin'] || req.headers['origin'], + )) + ) { + corsHeaders['Access-Control-Allow-Origin'] = + req.headers['Origin'] || req.headers['origin']; + } + + if (!cors.allowedHeaders) { + corsHeaders['Access-Control-Allow-Headers'] = + req.headers['Access-Control-Request-Headers']; + } + } + + if (req.method === 'OPTIONS') { + context.done(null, { + body: '', + status: 204, + headers: corsHeaders, + }); + return; + } + + if (this.playgroundOptions && req.method === 'GET') { + const acceptHeader = req.headers['Accept'] || req.headers['accept']; + if (acceptHeader && acceptHeader.includes('text/html')) { + // const requestUrl = new URL(req.originalUrl); + const path = req.originalUrl || '/'; + + const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { + endpoint: path, + ...this.playgroundOptions, + }; + const body = renderPlaygroundPage(playgroundRenderPageOptions); + context.done(null, { + body: body, + status: 200, + headers: { + 'Content-Type': 'text/html', + ...corsHeaders, + }, + }); + return; + } + } + + const callbackFilter = (error?: any, output?: FunctionResponse) => { + context.done( + error, + output && { + ...output, + headers: { + ...output.headers, + ...corsHeaders, + }, + }, + ); + }; + graphqlAzureFunction(this.createGraphQLServerOptions.bind(this))( + context, + req, + callbackFilter, + ); + }; + } +} diff --git a/packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts b/packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts new file mode 100644 index 00000000000..1bc502cd44e --- /dev/null +++ b/packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts @@ -0,0 +1,55 @@ +import { ApolloServer } from '../ApolloServer'; +import testSuite, { + schema as Schema, + CreateAppOptions, +} from 'apollo-server-integration-testsuite'; +import { Config } from 'apollo-server-core'; +import url from 'url'; +import { IncomingMessage, ServerResponse } from 'http'; + +const createLambda = (options: CreateAppOptions = {}) => { + const server = new ApolloServer( + (options.graphqlOptions as Config) || { schema: Schema }, + ); + + const handler = server.createHandler(); + + return (req: IncomingMessage, res: ServerResponse) => { + // return 404 if path is /bogus-route to pass the test, azure doesn't have paths + if (req.url.includes('/bogus-route')) { + res.statusCode = 404; + return res.end(); + } + + let body = ''; + req.on('data', chunk => (body += chunk)); + req.on('end', () => { + const urlObject = url.parse(req.url, true); + const request = { + method: req.method, + body: body && JSON.parse(body), + path: req.url, + query: urlObject.query, + headers: req.headers, + }; + const context = { + done(error, result) { + if (error) throw error; + res.statusCode = result.status; + for (let key in result.headers) { + if (result.headers.hasOwnProperty(key)) { + res.setHeader(key, result.headers[key]); + } + } + res.write(result.body); + res.end(); + }, + }; + handler(context as any, request as any); + }); + }; +}; + +describe('integration:AzureFunctions', () => { + testSuite(createLambda); +}); diff --git a/packages/apollo-server-azure-function/src/azureFunctionApollo.ts b/packages/apollo-server-azure-function/src/azureFunctionApollo.ts new file mode 100644 index 00000000000..bc7dc138898 --- /dev/null +++ b/packages/apollo-server-azure-function/src/azureFunctionApollo.ts @@ -0,0 +1,88 @@ +import { + HttpContext, + FunctionRequest, + FunctionResponse, +} from './azureFunctions'; +import { + GraphQLOptions, + HttpQueryError, + runHttpQuery, +} from 'apollo-server-core'; +import { Headers } from 'apollo-server-env'; + +export interface AzureFunctionGraphQLOptionsFunction { + (request: FunctionRequest, context: HttpContext): + | GraphQLOptions + | Promise; +} + +export interface AzureFunctionHandler { + ( + context: HttpContext, + request: FunctionRequest, + callback: (err?: any, output?: FunctionResponse) => void, + ): void; +} + +export function graphqlAzureFunction( + options: GraphQLOptions | AzureFunctionGraphQLOptionsFunction, +): AzureFunctionHandler { + 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}`, + ); + } + + const graphqlHandler: AzureFunctionHandler = ( + context, + request, + callback, + ): void => { + if (request.method === 'POST' && !request.body) { + callback(null, { + body: 'POST body missing.', + status: 500, + }); + return; + } + // const requestUrl = new URL(request.originalUrl); + runHttpQuery([request, context], { + method: request.method, + options: options, + query: + request.method === 'POST' && request.body + ? request.body + : request.query, + request: { + url: request.originalUrl, + method: request.method, + headers: new Headers(request.headers), + }, + }).then( + ({ graphqlResponse, responseInit }) => { + callback(null, { + body: graphqlResponse, + status: 200, + headers: responseInit.headers, + }); + }, + (error: HttpQueryError) => { + if ('HttpQueryError' !== error.name) { + callback(error); + } else { + callback(null, { + body: error.message, + status: error.statusCode, + headers: error.headers, + }); + } + }, + ); + }; + + return graphqlHandler; +} diff --git a/packages/apollo-server-azure-function/src/azureFunctions.d.ts b/packages/apollo-server-azure-function/src/azureFunctions.d.ts new file mode 100644 index 00000000000..c9dbd78a553 --- /dev/null +++ b/packages/apollo-server-azure-function/src/azureFunctions.d.ts @@ -0,0 +1,151 @@ +export type Context = { + invocationId: string; + bindingData: any; + bindings: any; + + log: (text: any) => void; + done: (err?: any, output?: object) => void; +}; + +export type HttpMethod = + | 'GET' + | 'POST' + | 'PUT' + | 'DELETE' + | 'HEAD' + | 'OPTIONS' + | 'TRACE' + | 'CONNECT' + | 'PATCH'; + +export type FunctionRequest = { + originalUrl: string; + method: HttpMethod; + query: { [key: string]: string }; + headers: { [name: string]: string }; + body: any; + rawbody: any; +}; + +export type FunctionResponse = { + body?: any; + status?: number; + headers?: { + '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; + [name: string]: any; + }; +}; + +export enum HttpStatusCodes { + // 1XX Informational + Continue = 100, + SwitchingProtocols = 101, + Processing = 102, + Checkpoint = 103, + + // 2XX Success + OK = 200, + Created = 201, + Accepted = 202, + NonAuthoritativeInformation = 203, + NoContent = 204, + ResetContent = 205, + PartialContent = 206, + MultiStatus = 207, + AlreadyReported = 208, + IMUsed = 226, + + // 3XX Redirection + MultipleChoices = 300, + MovedPermanently = 301, + Found = 302, + SeeOther = 303, + NotModified = 304, + UseProxy = 305, + SwitchProxy = 306, + TemporaryRedirect = 307, + PermanentRedirect = 308, + + // 4XX Client Error + BadRequest = 400, + Unauthorized = 401, + PaymentRequired = 402, + Forbidden = 403, + NotFound = 404, + MethodNotAllowed = 405, + NotAcceptable = 406, + ProxyAuthenticationRequired = 407, + RequestTimeout = 408, + Conflict = 409, + Gone = 410, + LengthRequired = 411, + PreconditionFailed = 412, + PayloadTooLarge = 413, + URITooLong = 414, + UnsupportedMediaType = 415, + RangeNotSatisfiable = 416, + ExpectationFailed = 417, + ImATeapot = 418, + MethodFailure = 420, + EnhanceYourCalm = 420, + MisdirectedRequest = 421, + UnprocessableEntity = 422, + Locked = 423, + FailedDependency = 424, + UpgradeRequired = 426, + PreconditionRequired = 428, + TooManyRequests = 429, + RequestHeaderFieldsTooLarge = 431, + LoginTimeout = 440, + NoResponse = 444, + RetryWith = 449, + BlockedByWindowsParentalControls = 450, + UnavailableForLegalReasons = 451, + Redirect = 451, + SSLCertificateError = 495, + SSLCertificateRequired = 496, + HttpRequestSentToHttpsPort = 497, + ClientClosedRequest = 499, + InvalidToken = 498, + TokenRequired = 499, + RequestWasForbiddenByAntivirus = 499, + + // 5XX Server Error + InternalServerError = 500, + NotImplemented = 501, + BadGateway = 502, + ServiceUnavailable = 503, + GatewayTimeout = 504, + HttpVersionNotSupported = 505, + VariantAlsoNegotiates = 506, + InsufficientStorage = 507, + LoopDetected = 508, + BandwidthLimitExceeded = 509, + NotExtended = 510, + NetworkAuthenticationRequired = 511, + UnknownError = 520, + WebServerIsDown = 521, + ConnectionTimedOut = 522, + OriginIsUnreachable = 523, + ATimeoutOccurred = 524, + SSLHandshakeFailed = 525, + InvalidSSLCertificate = 526, + SiteIsFrozen = 530, +} + +export type HttpContext = Context & { + res: FunctionResponse; +}; + +export interface FunctionHandler { + (context: HttpContext, request: FunctionRequest): void | Promise; +} diff --git a/packages/apollo-server-azure-function/src/index.ts b/packages/apollo-server-azure-function/src/index.ts new file mode 100644 index 00000000000..2ff184ef590 --- /dev/null +++ b/packages/apollo-server-azure-function/src/index.ts @@ -0,0 +1,24 @@ +export { + GraphQLUpload, + GraphQLOptions, + GraphQLExtension, + Config, + gql, + // Errors + ApolloError, + toApolloError, + SyntaxError, + ValidationError, + AuthenticationError, + ForbiddenError, + UserInputError, + // playground + defaultPlaygroundOptions, + PlaygroundConfig, + PlaygroundRenderPageOptions, +} from 'apollo-server-core'; + +export * from 'graphql-tools'; + +// ApolloServer integration. +export { ApolloServer, CreateHandlerOptions } from './ApolloServer'; diff --git a/packages/apollo-server-azure-function/tsconfig.json b/packages/apollo-server-azure-function/tsconfig.json new file mode 100644 index 00000000000..b8b81721105 --- /dev/null +++ b/packages/apollo-server-azure-function/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"], + "exclude": ["**/__tests__", "**/__mocks__"] +} From 59b1f4d5e485e6a4d8ac8c564f109a2dae6653dc Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 30 Sep 2018 17:49:57 +0200 Subject: [PATCH 02/11] Updated changelog mentioning #1752 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d82ba35e11c..1ce0d793437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Allow an optional function to resolve the `rootValue`, passing the `DocumentNode` AST to determine the value. [PR #1555](https://github.com/apollographql/apollo-server/pull/1555) - Follow-up on the work in [PR #1516](https://github.com/apollographql/apollo-server/pull/1516) to also fix missing insertion cursor/caret when a custom GraphQL configuration is specified which doesn't specify its own `cursorShape` property. [PR #1607](https://github.com/apollographql/apollo-server/pull/1607) +- Azure functions support [#1752] [PR #1753] ### v2.1.0 From 92a1e6ba0161fe9decb0997ce009dc6f18937ebf Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 30 Sep 2018 17:53:19 +0200 Subject: [PATCH 03/11] Readme update for #1752 --- .../apollo-server-azure-function/README.md | 144 +++++------------- 1 file changed, 39 insertions(+), 105 deletions(-) diff --git a/packages/apollo-server-azure-function/README.md b/packages/apollo-server-azure-function/README.md index 1ed9c8d2ec7..12e5905d2ad 100644 --- a/packages/apollo-server-azure-function/README.md +++ b/packages/apollo-server-azure-function/README.md @@ -9,16 +9,17 @@ This is the Azure functions integration of GraphQL Server. Apollo Server is a co npm install apollo-server-azure-functions@rc graphql ``` -## Deploying with AWS Serverless Application Model (SAM) +## Writing azure function -To deploy the AWS Lambda function we must create a Cloudformation Template and a S3 bucket to store the artifact (zip of source code) and template. We will use the [AWS Command Line Interface](https://aws.amazon.com/cli/). +Azure functions currently support two runtime versions. This package assumes that function is running under **runtime 2.0**. -#### 1. Write the API handlers +Azure functions typically consist of at least 2 files - index.js (function handler definition) and function.json (function settings and bindings). +For more information about azure functions development model in general, refer to [official Azure functions docs](https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node). -In a file named `graphql.js`, place the following code: +Example index.js: ```js -const { ApolloServer, gql } = require('apollo-server-lambda'); +const { gql, ApolloServer } = require("apollo-server-azure-function"); // Construct a schema, using GraphQL schema language const typeDefs = gql` @@ -27,117 +28,50 @@ const typeDefs = gql` } `; -// Provide resolver functions for your schema fields +// A map of functions which return data for the schema. const resolvers = { Query: { - hello: () => 'Hello world!', - }, + hello: () => "world" + } }; -const server = new ApolloServer({ - typeDefs, - resolvers, -}); - -exports.handler = server.createHandler(); -``` - -#### 2. Create an S3 bucket - -The bucket name must be universally unique. +const server = new ApolloServer({ typeDefs, resolvers }); -```bash -aws s3 mb s3:// +module.exports = server.createHandler(); ``` -#### 3. Create the Template - -This will look for a file called graphql.js with the export `graphqlHandler`. It creates one API endpoints: - -* `/graphql` (GET and POST) - -In a file called `template.yaml`: - -```yaml -AWSTemplateFormatVersion: '2010-09-09' -Transform: AWS::Serverless-2016-10-31 -Resources: - GraphQL: - Type: AWS::Serverless::Function - Properties: - Handler: graphql.handler - Runtime: nodejs8.10 - Events: - AnyRequest: - Type: Api - Properties: - Path: /graphql - Method: ANY +Example function.json: +```json +{ + "disabled": false, + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "$return" + } + ] +} ``` -#### 4. Package source code and dependencies - -This will read and transform the template, created in previous step. Package and upload the artifact to the S3 bucket and generate another template for the deployment. - -```sh -aws cloudformation package \ - --template-file template.yaml \ - --output-template-file serverless-output.yaml \ - --s3-bucket -``` - -#### 5. Deploy the API - -The will create the Lambda Function and API Gateway for GraphQL. We use the stack-name `prod` to mean production but any stack name can be used. - -``` -aws cloudformation deploy \ - --template-file serverless-output.yaml \ - --stack-name prod \ - --capabilities CAPABILITY_IAM -``` - -## Getting request info - -To read information about the current request from the API Gateway event (HTTP headers, HTTP method, body, path, ...) or the current Lambda Context (Function Name, Function Version, awsRequestId, time remaining, ...) use the options function. This way they can be passed to your schema resolvers using the context option. - -```js -const { ApolloServer, gql } = require('apollo-server-lambda'); - -// Construct a schema, using GraphQL schema language -const typeDefs = gql` - type Query { - hello: String - } -`; - -// Provide resolver functions for your schema fields -const resolvers = { - Query: { - hello: () => 'Hello world!', - }, -}; - -const server = new ApolloServer({ - typeDefs, - resolvers, - context: ({ event, context }) => ({ - headers: event.headers, - functionName: context.functionName, - event, - context, - }), -}); - -exports.handler = server.createHandler(); -``` +It is important to set output binding name to '$return' for apollo-server-azure-function to work correctly. -## Modifying the Lambda Response (Enable CORS) +## Modifying the Azure Function Response (Enable CORS) To enable CORS the response HTTP headers need to be modified. To accomplish this use the `cors` option. ```js -const { ApolloServer, gql } = require('apollo-server-lambda'); +const { ApolloServer, gql } = require('apollo-server-azure-function'); // Construct a schema, using GraphQL schema language const typeDefs = gql` @@ -158,7 +92,7 @@ const server = new ApolloServer({ resolvers, }); -exports.handler = server.createHandler({ +module.exports = server.createHandler({ cors: { origin: '*', credentials: true, @@ -169,7 +103,7 @@ exports.handler = server.createHandler({ To enable CORS response for requests with credentials (cookies, http authentication) the allow origin header must equal the request origin and the allow credential header must be set to true. ```js -const { ApolloServer, gql } = require('apollo-server-lambda'); +const { ApolloServer, gql } = require('apollo-server-azure-function'); // Construct a schema, using GraphQL schema language const typeDefs = gql` @@ -190,7 +124,7 @@ const server = new ApolloServer({ resolvers, }); -exports.handler = server.createHandler({ +module.exports = server.createHandler({ cors: { origin: true, credentials: true, From 4bc8572450e240778a81407e22fe5006f64c0912 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 30 Sep 2018 21:09:05 +0200 Subject: [PATCH 04/11] Added cors and playground tests to apollo-server-azure-function --- .../src/__tests__/azureFunctionApollo.test.ts | 104 +++++++++++++++++- 1 file changed, 102 insertions(+), 2 deletions(-) diff --git a/packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts b/packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts index 1bc502cd44e..8a918178157 100644 --- a/packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts +++ b/packages/apollo-server-azure-function/src/__tests__/azureFunctionApollo.test.ts @@ -7,7 +7,7 @@ import { Config } from 'apollo-server-core'; import url from 'url'; import { IncomingMessage, ServerResponse } from 'http'; -const createLambda = (options: CreateAppOptions = {}) => { +const createAzureFunction = (options: CreateAppOptions = {}) => { const server = new ApolloServer( (options.graphqlOptions as Config) || { schema: Schema }, ); @@ -51,5 +51,105 @@ const createLambda = (options: CreateAppOptions = {}) => { }; describe('integration:AzureFunctions', () => { - testSuite(createLambda); + testSuite(createAzureFunction); + + it('can append CORS headers to GET request', () => { + const server = new ApolloServer({ schema: Schema }); + const handler = server.createHandler({ + cors: { + origin: 'CORSOrigin', + methods: ['GET', 'POST', 'PUT'], + allowedHeaders: 'AllowedCORSHeader1,AllowedCORSHeader1', + exposedHeaders: 'ExposedCORSHeader1,ExposedCORSHeader2', + credentials: true, + maxAge: 42, + }, + }); + const expectedResult = { + testString: 'it works', + }; + const query = { + query: 'query test{ testString }', + }; + const request = { + method: 'GET', + body: null, + path: '/graphql', + query: query, + headers: {}, + }; + const context = { + done(error, result) { + if (error) throw error; + expect(result.status).toEqual(200); + expect(result.body).toEqual(expectedResult); + expect(result.headers['Access-Control-Allow-Origin']).toEqual( + 'CORSOrigin', + ); + expect(result.headers['Access-Control-Allow-Methods']).toEqual( + 'GET,POST,PUT', + ); + expect(result.headers['Access-Control-Allow-Headers']).toEqual( + 'AllowedCORSHeader1,AllowedCORSHeader1', + ); + expect(result.headers['Access-Control-Expose-Headers']).toEqual( + 'ExposedCORSHeader1,ExposedCORSHeader2', + ); + expect(result.headers['Access-Control-Allow-Credentials']).toEqual( + 'true', + ); + expect(result.headers['Access-Control-Max-Age']).toEqual(42); + }, + }; + handler(context as any, request as any); + }); + + it('can handle OPTIONS request with CORS headers', () => { + const server = new ApolloServer({ schema: Schema }); + const handler = server.createHandler({ + cors: { + allowedHeaders: 'AllowedCORSHeader1,AllowedCORSHeader1', + }, + }); + const request = { + method: 'OPTIONS', + body: null, + path: '/graphql', + query: null, + headers: {}, + }; + const context = { + done(error, result) { + if (error) throw error; + expect(result.status).toEqual(204); + expect(result.headers['Access-Control-Allow-Headers']).toEqual( + 'AllowedCORSHeader1,AllowedCORSHeader1', + ); + }, + }; + handler(context as any, request as any); + }); + + it('can return playground html', () => { + const server = new ApolloServer({ schema: Schema }); + const handler = server.createHandler({}); + const request = { + method: 'GET', + body: null, + path: '/', + query: null, + headers: { + Accept: 'text/html', + }, + }; + const context = { + done(error, result) { + if (error) throw error; + expect(result.status).toEqual(200); + expect(result.body).toMatch(/GraphQL Playground/gi); + expect(result.headers['Content-Type']).toEqual('text/html'); + }, + }; + handler(context as any, request as any); + }); }); From a93b5d27e0ea337a5e3a4a34d69f88eaf2efac9e Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 30 Sep 2018 21:12:22 +0200 Subject: [PATCH 05/11] #1752 - removed commented out code --- packages/apollo-server-azure-function/src/ApolloServer.ts | 1 - packages/apollo-server-azure-function/src/azureFunctionApollo.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/apollo-server-azure-function/src/ApolloServer.ts b/packages/apollo-server-azure-function/src/ApolloServer.ts index c7521a0b74c..0e22f89121d 100644 --- a/packages/apollo-server-azure-function/src/ApolloServer.ts +++ b/packages/apollo-server-azure-function/src/ApolloServer.ts @@ -120,7 +120,6 @@ export class ApolloServer extends ApolloServerBase { if (this.playgroundOptions && req.method === 'GET') { const acceptHeader = req.headers['Accept'] || req.headers['accept']; if (acceptHeader && acceptHeader.includes('text/html')) { - // const requestUrl = new URL(req.originalUrl); const path = req.originalUrl || '/'; const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { diff --git a/packages/apollo-server-azure-function/src/azureFunctionApollo.ts b/packages/apollo-server-azure-function/src/azureFunctionApollo.ts index bc7dc138898..fd680501701 100644 --- a/packages/apollo-server-azure-function/src/azureFunctionApollo.ts +++ b/packages/apollo-server-azure-function/src/azureFunctionApollo.ts @@ -49,7 +49,6 @@ export function graphqlAzureFunction( }); return; } - // const requestUrl = new URL(request.originalUrl); runHttpQuery([request, context], { method: request.method, options: options, From 7e875877e8e0e92f00cfbad4f6dad1baa8dff37e Mon Sep 17 00:00:00 2001 From: Michael Watson Date: Tue, 6 Nov 2018 10:38:13 -0800 Subject: [PATCH 06/11] Incorporate willStart from ApolloServerBase - Introduced willStart call from ApolloServerBase in the createHandler. This is similar to what is done in Lambda. - Update scripts for compiling - Bumbp version to alpha --- .DS_Store | Bin 0 -> 6148 bytes .../apollo-server-azure-function/package.json | 73 ++--- .../src/ApolloServer.ts | 286 +++++++++--------- .../tsconfig.json | 7 +- tsconfig.build.json | 1 + 5 files changed, 174 insertions(+), 193 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..404efbf7d3aa1a1299d19c1fccca39ab024b6fda GIT binary patch literal 6148 zcmeHKISv9b477n_B^pY~e1RWC2wuPkI7I;xNQizb-o?`x9|dTkg9eQyXA;MgC{wK0 zBBImlb|Nwokp^xkHygTU`{o_%Wki8+oUxLu7m_W|QJat3oFhiCr!bUNB5 zjS5f!DnJFO02TPE0$E<0;a5-PVN`$${DT7aeJF6ln%D;Vrvrnx0KfslZkT&70W1~( z*2Fdt5ts%Q7*x#`LxYZZ$-J7_1_oU;n-9&KH9Hjb+i`yJbkQ2fkqS_OR|Wd999jLJ z!$0)@uOzOh02TNv1#~c5%%*r!*4EDBtkxFz5^gznxEbb7!QkZ>=;as-E5~C`io9ZT X?AOFL(CLUf9mt;n(}hL_eyzX*+7K0P literal 0 HcmV?d00001 diff --git a/packages/apollo-server-azure-function/package.json b/packages/apollo-server-azure-function/package.json index fb2fc62351e..d8bf3c176e6 100644 --- a/packages/apollo-server-azure-function/package.json +++ b/packages/apollo-server-azure-function/package.json @@ -1,44 +1,33 @@ { - "name": "apollo-server-azure-function", - "version": "2.1.0", - "description": "Production-ready Node.js GraphQL server for Azure Functions", - "keywords": [ - "GraphQL", - "Apollo", - "Server", - "Azure", - "Javascript" - ], - "author": "opensource@apollographql.com", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-azure-function" - }, - "homepage": "https://github.com/apollographql/apollo-server#readme", - "bugs": { - "url": "https://github.com/apollographql/apollo-server/issues" - }, - "main": "dist/index.js", - "types": "dist/index.d.ts", - "engines": { - "node": ">=6" - }, - "scripts": { - "clean": "rm -rf dist", - "compile": "tsc", - "prepare": "npm run clean && npm run compile" - }, - "dependencies": { - "@apollographql/graphql-playground-html": "^1.6.0", - "apollo-server-core": "file:../apollo-server-core", - "apollo-server-env": "file:../apollo-server-env", - "graphql-tools": "^3.0.4" - }, - "devDependencies": { - "apollo-server-integration-testsuite": "file:../apollo-server-integration-testsuite" - }, - "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" - } + "name": "apollo-server-azure-function", + "version": "2.2.0-alpha.2", + "description": "Production-ready Node.js GraphQL server for Azure Functions", + "keywords": [ "GraphQL", "Apollo", "Server", "Azure", "Javascript" ], + "author": "opensource@apollographql.com", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-azure-function" + }, + "homepage": "https://github.com/apollographql/apollo-server#readme", + "bugs": { + "url": "https://github.com/apollographql/apollo-server/issues" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "engines": { + "node": ">=6" + }, + "dependencies": { + "@apollographql/graphql-playground-html": "^1.6.4", + "apollo-server-core": "file:../apollo-server-core", + "apollo-server-env": "file:../apollo-server-env", + "graphql-tools": "^4.0.0" + }, + "devDependencies": { + "apollo-server-integration-testsuite": "file:../apollo-server-integration-testsuite" + }, + "peerDependencies": { + "graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" + } } diff --git a/packages/apollo-server-azure-function/src/ApolloServer.ts b/packages/apollo-server-azure-function/src/ApolloServer.ts index 0e22f89121d..9560c9942e2 100644 --- a/packages/apollo-server-azure-function/src/ApolloServer.ts +++ b/packages/apollo-server-azure-function/src/ApolloServer.ts @@ -1,161 +1,149 @@ -import { - HttpContext, - FunctionRequest, - FunctionResponse, -} from './azureFunctions'; +import { HttpContext, FunctionRequest, FunctionResponse } from './azureFunctions'; import { ApolloServerBase } from 'apollo-server-core'; import { GraphQLOptions, Config } from 'apollo-server-core'; import { - renderPlaygroundPage, - RenderPageOptions as PlaygroundRenderPageOptions, + renderPlaygroundPage, + RenderPageOptions as PlaygroundRenderPageOptions } from '@apollographql/graphql-playground-html'; import { graphqlAzureFunction } from './azureFunctionApollo'; export interface CreateHandlerOptions { - cors?: { - origin?: boolean | string | string[]; - methods?: string | string[]; - allowedHeaders?: string | string[]; - exposedHeaders?: string | string[]; - credentials?: boolean; - maxAge?: number; - }; + cors?: { + origin?: boolean | string | string[]; + methods?: string | string[]; + allowedHeaders?: string | string[]; + exposedHeaders?: string | string[]; + credentials?: boolean; + maxAge?: number; + }; } export class ApolloServer extends ApolloServerBase { - // If you feel tempted to add an option to this constructor. Please consider - // another place, since the documentation becomes much more complicated when - // the constructor is not longer shared between all integration - constructor(options: Config) { - if (process.env.ENGINE_API_KEY || options.engine) { - options.engine = { - sendReportsImmediately: true, - ...(typeof options.engine !== 'boolean' ? options.engine : {}), - }; - } - super(options); - } - - // This translates the arguments from the middleware into graphQL options It - // provides typings for the integration specific behavior, ideally this would - // be propagated with a generic to the super class - createGraphQLServerOptions( - request: FunctionRequest, - context: HttpContext, - ): Promise { - return super.graphQLServerOptions({ request, context }); - } - - public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { - const corsHeaders: FunctionResponse['headers'] = {}; - - if (cors) { - if (cors.methods) { - if (typeof cors.methods === 'string') { - corsHeaders['Access-Control-Allow-Methods'] = cors.methods; - } else if (Array.isArray(cors.methods)) { - corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(','); - } - } - - if (cors.allowedHeaders) { - if (typeof cors.allowedHeaders === 'string') { - corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders; - } else if (Array.isArray(cors.allowedHeaders)) { - corsHeaders[ - 'Access-Control-Allow-Headers' - ] = cors.allowedHeaders.join(','); - } - } - - if (cors.exposedHeaders) { - if (typeof cors.exposedHeaders === 'string') { - corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders; - } else if (Array.isArray(cors.exposedHeaders)) { - corsHeaders[ - 'Access-Control-Expose-Headers' - ] = cors.exposedHeaders.join(','); - } - } - - if (cors.credentials) { - corsHeaders['Access-Control-Allow-Credentials'] = 'true'; - } - if (cors.maxAge) { - corsHeaders['Access-Control-Max-Age'] = cors.maxAge; - } - } - - return (context: HttpContext, req: FunctionRequest) => { - if (cors && cors.origin) { - if (typeof cors.origin === 'string') { - corsHeaders['Access-Control-Allow-Origin'] = cors.origin; - } else if ( - typeof cors.origin === 'boolean' || - (Array.isArray(cors.origin) && - cors.origin.includes( - req.headers['Origin'] || req.headers['origin'], - )) - ) { - corsHeaders['Access-Control-Allow-Origin'] = - req.headers['Origin'] || req.headers['origin']; - } - - if (!cors.allowedHeaders) { - corsHeaders['Access-Control-Allow-Headers'] = - req.headers['Access-Control-Request-Headers']; - } - } - - if (req.method === 'OPTIONS') { - context.done(null, { - body: '', - status: 204, - headers: corsHeaders, - }); - return; - } - - if (this.playgroundOptions && req.method === 'GET') { - const acceptHeader = req.headers['Accept'] || req.headers['accept']; - if (acceptHeader && acceptHeader.includes('text/html')) { - const path = req.originalUrl || '/'; - - const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { - endpoint: path, - ...this.playgroundOptions, - }; - const body = renderPlaygroundPage(playgroundRenderPageOptions); - context.done(null, { - body: body, - status: 200, - headers: { - 'Content-Type': 'text/html', - ...corsHeaders, - }, - }); - return; - } - } - - const callbackFilter = (error?: any, output?: FunctionResponse) => { - context.done( - error, - output && { - ...output, - headers: { - ...output.headers, - ...corsHeaders, - }, - }, - ); - }; - graphqlAzureFunction(this.createGraphQLServerOptions.bind(this))( - context, - req, - callbackFilter, - ); - }; - } + // If you feel tempted to add an option to this constructor. Please consider + // another place, since the documentation becomes much more complicated when + // the constructor is not longer shared between all integration + constructor(options: Config) { + if (process.env.ENGINE_API_KEY || options.engine) { + options.engine = { + sendReportsImmediately: true, + ...typeof options.engine !== 'boolean' ? options.engine : {} + }; + } + super(options); + } + + // This translates the arguments from the middleware into graphQL options It + // provides typings for the integration specific behavior, ideally this would + // be propagated with a generic to the super class + createGraphQLServerOptions(request: FunctionRequest, context: HttpContext): Promise { + return super.graphQLServerOptions({ request, context }); + } + + public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { + // We will kick off the `willStart` event once for the server, and then + // await it before processing any requests by incorporating its `await` into + // the GraphQLServerOptions function which is called before each request. + const promiseWillStart = this.willStart(); + + const corsHeaders: FunctionResponse['headers'] = {}; + + if (cors) { + if (cors.methods) { + if (typeof cors.methods === 'string') { + corsHeaders['Access-Control-Allow-Methods'] = cors.methods; + } else if (Array.isArray(cors.methods)) { + corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(','); + } + } + + if (cors.allowedHeaders) { + if (typeof cors.allowedHeaders === 'string') { + corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders; + } else if (Array.isArray(cors.allowedHeaders)) { + corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders.join(','); + } + } + + if (cors.exposedHeaders) { + if (typeof cors.exposedHeaders === 'string') { + corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders; + } else if (Array.isArray(cors.exposedHeaders)) { + corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders.join(','); + } + } + + if (cors.credentials) { + corsHeaders['Access-Control-Allow-Credentials'] = 'true'; + } + if (cors.maxAge) { + corsHeaders['Access-Control-Max-Age'] = cors.maxAge; + } + } + + return (context: HttpContext, req: FunctionRequest) => { + if (cors && cors.origin) { + if (typeof cors.origin === 'string') { + corsHeaders['Access-Control-Allow-Origin'] = cors.origin; + } else if ( + typeof cors.origin === 'boolean' || + (Array.isArray(cors.origin) && cors.origin.includes(req.headers['Origin'] || req.headers['origin'])) + ) { + corsHeaders['Access-Control-Allow-Origin'] = req.headers['Origin'] || req.headers['origin']; + } + + if (!cors.allowedHeaders) { + corsHeaders['Access-Control-Allow-Headers'] = req.headers['Access-Control-Request-Headers']; + } + } + + if (req.method === 'OPTIONS') { + context.done(null, { + body: '', + status: 204, + headers: corsHeaders + }); + return; + } + + if (this.playgroundOptions && req.method === 'GET') { + const acceptHeader = req.headers['Accept'] || req.headers['accept']; + if (acceptHeader && acceptHeader.includes('text/html')) { + const path = req.originalUrl || '/'; + + const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { + endpoint: path, + ...this.playgroundOptions + }; + const body = renderPlaygroundPage(playgroundRenderPageOptions); + context.done(null, { + body: body, + status: 200, + headers: { + 'Content-Type': 'text/html', + ...corsHeaders + } + }); + return; + } + } + + const callbackFilter = (error?: any, output?: FunctionResponse) => { + context.done( + error, + output && { + ...output, + headers: { + ...output.headers, + ...corsHeaders + } + } + ); + }; + graphqlAzureFunction(async () => { + await promiseWillStart; + return this.createGraphQLServerOptions(req, context); + })(context, req, callbackFilter); + }; + } } diff --git a/packages/apollo-server-azure-function/tsconfig.json b/packages/apollo-server-azure-function/tsconfig.json index b8b81721105..b9d700923fa 100644 --- a/packages/apollo-server-azure-function/tsconfig.json +++ b/packages/apollo-server-azure-function/tsconfig.json @@ -1,9 +1,12 @@ { - "extends": "../../tsconfig", + "extends": "../../tsconfig.base", "compilerOptions": { "rootDir": "./src", "outDir": "./dist" }, "include": ["src/**/*"], - "exclude": ["**/__tests__", "**/__mocks__"] + "exclude": ["**/__tests__", "**/__mocks__"], + "references": [ + { "path": "../apollo-server-core" }, + ] } diff --git a/tsconfig.build.json b/tsconfig.build.json index 39809dba39e..fccce9f4d10 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -10,6 +10,7 @@ { "path": "./packages/apollo-datasource-rest" }, { "path": "./packages/apollo-engine-reporting" }, { "path": "./packages/apollo-server" }, + { "path": "./packages/apollo-server-azure-function" }, { "path": "./packages/apollo-server-cache-memcached" }, { "path": "./packages/apollo-server-cache-redis" }, { "path": "./packages/apollo-server-caching" }, From 1bc682f7d909eee3baf64948cebce9cd6a567e33 Mon Sep 17 00:00:00 2001 From: Michael Watson Date: Tue, 6 Nov 2018 10:45:45 -0800 Subject: [PATCH 07/11] Include azure-function package reference Include reference to apollo-server-function into tsconfig for testing and the root project package.json --- package.json | 259 ++++++++++++++++++++++----------------------- tsconfig.test.json | 1 + 2 files changed, 127 insertions(+), 133 deletions(-) diff --git a/package.json b/package.json index 495718b3ca0..e6d216bf61e 100644 --- a/package.json +++ b/package.json @@ -1,135 +1,128 @@ { - "name": "apollo-server-monorepo", - "private": true, - "license": "MIT", - "repository": "github:apollographql/apollo-server", - "scripts": { - "clean": "git clean -dfqX -- ./node_modules **/{dist,node_modules}/", - "compile": "tsc --build tsconfig.build.json", - "compile:clean": "tsc --build tsconfig.build.json --clean", - "watch": "tsc --build tsconfig.build.json --watch", - "release": "npm run clean && npm ci && lerna publish --exact", - "postinstall": "lerna run prepare && npm run compile", - "lint": "prettier-check '**/*.{js,ts}'", - "lint-fix": "prettier '**/*.{js,ts}' --write", - "test": "jest --verbose", - "test:clean": "jest --clearCache", - "test:watch": "jest --verbose --watchAll", - "testonly": "npm test", - "test:ci": "npm run coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", - "coverage": "npm test -- --coverage", - "coverage:upload": "codecov" - }, - "lint-staged": { - "*.ts": [ - "prettier --write", - "git add" - ], - "*.js": [ - "prettier --write", - "git add" - ] - }, - "engines": { - "node": ">=6" - }, - "dependencies": { - "apollo-cache-control": "file:packages/apollo-cache-control", - "apollo-datasource": "file:packages/apollo-datasource", - "apollo-datasource-rest": "file:packages/apollo-datasource-rest", - "apollo-engine-reporting": "file:packages/apollo-engine-reporting", - "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", - "apollo-server": "file:packages/apollo-server", - "apollo-server-cache-memcached": "file:packages/apollo-server-cache-memcached", - "apollo-server-cache-redis": "file:packages/apollo-server-cache-redis", - "apollo-server-caching": "file:packages/apollo-server-caching", - "apollo-server-cloudflare": "file:packages/apollo-server-cloudflare", - "apollo-server-core": "file:packages/apollo-server-core", - "apollo-server-env": "file:packages/apollo-server-env", - "apollo-server-errors": "file:packages/apollo-server-errors", - "apollo-server-express": "file:packages/apollo-server-express", - "apollo-server-hapi": "file:packages/apollo-server-hapi", - "apollo-server-integration-testsuite": "file:packages/apollo-server-integration-testsuite", - "apollo-server-koa": "file:packages/apollo-server-koa", - "apollo-server-lambda": "file:packages/apollo-server-lambda", - "apollo-server-micro": "file:packages/apollo-server-micro", - "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", - "apollo-server-testing": "file:packages/apollo-server-testing", - "apollo-tracing": "file:packages/apollo-tracing", - "graphql-extensions": "file:packages/graphql-extensions" - }, - "devDependencies": { - "@types/async-retry": "1.2.1", - "@types/aws-lambda": "8.10.15", - "@types/body-parser": "1.17.0", - "@types/connect": "3.4.32", - "@types/fibers": "0.0.30", - "@types/graphql": "14.0.3", - "@types/hapi": "17.6.2", - "@types/jest": "23.3.9", - "@types/json-stable-stringify": "1.0.32", - "@types/koa-multer": "1.0.0", - "@types/koa-router": "7.0.33", - "@types/lodash": "4.14.118", - "@types/lru-cache": "4.1.1", - "@types/memcached": "2.2.5", - "@types/micro": "7.3.3", - "@types/multer": "1.3.7", - "@types/node": "10.12.2", - "@types/node-fetch": "2.1.2", - "@types/redis": "2.8.7", - "@types/request": "2.48.1", - "@types/request-promise": "4.1.42", - "@types/test-listen": "1.1.0", - "@types/type-is": "1.6.2", - "@types/ws": "6.0.1", - "apollo-fetch": "0.7.0", - "apollo-link": "1.2.3", - "apollo-link-http": "1.5.5", - "apollo-link-persisted-queries": "0.2.1", - "body-parser": "1.18.3", - "codecov": "3.1.0", - "connect": "3.6.6", - "express": "4.16.4", - "fibers": "3.0.0", - "form-data": "2.3.3", - "graphql": "14.0.2", - "graphql-subscriptions": "1.0.0", - "graphql-tag": "2.10.0", - "graphql-tools": "4.0.3", - "hapi": "17.7.0", - "husky": "1.1.3", - "jest": "23.6.0", - "jest-junit": "5.2.0", - "jest-matcher-utils": "23.6.0", - "js-sha256": "0.9.0", - "koa": "2.6.1", - "koa-multer": "1.0.2", - "lerna": "3.4.3", - "lint-staged": "7.2.2", - "memcached-mock": "0.1.0", - "meteor-promise": "0.8.6", - "mock-req": "0.2.0", - "multer": "1.4.1", - "node-fetch": "2.2.0", - "prettier": "1.14.3", - "prettier-check": "2.0.0", - "qs-middleware": "1.0.3", - "redis-mock": "0.37.0", - "request": "2.88.0", - "request-promise": "4.2.2", - "subscriptions-transport-ws": "0.9.15", - "supertest": "3.3.0", - "test-listen": "1.1.0", - "ts-jest": "23.10.4", - "tslint": "5.11.0", - "typescript": "next", - "ws": "6.1.0", - "yup": "0.26.5" - }, - "jest": { - "projects": [ - "/packages/*" - ] - } + "name": "apollo-server-monorepo", + "private": true, + "license": "MIT", + "repository": "github:apollographql/apollo-server", + "scripts": { + "clean": "git clean -dfqX -- ./node_modules **/{dist,node_modules}/", + "compile": "tsc --build tsconfig.build.json", + "compile:clean": "tsc --build tsconfig.build.json --clean", + "watch": "tsc --build tsconfig.build.json --watch", + "release": "npm run clean && npm ci && lerna publish --exact", + "postinstall": "lerna run prepare && npm run compile", + "lint": "prettier-check '**/*.{js,ts}'", + "lint-fix": "prettier '**/*.{js,ts}' --write", + "test": "jest --verbose", + "test:clean": "jest --clearCache", + "test:watch": "jest --verbose --watchAll", + "testonly": "npm test", + "test:ci": "npm run coverage -- --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", + "coverage": "npm test -- --coverage", + "coverage:upload": "codecov" + }, + "lint-staged": { + "*.ts": [ "prettier --write", "git add" ], + "*.js": [ "prettier --write", "git add" ] + }, + "engines": { + "node": ">=6" + }, + "dependencies": { + "apollo-cache-control": "file:packages/apollo-cache-control", + "apollo-datasource": "file:packages/apollo-datasource", + "apollo-datasource-rest": "file:packages/apollo-datasource-rest", + "apollo-engine-reporting": "file:packages/apollo-engine-reporting", + "apollo-engine-reporting-protobuf": "file:packages/apollo-engine-reporting-protobuf", + "apollo-server": "file:packages/apollo-server", + "apollo-server-azure-function": "file:packages/apollo-server-azure-function", + "apollo-server-cache-memcached": "file:packages/apollo-server-cache-memcached", + "apollo-server-cache-redis": "file:packages/apollo-server-cache-redis", + "apollo-server-caching": "file:packages/apollo-server-caching", + "apollo-server-cloudflare": "file:packages/apollo-server-cloudflare", + "apollo-server-core": "file:packages/apollo-server-core", + "apollo-server-env": "file:packages/apollo-server-env", + "apollo-server-errors": "file:packages/apollo-server-errors", + "apollo-server-express": "file:packages/apollo-server-express", + "apollo-server-hapi": "file:packages/apollo-server-hapi", + "apollo-server-integration-testsuite": "file:packages/apollo-server-integration-testsuite", + "apollo-server-koa": "file:packages/apollo-server-koa", + "apollo-server-lambda": "file:packages/apollo-server-lambda", + "apollo-server-micro": "file:packages/apollo-server-micro", + "apollo-server-plugin-base": "file:packages/apollo-server-plugin-base", + "apollo-server-testing": "file:packages/apollo-server-testing", + "apollo-tracing": "file:packages/apollo-tracing", + "graphql-extensions": "file:packages/graphql-extensions" + }, + "devDependencies": { + "@types/async-retry": "1.2.1", + "@types/aws-lambda": "8.10.15", + "@types/body-parser": "1.17.0", + "@types/connect": "3.4.32", + "@types/fibers": "0.0.30", + "@types/graphql": "14.0.3", + "@types/hapi": "17.6.2", + "@types/jest": "23.3.9", + "@types/json-stable-stringify": "1.0.32", + "@types/koa-multer": "1.0.0", + "@types/koa-router": "7.0.33", + "@types/lodash": "4.14.118", + "@types/lru-cache": "4.1.1", + "@types/memcached": "2.2.5", + "@types/micro": "7.3.3", + "@types/multer": "1.3.7", + "@types/node": "10.12.2", + "@types/node-fetch": "2.1.2", + "@types/redis": "2.8.7", + "@types/request": "2.48.1", + "@types/request-promise": "4.1.42", + "@types/test-listen": "1.1.0", + "@types/type-is": "1.6.2", + "@types/ws": "6.0.1", + "apollo-fetch": "0.7.0", + "apollo-link": "1.2.3", + "apollo-link-http": "1.5.5", + "apollo-link-persisted-queries": "0.2.1", + "body-parser": "1.18.3", + "codecov": "3.1.0", + "connect": "3.6.6", + "express": "4.16.4", + "fibers": "3.0.0", + "form-data": "2.3.3", + "graphql": "14.0.2", + "graphql-subscriptions": "1.0.0", + "graphql-tag": "2.10.0", + "graphql-tools": "4.0.3", + "hapi": "17.7.0", + "husky": "1.1.3", + "jest": "23.6.0", + "jest-junit": "5.2.0", + "jest-matcher-utils": "23.6.0", + "js-sha256": "0.9.0", + "koa": "2.6.1", + "koa-multer": "1.0.2", + "lerna": "3.4.3", + "lint-staged": "7.2.2", + "memcached-mock": "0.1.0", + "meteor-promise": "0.8.6", + "mock-req": "0.2.0", + "multer": "1.4.1", + "node-fetch": "2.2.0", + "prettier": "1.14.3", + "prettier-check": "2.0.0", + "qs-middleware": "1.0.3", + "redis-mock": "0.37.0", + "request": "2.88.0", + "request-promise": "4.2.2", + "subscriptions-transport-ws": "0.9.15", + "supertest": "3.3.0", + "test-listen": "1.1.0", + "ts-jest": "23.10.4", + "tslint": "5.11.0", + "typescript": "next", + "ws": "6.1.0", + "yup": "0.26.5" + }, + "jest": { + "projects": [ "/packages/*" ] + } } diff --git a/tsconfig.test.json b/tsconfig.test.json index c740e3dbf68..2270d895d6d 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -9,6 +9,7 @@ { "path": "./packages/apollo-datasource-rest/src/__tests__/" }, { "path": "./packages/apollo-engine-reporting/src/__tests__/" }, { "path": "./packages/apollo-server/src/__tests__/" }, + { "path": "./packages/apollo-server-azure-function/src/__tests__/" }, { "path": "./packages/apollo-server-cache-memcached/src/__tests__/" }, { "path": "./packages/apollo-server-cache-redis/src/__tests__/" }, { "path": "./packages/apollo-server-caching/src/__tests__/" }, From 7910c971c04f259edee9eab7ea40954fb9bab9d6 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 6 Nov 2018 11:14:08 -0800 Subject: [PATCH 08/11] Update package-lock.json for `apollo-server-azure-functions`. --- package-lock.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/package-lock.json b/package-lock.json index bdcfa7f7e24..806b68a22b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2090,6 +2090,15 @@ "graphql-tools": "^4.0.0" } }, + "apollo-server-azure-function": { + "version": "file:packages/apollo-server-azure-function", + "requires": { + "@apollographql/graphql-playground-html": "^1.6.4", + "apollo-server-core": "file:packages/apollo-server-core", + "apollo-server-env": "file:packages/apollo-server-env", + "graphql-tools": "^4.0.0" + } + }, "apollo-server-cache-memcached": { "version": "file:packages/apollo-server-cache-memcached", "requires": { From 043b5ee3879fb737e9551ee73f95be1df51a1a49 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 6 Nov 2018 11:20:04 -0800 Subject: [PATCH 09/11] Prettier fixes. --- .../src/ApolloServer.ts | 290 +++++++++--------- 1 file changed, 153 insertions(+), 137 deletions(-) diff --git a/packages/apollo-server-azure-function/src/ApolloServer.ts b/packages/apollo-server-azure-function/src/ApolloServer.ts index 9560c9942e2..0a257b68c17 100644 --- a/packages/apollo-server-azure-function/src/ApolloServer.ts +++ b/packages/apollo-server-azure-function/src/ApolloServer.ts @@ -1,149 +1,165 @@ -import { HttpContext, FunctionRequest, FunctionResponse } from './azureFunctions'; +import { + HttpContext, + FunctionRequest, + FunctionResponse, +} from './azureFunctions'; import { ApolloServerBase } from 'apollo-server-core'; import { GraphQLOptions, Config } from 'apollo-server-core'; import { - renderPlaygroundPage, - RenderPageOptions as PlaygroundRenderPageOptions + renderPlaygroundPage, + RenderPageOptions as PlaygroundRenderPageOptions, } from '@apollographql/graphql-playground-html'; import { graphqlAzureFunction } from './azureFunctionApollo'; export interface CreateHandlerOptions { - cors?: { - origin?: boolean | string | string[]; - methods?: string | string[]; - allowedHeaders?: string | string[]; - exposedHeaders?: string | string[]; - credentials?: boolean; - maxAge?: number; - }; + cors?: { + origin?: boolean | string | string[]; + methods?: string | string[]; + allowedHeaders?: string | string[]; + exposedHeaders?: string | string[]; + credentials?: boolean; + maxAge?: number; + }; } export class ApolloServer extends ApolloServerBase { - // If you feel tempted to add an option to this constructor. Please consider - // another place, since the documentation becomes much more complicated when - // the constructor is not longer shared between all integration - constructor(options: Config) { - if (process.env.ENGINE_API_KEY || options.engine) { - options.engine = { - sendReportsImmediately: true, - ...typeof options.engine !== 'boolean' ? options.engine : {} - }; - } - super(options); - } - - // This translates the arguments from the middleware into graphQL options It - // provides typings for the integration specific behavior, ideally this would - // be propagated with a generic to the super class - createGraphQLServerOptions(request: FunctionRequest, context: HttpContext): Promise { - return super.graphQLServerOptions({ request, context }); - } - - public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { - // We will kick off the `willStart` event once for the server, and then - // await it before processing any requests by incorporating its `await` into - // the GraphQLServerOptions function which is called before each request. - const promiseWillStart = this.willStart(); - - const corsHeaders: FunctionResponse['headers'] = {}; - - if (cors) { - if (cors.methods) { - if (typeof cors.methods === 'string') { - corsHeaders['Access-Control-Allow-Methods'] = cors.methods; - } else if (Array.isArray(cors.methods)) { - corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(','); - } - } - - if (cors.allowedHeaders) { - if (typeof cors.allowedHeaders === 'string') { - corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders; - } else if (Array.isArray(cors.allowedHeaders)) { - corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders.join(','); - } - } - - if (cors.exposedHeaders) { - if (typeof cors.exposedHeaders === 'string') { - corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders; - } else if (Array.isArray(cors.exposedHeaders)) { - corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders.join(','); - } - } - - if (cors.credentials) { - corsHeaders['Access-Control-Allow-Credentials'] = 'true'; - } - if (cors.maxAge) { - corsHeaders['Access-Control-Max-Age'] = cors.maxAge; - } - } - - return (context: HttpContext, req: FunctionRequest) => { - if (cors && cors.origin) { - if (typeof cors.origin === 'string') { - corsHeaders['Access-Control-Allow-Origin'] = cors.origin; - } else if ( - typeof cors.origin === 'boolean' || - (Array.isArray(cors.origin) && cors.origin.includes(req.headers['Origin'] || req.headers['origin'])) - ) { - corsHeaders['Access-Control-Allow-Origin'] = req.headers['Origin'] || req.headers['origin']; - } - - if (!cors.allowedHeaders) { - corsHeaders['Access-Control-Allow-Headers'] = req.headers['Access-Control-Request-Headers']; - } - } - - if (req.method === 'OPTIONS') { - context.done(null, { - body: '', - status: 204, - headers: corsHeaders - }); - return; - } - - if (this.playgroundOptions && req.method === 'GET') { - const acceptHeader = req.headers['Accept'] || req.headers['accept']; - if (acceptHeader && acceptHeader.includes('text/html')) { - const path = req.originalUrl || '/'; - - const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { - endpoint: path, - ...this.playgroundOptions - }; - const body = renderPlaygroundPage(playgroundRenderPageOptions); - context.done(null, { - body: body, - status: 200, - headers: { - 'Content-Type': 'text/html', - ...corsHeaders - } - }); - return; - } - } - - const callbackFilter = (error?: any, output?: FunctionResponse) => { - context.done( - error, - output && { - ...output, - headers: { - ...output.headers, - ...corsHeaders - } - } - ); - }; - graphqlAzureFunction(async () => { - await promiseWillStart; - return this.createGraphQLServerOptions(req, context); - })(context, req, callbackFilter); - }; - } + // If you feel tempted to add an option to this constructor. Please consider + // another place, since the documentation becomes much more complicated when + // the constructor is not longer shared between all integration + constructor(options: Config) { + if (process.env.ENGINE_API_KEY || options.engine) { + options.engine = { + sendReportsImmediately: true, + ...(typeof options.engine !== 'boolean' ? options.engine : {}), + }; + } + super(options); + } + + // This translates the arguments from the middleware into graphQL options It + // provides typings for the integration specific behavior, ideally this would + // be propagated with a generic to the super class + createGraphQLServerOptions( + request: FunctionRequest, + context: HttpContext, + ): Promise { + return super.graphQLServerOptions({ request, context }); + } + + public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { + // We will kick off the `willStart` event once for the server, and then + // await it before processing any requests by incorporating its `await` into + // the GraphQLServerOptions function which is called before each request. + const promiseWillStart = this.willStart(); + + const corsHeaders: FunctionResponse['headers'] = {}; + + if (cors) { + if (cors.methods) { + if (typeof cors.methods === 'string') { + corsHeaders['Access-Control-Allow-Methods'] = cors.methods; + } else if (Array.isArray(cors.methods)) { + corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(','); + } + } + + if (cors.allowedHeaders) { + if (typeof cors.allowedHeaders === 'string') { + corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders; + } else if (Array.isArray(cors.allowedHeaders)) { + corsHeaders[ + 'Access-Control-Allow-Headers' + ] = cors.allowedHeaders.join(','); + } + } + + if (cors.exposedHeaders) { + if (typeof cors.exposedHeaders === 'string') { + corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders; + } else if (Array.isArray(cors.exposedHeaders)) { + corsHeaders[ + 'Access-Control-Expose-Headers' + ] = cors.exposedHeaders.join(','); + } + } + + if (cors.credentials) { + corsHeaders['Access-Control-Allow-Credentials'] = 'true'; + } + if (cors.maxAge) { + corsHeaders['Access-Control-Max-Age'] = cors.maxAge; + } + } + + return (context: HttpContext, req: FunctionRequest) => { + if (cors && cors.origin) { + if (typeof cors.origin === 'string') { + corsHeaders['Access-Control-Allow-Origin'] = cors.origin; + } else if ( + typeof cors.origin === 'boolean' || + (Array.isArray(cors.origin) && + cors.origin.includes( + req.headers['Origin'] || req.headers['origin'], + )) + ) { + corsHeaders['Access-Control-Allow-Origin'] = + req.headers['Origin'] || req.headers['origin']; + } + + if (!cors.allowedHeaders) { + corsHeaders['Access-Control-Allow-Headers'] = + req.headers['Access-Control-Request-Headers']; + } + } + + if (req.method === 'OPTIONS') { + context.done(null, { + body: '', + status: 204, + headers: corsHeaders, + }); + return; + } + + if (this.playgroundOptions && req.method === 'GET') { + const acceptHeader = req.headers['Accept'] || req.headers['accept']; + if (acceptHeader && acceptHeader.includes('text/html')) { + const path = req.originalUrl || '/'; + + const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { + endpoint: path, + ...this.playgroundOptions, + }; + const body = renderPlaygroundPage(playgroundRenderPageOptions); + context.done(null, { + body: body, + status: 200, + headers: { + 'Content-Type': 'text/html', + ...corsHeaders, + }, + }); + return; + } + } + + const callbackFilter = (error?: any, output?: FunctionResponse) => { + context.done( + error, + output && { + ...output, + headers: { + ...output.headers, + ...corsHeaders, + }, + }, + ); + }; + graphqlAzureFunction(async () => { + await promiseWillStart; + return this.createGraphQLServerOptions(req, context); + })(context, req, callbackFilter); + }; + } } From 10cc0f8fd7d75386c94da9fc881704ef277cc772 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 6 Nov 2018 11:22:16 -0800 Subject: [PATCH 10/11] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa31a3137a0..0bb0c701bbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Update `graphql-playground-html` to 1.7.8 - Allow an optional function to resolve the `rootValue`, passing the `DocumentNode` AST to determine the value. [PR #1555](https://github.com/apollographql/apollo-server/pull/1555) - Follow-up on the work in [PR #1516](https://github.com/apollographql/apollo-server/pull/1516) to also fix missing insertion cursor/caret when a custom GraphQL configuration is specified which doesn't specify its own `cursorShape` property. [PR #1607](https://github.com/apollographql/apollo-server/pull/1607) -- Azure functions support [#1752] [PR #1753] +- Azure functions support [Issue #1752](https://github.com/apollographql/apollo-server/issue/1752) [PR #1753](https://github.com/apollographql/apollo-server/pull/1753) - Allow JSON parsing in `RESTDataSource` of Content Type `application/hal+json`. [PR ##185](https://github.com/apollographql/apollo-server/pull/1853) - Add support for a `requestAgent` configuration parameter within the `engine` configuration. This can be utilized when a proxy is necessary to transmit tracing and metrics data to Apollo Engine. It accepts either an [`http.Agent`](https://nodejs.org/docs/latest-v8.x/api/http.html#http_class_http_agent) or [`https.Agent`](https://nodejs.org/docs/latest-v8.x/api/https.html#https_class_https_agent) and behaves the same as the `agent` parameter to Node.js' [`http.request`](https://nodejs.org/docs/latest-v8.x/api/http.html#http_http_request_options_callback). [PR #1879](https://github.com/apollographql/apollo-server/pull/1879) From 94cb2448e7f5de3a1a19927a2942b280112ed3e8 Mon Sep 17 00:00:00 2001 From: Jesse Rosenberger Date: Tue, 6 Nov 2018 11:24:12 -0800 Subject: [PATCH 11/11] Update README.md --- packages/apollo-server-azure-function/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apollo-server-azure-function/README.md b/packages/apollo-server-azure-function/README.md index 12e5905d2ad..89728cafcf3 100644 --- a/packages/apollo-server-azure-function/README.md +++ b/packages/apollo-server-azure-function/README.md @@ -6,7 +6,7 @@ description: Setting up Apollo Server with Azure Functions This is the Azure functions integration of GraphQL Server. Apollo Server is a community-maintained open-source GraphQL server that works with many Node.js HTTP server frameworks. [Read the docs](https://www.apollographql.com/docs/apollo-server/v2). [Read the CHANGELOG](https://github.com/apollographql/apollo-server/blob/master/CHANGELOG.md). ```sh -npm install apollo-server-azure-functions@rc graphql +npm install apollo-server-azure-functions@alpha graphql ``` ## Writing azure function