-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementation of Apollo Server 2 for Google Cloud Functions (#1446)
* Initial implementation of Apollo Server 2 for gcf * First try at running with tests * Updated naming * Removed lambda mentions * Simply use referer * Updated README * Updated Changelog * Renamed gqlApollo to googleCloudApollo * Added more details * Removed extra check
- Loading branch information
Showing
9 changed files
with
480 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
* | ||
!src/**/* | ||
!dist/**/* | ||
dist/**/*.test.* | ||
!package.json | ||
!README.md |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
--- | ||
title: Google Cloud Functions | ||
description: Setting up Apollo Server with Google Cloud Functions | ||
--- | ||
|
||
[![npm version](https://badge.fury.io/js/apollo-server-cloud-function.svg)](https://badge.fury.io/js/apollo-server-cloud-function) [![Build Status](https://circleci.com/gh/apollographql/apollo-server.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-server) [![Coverage Status](https://coveralls.io/repos/github/apollographql/apollo-server/badge.svg?branch=master)](https://coveralls.io/github/apollographql/apollo-server?branch=master) [![Get on Slack](https://img.shields.io/badge/slack-join-orange.svg)](https://www.apollographql.com/#slack) | ||
|
||
This is the Google Cloud Function 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-cloud-function@rc graphql | ||
``` | ||
|
||
## Deploying with Google Cloud Function | ||
|
||
#### 1. Write the API handlers | ||
|
||
First, create a `package.json` file and include `apollo-server-cloud-function` in your dependencies. Then in a file named `index.js`, place the following code: | ||
|
||
```js | ||
const { ApolloServer, gql } = require('apollo-server-cloud-function'); | ||
|
||
// 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, | ||
playground: true, | ||
introspection: true, | ||
}); | ||
|
||
exports.handler = server.createHandler(); | ||
``` | ||
|
||
#### 2. Configure your Cloud Function and deploy | ||
|
||
On the Create Function page, set _Trigger_ to `HTTP` and _Function to execute_ to the name of your exported handler, in this case `handler`. | ||
|
||
Since NODE_ENV is a reserved environment variable in GCF and it defaults to "production", both the **playground** and **introspection** | ||
options need to be explicitly set to `true` for the GraphQL Playground to work correctly. | ||
|
||
After configuring your Function you can press **Create** and an http endpoint will be created a few seconds later. | ||
|
||
You can refer to the [Cloud Functions documentation](https://cloud.google.com/functions/docs/quickstart-console) for more details | ||
|
||
## Getting request info | ||
|
||
To read information about the currently executing Google Cloud Function (HTTP headers, HTTP method, body, path, ...) use the context option. This way you can pass any request specific data to your schema resolvers. | ||
|
||
```js | ||
const { ApolloServer, gql } = require('apollo-server-cloud-function'); | ||
|
||
// 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: ({ req, res }) => ({ | ||
headers: req.headers, | ||
req, | ||
res, | ||
}), | ||
}); | ||
|
||
exports.handler = server.createHandler(); | ||
``` | ||
|
||
## Modifying the GCF 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-cloud-function'); | ||
|
||
// 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-cloud-function'); | ||
|
||
// 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! |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"name": "apollo-server-cloud-functions", | ||
"version": "2.0.0", | ||
"description": "Production-ready Node.js GraphQL server for Google Cloud Functions", | ||
"keywords": [ | ||
"GraphQL", | ||
"Apollo", | ||
"Server", | ||
"Google Cloud Functions", | ||
"Javascript" | ||
], | ||
"author": "[email protected]", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-cloud-functions" | ||
}, | ||
"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", | ||
"prepublish": "npm run clean && npm run compile" | ||
}, | ||
"dependencies": { | ||
"@apollographql/graphql-playground-html": "^1.6.0", | ||
"apollo-server-core": "2.0.0", | ||
"apollo-server-env": "2.0.0", | ||
"graphql-tools": "^3.0.4" | ||
}, | ||
"devDependencies": { | ||
"apollo-server-integration-testsuite": "2.0.0" | ||
}, | ||
"peerDependencies": { | ||
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0" | ||
} | ||
} |
129 changes: 129 additions & 0 deletions
129
packages/apollo-server-cloud-function/src/ApolloServer.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { ApolloServerBase } from 'apollo-server-core'; | ||
import { GraphQLOptions, Config } from 'apollo-server-core'; | ||
import { | ||
renderPlaygroundPage, | ||
RenderPageOptions as PlaygroundRenderPageOptions, | ||
} from '@apollographql/graphql-playground-html'; | ||
|
||
import { graphqlCloudFunction } from './googleCloudApollo'; | ||
|
||
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(req, res): Promise<GraphQLOptions> { | ||
return super.graphQLServerOptions({ req, res }); | ||
} | ||
|
||
public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { | ||
const corsHeaders = {}; | ||
|
||
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 (req: any, res: any) => { | ||
if (cors) { | ||
if (typeof cors.origin === 'string') { | ||
res.set('Access-Control-Allow-Origin', cors.origin); | ||
} else if ( | ||
typeof cors.origin === 'boolean' || | ||
(Array.isArray(cors.origin) && | ||
cors.origin.includes(req.get('origin'))) | ||
) { | ||
res.set('Access-Control-Allow-Origin', req.get('origin')); | ||
} | ||
|
||
if (!cors.allowedHeaders) { | ||
res.set( | ||
'Access-Control-Allow-Headers', | ||
req.get('Access-Control-Request-Headers'), | ||
); | ||
} | ||
} | ||
|
||
if (req.method === 'OPTIONS') { | ||
res.status(204).send(''); | ||
return; | ||
} | ||
|
||
if (this.playgroundOptions && req.method === 'GET') { | ||
if (req.accepts('text/html')) { | ||
const playgroundRenderPageOptions: PlaygroundRenderPageOptions = { | ||
endpoint: req.get('referer'), | ||
...this.playgroundOptions, | ||
}; | ||
|
||
res | ||
.status(200) | ||
.send(renderPlaygroundPage(playgroundRenderPageOptions)); | ||
return; | ||
} | ||
} | ||
|
||
res.set(corsHeaders); | ||
|
||
graphqlCloudFunction(this.createGraphQLServerOptions.bind(this))( | ||
req, | ||
res, | ||
); | ||
}; | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
packages/apollo-server-cloud-function/src/googleCloudApollo.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { ApolloServer } from './ApolloServer'; | ||
import testSuite, { | ||
schema as Schema, | ||
CreateAppOptions, | ||
} from 'apollo-server-integration-testsuite'; | ||
import { Config } from 'apollo-server-core'; | ||
import 'mocha'; | ||
import { IncomingMessage, ServerResponse } from 'http'; | ||
|
||
const createCloudFunction = (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, lambda doesn't have paths | ||
if (req.url.includes('/bogus-route')) { | ||
res.statusCode = 404; | ||
return res.end(); | ||
} | ||
|
||
return handler(req, res); | ||
}; | ||
}; | ||
|
||
describe('integration:CloudFunction', () => { | ||
testSuite(createCloudFunction); | ||
}); |
Oops, something went wrong.