import { baseType , CompositeType , Directive , FieldDefinition , InputFieldDefinition , InputObjectType , InterfaceType , isEnumType , isInterfaceType , isObjectType , isUnionType , ListType , NamedType , newNamedType , NonNullType , NullableType , ObjectType , Schema , Type , } from "./definitions" ; import { addSubgraphToError , externalDirectiveName , federationBuiltIns , parseFieldSetArgument } from "./federation" ; import { CoreSpecDefinition , FeatureVersion } from "./coreSpec" ; import { JoinSpecDefinition } from "./joinSpec" ; import { Subgraph , Subgraphs } from "./federation" ; import { assert } from "./utils" ; import { validateSupergraph } from "./supergraphs" ; import { builtTypeReference } from "./buildSchema" ; import { GraphQLError } from "graphql" ; import { selectionOfElement , SelectionSet } from "./operations" ; import { isSubtype } from "./types" ; import { printSchema } from "./print" ; import fs from 'fs' ; import path from 'path' ; import { validateStringContainsBoolean } from "./utils" ; function filteredTypes ( supergraph , joinSpec , coreSpec ) { return supergraph . types ( ) . filter ( t => ! joinSpec . isSpecType ( t ) && ! coreSpec . isSpecType ( t ) ) ; } export function extractSubgraphsNamesAndUrlsFromSupergraph ( supergraph ) { const [ _ , joinSpec ] = validateSupergraph ( supergraph ) ; const [ subgraphs ] = collectEmptySubgraphs ( supergraph , joinSpec ) ; return subgraphs . values ( ) . map ( subgraph => { return { name : subgraph . name , url : subgraph . url } ; } ) ; } function collectEmptySubgraphs ( supergraph , joinSpec ) { const subgraphs = new Subgraphs ( ) ; const graphDirective = joinSpec . graphDirective ( supergraph ) ; const graphEnum = joinSpec . graphEnum ( supergraph ) ; const graphEnumNameToSubgraphName = new Map < string , string > ( ) ; for ( const value of graphEnum . values ) { const graphApplications = value . appliedDirectivesOf ( graphDirective ) ; if ( ! graphApplications . length ) { throw new Error ( `Value ${ value } of join__Graph enum has no @join__graph directive` ) ; } const info = graphApplications [ 0 ] . arguments ( ) ; const subgraph = new Subgraph ( info . name , info . url , new Schema ( federationBuiltIns ) , false ) ; subgraphs . add ( subgraph ) ; graphEnumNameToSubgraphName . set ( value . name , info . name ) ; } return [ subgraphs , graphEnumNameToSubgraphName ] ; } export function extractSubgraphsFromSupergraph ( supergraph ) { const [ coreFeatures , joinSpec ] = validateSupergraph ( supergraph ) ; const isFed1 = joinSpec . version . equals ( new FeatureVersion ( 0 , 1 ) ) ; const [ subgraphs , graphEnumNameToSubgraphName ] = collectEmptySubgraphs ( supergraph , joinSpec ) ; const typeDirective = joinSpec . typeDirective ( supergraph ) ; const implementsDirective = joinSpec . implementsDirective ( supergraph ) ; for ( const type of filteredTypes ( supergraph , joinSpec , coreFeatures . coreDefinition ) ) { const typeApplications = type . appliedDirectivesOf ( typeDirective ) ; if ( ! typeApplications . length ) { subgraphs . values ( ) . map ( sg => sg . schema ) . forEach ( schema => schema . addType ( newNamedType ( type . kind , type . name ) ) ) ; } else { for ( const application of typeApplications ) { const args = application . arguments ( ) ; const subgraphName = graphEnumNameToSubgraphName . get ( args . graph ) ! ; const schema = subgraphs . get ( subgraphName ) ! . schema ; let subgraphType = schema . type ( type . name ) ; if ( ! subgraphType ) { subgraphType = schema . addType ( newNamedType ( type . kind , type . name ) ) ; } if ( args . key ) { const directive = subgraphType . applyDirective ( 'key' , { 'fields' : args . key } ) ; if ( args . extension ) { directive . setOfExtension ( subgraphType . newExtension ( ) ) ; } } } } } const ownerDirective = joinSpec . ownerDirective ( supergraph ) ; const fieldDirective = joinSpec . fieldDirective ( supergraph ) ; for ( const type of filteredTypes ( supergraph , joinSpec , coreFeatures . coreDefinition ) ) { switch ( type . kind ) { case 'ObjectType' : case 'InterfaceType' : const addedInterfaces = [ ] ; const implementsApplications = implementsDirective ? type . appliedDirectivesOf ( implementsDirective ) : [ ] ; for ( const application of implementsApplications ) { const args = application . arguments ( ) ; const subgraph = subgraphs . get ( graphEnumNameToSubgraphName . get ( args . graph ) ! ) ! ; const schema = subgraph . schema ; ( schema . type ( type . name ) ! as ( ObjectType | InterfaceType ) ) . addImplementedInterface ( args . interface ) ; addedInterfaces . push ( args . interface ) ; } for ( const implementations of type . interfaceImplementations ( ) ) { const name = implementations . interface . name ; if ( ! addedInterfaces . includes ( name ) ) { for ( const subgraph of subgraphs ) { const subgraphType = subgraph . schema . type ( type . name ) ; const subgraphItf = subgraph . schema . type ( name ) ; if ( subgraphType && subgraphItf ) { ( subgraphType as ( ObjectType | InterfaceType ) ) . addImplementedInterface ( name ) ; } } } } case 'InputObjectType' : for ( const field of type . fields ( ) ) { const fieldApplications = field . appliedDirectivesOf ( fieldDirective ) ; if ( ! fieldApplications . length ) { const ownerApplications = ownerDirective ? type . appliedDirectivesOf ( ownerDirective ) : [ ] ; if ( ! ownerApplications . length ) { const fieldBaseType = baseType ( field . type ! ) ; for ( const subgraph of subgraphs ) { if ( subgraph . schema . type ( fieldBaseType . name ) ) { addSubgraphField ( field , subgraph ) ; } } } else { assert ( ownerApplications . length == 1 , ( ) => `Found multiple join__owner directives on type ${ type }` ) ; const subgraph = subgraphs . get ( graphEnumNameToSubgraphName . get ( ownerApplications [ 0 ] . arguments ( ) . graph ) ! ) ! ; const subgraphField = addSubgraphField ( field , subgraph ) ; assert ( subgraphField , ( ) => `Found join__owner directive on ${ type } but no corresponding join__type` ) ; } } else { for ( const application of fieldApplications ) { const args = application . arguments ( ) ; const subgraph = subgraphs . get ( graphEnumNameToSubgraphName . get ( args . graph ) ! ) ! ; const subgraphField = addSubgraphField ( field , subgraph , args . type ) ; assert ( subgraphField , ( ) => `Found join__field directive for graph ${ subgraph . name } on field ${ field . coordinate } but no corresponding join__type on ${ type }` ) ; if ( args . requires ) { subgraphField . applyDirective ( 'requires' , { 'fields' : args . requires } ) ; } if ( args . provides ) { subgraphField . applyDirective ( 'provides' , { 'fields' : args . provides } ) ; } if ( args . external ) { subgraphField . applyDirective ( 'external' ) ; } } } } break ; case 'EnumType' : for ( const subgraph of subgraphs ) { const subgraphEnum = subgraph . schema . type ( type . name ) ; if ( ! subgraphEnum ) { continue ; } assert ( isEnumType ( subgraphEnum ) , ( ) => `${ subgraphEnum } should be an enum but found a ${ subgraphEnum . kind }` ) ; for ( const value of type . values ) { subgraphEnum . addValue ( value . name ) ; } } break ; case 'UnionType' : for ( const subgraph of subgraphs ) { const subgraphUnion = subgraph . schema . type ( type . name ) ; if ( ! subgraphUnion ) { continue ; } assert ( isUnionType ( subgraphUnion ) , ( ) => `${ subgraphUnion } should be an enum but found a ${ subgraphUnion . kind }` ) ; for ( const memberType of type . types ( ) ) { const subgraphType = subgraph . schema . type ( memberType . name ) ; if ( subgraphType ) { subgraphUnion . addType ( subgraphType as ObjectType ) ; } } } break ; } } for ( const subgraph of subgraphs ) { if ( isFed1 ) { addExternalFields ( subgraph , supergraph , isFed1 ) ; } removeNeedlessProvides ( subgraph ) ; for ( const type of subgraph . schema . types ( ) ) { switch ( type . kind ) { case 'ObjectType' : case 'InterfaceType' : case 'InputObjectType' : if ( ! type . hasFields ( ) ) { type . removeRecursive ( ) ; } break ; case 'UnionType' : if ( type . membersCount ( ) === 0 ) { type . remove ( ) ; } break ; } } } if ( isFed1 ) { for ( const subgraph of subgraphs ) { for ( const itf of subgraph . schema . types < InterfaceType > ( 'InterfaceType' ) ) { const implementations = itf . possibleRuntimeTypes ( ) ; for ( const field of itf . fields ( ) ) { if ( ! implementations . every ( implem => implem . field ( field . name ) ) ) { field . remove ( ) ; } } if ( ! itf . hasFields ( ) ) { itf . remove ( ) ; } } } } for ( const subgraph of subgraphs ) { try { subgraph . schema . validate ( ) ; } catch ( e ) { if ( isFed1 ) { const msg = `Error extracting subgraph ${ subgraph . name } from the supergraph: this might due to errors in subgraphs that were mistakenly ignored by federation 0.x versions but are rejected by federation 2.\n` + 'Please try composing your subgraphs with federation 2: this should help precisely pinpoint the errors and generate a correct federation 2 supergraph.' ; throw new Error ( `${ msg }.\n\nDetails:\n${ errorToString ( e , subgraph . name ) }` ) ; } else { const msg = `Unexpected error extracting subgraph ${ subgraph . name } from the supergraph: this is either a bug, or the supergraph has been corrupted.` ; const dumpMsg = maybeDumpSubgraphSchema ( subgraph ) ; throw new Error ( `${ msg }.\n\nDetails:\n${ errorToString ( e , subgraph . name ) }\n\n${ dumpMsg }` ) ; } } } return subgraphs ; } const DEBUG_SUBGRAPHS_ENV_VARIABLE_NAME = 'APOLLO_FEDERATION_DEBUG_SUBGRAPHS' ; function maybeDumpSubgraphSchema ( subgraph ) { const shouldDump = ! ! validateStringContainsBoolean ( process . env [ DEBUG_SUBGRAPHS_ENV_VARIABLE_NAME ] ) ; if ( ! shouldDump ) { return `Re-run with environment variable '${ DEBUG_SUBGRAPHS_ENV_VARIABLE_NAME }' set to 'true' to extract the invalid subgraph` ; } try { const filename = `extracted-subgraph-${ subgraph . name }-${ Date . now ( ) }.graphql` ; const file = path . resolve ( filename ) ; if ( fs . existsSync ( file ) ) { throw new Error ( `candidate file ${ filename } already existed` ) ; } fs . writeFileSync ( file , printSchema ( subgraph . schema ) ) ; return `The (invalid) extracted subgraph has been written in: ${ file }.` ; } catch ( e2 ) { return `Was not able to print generated subgraph because: ${ errorToString ( e2 , subgraph . name ) }` ; } } function errorToString ( e , subgraphName ) { return e instanceof GraphQLError ? addSubgraphToError ( e , subgraphName ) . toString ( ) : String ( e ) ; } type AnyField = FieldDefinition < ObjectType | InterfaceType > | InputFieldDefinition ; function addSubgraphField ( supergraphField , subgraph , encodedType ? ) { if ( supergraphField instanceof FieldDefinition ) { return addSubgraphObjectOrInterfaceField ( supergraphField , subgraph , encodedType ) ; } else { return addSubgraphInputField ( supergraphField , subgraph , encodedType ) ; } } function addSubgraphObjectOrInterfaceField ( supergraphField , subgraph , encodedType ? ) { const subgraphType = subgraph . schema . type ( supergraphField . parent . name ) ; if ( subgraphType ) { const copiedType = encodedType ? decodeType ( encodedType , subgraph . schema , subgraph . name ) : copyType ( supergraphField . type ! , subgraph . schema , subgraph . name ) ; const field = ( subgraphType as ObjectType | InterfaceType ) . addField ( supergraphField . name , copiedType ) ; for ( const arg of supergraphField . arguments ( ) ) { field . addArgument ( arg . name , copyType ( arg . type ! , subgraph . schema , subgraph . name ) , arg . defaultValue ) ; } return field ; } else { return undefined ; } } function addSubgraphInputField ( supergraphField , subgraph , encodedType ? ) { const subgraphType = subgraph . schema . type ( supergraphField . parent . name ) ; if ( subgraphType ) { const copiedType = encodedType ? decodeType ( encodedType , subgraph . schema , subgraph . name ) : copyType ( supergraphField . type ! , subgraph . schema , subgraph . name ) ; return ( subgraphType as InputObjectType ) . addField ( supergraphField . name , copiedType ) ; } else { return undefined ; } } function decodeType ( encodedType , subgraph , subgraphName ) { try { return builtTypeReference ( encodedType , subgraph ) ; } catch ( e ) { assert ( false , ( ) => `Cannot parse type "${ encodedType }" in subgraph ${ subgraphName }: ${ e }` ) ; } } function copyType ( type , subgraph , subgraphName ) { switch ( type . kind ) { case 'ListType' : return new ListType ( copyType ( type . ofType , subgraph , subgraphName ) ) ; case 'NonNullType' : return new NonNullType ( copyType ( type . ofType , subgraph , subgraphName ) as NullableType ) ; default : const subgraphType = subgraph . type ( type . name ) ; assert ( subgraphType , ( ) => `Cannot find type ${ type . name } in subgraph ${ subgraphName }` ) ; return subgraphType ! ; } } function addExternalFields ( subgraph , supergraph , isFed1 ) { for ( const type of subgraph . schema . types ( ) ) { if ( ! isObjectType ( type ) && ! isInterfaceType ( type ) ) { continue ; } for ( const keyApplication of type . appliedDirectivesOf ( federationBuiltIns . keyDirective ( subgraph . schema ) ) ) { const forceNonExternal = isFed1 || ! ! keyApplication . ofExtension ( ) ; addExternalFieldsFromDirectiveFieldSet ( subgraph , type , keyApplication , supergraph , forceNonExternal ) ; } for ( const field of type . fields ( ) ) { for ( const requiresApplication of field . appliedDirectivesOf ( federationBuiltIns . requiresDirective ( subgraph . schema ) ) ) { addExternalFieldsFromDirectiveFieldSet ( subgraph , type , requiresApplication , supergraph ) ; } const fieldBaseType = baseType ( field . type ! ) ; for ( const providesApplication of field . appliedDirectivesOf ( federationBuiltIns . providesDirective ( subgraph . schema ) ) ) { assert ( isObjectType ( fieldBaseType ) || isInterfaceType ( fieldBaseType ) , ( ) => `Found @provides on field ${ field . coordinate } whose type ${ field . type ! } (${ fieldBaseType . kind }) is not an object or interface ` ) ; addExternalFieldsFromDirectiveFieldSet ( subgraph , fieldBaseType , providesApplication , supergraph ) ; } } addExternalFieldsFromInterface ( type ) ; } } function addExternalFieldsFromDirectiveFieldSet ( subgraph , parentType , directive , supergraph , forceNonExternal = false ) { const external = federationBuiltIns . externalDirective ( subgraph . schema ) ; const accessor = function ( type , fieldName ) : FieldDefinition < any > { const field = type . field ( fieldName ) ; if ( field ) { if ( forceNonExternal && field . hasAppliedDirective ( external ) ) { field . appliedDirectivesOf ( external ) . forEach ( d => d . remove ( ) ) ; } return field ; } assert ( ! isUnionType ( type ) , ( ) => `Shouldn't select field ${ fieldName } from union type ${ type }` ) ; const supergraphType = supergraph . type ( type . name ) as ObjectType | InterfaceType ; const supergraphField = supergraphType . field ( fieldName ) ; assert ( supergraphField , ( ) => `No field named ${ fieldName } found on type ${ type . name } in the supergraph` ) ; const created = addSubgraphObjectOrInterfaceField ( supergraphField , subgraph ) ! ; if ( ! forceNonExternal ) { created . applyDirective ( external ) ; } return created ; } ; parseFieldSetArgument ( parentType , directive , accessor ) ; } function addExternalFieldsFromInterface ( type ) { for ( const itf of type . interfaces ( ) ) { for ( const field of itf . fields ( ) ) { const typeField = type . field ( field . name ) ; if ( ! typeField ) { copyFieldAsExternal ( field , type ) ; } else if ( typeField . hasAppliedDirective ( externalDirectiveName ) ) { maybeUpdateFieldForInterface ( typeField , field ) ; } } } } function copyFieldAsExternal ( field , type ) { const newField = type . addField ( field . name , field . type ) ; for ( const arg of field . arguments ( ) ) { newField . addArgument ( arg . name , arg . type , arg . defaultValue ) ; } newField . applyDirective ( externalDirectiveName ) ; } function maybeUpdateFieldForInterface ( toModify , itfField ) { if ( ! isSubtype ( itfField . type ! , toModify . type ! ) ) { assert ( isSubtype ( toModify . type ! , itfField . type ! ) , ( ) => `For ${ toModify . coordinate }, expected ${ itfField . type } and ${ toModify . type } to be in a subtyping relationship` ) ; toModify . type = itfField . type ! ; } } function removeNeedlessProvides ( subgraph ) { for ( const type of subgraph . schema . types ( ) ) { if ( ! isObjectType ( type ) && ! isInterfaceType ( type ) ) { continue ; } const providesDirective = federationBuiltIns . providesDirective ( subgraph . schema ) ; for ( const field of type . fields ( ) ) { const fieldBaseType = baseType ( field . type ! ) ; for ( const providesApplication of field . appliedDirectivesOf ( providesDirective ) ) { const selection = parseFieldSetArgument ( fieldBaseType as CompositeType , providesApplication ) ; if ( selectsNonExternalLeafField ( selection ) ) { providesApplication . remove ( ) ; const updated = withoutNonExternalLeafFields ( selection ) ; if ( ! updated . isEmpty ( ) ) { field . applyDirective ( providesDirective , { fields : updated . toString ( true , false ) } ) ; } } } } } } function isExternalOrHasExternalImplementations ( field ) { if ( field . hasAppliedDirective ( externalDirectiveName ) ) { return true ; } const parentType = field . parent ; if ( isInterfaceType ( parentType ) ) { for ( const implem of parentType . possibleRuntimeTypes ( ) ) { const fieldInImplem = implem . field ( field . name ) ; if ( fieldInImplem && fieldInImplem . hasAppliedDirective ( externalDirectiveName ) ) { return true ; } } } return false ; } function selectsNonExternalLeafField ( selection ) { return selection . selections ( ) . some ( s => { if ( s . kind === 'FieldSelection' ) { if ( isExternalOrHasExternalImplementations ( s . field . definition ) ) { return false ; } return ! s . selectionSet || selectsNonExternalLeafField ( s . selectionSet ) ; } else { return selectsNonExternalLeafField ( s . selectionSet ) ; } } ) ; } function withoutNonExternalLeafFields ( selectionSet ) { const newSelectionSet = new SelectionSet ( selectionSet . parentType ) ; for ( const selection of selectionSet . selections ( ) ) { if ( selection . kind === 'FieldSelection' ) { if ( isExternalOrHasExternalImplementations ( selection . field . definition ) ) { newSelectionSet . add ( selection ) ; continue ; } } if ( selection . selectionSet ) { const updated = withoutNonExternalLeafFields ( selection . selectionSet ) ; if ( ! updated . isEmpty ( ) ) { newSelectionSet . add ( selectionOfElement ( selection . element ( ) , updated ) ) ; } } } return newSelectionSet ; }