From dfb204e4fdd8765bd2ca3b817a60978fab88bc6b Mon Sep 17 00:00:00 2001 From: ksmithson Date: Wed, 24 Oct 2018 11:23:32 -0700 Subject: [PATCH 1/3] Add deprecated directive to arguments and input values --- src/type/__tests__/introspection-test.js | 57 ++++++++++++++++++- src/type/definition.js | 15 ++++- src/type/directives.js | 7 ++- src/type/introspection.js | 49 ++++++++++++++-- src/utilities/__tests__/schemaPrinter-test.js | 16 ++++-- src/utilities/buildASTSchema.js | 5 +- 6 files changed, 132 insertions(+), 17 deletions(-) diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 532a19db3e3..bc77ad8b8a4 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -307,7 +307,17 @@ describe('Introspection', () => { }, { name: 'inputFields', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'LIST', name: null, @@ -432,7 +442,17 @@ describe('Introspection', () => { }, { name: 'args', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'NON_NULL', name: null, @@ -556,6 +576,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: [], @@ -853,7 +899,12 @@ describe('Introspection', () => { }, { name: 'deprecated', - locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], + locations: [ + 'FIELD_DEFINITION', + 'ENUM_VALUE', + 'ARGUMENT_DEFINITION', + 'INPUT_FIELD_DEFINITION', + ], args: [ { defaultValue: '"No longer supported"', diff --git a/src/type/definition.js b/src/type/definition.js index 675092d0a7a..d3847a4a82b 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -827,6 +827,7 @@ export type GraphQLArgumentConfig = {| type: GraphQLInputType, defaultValue?: mixed, description?: ?string, + deprecationReason?: ?string, astNode?: ?InputValueDefinitionNode, |}; @@ -855,6 +856,8 @@ export type GraphQLArgument = { type: GraphQLInputType, defaultValue?: mixed, description?: ?string, + isDeprecated?: boolean, + deprecationReason?: ?string, astNode?: ?InputValueDefinitionNode, }; @@ -1238,8 +1241,15 @@ function defineInputFieldMap( ); const resultFieldMap = Object.create(null); for (const fieldName of Object.keys(fieldMap)) { + const fieldConfig = fieldMap[fieldName]; + invariant( + !fieldConfig.hasOwnProperty('isDeprecated'), + `${config.name}.${fieldName} should provide "deprecationReason" ` + + 'instead of "isDeprecated".', + ); const field = { - ...fieldMap[fieldName], + ...fieldConfig, + isDeprecated: Boolean(fieldConfig.deprecationReason), name: fieldName, }; invariant( @@ -1264,6 +1274,7 @@ export type GraphQLInputFieldConfig = {| type: GraphQLInputType, defaultValue?: mixed, description?: ?string, + deprecationReason?: ?string, astNode?: ?InputValueDefinitionNode, |}; @@ -1274,6 +1285,8 @@ export type GraphQLInputField = { type: GraphQLInputType, defaultValue?: mixed, description?: ?string, + isDeprecated?: boolean, + deprecationReason?: ?string, astNode?: ?InputValueDefinitionNode, }; diff --git a/src/type/directives.js b/src/type/directives.js index e8a7b74fdbd..7679a5ead07 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -147,7 +147,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.ENUM_VALUE, + DirectiveLocation.ARGUMENT_DEFINITION, + DirectiveLocation.INPUT_FIELD_DEFINITION, + ], args: { reason: { type: GraphQLString, diff --git a/src/type/introspection.js b/src/type/introspection.js index 63630d95c94..17ee60d5d2f 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -232,7 +232,10 @@ export const __Type = new GraphQLObjectType({ fields: { type: GraphQLList(GraphQLNonNull(__Field)), args: { - includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + includeDeprecated: { + type: GraphQLBoolean, + defaultValue: false, + }, }, resolve(type, { includeDeprecated }) { if (isObjectType(type) || isInterfaceType(type)) { @@ -264,7 +267,10 @@ export const __Type = new GraphQLObjectType({ enumValues: { type: GraphQLList(GraphQLNonNull(__EnumValue)), args: { - includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + includeDeprecated: { + type: GraphQLBoolean, + defaultValue: false, + }, }, resolve(type, { includeDeprecated }) { if (isEnumType(type)) { @@ -278,9 +284,19 @@ export const __Type = new GraphQLObjectType({ }, inputFields: { type: GraphQLList(GraphQLNonNull(__InputValue)), - resolve(type) { + args: { + includeDeprecated: { + type: GraphQLBoolean, + defaultValue: false, + }, + }, + resolve(type, { includeDeprecated }) { if (isInputObjectType(type)) { - return objectValues(type.getFields()); + let values = objectValues(type.getFields()); + if (!includeDeprecated) { + values = values.filter(value => !value.deprecationReason); + } + return values; } }, }, @@ -307,7 +323,22 @@ export const __Field = new GraphQLObjectType({ }, args: { type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue))), - resolve: field => field.args || [], + args: { + includeDeprecated: { + type: GraphQLBoolean, + defaultValue: false, + }, + }, + // resolve: field => field.args || [], + resolve(field, { includeDeprecated }) { + let args = field.args || []; + + if (!includeDeprecated) { + args = args.filter(arg => !arg.deprecationReason); + } + + return args; + }, }, type: { type: GraphQLNonNull(__Type), @@ -353,6 +384,14 @@ export const __InputValue = new GraphQLObjectType({ ? null : print(astFromValue(inputVal.defaultValue, inputVal.type)), }, + isDeprecated: { + type: GraphQLNonNull(GraphQLBoolean), + resolve: obj => obj.isDeprecated, + }, + deprecationReason: { + type: GraphQLString, + resolve: obj => obj.deprecationReason, + }, }), }); diff --git a/src/utilities/__tests__/schemaPrinter-test.js b/src/utilities/__tests__/schemaPrinter-test.js index e4a1a6c1ad0..cfefdee0633 100644 --- a/src/utilities/__tests__/schemaPrinter-test.js +++ b/src/utilities/__tests__/schemaPrinter-test.js @@ -635,7 +635,7 @@ describe('Type System Printer', () => { (as specified by [CommonMark](https://commonmark.org/). """ reason: String = "No longer supported" - ) on FIELD_DEFINITION | ENUM_VALUE + ) on FIELD_DEFINITION | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION """ A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. @@ -734,7 +734,7 @@ describe('Type System Printer', () => { type __Field { name: String! description: String - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -754,6 +754,8 @@ describe('Type System Printer', () => { A GraphQL-formatted string representing the default value for this input value. """ defaultValue: String + isDeprecated: Boolean! + deprecationReason: String } """ @@ -800,7 +802,7 @@ describe('Type System Printer', () => { interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] + inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type } @@ -870,7 +872,7 @@ describe('Type System Printer', () => { # 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 | ENUM_VALUE | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION # A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document. # @@ -961,7 +963,7 @@ describe('Type System Printer', () => { type __Field { name: String! description: String - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -977,6 +979,8 @@ describe('Type System Printer', () => { # A GraphQL-formatted string representing the default value for this input value. defaultValue: String + isDeprecated: Boolean! + deprecationReason: String } # A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all @@ -1015,7 +1019,7 @@ describe('Type System Printer', () => { interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] + inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type } diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 2319e740b37..1ed4c07651f 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -441,7 +441,10 @@ export class ASTDefinitionBuilder { * deprecation reason. */ function getDeprecationReason( - node: EnumValueDefinitionNode | FieldDefinitionNode, + node: + | EnumValueDefinitionNode + | FieldDefinitionNode + | InputValueDefinitionNode, ): ?string { const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node); return deprecated && (deprecated.reason: any); From a22962e4660f17f3885c90c6f98cd2a748c8b8ce Mon Sep 17 00:00:00 2001 From: ksmithson Date: Wed, 24 Oct 2018 14:52:24 -0700 Subject: [PATCH 2/3] Add test and support for deprecated args --- src/type/definition.js | 2 ++ src/utilities/__tests__/buildASTSchema-test.js | 10 ++++++++++ src/utilities/buildASTSchema.js | 1 + src/utilities/schemaPrinter.js | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/type/definition.js b/src/type/definition.js index d3847a4a82b..91538810079 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -743,6 +743,8 @@ function defineFieldMap( description: arg.description === undefined ? null : arg.description, type: arg.type, defaultValue: arg.defaultValue, + isDeprecated: Boolean(arg.deprecationReason), + deprecationReason: arg.deprecationReason, astNode: arg.astNode, }; }); diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 9305cd31a92..f916c55cf9d 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -631,6 +631,8 @@ describe('Schema Builder', () => { 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 } `; const output = cycleOutput(body); @@ -658,6 +660,14 @@ describe('Schema Builder', () => { expect(rootFields.field2.isDeprecated).to.equal(true); expect(rootFields.field2.deprecationReason).to.equal('Because I said so'); + + const field3OldArg = rootFields.field3.args[0]; + expect(field3OldArg.isDeprecated).to.equal(true); + expect(field3OldArg.deprecationReason).to.equal('No longer supported'); + + const field4OldArg = rootFields.field4.args[0]; + expect(field4OldArg.isDeprecated).to.equal(true); + expect(field4OldArg.deprecationReason).to.equal('why not?'); }); it('Correctly assign AST nodes', () => { diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 1ed4c07651f..aa7cfc95959 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -309,6 +309,7 @@ export class ASTDefinitionBuilder { type, description: getDescription(value, this._options), defaultValue: valueFromAST(value.defaultValue, type), + deprecationReason: getDeprecationReason(value), astNode: value, }; } diff --git a/src/utilities/schemaPrinter.js b/src/utilities/schemaPrinter.js index aa9b01c096e..a1ada18cc2d 100644 --- a/src/utilities/schemaPrinter.js +++ b/src/utilities/schemaPrinter.js @@ -300,7 +300,7 @@ function printInputValue(arg) { if (!isInvalid(arg.defaultValue)) { argDecl += ` = ${print(astFromValue(arg.defaultValue, arg.type))}`; } - return argDecl; + return argDecl + printDeprecated(arg); } function printDirective(directive, options) { From f84a7df3c0c52931bb0f4253c3eccdeff22ffb1a Mon Sep 17 00:00:00 2001 From: ksmithson Date: Thu, 25 Oct 2018 07:00:12 -0700 Subject: [PATCH 3/3] Add test for input arguments being deprecated --- .../__tests__/buildASTSchema-test.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index f916c55cf9d..37a39d0280a 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -627,12 +627,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 } `; const output = cycleOutput(body); @@ -668,6 +675,20 @@ describe('Schema Builder', () => { const field4OldArg = rootFields.field4.args[0]; expect(field4OldArg.isDeprecated).to.equal(true); expect(field4OldArg.deprecationReason).to.equal('why not?'); + + const myInput = schema.getType('MyInput'); + const inputFields = myInput.getFields(); + + const newInput = inputFields.newInput; + expect(newInput.isDeprecated).to.equal(false); + + const oldInput = inputFields.oldInput; + expect(oldInput.isDeprecated).to.equal(true); + expect(oldInput.deprecationReason).to.equal('No longer supported'); + + const otherInput = inputFields.otherInput; + expect(otherInput.isDeprecated).to.equal(true); + expect(otherInput.deprecationReason).to.equal('Use newInput'); }); it('Correctly assign AST nodes', () => {