From 01bcc7d9be982226bcf56b8f983f38fd89dced1b Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Thu, 3 Sep 2020 23:38:20 +0300 Subject: [PATCH] Allow deprecating input fields and arguments (#2733) --- src/type/__tests__/definition-test.js | 3 + src/type/__tests__/directive-test.js | 2 + src/type/__tests__/introspection-test.js | 243 +++++++++++++++++- src/type/__tests__/predicate-test.js | 2 + src/type/__tests__/validation-test.js | 59 +++++ src/type/definition.d.ts | 4 + src/type/definition.js | 7 + src/type/directives.js | 8 +- src/type/introspection.js | 34 ++- src/type/validate.js | 46 +++- .../__tests__/buildASTSchema-test.js | 38 +++ src/utilities/__tests__/printSchema-test.js | 16 +- src/utilities/extendSchema.js | 7 +- src/utilities/printSchema.js | 19 +- .../__tests__/NoDeprecatedCustomRule-test.js | 152 ++++++++++- .../rules/custom/NoDeprecatedCustomRule.js | 70 ++++- 16 files changed, 664 insertions(+), 46 deletions(-) diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index 05f4c4569d..d63bc922aa 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -282,6 +282,7 @@ describe('Type System: Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, @@ -771,6 +772,7 @@ describe('Type System: Input Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, @@ -791,6 +793,7 @@ describe('Type System: Input Objects', () => { type: ScalarType, defaultValue: undefined, extensions: undefined, + deprecationReason: undefined, astNode: undefined, }, }); diff --git a/src/type/__tests__/directive-test.js b/src/type/__tests__/directive-test.js index cb11dc739e..0dc7de5132 100644 --- a/src/type/__tests__/directive-test.js +++ b/src/type/__tests__/directive-test.js @@ -37,6 +37,7 @@ describe('Type System: Directive', () => { description: undefined, type: GraphQLString, defaultValue: undefined, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, @@ -45,6 +46,7 @@ describe('Type System: Directive', () => { description: undefined, type: GraphQLInt, defaultValue: undefined, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 2dce63ac46..478cc9bd18 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -332,7 +332,17 @@ describe('Introspection', () => { }, { name: 'inputFields', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'LIST', name: null, @@ -450,7 +460,17 @@ describe('Introspection', () => { }, { name: 'args', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'NON_NULL', name: null, @@ -575,6 +595,32 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'isDeprecated', + args: [], + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + }, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'deprecationReason', + args: [], + type: { + kind: 'SCALAR', + name: 'String', + ofType: null, + }, + isDeprecated: false, + deprecationReason: null, + }, ], inputFields: null, interfaces: [], @@ -893,7 +939,12 @@ describe('Introspection', () => { { name: 'deprecated', isRepeatable: false, - locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], + locations: [ + 'FIELD_DEFINITION', + 'ARGUMENT_DEFINITION', + 'INPUT_FIELD_DEFINITION', + 'ENUM_VALUE', + ], args: [ { defaultValue: '"No longer supported"', @@ -1122,6 +1173,103 @@ describe('Introspection', () => { }); }); + it('identifies deprecated args', () => { + const schema = buildSchema(` + type Query { + someField( + nonDeprecated: String + deprecated: String @deprecated(reason: "Removed in 1.0") + deprecatedWithEmptyReason: String @deprecated(reason: "") + ): String + } + `); + + const source = ` + { + __type(name: "Query") { + fields { + args(includeDeprecated: true) { + name + isDeprecated, + deprecationReason + } + } + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + __type: { + fields: [ + { + args: [ + { + name: 'nonDeprecated', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'deprecated', + isDeprecated: true, + deprecationReason: 'Removed in 1.0', + }, + { + name: 'deprecatedWithEmptyReason', + isDeprecated: true, + deprecationReason: '', + }, + ], + }, + ], + }, + }, + }); + }); + + it('respects the includeDeprecated parameter for args', () => { + const schema = buildSchema(` + type Query { + someField( + nonDeprecated: String + deprecated: String @deprecated(reason: "Removed in 1.0") + ): String + } + `); + + const source = ` + { + __type(name: "Query") { + fields { + trueArgs: args(includeDeprecated: true) { + name + } + falseArgs: args(includeDeprecated: false) { + name + } + omittedArgs: args { + name + } + } + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + __type: { + fields: [ + { + trueArgs: [{ name: 'nonDeprecated' }, { name: 'deprecated' }], + falseArgs: [{ name: 'nonDeprecated' }], + omittedArgs: [{ name: 'nonDeprecated' }], + }, + ], + }, + }, + }); + }); + it('identifies deprecated enum values', () => { const schema = buildSchema(` enum SomeEnum { @@ -1224,6 +1372,95 @@ describe('Introspection', () => { }); }); + it('identifies deprecated for input fields', () => { + const schema = buildSchema(` + input SomeInputObject { + nonDeprecated: String + deprecated: String @deprecated(reason: "Removed in 1.0") + deprecatedWithEmptyReason: String @deprecated(reason: "") + } + + type Query { + someField(someArg: SomeInputObject): String + } + `); + + const source = ` + { + __type(name: "SomeInputObject") { + inputFields(includeDeprecated: true) { + name + isDeprecated, + deprecationReason + } + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + __type: { + inputFields: [ + { + name: 'nonDeprecated', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'deprecated', + isDeprecated: true, + deprecationReason: 'Removed in 1.0', + }, + { + name: 'deprecatedWithEmptyReason', + isDeprecated: true, + deprecationReason: '', + }, + ], + }, + }, + }); + }); + + it('respects the includeDeprecated parameter for input fields', () => { + const schema = buildSchema(` + input SomeInputObject { + nonDeprecated: String + deprecated: String @deprecated(reason: "Removed in 1.0") + } + + type Query { + someField(someArg: SomeInputObject): String + } + `); + + const source = ` + { + __type(name: "SomeInputObject") { + trueFields: inputFields(includeDeprecated: true) { + name + } + falseFields: inputFields(includeDeprecated: false) { + name + } + omittedFields: inputFields { + name + } + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + __type: { + trueFields: [{ name: 'nonDeprecated' }, { name: 'deprecated' }], + falseFields: [{ name: 'nonDeprecated' }], + omittedFields: [{ name: 'nonDeprecated' }], + }, + }, + }); + }); + it('fails as expected on the __type root field without an arg', () => { const schema = buildSchema(` type Query { diff --git a/src/type/__tests__/predicate-test.js b/src/type/__tests__/predicate-test.js index cfc384afa6..33c2c49f57 100644 --- a/src/type/__tests__/predicate-test.js +++ b/src/type/__tests__/predicate-test.js @@ -564,6 +564,7 @@ describe('Type predicates', () => { name: 'someArg', description: undefined, defaultValue: undefined, + deprecationReason: null, extensions: undefined, astNode: undefined, ...config, @@ -608,6 +609,7 @@ describe('Type predicates', () => { name: 'someInputField', description: undefined, defaultValue: undefined, + deprecationReason: null, extensions: undefined, astNode: undefined, ...config, diff --git a/src/type/__tests__/validation-test.js b/src/type/__tests__/validation-test.js index 27c92d4453..a3e35443da 100644 --- a/src/type/__tests__/validation-test.js +++ b/src/type/__tests__/validation-test.js @@ -898,6 +898,30 @@ describe('Type System: Input Objects must have fields', () => { }, ]); }); + + it('rejects an Input Object type with required argument that is deprecated', () => { + const schema = buildSchema(` + type Query { + field(arg: SomeInputObject): String + } + + input SomeInputObject { + badField: String! @deprecated + optionalField: String @deprecated + anotherOptionalField: String! = "" @deprecated + } + `); + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Required input field SomeInputObject.badField cannot be deprecated.', + locations: [ + { line: 7, column: 27 }, + { line: 7, column: 19 }, + ], + }, + ]); + }); }); describe('Type System: Enum types must be well defined', () => { @@ -1517,6 +1541,41 @@ describe('Type System: Arguments must have input types', () => { ]); }); + it('rejects an required argument that is deprecated', () => { + const schema = buildSchema(` + directive @BadDirective( + badArg: String! @deprecated + optionalArg: String @deprecated + anotherOptionalArg: String! = "" @deprecated + ) on FIELD + + type Query { + test( + badArg: String! @deprecated + optionalArg: String @deprecated + anotherOptionalArg: String! = "" @deprecated + ): String + } + `); + expect(validateSchema(schema)).to.deep.equal([ + { + message: + 'Required argument @BadDirective(badArg:) cannot be deprecated.', + locations: [ + { line: 3, column: 25 }, + { line: 3, column: 17 }, + ], + }, + { + message: 'Required argument Query.test(badArg:) cannot be deprecated.', + locations: [ + { line: 10, column: 27 }, + { line: 10, column: 19 }, + ], + }, + ]); + }); + it('rejects a non-input type as a field arg with locations', () => { const schema = buildSchema(` type Query { diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index 9d07f8822d..06799981f9 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -546,6 +546,7 @@ export interface GraphQLArgumentConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: any; + deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; } @@ -578,6 +579,7 @@ export interface GraphQLArgument { description: Maybe; type: GraphQLInputType; defaultValue: any; + deprecationReason: Maybe; extensions: Maybe>; astNode: Maybe; } @@ -919,6 +921,7 @@ export interface GraphQLInputFieldConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: any; + deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; } @@ -932,6 +935,7 @@ export interface GraphQLInputField { description?: Maybe; type: GraphQLInputType; defaultValue?: any; + deprecationReason: Maybe; extensions: Maybe>; astNode?: Maybe; } diff --git a/src/type/definition.js b/src/type/definition.js index ad3097e945..c8f92e21d3 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -854,6 +854,7 @@ function defineFieldMap( description: argConfig.description, type: argConfig.type, defaultValue: argConfig.defaultValue, + deprecationReason: argConfig.deprecationReason, extensions: argConfig.extensions && toObjMap(argConfig.extensions), astNode: argConfig.astNode, })); @@ -905,6 +906,7 @@ export function argsToArgsConfig( description: arg.description, type: arg.type, defaultValue: arg.defaultValue, + deprecationReason: arg.deprecationReason, extensions: arg.extensions, astNode: arg.astNode, }), @@ -981,6 +983,7 @@ export type GraphQLArgumentConfig = {| type: GraphQLInputType, defaultValue?: mixed, extensions?: ?ReadOnlyObjMapLike, + deprecationReason?: ?string, astNode?: ?InputValueDefinitionNode, |}; @@ -1012,6 +1015,7 @@ export type GraphQLArgument = {| description: ?string, type: GraphQLInputType, defaultValue: mixed, + deprecationReason: ?string, extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; @@ -1584,6 +1588,7 @@ function defineInputFieldMap( description: fieldConfig.description, type: fieldConfig.type, defaultValue: fieldConfig.defaultValue, + deprecationReason: fieldConfig.deprecationReason, extensions: fieldConfig.extensions && toObjMap(fieldConfig.extensions), astNode: fieldConfig.astNode, }; @@ -1603,6 +1608,7 @@ export type GraphQLInputFieldConfig = {| description?: ?string, type: GraphQLInputType, defaultValue?: mixed, + deprecationReason?: ?string, extensions?: ?ReadOnlyObjMapLike, astNode?: ?InputValueDefinitionNode, |}; @@ -1614,6 +1620,7 @@ export type GraphQLInputField = {| description: ?string, type: GraphQLInputType, defaultValue: mixed, + deprecationReason: ?string, extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; diff --git a/src/type/directives.js b/src/type/directives.js index a5cf7b7fc8..ff4cce6dd2 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -78,6 +78,7 @@ export class GraphQLDirective { description: argConfig.description, type: argConfig.type, defaultValue: argConfig.defaultValue, + deprecationReason: argConfig.deprecationReason, extensions: argConfig.extensions && toObjMap(argConfig.extensions), astNode: argConfig.astNode, })); @@ -178,7 +179,12 @@ export const DEFAULT_DEPRECATION_REASON = 'No longer supported'; export const GraphQLDeprecatedDirective = new GraphQLDirective({ name: 'deprecated', description: 'Marks an element of a GraphQL schema as no longer supported.', - locations: [DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE], + locations: [ + DirectiveLocation.FIELD_DEFINITION, + DirectiveLocation.ARGUMENT_DEFINITION, + DirectiveLocation.INPUT_FIELD_DEFINITION, + DirectiveLocation.ENUM_VALUE, + ], args: { reason: { type: GraphQLString, diff --git a/src/type/introspection.js b/src/type/introspection.js index b7cdd6ba81..77362ed02d 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -294,9 +294,18 @@ export const __Type = new GraphQLObjectType({ }, inputFields: { type: new GraphQLList(new GraphQLNonNull(__InputValue)), - resolve(type) { + args: { + includeDeprecated: { + type: GraphQLBoolean, + defaultValue: false, + }, + }, + resolve(type, { includeDeprecated }) { if (isInputObjectType(type)) { - return objectValues(type.getFields()); + const values = objectValues(type.getFields()); + return includeDeprecated + ? values + : values.filter((field) => field.deprecationReason == null); } }, }, @@ -326,7 +335,17 @@ export const __Field = new GraphQLObjectType({ type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(__InputValue)), ), - resolve: (field) => field.args, + args: { + includeDeprecated: { + type: GraphQLBoolean, + defaultValue: false, + }, + }, + resolve(field, { includeDeprecated }) { + return includeDeprecated + ? field.args + : field.args.filter((arg) => arg.deprecationReason == null); + }, }, type: { type: new GraphQLNonNull(__Type), @@ -371,6 +390,14 @@ export const __InputValue = new GraphQLObjectType({ return valueAST ? print(valueAST) : null; }, }, + isDeprecated: { + type: new GraphQLNonNull(GraphQLBoolean), + resolve: (field) => field.deprecationReason != null, + }, + deprecationReason: { + type: GraphQLString, + resolve: (obj) => obj.deprecationReason, + }, }: GraphQLFieldConfigMap), }); @@ -482,6 +509,7 @@ export const TypeMetaFieldDef: GraphQLField = { description: undefined, type: new GraphQLNonNull(GraphQLString), defaultValue: undefined, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, diff --git a/src/type/validate.js b/src/type/validate.js index 0f03a19ff5..a2ddfe4a43 100644 --- a/src/type/validate.js +++ b/src/type/validate.js @@ -9,6 +9,7 @@ import { locatedError } from '../error/locatedError'; import type { ASTNode, NamedTypeNode, + DirectiveNode, OperationTypeNode, } from '../language/ast'; @@ -24,8 +25,8 @@ import type { GraphQLInputObjectType, } from './definition'; import { assertSchema } from './schema'; -import { isDirective } from './directives'; import { isIntrospectionType } from './introspection'; +import { isDirective, GraphQLDeprecatedDirective } from './directives'; import { isObjectType, isInterfaceType, @@ -37,6 +38,7 @@ import { isInputType, isOutputType, isRequiredArgument, + isRequiredInputField, } from './definition'; /** @@ -182,6 +184,17 @@ function validateDirectives(context: SchemaValidationContext): void { arg.astNode, ); } + + if (isRequiredArgument(arg) && arg.deprecationReason != null) { + context.reportError( + `Required argument @${directive.name}(${arg.name}:) cannot be deprecated.`, + [ + getDeprecatedDirectiveNode(arg.astNode), + // istanbul ignore next (TODO need to write coverage tests) + arg.astNode?.type, + ], + ); + } } } } @@ -287,6 +300,17 @@ function validateFields( arg.astNode?.type, ); } + + if (isRequiredArgument(arg) && arg.deprecationReason != null) { + context.reportError( + `Required argument ${type.name}.${field.name}(${argName}:) cannot be deprecated.`, + [ + getDeprecatedDirectiveNode(arg.astNode), + // istanbul ignore next (TODO need to write coverage tests) + arg.astNode?.type, + ], + ); + } } } } @@ -522,6 +546,17 @@ function validateInputFields( field.astNode?.type, ); } + + if (isRequiredInputField(field) && field.deprecationReason != null) { + context.reportError( + `Required input field ${inputObj.name}.${field.name} cannot be deprecated.`, + [ + getDeprecatedDirectiveNode(field.astNode), + // istanbul ignore next (TODO need to write coverage tests) + field.astNode?.type, + ], + ); + } } } @@ -623,3 +658,12 @@ function getUnionMemberTypeNodes( (typeNode) => typeNode.name.value === typeName, ); } + +function getDeprecatedDirectiveNode( + definitionNode: ?{ +directives?: $ReadOnlyArray, ... }, +): ?DirectiveNode { + // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') + return definitionNode?.directives?.find( + (node) => node.name.value === GraphQLDeprecatedDirective.name, + ); +} diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 9feb0645c6..f2a92c96a5 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -776,10 +776,19 @@ describe('Schema Builder', () => { OTHER_VALUE @deprecated(reason: "Terrible reasons") } + input MyInput { + oldInput: String @deprecated + otherInput: String @deprecated(reason: "Use newInput") + newInput: String + } + type Query { field1: String @deprecated field2: Int @deprecated(reason: "Because I said so") enum: MyEnum + field3(oldArg: String @deprecated, arg: String): String + field4(oldArg: String @deprecated(reason: "Why not?"), arg: String): String + field5(arg: MyInput): String } `; expect(cycleSDL(sdl)).to.equal(sdl); @@ -812,6 +821,35 @@ describe('Schema Builder', () => { isDeprecated: true, deprecationReason: 'Because I said so', }); + + const inputFields = assertInputObjectType( + schema.getType('MyInput'), + ).getFields(); + + const newInput = inputFields.newInput; + expect(newInput).to.include({ + deprecationReason: undefined, + }); + + const oldInput = inputFields.oldInput; + expect(oldInput).to.include({ + deprecationReason: 'No longer supported', + }); + + const otherInput = inputFields.otherInput; + expect(otherInput).to.include({ + deprecationReason: 'Use newInput', + }); + + const field3OldArg = rootFields.field3.args[0]; + expect(field3OldArg).to.include({ + deprecationReason: 'No longer supported', + }); + + const field4OldArg = rootFields.field4.args[0]; + expect(field4OldArg).to.include({ + deprecationReason: 'Why not?', + }); }); it('Supports @specifiedBy', () => { diff --git a/src/utilities/__tests__/printSchema-test.js b/src/utilities/__tests__/printSchema-test.js index 01cde61706..df064c3724 100644 --- a/src/utilities/__tests__/printSchema-test.js +++ b/src/utilities/__tests__/printSchema-test.js @@ -633,7 +633,7 @@ describe('Type System Printer', () => { Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/). """ reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE """Exposes a URL that specifies the behaviour of this scalar.""" directive @specifiedBy( @@ -681,7 +681,7 @@ describe('Type System Printer', () => { interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] + inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type } @@ -724,7 +724,7 @@ describe('Type System Printer', () => { type __Field { name: String! description: String - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -742,6 +742,8 @@ describe('Type System Printer', () => { A GraphQL-formatted string representing the default value for this input value. """ defaultValue: String + isDeprecated: Boolean! + deprecationReason: String } """ @@ -854,7 +856,7 @@ describe('Type System Printer', () => { directive @deprecated( # Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/). reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE # Exposes a URL that specifies the behaviour of this scalar. directive @specifiedBy( @@ -894,7 +896,7 @@ describe('Type System Printer', () => { interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] + inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type } @@ -929,7 +931,7 @@ describe('Type System Printer', () => { type __Field { name: String! description: String - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -943,6 +945,8 @@ describe('Type System Printer', () => { # A GraphQL-formatted string representing the default value for this input value. defaultValue: String + isDeprecated: Boolean! + deprecationReason: String } # One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string. diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index a3254c61a9..b7d3bfcfd0 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -518,6 +518,7 @@ export function extendSchemaImpl( type, description: getDescription(arg, options), defaultValue: valueFromAST(arg.defaultValue, type), + deprecationReason: getDeprecationReason(arg), astNode: arg, }; } @@ -544,6 +545,7 @@ export function extendSchemaImpl( type, description: getDescription(field, options), defaultValue: valueFromAST(field.defaultValue, type), + deprecationReason: getDeprecationReason(field), astNode: field, }; } @@ -712,7 +714,10 @@ const stdTypeMap = keyMap( * deprecation reason. */ function getDeprecationReason( - node: EnumValueDefinitionNode | FieldDefinitionNode, + node: + | EnumValueDefinitionNode + | FieldDefinitionNode + | InputValueDefinitionNode, ): ?string { const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node); return (deprecated?.reason: any); diff --git a/src/utilities/printSchema.js b/src/utilities/printSchema.js index cc3d0716b7..c76f01bef8 100644 --- a/src/utilities/printSchema.js +++ b/src/utilities/printSchema.js @@ -10,12 +10,10 @@ import type { GraphQLSchema } from '../type/schema'; import type { GraphQLDirective } from '../type/directives'; import type { GraphQLNamedType, - GraphQLField, GraphQLArgument, GraphQLInputField, GraphQLScalarType, GraphQLEnumType, - GraphQLEnumValue, GraphQLObjectType, GraphQLInterfaceType, GraphQLUnionType, @@ -234,7 +232,7 @@ function printEnum(type: GraphQLEnumType, options): string { printDescription(options, value, ' ', !i) + ' ' + value.name + - printDeprecated(value), + printDeprecated(value.deprecationReason), ); return ( @@ -264,7 +262,7 @@ function printFields( printArgs(options, f.args, ' ') + ': ' + String(f.type) + - printDeprecated(f), + printDeprecated(f.deprecationReason), ); return printBlock(fields); } @@ -310,7 +308,7 @@ function printInputValue(arg: GraphQLInputField): string { if (defaultAST) { argDecl += ` = ${print(defaultAST)}`; } - return argDecl; + return argDecl + printDeprecated(arg.deprecationReason); } function printDirective(directive: GraphQLDirective, options): string { @@ -325,15 +323,12 @@ function printDirective(directive: GraphQLDirective, options): string { ); } -function printDeprecated( - fieldOrEnumVal: GraphQLEnumValue | GraphQLField, -): string { - const { deprecationReason } = fieldOrEnumVal; - if (deprecationReason == null) { +function printDeprecated(reason: ?string): string { + if (reason == null) { return ''; } - const reasonAST = astFromValue(deprecationReason, GraphQLString); - if (reasonAST && deprecationReason !== DEFAULT_DEPRECATION_REASON) { + const reasonAST = astFromValue(reason, GraphQLString); + if (reasonAST && reason !== DEFAULT_DEPRECATION_REASON) { return ' @deprecated(reason: ' + print(reasonAST) + ')'; } return ' @deprecated'; diff --git a/src/validation/__tests__/NoDeprecatedCustomRule-test.js b/src/validation/__tests__/NoDeprecatedCustomRule-test.js index 7a148c9bb3..12d66eafc2 100644 --- a/src/validation/__tests__/NoDeprecatedCustomRule-test.js +++ b/src/validation/__tests__/NoDeprecatedCustomRule-test.js @@ -47,7 +47,7 @@ describe('Validate: no deprecated', () => { } fragment UnknownFragment on UnknownType { - unknownField + deprecatedField } `); }); @@ -71,6 +71,151 @@ describe('Validate: no deprecated', () => { }); }); + describe('no deprecated arguments on fields', () => { + const { expectValid, expectErrors } = buildAssertion(` + type Query { + someField( + normalArg: String, + deprecatedArg: String @deprecated(reason: "Some arg reason."), + ): String + } + `); + + it('ignores arguments that are not deprecated', () => { + expectValid(` + { + normalField(normalArg: "") + } + `); + }); + + it('ignores unknown arguments', () => { + expectValid(` + { + someField(unknownArg: "") + unknownField(deprecatedArg: "") + } + `); + }); + + it('reports error when a deprecated argument is used', () => { + expectErrors(` + { + someField(deprecatedArg: "") + } + `).to.deep.equal([ + { + message: + 'Field "Query.someField" argument "deprecatedArg" is deprecated. Some arg reason.', + locations: [{ line: 3, column: 21 }], + }, + ]); + }); + }); + + describe('no deprecated arguments on directives', () => { + const { expectValid, expectErrors } = buildAssertion(` + type Query { + someField: String + } + + directive @someDirective( + normalArg: String, + deprecatedArg: String @deprecated(reason: "Some arg reason."), + ) on FIELD + `); + + it('ignores arguments that are not deprecated', () => { + expectValid(` + { + someField @someDirective(normalArg: "") + } + `); + }); + + it('ignores unknown arguments', () => { + expectValid(` + { + someField @someDirective(unknownArg: "") + someField @unknownDirective(deprecatedArg: "") + } + `); + }); + + it('reports error when a deprecated argument is used', () => { + expectErrors(` + { + someField @someDirective(deprecatedArg: "") + } + `).to.deep.equal([ + { + message: + 'Directive "@someDirective" argument "deprecatedArg" is deprecated. Some arg reason.', + locations: [{ line: 3, column: 36 }], + }, + ]); + }); + }); + + describe('no deprecated input fields', () => { + const { expectValid, expectErrors } = buildAssertion(` + input InputType { + normalField: String + deprecatedField: String @deprecated(reason: "Some input field reason.") + } + + type Query { + someField(someArg: InputType): String + } + + directive @someDirective(someArg: InputType) on FIELD + `); + + it('ignores input fields that are not deprecated', () => { + expectValid(` + { + someField( + someArg: { normalField: "" } + ) @someDirective(someArg: { normalField: "" }) + } + `); + }); + + it('ignores unknown input fields', () => { + expectValid(` + { + someField( + someArg: { unknownField: "" } + ) + + someField( + unknownArg: { unknownField: "" } + ) + + unknownField( + unknownArg: { unknownField: "" } + ) + } + `); + }); + + it('reports error when a deprecated input field is used', () => { + const message = + 'The input field InputType.deprecatedField is deprecated. Some input field reason.'; + + expectErrors(` + { + someField( + someArg: { deprecatedField: "" } + ) @someDirective(someArg: { deprecatedField: "" }) + } + `).to.deep.equal([ + { message, locations: [{ line: 4, column: 24 }] }, + { message, locations: [{ line: 5, column: 39 }] }, + ]); + }); + }); + describe('no deprecated enum values', () => { const { expectValid, expectErrors } = buildAssertion(` enum EnumType { @@ -118,14 +263,9 @@ describe('Validate: no deprecated', () => { ) { someField(enumArg: DEPRECATED_VALUE) } - - fragment QueryFragment on Query { - someField(enumArg: DEPRECATED_VALUE) - } `).to.deep.equal([ { message, locations: [{ line: 3, column: 33 }] }, { message, locations: [{ line: 5, column: 30 }] }, - { message, locations: [{ line: 9, column: 30 }] }, ]); }); }); diff --git a/src/validation/rules/custom/NoDeprecatedCustomRule.js b/src/validation/rules/custom/NoDeprecatedCustomRule.js index 597f51654f..7fe6598bc4 100644 --- a/src/validation/rules/custom/NoDeprecatedCustomRule.js +++ b/src/validation/rules/custom/NoDeprecatedCustomRule.js @@ -1,9 +1,10 @@ +import invariant from '../../../jsutils/invariant'; + import { GraphQLError } from '../../../error/GraphQLError'; -import type { EnumValueNode, FieldNode } from '../../../language/ast'; import type { ASTVisitor } from '../../../language/visitor'; -import { getNamedType } from '../../../type/definition'; +import { getNamedType, isInputObjectType } from '../../../type/definition'; import type { ValidationContext } from '../../ValidationContext'; @@ -19,27 +20,70 @@ import type { ValidationContext } from '../../ValidationContext'; */ export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { return { - Field(node: FieldNode) { + Field(node) { const fieldDef = context.getFieldDef(); - const parentType = context.getParentType(); - if (parentType && fieldDef?.deprecationReason != null) { + const deprecationReason = fieldDef?.deprecationReason; + if (fieldDef && deprecationReason != null) { + const parentType = context.getParentType(); + invariant(parentType != null); context.reportError( new GraphQLError( - `The field ${parentType.name}.${fieldDef.name} is deprecated. ` + - fieldDef.deprecationReason, + `The field ${parentType.name}.${fieldDef.name} is deprecated. ${deprecationReason}`, node, ), ); } }, - EnumValue(node: EnumValueNode) { - const type = getNamedType(context.getInputType()); - const enumValue = context.getEnumValue(); - if (type && enumValue?.deprecationReason != null) { + Argument(node) { + const argDef = context.getArgument(); + const deprecationReason = argDef?.deprecationReason; + if (argDef && deprecationReason != null) { + const directiveDef = context.getDirective(); + if (directiveDef != null) { + context.reportError( + new GraphQLError( + `Directive "@${directiveDef.name}" argument "${argDef.name}" is deprecated. ${deprecationReason}`, + node, + ), + ); + } else { + const parentType = context.getParentType(); + const fieldDef = context.getFieldDef(); + invariant(parentType != null && fieldDef != null); + context.reportError( + new GraphQLError( + `Field "${parentType.name}.${fieldDef.name}" argument "${argDef.name}" is deprecated. ${deprecationReason}`, + node, + ), + ); + } + } + }, + ObjectField(node) { + const inputObjectDef = getNamedType(context.getParentInputType()); + if (isInputObjectType(inputObjectDef)) { + const inputFieldDef = inputObjectDef.getFields()[node.name.value]; + // flowlint-next-line unnecessary-optional-chain:off + const deprecationReason = inputFieldDef?.deprecationReason; + if (deprecationReason != null) { + context.reportError( + new GraphQLError( + `The input field ${inputObjectDef.name}.${inputFieldDef.name} is deprecated. ${deprecationReason}`, + node, + ), + ); + } + } + }, + EnumValue(node) { + const enumValueDef = context.getEnumValue(); + const deprecationReason = enumValueDef?.deprecationReason; + if (enumValueDef && deprecationReason != null) { + const enumTypeDef = getNamedType(context.getInputType()); + invariant(enumTypeDef != null); context.reportError( new GraphQLError( - `The enum value "${type.name}.${enumValue.name}" is deprecated. ` + - enumValue.deprecationReason, + `The enum value "${enumTypeDef.name}.${enumValueDef.name}" is deprecated. ${deprecationReason}`, node, ), );