Repository: graphql/graphql-js
Pull request #3257 (2021-09-03 22:17:00)

fix: Preserve `deprecationReason` on `GraphQLInputField`s
`GraphQLInputObjectType.toConfig()` currently doesn't preserve `deprecationReason`s on its fields. Anything internal depending on this function omits the `deprecationReason` from a `GraphQLInputObjectType`'s fields.

This was easy to remedy - we just needed to copy the field's `deprecationReason` property during the mapping of fields done in `toConfig()`.

I experienced this issue when using `extendSchema` on a `GraphQLSchema` object which had a `GraphQLInputObjectType` with fields marked `@deprecated`. The reason would be dropped from the extended schema.

linux-foundation-easycla[bot]: <a href="https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/12261526/38307428/3257/#/?version=2"><img src="https://s3.amazonaws.com/cla-project-logo-prod/cla-not-signed.svg" alt="CLA Not Signed" align="left" height="28" width="328" ></a><br/><br /><ul><li><a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/12261526/38307428/3257/#/?version=2' target='_blank'>:x:</a> - Trevor Scheer The commit (138188b0f9c9bb4c2f7d000143c99908d8c47b4b ,f33cc7e0a9b8b64ebe7d7995409a61a2228a47fb) is not authorized under a signed CLA. <a href='https://api.easycla.lfx.linuxfoundation.org/v2/repository-provider/github/sign/12261526/38307428/3257/#/?version=2' target='_blank'>Please click here to be authorized</a>. For further assistance with EasyCLA, <a href='https://jira.linuxfoundation.org/servicedesk/customer/portal/4' target='_blank'>please submit a support request ticket</a>.</li></ul>

Patch:
diff --git a/src/error/GraphQLError.ts b/src/error/GraphQLError.ts
index 448d7459..a3a79925 100644
--- a/src/error/GraphQLError.ts
+++ b/src/error/GraphQLError.ts
@@ -215,12 +215,64 @@ export class GraphQLError extends Error {
     return output;
   }
 
-  // FIXME: workaround to not break chai comparisons, should be remove in v16
+  toJSON(): GraphQLFormattedError {
+    type WritableFormattedError = {
+      -readonly [P in keyof GraphQLFormattedError]: GraphQLFormattedError[P];
+    };
+
+    const formattedError: WritableFormattedError = {
+      message: this.message,
+    };
+
+    if (this.locations != null) {
+      formattedError.locations = this.locations;
+    }
+
+    if (this.path != null) {
+      formattedError.path = this.path;
+    }
+
+    if (this.extensions != null) {
+      formattedError.extensions = this.extensions;
+    }
+
+    return formattedError;
+  }
+
   get [Symbol.toStringTag](): string {
     return 'Object';
   }
 }
 
+/**
+ * See: https://spec.graphql.org/draft/#sec-Errors
+ */
+export interface GraphQLFormattedError {
+  /**
+   * A short, human-readable summary of the problem that **SHOULD NOT** change
+   * from occurrence to occurrence of the problem, except for purposes of
+   * localization.
+   */
+  readonly message: string;
+  /**
+   * If an error can be associated to a particular point in the requested
+   * GraphQL document, it should contain a list of locations.
+   */
+  readonly locations?: ReadonlyArray<SourceLocation>;
+  /**
+   * If an error can be associated to a particular field in the GraphQL result,
+   * it _must_ contain an entry with the key `path` that details the path of
+   * the response field which experienced the error. This allows clients to
+   * identify whether a null result is intentional or caused by a runtime error.
+   */
+  readonly path?: ReadonlyArray<string | number>;
+  /**
+   * Reserved for implementors to extend the protocol however they see fit,
+   * and hence there are no additional restrictions on its contents.
+   */
+  readonly extensions?: { [key: string]: unknown };
+}
+
 /**
  * Prints a GraphQLError to a string, representing useful location information
  * about the error's position in the source.
@@ -230,3 +282,13 @@ export class GraphQLError extends Error {
 export function printError(error: GraphQLError): string {
   return error.toString();
 }
+
+/**
+ * Given a GraphQLError, format it according to the rules described by the
+ * Response Format, Errors section of the GraphQL Specification.
+ *
+ * @deprecated Please use `error.toString` instead. Will be removed in v17
+ */
+export function formatError(error: GraphQLError): GraphQLFormattedError {
+  return error.toJSON();
+}
diff --git a/src/error/formatError.ts b/src/error/formatError.ts
deleted file mode 100644
index 259f24e5..00000000
--- a/src/error/formatError.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { devAssert } from '../jsutils/devAssert';
-
-import type { SourceLocation } from '../language/location';
-
-import type { GraphQLError } from './GraphQLError';
-
-/**
- * Given a GraphQLError, format it according to the rules described by the
- * Response Format, Errors section of the GraphQL Specification.
- */
-export function formatError(error: GraphQLError): GraphQLFormattedError {
-  devAssert(error, 'Received null or undefined error.');
-  const message = error.message ?? 'An unknown error occurred.';
-  const locations = error.locations;
-  const path = error.path;
-  const extensions = error.extensions;
-
-  return extensions
-    ? { message, locations, path, extensions }
-    : { message, locations, path };
-}
-
-/**
- * See: https://spec.graphql.org/draft/#sec-Errors
- */
-export interface GraphQLFormattedError {
-  /**
-   * A short, human-readable summary of the problem that **SHOULD NOT** change
-   * from occurrence to occurrence of the problem, except for purposes of
-   * localization.
-   */
-  readonly message: string;
-  /**
-   * If an error can be associated to a particular point in the requested
-   * GraphQL document, it should contain a list of locations.
-   */
-  readonly locations?: ReadonlyArray<SourceLocation>;
-  /**
-   * If an error can be associated to a particular field in the GraphQL result,
-   * it _must_ contain an entry with the key `path` that details the path of
-   * the response field which experienced the error. This allows clients to
-   * identify whether a null result is intentional or caused by a runtime error.
-   */
-  readonly path?: ReadonlyArray<string | number>;
-  /**
-   * Reserved for implementors to extend the protocol however they see fit,
-   * and hence there are no additional restrictions on its contents.
-   */
-  readonly extensions?: { [key: string]: unknown };
-}
diff --git a/src/error/index.ts b/src/error/index.ts
index 914a6dbe..b947c93f 100644
--- a/src/error/index.ts
+++ b/src/error/index.ts
@@ -1,8 +1,6 @@
-export { GraphQLError, printError } from './GraphQLError';
+export { GraphQLError, printError, formatError } from './GraphQLError';
+export type { GraphQLFormattedError } from './GraphQLError';
 
 export { syntaxError } from './syntaxError';
 
 export { locatedError } from './locatedError';
-
-export { formatError } from './formatError';
-export type { GraphQLFormattedError } from './formatError';
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 8429cae8..1ef3adc8 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -13,7 +13,7 @@ import { promiseForObject } from '../jsutils/promiseForObject';
 import { addPath, pathToArray } from '../jsutils/Path';
 import { isIterableObject } from '../jsutils/isIterableObject';
 
-import type { GraphQLFormattedError } from '../error/formatError';
+import type { GraphQLFormattedError } from '../error/GraphQLError';
 import { GraphQLError } from '../error/GraphQLError';
 import { locatedError } from '../error/locatedError';
 
diff --git a/src/type/definition.ts b/src/type/definition.ts
index 6e5f0fe4..42b2b42a 100644
--- a/src/type/definition.ts
+++ b/src/type/definition.ts
@@ -1648,6 +1648,7 @@ export class GraphQLInputObjectType {
       description: field.description,
       type: field.type,
       defaultValue: field.defaultValue,
+      deprecationReason: field.deprecationReason,
       extensions: field.extensions,
       astNode: field.astNode,
     }));


Test patch:
diff --git a/src/error/__tests__/GraphQLError-test.ts b/src/error/__tests__/GraphQLError-test.ts
index d4cdf92a..ca567971 100644
--- a/src/error/__tests__/GraphQLError-test.ts
+++ b/src/error/__tests__/GraphQLError-test.ts
@@ -9,7 +9,7 @@ import { Kind } from '../../language/kinds';
 import { parse } from '../../language/parser';
 import { Source } from '../../language/source';
 
-import { GraphQLError, printError } from '../GraphQLError';
+import { GraphQLError, printError, formatError } from '../GraphQLError';
 
 const source = new Source(dedent`
   {
@@ -204,3 +204,37 @@ describe('toString', () => {
     `);
   });
 });
+
+describe('toJSON', () => {
+  it('Deprecated: format an error using formatError', () => {
+    const error = new GraphQLError('Example Error');
+    expect(formatError(error)).to.deep.equal({
+      message: 'Example Error',
+    });
+  });
+
+  it('includes path', () => {
+    const error = new GraphQLError('msg', null, null, null, [
+      'path',
+      3,
+      'to',
+      'field',
+    ]);
+
+    expect(error.toJSON()).to.deep.equal({
+      message: 'msg',
+      path: ['path', 3, 'to', 'field'],
+    });
+  });
+
+  it('includes extension fields', () => {
+    const error = new GraphQLError('msg', null, null, null, null, null, {
+      foo: 'bar',
+    });
+
+    expect(error.toJSON()).to.deep.equal({
+      message: 'msg',
+      extensions: { foo: 'bar' },
+    });
+  });
+});
diff --git a/src/error/__tests__/formatError-test.ts b/src/error/__tests__/formatError-test.ts
deleted file mode 100644
index b0798c95..00000000
--- a/src/error/__tests__/formatError-test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { expect } from 'chai';
-import { describe, it } from 'mocha';
-
-import { formatError } from '../formatError';
-import { GraphQLError } from '../GraphQLError';
-
-describe('formatError: default error formatter', () => {
-  it('uses default message', () => {
-    // @ts-expect-error
-    const e = new GraphQLError();
-
-    expect(formatError(e)).to.deep.equal({
-      message: 'An unknown error occurred.',
-      path: undefined,
-      locations: undefined,
-    });
-  });
-
-  it('includes path', () => {
-    const e = new GraphQLError('msg', null, null, null, [
-      'path',
-      3,
-      'to',
-      'field',
-    ]);
-
-    expect(formatError(e)).to.deep.equal({
-      message: 'msg',
-      locations: undefined,
-      path: ['path', 3, 'to', 'field'],
-    });
-  });
-
-  it('includes extension fields', () => {
-    const e = new GraphQLError('msg', null, null, null, null, null, {
-      foo: 'bar',
-    });
-
-    expect(formatError(e)).to.deep.equal({
-      message: 'msg',
-      locations: undefined,
-      path: undefined,
-      extensions: { foo: 'bar' },
-    });
-  });
-
-  it('rejects null and undefined errors', () => {
-    // @ts-expect-error (formatError expects a value)
-    expect(() => formatError(undefined)).to.throw(
-      'Received null or undefined error.',
-    );
-
-    // @ts-expect-error (formatError expects a value)
-    expect(() => formatError(null)).to.throw(
-      'Received null or undefined error.',
-    );
-  });
-});
diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts
index 1cf4e4f3..7011937c 100644
--- a/src/type/__tests__/definition-test.ts
+++ b/src/type/__tests__/definition-test.ts
@@ -818,6 +818,22 @@ describe('Type System: Input Objects', () => {
       );
     });
   });
+
+  it('Deprecation reason is preserved on fields', () => {
+    const inputObjType = new GraphQLInputObjectType({
+      name: 'SomeInputObject',
+      fields: {
+        deprecatedField: {
+          type: ScalarType,
+          deprecationReason: 'not used anymore',
+        },
+      },
+    });
+    expect(inputObjType.toConfig()).to.have.nested.property(
+      'fields.deprecatedField.deprecationReason',
+      'not used anymore',
+    );
+  });
 });
 
 describe('Type System: List', () => {