diff --git a/package-lock.json b/package-lock.json index 51f572bc500..adcb0982e29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21275,9 +21275,8 @@ "version": "3.0.0-preview.0", "license": "MIT", "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" @@ -26974,10 +26973,9 @@ "apollo-server-cloud-functions": { "version": "file:packages/apollo-server-cloud-functions", "requires": { - "apollo-server-core": "file:../apollo-server-core", - "apollo-server-env": "file:../apollo-server-env", + "apollo-server-express": "file:../apollo-server-express", "apollo-server-integration-testsuite": "file:../apollo-server-integration-testsuite", - "apollo-server-types": "file:../apollo-server-types" + "express": "^4.17.1" } }, "apollo-server-cloudflare": { diff --git a/packages/apollo-server-cloud-functions/package.json b/packages/apollo-server-cloud-functions/package.json index 204dd9d399a..0656c498f96 100644 --- a/packages/apollo-server-cloud-functions/package.json +++ b/packages/apollo-server-cloud-functions/package.json @@ -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" diff --git a/packages/apollo-server-cloud-functions/src/ApolloServer.ts b/packages/apollo-server-cloud-functions/src/ApolloServer.ts index 733c4ecae4c..035033ba637 100644 --- a/packages/apollo-server-cloud-functions/src/ApolloServer.ts +++ b/packages/apollo-server-cloud-functions/src/ApolloServer.ts @@ -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 { - return super.graphQLServerOptions({ req, res }); - } - - public createHandler({ cors }: CreateHandlerOptions = { cors: undefined }) { - const corsHeaders = {} as Record; - - 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); }; } } diff --git a/packages/apollo-server-cloud-functions/src/__tests__/googleCloudApollo.test.ts b/packages/apollo-server-cloud-functions/src/__tests__/googleCloudApollo.test.ts index c65da35e1e0..74b1a56e723 100644 --- a/packages/apollo-server-cloud-functions/src/__tests__/googleCloudApollo.test.ts +++ b/packages/apollo-server-cloud-functions/src/__tests__/googleCloudApollo.test.ts @@ -1,4 +1,4 @@ -import { ApolloServer } from '../ApolloServer'; +import { ApolloServer, CreateHandlerOptions } from '../ApolloServer'; import testSuite, { schema as Schema, CreateAppOptions, @@ -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()); @@ -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); }); diff --git a/packages/apollo-server-cloud-functions/src/googleCloudApollo.ts b/packages/apollo-server-cloud-functions/src/googleCloudApollo.ts deleted file mode 100644 index 4c7371e937d..00000000000 --- a/packages/apollo-server-cloud-functions/src/googleCloudApollo.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - GraphQLOptions, - HttpQueryError, - runHttpQuery, -} from 'apollo-server-core'; -import { Headers } from 'apollo-server-env'; -import { Request, Response } from 'express'; -import { ValueOrPromise } from 'apollo-server-types'; - -export interface CloudFunctionGraphQLOptionsFunction { - (req?: Request, res?: Response): ValueOrPromise; -} - -export function graphqlCloudFunction( - options: GraphQLOptions | CloudFunctionGraphQLOptionsFunction, -): any { - 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: any = (req: Request, res: Response): void => { - const hasPostBody = req.body && Object.keys(req.body).length > 0; - if (req.method === 'POST' && !hasPostBody) { - res.status(500).send('POST body missing.'); - return; - } - - runHttpQuery([req, res], { - method: req.method, - options: options, - query: hasPostBody ? req.body : (req.query as any), - request: { - url: req.url, - method: req.method, - headers: new Headers(req.headers as any), // ? Check if this actually works - }, - }).then( - ({ graphqlResponse, responseInit }) => { - res - .status(200) - .set(responseInit.headers) - .send(graphqlResponse); - }, - (error: HttpQueryError) => { - if ('HttpQueryError' !== error.name) { - res.status(500).send(error); - return; - } - res - .status(error.statusCode) - .set(error.headers) - .send(error.message); - }, - ); - }; - - return graphqlHandler; -} diff --git a/packages/apollo-server-cloud-functions/tsconfig.json b/packages/apollo-server-cloud-functions/tsconfig.json index e34feba8eb6..6f7f1c89ffc 100644 --- a/packages/apollo-server-cloud-functions/tsconfig.json +++ b/packages/apollo-server-cloud-functions/tsconfig.json @@ -7,7 +7,6 @@ "include": ["src/**/*"], "exclude": ["**/__tests__"], "references": [ - { "path": "../apollo-server-core" }, - { "path": "../apollo-server-types" }, + { "path": "../apollo-server-express" }, ] }