From 02864374aa25c4b326673347d4f683ce30af3703 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 16 Aug 2024 04:52:49 +0300 Subject: [PATCH] enhance: directive extraction --- .changeset/stale-weeks-type.md | 7 + packages/merge/src/extensions.ts | 8 +- packages/schema/src/merge-schemas.ts | 7 +- packages/utils/src/get-directives.ts | 37 +---- packages/utils/src/getDirectiveExtensions.ts | 69 ++++++---- .../utils/src/print-schema-with-directives.ts | 129 ++++++++---------- 6 files changed, 113 insertions(+), 144 deletions(-) create mode 100644 .changeset/stale-weeks-type.md diff --git a/.changeset/stale-weeks-type.md b/.changeset/stale-weeks-type.md new file mode 100644 index 00000000000..e6ae454ed84 --- /dev/null +++ b/.changeset/stale-weeks-type.md @@ -0,0 +1,7 @@ +--- +'@graphql-tools/schema': patch +'@graphql-tools/merge': patch +'@graphql-tools/utils': patch +--- + +Improve directive extraction diff --git a/packages/merge/src/extensions.ts b/packages/merge/src/extensions.ts index 0e01d07f3c9..4185c1f5f81 100644 --- a/packages/merge/src/extensions.ts +++ b/packages/merge/src/extensions.ts @@ -11,10 +11,14 @@ function applyExtensionObject( obj: Maybe<{ extensions: Maybe>> }>, extensions: ExtensionsObject, ) { - if (!obj) { + if (!obj || !extensions || extensions === obj.extensions) { return; } - obj.extensions = mergeDeep([obj.extensions || {}, extensions || {}], false, true); + if (!obj.extensions) { + obj.extensions = extensions; + return; + } + obj.extensions = mergeDeep([obj.extensions, extensions], false, true); } export function applyExtensions( diff --git a/packages/schema/src/merge-schemas.ts b/packages/schema/src/merge-schemas.ts index 78ab8dc5387..44c3467cb2e 100644 --- a/packages/schema/src/merge-schemas.ts +++ b/packages/schema/src/merge-schemas.ts @@ -32,12 +32,7 @@ export function mergeSchemas(config: MergeSchemasConfig) { if (config.schemas != null) { for (const schema of config.schemas) { - extractedTypeDefs.push( - getDocumentNodeFromSchema(schema, { - ...config, - pathToDirectivesInExtensions: ['NONEXISTENT'], - }), - ); + extractedTypeDefs.push(getDocumentNodeFromSchema(schema)); extractedResolvers.push(getResolversFromSchema(schema)); extractedSchemaExtensions.push(extractExtensionsFromSchema(schema)); } diff --git a/packages/utils/src/get-directives.ts b/packages/utils/src/get-directives.ts index f3a9663c781..96f65b3db7c 100644 --- a/packages/utils/src/get-directives.ts +++ b/packages/utils/src/get-directives.ts @@ -1,43 +1,12 @@ -import { - GraphQLEnumTypeConfig, - GraphQLEnumValue, - GraphQLEnumValueConfig, - GraphQLField, - GraphQLFieldConfig, - GraphQLInputField, - GraphQLInputFieldConfig, - GraphQLInputObjectTypeConfig, - GraphQLInterfaceTypeConfig, - GraphQLNamedType, - GraphQLObjectTypeConfig, - GraphQLScalarTypeConfig, - GraphQLSchema, - GraphQLSchemaConfig, - GraphQLUnionTypeConfig, -} from 'graphql'; -import { getDirectiveExtensions } from './getDirectiveExtensions.js'; +import { GraphQLSchema } from 'graphql'; +import { DirectableObject, getDirectiveExtensions } from './getDirectiveExtensions.js'; export interface DirectiveAnnotation { name: string; args?: Record; } -export type DirectableGraphQLObject = - | GraphQLSchema - | GraphQLSchemaConfig - | GraphQLNamedType - | GraphQLObjectTypeConfig - | GraphQLInterfaceTypeConfig - | GraphQLUnionTypeConfig - | GraphQLScalarTypeConfig - | GraphQLEnumTypeConfig - | GraphQLEnumValue - | GraphQLEnumValueConfig - | GraphQLInputObjectTypeConfig - | GraphQLField - | GraphQLInputField - | GraphQLFieldConfig - | GraphQLInputFieldConfig; +export type DirectableGraphQLObject = DirectableObject; export function getDirectivesInExtensions( node: DirectableGraphQLObject, diff --git a/packages/utils/src/getDirectiveExtensions.ts b/packages/utils/src/getDirectiveExtensions.ts index 6f1829f7d2a..d0e82269a0d 100644 --- a/packages/utils/src/getDirectiveExtensions.ts +++ b/packages/utils/src/getDirectiveExtensions.ts @@ -1,6 +1,7 @@ import type { ASTNode, DirectiveNode, GraphQLSchema } from 'graphql'; import { valueFromAST, valueFromASTUntyped } from 'graphql'; import { getArgumentValues } from './getArgumentValues.js'; +import { memoize1 } from './memoize.js'; export type DirectableASTNode = ASTNode & { directives?: readonly DirectiveNode[] }; export type DirectableObject = { @@ -25,6 +26,39 @@ export function getDirectiveExtensions< TDirectiveAnnotationsMap[directiveName] >; } = {}; + + if (directableObj.extensions) { + let directivesInExtensions = directableObj.extensions; + for (const pathSegment of pathToDirectivesInExtensions) { + directivesInExtensions = directivesInExtensions?.[pathSegment]; + } + if (directivesInExtensions != null) { + for (const directiveNameProp in directivesInExtensions) { + const directiveObjs = directivesInExtensions[directiveNameProp]; + const directiveName = directiveNameProp as keyof TDirectiveAnnotationsMap; + if (Array.isArray(directiveObjs)) { + for (const directiveObj of directiveObjs) { + let existingDirectiveExtensions = directiveExtensions[directiveName]; + if (!existingDirectiveExtensions) { + existingDirectiveExtensions = []; + directiveExtensions[directiveName] = existingDirectiveExtensions; + } + existingDirectiveExtensions.push(directiveObj); + } + } else { + let existingDirectiveExtensions = directiveExtensions[directiveName]; + if (!existingDirectiveExtensions) { + existingDirectiveExtensions = []; + directiveExtensions[directiveName] = existingDirectiveExtensions; + } + existingDirectiveExtensions.push(directiveObjs); + } + } + } + } + + const memoizedStringify = memoize1(obj => JSON.stringify(obj)); + const astNodes: DirectableASTNode[] = []; if (directableObj.astNode) { astNodes.push(directableObj.astNode); @@ -60,39 +94,16 @@ export function getDirectiveExtensions< } } } - existingDirectiveExtensions.push(value); - } - } - } - - if (directableObj.extensions) { - let directivesInExtensions = directableObj.extensions; - for (const pathSegment of pathToDirectivesInExtensions) { - directivesInExtensions = directivesInExtensions?.[pathSegment]; - } - if (directivesInExtensions != null) { - for (const directiveNameProp in directivesInExtensions) { - const directiveObjs = directivesInExtensions[directiveNameProp]; - const directiveName = directiveNameProp as keyof TDirectiveAnnotationsMap; - if (Array.isArray(directiveObjs)) { - for (const directiveObj of directiveObjs) { - let existingDirectiveExtensions = directiveExtensions[directiveName]; - if (!existingDirectiveExtensions) { - existingDirectiveExtensions = []; - directiveExtensions[directiveName] = existingDirectiveExtensions; - } - existingDirectiveExtensions.push(directiveObj); + if (astNodes.length > 0 && existingDirectiveExtensions.length > 0) { + const valStr = memoizedStringify(value); + if (existingDirectiveExtensions.some(val => memoizedStringify(val) === valStr)) { + continue; } - } else { - let existingDirectiveExtensions = directiveExtensions[directiveName]; - if (!existingDirectiveExtensions) { - existingDirectiveExtensions = []; - directiveExtensions[directiveName] = existingDirectiveExtensions; - } - existingDirectiveExtensions.push(directiveObjs); } + existingDirectiveExtensions.push(value); } } } + return directiveExtensions; } diff --git a/packages/utils/src/print-schema-with-directives.ts b/packages/utils/src/print-schema-with-directives.ts index 70a8098da40..ef473b7006c 100644 --- a/packages/utils/src/print-schema-with-directives.ts +++ b/packages/utils/src/print-schema-with-directives.ts @@ -16,7 +16,6 @@ import { GraphQLInputField, GraphQLInputObjectType, GraphQLInterfaceType, - GraphQLNamedType, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, @@ -42,8 +41,6 @@ import { ScalarTypeDefinitionNode, SchemaDefinitionNode, SchemaExtensionNode, - TypeDefinitionNode, - TypeExtensionNode, UnionTypeDefinitionNode, ValueNode, } from 'graphql'; @@ -51,7 +48,11 @@ import { astFromType } from './astFromType.js'; import { astFromValue } from './astFromValue.js'; import { astFromValueUntyped } from './astFromValueUntyped.js'; import { getDescriptionNode } from './descriptionFromObject.js'; -import { DirectiveAnnotation, getDirectivesInExtensions } from './get-directives.js'; +import { + DirectableGraphQLObject, + DirectiveAnnotation, + getDirectivesInExtensions, +} from './get-directives.js'; import { isSome } from './helpers.js'; import { getRootTypeMap } from './rootTypes.js'; import { @@ -213,81 +214,64 @@ export function astFromDirective( }; } -export function getDirectiveNodes( - entity: GraphQLSchema | GraphQLNamedType | GraphQLEnumValue, - schema: GraphQLSchema, - pathToDirectivesInExtensions?: Array, -): Array { - const directivesInExtensions = getDirectivesInExtensions(entity, pathToDirectivesInExtensions); - let nodes: Array< - | SchemaDefinitionNode - | SchemaExtensionNode - | TypeDefinitionNode - | TypeExtensionNode - | EnumValueDefinitionNode - > = []; - if (entity.astNode != null) { - nodes.push(entity.astNode); - } - if ('extensionASTNodes' in entity && entity.extensionASTNodes != null) { - nodes = nodes.concat(entity.extensionASTNodes); - } - - let directives: Array; - if (directivesInExtensions != null) { - directives = makeDirectiveNodes(schema, directivesInExtensions); - } else { - directives = []; - for (const node of nodes) { - if (node.directives) { - directives.push(...node.directives); - } - } - } - - return directives; -} - -export function getDeprecatableDirectiveNodes( - entity: GraphQLArgument | GraphQLField | GraphQLInputField | GraphQLEnumValue, +export function getDirectiveNodes( + entity: DirectableGraphQLObject & { + deprecationReason?: string | null; + specifiedByUrl?: string | null; + specifiedByURL?: string | null; + }, schema?: GraphQLSchema, pathToDirectivesInExtensions?: Array, -): Array { - let directiveNodesBesidesDeprecated: Array = []; - let deprecatedDirectiveNode: Maybe = null; +): Array { + let directiveNodesBesidesDeprecatedAndSpecifiedBy: Array = []; const directivesInExtensions = getDirectivesInExtensions(entity, pathToDirectivesInExtensions); - let directives: Maybe>; + let directives: Maybe>; if (directivesInExtensions != null) { directives = makeDirectiveNodes(schema, directivesInExtensions); - } else { - directives = entity.astNode?.directives; } + let deprecatedDirectiveNode: Maybe = null; + let specifiedByDirectiveNode: Maybe = null; if (directives != null) { - directiveNodesBesidesDeprecated = directives.filter( - directive => directive.name.value !== 'deprecated', + directiveNodesBesidesDeprecatedAndSpecifiedBy = directives.filter( + directive => directive.name.value !== 'deprecated' && directive.name.value !== 'specifiedBy', ); - if ((entity as unknown as { deprecationReason: string }).deprecationReason != null) { + if (entity.deprecationReason != null) { deprecatedDirectiveNode = directives.filter( directive => directive.name.value === 'deprecated', )?.[0]; } + if (entity.specifiedByUrl != null || entity.specifiedByURL != null) { + specifiedByDirectiveNode = directives.filter( + directive => directive.name.value === 'specifiedBy', + )?.[0]; + } + } + + if (entity.deprecationReason != null && deprecatedDirectiveNode == null) { + deprecatedDirectiveNode = makeDeprecatedDirective(entity.deprecationReason); } if ( - (entity as unknown as { deprecationReason: string }).deprecationReason != null && - deprecatedDirectiveNode == null + entity.specifiedByUrl != null || + (entity.specifiedByURL != null && specifiedByDirectiveNode == null) ) { - deprecatedDirectiveNode = makeDeprecatedDirective( - (entity as unknown as { deprecationReason: string }).deprecationReason, - ); + const specifiedByValue = entity.specifiedByUrl || entity.specifiedByURL; + const specifiedByArgs = { + url: specifiedByValue, + }; + specifiedByDirectiveNode = makeDirectiveNode('specifiedBy', specifiedByArgs); } - return deprecatedDirectiveNode == null - ? directiveNodesBesidesDeprecated - : [deprecatedDirectiveNode].concat(directiveNodesBesidesDeprecated); + if (deprecatedDirectiveNode != null) { + directiveNodesBesidesDeprecatedAndSpecifiedBy.push(deprecatedDirectiveNode); + } + if (specifiedByDirectiveNode != null) { + directiveNodesBesidesDeprecatedAndSpecifiedBy.push(specifiedByDirectiveNode); + } + return directiveNodesBesidesDeprecatedAndSpecifiedBy; } export function astFromArg( @@ -308,7 +292,7 @@ export function astFromArg( arg.defaultValue !== undefined ? (astFromValue(arg.defaultValue, arg.type) ?? undefined) : (undefined as any), - directives: getDeprecatableDirectiveNodes(arg, schema, pathToDirectivesInExtensions) as any, + directives: getDirectiveNodes(arg, schema, pathToDirectivesInExtensions) as any, }; } @@ -426,9 +410,7 @@ export function astFromScalarType( ): ScalarTypeDefinitionNode { const directivesInExtensions = getDirectivesInExtensions(type, pathToDirectivesInExtensions); - const directives: DirectiveNode[] = directivesInExtensions - ? makeDirectiveNodes(schema, directivesInExtensions) - : (type.astNode?.directives as DirectiveNode[]) || []; + const directives = makeDirectiveNodes(schema, directivesInExtensions); const specifiedByValue = ((type as any)['specifiedByUrl'] || (type as any)['specifiedByURL']) as string; @@ -469,7 +451,7 @@ export function astFromField( arguments: field.args.map(arg => astFromArg(arg, schema, pathToDirectivesInExtensions)), type: astFromType(field.type), // ConstXNode has been introduced in v16 but it is not compatible with XNode so we do `as any` for backwards compatibility - directives: getDeprecatableDirectiveNodes(field, schema, pathToDirectivesInExtensions) as any, + directives: getDirectiveNodes(field, schema, pathToDirectivesInExtensions) as any, }; } @@ -487,7 +469,7 @@ export function astFromInputField( }, type: astFromType(field.type), // ConstXNode has been introduced in v16 but it is not compatible with XNode so we do `as any` for backwards compatibility - directives: getDeprecatableDirectiveNodes(field, schema, pathToDirectivesInExtensions) as any, + directives: getDirectiveNodes(field, schema, pathToDirectivesInExtensions) as any, defaultValue: astFromValue(field.defaultValue, field.type) ?? (undefined as any), }; } @@ -504,20 +486,21 @@ export function astFromEnumValue( kind: Kind.NAME, value: value.name, }, - // ConstXNode has been introduced in v16 but it is not compatible with XNode so we do `as any` for backwards compatibility - directives: getDeprecatableDirectiveNodes(value, schema, pathToDirectivesInExtensions) as any, + directives: getDirectiveNodes(value, schema, pathToDirectivesInExtensions), }; } -export function makeDeprecatedDirective(deprecationReason: string): DirectiveNode { +export function makeDeprecatedDirective( + deprecationReason: string, +): TDirectiveNode { return makeDirectiveNode('deprecated', { reason: deprecationReason }, GraphQLDeprecatedDirective); } -export function makeDirectiveNode( +export function makeDirectiveNode( name: string, args?: Record, directive?: Maybe, -): DirectiveNode { +): TDirectiveNode { const directiveArguments: Array = []; for (const argName in args) { @@ -551,14 +534,14 @@ export function makeDirectiveNode( value: name, }, arguments: directiveArguments, - }; + } as unknown as TDirectiveNode; } -export function makeDirectiveNodes( +export function makeDirectiveNodes( schema: Maybe, directiveValues: DirectiveAnnotation[], -): Array { - const directiveNodes: Array = []; +): Array { + const directiveNodes: Array = []; for (const { name, args } of directiveValues) { const directive = schema?.getDirective(name); directiveNodes.push(makeDirectiveNode(name, args, directive));