diff --git a/.changeset/eight-bats-shop.md b/.changeset/eight-bats-shop.md new file mode 100644 index 00000000000..7d9e0a86832 --- /dev/null +++ b/.changeset/eight-bats-shop.md @@ -0,0 +1,5 @@ +--- +'@apollo/server': minor +--- + +New `ApolloServerPluginSchemaReportingDisabled` plugin which can override the `APOLLO_SCHEMA_REPORTING` environment variable. diff --git a/.changeset/strong-laws-bow.md b/.changeset/strong-laws-bow.md new file mode 100644 index 00000000000..f7aac153fb7 --- /dev/null +++ b/.changeset/strong-laws-bow.md @@ -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`. diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx index 1f019931e26..53e6d5d2d03 100644 --- a/docs/source/api/apollo-server.mdx +++ b/docs/source/api/apollo-server.mdx @@ -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/). + diff --git a/docs/source/api/plugin/schema-reporting.mdx b/docs/source/api/plugin/schema-reporting.mdx index 5c71cccf123..39d42a0b620 100644 --- a/docs/source/api/plugin/schema-reporting.mdx +++ b/docs/source/api/plugin/schema-reporting.mdx @@ -122,3 +122,24 @@ Specifies which [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fet + +## 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: + + + +```ts +import { ApolloServer } from '@apollo/server'; +import { ApolloServerPluginSchemaReportingDisabled } from '@apollo/server/plugin/disabled'; + +const server = new ApolloServer({ + typeDefs, + resolvers, + plugins: [ + ApolloServerPluginSchemaReportingDisabled(), + ], +}); +``` + + diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx index 66c02d2784b..9e519f06c0b 100644 --- a/docs/source/migration.mdx +++ b/docs/source/migration.mdx @@ -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` diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 8bfb9fc465c..3911cc5ae95 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -804,9 +804,48 @@ export class ApolloServer { 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')) { diff --git a/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts b/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts index 6f16ad3a7f8..f6ed6a86f14 100644 --- a/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts +++ b/packages/server/src/__tests__/plugin/usageReporting/plugin.test.ts @@ -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'); @@ -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.', + ); +}); diff --git a/packages/server/src/internalPlugin.ts b/packages/server/src/internalPlugin.ts index 410c560c946..206f6a32671 100644 --- a/packages/server/src/internalPlugin.ts +++ b/packages/server/src/internalPlugin.ts @@ -11,7 +11,8 @@ export interface InternalApolloServerPlugin extends ApolloServerPlugin { // 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 diff --git a/packages/server/src/plugin/cacheControl/index.ts b/packages/server/src/plugin/cacheControl/index.ts index f80231dc223..8462e66f573 100644 --- a/packages/server/src/plugin/cacheControl/index.ts +++ b/packages/server/src/plugin/cacheControl/index.ts @@ -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 diff --git a/packages/server/src/plugin/disabled/index.ts b/packages/server/src/plugin/disabled/index.ts index 85adee10b90..b427fef6d90 100644 --- a/packages/server/src/plugin/disabled/index.ts +++ b/packages/server/src/plugin/disabled/index.ts @@ -14,9 +14,8 @@ import type { function disabledPlugin(id: InternalPluginId): ApolloServerPlugin { const plugin: InternalApolloServerPlugin = { - __internal_plugin_id__() { - return id; - }, + __internal_plugin_id__: id, + __is_disabled_plugin__: true, }; return plugin; } @@ -33,6 +32,10 @@ export function ApolloServerPluginLandingPageDisabled(): ApolloServerPlugin { + return disabledPlugin('SchemaReporting'); +} + export function ApolloServerPluginUsageReportingDisabled(): ApolloServerPlugin { return disabledPlugin('UsageReporting'); } diff --git a/packages/server/src/plugin/inlineTrace/index.ts b/packages/server/src/plugin/inlineTrace/index.ts index 2977fd2a31e..fa2de0215b8 100644 --- a/packages/server/src/plugin/inlineTrace/index.ts +++ b/packages/server/src/plugin/inlineTrace/index.ts @@ -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 diff --git a/packages/server/src/plugin/schemaReporting/index.ts b/packages/server/src/plugin/schemaReporting/index.ts index d895740674d..3ff24e86ccd 100644 --- a/packages/server/src/plugin/schemaReporting/index.ts +++ b/packages/server/src/plugin/schemaReporting/index.ts @@ -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) { diff --git a/packages/server/src/plugin/usageReporting/plugin.ts b/packages/server/src/plugin/usageReporting/plugin.ts index 213561918f6..02c4962635a 100644 --- a/packages/server/src/plugin/usageReporting/plugin.ts +++ b/packages/server/src/plugin/usageReporting/plugin.ts @@ -70,9 +70,8 @@ export function ApolloServerPluginUsageReporting( requestContext: GraphQLRequestContext, ) => GraphQLRequestListener; 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