Skip to content

Commit

Permalink
Make apollo-server-cloud-functions wrap apollo-server-express (#5293)
Browse files Browse the repository at this point in the history
You know how we recently changed apollo-server-lambda to be a thin
wrapper around apollo-server-express? That was a bit fiddly because we
needed to pull in a third-party package to translate Lambda objects into
Express objects.

Turns out we can do the same thing for Google Cloud Functions... except
even easier, because GCF gives us Express objects already!

Note that this PR has not been tested in a real GCF environment.

Fixes #5292.
  • Loading branch information
glasser authored Jun 9, 2021
1 parent e0c81c8 commit 45e69e0
Show file tree
Hide file tree
Showing 6 changed files with 49 additions and 197 deletions.
10 changes: 4 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions packages/apollo-server-cloud-functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@
"node": ">=12.0"
},
"dependencies": {
"apollo-server-core": "file:../apollo-server-core",
"apollo-server-env": "file:../apollo-server-env",
"apollo-server-types": "file:../apollo-server-types"
"apollo-server-express": "file:../apollo-server-express",
"express": "^4.17.1"
},
"devDependencies": {
"apollo-server-integration-testsuite": "file:../apollo-server-integration-testsuite"
Expand Down
150 changes: 32 additions & 118 deletions packages/apollo-server-cloud-functions/src/ApolloServer.ts
Original file line number Diff line number Diff line change
@@ -1,130 +1,44 @@
import { ApolloServerBase, GraphQLOptions } from 'apollo-server-core';
import { LandingPage } from 'apollo-server-plugin-base';
import { Request, Response } from 'express';

import { graphqlCloudFunction } from './googleCloudApollo';
import {
ApolloServer as ApolloServerExpress,
GetMiddlewareOptions,
} from 'apollo-server-express';
import express from 'express';

export interface CreateHandlerOptions {
cors?: {
origin?: boolean | string | string[];
methods?: string | string[];
allowedHeaders?: string | string[];
exposedHeaders?: string | string[];
credentials?: boolean;
maxAge?: number;
};
expressAppFromMiddleware?: (
middleware: express.RequestHandler,
) => express.Application;
expressGetMiddlewareOptions?: GetMiddlewareOptions;
}

function defaultExpressAppFromMiddleware(
middleware: express.RequestHandler,
): express.Handler {
const app = express();
app.use(middleware);
return app;
}

export class ApolloServer extends ApolloServerBase {
export class ApolloServer extends ApolloServerExpress {
protected serverlessFramework(): boolean {
return true;
}

// 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: Request,
res: Response,
): Promise<GraphQLOptions> {
return super.graphQLServerOptions({ req, res });
}

public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) {
const corsHeaders = {} as Record<string, any>;

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;
public createHandler(
options?: CreateHandlerOptions,
): express.Handler {
let realHandler: express.Handler;
return async (req, ...args) => {
await this.ensureStarted();
if (!realHandler) {
const middleware = this.getMiddleware(
options?.expressGetMiddlewareOptions,
);
realHandler = (
options?.expressAppFromMiddleware ?? defaultExpressAppFromMiddleware
)(middleware);
}
}

// undefined before load, null if loaded but there is none.
let landingPage: LandingPage | null | undefined;

return (req: Request, res: Response) => {
this.ensureStarted().then(() => {
if (landingPage === undefined) {
landingPage = this.getLandingPage();
}

// Handle both the root of the GCF endpoint and /graphql
// With bare endpoints, GCF sets request params' path to null.
// The check for '' is included in case that behaviour changes
if (req.path && !['', '/', '/graphql'].includes(req.path)) {
res.status(404).end();
return;
}

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'),
);
}
}

res.set(corsHeaders);

if (req.method === 'OPTIONS') {
res.status(204).send('');
return;
}

if (landingPage && req.method === 'GET') {
const acceptHeader = req.headers['accept'] as string;
if (acceptHeader && acceptHeader.includes('text/html')) {
res.status(200).send(landingPage.html);
return;
}
}

graphqlCloudFunction(async () => {
return this.createGraphQLServerOptions(req, res);
})(req, res);
});
return realHandler(req, ...args);
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApolloServer } from '../ApolloServer';
import { ApolloServer, CreateHandlerOptions } from '../ApolloServer';
import testSuite, {
schema as Schema,
CreateAppOptions,
Expand Down Expand Up @@ -26,10 +26,13 @@ const simulateGcfMiddleware = (
next();
};

const createCloudFunction = async (options: CreateAppOptions = {}) => {
const createCloudFunction = async (
options: CreateAppOptions = {},
createHandlerOptions: CreateHandlerOptions = {},
) => {
const handler = new ApolloServer(
(options.graphqlOptions as Config) || { schema: Schema },
).createHandler();
).createHandler(createHandlerOptions);

const app = express();
app.use(bodyParser.json());
Expand All @@ -40,7 +43,10 @@ const createCloudFunction = async (options: CreateAppOptions = {}) => {

describe('googleCloudApollo', () => {
it('handles requests with path set to null', async () => {
const app = await createCloudFunction();
const app = await createCloudFunction(
{},
{ expressGetMiddlewareOptions: { path: '/' } },
);
const res = await request(app).get('/').set('Accept', 'text/html');
expect(res.status).toEqual(200);
});
Expand Down
64 changes: 0 additions & 64 deletions packages/apollo-server-cloud-functions/src/googleCloudApollo.ts

This file was deleted.

3 changes: 1 addition & 2 deletions packages/apollo-server-cloud-functions/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"include": ["src/**/*"],
"exclude": ["**/__tests__"],
"references": [
{ "path": "../apollo-server-core" },
{ "path": "../apollo-server-types" },
{ "path": "../apollo-server-express" },
]
}

0 comments on commit 45e69e0

Please sign in to comment.