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

Improve behavior of Disabled plugins #7104

Merged
merged 2 commits into from
Oct 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/eight-bats-shop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/server': minor
---

New `ApolloServerPluginSchemaReportingDisabled` plugin which can override the `APOLLO_SCHEMA_REPORTING` environment variable.
5 changes: 5 additions & 0 deletions .changeset/strong-laws-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@apollo/server': minor
---

It is now an error to combine a "disabled" plugin such as `ApolloServerPluginUsageReportingDisabled` with its enabled counterpart such as `ApolloServerPluginUsageReporting`.
4 changes: 2 additions & 2 deletions docs/source/api/apollo-server.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -372,10 +372,10 @@ To learn more about configuring Apollo Server's cache, see [Configuring cache ba

An array of [plugins](../integrations/plugins) to install in your server instance. Each array element can be either a valid plugin object or a zero-argument function that _returns_ a valid plugin object.

In certain cases, Apollo Server installs some of its built-in plugins automatically (for example, when you provide an Apollo Studio API key with the `APOLLO_KEY` environment variable). For details, see the API references for these plugins: [usage reporting](./plugin/usage-reporting/), [schema reporting](./plugin/schema-reporting/), [landing page](./plugin/landing-pages/), and [inline trace](./plugin/inline-trace/).

You can _also_ add plugins to your server before you start it using the [`addPlugin`](#addplugin) method.

Apollo Server comes with several plugins that it installs automatically in their default configuration if certain conditions are met. For example, the usage reporting plugin is installed if you provide a graph API key and a graph ref. Apollo Server skips this automatic installation if you manually provide the plugin (in the `plugins` array or with the `addPlugin` method), or if you provide the plugin's corresponding "disabled" plugin (such as `ApolloServerPluginUsageReportingDisabled()` for `ApolloServerPluginUsageReporting`). For details, see the API references for these plugins: [usage reporting](./plugin/usage-reporting/), [schema reporting](./plugin/schema-reporting/), [landing page](./plugin/landing-pages/), [cache control](./plugin/cache-control/), and [inline trace](./plugin/inline-trace/).

</td>
</tr>

Expand Down
21 changes: 21 additions & 0 deletions docs/source/api/plugin/schema-reporting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,24 @@ Specifies which [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fet

</tbody>
</table>

## Disabling the plugin

The schema reporting plugin is only automatically installed if the `APOLLO_SCHEMA_REPORTING` is set to `true`, so the easiest way to disable the plugin is to avoid setting that environment variable (and to not explicitly install `ApolloServerPluginSchemaReporting`). However, if you'd like to ensure that the schema reporting plugin is not installed in your server (perhaps for a test that might run with arbitrary environment variables set), you can disable schema reporting by installing the `ApolloServerPluginSchemaReportingDisabled` plugin, like so:

<MultiCodeBlock>

```ts
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginSchemaReportingDisabled } from '@apollo/server/plugin/disabled';

const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginSchemaReportingDisabled(),
],
});
```

</MultiCodeBlock>
23 changes: 23 additions & 0 deletions docs/source/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,29 @@ new ApolloServer({
});
```

## "Disabled" plugins cannot be combined with their enabled counterpart

Apollo Server has several plugins that are installed by default in certain conditions. To override this behavior, each of these plugins has a "disabled" counterpart that prevents this default installation.

But what happens if you combine the manual installation of a plugin with its disabled counterpart? Consider the following code:

```ts
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginUsageReporting(),
ApolloServerPluginUsageReportingDisabled(),
]
});
await server.start();
```

In Apollo Server 3, the "disabled" plugin is simply ignored if combined with its enabled counterpart. This could lead to confusion, as it can appear that an attempt to disable a feature is completely ignored.

In Apollo Server 4, `await server.start()` will throw if you combine a "disabled" plugin with its enabled counterpart, with an error like `You have tried to install both ApolloServerPluginUsageReporting and ApolloServerPluginUsageReportingDisabled`. If your server throws this error, choose whether you want the feature enabled or disabled, and install only the appropriate plugin.

(This change affects the usage reporting, inline trace, and cache control features. It also affects the schema reporting feature, although `ApolloServerPluginSchemaReportingDisabled` did not exist in Apollo Server 3. For technical reasons, it does not affect the landing page feature: combining `ApolloServerPluginLandingPageDisabled` with a landing page plugin should be considered as unspecified behavior which may change in a future release of Apollo Server 4.)

## Plugin API changes

### Fields on `GraphQLRequestContext`
Expand Down
41 changes: 40 additions & 1 deletion packages/server/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -804,9 +804,48 @@ export class ApolloServer<in out TContext extends BaseContext = BaseContext> {

const alreadyHavePluginWithInternalId = (id: InternalPluginId) =>
plugins.some(
(p) => pluginIsInternal(p) && p.__internal_plugin_id__() === id,
(p) => pluginIsInternal(p) && p.__internal_plugin_id__ === id,
);

// Make sure we're not trying to explicitly enable and disable the same
// feature. (Be careful: we are not trying to stop people from installing
// the same plugin twice if they have a use case for it, like two usage
// reporting plugins for different variants.)
//
// Note that this check doesn't work for the landing page plugin, because
// users can write their own landing page plugins and we don't know if a
// given plugin is a landing page plugin until the server has started.
const pluginsByInternalID = new Map<
InternalPluginId,
{ sawDisabled: boolean; sawNonDisabled: boolean }
>();
for (const p of plugins) {
if (pluginIsInternal(p)) {
const id = p.__internal_plugin_id__;
if (!pluginsByInternalID.has(id)) {
pluginsByInternalID.set(id, {
sawDisabled: false,
sawNonDisabled: false,
});
}
const seen = pluginsByInternalID.get(id)!;
if (p.__is_disabled_plugin__) {
seen.sawDisabled = true;
} else {
seen.sawNonDisabled = true;
}

if (seen.sawDisabled && seen.sawNonDisabled) {
throw new Error(
`You have tried to install both ApolloServerPlugin${id} and ` +
`ApolloServerPlugin${id}Disabled in your server. Please choose ` +
`whether or not you want to disable the feature and install the ` +
`appropriate plugin for your use case.`,
);
}
}
}

// Special case: cache control is on unless you explicitly disable it.
{
if (!alreadyHavePluginWithInternalId('CacheControl')) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import {
ApolloServerPluginUsageReportingOptions,
ApolloServerPluginUsageReporting,
} from '../../../plugin/usageReporting';
import { ApolloServerPluginCacheControlDisabled } from '../../../plugin/disabled';
import {
ApolloServerPluginCacheControlDisabled,
ApolloServerPluginUsageReportingDisabled,
} from '../../../plugin/disabled';
import { describe, it, expect, afterEach } from '@jest/globals';

const quietLogger = loglevel.getLogger('quiet');
Expand Down Expand Up @@ -525,3 +528,19 @@ describe('sendHeaders makeHTTPRequestHeaders helper', () => {
expect(http.requestHeaders['set-cookie']).toBe(undefined);
});
});

it('cannot combine enabling with disabling', async () => {
const server = new ApolloServer({
typeDefs: 'type Query { x: ID }',
plugins: [
ApolloServerPluginUsageReporting(),
ApolloServerPluginUsageReportingDisabled(),
],
});
await expect(server.start()).rejects.toThrow(
'You have tried to install both ApolloServerPluginUsageReporting ' +
'and ApolloServerPluginUsageReportingDisabled in your server. Please ' +
'choose whether or not you want to disable the feature and install the ' +
'appropriate plugin for your use case.',
);
});
3 changes: 2 additions & 1 deletion packages/server/src/internalPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export interface InternalApolloServerPlugin<TContext extends BaseContext>
extends ApolloServerPlugin<TContext> {
// Used to identify a few specific plugins that are instantiated
// by default if not explicitly used or disabled.
__internal_plugin_id__(): InternalPluginId;
__internal_plugin_id__: InternalPluginId;
__is_disabled_plugin__: boolean;
}

// Helper function for writing internal plugins which lets you write an object
Expand Down
5 changes: 2 additions & 3 deletions packages/server/src/plugin/cacheControl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,8 @@ export function ApolloServerPluginCacheControl(
>;

return internalPlugin({
__internal_plugin_id__() {
return 'CacheControl';
},
__internal_plugin_id__: 'CacheControl',
__is_disabled_plugin__: false,

async serverWillStart({ schema }) {
// Set the size of the caches to be equal to the number of composite types
Expand Down
9 changes: 6 additions & 3 deletions packages/server/src/plugin/disabled/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ import type {

function disabledPlugin(id: InternalPluginId): ApolloServerPlugin {
const plugin: InternalApolloServerPlugin<BaseContext> = {
__internal_plugin_id__() {
return id;
},
__internal_plugin_id__: id,
__is_disabled_plugin__: true,
};
return plugin;
}
Expand All @@ -33,6 +32,10 @@ export function ApolloServerPluginLandingPageDisabled(): ApolloServerPlugin<Base
return disabledPlugin('LandingPageDisabled');
}

export function ApolloServerPluginSchemaReportingDisabled(): ApolloServerPlugin<BaseContext> {
return disabledPlugin('SchemaReporting');
}

export function ApolloServerPluginUsageReportingDisabled(): ApolloServerPlugin<BaseContext> {
return disabledPlugin('UsageReporting');
}
5 changes: 2 additions & 3 deletions packages/server/src/plugin/inlineTrace/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,8 @@ export function ApolloServerPluginInlineTrace(
): ApolloServerPlugin {
let enabled: boolean | null = options.__onlyIfSchemaIsFederated ? null : true;
return internalPlugin({
__internal_plugin_id__() {
return 'InlineTrace';
},
__internal_plugin_id__: 'InlineTrace',
__is_disabled_plugin__: false,
async serverWillStart({ schema, logger }) {
// Handle the case that the plugin was implicitly installed. We only want it
// to actually be active if the schema appears to be federated. If you don't
Expand Down
5 changes: 2 additions & 3 deletions packages/server/src/plugin/schemaReporting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,8 @@ export function ApolloServerPluginSchemaReporting(
const bootId = uuidv4();

return internalPlugin({
__internal_plugin_id__() {
return 'SchemaReporting';
},
__internal_plugin_id__: 'SchemaReporting',
__is_disabled_plugin__: false,
async serverWillStart({ apollo, schema, logger }) {
const { key, graphRef } = apollo;
if (!key) {
Expand Down
5 changes: 2 additions & 3 deletions packages/server/src/plugin/usageReporting/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
requestContext: GraphQLRequestContext<TContext>,
) => GraphQLRequestListener<TContext>;
return internalPlugin({
__internal_plugin_id__() {
return 'UsageReporting';
},
__internal_plugin_id__: 'UsageReporting',
__is_disabled_plugin__: false,

// We want to be able to access locals from `serverWillStart` in our `requestDidStart`, thus
// this little hack. (Perhaps we should also allow GraphQLServerListener to contain
Expand Down