import { BuiltIns , Schema , DirectiveDefinition , NonNullType , NamedType , Directive , UnionType , ObjectType , ListType , FieldDefinition , CompositeType , allSchemaRootKinds , defaultRootName , errorCauses , ErrGraphQLValidationFailed , SchemaElement , baseType , isInterfaceType , isObjectType , isListType , isUnionType , sourceASTs , VariableDefinitions , InterfaceType , InputFieldDefinition , isCompositeType } from "./definitions" ; import { assert , OrderedMap } from "./utils" ; import { SDLValidationRule } from "graphql/validation/ValidationContext" ; import { specifiedSDLRules } from "graphql/validation/specifiedRules" ; import { ASTNode , DocumentNode , GraphQLError , Kind , KnownTypeNamesRule , parse , PossibleTypeExtensionsRule , print as printAST , Source , DirectiveLocation , } from "graphql" ; import { defaultPrintOptions , printDirectiveDefinition } from "./print" ; import { KnownTypeNamesInFederationRule } from "./validation/KnownTypeNamesInFederationRule" ; import { buildSchema , buildSchemaFromAST } from "./buildSchema" ; import { parseSelectionSet , SelectionSet } from './operations' ; import { tagLocations , TAG_VERSIONS } from "./tagSpec" ; import { errorCodeDef , ErrorCodeDefinition , ERRORS , } from "./error" ; import { ERROR_CATEGORIES } from "." ; export const entityTypeName = '_Entity' ; export const serviceTypeName = '_Service' ; export const anyTypeName = '_Any' ; export const fieldSetTypeName = '_FieldSet' ; export const keyDirectiveName = 'key' ; export const extendsDirectiveName = 'extends' ; export const externalDirectiveName = 'external' ; export const requiresDirectiveName = 'requires' ; export const providesDirectiveName = 'provides' ; export const tagDirectiveName = 'tag' ; export const serviceFieldName = '_service' ; export const entitiesFieldName = '_entities' ; const tagSpec = TAG_VERSIONS . latest ( ) ; export const FEDERATION_RESERVED_SUBGRAPH_NAME = '_' ; const FEDERATION_TYPES = [ entityTypeName , serviceTypeName , anyTypeName , fieldSetTypeName ] ; const FEDERATION_DIRECTIVES = [ keyDirectiveName , extendsDirectiveName , externalDirectiveName , requiresDirectiveName , providesDirectiveName , tagDirectiveName ] ; const FEDERATION_ROOT_FIELDS = [ serviceFieldName , entitiesFieldName ] ; const FEDERATION_OMITTED_VALIDATION_RULES = [ PossibleTypeExtensionsRule , KnownTypeNamesRule ] ; const FEDERATION_SPECIFIC_VALIDATION_RULES = [ KnownTypeNamesInFederationRule ] ; const FEDERATION_VALIDATION_RULES = specifiedSDLRules . filter ( rule => ! FEDERATION_OMITTED_VALIDATION_RULES . includes ( rule ) ) . concat ( FEDERATION_SPECIFIC_VALIDATION_RULES ) ; function validateFieldSetSelections ( directiveName , selectionSet , hasExternalInParents , externalTester , externalFieldCoordinatesCollector , allowOnNonExternalLeafFields ) { for ( const selection of selectionSet . selections ( ) ) { if ( selection . kind === 'FieldSelection' ) { const field = selection . element ( ) . definition ; const isExternal = externalTester . isExternal ( field ) ; if ( isExternal ) { externalFieldCoordinatesCollector . push ( field . coordinate ) ; } if ( field . hasArguments ( ) ) { throw ERROR_CATEGORIES . FIELDS_HAS_ARGS . get ( directiveName ) . err ( { message : `field ${ field . coordinate } cannot be included because it has arguments (fields with argument are not allowed in @${ directiveName })` , nodes : field . sourceAST } ) ; } const mustBeExternal = ! selection . selectionSet && ! allowOnNonExternalLeafFields && ! hasExternalInParents ; if ( ! isExternal && mustBeExternal ) { const errorCode = ERROR_CATEGORIES . DIRECTIVE_FIELDS_MISSING_EXTERNAL . get ( directiveName ) ; if ( externalTester . isFakeExternal ( field ) ) { throw errorCode . err ( { message : `field "${ field . coordinate }" should not be part of a @${ directiveName } since it is already "effectively" provided by this subgraph ` + `(while it is marked @${ externalDirectiveName }, it is a @${ keyDirectiveName } field of an extension type, which are not internally considered external for historical/backward compatibility reasons)` , nodes : field . sourceAST } ) ; } else { throw errorCode . err ( { message : `field "${ field . coordinate }" should not be part of a @${ directiveName } since it is already provided by this subgraph (it is not marked @${ externalDirectiveName })` , nodes : field . sourceAST } ) ; } } if ( selection . selectionSet ) { let newHasExternalInParents = hasExternalInParents || isExternal ; const parentType = field . parent ; if ( ! newHasExternalInParents && isInterfaceType ( parentType ) ) { for ( const implem of parentType . possibleRuntimeTypes ( ) ) { const fieldInImplem = implem . field ( field . name ) ; if ( fieldInImplem && externalTester . isExternal ( fieldInImplem ) ) { newHasExternalInParents = true ; break ; } } } validateFieldSetSelections ( directiveName , selection . selectionSet , newHasExternalInParents , externalTester , externalFieldCoordinatesCollector , allowOnNonExternalLeafFields ) ; } } else { validateFieldSetSelections ( directiveName , selection . selectionSet , hasExternalInParents , externalTester , externalFieldCoordinatesCollector , allowOnNonExternalLeafFields ) ; } } } function validateFieldSet ( type , directive , externalTester , externalFieldCoordinatesCollector , allowOnNonExternalLeafFields , onFields ? ) { try { const fieldAcessor = onFields ? ( type , fieldName ) => { const field = type . field ( fieldName ) ; if ( field ) { onFields ( field ) ; } return field ; } : undefined ; const selectionSet = parseFieldSetArgument ( type , directive , fieldAcessor ) ; try { validateFieldSetSelections ( directive . name , selectionSet , false , externalTester , externalFieldCoordinatesCollector , allowOnNonExternalLeafFields ) ; return undefined ; } catch ( e ) { if ( ! ( e instanceof GraphQLError ) ) { throw e ; } const nodes = sourceASTs ( directive ) ; if ( e . nodes ) { nodes . push ( ... e . nodes ) ; } const codeDef = errorCodeDef ( e ) ?? ERROR_CATEGORIES . DIRECTIVE_INVALID_FIELDS . get ( directive . name ) ; return codeDef . err ( { message : `${ fieldSetErrorDescriptor ( directive ) }: ${ e . message . trim ( ) }` , nodes , originalError : e , } ) ; } } catch ( e ) { if ( e instanceof GraphQLError ) { return e ; } else { throw e ; } } } function fieldSetErrorDescriptor ( directive ) { return `On ${ fieldSetTargetDescription ( directive ) }, for ${ directiveStrUsingASTIfPossible ( directive ) }` ; } function directiveStrUsingASTIfPossible ( directive ) { return directive . sourceAST ? printAST ( directive . sourceAST ) : directive . toString ( ) ; } function fieldSetTargetDescription ( directive ) { const targetKind = directive . parent instanceof FieldDefinition ? "field" : "type" ; return `${ targetKind } "${ directive . parent ?. coordinate }"` ; } function validateAllFieldSet < TParent extends SchemaElement < any , any > > ( definition , targetTypeExtractor , errorCollector , externalTester , externalFieldCoordinatesCollector , isOnParentType , allowOnNonExternalLeafFields , onFields ? ) { for ( const application of definition . applications ( ) ) { const elt = application . parent as TParent ; const type = targetTypeExtractor ( elt ) ; const parentType = isOnParentType ? type : ( elt . parent as NamedType ) ; if ( isInterfaceType ( parentType ) ) { const code = ERROR_CATEGORIES . DIRECTIVE_UNSUPPORTED_ON_INTERFACE . get ( definition . name ) ; errorCollector . push ( code . err ( { message : isOnParentType ? `Cannot use ${ definition . coordinate } on interface "${ parentType . coordinate }": ${ definition . coordinate } is not yet supported on interfaces` : `Cannot use ${ definition . coordinate } on ${ fieldSetTargetDescription ( application ) } of parent type "${ parentType }": ${ definition . coordinate } is not yet supported within interfaces` , nodes : sourceASTs ( application ) . concat ( isOnParentType ? [ ] : sourceASTs ( type ) ) , } ) ) ; } const error = validateFieldSet ( type , application , externalTester , externalFieldCoordinatesCollector , allowOnNonExternalLeafFields , onFields ) ; if ( error ) { errorCollector . push ( error ) ; } } } /**
 * Checks that all fields marked @external is used in a federation directive (@key, @provides or @requires) _or_ to satisfy an
 * interface implementation. Otherwise, the field declaration is somewhat useless.
 */ function validateAllExternalFieldsUsed ( schema , externalTester , allExternalFieldsUsedInFederationDirectivesCoordinates , errorCollector ) { for ( const type of schema . types ( ) ) { if ( ! isObjectType ( type ) && ! isInterfaceType ( type ) ) { continue ; } for ( const field of type . fields ( ) ) { if ( ! externalTester . isExternal ( field ) || allExternalFieldsUsedInFederationDirectivesCoordinates . includes ( field . coordinate ) ) { continue ; } if ( ! isFieldSatisfyingInterface ( field ) ) { errorCollector . push ( ERRORS . EXTERNAL_UNUSED . err ( { message : `Field "${ field . coordinate }" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;` + ' the field declaration has no use and should be removed (or the field should not be @external).' , nodes : field . sourceAST , } ) ) ; } } } } function isFieldSatisfyingInterface ( field ) { return field . parent . interfaces ( ) . some ( itf => itf . field ( field . name ) ) ; } export class FederationBuiltIns extends BuiltIns { addBuiltInTypes ( schema ) { super . addBuiltInTypes ( schema ) ; this . addBuiltInUnion ( schema , entityTypeName ) ; this . addBuiltInObject ( schema , serviceTypeName ) . addField ( 'sdl' , schema . stringType ( ) ) ; this . addBuiltInScalar ( schema , anyTypeName ) ; this . addBuiltInScalar ( schema , fieldSetTypeName ) ; } addBuiltInDirectives ( schema ) { super . addBuiltInDirectives ( schema ) ; const fieldSetType = new NonNullType ( schema . type ( fieldSetTypeName ) ! ) ; const keyDirective = this . addBuiltInDirective ( schema , keyDirectiveName ) . addLocations ( DirectiveLocation . OBJECT , DirectiveLocation . INTERFACE ) ; keyDirective . repeatable = true ; keyDirective . addArgument ( 'fields' , fieldSetType ) ; this . addBuiltInDirective ( schema , extendsDirectiveName ) . addLocations ( DirectiveLocation . OBJECT , DirectiveLocation . INTERFACE ) ; this . addBuiltInDirective ( schema , externalDirectiveName ) . addLocations ( DirectiveLocation . OBJECT , DirectiveLocation . FIELD_DEFINITION ) ; for ( const name of [ requiresDirectiveName , providesDirectiveName ] ) { this . addBuiltInDirective ( schema , name ) . addLocations ( DirectiveLocation . FIELD_DEFINITION ) . addArgument ( 'fields' , fieldSetType ) ; } const directive = this . addBuiltInDirective ( schema , 'tag' ) . addLocations ( ... tagLocations ) ; directive . addArgument ( "name" , new NonNullType ( schema . stringType ( ) ) ) ; } prepareValidation ( schema ) { super . prepareValidation ( schema ) ; let entityType = schema . type ( entityTypeName ) as UnionType ; if ( ! entityType . isBuiltIn ) { if ( entityType . membersCount ( ) === 0 ) { entityType . remove ( ) ; } entityType = schema . builtInTypes < UnionType > ( 'UnionType' , true ) . find ( u => u . name === entityTypeName ) ! ; } entityType . clearTypes ( ) ; for ( const objectType of schema . types < ObjectType > ( "ObjectType" ) ) { if ( isEntityType ( objectType ) ) { entityType . addType ( objectType ) ; } } const hasEntities = entityType . membersCount ( ) > 0 ; if ( ! hasEntities ) { entityType . remove ( ) ; } const queryRoot = schema . schemaDefinition . root ( "query" ) ; const queryType = queryRoot ? queryRoot . type : schema . addType ( new ObjectType ( "Query" ) ) ; const entityField = queryType . field ( entitiesFieldName ) ; if ( hasEntities ) { const anyType = schema . type ( anyTypeName ) ; assert ( anyType , `The schema should have the _Any type` ) ; const entityFieldType = new NonNullType ( new ListType ( entityType ) ) ; if ( ! entityField ) { this . addBuiltInField ( queryType , entitiesFieldName , entityFieldType ) . addArgument ( 'representations' , new NonNullType ( new ListType ( new NonNullType ( anyType ) ) ) ) ; } else if ( ! entityField . type ) { entityField . type = entityType ; } } else if ( entityField ) { entityField . remove ( ) ; } if ( ! queryType . field ( serviceFieldName ) ) { this . addBuiltInField ( queryType , serviceFieldName , schema . type ( serviceTypeName ) as ObjectType ) ; } } onValidation ( schema ) { const errors = super . onValidation ( schema , [ tagDirectiveName ] ) ; for ( const k of allSchemaRootKinds ) { const type = schema . schemaDefinition . root ( k ) ?. type ; const defaultName = defaultRootName ( k ) ; if ( type && type . name !== defaultName ) { const existing = schema . type ( defaultName ) ; if ( existing ) { errors . push ( ERROR_CATEGORIES . ROOT_TYPE_USED . get ( k ) . err ( { message : `The schema has a type named "${ defaultName }" but it is not set as the ${ k } root type ("${ type . name }" is instead): ` + 'this is not supported by federation. ' + 'If a root type does not use its default name, there should be no other type with that default name.' , nodes : sourceASTs ( type , existing ) , } ) ) ; } type . rename ( defaultName ) ; } } const externalTester = new ExternalTester ( schema ) ; const externalFieldsInFedDirectivesCoordinates = [ ] ; const keyDirective = this . keyDirective ( schema ) ; validateAllFieldSet < CompositeType > ( keyDirective , type => type , errors , externalTester , externalFieldsInFedDirectivesCoordinates , true , true , field => { if ( isListType ( field . type ! ) || isUnionType ( field . type ! ) || isInterfaceType ( field . type ! ) ) { let kind = field . type ! . kind ; kind = kind . slice ( 0 , kind . length - 'Type' . length ) ; throw ERRORS . KEY_FIELDS_SELECT_INVALID_TYPE . err ( { message : `field "${ field . coordinate }" is a ${ kind } type which is not allowed in @key` } ) ; } } ) ; validateAllFieldSet < FieldDefinition < CompositeType > > ( this . requiresDirective ( schema ) , field => field . parent , errors , externalTester , externalFieldsInFedDirectivesCoordinates , false , false ) ; validateAllFieldSet < FieldDefinition < CompositeType > > ( this . providesDirective ( schema ) , field => { if ( externalTester . isExternal ( field ) ) { throw new GraphQLError ( `Cannot have both @provides and @external on field "${ field . coordinate }"` , field . sourceAST ) ; } const type = baseType ( field . type ! ) ; if ( ! isCompositeType ( type ) ) { throw ERRORS . PROVIDES_ON_NON_OBJECT_FIELD . err ( { message : `Invalid @provides directive on field "${ field . coordinate }": field has type "${ field . type }" which is not a Composite Type` , nodes : field . sourceAST , } ) ; } return type ; } , errors , externalTester , externalFieldsInFedDirectivesCoordinates , false , false ) ; validateAllExternalFieldsUsed ( schema , externalTester , externalFieldsInFedDirectivesCoordinates , errors ) ; const tagDirective = this . tagDirective ( schema ) ; if ( ! tagDirective . isBuiltIn ) { const error = tagSpec . checkCompatibleDirective ( tagDirective ) ; if ( error ) { errors . push ( error ) ; } } return errors ; } validationRules ( ) { return FEDERATION_VALIDATION_RULES ; } keyDirective ( schema ) { return this . getTypedDirective ( schema , keyDirectiveName ) ; } extendsDirective ( schema ) { return this . getTypedDirective ( schema , extendsDirectiveName ) ; } externalDirective ( schema ) { return this . getTypedDirective ( schema , externalDirectiveName ) ; } requiresDirective ( schema ) { return this . getTypedDirective ( schema , requiresDirectiveName ) ; } providesDirective ( schema ) { return this . getTypedDirective ( schema , providesDirectiveName ) ; } tagDirective ( schema ) { return this . getTypedDirective ( schema , tagDirectiveName ) ; } maybeUpdateSubgraphDocument ( schema , document ) { document = super . maybeUpdateSubgraphDocument ( schema , document ) ; const definitions = document . definitions . concat ( ) ; for ( const directiveName of FEDERATION_DIRECTIVES ) { const directive = schema . directive ( directiveName ) ; assert ( directive , 'This method should only have been called on a schema with federation built-ins' ) ; if ( directive . isBuiltIn ) { definitions . push ( parse ( printDirectiveDefinition ( directive , defaultPrintOptions ) ) . definitions [ 0 ] ) ; } } return { kind : Kind . DOCUMENT , loc : document . loc , definitions } ; } } export const federationBuiltIns = new FederationBuiltIns ( ) ; export function isFederationSubgraphSchema ( schema ) { return schema . builtIns instanceof FederationBuiltIns ; } export function isFederationType ( type ) { return isFederationTypeName ( type . name ) ; } export function isFederationTypeName ( typeName ) { return FEDERATION_TYPES . includes ( typeName ) ; } export function isFederationField ( field ) { if ( field . parent === field . schema ( ) . schemaDefinition . root ( "query" ) ?. type ) { return FEDERATION_ROOT_FIELDS . includes ( field . name ) ; } return false ; } export function isFederationDirective ( directive ) { return FEDERATION_DIRECTIVES . includes ( directive . name ) ; } export function isEntityType ( type ) { return type . kind == "ObjectType" && type . hasAppliedDirective ( keyDirectiveName ) ; } export function buildSubgraph ( name , source ) { try { return typeof source === 'string' ? buildSchema ( new Source ( source , name ) , federationBuiltIns ) : buildSchemaFromAST ( source , federationBuiltIns ) ; } catch ( e ) { if ( e instanceof GraphQLError ) { throw addSubgraphToError ( e , name , ERRORS . INVALID_GRAPHQL ) ; } else { throw e ; } } } export function parseFieldSetArgument ( parentType , directive , fieldAccessor = ( type , name ) => type . field ( name ) ) { try { const selectionSet = parseSelectionSet ( parentType , validateFieldSetValue ( directive ) , new VariableDefinitions ( ) , undefined , fieldAccessor ) ; selectionSet . validate ( ) ; return selectionSet ; } catch ( e ) { if ( ! ( e instanceof GraphQLError ) ) { throw e ; } const nodes = sourceASTs ( directive ) ; if ( e . nodes ) { nodes . push ( ... e . nodes ) ; } let msg = e . message . trim ( ) ; if ( msg . startsWith ( 'Cannot query field' ) ) { if ( msg . endsWith ( '.' ) ) { msg = msg . slice ( 0 , msg . length - 1 ) ; } if ( directive . name === keyDirectiveName ) { msg = msg + ' (the field should be either be added to this subgraph or, if it should not be resolved by this subgraph, you need to add it to this subgraph with @external).' ; } else { msg = msg + ' (if the field is defined in another subgraph, you need to add it to this subgraph with @external).' ; } } const codeDef = errorCodeDef ( e ) ?? ERROR_CATEGORIES . DIRECTIVE_INVALID_FIELDS . get ( directive . name ) ; throw codeDef . err ( { message : `${ fieldSetErrorDescriptor ( directive ) }: ${ msg }` , nodes , originalError : e , } ) ; } } function validateFieldSetValue ( directive ) { const fields = directive . arguments ( ) . fields ; const nodes = directive . sourceAST ; if ( typeof fields !== 'string' ) { throw ERROR_CATEGORIES . DIRECTIVE_INVALID_FIELDS_TYPE . get ( directive . name ) . err ( { message : `Invalid value for argument "${ directive . definition ! . argument ( 'fields' ) ! . name }": must be a string.` , nodes , } ) ; } if ( nodes && nodes . kind === 'Directive' ) { for ( const argNode of nodes . arguments ?? [ ] ) { if ( argNode . name . value === 'fields' ) { if ( argNode . value . kind !== 'StringValue' ) { throw ERROR_CATEGORIES . DIRECTIVE_INVALID_FIELDS_TYPE . get ( directive . name ) . err ( { message : `Invalid value for argument "${ directive . definition ! . argument ( 'fields' ) ! . name }": must be a string.` , nodes , } ) ; } break ; } } } return fields ; } export interface ServiceDefinition { typeDefs : DocumentNode ; name : string ; url ? : string ; } export function subgraphsFromServiceList ( serviceList ) { let errors = [ ] ; const subgraphs = new Subgraphs ( ) ; for ( const service of serviceList ) { try { subgraphs . add ( service . name , service . url ?? '' , service . typeDefs ) ; } catch ( e ) { const causes = errorCauses ( e ) ; if ( causes ) { errors = errors . concat ( causes ) ; } else { throw e ; } } } return errors . length === 0 ? subgraphs : errors ; } export class Subgraphs { private readonly subgraphs = new OrderedMap < string , Subgraph > ( ) ; add ( subgraph ) ; add ( name , url , schema ) ; add ( subgraphOrName , url ? , schema ? ) { const toAdd = typeof subgraphOrName === 'string' ? new Subgraph ( subgraphOrName , url ! , schema instanceof Schema ? schema ! : buildSubgraph ( subgraphOrName , schema ! ) ) : subgraphOrName ; if ( toAdd . name === FEDERATION_RESERVED_SUBGRAPH_NAME ) { throw ERRORS . INVALID_SUBGRAPH_NAME . err ( { message : `Invalid name ${ FEDERATION_RESERVED_SUBGRAPH_NAME } for a subgraph: this name is reserved` } ) ; } if ( this . subgraphs . has ( toAdd . name ) ) { throw new Error ( `A subgraph named ${ toAdd . name } already exists` + ( toAdd . url ? ` (with url '${ toAdd . url }')` : '' ) ) ; } this . subgraphs . add ( toAdd . name , toAdd ) ; return toAdd ; } get ( name ) { return this . subgraphs . get ( name ) ; } size ( ) { return this . subgraphs . size ; } names ( ) { return this . subgraphs . keys ( ) ; } values ( ) { return this . subgraphs . values ( ) ; } * [ Symbol . iterator ] ( ) { for ( const subgraph of this . subgraphs ) { yield subgraph ; } } toString ( ) { return '[' + this . subgraphs . keys ( ) . join ( ', ' ) + ']' ; } } export class Subgraph { constructor ( readonly name , readonly url , readonly schema , validateSchema = true ) { if ( validateSchema ) { schema . validate ( ) ; } } toString ( ) { return `${ this . name } (${ this . url })` ; } } export type SubgraphASTNode = ASTNode & { subgraph : string ; } ; export function addSubgraphToASTNode ( node , subgraph ) { return { ... node , subgraph } ; } export function addSubgraphToError ( e , subgraphName , errorCode ? ) { const updatedCauses = errorCauses ( e ) ! . map ( cause => { const message = `[${ subgraphName }] ${ cause . message }` ; const nodes = cause . nodes ? cause . nodes . map ( node => addSubgraphToASTNode ( node , subgraphName ) ) : undefined ; const code = errorCodeDef ( cause ) ?? errorCode ; if ( code ) { return code . err ( { message , nodes , source : cause . source , positions : cause . positions , path : cause . path , originalError : cause . originalError , extensions : cause . extensions , } ) ; } else { return new GraphQLError ( message , nodes , cause . source , cause . positions , cause . path , cause . originalError , cause . extensions ) ; } } ) ; return ErrGraphQLValidationFailed ( updatedCauses ) ; } export class ExternalTester { private readonly fakeExternalFields = new Set < string > ( ) ; constructor ( readonly schema ) { this . collectFakeExternals ( ) ; } private collectFakeExternals ( ) { const keyDirective = federationBuiltIns . keyDirective ( this . schema ) ; if ( ! keyDirective ) { return ; } for ( const key of keyDirective . applications ( ) ) { const parent = key . parent as CompositeType ; if ( ! ( key . ofExtension ( ) || parent . hasAppliedDirective ( extendsDirectiveName ) ) ) { continue ; } try { parseFieldSetArgument ( parent , key as Directive < any , { fields : any ; } > , ( parentType , fieldName ) => { const field = parentType . field ( fieldName ) ; if ( field && field . hasAppliedDirective ( externalDirectiveName ) ) { this . fakeExternalFields . add ( field . coordinate ) ; } return field ; } ) ; } catch ( e ) { } } } isExternal ( field ) { return field . hasAppliedDirective ( externalDirectiveName ) && ! this . isFakeExternal ( field ) ; } isFakeExternal ( field ) { return this . fakeExternalFields . has ( field . coordinate ) ; } selectsAnyExternalField ( selectionSet ) { for ( const selection of selectionSet . selections ( ) ) { if ( selection . kind === 'FieldSelection' && this . isExternal ( selection . element ( ) . definition ) ) { return true ; } if ( selection . selectionSet ) { if ( this . selectsAnyExternalField ( selection . selectionSet ) ) { return true ; } } } return false ; } }