@@ -31,7 +31,6 @@ import {
3131 CompositeType ,
3232 Subgraphs ,
3333 JOIN_VERSIONS ,
34- INACCESSIBLE_VERSIONS ,
3534 NamedSchemaElement ,
3635 errorCauses ,
3736 isObjectType ,
@@ -69,7 +68,6 @@ import {
6968 CoreSpecDefinition ,
7069 FeatureVersion ,
7170 FEDERATION_VERSIONS ,
72- InaccessibleSpecDefinition ,
7371 LinkDirectiveArgs ,
7472 sourceIdentity ,
7573 FeatureUrl ,
@@ -81,6 +79,10 @@ import {
8179 isNullableType ,
8280 isFieldDefinition ,
8381 Post20FederationDirectiveDefinition ,
82+ DirectiveCompositionSpecification ,
83+ FeatureDefinition ,
84+ CoreImport ,
85+ inaccessibleIdentity ,
8486} from "@apollo/federation-internals" ;
8587import { ASTNode , GraphQLError , DirectiveLocation } from "graphql" ;
8688import {
@@ -345,6 +347,12 @@ interface OverrideArgs {
345347 label ?: string ;
346348}
347349
350+ interface MergedDirectiveInfo {
351+ definition : DirectiveDefinition ;
352+ argumentsMerger ?: ArgumentMerger ;
353+ staticArgumentTransform ?: StaticArgumentsTransform ;
354+ }
355+
348356class Merger {
349357 readonly names : readonly string [ ] ;
350358 readonly subgraphsSchema : readonly Schema [ ] ;
@@ -353,7 +361,8 @@ class Merger {
353361 readonly merged : Schema = new Schema ( ) ;
354362 readonly subgraphNamesToJoinSpecName : Map < string , string > ;
355363 readonly mergedFederationDirectiveNames = new Set < string > ( ) ;
356- readonly mergedFederationDirectiveInSupergraph = new Map < string , { definition : DirectiveDefinition , argumentsMerger ?: ArgumentMerger , staticArgumentTransform ?: StaticArgumentsTransform } > ( ) ;
364+ readonly mergedFederationDirectiveInSupergraphByDirectiveName =
365+ new Map < string , MergedDirectiveInfo > ( ) ;
357366 readonly enumUsages = new Map < string , EnumTypeUsage > ( ) ;
358367 private composeDirectiveManager : ComposeDirectiveManager ;
359368 private mismatchReporter : MismatchReporter ;
@@ -364,7 +373,7 @@ class Merger {
364373 } [ ] ;
365374 private joinSpec : JoinSpecDefinition ;
366375 private linkSpec : CoreSpecDefinition ;
367- private inaccessibleSpec : InaccessibleSpecDefinition ;
376+ private inaccessibleDirectiveInSupergraph ?: DirectiveDefinition ;
368377 private latestFedVersionUsed : FeatureVersion ;
369378 private joinDirectiveIdentityURLs = new Set < string > ( ) ;
370379 private schemaToImportNameToFeatureUrl = new Map < Schema , Map < string , FeatureUrl > > ( ) ;
@@ -375,7 +384,6 @@ class Merger {
375384 this . latestFedVersionUsed = this . getLatestFederationVersionUsed ( ) ;
376385 this . joinSpec = JOIN_VERSIONS . getMinimumRequiredVersion ( this . latestFedVersionUsed ) ;
377386 this . linkSpec = LINK_VERSIONS . getMinimumRequiredVersion ( this . latestFedVersionUsed ) ;
378- this . inaccessibleSpec = INACCESSIBLE_VERSIONS . getMinimumRequiredVersion ( this . latestFedVersionUsed ) ;
379387 this . fieldsWithFromContext = this . getFieldsWithFromContextDirective ( ) ;
380388 this . fieldsWithOverride = this . getFieldsWithOverrideDirective ( ) ;
381389
@@ -470,59 +478,127 @@ class Merger {
470478 assert ( errors . length === 0 , "We shouldn't have errors adding the join spec to the (still empty) supergraph schema" ) ;
471479
472480 const directivesMergeInfo = collectCoreDirectivesToCompose ( this . subgraphs ) ;
473- for ( const mergeInfo of directivesMergeInfo ) {
474- this . validateAndMaybeAddSpec ( mergeInfo ) ;
475- }
476-
481+ this . validateAndMaybeAddSpecs ( directivesMergeInfo ) ;
477482 return this . joinSpec . populateGraphEnum ( this . merged , this . subgraphs ) ;
478483 }
479484
480- private validateAndMaybeAddSpec ( { url, name, definitionsPerSubgraph, compositionSpec} : CoreDirectiveInSubgraphs ) {
481- // Not composition specification means that it shouldn't be composed.
482- if ( ! compositionSpec ) {
483- return ;
484- }
485-
486- let nameInSupergraph : string | undefined ;
487- for ( const subgraph of this . subgraphs ) {
488- const directive = definitionsPerSubgraph . get ( subgraph . name ) ;
489- if ( ! directive ) {
490- continue ;
485+ private validateAndMaybeAddSpecs ( directivesMergeInfo : CoreDirectiveInSubgraphs [ ] ) {
486+ const supergraphInfoByIdentity = new Map <
487+ string ,
488+ {
489+ specInSupergraph : FeatureDefinition ;
490+ directives : {
491+ nameInFeature : string ;
492+ nameInSupergraph : string ;
493+ compositionSpec : DirectiveCompositionSpecification ;
494+ } [ ] ;
491495 }
496+ > ;
492497
493- if ( ! nameInSupergraph ) {
494- nameInSupergraph = directive . name ;
495- } else if ( nameInSupergraph !== directive . name ) {
496- this . mismatchReporter . reportMismatchError (
497- ERRORS . LINK_IMPORT_NAME_MISMATCH ,
498- `The "@${ name } " directive (from ${ url } ) is imported with mismatched name between subgraphs: it is imported as ` ,
499- directive ,
500- sourcesFromArray ( this . subgraphs . values ( ) . map ( ( s ) => definitionsPerSubgraph . get ( s . name ) ) ) ,
501- ( def ) => `"@${ def . name } "` ,
502- ) ;
498+ for ( const { url, name, definitionsPerSubgraph, compositionSpec} of directivesMergeInfo ) {
499+ // No composition specification means that it shouldn't be composed.
500+ if ( ! compositionSpec ) {
503501 return ;
504502 }
503+
504+ let nameInSupergraph : string | undefined ;
505+ for ( const subgraph of this . subgraphs ) {
506+ const directive = definitionsPerSubgraph . get ( subgraph . name ) ;
507+ if ( ! directive ) {
508+ continue ;
509+ }
510+
511+ if ( ! nameInSupergraph ) {
512+ nameInSupergraph = directive . name ;
513+ } else if ( nameInSupergraph !== directive . name ) {
514+ this . mismatchReporter . reportMismatchError (
515+ ERRORS . LINK_IMPORT_NAME_MISMATCH ,
516+ `The "@${ name } " directive (from ${ url } ) is imported with mismatched name between subgraphs: it is imported as ` ,
517+ directive ,
518+ sourcesFromArray ( this . subgraphs . values ( ) . map ( ( s ) => definitionsPerSubgraph . get ( s . name ) ) ) ,
519+ ( def ) => `"@${ def . name } "` ,
520+ ) ;
521+ return ;
522+ }
523+ }
524+
525+ // If we get here with `nameInSupergraph` unset, it means there is no usage for the directive at all and we
526+ // don't bother adding the spec to the supergraph.
527+ if ( nameInSupergraph ) {
528+ const specInSupergraph = compositionSpec . supergraphSpecification ( this . latestFedVersionUsed ) ;
529+ let supergraphInfo = supergraphInfoByIdentity . get ( specInSupergraph . url . identity ) ;
530+ if ( supergraphInfo ) {
531+ assert (
532+ specInSupergraph . url . equals ( supergraphInfo . specInSupergraph . url ) ,
533+ `Spec ${ specInSupergraph . url } directives disagree on version for supergraph` ,
534+ ) ;
535+ } else {
536+ supergraphInfo = {
537+ specInSupergraph,
538+ directives : [ ] ,
539+ } ;
540+ supergraphInfoByIdentity . set ( specInSupergraph . url . identity , supergraphInfo ) ;
541+ }
542+ supergraphInfo . directives . push ( {
543+ nameInFeature : name ,
544+ nameInSupergraph,
545+ compositionSpec,
546+ } ) ;
547+ }
505548 }
506549
507- // If we get here with `nameInSupergraph` unset, it means there is no usage for the directive at all and we
508- // don't bother adding the spec to the supergraph.
509- if ( nameInSupergraph ) {
510- const specInSupergraph = compositionSpec . supergraphSpecification ( this . latestFedVersionUsed ) ;
511- const errors = this . linkSpec . applyFeatureAsLink ( this . merged , specInSupergraph , specInSupergraph . defaultCorePurpose , [ { name, as : name === nameInSupergraph ? undefined : nameInSupergraph } ] , ) ;
512- assert ( errors . length === 0 , "We shouldn't have errors adding the join spec to the (still empty) supergraph schema" ) ;
513- const feature = this . merged ?. coreFeatures ?. getByIdentity ( specInSupergraph . url . identity ) ;
550+ for ( const { specInSupergraph, directives } of supergraphInfoByIdentity . values ( ) ) {
551+ const imports : CoreImport [ ] = [ ] ;
552+ for ( const { nameInFeature, nameInSupergraph } of directives ) {
553+ const defaultNameInSupergraph = CoreFeature . directiveNameInSchemaForCoreArguments (
554+ specInSupergraph . url ,
555+ specInSupergraph . url . name ,
556+ [ ] ,
557+ nameInFeature ,
558+ ) ;
559+ if ( nameInSupergraph !== defaultNameInSupergraph ) {
560+ imports . push ( nameInFeature === nameInSupergraph
561+ ? { name : `@${ nameInFeature } ` }
562+ : { name : `@${ nameInFeature } ` , as : `@${ nameInSupergraph } ` }
563+ ) ;
564+ }
565+ }
566+ const errors = this . linkSpec . applyFeatureToSchema (
567+ this . merged ,
568+ specInSupergraph ,
569+ undefined ,
570+ specInSupergraph . defaultCorePurpose ,
571+ imports ,
572+ ) ;
573+ assert (
574+ errors . length === 0 ,
575+ "We shouldn't have errors adding the join spec to the (still empty) supergraph schema"
576+ ) ;
577+ const feature = this . merged . coreFeatures ?. getByIdentity ( specInSupergraph . url . identity ) ;
514578 assert ( feature , 'Should have found the feature we just added' ) ;
515- const argumentsMerger = compositionSpec . argumentsMerger ?. call ( null , this . merged , feature ) ;
516- if ( argumentsMerger instanceof GraphQLError ) {
517- // That would mean we made a mistake in the declaration of a hard-coded directive, so we just throw right away so this can be caught and corrected.
518- throw argumentsMerger ;
519- }
520- this . mergedFederationDirectiveNames . add ( nameInSupergraph ) ;
521- this . mergedFederationDirectiveInSupergraph . set ( name , {
522- definition : this . merged . directive ( nameInSupergraph ) ! ,
523- argumentsMerger,
524- staticArgumentTransform : compositionSpec . staticArgumentTransform ,
525- } ) ;
579+ for ( const { nameInFeature, nameInSupergraph, compositionSpec } of directives ) {
580+ const argumentsMerger = compositionSpec . argumentsMerger ?. call ( null , this . merged , feature ) ;
581+ if ( argumentsMerger instanceof GraphQLError ) {
582+ // That would mean we made a mistake in the declaration of a hard-coded directive,
583+ // so we just throw right away so this can be caught and corrected.
584+ throw argumentsMerger ;
585+ }
586+ this . mergedFederationDirectiveNames . add ( nameInSupergraph ) ;
587+ this . mergedFederationDirectiveInSupergraphByDirectiveName . set ( nameInSupergraph , {
588+ definition : this . merged . directive ( nameInSupergraph ) ! ,
589+ argumentsMerger,
590+ staticArgumentTransform : compositionSpec . staticArgumentTransform ,
591+ } ) ;
592+ // If we encounter the @inaccessible directive, we need to record its
593+ // definition so certain merge validations that care about @inaccessible
594+ // can act accordingly.
595+ if (
596+ specInSupergraph . identity === inaccessibleIdentity
597+ && nameInFeature === specInSupergraph . url . name
598+ ) {
599+ this . inaccessibleDirectiveInSupergraph = this . merged . directive ( nameInSupergraph ) ! ;
600+ }
601+ }
526602 }
527603 }
528604
@@ -2464,8 +2540,8 @@ class Merger {
24642540 this . recordAppliedDirectivesToMerge ( valueSources , value ) ;
24652541 this . addJoinEnumValue ( valueSources , value ) ;
24662542
2467- const inaccessibleInSupergraph = this . mergedFederationDirectiveInSupergraph . get ( this . inaccessibleSpec . inaccessibleDirectiveSpec . name ) ;
2468- const isInaccessible = inaccessibleInSupergraph && value . hasAppliedDirective ( inaccessibleInSupergraph . definition ) ;
2543+ const isInaccessible = this . inaccessibleDirectiveInSupergraph
2544+ && value . hasAppliedDirective ( this . inaccessibleDirectiveInSupergraph ) ;
24692545 // The merging strategy depends on the enum type usage:
24702546 // - if it is _only_ used in position of Input type, we merge it with an "intersection" strategy (like other input types/things).
24712547 // - if it is _only_ used in position of Output type, we merge it with an "union" strategy (like other output types/things).
@@ -2562,8 +2638,6 @@ class Merger {
25622638 }
25632639
25642640 private mergeInput ( inputSources : Sources < InputObjectType > , dest : InputObjectType ) {
2565- const inaccessibleInSupergraph = this . mergedFederationDirectiveInSupergraph . get ( this . inaccessibleSpec . inaccessibleDirectiveSpec . name ) ;
2566-
25672641 // Like for other inputs, we add all the fields found in any subgraphs initially as a simple mean to have a complete list of
25682642 // field to iterate over, but we will remove those that are not in all subgraphs.
25692643 const added = this . addFieldsShallow ( inputSources , dest ) ;
@@ -2572,7 +2646,8 @@ class Merger {
25722646 // compatibility between definitions and 2) we actually want to see if the result is marked inaccessible or not and it makes
25732647 // that easier.
25742648 this . mergeInputField ( subgraphFields , destField ) ;
2575- const isInaccessible = inaccessibleInSupergraph && destField . hasAppliedDirective ( inaccessibleInSupergraph . definition ) ;
2649+ const isInaccessible = this . inaccessibleDirectiveInSupergraph
2650+ && destField . hasAppliedDirective ( this . inaccessibleDirectiveInSupergraph ) ;
25762651 // Note that if the field is manually marked @inaccessible , we can always accept it to be inconsistent between subgraphs since
25772652 // it won't be exposed in the API, and we don't hint about it because we're just doing what the user is explicitely asking.
25782653 if ( ! isInaccessible && someSources ( subgraphFields , field => ! field ) ) {
@@ -2840,8 +2915,7 @@ class Merger {
28402915 // is @inaccessible , which is necessary to exist in the supergraph for EnumValues to properly
28412916 // determine whether the fact that a value is both input / output will matter
28422917 private recordAppliedDirectivesToMerge ( sources : Sources < SchemaElement < any , any > > , dest : SchemaElement < any , any > ) {
2843- const inaccessibleInSupergraph = this . mergedFederationDirectiveInSupergraph . get ( this . inaccessibleSpec . inaccessibleDirectiveSpec . name ) ;
2844- const inaccessibleName = inaccessibleInSupergraph ?. definition . name ;
2918+ const inaccessibleName = this . inaccessibleDirectiveInSupergraph ?. name ;
28452919 const names = this . gatherAppliedDirectiveNames ( sources ) ;
28462920
28472921 if ( inaccessibleName && names . has ( inaccessibleName ) ) {
@@ -2905,7 +2979,7 @@ class Merger {
29052979 return ;
29062980 }
29072981
2908- const directiveInSupergraph = this . mergedFederationDirectiveInSupergraph . get ( name ) ;
2982+ const directiveInSupergraph = this . mergedFederationDirectiveInSupergraphByDirectiveName . get ( name ) ;
29092983
29102984 if ( dest . schema ( ) . directive ( name ) ?. repeatable ) {
29112985 // For repeatable directives, we simply include each application found but with exact duplicates removed
@@ -2945,7 +3019,7 @@ class Merger {
29453019 if ( differentApplications . length === 1 ) {
29463020 dest . applyDirective ( name , differentApplications [ 0 ] . arguments ( false ) ) ;
29473021 } else {
2948- const info = this . mergedFederationDirectiveInSupergraph . get ( name ) ;
3022+ const info = this . mergedFederationDirectiveInSupergraphByDirectiveName . get ( name ) ;
29493023 if ( info && info . argumentsMerger ) {
29503024 const mergedArguments = Object . create ( null ) ;
29513025 const applicationsArguments = differentApplications . map ( ( a ) => a . arguments ( true ) ) ;
0 commit comments