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

Add deprecation warnings for GraphQLExtension usage. #4135

Merged
merged 1 commit into from May 19, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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(
Copy link
Member

Choose a reason for hiding this comment

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

This is nice!

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