@@ -62,6 +62,7 @@ import {
6262} from "@apollo/query-graphs" ;
6363import { CompositionHint , HINTS } from "./hints" ;
6464import { ASTNode , GraphQLError , print } from "graphql" ;
65+ import { CompositionOptions } from './compose' ;
6566
6667const debug = newDebugLogger ( 'validation' ) ;
6768
@@ -310,6 +311,7 @@ export function validateGraphComposition(
310311 subgraphNameToGraphEnumValue : Map < string , string > ,
311312 supergraphAPI : QueryGraph ,
312313 federatedQueryGraph : QueryGraph ,
314+ compositionOptions : CompositionOptions = { } ,
313315) : {
314316 errors ? : GraphQLError [ ] ,
315317 hints ? : CompositionHint [ ] ,
@@ -319,6 +321,7 @@ export function validateGraphComposition(
319321 subgraphNameToGraphEnumValue ,
320322 supergraphAPI ,
321323 federatedQueryGraph ,
324+ compositionOptions ,
322325 ) . validate ( ) ;
323326 return errors . length > 0 ? { errors, hints } : { hints } ;
324327}
@@ -695,19 +698,26 @@ class ValidationTraversal {
695698 private readonly validationHints : CompositionHint [ ] = [ ] ;
696699
697700 private readonly context : ValidationContext ;
698-
701+ private totalValidationSubgraphPaths = 0 ;
702+ private maxValidationSubgraphPaths : number ;
703+
704+ private static DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS = 1000000 ;
705+
699706 constructor (
700707 supergraphSchema : Schema ,
701708 subgraphNameToGraphEnumValue : Map < string , string > ,
702709 supergraphAPI : QueryGraph ,
703710 federatedQueryGraph : QueryGraph ,
711+ compositionOptions : CompositionOptions ,
704712 ) {
713+ this . maxValidationSubgraphPaths = compositionOptions . maxValidationSubgraphPaths ?? ValidationTraversal . DEFAULT_MAX_VALIDATION_SUBGRAPH_PATHS ;
714+
705715 this . conditionResolver = simpleValidationConditionResolver ( {
706716 supergraph : supergraphSchema ,
707717 queryGraph : federatedQueryGraph ,
708718 withCaching : true ,
709719 } ) ;
710- supergraphAPI . rootKinds ( ) . forEach ( ( kind ) => this . stack . push ( ValidationState . initial ( {
720+ supergraphAPI . rootKinds ( ) . forEach ( ( kind ) => this . pushStack ( ValidationState . initial ( {
711721 supergraphAPI,
712722 kind,
713723 federatedQueryGraph,
@@ -720,18 +730,38 @@ class ValidationTraversal {
720730 subgraphNameToGraphEnumValue ,
721731 ) ;
722732 }
733+
734+ pushStack ( state : ValidationState ) : { error ?: GraphQLError } {
735+ this . totalValidationSubgraphPaths += state . subgraphPathInfos . length ;
736+ this . stack . push ( state ) ;
737+ if ( this . totalValidationSubgraphPaths > this . maxValidationSubgraphPaths ) {
738+ return { error : ERRORS . MAX_VALIDATION_SUBGRAPH_PATHS_EXCEEDED . err ( `Maximum number of validation subgraph paths exceeded: ${ this . totalValidationSubgraphPaths } ` ) } ;
739+ }
740+ return { } ;
741+ }
742+
743+ popStack ( ) {
744+ const state = this . stack . pop ( ) ;
745+ if ( state ) {
746+ this . totalValidationSubgraphPaths -= state . subgraphPathInfos . length ;
747+ }
748+ return state ;
749+ }
723750
724751 validate ( ) : {
725752 errors : GraphQLError [ ] ,
726753 hints : CompositionHint [ ] ,
727754 } {
728755 while ( this . stack . length > 0 ) {
729- this . handleState ( this . stack . pop ( ) ! ) ;
756+ const { error } = this . handleState ( this . popStack ( ) ! ) ;
757+ if ( error ) {
758+ return { errors : [ error ] , hints : this . validationHints } ;
759+ }
730760 }
731761 return { errors : this . validationErrors , hints : this . validationHints } ;
732762 }
733763
734- private handleState ( state : ValidationState ) {
764+ private handleState ( state : ValidationState ) : { error ?: GraphQLError } {
735765 debug . group ( ( ) => `Validation: ${ this . stack . length + 1 } open states. Validating ${ state } ` ) ;
736766 const vertex = state . supergraphPath . tail ;
737767
@@ -748,7 +778,7 @@ class ValidationTraversal {
748778 // type, and have strictly more options regarding subgraphs. So whatever comes next, we can handle in the exact
749779 // same way we did previously, and there is thus no way to bother.
750780 debug . groupEnd ( `Has already validated this vertex.` ) ;
751- return ;
781+ return { } ;
752782 }
753783 }
754784 // We're gonna have to validate, but we can save the new set of sources here to hopefully save work later.
@@ -799,12 +829,16 @@ class ValidationTraversal {
799829 // state to the stack this method, `handleState`, will do nothing later. But it's
800830 // worth checking it now and save some memory/cycles.
801831 if ( newState && ! newState . supergraphPath . isTerminal ( ) ) {
802- this . stack . push ( newState ) ;
832+ const { error } = this . pushStack ( newState ) ;
833+ if ( error ) {
834+ return { error } ;
835+ }
803836 debug . groupEnd ( ( ) => `Reached new state ${ newState } ` ) ;
804837 } else {
805838 debug . groupEnd ( `Reached terminal vertex/cycle` ) ;
806839 }
807840 }
808841 debug . groupEnd ( ) ;
842+ return { } ;
809843 }
810844}
0 commit comments