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

Documentation for new request pipeline life-cycle hooks/p… #2008

Merged
merged 18 commits into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sidebar_categories:
- features/creating-directives
- features/authentication
- features/testing
- features/plugins
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should rearchitect the sidebar -- features seems like it's getting too long. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's an accurate statement, but I might hold off until we've done some additional Apollo Server 3 planning. Suggestions welcomed though!

# Schema stitching:
# - features/schema-stitching
# - features/remote-schemas
Expand Down
320 changes: 320 additions & 0 deletions docs/source/features/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
---
title: Plugins
description: Extending Apollo Server through the use of plugins.
---

> **Note:** Plugins are available in Apollo Server 2.2.x or higher.

## Overview

The default Apollo Server installation is designed for a reliable out-of-the-box experience with as little configuration as possible. In order to provide additional features, Apollo Server supports _plugins_ which can interface with various stages of server operation and each request/response cycle.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a sentence or two about why plugins are valuable for developers with some potential use cases -- it's unclear right now why someone would want to read this docs page.

Copy link
Member Author

@abernix abernix Feb 6, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts?

Suggested change
For example, these life-cycle hooks might be used to:
* Change the HTTP response code depending on the result of the operation's execution using the `willSendResponse` life-cycle hook.
* Implement rate-limiting for certain IP addresses using the `requestDidStart` life-cycle hook.

## Usage

Plugins for Apollo Server can be specified using the `plugins` configuration parameter to the Apollo Server constructor options.

The `plugins` array is an array of plugins. They might be provided as a module (and optionally published to a registry — e.g. npm) or defined in-line within the `ApolloServer` constructor. Plugins should be defined correctly and the requirements of building a plugin are explained in the plugin [definition](#definition) section below.

> **Note:** If a plugin is provided by a package published to a registry (for example, npm), that package must be installed using `npm install <plugin>` or `yarn add <plugin>` prior to use. In-line plugins or plugins which reside locally do not need to be installed.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this note, since the vast majority of people reading this know how to install a package from a registry.

abernix marked this conversation as resolved.
Show resolved Hide resolved

An example of Apollo Server which installed three plugins might look like:

```js
const { ApolloServer } = require('apollo-server');

/* Note: This example doesn't provide `typeDefs` or `resolvers`,
both of which are necessary to start the server. */
const { typeDefs, resolvers } = require('./separatelyDefined');

const server = new ApolloServer({
typeDefs,
resolvers,

/* Plugins are defined within this array and initialized sequentially. */
plugins: [

/* A plugin installed from the npm registry. */
require('apollo-server-operation-registry')({ /* options */ }),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semantically, wouldn't most people define variables at the top of the file when they load their modules? I think it's more readable to define const OperationRegistry = require('apollo-server-operation-registry') at the top of the file and pass OperationRegistry({ /* options */ }) to the plugin array. We can use the line highlighting feature in the code block to draw their attention to both areas of the file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe; patterns will vary! But this is a fair suggestion.

Here's what it would look like with your suggestion applied: https://5c5ad797c4ed1300088f2dca--apollo-server-docs.netlify.com/docs/apollo-server/features/plugins.html

I thiiinnk...I'm indifferent. The primary reason I had it the other way was to tightly-couple the three examples together. Also, if you think this looks good in the preview, do you think I should change the ./localPluginModule import (below) as well?


/* A plugin which is defined locally. */
require('./localPluginModule'),

/* A plugin which is defined in-line. */
{
/* ... plugin event hooks ... */
},
],
})
```

## Definition

> **Types:** To facilitate plugin development, the `apollo-server-plugin-base` module exports [the `ApolloServerPlugin` interface](https://github.com/apollographql/apollo-server/blob/master/packages/apollo-server-plugin-base/src/index.ts) for plugins to utilize. It's recommended to use this interafce when building custom plugins.
abernix marked this conversation as resolved.
Show resolved Hide resolved


A plugin defines the life-cycle events it wishes to act upon using an object which maps events (specific events are defined in further detail later in this document) to the functions that implement them.

For example, here is a very simple plugin, defined as an object literal, which implements a `requestDidStart` event:

```js
{
requestDidStart() {
console.log('The request started.');
},
}
```

This plugin might be directly included as an element of the `plugins`, or it could be provided as a separate module:

```js
module.exports = {
requestDidStart() {
/* ... */
},
};
```

Plugins which accepted options might providing a function which returns an object that implements a object matching the `ApolloServerPlugin` interface:

```js
/* localPluginModule.js */
module.exports = (options) => {
/* ...Plugin specific implementation... */

return {
requestDidStart() {
console.log('The options were', options);
},
};
};
```

Within the `plugins` array, this `localPluginModule.js` would be used as:

```js
/* ... Existing, required ApolloServer configuration. ... */

plugins: [

require('./localPluginModule')({
/* ...configuration options, when necessary! */
}),

],

/* ... any additional ApolloServer configuration. ... */
```

And finally, advanced cases can implement the `ApolloServerPlugin` interface via a factory function. The factory function will receive `pluginInfo`, which can allow implementors to adjust their behavior based on circumstantial factors:

```js
/* advancedPluginModule.js */
module.exports = (options) => {
/* ...Plugin specific implementation... */

return (pluginInfo) => {
abernix marked this conversation as resolved.
Show resolved Hide resolved
console.log('The pluginInfo was', pluginInfo);
abernix marked this conversation as resolved.
Show resolved Hide resolved

return {
requestDidStart() {
console.log('The options were', options);
}
};
};
}
```

And again, this could be used as a plugin by defining it in the `plugins` array:

```js
/* ... Existing, required ApolloServer configuration. ... */

plugins: [

require('./advancedPluginModule')({
/* ...configuration options, when necessary! */
}),

],

/* ... any additional ApolloServer configuration. ... */
```

> **Note:** Currently the `pluginInfo` is undefined, but future additions to the plugin API will enable this functionality. For now, the factory function facilities are in place to use, but `pluginInfo` is simply not available.
abernix marked this conversation as resolved.
Show resolved Hide resolved
abernix marked this conversation as resolved.
Show resolved Hide resolved

## Events
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this section should be higher up the hierarchy chain, possibly even above the definition, since it's core to understanding plugins. Then, we can pull Server lifecycle events and Request lifecycle events into their own headings with all of the lifecycle methods as subheadings. Right now, it looks strange in the deploy preview that all of the methods are on the same hierarchy level as Server lifecycle events and Request lifecycle events.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a great suggestion; even though it slightly compromises the hierarchy, since it moves Events away from the event details that were under it, I think I like it better too.

Though it's possible that the oddness is partially because we only show the top few levels of headings on the sidebar.

Let me know what you think on https://5c5add5794fb530008ede1aa--apollo-server-docs.netlify.com/docs/apollo-server/features/plugins.html#Events?


There are two main categories of events: server life-cycle events and request life-cycle events.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to drop the dash in between life-cycle? In the React docs, they spell it without the dash. https://reactjs.org/docs/state-and-lifecycle.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's drop it! (It's hard to say; life-cycle has almost a billion matches on Google and lifecycle only has 128 million. Hard to know if "lifecycle" is more closely associated with CS, but I digress.)

My vote would be that we do it consistently across all our documentation, and based on this search, that would be lifecycle. I'll make the change in a follow-up commit.


As the names imply, server life-cycle events are those which might not be directly related to a specific request. Instead, these events are more generally aimed at providing integrations for the server as a whole, rather than integrations which apply per request.

On the other hand, request life-cycle events are those which are specifically coupled to a specific request. The organization of plugin event registration aims to make it simple to couple request life-cycle events with server life-cycle events by nesting the request life-cycle events within appropriate server life-cycle events. For example, the definition of request life-cycle events is done as an extension of the `requestDidStart` server life-cycle event. This will be explained further below.

### Server life-cycle events

Server life-cycle events are custom integration points which generally cover the life-cycle of the server, rather than focusing on a specific request. Specific server life-cycle events may expose additional events which are relevant to that portion of their life-cycle, but these are intended to be the most high-level events which represent the super-set of all events which occur will occur within Apollo Server.

In the case that an event exposes additional events, the additional events are coupled to the server life-cycle event in order to provide a focused context which allows developers to couple related logic together. This will be explored more concisely in the `requestDidStart` server life-cycle event below.

### `serverWillStart`

The `serverWillStart` event is fired when the GraphQL server is preparing to start. If this is defined as an `async` function (or if it returns a `Promise`) the server will not start until the asynchronous behavior is resolved. Any rejection in this event will cause the server to not start, which provides a technique to ensure particular behavior is met before starting (for example, confirming that an underlying dependency is ready).

#### Example

```js
const server = new ApolloServer({
/* ... other necessary configuration ... */

plugins: [
{
serverWillStart() {

}
}
]
})
```

### `requestDidStart`

This event is emitted when the server has begun fulfilling a request. This life-cycle may return an object which implements request life-cycle events, as necessary.

The `requestDidStart` event can return an object which implements the `GraphQLRequestListener` interface in order to define more specific [request- life-cycle events](#Request-life-cycle-events) &mdash; e.g. `parsingDidStart`, `didResolveOperation` `willSendResponse`, etc. By including these as a subset of `requestDidStart`, plugin specific request scope can be created and used by the more granular events.

```js
const server = new ApolloServer({
/* ... other necessary configuration ... */

plugins: [
{
/* The `requestDidStart` will be called when the request has started
processing and more granular events — like `parsingDidStart` below —
are executed when those particular events occur. */
requestDidStart(requestContext) {

/* Request-specific scope can be created here and
used in more granular life-cycle events below. */

return {

parsingDidStart(requestContext) {
/* This `parsingDidStart` life-cycle event is
called when parsing begins, but scoped within the
`requestDidStart` server life-cycle event. */
},

}
}
}
],

/* */
})
```

If there are no specific request life-cycle events to implement, `requestDidStart` should not return anything.

### Request life-cycle events

Request life-cycle events must be implemented by returning an object which defines their behavior from the `requestDidStart` server life-cycle event. By maintaining this structure, coupling logic, and defining plugin-specific request scope becomes semantic and co-located.

> **Types:** The `apollo-server-plugin-base` module exports [the `GraphQLRequestListener` interface](https://github.com/apollographql/apollo-server/blob/master/packages/apollo-server-plugin-base/src/index.ts) which defines the shape of request life-cycle events. It's recommended to use this interafce when building custom plugins which implement granular request life-cycle events via `requestDidStart`.
abernix marked this conversation as resolved.
Show resolved Hide resolved

For example, to implement any of the request life-cycle events, an object should be returned from `requestDidStart` as such:

```js
const server = new ApolloServer({
/* ... other necessary configuration ... */

plugins: [
{
requestDidStart(requestContext) {
/* Plugin-specific request scope can be created here and
used in more granular life-cycle events below. */
return {

parsingDidStart(requestContext) {

},

validationDidStart(requestContext) {

},

didResolveOperation(requestContext) {

},

/* ... any additional request life-cycle events... */

}
}
}
],

/* */
})
```

### `parsingDidStart`

The `parsingDidStart` request life-cycle event will receive the request context as the first argument. At this stage, the `document` AST may not be defined since the parsing may not succeed.

#### TypeScript signature

```typescript
parsingDidStart?(
requestContext: GraphQLRequestContext<TContext>,
): (err?: Error) => void | void;
```

### `validationDidStart`

The `validationDidStart` request life-cycle event will receive the request context as the first argument. Since parsing would have been successful prior to validation, the `document` AST will be present.

#### TypeScript signature

```typescript
validationDidStart?(
requestContext: WithRequired<GraphQLRequestContext<TContext>, 'document'>,
): (err?: ReadonlyArray<Error>) => void | void;
```

### `didResolveOperation`

The `didResolveOperation` request life-cycle event is triggered after the operation to be executed has been successfully retrieved from the `document` AST by `graphql`'s `getOperationAST`. This focusing of execution which identifies the correct operation is important when a `document` contains multiple operations. At this stage, in addition to the `document` the `operationName` (`String`) and `operation` (AST) will be present on the context.

If the operation is anonymous (e.g. the operation is `query { ... }` rather than `query NamedQuery { ... }`, then `operationName` will be `null`.

```typescript
didResolveOperation?(
requestContext: WithRequired<
GraphQLRequestContext<TContext>,
'document' | 'operationName' | 'operation'
>,
): ValueOrPromise<void>;
```

### `executionDidStart`

> TODO
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO.


```typescript
executionDidStart?(
requestContext: WithRequired<
GraphQLRequestContext<TContext>,
'document' | 'operationName' | 'operation'
>,
): (err?: Error) => void | void;
```

### `willSendResponse`

> TODO
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth leaving myself a comment that this is still TODO and waiting to be filled out.


```typescript
willSendResponse?(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be used to modify the response before returning, such as adding additional metrics?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes!

Copy link

@hochoy hochoy Jul 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for including this. If we wanted to alter the extensions object in the response, I noticed that only Approach 1 worked. This feels a little counter-intuitive for devs used to functional programming where state-in -> function -> state out.

// Approach 1: Direct mutation of object
const helloPlugin1 = {

  requestDidStart() {

    return {
      willSendResponse(requestContext) {

        requestContext.response.extensions.randomField = 'Hello World!';

      },
    };
  },
};

// Approach 2: clone and return object
const _ = require('lodash');

const helloPlugin2 = {

  requestDidStart() {

    return {
      willSendResponse(requestContext) {

        const newReqContext = _.cloneDeep(requestContext);

        newReqContext.response.extensions.randomField = 'Hello World!';

        return newReqContext;
      },
    };
  },
};

requestContext: WithRequired<GraphQLRequestContext<TContext>, 'response'>,
): ValueOrPromise<void>;
```