Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose getMiddleware function on server integrations #2947

Closed
wants to merge 15 commits into from
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

### vNEXT

### v2.4.10

- `apollo-server-koa`: Expose `server.getMiddleware()` function for getting access to koa middleware directly. [PR #2435](https://github.com/apollographql/apollo-server/pull/2435)

### v2.4.9

- Allow `GraphQLRequestListener` callbacks in plugins to depend on `this`. [PR #2470](https://github.com/apollographql/apollo-server/pull/2470)

### v2.4.8
Expand Down
48 changes: 47 additions & 1 deletion package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"@types/multer": "1.3.7",
"@types/node": "8.10.45",
"@types/node-fetch": "2.3.0",
"@types/parseurl": "1.3.1",
"@types/redis": "2.8.12",
"@types/request": "2.48.1",
"@types/request-promise": "4.1.42",
Expand Down
1 change: 1 addition & 0 deletions packages/apollo-server-express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"accepts": "^1.3.5",
"apollo-server-core": "file:../apollo-server-core",
"body-parser": "^1.18.3",
"compose-middleware": "^5.0.1",
"cors": "^2.8.4",
"graphql-subscriptions": "^1.0.0",
"graphql-tools": "^4.0.0",
Expand Down
117 changes: 78 additions & 39 deletions packages/apollo-server-express/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,28 @@ import {
} from 'apollo-server-core';
import accepts from 'accepts';
import typeis from 'type-is';

import { compose } from 'compose-middleware';
import parseurl from 'parseurl';
import { graphqlExpress } from './expressApollo';

export { GraphQLOptions, GraphQLExtension } from 'apollo-server-core';

export interface ServerRegistration {
export interface GetMiddlewareOptions {
path?: string;
cors?: corsMiddleware.CorsOptions | boolean;
bodyParserConfig?: OptionsJson | boolean;
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
}

export interface ServerRegistration extends GetMiddlewareOptions {
// Note: You can also pass a connect.Server here. If we changed this field to
// `express.Application | connect.Server`, it would be very hard to get the
// app.use calls to typecheck even though they do work properly. Our
// assumption is that very few people use connect with TypeScript (and in fact
// we suspect the only connect users left writing GraphQL apps are Meteor
// users).
app: express.Application;
path?: string;
cors?: corsMiddleware.CorsOptions | boolean;
bodyParserConfig?: OptionsJson | boolean;
onHealthCheck?: (req: express.Request) => Promise<any>;
disableHealthCheck?: boolean;
}

const fileUploadMiddleware = (
Expand Down Expand Up @@ -70,6 +74,28 @@ const fileUploadMiddleware = (
}
};

const middlewareFromPath = (
path: string,
middleware: express.RequestHandler,
) => (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
// While Express is quite capable of providing the `path`, `connect` doesn't
// provide `req.path` in the same way, even though it's available on the `req`
// as `req._parsedUrl`. That property is a cached representation of a
// previous parse done by the `parseurl` package, and by using that (popular)
// package here, we can still reap those cache benefits without directly
// accessing _parsedUrl ourselves, which could be risky.
const parsedUrl = parseurl(req);
if ( parsedUrl && parsedUrl.pathname === path) {
return middleware(req, res, next);
} else {
return next();
}
};

export interface ExpressContext {
req: express.Request;
res: express.Response;
Expand Down Expand Up @@ -102,17 +128,20 @@ export class ApolloServer extends ApolloServerBase {
return true;
}

public applyMiddleware({ app, ...rest }: ServerRegistration) {
app.use(this.getMiddleware(rest));
}

// TODO: While `express` is not Promise-aware, this should become `async` in
// a major release in order to align the API with other integrations (e.g.
// Hapi) which must be `async`.
public applyMiddleware({
app,
public getMiddleware({
path,
cors,
bodyParserConfig,
disableHealthCheck,
onHealthCheck,
}: ServerRegistration) {
}: GetMiddlewareOptions = {}) {
if (!path) path = '/graphql';

// Despite the fact that this `applyMiddleware` function is `async` in
Expand All @@ -128,27 +157,34 @@ export class ApolloServer extends ApolloServerBase {
// request comes in, but we won't call `next` on this middleware until it
// does. (And we'll take care to surface any errors via the `.catch`-able.)
const promiseWillStart = this.willStart();
app.use(path, (_req, _res, next) => {
promiseWillStart.then(() => next()).catch(next);
});

const middleware: express.RequestHandler[] = [];

middleware.push(
middlewareFromPath(path, (_req, _res, next) => {
promiseWillStart.then(() => next()).catch(next);
}),
);

if (!disableHealthCheck) {
app.use('/.well-known/apollo/server-health', (req, res) => {
// Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');

if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
res.json({ status: 'pass' });
})
.catch(() => {
res.status(503).json({ status: 'fail' });
});
} else {
res.json({ status: 'pass' });
}
});
middleware.push(
middlewareFromPath('/.well-known/apollo/server-health', (req, res) => {
// Response follows https://tools.ietf.org/html/draft-inadarei-api-health-check-01
res.type('application/health+json');

if (onHealthCheck) {
onHealthCheck(req)
.then(() => {
res.json({ status: 'pass' });
})
.catch(() => {
res.status(503).json({ status: 'fail' });
});
} else {
res.json({ status: 'pass' });
}
}),
);
}

let uploadsMiddleware;
Expand All @@ -162,26 +198,26 @@ export class ApolloServer extends ApolloServerBase {
// Note that we don't just pass all of these handlers to a single app.use call
// for 'connect' compatibility.
if (cors === true) {
app.use(path, corsMiddleware());
middleware.push(middlewareFromPath(path, corsMiddleware()));
} else if (cors !== false) {
app.use(path, corsMiddleware(cors));
middleware.push(middlewareFromPath(path, corsMiddleware(cors)));
}

if (bodyParserConfig === true) {
app.use(path, json());
middleware.push(middlewareFromPath(path, json()));
} else if (bodyParserConfig !== false) {
app.use(path, json(bodyParserConfig));
middleware.push(middlewareFromPath(path, json(bodyParserConfig)));
}

if (uploadsMiddleware) {
app.use(path, uploadsMiddleware);
middleware.push(middlewareFromPath(path, uploadsMiddleware));
}

// Note: if you enable playground in production and expect to be able to see your
// schema, you'll need to manually specify `introspection: true` in the
// ApolloServer constructor; by default, the introspection query is only
// enabled in dev.
app.use(path, (req, res, next) => {
middleware.push(middlewareFromPath(path, (req, res, next) => {
if (this.playgroundOptions && req.method === 'GET') {
// perform more expensive content-type check only if necessary
// XXX We could potentially move this logic into the GuiOptions lambda,
Expand All @@ -206,10 +242,13 @@ export class ApolloServer extends ApolloServerBase {
return;
}
}
return graphqlExpress(() => {
return this.createGraphQLServerOptions(req, res);
})(req, res, next);
});

return graphqlExpress(() =>
this.createGraphQLServerOptions(req, res),
)(req, res, next);
}));

return compose(middleware);
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-server-express/src/expressApollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import { ValueOrPromise } from 'apollo-server-env';

export interface ExpressGraphQLOptionsFunction {
(req?: express.Request, res?: express.Response): ValueOrPromise<
(req: express.Request, res: express.Response): ValueOrPromise<
GraphQLOptions
>;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/apollo-server-koa/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const server = new ApolloServer({ typeDefs, resolvers });

const app = new Koa();
server.applyMiddleware({ app });
// alternatively you can get a composed middleware from the apollo server
// app.use(server.getMiddleware());

app.listen({ port: 4000 }, () =>
console.log(`🚀 Server ready at http://localhost:4000${server.graphqlPath}`),
Expand Down
Loading