Skip to content

Commit

Permalink
Add deprecation warnings for GraphQLExtension usage. (#4135)
Browse files Browse the repository at this point in the history
  • Loading branch information
abernix committed May 19, 2020
1 parent ba58cc4 commit 7dd68b3
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 0 deletions.
83 changes: 83 additions & 0 deletions packages/apollo-server-core/src/__tests__/runQuery.test.ts
Expand Up @@ -31,6 +31,7 @@ import {
} from 'apollo-server-plugin-base';
import { InMemoryLRUCache } from 'apollo-server-caching';
import { generateSchemaHash } from "../utils/schemaHash";
import { Logger } from "apollo-server-types";

// This is a temporary kludge to ensure we preserve runQuery behavior with the
// GraphQLRequestProcessor refactoring.
Expand Down Expand Up @@ -399,6 +400,88 @@ describe('runQuery', () => {
}
}

describe('deprecation warnings', () => {
const queryString = `{ testString }`;
async function runWithExtAndReturnLogger(
extensions: QueryOptions['extensions'],
): Promise<Logger> {
const logger = {
warn: jest.fn(() => {}),
info: console.info,
debug: console.debug,
error: console.error,
};

await runQuery(
{
schema,
queryString,
extensions,
request: new MockReq(),
},
{
logger,
},
);

return logger;
}

it('warns about named extensions', async () => {
const logger = await runWithExtAndReturnLogger([
() => new (class NamedExtension implements GraphQLExtension<any> {})(),
]);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringMatching(/^\[deprecated\] A "NamedExtension" was/));
});

it('warns about anonymous extensions', async () => {
const logger = await runWithExtAndReturnLogger([
() => new (class implements GraphQLExtension<any> {})(),
]);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringMatching(/^\[deprecated\] An anonymous extension was/));
});

it('warns about anonymous class expressions', async () => {
// In other words, when the name is the name of the variable.
const anon = class implements GraphQLExtension<any> {};
const logger = await runWithExtAndReturnLogger([
() => new anon(),
]);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringMatching(/^\[deprecated\] A "anon" was/));
});

it('warns for multiple extensions', async () => {
const logger = await runWithExtAndReturnLogger([
() => new (class Name1Ext implements GraphQLExtension<any> {})(),
() => new (class Name2Ext implements GraphQLExtension<any> {})(),
]);
expect(logger.warn).toHaveBeenCalledWith(
expect.stringMatching(/^\[deprecated\] A "Name1Ext" was/));
expect(logger.warn).toHaveBeenCalledWith(
expect.stringMatching(/^\[deprecated\] A "Name2Ext" was/));
});

it('warns only once', async () => {
// Will use the same extension across two invocations.
class NameExt implements GraphQLExtension<any> {};

const logger1 = await runWithExtAndReturnLogger([
() => new NameExt,
]);
expect(logger1.warn).toHaveBeenCalledWith(
expect.stringMatching(/^\[deprecated\] A "NameExt" was/));

const logger2 = await runWithExtAndReturnLogger([
() => new NameExt,
]);
expect(logger2.warn).not.toHaveBeenCalledWith(
expect.stringMatching(/^\[deprecated\] A "NameExt" was/));
});
});

it('creates the extension stack', async () => {
const queryString = `{ testString }`;
const extensions = [() => new CustomExtension()];
Expand Down
46 changes: 46 additions & 0 deletions packages/apollo-server-core/src/requestPipeline.ts
Expand Up @@ -113,6 +113,14 @@ export type DataSources<TContext> = {

type Mutable<T> = { -readonly [P in keyof T]: T[P] };

/**
* We attach this symbol to the constructor of extensions to mark that we've
* already warned about the deprecation of the `graphql-extensions` API for that
* particular definition.
*/
const symbolExtensionDeprecationDone =
Symbol("apolloServerExtensionDeprecationDone");

export async function processGraphQLRequest<TContext>(
config: GraphQLRequestPipelineConfig<TContext>,
requestContext: Mutable<GraphQLRequestContext<TContext>>,
Expand Down Expand Up @@ -634,6 +642,44 @@ export async function processGraphQLRequest<TContext>(
// objects.
const extensions = config.extensions ? config.extensions.map(f => f()) : [];

// Warn about usage of (deprecated) `graphql-extensions` implementations.
// Since extensions are often provided as factory functions which
// instantiate an extension on each request, we'll attach a symbol to the
// constructor after we've warned to ensure that we don't do it on each
// request. Another option here might be to keep a `Map` of constructor
// instances within this module, but I hope this will do the trick.
const hasOwn = Object.prototype.hasOwnProperty;
extensions.forEach((extension) => {
// Using `hasOwn` just in case there is a user-land `hasOwnProperty`
// defined on the `constructor` object.
if (
!extension.constructor ||
hasOwn.call(extension.constructor, symbolExtensionDeprecationDone)
) {
return;
}

Object.defineProperty(
extension.constructor,
symbolExtensionDeprecationDone,
{ value: true }
);

const extensionName = extension.constructor.name;
logger.warn(
'[deprecated] ' +
(extensionName
? 'A "' + extensionName + '" '
: 'An anonymous extension ') +
'was defined within the "extensions" configuration for ' +
'Apollo Server. The API on which this extension is built ' +
'("graphql-extensions") is being deprecated in the next major ' +
'version of Apollo Server in favor of the new plugin API. See ' +
'https://go.apollo.dev/s/plugins for the documentation on how ' +
'these plugins are to be defined and used.',
);
});

return new GraphQLExtensionStack(extensions);
}

Expand Down

0 comments on commit 7dd68b3

Please sign in to comment.