From ebc31f3d63d0c772ccc485af5931c9ee4df7b301 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Sat, 22 Nov 2025 04:58:12 +0300 Subject: [PATCH] fix: check the actual `GraphQLDirective` instance in `isSpecifiedDirective` --- src/type/__tests__/predicate-test.ts | 8 +++ src/type/directives.ts | 4 +- src/utilities/buildClientSchema.ts | 69 +++++++++++++++++++++++- src/utilities/extendSchema.ts | 63 ++++++++++++++++++++++ src/utilities/lexicographicSortSchema.ts | 5 +- 5 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/type/__tests__/predicate-test.ts b/src/type/__tests__/predicate-test.ts index 81e721e7df..95a7495de6 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -700,5 +700,13 @@ describe('Directive predicates', () => { it('returns false for custom directive', () => { expect(isSpecifiedDirective(Directive)).to.equal(false); }); + + it('returns false for the directives with the same name as specified directives', () => { + const FakeSkipDirective = new GraphQLDirective({ + name: 'skip', + locations: [DirectiveLocation.QUERY], + }); + expect(isSpecifiedDirective(FakeSkipDirective)).to.equal(false); + }); }); }); diff --git a/src/type/directives.ts b/src/type/directives.ts index 6881f20532..5c0c9c9d1c 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -233,5 +233,7 @@ export const specifiedDirectives: ReadonlyArray = ]); export function isSpecifiedDirective(directive: GraphQLDirective): boolean { - return specifiedDirectives.some(({ name }) => name === directive.name); + return specifiedDirectives.some( + (specifiedDirective) => specifiedDirective === directive, + ); } diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 83f6abada8..ddab0b427b 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -24,9 +24,10 @@ import { GraphQLScalarType, GraphQLUnionType, isInputType, + isNamedType, isOutputType, } from '../type/definition'; -import { GraphQLDirective } from '../type/directives'; +import { GraphQLDirective, specifiedDirectives } from '../type/directives'; import { introspectionTypes, TypeKind } from '../type/introspection'; import { specifiedScalarTypes } from '../type/scalars'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; @@ -381,6 +382,66 @@ export function buildClientSchema( }; } + function getSpecifiedDirectiveFromIntrospection( + directiveIntrospection: IntrospectionDirective, + ): GraphQLDirective | undefined { + const possibleSpecifiedDirective = specifiedDirectives.find( + (dir) => dir.name === directiveIntrospection.name, + ); + if (possibleSpecifiedDirective == null) { + return; + } + + for (const location of directiveIntrospection.locations) { + if (!possibleSpecifiedDirective.locations.includes(location)) { + return; + } + } + + for (const arg of directiveIntrospection.args) { + const possibleArg = possibleSpecifiedDirective.args.find( + (a) => a.name === arg.name, + ); + if (possibleArg == null) { + return; + } + const argType = getType(arg.type); + // Is same type + let currentType = argType; + let expectedType = possibleArg.type; + // eslint-disable-next-line no-constant-condition + while (true) { + if (currentType instanceof GraphQLNonNull) { + if (expectedType instanceof GraphQLNonNull) { + currentType = currentType.ofType; + expectedType = expectedType.ofType; + continue; + } else { + return; + } + } + if (currentType instanceof GraphQLList) { + if (expectedType instanceof GraphQLList) { + currentType = currentType.ofType; + expectedType = expectedType.ofType; + continue; + } else { + return; + } + } + if (!isNamedType(currentType) || !isNamedType(expectedType)) { + return; + } + if (currentType !== expectedType) { + return; + } + break; + } + } + + return possibleSpecifiedDirective; + } + function buildDirective( directiveIntrospection: IntrospectionDirective, ): GraphQLDirective { @@ -396,6 +457,12 @@ export function buildClientSchema( `Introspection result missing directive locations: ${directiveIntrospectionStr}.`, ); } + const specifiedDirective = getSpecifiedDirectiveFromIntrospection( + directiveIntrospection, + ); + if (specifiedDirective != null) { + return specifiedDirective; + } return new GraphQLDirective({ name: directiveIntrospection.name, description: directiveIntrospection.description, diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index d53752d919..e0b3b4cdc9 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -68,6 +68,8 @@ import { GraphQLDirective, GraphQLOneOfDirective, GraphQLSpecifiedByDirective, + isSpecifiedDirective, + specifiedDirectives, } from '../type/directives'; import { introspectionTypes, isIntrospectionType } from '../type/introspection'; import { isSpecifiedScalarType, specifiedScalarTypes } from '../type/scalars'; @@ -237,6 +239,9 @@ export function extendSchemaImpl( } function replaceDirective(directive: GraphQLDirective): GraphQLDirective { + if (isSpecifiedDirective(directive)) { + return directive; + } const config = directive.toConfig(); return new GraphQLDirective({ ...config, @@ -435,7 +440,65 @@ export function extendSchemaImpl( return getNamedType(node); } + function findSpecifiedDirectiveNode(node: DirectiveDefinitionNode) { + const possibleDirective = specifiedDirectives.find( + (stdDirective) => stdDirective.name === node.name.value, + ); + if (possibleDirective == null) { + return; + } + if (possibleDirective.description !== node.description?.value) { + return; + } + if (possibleDirective.args.length !== node.arguments?.length) { + return; + } + if (node.arguments?.length > 0) { + for (const argNode of node.arguments) { + const argDef = possibleDirective.args.find( + (stdArg) => stdArg.name === argNode.name.value, + ); + if (argDef == null) { + return; + } + let type = argDef.type; + let argDefType = argNode.type; + // eslint-disable-next-line no-constant-condition + while (true) { + if (isNonNullType(type)) { + if (argDefType.kind !== Kind.NON_NULL_TYPE) { + return; + } + type = type.ofType; + argDefType = argDefType.type; + continue; + } else if (isListType(type)) { + if (argDefType.kind !== Kind.LIST_TYPE) { + return; + } + type = type.ofType; + argDefType = argDefType.type; + continue; + } else { + if (argDefType.kind !== Kind.NAMED_TYPE) { + return; + } + if (type.name !== argDefType.name.value) { + return; + } + break; + } + } + } + } + return possibleDirective; + } + function buildDirective(node: DirectiveDefinitionNode): GraphQLDirective { + const specifiedDirective = findSpecifiedDirectiveNode(node); + if (specifiedDirective != null) { + return specifiedDirective; + } return new GraphQLDirective({ name: node.name.value, description: node.description?.value, diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index 26b6908c9f..d8ad55150b 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -29,7 +29,7 @@ import { isScalarType, isUnionType, } from '../type/definition'; -import { GraphQLDirective } from '../type/directives'; +import { GraphQLDirective, isSpecifiedDirective } from '../type/directives'; import { isIntrospectionType } from '../type/introspection'; import { GraphQLSchema } from '../type/schema'; @@ -78,6 +78,9 @@ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { } function sortDirective(directive: GraphQLDirective) { + if (isSpecifiedDirective(directive)) { + return directive; + } const config = directive.toConfig(); return new GraphQLDirective({ ...config,