From 6d3b630d66b739c4e476587bcc1b9c9dff92b851 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 19 Dec 2025 18:34:36 +0100 Subject: [PATCH 1/2] Add support for directives on directive definitions --- src/__testUtils__/kitchenSinkSDL.ts | 4 ++++ src/index.ts | 1 + src/language/__tests__/predicates-test.ts | 2 ++ src/language/__tests__/schema-parser-test.ts | 2 ++ src/language/__tests__/schema-printer-test.ts | 4 ++++ src/language/ast.ts | 24 +++++++++++++++++-- src/language/directiveLocation.ts | 1 + src/language/index.ts | 1 + src/language/kinds.ts | 1 + src/language/parser.ts | 24 +++++++++++++++++++ src/language/predicates.ts | 6 ++++- src/language/printer.ts | 15 +++++++++++- src/type/__tests__/introspection-test.ts | 6 +++++ src/type/directives.ts | 5 ++++ src/type/introspection.ts | 4 ++++ src/utilities/__tests__/printSchema-test.ts | 5 +++- .../__tests__/KnownDirectivesRule-test.ts | 11 +++++++++ src/validation/rules/KnownDirectivesRule.ts | 3 +++ 18 files changed, 114 insertions(+), 5 deletions(-) diff --git a/src/__testUtils__/kitchenSinkSDL.ts b/src/__testUtils__/kitchenSinkSDL.ts index 7b7a537783..e6590f248f 100644 --- a/src/__testUtils__/kitchenSinkSDL.ts +++ b/src/__testUtils__/kitchenSinkSDL.ts @@ -161,4 +161,8 @@ extend schema @onSchema extend schema @onSchema { subscription: SubscriptionType } + +directive @myDirective @onDirective on OBJECT | FIELD_DEFINITION + +extend directive @myDirective @onDirective2 `; diff --git a/src/index.ts b/src/index.ts index 54ab38437f..317c9f8ca9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -312,6 +312,7 @@ export type { UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, + DirectiveExtensionNode, // Schema Coordinates SchemaCoordinateNode, TypeCoordinateNode, diff --git a/src/language/__tests__/predicates-test.ts b/src/language/__tests__/predicates-test.ts index 4cf0057abe..427b424606 100644 --- a/src/language/__tests__/predicates-test.ts +++ b/src/language/__tests__/predicates-test.ts @@ -39,6 +39,7 @@ describe('AST node predicates', () => { 'InputObjectTypeDefinition', 'DirectiveDefinition', 'SchemaExtension', + 'DirectiveExtension', 'ScalarTypeExtension', 'ObjectTypeExtension', 'InterfaceTypeExtension', @@ -123,6 +124,7 @@ describe('AST node predicates', () => { it('isTypeSystemExtensionNode', () => { expect(filterNodes(isTypeSystemExtensionNode)).to.deep.equal([ 'SchemaExtension', + 'DirectiveExtension', 'ScalarTypeExtension', 'ObjectTypeExtension', 'InterfaceTypeExtension', diff --git a/src/language/__tests__/schema-parser-test.ts b/src/language/__tests__/schema-parser-test.ts index 5159939cfd..38358a676b 100644 --- a/src/language/__tests__/schema-parser-test.ts +++ b/src/language/__tests__/schema-parser-test.ts @@ -1029,6 +1029,7 @@ input Hello { { kind: 'DirectiveDefinition', description: undefined, + directives: [], name: { kind: 'Name', value: 'foo', @@ -1065,6 +1066,7 @@ input Hello { { kind: 'DirectiveDefinition', description: undefined, + directives: [], name: { kind: 'Name', value: 'foo', diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts index 41cf6c5419..cdef825ce7 100644 --- a/src/language/__tests__/schema-printer-test.ts +++ b/src/language/__tests__/schema-printer-test.ts @@ -178,6 +178,10 @@ describe('Printer: SDL document', () => { extend schema @onSchema { subscription: SubscriptionType } + + directive @myDirective @onDirective on OBJECT | FIELD_DEFINITION + + extend directive @myDirective @onDirective2 `); }); }); diff --git a/src/language/ast.ts b/src/language/ast.ts index 9b80a86206..6810b73961 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -180,6 +180,7 @@ export type ASTNode = | UnionTypeExtensionNode | EnumTypeExtensionNode | InputObjectTypeExtensionNode + | DirectiveExtensionNode | TypeCoordinateNode | MemberCoordinateNode | ArgumentCoordinateNode @@ -280,10 +281,18 @@ export const QueryDocumentKeys: { EnumValueDefinition: ['description', 'name', 'directives'], InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'], - DirectiveDefinition: ['description', 'name', 'arguments', 'locations'], + DirectiveDefinition: [ + 'description', + 'name', + 'arguments', + 'directives', + 'locations', + ], SchemaExtension: ['directives', 'operationTypes'], + DirectiveExtension: ['name', 'directives'], + ScalarTypeExtension: ['name', 'directives'], ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'], InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'], @@ -686,13 +695,17 @@ export interface DirectiveDefinitionNode { readonly description?: StringValueNode; readonly name: NameNode; readonly arguments?: ReadonlyArray; + readonly directives?: ReadonlyArray; readonly repeatable: boolean; readonly locations: ReadonlyArray; } /** Type System Extensions */ -export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode; +export type TypeSystemExtensionNode = + | SchemaExtensionNode + | TypeExtensionNode + | DirectiveExtensionNode; export interface SchemaExtensionNode { readonly kind: Kind.SCHEMA_EXTENSION; @@ -760,6 +773,13 @@ export interface InputObjectTypeExtensionNode { readonly fields?: ReadonlyArray; } +export interface DirectiveExtensionNode { + readonly kind: Kind.DIRECTIVE_EXTENSION; + readonly loc?: Location; + readonly name: NameNode; + readonly directives?: ReadonlyArray; +} + /** Schema Coordinates */ export type SchemaCoordinateNode = diff --git a/src/language/directiveLocation.ts b/src/language/directiveLocation.ts index 5c8aeb7240..ac99f2aeea 100644 --- a/src/language/directiveLocation.ts +++ b/src/language/directiveLocation.ts @@ -23,6 +23,7 @@ enum DirectiveLocation { ENUM_VALUE = 'ENUM_VALUE', INPUT_OBJECT = 'INPUT_OBJECT', INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION', + DIRECTIVE_DEFINITION = 'DIRECTIVE_DEFINITION', } export { DirectiveLocation }; diff --git a/src/language/index.ts b/src/language/index.ts index 28d6400bc4..615ca1287a 100644 --- a/src/language/index.ts +++ b/src/language/index.ts @@ -96,6 +96,7 @@ export type { UnionTypeExtensionNode, EnumTypeExtensionNode, InputObjectTypeExtensionNode, + DirectiveExtensionNode, // Schema Coordinates SchemaCoordinateNode, TypeCoordinateNode, diff --git a/src/language/kinds.ts b/src/language/kinds.ts index 9c10348a32..fb5ecb35f2 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -58,6 +58,7 @@ enum Kind { /** Type System Extensions */ SCHEMA_EXTENSION = 'SchemaExtension', + DIRECTIVE_EXTENSION = 'DirectiveExtension', /** Type Extensions */ SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension', diff --git a/src/language/parser.ts b/src/language/parser.ts index f489027b6b..3fd3d07318 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -17,6 +17,7 @@ import type { DirectiveArgumentCoordinateNode, DirectiveCoordinateNode, DirectiveDefinitionNode, + DirectiveExtensionNode, DirectiveNode, DocumentNode, EnumTypeDefinitionNode, @@ -1184,6 +1185,7 @@ export class Parser { * - UnionTypeExtension * - EnumTypeExtension * - InputObjectTypeDefinition + * - DirectiveDefinitionExtension */ parseTypeSystemExtension(): TypeSystemExtensionNode { const keywordToken = this._lexer.lookahead(); @@ -1204,6 +1206,8 @@ export class Parser { return this.parseEnumTypeExtension(); case 'input': return this.parseInputObjectTypeExtension(); + case 'directive': + return this.parseDirectiveDefinitionExtension(); } } @@ -1386,6 +1390,23 @@ export class Parser { }); } + parseDirectiveDefinitionExtension(): DirectiveExtensionNode { + const start = this._lexer.token; + this.expectKeyword('extend'); + this.expectKeyword('directive'); + this.expectToken(TokenKind.AT); + const name = this.parseName(); + const directives = this.parseConstDirectives(); + if (directives.length === 0) { + throw this.unexpected(); + } + return this.node(start, { + kind: Kind.DIRECTIVE_EXTENSION, + name, + directives, + }); + } + /** * ``` * DirectiveDefinition : @@ -1399,6 +1420,7 @@ export class Parser { this.expectToken(TokenKind.AT); const name = this.parseName(); const args = this.parseArgumentDefs(); + const directives = this.parseConstDirectives(); const repeatable = this.expectOptionalKeyword('repeatable'); this.expectKeyword('on'); const locations = this.parseDirectiveLocations(); @@ -1407,6 +1429,7 @@ export class Parser { description, name, arguments: args, + directives, repeatable, locations, }); @@ -1447,6 +1470,7 @@ export class Parser { * `ENUM_VALUE` * `INPUT_OBJECT` * `INPUT_FIELD_DEFINITION` + * `DIRECTIVE_DEFINITION` */ parseDirectiveLocation(): NameNode { const start = this._lexer.token; diff --git a/src/language/predicates.ts b/src/language/predicates.ts index dd53709c61..afc861c9d8 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -98,7 +98,11 @@ export function isTypeDefinitionNode( export function isTypeSystemExtensionNode( node: ASTNode, ): node is TypeSystemExtensionNode { - return node.kind === Kind.SCHEMA_EXTENSION || isTypeExtensionNode(node); + return ( + node.kind === Kind.SCHEMA_EXTENSION || + node.kind === Kind.DIRECTIVE_EXTENSION || + isTypeExtensionNode(node) + ); } export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode { diff --git a/src/language/printer.ts b/src/language/printer.ts index fde1cbcf2f..84746181ab 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -235,13 +235,21 @@ const printDocASTReducer: ASTReducer = { }, DirectiveDefinition: { - leave: ({ description, name, arguments: args, repeatable, locations }) => + leave: ({ + description, + name, + arguments: args, + directives, + repeatable, + locations, + }) => wrap('', description, '\n') + 'directive @' + name + (hasMultilineItems(args) ? wrap('(\n', indent(join(args, '\n')), '\n)') : wrap('(', join(args, ', '), ')')) + + wrap(' ', join(directives, ' ')) + (repeatable ? ' repeatable' : '') + ' on ' + join(locations, ' | '), @@ -311,6 +319,11 @@ const printDocASTReducer: ASTReducer = { join(['extend input', name, join(directives, ' '), block(fields)], ' '), }, + DirectiveExtension: { + leave: ({ name, directives }) => + join(['extend directive @' + name, join(directives, ' ')], ' '), + }, + // Schema Coordinates TypeCoordinate: { leave: ({ name }) => name }, diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 8c5cacba0d..55c459fc58 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -914,6 +914,11 @@ describe('Introspection', () => { isDeprecated: false, deprecationReason: null, }, + { + name: 'DIRECTIVE_DEFINITION', + isDeprecated: false, + deprecationReason: null, + }, ], possibleTypes: null, }, @@ -967,6 +972,7 @@ describe('Introspection', () => { 'ARGUMENT_DEFINITION', 'INPUT_FIELD_DEFINITION', 'ENUM_VALUE', + 'DIRECTIVE_DEFINITION', ], args: [ { diff --git a/src/type/directives.ts b/src/type/directives.ts index 6881f20532..a1f3f3eea2 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -59,6 +59,7 @@ export class GraphQLDirective { locations: ReadonlyArray; args: ReadonlyArray; isRepeatable: boolean; + deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; @@ -67,6 +68,7 @@ export class GraphQLDirective { this.description = config.description; this.locations = config.locations; this.isRepeatable = config.isRepeatable ?? false; + this.deprecationReason = config.deprecationReason; this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; @@ -95,6 +97,7 @@ export class GraphQLDirective { locations: this.locations, args: argsToArgsConfig(this.args), isRepeatable: this.isRepeatable, + deprecationReason: this.deprecationReason, extensions: this.extensions, astNode: this.astNode, }; @@ -115,6 +118,7 @@ export interface GraphQLDirectiveConfig { locations: ReadonlyArray; args?: Maybe; isRepeatable?: Maybe; + deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; } @@ -182,6 +186,7 @@ export const GraphQLDeprecatedDirective: GraphQLDirective = DirectiveLocation.ARGUMENT_DEFINITION, DirectiveLocation.INPUT_FIELD_DEFINITION, DirectiveLocation.ENUM_VALUE, + DirectiveLocation.DIRECTIVE_DEFINITION, ], args: { reason: { diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 2c66ca5098..289816d66a 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -201,6 +201,10 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({ value: DirectiveLocation.INPUT_FIELD_DEFINITION, description: 'Location adjacent to an input object field definition.', }, + DIRECTIVE_DEFINITION: { + value: DirectiveLocation.DIRECTIVE_DEFINITION, + description: 'Location adjacent to a directive definition.', + }, }, }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 37af4a60f7..3fbeb64d78 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -681,7 +681,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 | DIRECTIVE_DEFINITION """Exposes a URL that specifies the behavior of this scalar.""" directive @specifiedBy( @@ -883,6 +883,9 @@ describe('Type System Printer', () => { """Location adjacent to an input object field definition.""" INPUT_FIELD_DEFINITION + + """Location adjacent to a directive definition.""" + DIRECTIVE_DEFINITION } `); }); diff --git a/src/validation/__tests__/KnownDirectivesRule-test.ts b/src/validation/__tests__/KnownDirectivesRule-test.ts index 4cb6e225c1..ff6bba5f83 100644 --- a/src/validation/__tests__/KnownDirectivesRule-test.ts +++ b/src/validation/__tests__/KnownDirectivesRule-test.ts @@ -58,6 +58,7 @@ const schemaWithSDLDirectives = buildSchema(` directive @onEnumValue on ENUM_VALUE directive @onInputObject on INPUT_OBJECT directive @onInputFieldDefinition on INPUT_FIELD_DEFINITION + directive @onDirective on DIRECTIVE_DEFINITION `); describe('Validate: Known directives', () => { @@ -349,6 +350,10 @@ describe('Validate: Known directives', () => { } extend schema @onSchema + + directive @myDirective on OBJECT + + extend directive @myDirective @onDirective `, schemaWithSDLDirectives, ); @@ -382,6 +387,8 @@ describe('Validate: Known directives', () => { } extend schema @onObject + + extend type MyObj @onDirective `, schemaWithSDLDirectives, ).toDeepEqual([ @@ -446,6 +453,10 @@ describe('Validate: Known directives', () => { message: 'Directive "@onObject" may not be used on SCHEMA.', locations: [{ line: 26, column: 25 }], }, + { + message: 'Directive "@onDirective" may not be used on OBJECT.', + locations: [{ line: 28, column: 29 }], + }, ]); }); }); diff --git a/src/validation/rules/KnownDirectivesRule.ts b/src/validation/rules/KnownDirectivesRule.ts index f24dbe7d28..162b9e9680 100644 --- a/src/validation/rules/KnownDirectivesRule.ts +++ b/src/validation/rules/KnownDirectivesRule.ts @@ -120,6 +120,9 @@ function getDirectiveLocationForASTPath( ? DirectiveLocation.INPUT_FIELD_DEFINITION : DirectiveLocation.ARGUMENT_DEFINITION; } + case Kind.DIRECTIVE_DEFINITION: + case Kind.DIRECTIVE_EXTENSION: + return DirectiveLocation.DIRECTIVE_DEFINITION; // Not reachable, all possible types have been considered. /* c8 ignore next */ default: From 519cd493c4b02220b56895857c5123b0b82dadfd Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 6 Jan 2026 18:23:16 +0100 Subject: [PATCH 2/2] Introspection related changes --- src/type/__tests__/directive-test.ts | 16 +++ src/type/__tests__/introspection-test.ts | 104 ++++++++++++++++++- src/type/directives.ts | 10 +- src/type/introspection.ts | 18 +++- src/utilities/__tests__/extendSchema-test.ts | 15 +++ src/utilities/__tests__/printSchema-test.ts | 4 +- src/utilities/extendSchema.ts | 38 ++++++- 7 files changed, 199 insertions(+), 6 deletions(-) diff --git a/src/type/__tests__/directive-test.ts b/src/type/__tests__/directive-test.ts index 110a3cc940..4a0793d483 100644 --- a/src/type/__tests__/directive-test.ts +++ b/src/type/__tests__/directive-test.ts @@ -73,6 +73,22 @@ describe('Type System: Directive', () => { }); }); + it('defines a deprecated directive', () => { + const directive = new GraphQLDirective({ + name: 'Foo', + locations: [DirectiveLocation.QUERY], + deprecationReason: 'Some reason', + }); + + expect(directive).to.deep.include({ + name: 'Foo', + args: [], + isRepeatable: false, + locations: ['QUERY'], + deprecationReason: 'Some reason', + }); + }); + it('can be stringified, JSON.stringified and Object.toStringified', () => { const directive = new GraphQLDirective({ name: 'Foo', diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 55c459fc58..be85dd5ab5 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -156,7 +156,17 @@ describe('Introspection', () => { }, { name: 'directives', - args: [], + args: [ + { + name: 'includeDeprecated', + type: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, + defaultValue: 'false', + }, + ], type: { kind: 'NON_NULL', name: null, @@ -805,6 +815,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: [], @@ -1760,4 +1796,70 @@ describe('Introspection', () => { }); expect(result).to.not.have.property('errors'); }); + + it('identifies deprecated directives', () => { + const schema = buildSchema(` + type Query { + someField: String + } + directive @isNotDeprecated on FIELD_DEFINITION + directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION + `); + + const source = ` + { + __schema { + directives(includeDeprecated: true) { + name + isDeprecated + deprecationReason + } + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + __schema: { + directives: [ + { + name: 'isNotDeprecated', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'isDeprecated', + isDeprecated: true, + deprecationReason: 'No longer supported', + }, + { + name: 'include', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'skip', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'deprecated', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'specifiedBy', + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'oneOf', + isDeprecated: false, + deprecationReason: null, + }, + ], + }, + }, + }); + }); }); diff --git a/src/type/directives.ts b/src/type/directives.ts index a1f3f3eea2..9c293ae411 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -5,7 +5,10 @@ import { isObjectLike } from '../jsutils/isObjectLike'; import type { Maybe } from '../jsutils/Maybe'; import { toObjMap } from '../jsutils/toObjMap'; -import type { DirectiveDefinitionNode } from '../language/ast'; +import type { + DirectiveDefinitionNode, + DirectiveExtensionNode, +} from '../language/ast'; import { DirectiveLocation } from '../language/directiveLocation'; import { assertName } from './assertName'; @@ -62,6 +65,7 @@ export class GraphQLDirective { deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; + extensionASTNodes: ReadonlyArray; constructor(config: Readonly) { this.name = assertName(config.name); @@ -71,6 +75,7 @@ export class GraphQLDirective { this.deprecationReason = config.deprecationReason; this.extensions = toObjMap(config.extensions); this.astNode = config.astNode; + this.extensionASTNodes = config.extensionASTNodes ?? []; devAssert( Array.isArray(config.locations), @@ -100,6 +105,7 @@ export class GraphQLDirective { deprecationReason: this.deprecationReason, extensions: this.extensions, astNode: this.astNode, + extensionASTNodes: this.extensionASTNodes, }; } @@ -121,12 +127,14 @@ export interface GraphQLDirectiveConfig { deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; + extensionASTNodes?: Maybe>; } interface GraphQLDirectiveNormalizedConfig extends GraphQLDirectiveConfig { args: GraphQLFieldConfigArgumentMap; isRepeatable: boolean; extensions: Readonly; + extensionASTNodes: ReadonlyArray; } /** diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 289816d66a..29688395c8 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -72,7 +72,15 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ type: new GraphQLNonNull( new GraphQLList(new GraphQLNonNull(__Directive)), ), - resolve: (schema) => schema.getDirectives(), + args: { + includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + }, + resolve: (schema, { includeDeprecated }) => + includeDeprecated + ? schema.getDirectives() + : schema + .getDirectives() + .filter((directive) => directive.deprecationReason == null), }, } as GraphQLFieldConfigMap), }); @@ -117,6 +125,14 @@ export const __Directive: GraphQLObjectType = new GraphQLObjectType({ : field.args.filter((arg) => arg.deprecationReason == null); }, }, + isDeprecated: { + type: new GraphQLNonNull(GraphQLBoolean), + resolve: (directive) => directive.deprecationReason != null, + }, + deprecationReason: { + type: GraphQLString, + resolve: (directive) => directive.deprecationReason, + }, } as GraphQLFieldConfigMap), }); diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 86baf0e699..6cab9289f2 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -1318,5 +1318,20 @@ describe('extendSchema', () => { extend schema @foo `); }); + + it('extend directive to make it deprecated', () => { + const schema = buildSchema('directive @isDeprecated on FIELD_DEFINITION'); + const extendAST = parse(` + extend directive @isDeprecated @deprecated(reason: "use another directive") + `); + const extendedSchema = extendSchema(schema, extendAST); + + const someDirective = assertDirective( + extendedSchema.getDirective('isDeprecated'), + ); + expect(someDirective).to.include({ + deprecationReason: 'use another directive', + }); + }); }); }); diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 3fbeb64d78..1b45120ef2 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -717,7 +717,7 @@ describe('Type System Printer', () => { subscriptionType: __Type """A list of all directives supported by this server.""" - directives: [__Directive!]! + directives(includeDeprecated: Boolean = false): [__Directive!]! } """ @@ -821,6 +821,8 @@ describe('Type System Printer', () => { isRepeatable: Boolean! locations: [__DirectiveLocation!]! args(includeDeprecated: Boolean = false): [__InputValue!]! + isDeprecated: Boolean! + deprecationReason: String } """ diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index d53752d919..dfa68e2907 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -7,6 +7,7 @@ import type { Maybe } from '../jsutils/Maybe'; import type { DirectiveDefinitionNode, + DirectiveExtensionNode, DocumentNode, EnumTypeDefinitionNode, EnumTypeExtensionNode, @@ -138,6 +139,7 @@ export function extendSchemaImpl( // Collect the type definitions and extensions found in the document. const typeDefs: Array = []; const typeExtensionsMap = Object.create(null); + const directiveExtensionsMap = Object.create(null); // New directives and types are separate because a directives and types can // have the same name. For example, a type named "skip". @@ -162,6 +164,14 @@ export function extendSchemaImpl( : [def]; } else if (def.kind === Kind.DIRECTIVE_DEFINITION) { directiveDefs.push(def); + } else if (def.kind === Kind.DIRECTIVE_EXTENSION) { + const extendedDirectiveName = def.name.value; + const existingDirectiveExtensions = + directiveExtensionsMap[extendedDirectiveName]; + directiveExtensionsMap[extendedDirectiveName] = + existingDirectiveExtensions + ? existingDirectiveExtensions.concat([def]) + : [def]; } } @@ -170,6 +180,7 @@ export function extendSchemaImpl( if ( Object.keys(typeExtensionsMap).length === 0 && typeDefs.length === 0 && + Object.keys(directiveExtensionsMap).length === 0 && directiveDefs.length === 0 && schemaExtensions.length === 0 && schemaDef == null @@ -187,6 +198,11 @@ export function extendSchemaImpl( typeMap[name] = stdTypeMap[name] ?? buildType(typeNode); } + const directiveMap = Object.create(null); + for (const existingDirective of schemaConfig.directives) { + directiveMap[existingDirective.name] = extendDirective(existingDirective); + } + const operationTypes = { // Get the extended root operation types. query: schemaConfig.query && replaceNamedType(schemaConfig.query), @@ -199,12 +215,13 @@ export function extendSchemaImpl( }; // Then produce and return a Schema config with these types. + const directives: Array = Object.values(directiveMap); return { description: schemaDef?.description?.value, ...operationTypes, types: Object.values(typeMap), directives: [ - ...schemaConfig.directives.map(replaceDirective), + ...directives.map(replaceDirective), ...directiveDefs.map(buildDirective), ], extensions: Object.create(null), @@ -415,6 +432,20 @@ export function extendSchemaImpl( return opTypes; } + function extendDirective(directive: GraphQLDirective): GraphQLDirective { + const config = directive.toConfig(); + const extensions = directiveExtensionsMap[config.name] ?? []; + const deprecatedReason = extensions + .map((ext: DirectiveExtensionNode) => getDeprecationReason(ext)) + .find((reason: Maybe) => reason != null); + + return new GraphQLDirective({ + ...config, + deprecationReason: deprecatedReason, + extensionASTNodes: config.extensionASTNodes.concat(extensions), + }); + } + function getNamedType(node: NamedTypeNode): GraphQLNamedType { const name = node.name.value; const type = stdTypeMap[name] ?? typeMap[name]; @@ -443,6 +474,7 @@ export function extendSchemaImpl( locations: node.locations.map(({ value }) => value), isRepeatable: node.repeatable, args: buildArgumentMap(node.arguments), + deprecationReason: getDeprecationReason(node), astNode: node, }); } @@ -667,7 +699,9 @@ function getDeprecationReason( node: | EnumValueDefinitionNode | FieldDefinitionNode - | InputValueDefinitionNode, + | InputValueDefinitionNode + | DirectiveDefinitionNode + | DirectiveExtensionNode, ): Maybe { const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node); // @ts-expect-error validated by `getDirectiveValues`