Skip to content

Commit 724d9ff

Browse files
reaktivohwillson
authored andcommitted
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
1 parent ee7202e commit 724d9ff

9 files changed

Lines changed: 480 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ All of the packages in the `apollo-server` repo are released with the same versi
44

55
### vNEXT
66

7+
- Google Cloud Function support [#1402](https://github.com/apollographql/apollo-server/issues/1402)
8+
79
### v2.0.4
810

911
- apollo-server: Release due to failed build and install
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*
2+
!src/**/*
3+
!dist/**/*
4+
dist/**/*.test.*
5+
!package.json
6+
!README.md
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
---
2+
title: Google Cloud Functions
3+
description: Setting up Apollo Server with Google Cloud Functions
4+
---
5+
6+
[![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)
7+
8+
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).
9+
10+
```sh
11+
npm install apollo-server-cloud-function@rc graphql
12+
```
13+
14+
## Deploying with Google Cloud Function
15+
16+
#### 1. Write the API handlers
17+
18+
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:
19+
20+
```js
21+
const { ApolloServer, gql } = require('apollo-server-cloud-function');
22+
23+
// Construct a schema, using GraphQL schema language
24+
const typeDefs = gql`
25+
type Query {
26+
hello: String
27+
}
28+
`;
29+
30+
// Provide resolver functions for your schema fields
31+
const resolvers = {
32+
Query: {
33+
hello: () => 'Hello world!',
34+
},
35+
};
36+
37+
const server = new ApolloServer({
38+
typeDefs,
39+
resolvers,
40+
playground: true,
41+
introspection: true,
42+
});
43+
44+
exports.handler = server.createHandler();
45+
```
46+
47+
#### 2. Configure your Cloud Function and deploy
48+
49+
On the Create Function page, set _Trigger_ to `HTTP` and _Function to execute_ to the name of your exported handler, in this case `handler`.
50+
51+
Since NODE_ENV is a reserved environment variable in GCF and it defaults to "production", both the **playground** and **introspection**
52+
options need to be explicitly set to `true` for the GraphQL Playground to work correctly.
53+
54+
After configuring your Function you can press **Create** and an http endpoint will be created a few seconds later.
55+
56+
You can refer to the [Cloud Functions documentation](https://cloud.google.com/functions/docs/quickstart-console) for more details
57+
58+
## Getting request info
59+
60+
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.
61+
62+
```js
63+
const { ApolloServer, gql } = require('apollo-server-cloud-function');
64+
65+
// Construct a schema, using GraphQL schema language
66+
const typeDefs = gql`
67+
type Query {
68+
hello: String
69+
}
70+
`;
71+
72+
// Provide resolver functions for your schema fields
73+
const resolvers = {
74+
Query: {
75+
hello: () => 'Hello world!',
76+
},
77+
};
78+
79+
const server = new ApolloServer({
80+
typeDefs,
81+
resolvers,
82+
context: ({ req, res }) => ({
83+
headers: req.headers,
84+
req,
85+
res,
86+
}),
87+
});
88+
89+
exports.handler = server.createHandler();
90+
```
91+
92+
## Modifying the GCF Response (Enable CORS)
93+
94+
To enable CORS the response HTTP headers need to be modified. To accomplish this use the `cors` option.
95+
96+
```js
97+
const { ApolloServer, gql } = require('apollo-server-cloud-function');
98+
99+
// Construct a schema, using GraphQL schema language
100+
const typeDefs = gql`
101+
type Query {
102+
hello: String
103+
}
104+
`;
105+
106+
// Provide resolver functions for your schema fields
107+
const resolvers = {
108+
Query: {
109+
hello: () => 'Hello world!',
110+
},
111+
};
112+
113+
const server = new ApolloServer({
114+
typeDefs,
115+
resolvers,
116+
});
117+
118+
exports.handler = server.createHandler({
119+
cors: {
120+
origin: '*',
121+
credentials: true,
122+
},
123+
});
124+
```
125+
126+
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.
127+
128+
```js
129+
const { ApolloServer, gql } = require('apollo-server-cloud-function');
130+
131+
// Construct a schema, using GraphQL schema language
132+
const typeDefs = gql`
133+
type Query {
134+
hello: String
135+
}
136+
`;
137+
138+
// Provide resolver functions for your schema fields
139+
const resolvers = {
140+
Query: {
141+
hello: () => 'Hello world!',
142+
},
143+
};
144+
145+
const server = new ApolloServer({
146+
typeDefs,
147+
resolvers,
148+
});
149+
150+
exports.handler = server.createHandler({
151+
cors: {
152+
origin: true,
153+
credentials: true,
154+
},
155+
});
156+
```
157+
158+
### Cors Options
159+
160+
The options correspond to the [express cors configuration](https://github.com/expressjs/cors#configuration-options) with the following fields(all are optional):
161+
162+
- `origin`: boolean | string | string[]
163+
- `methods`: string | string[]
164+
- `allowedHeaders`: string | string[]
165+
- `exposedHeaders`: string | string[]
166+
- `credentials`: boolean
167+
- `maxAge`: number
168+
169+
## Principles
170+
171+
GraphQL Server is built with the following principles in mind:
172+
173+
- **By the community, for the community**: GraphQL Server's development is driven by the needs of developers
174+
- **Simplicity**: by keeping things simple, GraphQL Server is easier to use, easier to contribute to, and more secure
175+
- **Performance**: GraphQL Server is well-tested and production-ready - no modifications needed
176+
177+
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!
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "apollo-server-cloud-functions",
3+
"version": "2.0.0",
4+
"description": "Production-ready Node.js GraphQL server for Google Cloud Functions",
5+
"keywords": [
6+
"GraphQL",
7+
"Apollo",
8+
"Server",
9+
"Google Cloud Functions",
10+
"Javascript"
11+
],
12+
"author": "opensource@apollographql.com",
13+
"license": "MIT",
14+
"repository": {
15+
"type": "git",
16+
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-cloud-functions"
17+
},
18+
"homepage": "https://github.com/apollographql/apollo-server#readme",
19+
"bugs": {
20+
"url": "https://github.com/apollographql/apollo-server/issues"
21+
},
22+
"main": "dist/index.js",
23+
"types": "dist/index.d.ts",
24+
"engines": {
25+
"node": ">=6"
26+
},
27+
"scripts": {
28+
"clean": "rm -rf dist",
29+
"compile": "tsc",
30+
"prepublish": "npm run clean && npm run compile"
31+
},
32+
"dependencies": {
33+
"@apollographql/graphql-playground-html": "^1.6.0",
34+
"apollo-server-core": "2.0.0",
35+
"apollo-server-env": "2.0.0",
36+
"graphql-tools": "^3.0.4"
37+
},
38+
"devDependencies": {
39+
"apollo-server-integration-testsuite": "2.0.0"
40+
},
41+
"peerDependencies": {
42+
"graphql": "^0.12.0 || ^0.13.0 || ^14.0.0"
43+
}
44+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { ApolloServerBase } from 'apollo-server-core';
2+
import { GraphQLOptions, Config } from 'apollo-server-core';
3+
import {
4+
renderPlaygroundPage,
5+
RenderPageOptions as PlaygroundRenderPageOptions,
6+
} from '@apollographql/graphql-playground-html';
7+
8+
import { graphqlCloudFunction } from './googleCloudApollo';
9+
10+
export interface CreateHandlerOptions {
11+
cors?: {
12+
origin?: boolean | string | string[];
13+
methods?: string | string[];
14+
allowedHeaders?: string | string[];
15+
exposedHeaders?: string | string[];
16+
credentials?: boolean;
17+
maxAge?: number;
18+
};
19+
}
20+
21+
export class ApolloServer extends ApolloServerBase {
22+
// If you feel tempted to add an option to this constructor. Please consider
23+
// another place, since the documentation becomes much more complicated when
24+
// the constructor is not longer shared between all integration
25+
constructor(options: Config) {
26+
if (process.env.ENGINE_API_KEY || options.engine) {
27+
options.engine = {
28+
sendReportsImmediately: true,
29+
...(typeof options.engine !== 'boolean' ? options.engine : {}),
30+
};
31+
}
32+
super(options);
33+
}
34+
35+
// This translates the arguments from the middleware into graphQL options It
36+
// provides typings for the integration specific behavior, ideally this would
37+
// be propagated with a generic to the super class
38+
createGraphQLServerOptions(req, res): Promise<GraphQLOptions> {
39+
return super.graphQLServerOptions({ req, res });
40+
}
41+
42+
public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) {
43+
const corsHeaders = {};
44+
45+
if (cors) {
46+
if (cors.methods) {
47+
if (typeof cors.methods === 'string') {
48+
corsHeaders['Access-Control-Allow-Methods'] = cors.methods;
49+
} else if (Array.isArray(cors.methods)) {
50+
corsHeaders['Access-Control-Allow-Methods'] = cors.methods.join(',');
51+
}
52+
}
53+
54+
if (cors.allowedHeaders) {
55+
if (typeof cors.allowedHeaders === 'string') {
56+
corsHeaders['Access-Control-Allow-Headers'] = cors.allowedHeaders;
57+
} else if (Array.isArray(cors.allowedHeaders)) {
58+
corsHeaders[
59+
'Access-Control-Allow-Headers'
60+
] = cors.allowedHeaders.join(',');
61+
}
62+
}
63+
64+
if (cors.exposedHeaders) {
65+
if (typeof cors.exposedHeaders === 'string') {
66+
corsHeaders['Access-Control-Expose-Headers'] = cors.exposedHeaders;
67+
} else if (Array.isArray(cors.exposedHeaders)) {
68+
corsHeaders[
69+
'Access-Control-Expose-Headers'
70+
] = cors.exposedHeaders.join(',');
71+
}
72+
}
73+
74+
if (cors.credentials) {
75+
corsHeaders['Access-Control-Allow-Credentials'] = 'true';
76+
}
77+
if (cors.maxAge) {
78+
corsHeaders['Access-Control-Max-Age'] = cors.maxAge;
79+
}
80+
}
81+
82+
return (req: any, res: any) => {
83+
if (cors) {
84+
if (typeof cors.origin === 'string') {
85+
res.set('Access-Control-Allow-Origin', cors.origin);
86+
} else if (
87+
typeof cors.origin === 'boolean' ||
88+
(Array.isArray(cors.origin) &&
89+
cors.origin.includes(req.get('origin')))
90+
) {
91+
res.set('Access-Control-Allow-Origin', req.get('origin'));
92+
}
93+
94+
if (!cors.allowedHeaders) {
95+
res.set(
96+
'Access-Control-Allow-Headers',
97+
req.get('Access-Control-Request-Headers'),
98+
);
99+
}
100+
}
101+
102+
if (req.method === 'OPTIONS') {
103+
res.status(204).send('');
104+
return;
105+
}
106+
107+
if (this.playgroundOptions && req.method === 'GET') {
108+
if (req.accepts('text/html')) {
109+
const playgroundRenderPageOptions: PlaygroundRenderPageOptions = {
110+
endpoint: req.get('referer'),
111+
...this.playgroundOptions,
112+
};
113+
114+
res
115+
.status(200)
116+
.send(renderPlaygroundPage(playgroundRenderPageOptions));
117+
return;
118+
}
119+
}
120+
121+
res.set(corsHeaders);
122+
123+
graphqlCloudFunction(this.createGraphQLServerOptions.bind(this))(
124+
req,
125+
res,
126+
);
127+
};
128+
}
129+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ApolloServer } from './ApolloServer';
2+
import testSuite, {
3+
schema as Schema,
4+
CreateAppOptions,
5+
} from 'apollo-server-integration-testsuite';
6+
import { Config } from 'apollo-server-core';
7+
import 'mocha';
8+
import { IncomingMessage, ServerResponse } from 'http';
9+
10+
const createCloudFunction = (options: CreateAppOptions = {}) => {
11+
const server = new ApolloServer(
12+
(options.graphqlOptions as Config) || { schema: Schema },
13+
);
14+
15+
const handler = server.createHandler();
16+
17+
return (req: IncomingMessage, res: ServerResponse) => {
18+
// return 404 if path is /bogus-route to pass the test, lambda doesn't have paths
19+
if (req.url.includes('/bogus-route')) {
20+
res.statusCode = 404;
21+
return res.end();
22+
}
23+
24+
return handler(req, res);
25+
};
26+
};
27+
28+
describe('integration:CloudFunction', () => {
29+
testSuite(createCloudFunction);
30+
});

0 commit comments

Comments
 (0)