diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index c54b589611..a86b2b9d4c 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -281,6 +281,8 @@ describe('Type System: Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + isDeprecated: false, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, @@ -772,6 +774,8 @@ describe('Type System: Input Objects', () => { description: undefined, type: ScalarType, defaultValue: undefined, + isDeprecated: false, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, @@ -792,6 +796,8 @@ describe('Type System: Input Objects', () => { type: ScalarType, defaultValue: undefined, extensions: undefined, + isDeprecated: false, + deprecationReason: undefined, astNode: undefined, }, }); diff --git a/src/type/__tests__/directive-test.js b/src/type/__tests__/directive-test.js index 178ca4db91..13099bc63c 100644 --- a/src/type/__tests__/directive-test.js +++ b/src/type/__tests__/directive-test.js @@ -39,6 +39,8 @@ describe('Type System: Directive', () => { description: undefined, type: GraphQLString, defaultValue: undefined, + isDeprecated: false, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, @@ -47,6 +49,8 @@ describe('Type System: Directive', () => { description: undefined, type: GraphQLInt, defaultValue: undefined, + isDeprecated: false, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, diff --git a/src/type/__tests__/introspection-test.js b/src/type/__tests__/introspection-test.js index 842072de1b..1d731027b9 100644 --- a/src/type/__tests__/introspection-test.js +++ b/src/type/__tests__/introspection-test.js @@ -342,7 +342,17 @@ describe('Introspection', () => { }, { name: 'inputFields', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'LIST', name: null, @@ -460,7 +470,17 @@ describe('Introspection', () => { }, { name: 'args', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'NON_NULL', name: null, @@ -585,6 +605,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: [], @@ -903,7 +949,12 @@ describe('Introspection', () => { { name: 'deprecated', isRepeatable: false, - locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], + locations: [ + 'FIELD_DEFINITION', + 'ARGUMENT_DEFINITION', + 'ENUM_VALUE', + 'INPUT_FIELD_DEFINITION', + ], args: [ { defaultValue: '"No longer supported"', diff --git a/src/type/__tests__/predicate-test.js b/src/type/__tests__/predicate-test.js index 40dcca7ee8..7f09752254 100644 --- a/src/type/__tests__/predicate-test.js +++ b/src/type/__tests__/predicate-test.js @@ -549,6 +549,8 @@ describe('Type predicates', () => { name: 'someArg', description: undefined, defaultValue: undefined, + isDeprecated: false, + deprecationReason: null, extensions: undefined, astNode: undefined, ...config, @@ -593,6 +595,8 @@ describe('Type predicates', () => { name: 'someInputField', description: undefined, defaultValue: undefined, + isDeprecated: false, + deprecationReason: null, extensions: undefined, astNode: undefined, ...config, diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index aa3177a69d..c4626e8a6b 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; } @@ -576,6 +577,8 @@ export interface GraphQLArgument { description: Maybe; type: GraphQLInputType; defaultValue: any; + isDeprecated: boolean; + deprecationReason: Maybe; extensions: Maybe>; astNode: Maybe; } @@ -915,6 +918,7 @@ export interface GraphQLInputFieldConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: any; + deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; } @@ -928,6 +932,8 @@ export interface GraphQLInputField { description?: Maybe; type: GraphQLInputType; defaultValue?: any; + isDeprecated: boolean; + deprecationReason: Maybe; extensions: Maybe>; astNode?: Maybe; } diff --git a/src/type/definition.js b/src/type/definition.js index ab94313263..27c0902e4d 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -848,14 +848,23 @@ function defineFieldMap( `${config.name}.${fieldName} args must be an object with argument names as keys.`, ); - const args = objectEntries(argsConfig).map(([argName, argConfig]) => ({ - name: argName, - description: argConfig.description, - type: argConfig.type, - defaultValue: argConfig.defaultValue, - extensions: argConfig.extensions && toObjMap(argConfig.extensions), - astNode: argConfig.astNode, - })); + const args = objectEntries(argsConfig).map(([argName, argConfig]) => { + devAssert( + !('isDeprecated' in argConfig), + `${config.name}.${fieldName}.${argName} should provide "deprecationReason" instead of "isDeprecated".`, + ); + + return { + name: argName, + description: argConfig.description, + type: argConfig.type, + defaultValue: argConfig.defaultValue, + isDeprecated: argConfig.deprecationReason != null, + deprecationReason: argConfig.deprecationReason, + extensions: argConfig.extensions && toObjMap(argConfig.extensions), + astNode: argConfig.astNode, + }; + }); return { name: fieldName, @@ -902,6 +911,7 @@ export function argsToArgsConfig( description: arg.description, type: arg.type, defaultValue: arg.defaultValue, + deprecationReason: arg.deprecationReason, extensions: arg.extensions, astNode: arg.astNode, }), @@ -978,6 +988,7 @@ export type GraphQLArgumentConfig = {| type: GraphQLInputType, defaultValue?: mixed, extensions?: ?ReadOnlyObjMapLike, + deprecationReason?: ?string, astNode?: ?InputValueDefinitionNode, |}; @@ -1007,6 +1018,8 @@ export type GraphQLArgument = {| description: ?string, type: GraphQLInputType, defaultValue: mixed, + isDeprecated: boolean, + deprecationReason: ?string, extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; @@ -1566,17 +1579,24 @@ function defineInputFieldMap( isPlainObj(fieldMap), `${config.name} fields must be an object with field names as keys or a function which returns such an object.`, ); + return mapValue(fieldMap, (fieldConfig, fieldName) => { devAssert( !('resolve' in fieldConfig), `${config.name}.${fieldName} field has a resolve property, but Input Types cannot define resolvers.`, ); + devAssert( + !('isDeprecated' in fieldConfig), + `${config.name}.${fieldName} should provide "deprecationReason" instead of "isDeprecated".`, + ); return { name: fieldName, description: fieldConfig.description, type: fieldConfig.type, defaultValue: fieldConfig.defaultValue, + isDeprecated: fieldConfig.deprecationReason != null, + deprecationReason: fieldConfig.deprecationReason, extensions: fieldConfig.extensions && toObjMap(fieldConfig.extensions), astNode: fieldConfig.astNode, }; @@ -1596,6 +1616,7 @@ export type GraphQLInputFieldConfig = {| description?: ?string, type: GraphQLInputType, defaultValue?: mixed, + deprecationReason?: ?string, extensions?: ?ReadOnlyObjMapLike, astNode?: ?InputValueDefinitionNode, |}; @@ -1607,6 +1628,8 @@ export type GraphQLInputField = {| description: ?string, type: GraphQLInputType, defaultValue: mixed, + isDeprecated: boolean, + deprecationReason: ?string, extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; diff --git a/src/type/directives.js b/src/type/directives.js index b5074b8266..bb4f57abb9 100644 --- a/src/type/directives.js +++ b/src/type/directives.js @@ -75,14 +75,23 @@ export class GraphQLDirective { `@${config.name} args must be an object with argument names as keys.`, ); - this.args = objectEntries(args).map(([argName, argConfig]) => ({ - name: argName, - description: argConfig.description, - type: argConfig.type, - defaultValue: argConfig.defaultValue, - extensions: argConfig.extensions && toObjMap(argConfig.extensions), - astNode: argConfig.astNode, - })); + this.args = objectEntries(args).map(([argName, argConfig]) => { + devAssert( + !('isDeprecated' in argConfig), + `@${config.name}.${argName} should provide "deprecationReason" instead of "isDeprecated".`, + ); + + return { + name: argName, + description: argConfig.description, + type: argConfig.type, + defaultValue: argConfig.defaultValue, + isDeprecated: argConfig.deprecationReason != null, + deprecationReason: argConfig.deprecationReason, + extensions: argConfig.extensions && toObjMap(argConfig.extensions), + astNode: argConfig.astNode, + }; + }); } toConfig(): {| @@ -180,7 +189,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.ENUM_VALUE, + DirectiveLocation.INPUT_FIELD_DEFINITION, + ], args: { reason: { type: GraphQLString, diff --git a/src/type/introspection.js b/src/type/introspection.js index be86f4e5ac..ef2cf90b49 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -293,9 +293,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; } }, }, @@ -323,7 +333,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), @@ -368,6 +393,14 @@ export const __InputValue = new GraphQLObjectType({ return valueAST ? print(valueAST) : null; }, }, + isDeprecated: { + type: GraphQLNonNull(GraphQLBoolean), + resolve: (obj) => obj.isDeprecated, + }, + deprecationReason: { + type: GraphQLString, + resolve: (obj) => obj.deprecationReason, + }, }: GraphQLFieldConfigMap), }); @@ -479,6 +512,8 @@ export const TypeMetaFieldDef: GraphQLField = { description: undefined, type: GraphQLNonNull(GraphQLString), defaultValue: undefined, + isDeprecated: false, + deprecationReason: undefined, extensions: undefined, astNode: undefined, }, diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 2bb6a32e21..7d95c2715a 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -742,10 +742,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); @@ -778,6 +787,39 @@ 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({ + isDeprecated: false, + }); + + const oldInput = inputFields.oldInput; + expect(oldInput).to.include({ + isDeprecated: true, + deprecationReason: 'No longer supported', + }); + + const otherInput = inputFields.otherInput; + expect(otherInput).to.include({ + isDeprecated: true, + deprecationReason: 'Use newInput', + }); + + const field3OldArg = rootFields.field3.args[0]; + expect(field3OldArg).to.include({ + isDeprecated: true, + deprecationReason: 'No longer supported', + }); + + const field4OldArg = rootFields.field4.args[0]; + expect(field4OldArg).to.include({ + isDeprecated: true, + deprecationReason: 'Why not?', + }); }); it('Supports @specifiedBy', () => { diff --git a/src/utilities/__tests__/printSchema-test.js b/src/utilities/__tests__/printSchema-test.js index 6b5bbd7414..e5d460f8e5 100644 --- a/src/utilities/__tests__/printSchema-test.js +++ b/src/utilities/__tests__/printSchema-test.js @@ -629,7 +629,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 | ENUM_VALUE | INPUT_FIELD_DEFINITION """Exposes a URL that specifies the behaviour of this scalar.""" directive @specifiedBy( @@ -677,7 +677,7 @@ describe('Type System Printer', () => { interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] + inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type } @@ -720,7 +720,7 @@ describe('Type System Printer', () => { type __Field { name: String! description: String - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -738,6 +738,8 @@ describe('Type System Printer', () => { A GraphQL-formatted string representing the default value for this input value. """ defaultValue: String + isDeprecated: Boolean! + deprecationReason: String } """ @@ -850,7 +852,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 | ENUM_VALUE | INPUT_FIELD_DEFINITION # Exposes a URL that specifies the behaviour of this scalar. directive @specifiedBy( @@ -890,7 +892,7 @@ describe('Type System Printer', () => { interfaces: [__Type!] possibleTypes: [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] - inputFields: [__InputValue!] + inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type } @@ -925,7 +927,7 @@ describe('Type System Printer', () => { type __Field { name: String! description: String - args: [__InputValue!]! + args(includeDeprecated: Boolean = false): [__InputValue!]! type: __Type! isDeprecated: Boolean! deprecationReason: String @@ -939,6 +941,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 2950441576..3bb6a088e6 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -512,6 +512,7 @@ export function extendSchemaImpl( type, description: getDescription(arg, options), defaultValue: valueFromAST(arg.defaultValue, type), + deprecationReason: getDeprecationReason(arg), astNode: arg, }; } @@ -538,6 +539,7 @@ export function extendSchemaImpl( type, description: getDescription(field, options), defaultValue: valueFromAST(field.defaultValue, type), + deprecationReason: getDeprecationReason(field), astNode: field, }; } @@ -706,7 +708,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 73e6afb277..0ce13037d6 100644 --- a/src/utilities/printSchema.js +++ b/src/utilities/printSchema.js @@ -232,7 +232,7 @@ function printEnum(type: GraphQLEnumType, options): string { printDescription(options, value, ' ', !i) + ' ' + value.name + - printDeprecated(value), + printDeprecated(value.deprecationReason), ); return ( @@ -259,7 +259,7 @@ function printFields(options, type) { printArgs(options, f.args, ' ') + ': ' + String(f.type) + - printDeprecated(f), + printDeprecated(f.deprecationReason), ); return printBlock(fields); } @@ -301,7 +301,7 @@ function printInputValue(arg) { if (defaultAST) { argDecl += ` = ${print(defaultAST)}`; } - return argDecl; + return argDecl + printDeprecated(arg.deprecationReason); } function printDirective(directive, options) { @@ -316,11 +316,10 @@ function printDirective(directive, options) { ); } -function printDeprecated(fieldOrEnumVal) { - if (!fieldOrEnumVal.isDeprecated) { +function printDeprecated(reason) { + if (reason == null) { return ''; } - const reason = fieldOrEnumVal.deprecationReason; const reasonAST = astFromValue(reason, GraphQLString); if (reasonAST && reason !== DEFAULT_DEPRECATION_REASON) { return ' @deprecated(reason: ' + print(reasonAST) + ')';