From 38debcf2f9af1a719bd1c8acbd9335efa8427ddb Mon Sep 17 00:00:00 2001 From: "Sachin D. Shinde" Date: Mon, 8 Jul 2024 15:51:47 -0700 Subject: [PATCH] Wrap dead-end logic with closures (#3068) While looking at satisfiability validations, @duckki noticed that we were throwing away dead-end/unadvanceable data. I wanted to check whether we could use closures to avoid executing dead-logic entirely unless it's needed (when no options are found for a path), and it turns out it's possible (specifically, the closure's state ends up being immutable, so delaying execution shouldn't change behavior). This PR accordingly wraps the dead-end logic with closures, and only executes them when needed for generating satisfiability errors. --- .changeset/afraid-phones-reflect.md | 6 + composition-js/src/validate.ts | 9 +- query-graphs-js/src/graphPath.ts | 416 +++++++++++++++------------- 3 files changed, 241 insertions(+), 190 deletions(-) create mode 100644 .changeset/afraid-phones-reflect.md diff --git a/.changeset/afraid-phones-reflect.md b/.changeset/afraid-phones-reflect.md new file mode 100644 index 000000000..af613fa08 --- /dev/null +++ b/.changeset/afraid-phones-reflect.md @@ -0,0 +1,6 @@ +--- +"@apollo/composition": patch +"@apollo/query-graphs": patch +--- + +Error messages are now lazily evaluated for satisfiability validations. diff --git a/composition-js/src/validate.ts b/composition-js/src/validate.ts index 82a404822..82b58a962 100644 --- a/composition-js/src/validate.ts +++ b/composition-js/src/validate.ts @@ -51,13 +51,14 @@ import { Transition, QueryGraphState, Unadvanceables, - isUnadvanceable, Unadvanceable, noConditionsResolution, TransitionPathWithLazyIndirectPaths, RootVertex, simpleValidationConditionResolver, ConditionResolver, + UnadvanceableClosures, + isUnadvanceableClosures, } from "@apollo/query-graphs"; import { CompositionHint, HINTS } from "./hints"; import { ASTNode, GraphQLError, print } from "graphql"; @@ -486,7 +487,7 @@ export class ValidationState { const transition = supergraphEdge.transition; const targetType = supergraphEdge.tail.type; const newSubgraphPathInfos: SubgraphPathInfo[] = []; - const deadEnds: Unadvanceables[] = []; + const deadEnds: UnadvanceableClosures[] = []; // If the edge has an override condition, we should capture it in the state so // that we can ignore later edges that don't satisfy the condition. const newOverrideConditions = new Map([...this.selectedOverrideConditions]); @@ -504,7 +505,7 @@ export class ValidationState { targetType, newOverrideConditions, ); - if (isUnadvanceable(options)) { + if (isUnadvanceableClosures(options)) { deadEnds.push(options); continue; } @@ -533,7 +534,7 @@ export class ValidationState { } const newPath = this.supergraphPath.add(transition, supergraphEdge, noConditionsResolution); if (newSubgraphPathInfos.length === 0) { - return { error: satisfiabilityError(newPath, this.subgraphPathInfos.map((p) => p.path.path), deadEnds) }; + return { error: satisfiabilityError(newPath, this.subgraphPathInfos.map((p) => p.path.path), deadEnds.map((d) => d.toUnadvanceables())) }; } const updatedState = new ValidationState( diff --git a/query-graphs-js/src/graphPath.ts b/query-graphs-js/src/graphPath.ts index eed54b0b8..e37751028 100644 --- a/query-graphs-js/src/graphPath.ts +++ b/query-graphs-js/src/graphPath.ts @@ -1028,8 +1028,29 @@ export class Unadvanceables { } } -export function isUnadvanceable(result: any[] | Unadvanceables): result is Unadvanceables { - return result instanceof Unadvanceables; +export type UnadvanceableClosure = () => Unadvanceable | Unadvanceable[]; + +export class UnadvanceableClosures { + private _unadvanceables: Unadvanceables | undefined; + readonly closures: UnadvanceableClosure[]; + constructor(closures: UnadvanceableClosure | UnadvanceableClosure[]) { + if (Array.isArray(closures)) { + this.closures = closures; + } else { + this.closures = [closures]; + } + } + + toUnadvanceables(): Unadvanceables { + if (!this._unadvanceables) { + this._unadvanceables = new Unadvanceables(this.closures.map((c) => c()).flat()); + } + return this._unadvanceables; + } +} + +export function isUnadvanceableClosures(result: any[] | UnadvanceableClosures): result is UnadvanceableClosures { + return result instanceof UnadvanceableClosures; } function pathTransitionToEdge(graph: QueryGraph, vertex: Vertex, transition: Transition, overrideConditions: Map): Edge | null | undefined { @@ -1115,7 +1136,7 @@ export function advancePathWithTransition( transition: Transition, targetType: NamedType, overrideConditions: Map, -) : TransitionPathWithLazyIndirectPaths[] | Unadvanceables { +) : TransitionPathWithLazyIndirectPaths[] | UnadvanceableClosures { // The `transition` comes from the supergraph. Now, it is possible that a transition can be expressed on the supergraph, but correspond // to an 'unsatisfiable' condition on the subgraph. Let's consider: // - Subgraph A: @@ -1198,11 +1219,11 @@ export function advancePathWithTransition( overrideConditions, ); let options: GraphPath[]; - const deadEnds: Unadvanceable[] = []; - if (isUnadvanceable(directOptions)) { + const deadEndClosures: UnadvanceableClosure[] = []; + if (isUnadvanceableClosures(directOptions)) { options = []; debug.groupEnd(() => 'No direct options'); - deadEnds.push(...directOptions.reasons); + deadEndClosures.push(...directOptions.closures); } else { debug.groupEnd(() => advanceOptionsToString(directOptions)); // If we can fulfill the transition directly (without taking an edge) and the target type is "terminal", then there is @@ -1227,9 +1248,9 @@ export function advancePathWithTransition( subgraphPath.conditionResolver, overrideConditions, ); - if (isUnadvanceable(pathsWithTransition)) { + if (isUnadvanceableClosures(pathsWithTransition)) { debug.groupEnd(() => `Cannot be advanced with ${transition}`); - deadEnds.push(...pathsWithTransition.reasons); + deadEndClosures.push(...pathsWithTransition.closures); } else { debug.groupEnd(() => `Adding valid option: ${pathsWithTransition}`); options = options.concat(pathsWithTransition); @@ -1244,58 +1265,62 @@ export function advancePathWithTransition( return createLazyTransitionOptions(options, subgraphPath, overrideConditions); } - const allDeadEnds = deadEnds.concat(pathsWithNonCollecting.deadEnds.reasons); - if (transition.kind === 'FieldCollection') { - const typeName = transition.definition.parent.name; - const fieldName = transition.definition.name; - const subgraphsWithDeadEnd = new Set(allDeadEnds.map(e => e.destSubgraph)); - for (const [subgraph, schema] of subgraphPath.path.graph.sources.entries()) { - if (subgraphsWithDeadEnd.has(subgraph)) { - continue; - } - const type = schema.type(typeName); - if (type && isCompositeType(type) && type.field(fieldName)) { - // That subgraph has the type we look for, but we have recorded no dead-ends. This means there is no edge to that type, - // and thus that either: - // - it has no keys. - // - the path to advance it an @interfaceObject type, the type we look for is an implementation of that interface, and - // there no key on the interface. - const typenameOfTail = subgraphPath.path.tail.type.name; - const typeOfTailInSubgraph = schema.type(typenameOfTail); - if (!typeOfTailInSubgraph) { - // This means that 1) the type of the path we're trying to advance is different from the transition we're considering, - // and that should only happen if the path is on an @interfaceObject type, and 2) the subgraph we're looking at - // actually doesn't have that interface. To be able to jump to that subgraph, we would need the interface and it - // would need to have a resolvable key, but it has neither. - allDeadEnds.push({ - sourceSubgraph: subgraphPath.path.tail.source, - destSubgraph: subgraph, - reason: UnadvanceableReason.UNREACHABLE_TYPE, - details: `cannot move to subgraph "${subgraph}", which has field "${transition.definition.coordinate}", because interface "${typenameOfTail}" is not defined in this subgraph (to jump to "${subgraph}", it would need to both define interface "${typenameOfTail}" and have a @key on it)`, - }); - } else { - // `typeOfTailInSubgraph` exists, so it's either equal to `type`, or it's an interface of it. In any case, it's composite. - assert(isCompositeType(typeOfTailInSubgraph), () => `Type ${typeOfTailInSubgraph} in ${subgraph} should be composite`); - const metadata = federationMetadata(schema); - const keys: Directive[] = metadata ? typeOfTailInSubgraph.appliedDirectivesOf(metadata.keyDirective()) : []; - const allNonResolvable = keys.length > 0 && keys.every((k) => !(k.arguments().resolvable ?? true)); - assert(keys.length === 0 || allNonResolvable, () => `After ${subgraphPath} and for transition ${transition}, expected type ${type} in ${subgraph} to have no resolvable keys`); - const kindOfType = typeOfTailInSubgraph === type ? 'type' : 'interface'; - const explanation = keys.length === 0 - ? `${kindOfType} "${typenameOfTail}" has no @key defined in subgraph "${subgraph}"` - : `none of the @key defined on ${kindOfType} "${typenameOfTail}" in subgraph "${subgraph}" are resolvable (they are all declared with their "resolvable" argument set to false)`; - allDeadEnds.push({ - sourceSubgraph: subgraphPath.path.tail.source, - destSubgraph: subgraph, - reason: UnadvanceableReason.UNREACHABLE_TYPE, - details: `cannot move to subgraph "${subgraph}", which has field "${transition.definition.coordinate}", because ${explanation}` - }); + const indirectDeadEndClosures = pathsWithNonCollecting.deadEnds.closures; + return new UnadvanceableClosures(() => { + const allDeadEnds = new UnadvanceableClosures(deadEndClosures.concat(indirectDeadEndClosures)) + .toUnadvanceables().reasons; + if (transition.kind === 'FieldCollection') { + const typeName = transition.definition.parent.name; + const fieldName = transition.definition.name; + const subgraphsWithDeadEnd = new Set(allDeadEnds.map(e => e.destSubgraph)); + for (const [subgraph, schema] of subgraphPath.path.graph.sources.entries()) { + if (subgraphsWithDeadEnd.has(subgraph)) { + continue; + } + const type = schema.type(typeName); + if (type && isCompositeType(type) && type.field(fieldName)) { + // That subgraph has the type we look for, but we have recorded no dead-ends. This means there is no edge to that type, + // and thus that either: + // - it has no keys. + // - the path to advance it an @interfaceObject type, the type we look for is an implementation of that interface, and + // there no key on the interface. + const typenameOfTail = subgraphPath.path.tail.type.name; + const typeOfTailInSubgraph = schema.type(typenameOfTail); + if (!typeOfTailInSubgraph) { + // This means that 1) the type of the path we're trying to advance is different from the transition we're considering, + // and that should only happen if the path is on an @interfaceObject type, and 2) the subgraph we're looking at + // actually doesn't have that interface. To be able to jump to that subgraph, we would need the interface and it + // would need to have a resolvable key, but it has neither. + allDeadEnds.push({ + sourceSubgraph: subgraphPath.path.tail.source, + destSubgraph: subgraph, + reason: UnadvanceableReason.UNREACHABLE_TYPE, + details: `cannot move to subgraph "${subgraph}", which has field "${transition.definition.coordinate}", because interface "${typenameOfTail}" is not defined in this subgraph (to jump to "${subgraph}", it would need to both define interface "${typenameOfTail}" and have a @key on it)`, + }); + } else { + // `typeOfTailInSubgraph` exists, so it's either equal to `type`, or it's an interface of it. In any case, it's composite. + assert(isCompositeType(typeOfTailInSubgraph), () => `Type ${typeOfTailInSubgraph} in ${subgraph} should be composite`); + const metadata = federationMetadata(schema); + const keys: Directive[] = metadata ? typeOfTailInSubgraph.appliedDirectivesOf(metadata.keyDirective()) : []; + const allNonResolvable = keys.length > 0 && keys.every((k) => !(k.arguments().resolvable ?? true)); + assert(keys.length === 0 || allNonResolvable, () => `After ${subgraphPath} and for transition ${transition}, expected type ${type} in ${subgraph} to have no resolvable keys`); + const kindOfType = typeOfTailInSubgraph === type ? 'type' : 'interface'; + const explanation = keys.length === 0 + ? `${kindOfType} "${typenameOfTail}" has no @key defined in subgraph "${subgraph}"` + : `none of the @key defined on ${kindOfType} "${typenameOfTail}" in subgraph "${subgraph}" are resolvable (they are all declared with their "resolvable" argument set to false)`; + allDeadEnds.push({ + sourceSubgraph: subgraphPath.path.tail.source, + destSubgraph: subgraph, + reason: UnadvanceableReason.UNREACHABLE_TYPE, + details: `cannot move to subgraph "${subgraph}", which has field "${transition.definition.coordinate}", because ${explanation}` + }); + } } } } - } - - return new Unadvanceables(allDeadEnds); + + return allDeadEnds; + }); } function createLazyTransitionOptions( @@ -1363,12 +1388,12 @@ function popMin = { +export type IndirectPaths = { paths: GraphPath[], deadEnds: TDeadEnds } -function advancePathWithNonCollectingAndTypePreservingTransitions( +function advancePathWithNonCollectingAndTypePreservingTransitions( path: GraphPath, context: PathContext, conditionResolver: ConditionResolver, @@ -1386,15 +1411,17 @@ function advancePathWithNonCollectingAndTypePreservingTransitions !e.transition.collectOperationElements && e.tail.source !== path.tail.source).map((e) => e.tail.source)); return { paths: [], - deadEnds: new Unadvanceables(Array.from(reachableSubgraphs).map((s) => ({ - sourceSubgraph: path.tail.source, - destSubgraph: s, - reason: UnadvanceableReason.IGNORED_INDIRECT_PATH, - details: `ignoring moving from "${path.tail.source}" to "${s}" as a more direct option exists`, - }))) as TDeadEnds, + deadEnds: new UnadvanceableClosures(() => { + const reachableSubgraphs = new Set(path.nextEdges().filter((e) => !e.transition.collectOperationElements && e.tail.source !== path.tail.source).map((e) => e.tail.source)); + return Array.from(reachableSubgraphs).map((s) => ({ + sourceSubgraph: path.tail.source, + destSubgraph: s, + reason: UnadvanceableReason.IGNORED_INDIRECT_PATH, + details: `ignoring moving from "${path.tail.source}" to "${s}" as a more direct option exists`, + })) + }) as TDeadEnds, }; } @@ -1405,7 +1432,7 @@ function advancePathWithNonCollectingAndTypePreservingTransitions, number] | null>(); - const deadEnds: Unadvanceable[] = []; + const deadEndClosures: UnadvanceableClosure[] = []; const toTry: GraphPath[] = [ path ]; while (toTry.length > 0) { // Note that through `excluded` we avoid taking the same edge from multiple options. But that means it's important we try @@ -1422,16 +1449,20 @@ function advancePathWithNonCollectingAndTypePreservingTransitions !e.transition.collectOperationElements); if (outEdges.length > 0) { debug.log(() => `Nothing to try for ${toAdvance}: it only has "trivial" non-collecting outbound edges`); - for (const edge of outEdges) { - if (edge.tail.source !== toAdvance.tail.source && edge.tail.source !== originalSource) { - deadEnds.push({ - sourceSubgraph: toAdvance.tail.source, - destSubgraph: edge.tail.source, - reason: UnadvanceableReason.IGNORED_INDIRECT_PATH, - details: `ignoring moving to subgraph "${edge.tail.source}" using @key(fields: "${edge.conditions?.toString(true, false)}") of "${edge.head.type}" because there is a more direct path in ${edge.tail.source} that avoids ${toAdvance.tail.source} altogether` - }); + deadEndClosures.push(() => { + const unadvanceables = []; + for (const edge of outEdges) { + if (edge.tail.source !== toAdvance.tail.source && edge.tail.source !== originalSource) { + unadvanceables.push({ + sourceSubgraph: toAdvance.tail.source, + destSubgraph: edge.tail.source, + reason: UnadvanceableReason.IGNORED_INDIRECT_PATH, + details: `ignoring moving to subgraph "${edge.tail.source}" using @key(fields: "${edge.conditions?.toString(true, false)}") of "${edge.head.type}" because there is a more direct path in ${edge.tail.source} that avoids ${toAdvance.tail.source} altogether` + }); + } } - } + return unadvanceables; + }) } else { debug.log(() => `Nothing to try for ${toAdvance}: it has no non-collecting outbound edges`); } @@ -1575,11 +1606,13 @@ function advancePathWithNonCollectingAndTypePreservingTransitions { + return { + sourceSubgraph: toAdvance.tail.source, + destSubgraph: edge.tail.source, + reason: UnadvanceableReason.IGNORED_INDIRECT_PATH, + details: `ignoring moving to subgraph "${edge.tail.source}" using @key(fields: "${edge.conditions?.toString(true, false)}") of "${edge.head.type}" because there is a more direct path in ${edge.tail.source} that avoids ${toAdvance.tail.source} altogether` + }; }); continue; } @@ -1599,17 +1632,19 @@ function advancePathWithNonCollectingAndTypePreservingTransitions { + const source = toAdvance.tail.source; + const dest = edge.tail.source; + const hasOverriddenField = conditionHasOverriddenFieldsInSource(path.graph.sources.get(toAdvance.tail.source)!, edge.conditions!); + const extraMsg = hasOverriddenField + ? ` (note that some of those key fields are overridden in "${source}")` + : ""; + return { + sourceSubgraph: source, + destSubgraph: dest, + reason: UnadvanceableReason.UNSATISFIABLE_KEY_CONDITION, + details: `cannot move to subgraph "${dest}" using @key(fields: "${edge.conditions?.toString(true, false)}") of "${edge.head.type}", the key field(s) cannot be resolved from subgraph "${source}"${extraMsg}` + }; }); } debug.groupEnd(); // End of edge @@ -1618,7 +1653,7 @@ function advancePathWithNonCollectingAndTypePreservingTransitions p !== null).map(b => b![0]), - deadEnds: new Unadvanceables(deadEnds) as TDeadEnds + deadEnds: new UnadvanceableClosures(deadEndClosures) as TDeadEnds } } @@ -1670,7 +1705,7 @@ function advancePathWithDirectTransition( transition: Transition, conditionResolver: ConditionResolver, overrideConditions: Map, -) : GraphPath[] | Unadvanceables { +) : GraphPath[] | UnadvanceableClosures { assert(transition.collectOperationElements, "Supergraphs shouldn't have transitions that don't collect elements"); if ( @@ -1693,7 +1728,7 @@ function advancePathWithDirectTransition( // The case we described above should be the only case we capture here, and so the current // subgraph must have the implementation type (it may not have the field we want, but it // must have the type) and so we should be able to advance to it. - assert(!isUnadvanceable(updatedPath), () => `Advancing ${path} for ${transition} gave ${updatedPath}`); + assert(!isUnadvanceableClosures(updatedPath), () => `Advancing ${path} for ${transition} gave ${updatedPath}`); // Also note that there is currently no case where we should have more that one option. assert(updatedPath.length === 1, () => `Expect one path, got ${updatedPath.length}`) path = updatedPath[0]; @@ -1701,7 +1736,7 @@ function advancePathWithDirectTransition( } const options: GraphPath[] = []; - const deadEnds: Unadvanceable[] = []; + const deadEndClosures: UnadvanceableClosure[] = []; for (const edge of path.nextEdges()) { // The edge must match the transition. If it doesn't, we cannot use it. @@ -1713,11 +1748,14 @@ function advancePathWithDirectTransition( edge.overrideCondition && !edge.satisfiesOverrideConditions(overrideConditions) ) { - deadEnds.push({ - destSubgraph: edge.tail.source, - sourceSubgraph: edge.head.source, - reason: UnadvanceableReason.UNSATISFIABLE_OVERRIDE_CONDITION, - details: `Unable to take edge ${edge.toString()} because override condition "${edge.overrideCondition.label}" is ${overrideConditions.get(edge.overrideCondition.label)}`, + deadEndClosures.push(() => { + return { + destSubgraph: edge.tail.source, + sourceSubgraph: edge.head.source, + reason: UnadvanceableReason.UNSATISFIABLE_OVERRIDE_CONDITION, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + details: `Unable to take edge ${edge.toString()} because override condition "${edge.overrideCondition!.label}" is ${overrideConditions.get(edge.overrideCondition!.label)}`, + }; }); continue; } @@ -1727,101 +1765,107 @@ function advancePathWithDirectTransition( if (conditionResolution.satisfied) { options.push(path.add(transition, edge, conditionResolution)); } else { - switch (edge.transition.kind) { - case 'FieldCollection': - { - // Condition on a field means a @require - const field = edge.transition.definition; - const parentTypeInSubgraph = path.graph.sources.get(edge.head.source)!.type(field.parent.name)! as CompositeType; - const details = conditionResolution.unsatisfiedConditionReason === UnsatisfiedConditionReason.NO_POST_REQUIRE_KEY - ? `@require condition on field "${field.coordinate}" can be satisfied but missing usable key on "${parentTypeInSubgraph}" in subgraph "${edge.head.source}" to resume query` - : conditionResolution.unsatisfiedConditionReason === UnsatisfiedConditionReason.NO_CONTEXT_SET - ? `could not find a match for required context for field "${field.coordinate}"` - // TODO: This isn't necessarily just because an @requires - // condition was unsatisfied, but could also be because a - // @fromContext condition was unsatisfied. - : `cannot satisfy @require conditions on field "${field.coordinate}"${warnOnKeyFieldsMarkedExternal(parentTypeInSubgraph)}`; - deadEnds.push({ - sourceSubgraph: edge.head.source, - destSubgraph: edge.head.source, - reason: UnadvanceableReason.UNSATISFIABLE_REQUIRES_CONDITION, - details - }); - } - break; - case 'InterfaceObjectFakeDownCast': - { - // The condition on such edge is only __typename, so it essentially means that an @interfaceObject exists but there is no reachable subgraph - // with a @key on an interface to find out proper implementations. - const details = conditionResolution.unsatisfiedConditionReason === UnsatisfiedConditionReason.NO_POST_REQUIRE_KEY - ? `@interfaceObject type "${edge.transition.sourceType.coordinate}" misses a resolvable key to resume query once the implementation type has been resolved` - : `no subgraph can be reached to resolve the implementation type of @interfaceObject type "${edge.transition.sourceType.coordinate}"`; - deadEnds.push({ - sourceSubgraph: edge.head.source, - destSubgraph: edge.head.source, - reason: UnadvanceableReason.UNRESOLVABLE_INTERFACE_OBJECT, - details - }); - } - break; - default: - assert(false, () => `Shouldn't have conditions on direct transition ${transition}`); - } + deadEndClosures.push(() => { + switch (edge.transition.kind) { + case 'FieldCollection': + { + // Condition on a field means a @require + const field = edge.transition.definition; + const parentTypeInSubgraph = path.graph.sources.get(edge.head.source)!.type(field.parent.name)! as CompositeType; + const details = conditionResolution.unsatisfiedConditionReason === UnsatisfiedConditionReason.NO_POST_REQUIRE_KEY + ? `@require condition on field "${field.coordinate}" can be satisfied but missing usable key on "${parentTypeInSubgraph}" in subgraph "${edge.head.source}" to resume query` + : conditionResolution.unsatisfiedConditionReason === UnsatisfiedConditionReason.NO_CONTEXT_SET + ? `could not find a match for required context for field "${field.coordinate}"` + // TODO: This isn't necessarily just because an @requires + // condition was unsatisfied, but could also be because a + // @fromContext condition was unsatisfied. + : `cannot satisfy @require conditions on field "${field.coordinate}"${warnOnKeyFieldsMarkedExternal(parentTypeInSubgraph)}`; + return { + sourceSubgraph: edge.head.source, + destSubgraph: edge.head.source, + reason: UnadvanceableReason.UNSATISFIABLE_REQUIRES_CONDITION, + details + }; + } + case 'InterfaceObjectFakeDownCast': + { + // The condition on such edge is only __typename, so it essentially means that an @interfaceObject exists but there is no reachable subgraph + // with a @key on an interface to find out proper implementations. + const details = conditionResolution.unsatisfiedConditionReason === UnsatisfiedConditionReason.NO_POST_REQUIRE_KEY + ? `@interfaceObject type "${edge.transition.sourceType.coordinate}" misses a resolvable key to resume query once the implementation type has been resolved` + : `no subgraph can be reached to resolve the implementation type of @interfaceObject type "${edge.transition.sourceType.coordinate}"`; + return { + sourceSubgraph: edge.head.source, + destSubgraph: edge.head.source, + reason: UnadvanceableReason.UNRESOLVABLE_INTERFACE_OBJECT, + details + }; + } + default: + assert(false, () => `Shouldn't have conditions on direct transition ${transition}`); + } + }); } } if (options.length > 0) { return options; - } else if (deadEnds.length > 0) { - return new Unadvanceables(deadEnds); - } else { - let details: string; - const subgraph = path.tail.source; - if (transition.kind === 'FieldCollection') { - const schema = path.graph.sources.get(subgraph)!; - const fieldTypeName = transition.definition.parent.name; - const typeInSubgraph = schema.type(fieldTypeName); - if (!typeInSubgraph && path.tail.type.name !== fieldTypeName) { - // This is due to us looking for an implementation field, but the subgraph not having that implementation because - // it uses @interfaceObject on an interface of that implementation. - details = `cannot find implementation type "${fieldTypeName}" (supergraph interface "${path.tail.type.name}" is declared with @interfaceObject in "${subgraph}")`; - } else { - const fieldInSubgraph = typeInSubgraph && isCompositeType(typeInSubgraph) - ? typeInSubgraph.field(transition.definition.name) - : undefined; + } - if (fieldInSubgraph) { - // the subgraph has the field but no corresponding edge. This should only happen if the field is external. - const externalDirective = fieldInSubgraph.appliedDirectivesOf(federationMetadata(fieldInSubgraph.schema())!.externalDirective()).pop(); - assert( - externalDirective, - () => `${fieldInSubgraph.coordinate} in ${subgraph} is not external but there is no corresponding edge (edges from ${path} = [${path.nextEdges().join(', ')}])` - ); - // but the field is external in the "subgraph-extracted-from-the-supergraph", but it might have been forced to an external - // due to being a used-overriden field, in which case we want to amend the message to avoid confusing the user. - // Note that the subgraph extraction marks such "forced external due to being overriden" by setting the "reason" to "[overridden]". - const overriddingSources = externalDirective.arguments().reason === '[overridden]' - ? findOverriddingSourcesIfOverridden(fieldInSubgraph, subgraph, path.graph.sources) - : []; - if (overriddingSources.length > 0) { - details = `field "${transition.definition.coordinate}" is not resolvable because it is overridden by ${printSubgraphNames(overriddingSources)}`; + return new UnadvanceableClosures(() => { + const deadEnds = new UnadvanceableClosures(deadEndClosures).toUnadvanceables().reasons; + if (deadEnds.length > 0) { + return deadEnds; + } else { + let details: string; + const subgraph = path.tail.source; + if (transition.kind === 'FieldCollection') { + const schema = path.graph.sources.get(subgraph)!; + const fieldTypeName = transition.definition.parent.name; + const typeInSubgraph = schema.type(fieldTypeName); + if (!typeInSubgraph && path.tail.type.name !== fieldTypeName) { + // This is due to us looking for an implementation field, but the subgraph not having that implementation because + // it uses @interfaceObject on an interface of that implementation. + details = `cannot find implementation type "${fieldTypeName}" (supergraph interface "${path.tail.type.name}" is declared with @interfaceObject in "${subgraph}")`; + } else { + const fieldInSubgraph = typeInSubgraph && isCompositeType(typeInSubgraph) + ? typeInSubgraph.field(transition.definition.name) + : undefined; + + if (fieldInSubgraph) { + // the subgraph has the field but no corresponding edge. This should only happen if the field is external. + const externalDirective = fieldInSubgraph.appliedDirectivesOf(federationMetadata(fieldInSubgraph.schema())!.externalDirective()).pop(); + assert( + externalDirective, + () => `${fieldInSubgraph.coordinate} in ${subgraph} is not external but there is no corresponding edge (edges from ${path} = [${path.nextEdges().join(', ')}])` + ); + // but the field is external in the "subgraph-extracted-from-the-supergraph", but it might have been forced to an external + // due to being a used-overriden field, in which case we want to amend the message to avoid confusing the user. + // Note that the subgraph extraction marks such "forced external due to being overriden" by setting the "reason" to "[overridden]". + const overriddingSources = externalDirective.arguments().reason === '[overridden]' + ? findOverriddingSourcesIfOverridden(fieldInSubgraph, subgraph, path.graph.sources) + : []; + if (overriddingSources.length > 0) { + details = `field "${transition.definition.coordinate}" is not resolvable because it is overridden by ${printSubgraphNames(overriddingSources)}`; + } else { + details = `field "${transition.definition.coordinate}" is not resolvable because marked @external`; + } } else { - details = `field "${transition.definition.coordinate}" is not resolvable because marked @external`; + details = `cannot find field "${transition.definition.coordinate}"`; } - } else { - details = `cannot find field "${transition.definition.coordinate}"`; } + } else { + assert(transition.kind === 'DownCast', () => `Unhandled direct transition ${transition} of kind ${transition.kind}`); + details = `cannot find type "${transition.castedType}"`; } - } else { - assert(transition.kind === 'DownCast', () => `Unhandled direct transition ${transition} of kind ${transition.kind}`); - details = `cannot find type "${transition.castedType}"`; + return { + sourceSubgraph: subgraph, + destSubgraph: subgraph, + reason: UnadvanceableReason.NO_MATCHING_TRANSITION, + details + }; } - return new Unadvanceables([{ - sourceSubgraph: subgraph, - destSubgraph: subgraph, - reason: UnadvanceableReason.NO_MATCHING_TRANSITION, - details - }]); - } + }); + } function findOverriddingSourcesIfOverridden(