From 531cbf459786be2a9685b3c2db2b349cccda58d0 Mon Sep 17 00:00:00 2001 From: BoD Date: Fri, 19 Dec 2025 18:34:36 +0100 Subject: [PATCH 01/26] 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 e46ed2ec900e3779779b3fbac4434883b9136b42 Mon Sep 17 00:00:00 2001 From: BoD Date: Tue, 6 Jan 2026 18:23:16 +0100 Subject: [PATCH 02/26] 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` From d44ff03ff1ab6f832b81c06016b292a932362512 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 17 Mar 2026 16:24:41 +0100 Subject: [PATCH 03/26] Add ParseOption.experimentalDirectivesOnDirectiveDefinition --- src/__testUtils__/kitchenSinkSDL.ts | 4 ---- src/language/__tests__/schema-printer-test.ts | 4 ---- src/language/parser.ts | 24 +++++++++++++++++-- src/type/__tests__/introspection-test.ts | 7 +++--- src/utilities/__tests__/extendSchema-test.ts | 2 +- .../__tests__/KnownDirectivesRule-test.ts | 2 -- 6 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/__testUtils__/kitchenSinkSDL.ts b/src/__testUtils__/kitchenSinkSDL.ts index e6590f248f..7b7a537783 100644 --- a/src/__testUtils__/kitchenSinkSDL.ts +++ b/src/__testUtils__/kitchenSinkSDL.ts @@ -161,8 +161,4 @@ extend schema @onSchema extend schema @onSchema { subscription: SubscriptionType } - -directive @myDirective @onDirective on OBJECT | FIELD_DEFINITION - -extend directive @myDirective @onDirective2 `; diff --git a/src/language/__tests__/schema-printer-test.ts b/src/language/__tests__/schema-printer-test.ts index cdef825ce7..41cf6c5419 100644 --- a/src/language/__tests__/schema-printer-test.ts +++ b/src/language/__tests__/schema-printer-test.ts @@ -178,10 +178,6 @@ 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/parser.ts b/src/language/parser.ts index 3fd3d07318..2f93832388 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -113,6 +113,18 @@ export interface ParseOptions { */ allowLegacyFragmentVariables?: boolean; + /** + * EXPERIMENTAL: + * + * If enabled, the parser will parse directives on directive definitions. + * This syntax is not part of the GraphQL specification and may change. + * + * ```graphql + * directive @foo @bar on FIELD + * ``` + */ + experimentalDirectivesOnDirectiveDefinitions?: boolean; + /** * You may override the Lexer class used to lex the source; this is used by * schema coordinates to introduce a lexer with a restricted syntax. @@ -1207,7 +1219,12 @@ export class Parser { case 'input': return this.parseInputObjectTypeExtension(); case 'directive': - return this.parseDirectiveDefinitionExtension(); + if ( + this._options.experimentalDirectivesOnDirectiveDefinitions === true + ) { + return this.parseDirectiveDefinitionExtension(); + } + break; } } @@ -1420,7 +1437,10 @@ export class Parser { this.expectToken(TokenKind.AT); const name = this.parseName(); const args = this.parseArgumentDefs(); - const directives = this.parseConstDirectives(); + const directives = + this._options.experimentalDirectivesOnDirectiveDefinitions === true + ? this.parseConstDirectives() + : []; const repeatable = this.expectOptionalKeyword('repeatable'); this.expectKeyword('on'); const locations = this.parseDirectiveLocations(); diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index be85dd5ab5..8414fc8e8e 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -3,12 +3,13 @@ import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; -import { buildSchema } from '../../utilities/buildASTSchema'; +import { buildASTSchema, buildSchema } from '../../utilities/buildASTSchema'; import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery'; import { graphqlSync } from '../../graphql'; import type { GraphQLResolveInfo } from '../definition'; +import { parse } from '../..'; describe('Introspection', () => { it('executes an introspection query', () => { @@ -1798,13 +1799,13 @@ describe('Introspection', () => { }); it('identifies deprecated directives', () => { - const schema = buildSchema(` + const schema = buildASTSchema(parse(` type Query { someField: String } directive @isNotDeprecated on FIELD_DEFINITION directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION - `); + `, { experimentalDirectivesOnDirectiveDefinitions: true })); const source = ` { diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 6cab9289f2..c59e6377e9 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -1323,7 +1323,7 @@ describe('extendSchema', () => { const schema = buildSchema('directive @isDeprecated on FIELD_DEFINITION'); const extendAST = parse(` extend directive @isDeprecated @deprecated(reason: "use another directive") - `); + `, { experimentalDirectivesOnDirectiveDefinitions: true }); const extendedSchema = extendSchema(schema, extendAST); const someDirective = assertDirective( diff --git a/src/validation/__tests__/KnownDirectivesRule-test.ts b/src/validation/__tests__/KnownDirectivesRule-test.ts index ff6bba5f83..2ad1b8c70e 100644 --- a/src/validation/__tests__/KnownDirectivesRule-test.ts +++ b/src/validation/__tests__/KnownDirectivesRule-test.ts @@ -352,8 +352,6 @@ describe('Validate: Known directives', () => { extend schema @onSchema directive @myDirective on OBJECT - - extend directive @myDirective @onDirective `, schemaWithSDLDirectives, ); From ca49ec5cf9dd66e0434a5bbb1da0c8d5c83d3868 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 17 Mar 2026 16:30:38 +0100 Subject: [PATCH 04/26] npm run prettier --- src/type/__tests__/introspection-test.ts | 9 +++++++-- src/utilities/__tests__/extendSchema-test.ts | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 8414fc8e8e..779a1086e5 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -1799,13 +1799,18 @@ describe('Introspection', () => { }); it('identifies deprecated directives', () => { - const schema = buildASTSchema(parse(` + const schema = buildASTSchema( + parse( + ` type Query { someField: String } directive @isNotDeprecated on FIELD_DEFINITION directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION - `, { experimentalDirectivesOnDirectiveDefinitions: true })); + `, + { experimentalDirectivesOnDirectiveDefinitions: true }, + ), + ); const source = ` { diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index c59e6377e9..9471a19484 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -1321,9 +1321,12 @@ describe('extendSchema', () => { it('extend directive to make it deprecated', () => { const schema = buildSchema('directive @isDeprecated on FIELD_DEFINITION'); - const extendAST = parse(` + const extendAST = parse( + ` extend directive @isDeprecated @deprecated(reason: "use another directive") - `, { experimentalDirectivesOnDirectiveDefinitions: true }); + `, + { experimentalDirectivesOnDirectiveDefinitions: true }, + ); const extendedSchema = extendSchema(schema, extendAST); const someDirective = assertDirective( From ac59fc19c5a12a449712f6708cc0011809fc1f1a Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 17 Mar 2026 16:35:38 +0100 Subject: [PATCH 05/26] fix test coverage --- src/utilities/__tests__/extendSchema-test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 9471a19484..fff645e4b8 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -1336,5 +1336,13 @@ describe('extendSchema', () => { deprecationReason: 'use another directive', }); }); + + it('extend directive without adding new directives is an error', () => { + expect(() => + parse('extend directive @isDeprecated', { + experimentalDirectivesOnDirectiveDefinitions: true, + }), + ).to.throw('Syntax Error: Unexpected .'); + }); }); }); From 46a73d45289356adb0c1a39667e91f6e899f4c56 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 17 Mar 2026 16:39:50 +0100 Subject: [PATCH 06/26] fix imports --- src/type/__tests__/introspection-test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 779a1086e5..b5f8231a50 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -3,13 +3,14 @@ import { describe, it } from 'mocha'; import { expectJSON } from '../../__testUtils__/expectJSON'; +import { parse } from '../../language/parser'; + import { buildASTSchema, buildSchema } from '../../utilities/buildASTSchema'; import { getIntrospectionQuery } from '../../utilities/getIntrospectionQuery'; import { graphqlSync } from '../../graphql'; import type { GraphQLResolveInfo } from '../definition'; -import { parse } from '../..'; describe('Introspection', () => { it('executes an introspection query', () => { From 3d0a91c0f8ad6ba726292887fd05e8bbc9fe064d Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 17 Mar 2026 16:51:50 +0100 Subject: [PATCH 07/26] try to fix coverage --- src/language/__tests__/printer-test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 050cd5bdfa..82aac1aad5 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -122,6 +122,16 @@ describe('Printer: Query document', () => { `); }); + it('Experimental: prints directives on directives', () => { + const queryASTWithVariableDirective = parse( + 'directive @foo @bar on FIELD_DEFINITION', + { experimentalDirectivesOnDirectiveDefinitions: true }, + ); + expect(print(queryASTWithVariableDirective)).to.equal(dedent` + directive @foo @bar on FIELD_DEFINITION + `); + }); + it('Legacy: correctly prints fragment defined variables', () => { const fragmentWithVariable = parse( ` From 8db915f57d468288371de73745ceb4d3152c98fa Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 17 Mar 2026 16:57:37 +0100 Subject: [PATCH 08/26] try to make test coverage pass --- src/language/__tests__/printer-test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 82aac1aad5..2ab99d7f84 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -124,11 +124,16 @@ describe('Printer: Query document', () => { it('Experimental: prints directives on directives', () => { const queryASTWithVariableDirective = parse( - 'directive @foo @bar on FIELD_DEFINITION', + ` + directive @foo @bar on FIELD_DEFINITION + extend directive @foo @baz + `, { experimentalDirectivesOnDirectiveDefinitions: true }, ); expect(print(queryASTWithVariableDirective)).to.equal(dedent` directive @foo @bar on FIELD_DEFINITION + + extend directive @foo @baz `); }); From 64a56981a9365822faf6e00609468c9fb99acbc3 Mon Sep 17 00:00:00 2001 From: Martin Bonnin Date: Tue, 17 Mar 2026 17:05:04 +0100 Subject: [PATCH 09/26] make prettier happy --- src/language/__tests__/printer-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/__tests__/printer-test.ts b/src/language/__tests__/printer-test.ts index 2ab99d7f84..81824dd7be 100644 --- a/src/language/__tests__/printer-test.ts +++ b/src/language/__tests__/printer-test.ts @@ -122,7 +122,7 @@ describe('Printer: Query document', () => { `); }); - it('Experimental: prints directives on directives', () => { + it('Experimental: prints directives on directives', () => { const queryASTWithVariableDirective = parse( ` directive @foo @bar on FIELD_DEFINITION From e91532e5059c34eaa98ae086af4c8311a7843392 Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Fri, 3 Apr 2026 17:03:09 +0200 Subject: [PATCH 10/26] Introspection: change `includeDeprecated: Boolean` to `Boolean!` --- src/type/__tests__/introspection-test.ts | 10 +++++++--- src/type/introspection.ts | 5 ++++- src/utilities/__tests__/printSchema-test.ts | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index b5f8231a50..099afb855d 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -162,9 +162,13 @@ describe('Introspection', () => { { name: 'includeDeprecated', type: { - kind: 'SCALAR', - name: 'Boolean', - ofType: null, + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'Boolean', + ofType: null, + }, }, defaultValue: 'false', }, diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 29688395c8..86ad2218f5 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -73,7 +73,10 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ new GraphQLList(new GraphQLNonNull(__Directive)), ), args: { - includeDeprecated: { type: GraphQLBoolean, defaultValue: false }, + includeDeprecated: { + type: new GraphQLNonNull(GraphQLBoolean), + defaultValue: false, + }, }, resolve: (schema, { includeDeprecated }) => includeDeprecated diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 1b45120ef2..94197f0064 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(includeDeprecated: Boolean = false): [__Directive!]! + directives(includeDeprecated: Boolean! = false): [__Directive!]! } """ From c2dbb78268e7414261c49a8973981921704bbe30 Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Fri, 3 Apr 2026 17:14:59 +0200 Subject: [PATCH 11/26] Fix indent --- src/type/__tests__/introspection-test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 099afb855d..3c5ed71ff6 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -1807,12 +1807,12 @@ describe('Introspection', () => { const schema = buildASTSchema( parse( ` - type Query { - someField: String - } - directive @isNotDeprecated on FIELD_DEFINITION - directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION - `, + type Query { + someField: String + } + directive @isNotDeprecated on FIELD_DEFINITION + directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION + `, { experimentalDirectivesOnDirectiveDefinitions: true }, ), ); From a15d81bd89e031e1534f9361418af78db9ae5cb3 Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Fri, 3 Apr 2026 17:47:13 +0200 Subject: [PATCH 12/26] Add an introspection test for multiple directives with arguments applied to a directive --- src/type/__tests__/introspection-test.ts | 86 ++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 3c5ed71ff6..1ccac4fc83 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -1873,4 +1873,90 @@ describe('Introspection', () => { }, }); }); + + it('supports multiple directives with arguments applied to a directive definition', () => { + const schema = buildASTSchema( + parse( + ` + type Query { + someField: String + } + directive @foo(arg: String) repeatable on DIRECTIVE_DEFINITION + directive @bar(arg: String) on DIRECTIVE_DEFINITION + directive @baz(arg: String) @foo(arg: "foo1") @foo(arg: "foo2") @bar(arg: "bar") on FIELD_DEFINITION + `, + { experimentalDirectivesOnDirectiveDefinitions: true }, + ), + ); + + const source = ` + { + __schema { + directives { + name + isRepeatable + isDeprecated + deprecationReason + } + } + } + `; + + expect(graphqlSync({ schema, source })).to.deep.equal({ + data: { + __schema: { + directives: [ + { + name: 'foo', + isRepeatable: true, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'bar', + isRepeatable: false, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'baz', + isRepeatable: false, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'include', + isRepeatable: false, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'skip', + isRepeatable: false, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'deprecated', + isRepeatable: false, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'specifiedBy', + isRepeatable: false, + isDeprecated: false, + deprecationReason: null, + }, + { + name: 'oneOf', + isRepeatable: false, + isDeprecated: false, + deprecationReason: null, + }, + ], + }, + }, + }); + }); }); From e34401af7474eea87ffc9297e526335356a863da Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Tue, 7 Apr 2026 10:44:30 +0200 Subject: [PATCH 13/26] Update src/language/parser.ts Co-authored-by: Jerel Miller --- src/language/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/parser.ts b/src/language/parser.ts index 2f93832388..c6f8afd615 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -1220,7 +1220,7 @@ export class Parser { return this.parseInputObjectTypeExtension(); case 'directive': if ( - this._options.experimentalDirectivesOnDirectiveDefinitions === true + this._options.experimentalDirectivesOnDirectiveDefinitions ) { return this.parseDirectiveDefinitionExtension(); } From 9329e48a3f8e95f15f08b483a460a48b059c90a7 Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Tue, 7 Apr 2026 10:44:48 +0200 Subject: [PATCH 14/26] Update src/language/parser.ts Co-authored-by: Jerel Miller --- src/language/parser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/parser.ts b/src/language/parser.ts index c6f8afd615..dd6ab5299d 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -1438,7 +1438,7 @@ export class Parser { const name = this.parseName(); const args = this.parseArgumentDefs(); const directives = - this._options.experimentalDirectivesOnDirectiveDefinitions === true + this._options.experimentalDirectivesOnDirectiveDefinitions ? this.parseConstDirectives() : []; const repeatable = this.expectOptionalKeyword('repeatable'); From 30f7d58053dc07c86bee19fee081997df806a142 Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Tue, 7 Apr 2026 10:46:23 +0200 Subject: [PATCH 15/26] Update src/utilities/__tests__/extendSchema-test.ts Co-authored-by: Jerel Miller --- src/utilities/__tests__/extendSchema-test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index fff645e4b8..21f53df0f2 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -1329,10 +1329,10 @@ describe('extendSchema', () => { ); const extendedSchema = extendSchema(schema, extendAST); - const someDirective = assertDirective( + const isDeprecatedDirective = assertDirective( extendedSchema.getDirective('isDeprecated'), ); - expect(someDirective).to.include({ + expect(isDeprecatedDirective).to.include({ deprecationReason: 'use another directive', }); }); From edcc32406c0db82ead134479b0a552828d63378a Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Tue, 7 Apr 2026 10:56:21 +0200 Subject: [PATCH 16/26] Update src/utilities/extendSchema.ts Co-authored-by: Jerel Miller --- src/utilities/extendSchema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index dfa68e2907..ee251882e2 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -139,7 +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); + const directiveExtensionsMap: Record = 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". From 788f09030f3f35cd6469862ea7ecac2406cb1f76 Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Tue, 7 Apr 2026 11:10:51 +0200 Subject: [PATCH 17/26] Update src/utilities/extendSchema.ts Co-authored-by: Jerel Miller --- src/utilities/extendSchema.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index ee251882e2..bb099a2eec 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -215,7 +215,8 @@ export function extendSchemaImpl( }; // Then produce and return a Schema config with these types. - const directives: Array = Object.values(directiveMap); + const directives = Object.values(directiveMap); + // will be `Array` return { description: schemaDef?.description?.value, ...operationTypes, From e230a8c330a8daa83f1b5edf054a0888cba967b7 Mon Sep 17 00:00:00 2001 From: Benoit 'BoD' Lubek Date: Tue, 7 Apr 2026 11:12:29 +0200 Subject: [PATCH 18/26] Apply a few minor fixes suggested by Jerel --- src/language/parser.ts | 12 +++++------- src/utilities/extendSchema.ts | 14 ++++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/language/parser.ts b/src/language/parser.ts index dd6ab5299d..96e083e1dc 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -1219,9 +1219,7 @@ export class Parser { case 'input': return this.parseInputObjectTypeExtension(); case 'directive': - if ( - this._options.experimentalDirectivesOnDirectiveDefinitions - ) { + if (this._options.experimentalDirectivesOnDirectiveDefinitions) { return this.parseDirectiveDefinitionExtension(); } break; @@ -1437,10 +1435,10 @@ export class Parser { this.expectToken(TokenKind.AT); const name = this.parseName(); const args = this.parseArgumentDefs(); - const directives = - this._options.experimentalDirectivesOnDirectiveDefinitions - ? this.parseConstDirectives() - : []; + const directives = this._options + .experimentalDirectivesOnDirectiveDefinitions + ? this.parseConstDirectives() + : []; const repeatable = this.expectOptionalKeyword('repeatable'); this.expectKeyword('on'); const locations = this.parseDirectiveLocations(); diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index bb099a2eec..7658301282 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -139,7 +139,9 @@ export function extendSchemaImpl( // Collect the type definitions and extensions found in the document. const typeDefs: Array = []; const typeExtensionsMap = Object.create(null); - const directiveExtensionsMap: Record = Object.create(null); + const directiveExtensionsMap: { + [key: string]: Array; + } = 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". @@ -198,7 +200,7 @@ export function extendSchemaImpl( typeMap[name] = stdTypeMap[name] ?? buildType(typeNode); } - const directiveMap = Object.create(null); + const directiveMap: { [key: string]: GraphQLDirective } = Object.create(null); for (const existingDirective of schemaConfig.directives) { directiveMap[existingDirective.name] = extendDirective(existingDirective); } @@ -436,13 +438,13 @@ export function extendSchemaImpl( 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); + const deprecationReason = extensions + .map((ext) => getDeprecationReason(ext)) + .find((reason) => reason != null); return new GraphQLDirective({ ...config, - deprecationReason: deprecatedReason, + deprecationReason, extensionASTNodes: config.extensionASTNodes.concat(extensions), }); } From 1eb2a68e94982e07b7fcf2b1936204973fd1745c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 10:38:41 +0300 Subject: [PATCH 19/26] Forward parse options through buildSchema --- src/utilities/__tests__/buildASTSchema-test.ts | 18 ++++++++++++++++++ src/utilities/buildASTSchema.ts | 2 ++ 2 files changed, 20 insertions(+) diff --git a/src/utilities/__tests__/buildASTSchema-test.ts b/src/utilities/__tests__/buildASTSchema-test.ts index 29280474ec..21de5434c2 100644 --- a/src/utilities/__tests__/buildASTSchema-test.ts +++ b/src/utilities/__tests__/buildASTSchema-test.ts @@ -1088,6 +1088,24 @@ describe('Schema Builder', () => { buildSchema(sdl, { assumeValidSDL: true }); }); + it('Forwards parser options to buildSchema', () => { + const schema = buildSchema( + dedent` + type Query { + foo: String + } + + directive @bar @deprecated(reason: "Use another directive") on FIELD_DEFINITION + `, + { experimentalDirectivesOnDirectiveDefinitions: true }, + ); + + const barDirective = assertDirective(schema.getDirective('bar')); + expect(barDirective).to.include({ + deprecationReason: 'Use another directive', + }); + }); + it('Throws on unknown types', () => { const sdl = ` type Query { diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index eeff08e6ed..64494fbcd0 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -102,6 +102,8 @@ export function buildSchema( const document = parse(source, { noLocation: options?.noLocation, allowLegacyFragmentVariables: options?.allowLegacyFragmentVariables, + experimentalDirectivesOnDirectiveDefinitions: + options?.experimentalDirectivesOnDirectiveDefinitions, }); return buildASTSchema(document, { From a0349be386f50480e59d7da9369a4fb021193344 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 10:40:41 +0300 Subject: [PATCH 20/26] Preserve directive deprecations in extendSchema --- src/utilities/__tests__/extendSchema-test.ts | 30 +++++++++++++++++++- src/utilities/extendSchema.ts | 8 ++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 21f53df0f2..7cd8d262c2 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -31,7 +31,7 @@ import { validateSchema } from '../../type/validate'; import { graphqlSync } from '../../graphql'; -import { buildSchema } from '../buildASTSchema'; +import { buildASTSchema, buildSchema } from '../buildASTSchema'; import { concatAST } from '../concatAST'; import { extendSchema } from '../extendSchema'; import { printSchema } from '../printSchema'; @@ -1337,6 +1337,34 @@ describe('extendSchema', () => { }); }); + it('preserves deprecated directives when extending other types', () => { + const schema = buildASTSchema( + parse( + dedent` + type Query { + foo: String + } + + directive @isDeprecated @deprecated(reason: "use another directive") on FIELD_DEFINITION + `, + { experimentalDirectivesOnDirectiveDefinitions: true }, + ), + ); + const extendAST = parse(dedent` + extend type Query { + bar: Int + } + `); + const extendedSchema = extendSchema(schema, extendAST); + + const isDeprecatedDirective = assertDirective( + extendedSchema.getDirective('isDeprecated'), + ); + expect(isDeprecatedDirective).to.include({ + deprecationReason: 'use another directive', + }); + }); + it('extend directive without adding new directives is an error', () => { expect(() => parse('extend directive @isDeprecated', { diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 7658301282..599cd65d7e 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -438,9 +438,11 @@ export function extendSchemaImpl( function extendDirective(directive: GraphQLDirective): GraphQLDirective { const config = directive.toConfig(); const extensions = directiveExtensionsMap[config.name] ?? []; - const deprecationReason = extensions - .map((ext) => getDeprecationReason(ext)) - .find((reason) => reason != null); + const deprecationReason = + config.deprecationReason ?? + extensions + .map((ext) => getDeprecationReason(ext)) + .find((reason) => reason != null); return new GraphQLDirective({ ...config, From 7b9b5810426e6bd139690e659d42ea93e5147626 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 10:41:38 +0300 Subject: [PATCH 21/26] Apply same-document directive extensions --- src/utilities/__tests__/extendSchema-test.ts | 46 ++++++++++++++++++++ src/utilities/extendSchema.ts | 10 ++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/utilities/__tests__/extendSchema-test.ts b/src/utilities/__tests__/extendSchema-test.ts index 7cd8d262c2..7891a47793 100644 --- a/src/utilities/__tests__/extendSchema-test.ts +++ b/src/utilities/__tests__/extendSchema-test.ts @@ -1365,6 +1365,52 @@ describe('extendSchema', () => { }); }); + it('applies directive extensions defined in the same document', () => { + const schema = buildASTSchema( + parse( + dedent` + directive @onDirective on DIRECTIVE_DEFINITION + directive @someDirective on FIELD_DEFINITION + + extend directive @someDirective @onDirective + `, + { experimentalDirectivesOnDirectiveDefinitions: true }, + ), + ); + + const someDirective = assertDirective( + schema.getDirective('someDirective'), + ); + expectExtensionASTNodes(someDirective).to.equal( + 'extend directive @someDirective @onDirective', + ); + }); + + it('applies multiple directive extensions defined in the same document', () => { + const schema = buildASTSchema( + parse( + dedent` + directive @onDirective on DIRECTIVE_DEFINITION + directive @otherDirective on DIRECTIVE_DEFINITION + directive @someDirective on FIELD_DEFINITION + + extend directive @someDirective @onDirective + extend directive @someDirective @otherDirective + `, + { experimentalDirectivesOnDirectiveDefinitions: true }, + ), + ); + + const someDirective = assertDirective( + schema.getDirective('someDirective'), + ); + expectExtensionASTNodes(someDirective).to.equal(dedent` + extend directive @someDirective @onDirective + + extend directive @someDirective @otherDirective + `); + }); + it('extend directive without adding new directives is an error', () => { expect(() => parse('extend directive @isDeprecated', { diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 599cd65d7e..9dc9fc77ee 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -472,6 +472,13 @@ export function extendSchemaImpl( } function buildDirective(node: DirectiveDefinitionNode): GraphQLDirective { + const extensions = directiveExtensionsMap[node.name.value] ?? []; + const deprecationReason = + getDeprecationReason(node) ?? + extensions + .map((ext) => getDeprecationReason(ext)) + .find((reason) => reason != null); + return new GraphQLDirective({ name: node.name.value, description: node.description?.value, @@ -479,8 +486,9 @@ export function extendSchemaImpl( locations: node.locations.map(({ value }) => value), isRepeatable: node.repeatable, args: buildArgumentMap(node.arguments), - deprecationReason: getDeprecationReason(node), + deprecationReason, astNode: node, + extensionASTNodes: extensions, }); } From bd1929c92d221ffb2e3cbb558d1ff6be1cf8b305 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 10:41:57 +0300 Subject: [PATCH 22/26] Print deprecated directives in schemas --- src/utilities/__tests__/printSchema-test.ts | 16 ++++++++++++++++ src/utilities/printSchema.ts | 1 + 2 files changed, 17 insertions(+) diff --git a/src/utilities/__tests__/printSchema-test.ts b/src/utilities/__tests__/printSchema-test.ts index 94197f0064..5b92f43b3c 100644 --- a/src/utilities/__tests__/printSchema-test.ts +++ b/src/utilities/__tests__/printSchema-test.ts @@ -612,6 +612,22 @@ describe('Type System Printer', () => { `); }); + it('Prints deprecated directives', () => { + const schema = new GraphQLSchema({ + directives: [ + new GraphQLDirective({ + name: 'deprecatedDirective', + locations: [DirectiveLocation.FIELD], + deprecationReason: 'Use another directive', + }), + ], + }); + + expect(printSchema(schema)).to.equal(dedent` + directive @deprecatedDirective @deprecated(reason: "Use another directive") on FIELD + `); + }); + it('Prints an empty description', () => { const schema = buildSingleFieldSchema({ type: GraphQLString, diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index edac6262c5..6285c8630d 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -280,6 +280,7 @@ function printDirective(directive: GraphQLDirective): string { 'directive @' + directive.name + printArgs(directive.args) + + printDeprecated(directive.deprecationReason) + (directive.isRepeatable ? ' repeatable' : '') + ' on ' + directive.locations.join(' | ') From 53b88d4a1d812b93975816061a18a6426211af65 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 10:42:36 +0300 Subject: [PATCH 23/26] Add experimental directive deprecation to introspection queries --- .../__tests__/getIntrospectionQuery-test.ts | 44 +++++++++++++++++-- .../__tests__/introspectionFromSchema-test.ts | 35 +++++++++++++++ src/utilities/getIntrospectionQuery.ts | 18 +++++++- src/utilities/introspectionFromSchema.ts | 1 + 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/utilities/__tests__/getIntrospectionQuery-test.ts b/src/utilities/__tests__/getIntrospectionQuery-test.ts index 86d1c549db..656412022c 100644 --- a/src/utilities/__tests__/getIntrospectionQuery-test.ts +++ b/src/utilities/__tests__/getIntrospectionQuery-test.ts @@ -21,18 +21,30 @@ function expectIntrospectionQuery(options?: IntrospectionOptions) { const validationErrors = validate(dummySchema, parse(query)); expect(validationErrors).to.deep.equal([]); - return { - toMatch(name: string, times: number = 1): void { + const helpers = { + toMatch: (name: string, times: number = 1) => { const pattern = toRegExp(name); expect(query).to.match(pattern); expect(query.match(pattern)).to.have.lengthOf(times); + return helpers; }, - toNotMatch(name: string): void { + toContain: (text: string) => { + expect(query).to.include(text); + return helpers; + }, + toNotMatch: (name: string) => { expect(query).to.not.match(toRegExp(name)); + return helpers; + }, + toNotContain: (text: string) => { + expect(query).to.not.include(text); + return helpers; }, }; + return helpers; + function toRegExp(name: string): RegExp { return new RegExp('\\b' + name + '\\b', 'g'); } @@ -138,4 +150,30 @@ describe('getIntrospectionQuery', () => { 2, ); }); + + it('include "isDeprecated" field on directives', () => { + expectIntrospectionQuery().toMatch('isDeprecated', 2); + + expectIntrospectionQuery({ + experimentalDirectiveDeprecation: true, + }).toMatch('isDeprecated', 3); + + expectIntrospectionQuery({ + experimentalDirectiveDeprecation: false, + }).toMatch('isDeprecated', 2); + }); + + it('include "deprecationReason" field on directives', () => { + expectIntrospectionQuery() + .toNotContain('directives(includeDeprecated: true) {') + .toMatch('deprecationReason', 2); + + expectIntrospectionQuery({ experimentalDirectiveDeprecation: true }) + .toContain('directives(includeDeprecated: true) {') + .toMatch('deprecationReason', 3); + + expectIntrospectionQuery({ experimentalDirectiveDeprecation: false }) + .toNotContain('directives(includeDeprecated: true) {') + .toMatch('deprecationReason', 2); + }); }); diff --git a/src/utilities/__tests__/introspectionFromSchema-test.ts b/src/utilities/__tests__/introspectionFromSchema-test.ts index 2ba66348d3..ae8a0df6bf 100644 --- a/src/utilities/__tests__/introspectionFromSchema-test.ts +++ b/src/utilities/__tests__/introspectionFromSchema-test.ts @@ -3,7 +3,10 @@ import { describe, it } from 'mocha'; import { dedent } from '../../__testUtils__/dedent'; +import { DirectiveLocation } from '../../language/directiveLocation'; + import { GraphQLObjectType } from '../../type/definition'; +import { GraphQLDirective } from '../../type/directives'; import { GraphQLString } from '../../type/scalars'; import { GraphQLSchema } from '../../type/schema'; @@ -63,4 +66,36 @@ describe('introspectionFromSchema', () => { } `); }); + + it('includes deprecated directives', () => { + const schemaWithDeprecatedDirective = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + string: { + type: GraphQLString, + }, + }, + }), + directives: [ + new GraphQLDirective({ + name: 'deprecatedDirective', + locations: [DirectiveLocation.QUERY], + deprecationReason: 'Use another directive', + }), + ], + }); + const introspection = introspectionFromSchema( + schemaWithDeprecatedDirective, + ); + const deprecatedDirective = introspection.__schema.directives.find( + ({ name }) => name === 'deprecatedDirective', + ); + + expect(deprecatedDirective).to.deep.include({ + name: 'deprecatedDirective', + isDeprecated: true, + deprecationReason: 'Use another directive', + }); + }); }); diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 373b474ed5..d4bca051c1 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -33,6 +33,12 @@ export interface IntrospectionOptions { */ inputValueDeprecation?: boolean; + /** + * Whether target GraphQL server supports deprecation of directives. + * Default: false + */ + experimentalDirectiveDeprecation?: boolean; + /** * Whether target GraphQL server supports `@oneOf` input objects. * Default: false @@ -51,6 +57,7 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { directiveIsRepeatable: false, schemaDescription: false, inputValueDeprecation: false, + experimentalDirectiveDeprecation: false, oneOf: false, ...options, }; @@ -69,6 +76,9 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { function inputDeprecation(str: string) { return optionsWithDefault.inputValueDeprecation ? str : ''; } + function experimentalDirectiveDeprecation(str: string) { + return optionsWithDefault.experimentalDirectiveDeprecation ? str : ''; + } const oneOf = optionsWithDefault.oneOf ? 'isOneOf' : ''; return ` @@ -81,10 +91,14 @@ export function getIntrospectionQuery(options?: IntrospectionOptions): string { types { ...FullType } - directives { + directives${experimentalDirectiveDeprecation( + '(includeDeprecated: true)', + )} { name ${descriptions} ${directiveIsRepeatable} + ${experimentalDirectiveDeprecation('isDeprecated')} + ${experimentalDirectiveDeprecation('deprecationReason')} locations args${inputDeprecation('(includeDeprecated: true)')} { ...InputValue @@ -344,6 +358,8 @@ export interface IntrospectionDirective { readonly name: string; readonly description?: Maybe; readonly isRepeatable?: boolean; + readonly isDeprecated?: boolean; + readonly deprecationReason?: Maybe; readonly locations: ReadonlyArray; readonly args: ReadonlyArray; } diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index 5f58248363..e6b4a50db1 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -30,6 +30,7 @@ export function introspectionFromSchema( directiveIsRepeatable: true, schemaDescription: true, inputValueDeprecation: true, + experimentalDirectiveDeprecation: true, oneOf: true, ...options, }; From da43e82893182e5070b4e785d1bffb60662f09ad Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 10:43:08 +0300 Subject: [PATCH 24/26] Import directive deprecations from introspection --- .../__tests__/buildClientSchema-test.ts | 29 +++++++++++++++++++ src/utilities/buildClientSchema.ts | 1 + 2 files changed, 30 insertions(+) diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index e8cf046921..24d0c2e9b0 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -5,11 +5,14 @@ import { dedent } from '../../__testUtils__/dedent'; import { invariant } from '../../jsutils/invariant'; +import { DirectiveLocation } from '../../language/directiveLocation'; + import { assertEnumType, GraphQLEnumType, GraphQLObjectType, } from '../../type/definition'; +import { GraphQLDirective } from '../../type/directives'; import { GraphQLBoolean, GraphQLFloat, @@ -465,6 +468,32 @@ describe('Type System: build schema from introspection', () => { expect(cycleIntrospection(sdl)).to.equal(sdl); }); + it('builds a schema with deprecated directives', () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + string: { type: GraphQLString }, + }, + }), + directives: [ + new GraphQLDirective({ + name: 'someDirective', + locations: [DirectiveLocation.QUERY], + deprecationReason: 'Use another directive', + }), + ], + }); + const introspection = introspectionFromSchema(schema); + + const clientSchema = buildClientSchema(introspection); + + expect(clientSchema.getDirective('someDirective')).to.deep.include({ + name: 'someDirective', + deprecationReason: 'Use another directive', + }); + }); + it('builds a schema without directives', () => { const sdl = dedent` type Query { diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 83f6abada8..0c0566beda 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -400,6 +400,7 @@ export function buildClientSchema( name: directiveIntrospection.name, description: directiveIntrospection.description, isRepeatable: directiveIntrospection.isRepeatable, + deprecationReason: directiveIntrospection.deprecationReason, locations: directiveIntrospection.locations.slice(), args: buildInputValueDefMap(directiveIntrospection.args), }); From a475823383d3c84d3dd6930e36cafa31f3947671 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 10:55:38 +0300 Subject: [PATCH 25/26] Refine directive introspection tests --- src/type/__tests__/introspection-test.ts | 97 ++++++++++-------------- 1 file changed, 40 insertions(+), 57 deletions(-) diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index 1ccac4fc83..ccdcae7ddd 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -1812,6 +1812,7 @@ describe('Introspection', () => { } directive @isNotDeprecated on FIELD_DEFINITION directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION + directive @isDeprecatedWithEmptyReason @deprecated(reason: "") on FIELD_DEFINITION `, { experimentalDirectivesOnDirectiveDefinitions: true }, ), @@ -1843,6 +1844,11 @@ describe('Introspection', () => { isDeprecated: true, deprecationReason: 'No longer supported', }, + { + name: 'isDeprecatedWithEmptyReason', + isDeprecated: true, + deprecationReason: '', + }, { name: 'include', isDeprecated: false, @@ -1874,16 +1880,15 @@ describe('Introspection', () => { }); }); - it('supports multiple directives with arguments applied to a directive definition', () => { + it('respects the includeDeprecated parameter for directives', () => { const schema = buildASTSchema( parse( ` type Query { someField: String } - directive @foo(arg: String) repeatable on DIRECTIVE_DEFINITION - directive @bar(arg: String) on DIRECTIVE_DEFINITION - directive @baz(arg: String) @foo(arg: "foo1") @foo(arg: "foo2") @bar(arg: "bar") on FIELD_DEFINITION + directive @isNotDeprecated on FIELD_DEFINITION + directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION `, { experimentalDirectivesOnDirectiveDefinitions: true }, ), @@ -1892,11 +1897,14 @@ describe('Introspection', () => { const source = ` { __schema { - directives { + trueDirectives: directives(includeDeprecated: true) { + name + } + falseDirectives: directives(includeDeprecated: false) { + name + } + omittedDirectives: directives { name - isRepeatable - isDeprecated - deprecationReason } } } @@ -1905,55 +1913,30 @@ describe('Introspection', () => { expect(graphqlSync({ schema, source })).to.deep.equal({ data: { __schema: { - directives: [ - { - name: 'foo', - isRepeatable: true, - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'bar', - isRepeatable: false, - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'baz', - isRepeatable: false, - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'include', - isRepeatable: false, - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'skip', - isRepeatable: false, - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'deprecated', - isRepeatable: false, - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'specifiedBy', - isRepeatable: false, - isDeprecated: false, - deprecationReason: null, - }, - { - name: 'oneOf', - isRepeatable: false, - isDeprecated: false, - deprecationReason: null, - }, + trueDirectives: [ + { name: 'isNotDeprecated' }, + { name: 'isDeprecated' }, + { name: 'include' }, + { name: 'skip' }, + { name: 'deprecated' }, + { name: 'specifiedBy' }, + { name: 'oneOf' }, + ], + falseDirectives: [ + { name: 'isNotDeprecated' }, + { name: 'include' }, + { name: 'skip' }, + { name: 'deprecated' }, + { name: 'specifiedBy' }, + { name: 'oneOf' }, + ], + omittedDirectives: [ + { name: 'isNotDeprecated' }, + { name: 'include' }, + { name: 'skip' }, + { name: 'deprecated' }, + { name: 'specifiedBy' }, + { name: 'oneOf' }, ], }, }, From e287ba1bf7fdcfeac6576f7950b6b0e52e98332b Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Fri, 24 Apr 2026 12:40:00 +0300 Subject: [PATCH 26/26] Track directive definition directives across extensions --- .../UniqueDirectivesPerLocationRule-test.ts | 81 +++++++++++++++++++ .../rules/UniqueDirectivesPerLocationRule.ts | 11 +++ 2 files changed, 92 insertions(+) diff --git a/src/validation/__tests__/UniqueDirectivesPerLocationRule-test.ts b/src/validation/__tests__/UniqueDirectivesPerLocationRule-test.ts index d57a3df684..964d27282a 100644 --- a/src/validation/__tests__/UniqueDirectivesPerLocationRule-test.ts +++ b/src/validation/__tests__/UniqueDirectivesPerLocationRule-test.ts @@ -1,5 +1,7 @@ import { describe, it } from 'mocha'; +import { expectJSON } from '../../__testUtils__/expectJSON'; + import { parse } from '../../language/parser'; import type { GraphQLSchema } from '../../type/schema'; @@ -7,6 +9,7 @@ import type { GraphQLSchema } from '../../type/schema'; import { extendSchema } from '../../utilities/extendSchema'; import { UniqueDirectivesPerLocationRule } from '../rules/UniqueDirectivesPerLocationRule'; +import { validateSDL } from '../validate'; import { expectSDLValidationErrors, @@ -42,6 +45,14 @@ function expectSDLErrors(sdlStr: string, schema?: GraphQLSchema) { ); } +function expectExperimentalSDLErrors(sdlStr: string, schema?: GraphQLSchema) { + const doc = parse(sdlStr, { + experimentalDirectivesOnDirectiveDefinitions: true, + }); + const errors = validateSDL(doc, schema, [UniqueDirectivesPerLocationRule]); + return expectJSON(errors); +} + describe('Validate: Directives Are Unique Per Location', () => { it('no directives', () => { expectValid(` @@ -391,4 +402,74 @@ describe('Validate: Directives Are Unique Per Location', () => { }, ]); }); + + it('duplicate directives on directive definitions', () => { + expectExperimentalSDLErrors(` + directive @nonRepeatable on DIRECTIVE_DEFINITION + + directive @testDirective @nonRepeatable @nonRepeatable on FIELD_DEFINITION + `).toDeepEqual([ + { + message: + 'The directive "@nonRepeatable" can only be used once at this location.', + locations: [ + { line: 4, column: 32 }, + { line: 4, column: 47 }, + ], + }, + ]); + }); + + it('duplicate directives on directive extensions', () => { + expectExperimentalSDLErrors(` + directive @nonRepeatable on DIRECTIVE_DEFINITION + + extend directive @testDirective @nonRepeatable @nonRepeatable + `).toDeepEqual([ + { + message: + 'The directive "@nonRepeatable" can only be used once at this location.', + locations: [ + { line: 4, column: 39 }, + { line: 4, column: 54 }, + ], + }, + ]); + }); + + it('duplicate directives between directive definitions and extensions', () => { + expectExperimentalSDLErrors(` + directive @nonRepeatable on DIRECTIVE_DEFINITION + + directive @testDirective @nonRepeatable on FIELD_DEFINITION + extend directive @testDirective @nonRepeatable + `).toDeepEqual([ + { + message: + 'The directive "@nonRepeatable" can only be used once at this location.', + locations: [ + { line: 4, column: 32 }, + { line: 5, column: 39 }, + ], + }, + ]); + }); + + it('duplicate directives between directive extensions', () => { + expectExperimentalSDLErrors(` + directive @nonRepeatable on DIRECTIVE_DEFINITION + + extend directive @testDirective @nonRepeatable + extend directive @testDirective @nonRepeatable + `).toDeepEqual([ + { + message: + 'The directive "@nonRepeatable" can only be used once at this location.', + locations: [ + { line: 4, column: 39 }, + { line: 5, column: 39 }, + ], + }, + ]); + }); }); diff --git a/src/validation/rules/UniqueDirectivesPerLocationRule.ts b/src/validation/rules/UniqueDirectivesPerLocationRule.ts index a4fc54690a..9ceeb8ecb0 100644 --- a/src/validation/rules/UniqueDirectivesPerLocationRule.ts +++ b/src/validation/rules/UniqueDirectivesPerLocationRule.ts @@ -44,6 +44,7 @@ export function UniqueDirectivesPerLocationRule( const schemaDirectives = Object.create(null); const typeDirectivesMap = Object.create(null); + const directiveDirectivesMap = Object.create(null); return { // Many different AST nodes may contain directives. Rather than listing @@ -66,6 +67,16 @@ export function UniqueDirectivesPerLocationRule( if (seenDirectives === undefined) { typeDirectivesMap[typeName] = seenDirectives = Object.create(null); } + } else if ( + node.kind === Kind.DIRECTIVE_DEFINITION || + node.kind === Kind.DIRECTIVE_EXTENSION + ) { + const directiveName = node.name.value; + seenDirectives = directiveDirectivesMap[directiveName]; + if (seenDirectives === undefined) { + directiveDirectivesMap[directiveName] = seenDirectives = + Object.create(null); + } } else { seenDirectives = Object.create(null); }