diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index 6c540256a6..30e5213ced 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -186,6 +186,20 @@ describe('Type System: Objects', () => { }); }); + it('defines a deprecated object type', () => { + const DeprecatedType = new GraphQLObjectType({ + name: 'foo', + fields: { + bar: { + type: ScalarType, + }, + }, + deprecationReason: 'A terrible reason', + }); + + expect(DeprecatedType.deprecationReason).to.equal('A terrible reason'); + }); + it('accepts an Object type with a field function', () => { const objType = new GraphQLObjectType({ name: 'SomeObject', diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 1fa0518dd0..21d641a90a 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -96,7 +96,17 @@ describe('Introspection', () => { }, { name: 'types', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'NON_NULL', name: null, @@ -286,7 +296,17 @@ describe('Introspection', () => { }, { name: 'possibleTypes', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'LIST', name: null, @@ -372,6 +392,28 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'isDeprecated', + args: [], + type: { + 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: [], @@ -956,6 +998,7 @@ describe('Introspection', () => { 'ARGUMENT_DEFINITION', 'INPUT_FIELD_DEFINITION', 'ENUM_VALUE', + 'OBJECT', ], args: [ { @@ -1231,6 +1274,153 @@ describe('Introspection', () => { }); }); + it('identifies deprecated objects', () => { + const schema = buildSchema(` + type Query { + dragon: [Dragon] + } + type Dragon @deprecated(reason: "No longer known to exist") { + name: String + } + `); + + const source = ` + { + __schema { + types(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + dragon: __type(name: "Dragon") { + name + isDeprecated + deprecationReason + } + } + `; + + interface IntrospectionResponse { + __schema: { + types: [ + { name: string; isDeprecated: boolean; deprecationReason: string }, + ]; + }; + dragon: { name: string; isDeprecated: boolean; deprecationReason: string }; + } + + const resp = graphqlSync({ schema, source }) + .data as unknown as IntrospectionResponse; + + expect(resp.__schema.types).to.deep.include.members([ + { + name: 'Dragon', + isDeprecated: true, + deprecationReason: 'No longer known to exist', + }, + ]); + expect(resp.dragon).to.deep.equal({ + name: 'Dragon', + isDeprecated: true, + deprecationReason: 'No longer known to exist', + }); + }); + + it('respects the includeDeprecated parameter for types', () => { + const schema = buildSchema(` + type Query { + dragon: [Dragon] + } + type Dragon @deprecated(reason: "No longer known to exist") { + name: String + } + `); + + const source = ` + { + __schema { + trueTypes: types(includeDeprecated: true) { + name + } + falseTypes: types(includeDeprecated: false) { + name + } + omittedTypes: types { + name + } + } + } + `; + + interface IntrospectionResponse { + __schema: { + trueTypes: [{ name: string }]; + falseTypes: [{ name: string }]; + omittedTypes: [{ name: string }]; + }; + } + + const response = graphqlSync({ schema, source }) + .data as unknown as IntrospectionResponse; + expect(response.__schema.trueTypes).to.deep.include.members([ + { name: 'Dragon' }, + ]); + expect(response.__schema.falseTypes).to.not.deep.include.members([ + { name: 'Dragon' }, + ]); + expect(response.__schema.omittedTypes).to.not.deep.include.members([ + { name: 'Dragon' }, + ]); + }); + + it('respects the includeDeprecated parameter for possibleTypes', () => { + const schema = buildSchema(` + type Query { + animals: [Animal] + } + + interface Animal { + name: String + } + + type Dog implements Animal { + name: String + } + + type Dragon implements Animal @deprecated(reason: "No longer known to exist") { + name: String + } + `); + + const source = ` + { + animal: __type(name: "Animal") { + trueTypes: possibleTypes(includeDeprecated: true) { + name + } + falseTypes: possibleTypes(includeDeprecated: false) { + name + } + omittedTypes: possibleTypes { + name + } + } + } + `; + + const result = graphqlSync({ schema, source }); + const animal = result.data?.animal; + // @ts-expect-error + expect(animal.trueTypes).to.deep.include.members([{ name: 'Dragon' }]); + // @ts-expect-error + expect(animal.falseTypes).to.not.deep.include.members([{ name: 'Dragon' }]); + // @ts-expect-error + expect(animal.omittedTypes).to.not.deep.include.members([ + { name: 'Dragon' }, + ]); + }); + it('identifies deprecated args', () => { const schema = buildSchema(` type Query { diff --git a/src/type/definition.ts b/src/type/definition.ts index 81488efb39..800e930f3e 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -444,6 +444,10 @@ export type GraphQLNamedOutputType = | GraphQLUnionType | GraphQLEnumType; +export function getDeprecationReason(type: GraphQLNamedType) { + return 'deprecationReason' in type ? type.deprecationReason : undefined; +} + export function isNamedType(type: unknown): type is GraphQLNamedType { return ( isScalarType(type) || @@ -707,6 +711,7 @@ export class GraphQLObjectType { extensions: Readonly>; astNode: Maybe; extensionASTNodes: ReadonlyArray; + deprecationReason: Maybe; private _fields: ThunkObjMap>; private _interfaces: ThunkReadonlyArray; @@ -718,6 +723,7 @@ export class GraphQLObjectType { this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; this.extensionASTNodes = config.extensionASTNodes ?? []; + this.deprecationReason = config.deprecationReason; this._fields = () => defineFieldMap(config); this._interfaces = () => defineInterfaces(config); @@ -751,6 +757,7 @@ export class GraphQLObjectType { extensions: this.extensions, astNode: this.astNode, extensionASTNodes: this.extensionASTNodes, + deprecationReason: this.deprecationReason, }; } @@ -853,6 +860,7 @@ export interface GraphQLObjectTypeConfig { extensions?: Maybe>>; astNode?: Maybe; extensionASTNodes?: Maybe>; + deprecationReason?: Maybe; } interface GraphQLObjectTypeNormalizedConfig diff --git a/src/type/directives.ts b/src/type/directives.ts index 8fd5a6a62e..9eb34b8554 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -220,6 +220,7 @@ export const GraphQLDeprecatedDirective: GraphQLDirective = DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE, + DirectiveLocation.OBJECT, ], args: { reason: { diff --git a/src/type/introspection.ts b/src/type/introspection.ts index aedce3e6a8..fc02d4ff08 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -15,6 +15,7 @@ import type { GraphQLType, } from './definition.js'; import { + getDeprecationReason, GraphQLEnumType, GraphQLList, GraphQLNonNull, @@ -46,8 +47,14 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ types: { description: 'A list of all types supported by this server.', type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(__Type))), - resolve(schema) { - return Object.values(schema.getTypeMap()); + args: { + includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + }, + resolve(schema, { includeDeprecated }) { + const types = Object.values(schema.getTypeMap()); + return includeDeprecated + ? types + : types.filter((type) => getDeprecationReason(type) == null); }, }, queryType: { @@ -282,9 +289,15 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ }, possibleTypes: { type: new GraphQLList(new GraphQLNonNull(__Type)), - resolve(type, _args, _context, { schema }) { + args: { + includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + }, + resolve(type, { includeDeprecated }, _context, { schema }) { if (isAbstractType(type)) { - return schema.getPossibleTypes(type); + const possibleTypes = schema.getPossibleTypes(type); + return includeDeprecated + ? possibleTypes + : possibleTypes.filter((t) => t.deprecationReason == null); } }, }, @@ -323,6 +336,18 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ type: __Type, resolve: (type) => ('ofType' in type ? type.ofType : undefined), }, + isDeprecated: { + type: GraphQLBoolean, + resolve: (type) => + 'deprecationReason' in type + ? type.deprecationReason != null + : undefined, + }, + deprecationReason: { + type: GraphQLString, + resolve: (type) => + 'deprecationReason' in type ? type.deprecationReason : undefined, + }, } as GraphQLFieldConfigMap), }); diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 204ceb800c..0bb23bfe9f 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -634,6 +634,10 @@ describe('Schema Builder', () => { field4(oldArg: String @deprecated(reason: "Why not?"), arg: String): String field5(arg: MyInput): String } + + type DeprecatedObject @deprecated(reason: "It ain't") { + fields1: Boolean + } `; expect(cycleSDL(sdl)).to.equal(sdl); @@ -662,6 +666,13 @@ describe('Schema Builder', () => { deprecationReason: 'Because I said so', }); + const deprecatedObject = assertObjectType( + schema.getType('DeprecatedObject'), + ); + expect(deprecatedObject).to.include({ + deprecationReason: "It ain't", + }); + const inputFields = assertInputObjectType( schema.getType('MyInput'), ).getFields(); diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.ts b/src/utilities/__tests__/getIntrospectionQuery-test.ts index 62f64c968e..5faf787b12 100644 --- a/src/utilities/__tests__/getIntrospectionQuery-test.ts +++ b/src/utilities/__tests__/getIntrospectionQuery-test.ts @@ -103,6 +103,20 @@ describe('getIntrospectionQuery', () => { ); }); + it('include "isDeprecated" field on objects', () => { + expectIntrospectionQuery().toMatch('isDeprecated', 2); + + expectIntrospectionQuery({ objectDeprecation: true }).toMatch( + 'isDeprecated', + 3, + ); + + expectIntrospectionQuery({ objectDeprecation: false }).toMatch( + 'isDeprecated', + 2, + ); + }); + it('include "deprecationReason" field on input values', () => { expectIntrospectionQuery().toMatch('deprecationReason', 2); @@ -130,4 +144,18 @@ describe('getIntrospectionQuery', () => { 2, ); }); + + it('include deprecated objects', () => { + expectIntrospectionQuery().toMatch('includeDeprecated: true', 2); + + expectIntrospectionQuery({ objectDeprecation: true }).toMatch( + 'includeDeprecated: true', + 4, + ); + + expectIntrospectionQuery({ objectDeprecation: false }).toMatch( + 'includeDeprecated: true', + 2, + ); + }); }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 1608cfc644..adba264df2 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -668,7 +668,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 | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE + ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE | OBJECT """Exposes a URL that specifies the behavior of this scalar.""" directive @specifiedBy( @@ -683,7 +683,7 @@ describe('Type System Printer', () => { description: String """A list of all types supported by this server.""" - types: [__Type!]! + types(includeDeprecated: Boolean = false): [__Type!]! """The type that query operations will be rooted at.""" queryType: __Type! @@ -714,10 +714,12 @@ describe('Type System Printer', () => { specifiedByURL: String fields(includeDeprecated: Boolean = false): [__Field!] interfaces: [__Type!] - possibleTypes: [__Type!] + possibleTypes(includeDeprecated: Boolean = false): [__Type!] enumValues(includeDeprecated: Boolean = false): [__EnumValue!] inputFields(includeDeprecated: Boolean = false): [__InputValue!] ofType: __Type + isDeprecated: Boolean + deprecationReason: String } """An enum describing what kind of type a given \`__Type\` is.""" diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 0cae3398f2..f01bf0f4a0 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -240,6 +240,7 @@ export function buildClientSchema( description: objectIntrospection.description, interfaces: () => buildImplementationsList(objectIntrospection), fields: () => buildFieldDefMap(objectIntrospection), + deprecationReason: objectIntrospection.deprecationReason, }); } diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 831733b69b..7b9758e844 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -637,6 +637,7 @@ export function extendSchemaImpl( fields: () => buildFieldMap(allNodes), astNode, extensionASTNodes, + deprecationReason: getDeprecationReason(astNode), }); } case Kind.INTERFACE_TYPE_DEFINITION: { @@ -715,7 +716,8 @@ function getDeprecationReason( node: | EnumValueDefinitionNode | FieldDefinitionNode - | InputValueDefinitionNode, + | InputValueDefinitionNode + | ObjectTypeDefinitionNode, ): Maybe { const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node); // @ts-expect-error validated by `getDirectiveValues` diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 12c2aa6404..a4f9db50c4 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -32,6 +32,12 @@ export interface IntrospectionOptions { * Default: false */ inputValueDeprecation?: boolean; + + /** + * Whether target GraphQL server support deprecation of objects. + * Default: false + */ + objectDeprecation?: boolean; } /** @@ -45,6 +51,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { directiveIsRepeatable: false, schemaDescription: false, inputValueDeprecation: false, + objectDeprecation: false, ...options, }; @@ -63,6 +70,10 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { return optionsWithDefault.inputValueDeprecation ? str : ''; } + function objectDeprecation(str: string) { + return optionsWithDefault.objectDeprecation ? str : ''; + } + return ` query IntrospectionQuery { __schema { @@ -70,7 +81,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { queryType { name } mutationType { name } subscriptionType { name } - types { + types${objectDeprecation('(includeDeprecated: true)')} { ...FullType } directives { @@ -114,9 +125,11 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { isDeprecated deprecationReason } - possibleTypes { + possibleTypes${objectDeprecation('(includeDeprecated: true)')} { ...TypeRef } + ${objectDeprecation('isDeprecated')} + ${objectDeprecation('deprecationReason')} } fragment InputValue on __InputValue { @@ -215,6 +228,8 @@ export interface IntrospectionObjectType { readonly interfaces: ReadonlyArray< IntrospectionNamedTypeRef >; + readonly isDeprecated: boolean; + readonly deprecationReason: Maybe; } export interface IntrospectionInterfaceType { diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 527cf4be17..23294664ef 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -172,6 +172,7 @@ function printObject(type: GraphQLObjectType): string { printDescription(type) + `type ${type.name}` + printImplementedInterfaces(type) + + printDeprecated(type.deprecationReason) + printFields(type) ); }