UNPKG

@apollo/query-graphs

Version:

Apollo Federation library to work with 'query graphs'

1,115 lines (1,021 loc) 84.4 kB
import { assert, MultiMap, InterfaceType, isInterfaceType, isFed1Supergraph, isObjectType, isUnionType, NamedType, ObjectType, Schema, SchemaRootKind, Type, UnionType, baseType, SelectionSet, isFederationSubgraphSchema, FieldDefinition, isCompositeType, parseFieldSetArgument, AbstractType, isAbstractType, possibleRuntimeTypes, MapWithCachedArrays, mapKeys, firstOf, FEDERATION_RESERVED_SUBGRAPH_NAME, federationMetadata, FederationMetadata, DirectiveDefinition, Directive, typenameFieldName, Field, selectionSetOfElement, SelectionSetUpdates, Supergraph, NamedSchemaElement, validateSupergraph, parseContext, } from '@apollo/federation-internals'; import { inspect } from 'util'; import { DownCast, FieldCollection, subgraphEnteringTransition, SubgraphEnteringTransition, Transition, KeyResolution, RootTypeResolution, InterfaceObjectFakeDownCast } from './transition'; import { preComputeNonTrivialFollowupEdges } from './nonTrivialEdgePrecomputing'; import { NonLocalSelectionsMetadata } from './nonLocalSelectionsEstimation'; // We use our federation reserved subgraph name to avoid risk of conflict with other subgraph names (wouldn't be a huge // deal, but safer that way). Using something short like `_` is also on purpose: it makes it stand out in debug messages // without taking space. export const FEDERATED_GRAPH_ROOT_SOURCE = FEDERATION_RESERVED_SUBGRAPH_NAME; const FEDERATED_GRAPH_ROOT_SCHEMA = new Schema(); export function federatedGraphRootTypeName(rootKind: SchemaRootKind): string { return `[${rootKind}]`; } export function isFederatedGraphRootType(type: NamedType) { return type.name.startsWith('[') && type.name.endsWith(']'); } /** * A vertex of a query graph, which points to a type (definition) in a particular graphQL schema (the `source` being * an identifier for that schema). * * @see QueryGraph */ export class Vertex { hasReachableCrossSubgraphEdges: boolean = false; // @provides works by creating duplicates of the vertex/type involved in the provides and adding the provided // edges only to those copy. This means that with @provides, you can have more than one vertex per-type-and-subgraph // in a query graph. Which is fined, but this `provideId` allows to distinguish if a vertex was created as part of // this @provides duplication or not. The value of this field has no other meaning than to be unique per-@provide, // and so all the vertex copied for a given @provides application will have the same `provideId`. Overall, this // mostly exists for debugging visualization. provideId: number | undefined; constructor( /** Index used for this vertex in the query graph it is part of. */ readonly index: number, /** The graphQL type the vertex points to. */ readonly type: NamedType, /** * An identifier of the underlying schema containing the `type` this vertex points to. * This is mainly used in "federated" query graphs, where the `source` is a subgraph name. */ readonly source : string ) {} toString(): string { const label = `${this.type}(${this.source})`; return this.provideId ? `${label}-${this.provideId}` : label; } } /** * A "root" `Vertex`, that is a vertex that one of the root of a query graph. * * @see Vertex * @see QueryGraph.roots */ export class RootVertex extends Vertex { constructor( readonly rootKind: SchemaRootKind, index: number, type: NamedType, source : string ) { super(index, type, source); } toString(): string { return super.toString() + '*'; } } function toRootVertex(vertex: Vertex, rootKind: SchemaRootKind): RootVertex { return new RootVertex(rootKind, vertex.index, vertex.type, vertex.source); } export function isRootVertex(vertex: Vertex): vertex is RootVertex { return vertex instanceof RootVertex; } export interface OverrideCondition { label: string; condition: boolean; } export function checkOverrideCondition( overrideCondition: OverrideCondition, conditionsToCheck: Map<string, boolean> ): boolean { const { label, condition } = overrideCondition; return conditionsToCheck.has(label) ? conditionsToCheck.get(label) === condition : false; } export type ContextCondition = { context: string; subgraphName: string; namedParameter: string; selection: string; typesWithContextSet: Set<string>; argType: Type, coordinate: string; } /** * An edge of a query graph. * * Query graphs are directed and an edge goes from its `head` vertex to its `tail` one. * Edges also have additional metadata: their `transition` and, optionally, `conditions`. */ export class Edge { private _conditions?: SelectionSet; public requiredContexts: ContextCondition[] = []; constructor( /** * Index used for this edge in the query graph it is part of (note that this index is "scoped" within * the head vertex, meaning that if 2 different vertices of the same query graph both have a single * out-edge, then both of those edges have index 0, and if a vertex has 3 out-edges, their index will * be 0, 1 and 2). */ public readonly index: number, /** * The vertex from which the edge starts. */ public readonly head: Vertex, /** * The vertex on which the edge ends. */ public readonly tail: Vertex, /** * Indicates what kind of edges this is and what the edges does/represents. * For instance, if the edge represents a field, the `transition` will be a `FieldCollection` transition * and will link to the definition of the field it represents. * * @see Transition */ public readonly transition: Transition, /** * Optional conditions on an edge. * * Conditions are a select of selections (in the graphQL sense) that the traversal of a query graph * needs to "collect" (traverse edges with transitions corresponding to those selections) in order * to be able to collect that edge. * * Conditions are primarily used for edges corresponding to @key, in which case they correspond * to the fields composing the key. In other words, for a key edges, conditions basically represents * the fact that you need the key to be able to use a key edge. * * Outside of keys, @requires also rely on conditions. */ conditions?: SelectionSet, /** * Edges can require that an override condition (provided during query * planning) be met in order to be taken. This is used for progressive * @override, where (at least) 2 subgraphs can resolve the same field, but * one of them has an @override with a label. If the override condition * matches the query plan parameters, this edge can be taken. */ public overrideCondition?: OverrideCondition, /** * Potentially multiple context conditions. When @fromContext is used on a argument definition, the edge representing * the argument's field needs to reflect that the condition must be satisfied in order for the edge to be taken */ requiredContexts?: ContextCondition[], ) { this._conditions = conditions; if (requiredContexts) { this.requiredContexts = [...requiredContexts]; } } get conditions(): SelectionSet | undefined { return this._conditions; } isEdgeForField(name: string): boolean { return this.transition.kind === 'FieldCollection' && this.transition.definition.name === name; } matchesSupergraphTransition(otherTransition: Transition): boolean { assert(otherTransition.collectOperationElements, () => `Supergraphs shouldn't have transition that don't collect elements; got ${otherTransition}"`); const transition = this.transition; switch (transition.kind) { case 'FieldCollection': return otherTransition.kind === 'FieldCollection' && transition.definition.name === otherTransition.definition.name; case 'DownCast': return otherTransition.kind === 'DownCast' && transition.castedType.name === otherTransition.castedType.name; case 'InterfaceObjectFakeDownCast': return otherTransition.kind === 'DownCast' && transition.castedTypeName === otherTransition.castedType.name; default: return false; } } changesSubgraph(): boolean { return this.head.source !== this.tail.source; } label(): string { if (this.transition instanceof SubgraphEnteringTransition && !this._conditions) { return ""; } let conditionsString = (this._conditions ?? '').toString(); if (this.overrideCondition) { if (conditionsString.length) conditionsString += ', '; conditionsString += `${this.overrideCondition.label} = ${this.overrideCondition.condition}`; } // we had at least some condition, add the turnstile and spacing if (conditionsString.length) conditionsString += ' ⊢ '; return conditionsString + this.transition.toString(); } withNewHead(newHead: Vertex): Edge { return new Edge( this.index, newHead, this.tail, this.transition, this._conditions, this.overrideCondition, this.requiredContexts, ); } addToConditions(newConditions: SelectionSet) { this._conditions = this._conditions ? new SelectionSetUpdates().add(this._conditions).add(newConditions).toSelectionSet(this._conditions.parentType) : newConditions; } addToContextConditions(contextConditions: ContextCondition[]) { this.requiredContexts.push(...contextConditions); } isKeyOrRootTypeEdgeToSelf(): boolean { return this.head === this.tail && (this.transition.kind === 'KeyResolution' || this.transition.kind === 'RootTypeResolution'); } satisfiesOverrideConditions(conditionsToCheck: Map<string, boolean>) { if (!this.overrideCondition) return true; return checkOverrideCondition( this.overrideCondition, conditionsToCheck, ); } toString(): string { return `${this.head} -> ${this.tail} (${this.label()})`; } } /** * An immutable directed graph data structure (built of vertices and edges) that is layered over one or multiple * graphQL schema, that aims to facilitate reasoning about queries expressed on the underlying schema. * * On top of its set of vertices and edges, a query graph exposes: * - its set of "sources": pointers to the graphQL schema on which the query graph was built. * - a set of distinguished vertices called "root" vertices. A query graph has at most one root * vertex per `SchemaRootKind`, and those vertices are usually entry points for traversals of * a query graph. * * In practice, the code builds 2 "kind" of query graphs: * 1. a supergraph query graph, which is built on top of a supergraph API schema (@see buildGraph()), * and is built to reason about user queries (made on the supergraph API). This supergraph query * graph is used to validate composition. * 2. a "federated" query graph, which is a single graph built on top of a) a number of subgraph * API schema and b) the additional federation directives on those subgraphs (@see buildFederatedQueryGraph()). * This query graph is used both for validating composition and for query planning. * * Note that this class handles both cases, but a "supergraph query graph" will, by construction, have * a few more invariants than a "federated query graph". Including (but not necessarily limited to): * - edges in a super graph query graph will never have `conditions` or 'key' edges (edges with a `KeyResolution` edges). * - supergraph query graphs will have a single value in `sources` (the supergraph schema). * * Also note that as query graphs are about reasoning about queries over schema, they only contain vertices * that points to "reachable" types (reachable from any kind of operations). */ export class QueryGraph { /** * Given an edge, returns the possible edges that can follow it "productively", that is without creating * a trivially inefficient path. * * More precisely, `nonTrivialFollowupEdges(e)` is equivalent calling `outEdges(e.tail)` and filtering * the edges that "never make sense" after `e`, which mainly amounts to avoiding chaining key edges * when we know there is guaranteed to be a better option. As an example, suppose we have 3 subgraphs * A, B and C which all defined a `@key(fields: "id")` on some entity type `T`. Then it is never * interesting to take that key edge from B -> C after A -> B because if we're in A and want to get * to C, we can always do A -> C (of course, this is only true because it's the "same" key). * * See `preComputeNonTrivialFollowupEdges` for more details on which exact edges are filtered. * * Lastly, note that the main reason for exposing this method is that its result is pre-computed. * Which in turn is done for performance reasons: having the same key defined in multiple subgraphs * is _the_ most common pattern, and while our later algorithms (composition validation and query * planning) would know to not select those trivially inefficient "detour", they might have to redo * those checks many times and pre-computing once it is significantly faster (and pretty easy). * Fwiw, when originally introduced, this optimization lowered composition validation on a big * composition (100+ subgraphs) from ~4 "minutes" to ~10 seconds. */ readonly nonTrivialFollowupEdges: (edge: Edge) => readonly Edge[]; /** * To speed up the estimation of counting non-local selections, we * precompute specific metadata. We only computed this for federated query * graphs used during query planning. */ readonly nonLocalSelectionsMetadata: NonLocalSelectionsMetadata | null; /** * Creates a new query graph. * * This isn't meant to be be called directly outside of `GraphBuilder.build`. */ constructor( /** A name to identify the graph. Mostly for pretty-printing/debugging purpose. */ readonly name: string, /** The vertices of the query graph. The index of each vertex in the array will be the value of its `Vertex.index` value. */ readonly vertices: Vertex[], /** * For each vertex, the edges that originate from that array. This array has the same length as `vertices` and `_outEdges[i]` * is an array of all the edges starting at vertices[i]. */ private readonly _outEdges: Edge[][], /** * A map that associate type names of the underlying schema on which this query graph was built to each of the vertex * (vertex index) that points to a type of that name. Note that in a "supergraph query graph", each type name will only * map to a single vertex, */ private readonly typesToVertices: MultiMap<string, number>, /** The set of distinguished root vertices (pointers to vertices in `vertices`), keyed by their kind. */ private readonly rootVertices: MapWithCachedArrays<SchemaRootKind, RootVertex>, /** * The sources on which the query graph was built, that is a set (which can be of size 1) of graphQL schema keyed by * the name identifying them. Note that the `source` string in the `Vertex` of a query graph is guaranteed to be * valid key in this map. */ readonly sources: ReadonlyMap<string, Schema>, readonly subgraphToArgs: Map<string, string[]>, readonly subgraphToArgIndices: Map<string, Map<string, string>>, readonly schema: Schema, isFederatedAndForQueryPlanning?: boolean, ) { this.nonTrivialFollowupEdges = preComputeNonTrivialFollowupEdges(this); this.nonLocalSelectionsMetadata = isFederatedAndForQueryPlanning ? new NonLocalSelectionsMetadata(this) : null; } /** The number of vertices in this query graph. */ verticesCount(): number { return this.vertices.length; } /** The number of edges in this query graph. */ edgesCount(): number { // We could count edges as we add them and pass it to the ctor. For now though, it's not meant to be // on a hot path, so recomputing is probably fine. return this._outEdges.reduce((acc, v) => acc + v.length, 0); } /** * The set of `SchemaRootKind` for which this query graph has a root vertex (for * which `root(SchemaRootKind)` will _not_ return `undefined`). */ rootKinds(): readonly SchemaRootKind[] { return this.rootVertices.keys(); } /** * The set of root vertices in this query graph. */ roots(): readonly RootVertex[] { return this.rootVertices.values(); } /** * The root vertex corresponding to the provided root kind, if this query graph has one. * * @param kind - the root kind for which to get the root vertex. * @returns the root vertex for `kind` if this query graph has one. */ root(kind: SchemaRootKind): RootVertex | undefined { return this.rootVertices.get(kind); } /** * The edges out of the provided vertex. * * @param vertex - the vertex for which to return out edges. This method _assumes_ that * the provided vertex is a vertex of this query graph (and its behavior is undefined * if it isn't). * @param includeKeyAndRootTypeEdgesToSelf - whether key/root type edges that stay on the same * vertex should be included. This default to `false` are those are rarely useful. More * precisely, the only current use of them is for @defer where they may be needed to re-enter * the current subgraph in a deferred section. * @returns the list of all the edges out of this vertex. */ outEdges(vertex: Vertex, includeKeyAndRootTypeEdgesToSelf: boolean = false): readonly Edge[] { const allEdges = this._outEdges[vertex.index]; return includeKeyAndRootTypeEdgesToSelf ? allEdges : allEdges.filter((e) => !e.isKeyOrRootTypeEdgeToSelf()) } /** * The number of edges out of the provided vertex. * * This is a shortcut for `this.outEdges(vertex, true).length`, and the reason it considers * edge-to-self by default while `this.outEdges` doesn't is that this method is generally * used to size other arrays indexed by edges index, and so we want to consider all edges * in general. */ outEdgesCount(vertex: Vertex): number { return this._outEdges[vertex.index].length; } /** * The edge out of the provided vertex at the provided (edge) index. * * @param vertex - the vertex for which to return the out edge. This method _assumes_ that * the provided vertex is a vertex of this query graph (and its behavior is undefined * if it isn't). * @param edgeIndex - the edge index (relative to `vertex`, see `Edge.index`) to retrieve. * @returns the `edgeIndex`^th edge out of `vertex`, if such edges exists. */ outEdge(vertex: Vertex, edgeIndex: number): Edge | undefined { return this._outEdges[vertex.index][edgeIndex]; } allVertices(): Iterable<Vertex> { return this.vertices; } *allEdges(): Iterable<Edge> { for (const vertexOutEdges of this._outEdges) { for (const outEdge of vertexOutEdges) { yield outEdge; } } } /** * Whether the provided vertex is a terminal one (has no out edges). * * @param vertex - the vertex to check. * @returns whether the provided vertex is terminal. */ isTerminal(vertex: Vertex): boolean { return this.outEdgesCount(vertex) === 0; } /** * The set of vertices whose associated type (see `Vertex.type`) is of type `typeName`. */ verticesForType(typeName: string): Vertex[] { const indexes = this.typesToVertices.get(typeName); return indexes == undefined ? [] : indexes.map(i => this.vertices[i]); } } /** * A utility class that allows to associate state to the vertices and/or edges of a query graph. * * @param VertexState - the type of the state associated to vertices. * @param EdgeState - the type of the state associated to edges. Defaults to `undefined`, which * means that state is only associated to vertices. */ export class QueryGraphState<VertexState, EdgeState = undefined> { // Store some "user" state for each vertex (accessed by index) private readonly verticesStates: Map<number, VertexState> = new Map(); private readonly adjacenciesStates: Map<number, Map<number, EdgeState>> = new Map(); /** * Associates the provided state to the provided vertex. * * @param vertex - the vertex to which state should be associated. This method _assumes_ * that the provided vertex is a vertex of the query graph against which this * `QueryGraphState` was created (and its behavior is undefined if it isn't). * @param state - the state/value to associate to `vertex`. */ setVertexState(vertex: Vertex, state: VertexState) { this.verticesStates.set(vertex.index, state); } /** * Removes the state associated to the provided vertex (if any is). * * @param vertex - the vertex for which state should be removed. This method _assumes_ * that the provided vertex is a vertex of the query graph against which this * `QueryGraphState` was created (and its behavior is undefined if it isn't). */ removeVertexState(vertex: Vertex) { this.verticesStates.delete(vertex.index); } /** * Retrieves the state associated to the provided vertex (if any is). * * @param vertex - the vertex for which state should be retrieved. This method _assumes_ * that the provided vertex is a vertex of the query graph against which this * `QueryGraphState` was created (and its behavior is undefined if it isn't). * @return the state associated to `vertex`, if any. */ getVertexState(vertex: Vertex): VertexState | undefined { return this.verticesStates.get(vertex.index); } /** * Associates the provided state to the provided edge. * * @param edge - the edge to which state should be associated. This method _assumes_ * that the provided edge is an edge of the query graph against which this * `QueryGraphState` was created (and its behavior is undefined if it isn't). * @param state - the state/value to associate to `edge`. */ setEdgeState(edge: Edge, state: EdgeState) { let edgeMap = this.adjacenciesStates.get(edge.head.index) if (!edgeMap) { edgeMap = new Map(); this.adjacenciesStates.set(edge.head.index, edgeMap); } edgeMap.set(edge.index, state); } /** * Removes the state associated to the provided edge (if any is). * * @param edge - the edge for which state should be removed. This method _assumes_ * that the provided edge is an edge of the query graph against which this * `QueryGraphState` was created (and its behavior is undefined if it isn't). */ removeEdgeState(edge: Edge) { const edgeMap = this.adjacenciesStates.get(edge.head.index); if (edgeMap) { edgeMap.delete(edge.index); if (edgeMap.size === 0) { this.adjacenciesStates.delete(edge.head.index); } } } /** * Retrieves the state associated to the provided edge (if any is). * * @param edge - the edge for which state should be retrieved. This method _assumes_ * that the provided vertex is an edge of the query graph against which this * `QueryGraphState` was created (and its behavior is undefined if it isn't). * @return the state associated to `edge`, if any. */ getEdgeState(edge: Edge): EdgeState | undefined { return this.adjacenciesStates.get(edge.head.index)?.get(edge.index); } toDebugString( vertexMapper: (s: VertexState) => string, edgeMapper: (e: EdgeState) => string ): string { const vs = Array.from(this.verticesStates.entries()).sort(([a], [b]) => a - b).map(([idx, state]) => ` ${idx}: ${!state ? "<null>" : vertexMapper(state)}` ).join("\n"); const es = Array.from(this.adjacenciesStates.entries()).sort(([a], [b]) => a - b).map(([vIdx, adj]) => Array.from(adj.entries()).sort(([a], [b]) => a - b).map(([eIdx, state]) => ` ${vIdx}[${eIdx}]: ${!state ? "<null>" : edgeMapper(state)}` ).join("\n") ).join("\n"); return `vertices = {${vs}\n}, edges = {${es}\n}`; } } /** * Builds the query graph corresponding to the provided schema. * * Note that this method and mainly exported for the sake of testing but should rarely, if * ever, be used otherwise. Instead use either `buildSupergraphAPIQueryGraph` or * `buildFederatedQueryGraph` which are more explicit. * * @param name - the name to use for the created graph and as "source" name for the schema. * @param schema - the schema for which to build the query graph. * @param overrideLabelsByCoordinate - A Map of coordinate -> override label to apply to the query graph. * Additional "virtual" edges will be created for progressively overridden fields in order to ensure that * all possibilities are considered during query planning. * @returns the query graph corresponding to `schema` "API" (in the sense that no federation * directives are taken into account by this method in the building of the query graph). */ export function buildQueryGraph(name: string, schema: Schema, overrideLabelsByCoordinate?: Map<string, string>): QueryGraph { return buildGraphInternal(name, schema, false, undefined, overrideLabelsByCoordinate); } function buildGraphInternal( name: string, schema: Schema, addAdditionalAbstractTypeEdges: boolean, supergraphSchema?: Schema, overrideLabelsByCoordinate?: Map<string, string>, ): QueryGraph { const builder = new GraphBuilderFromSchema( name, schema, supergraphSchema ? { apiSchema: supergraphSchema.toAPISchema(), isFed1: isFed1Supergraph(supergraphSchema) } : undefined, overrideLabelsByCoordinate, ); for (const rootType of schema.schemaDefinition.roots()) { builder.addRecursivelyFromRoot(rootType.rootKind, rootType.type); } if (builder.isFederatedSubgraph) { builder.addInterfaceEntityEdges(); } if (addAdditionalAbstractTypeEdges) { builder.addAdditionalAbstractTypeEdges(); } return builder.build(); } /** * Builds a "supergraph API" query graph based on the provided supergraph schema. * * A "supergraph API" query graph is one that is used to reason about queries against said * supergraph API, but @see QueryGraph for more details. * * @param supergraph - the schema of the supergraph for which to build the query graph. * The provided schema should generally be a "supergraph" as generated by composition merging. * Note however that the query graph built by this method is only based on the supergraph * API and doesn't rely on the join spec directives, so it is valid to also directly * pass a schema that directly corresponds to the supergraph API. * @returns the built query graph. */ export function buildSupergraphAPIQueryGraph(supergraph: Supergraph): QueryGraph { const apiSchema = supergraph.apiSchema(); const overrideLabelsByCoordinate = new Map<string, string>(); const joinFieldApplications = validateSupergraph(supergraph.schema)[1] .fieldDirective(supergraph.schema).applications(); for (const application of joinFieldApplications) { const overrideLabel = application.arguments().overrideLabel; if (overrideLabel) { overrideLabelsByCoordinate.set( (application.parent as FieldDefinition<any>).coordinate, overrideLabel ); } } return buildQueryGraph("supergraph", apiSchema, overrideLabelsByCoordinate); } /** * Builds a "federated" query graph based on the provided supergraph schema. * * A "federated" query graph is one that is used to reason about queries made by a * gateway/router against a set of federated subgraph services. * * @see QueryGraph * * @param supergraph - the supergraph for which to build the query graph. * @param forQueryPlanning - whether the build query graph is built for query planning (if * so, it will include some additional edges that don't impact validation but allow * to generate more efficient query plans). * @returns the built federated query graph. */ export function buildFederatedQueryGraph(supergraph: Supergraph, forQueryPlanning: boolean): QueryGraph { const subgraphs = supergraph.subgraphs(); const graphs = []; for (const subgraph of subgraphs) { graphs.push(buildGraphInternal(subgraph.name, subgraph.schema, forQueryPlanning, supergraph.schema)); } return federateSubgraphs(supergraph.schema, graphs, forQueryPlanning); } function federatedProperties(subgraphs: QueryGraph[]) : [number, Set<SchemaRootKind>, Schema[]] { let vertices = 0; const rootKinds = new Set<SchemaRootKind>(); const schemas: Schema[] = []; for (const subgraph of subgraphs) { vertices += subgraph.verticesCount(); subgraph.rootKinds().forEach(k => rootKinds.add(k)); assert(subgraph.sources.size === 1, () => `Subgraphs should only have one sources, got ${subgraph.sources.size} ([${mapKeys(subgraph.sources).join(', ')}])`); schemas.push(firstOf(subgraph.sources.values())!); } return [vertices + rootKinds.size, rootKinds, schemas]; } function resolvableKeyApplications( keyDirective: DirectiveDefinition<{fields: any, resolvable?: boolean}>, type: NamedType ): Directive<NamedType, {fields: any, resolvable?: boolean}>[] { const applications: Directive<NamedType, {fields: any, resolvable?: boolean}>[] = type.appliedDirectivesOf(keyDirective); return applications.filter((application) => application.arguments().resolvable ?? true); } function federateSubgraphs( supergraph: Schema, subgraphs: QueryGraph[], forQueryPlanning: boolean, ): QueryGraph { const [verticesCount, rootKinds, schemas] = federatedProperties(subgraphs); const builder = new GraphBuilder(supergraph, verticesCount); rootKinds.forEach(k => builder.createRootVertex( k, new ObjectType(federatedGraphRootTypeName(k)), FEDERATED_GRAPH_ROOT_SOURCE, FEDERATED_GRAPH_ROOT_SCHEMA )); // We first add all the vertices and edges from the subgraphs const copyPointers: SubgraphCopyPointer[] = new Array(subgraphs.length); for (const [i, subgraph] of subgraphs.entries()) { copyPointers[i] = builder.copyGraph(subgraph); } // We then add the edges from supergraph roots to the subgraph ones. // Also, for each root kind, we also add edges from the corresponding root type of each subgraph to the root type of other subgraphs // (and for @defer, like for @key, we also add self-link looping on the current subgraph). // This essentially encode the fact that if a field return a root type, we can always query any subgraph from that point. for (const [i, subgraph] of subgraphs.entries()) { const copyPointer = copyPointers[i]; for (const rootKind of subgraph.rootKinds()) { const rootVertex = copyPointer.copiedVertex(subgraph.root(rootKind)!); builder.addEdge(builder.root(rootKind)!, rootVertex, subgraphEnteringTransition) for (const [j, otherSubgraph] of subgraphs.entries()) { const otherRootVertex = otherSubgraph.root(rootKind); if (otherRootVertex) { const otherCopyPointer = copyPointers[j]; builder.addEdge(rootVertex, otherCopyPointer.copiedVertex(otherRootVertex), new RootTypeResolution(rootKind)); } } } } // Then we add/update edges for @key and @requires. We do @provides in a second step because its handling requires // copying vertex and their edges, and it's easier to reason about this if we know all keys have already been created. for (const [i, subgraph] of subgraphs.entries()) { const subgraphSchema = schemas[i]; const subgraphMetadata = federationMetadata(subgraphSchema); assert(subgraphMetadata, `Subgraph ${i} is not a valid federation subgraph`); const keyDirective = subgraphMetadata.keyDirective(); const requireDirective = subgraphMetadata.requiresDirective(); simpleTraversal( subgraph, v => { const type = v.type; for (const keyApplication of resolvableKeyApplications(keyDirective, type)) { // The @key directive creates an edge from every subgraphs (having that type) // to the current subgraph. In other words, the fact this subgraph has a @key means // that the service of the current subgraph can be queried for the entity (through // _entities) as long as "the other side" can provide the proper field values. // Note that we only require that "the other side" can gather the key fields (through // the path conditions; note that it's possible those conditions are never satisfiable), // but don't care that it defines the same key, because it's not a technical // requirement (and while we probably don't want to allow in general a type to be an // entity in some subgraphs but not other, this is not the place to impose that // restriction, and this may be useful at least temporarily to allow convert a type to // an entity). assert(isInterfaceType(type) || isObjectType(type), () => `Invalid "@key" application on non Object || Interface type "${type}"`); const isInterfaceObject = subgraphMetadata.isInterfaceObjectType(type); const conditions = parseFieldSetArgument({ parentType: type, directive: keyApplication, normalize: true }); // We'll look at adding edges from "other subgraphs" to the current type. So the tail of all the edges // we'll build in this branch is always going to be the same. const tail = copyPointers[i].copiedVertex(v); // Note that each subgraph has a key edge to itself (when i === j below). We usually ignore // this edges, but they exists for the special case of @defer, where we technically may have // to take such "edge-to-self" as a mean to "re-enter" a subgraph for a deferred section. for (const [j, otherSubgraph] of subgraphs.entries()) { const otherVertices = otherSubgraph.verticesForType(type.name); if (otherVertices.length > 0) { // Note that later, when we've handled @provides, this might not be true anymore as @provides may create copy of a // certain type. But for now, it's true. assert( otherVertices.length == 1, () => `Subgraph ${j} should have a single vertex for type ${type.name} but got ${otherVertices.length}: ${inspect(otherVertices)}`); const otherVertex = otherVertices[0]; // The edge goes from the otherSubgraph to the current one. const head = copyPointers[j].copiedVertex(otherVertex); const tail = copyPointers[i].copiedVertex(v); builder.addEdge(head, tail, new KeyResolution(), conditions); } // Additionally, if the key is on an @interfaceObject and this "other" subgraph has some of the implementations // of the corresponding interface, then we need an edge from each of those implementations (to the @interfaceObject). // This is used when an entity of specific implementation is queried first, but then some of the // requested fields are only provided by that @interfaceObject. if (isInterfaceObject) { const typeInSupergraph = supergraph.type(type.name); assert(typeInSupergraph && isInterfaceType(typeInSupergraph), () => `Type ${type} is an interfaceObject in subgraph ${i}; should be an interface in the supergraph`); for (const implemTypeInSupergraph of typeInSupergraph.possibleRuntimeTypes()) { // That implementation type may or may not exists in "otherSubgraph". If it doesn't, we just have nothing to // do for that particular impelmentation. If it does, we'll add the proper edge, but note that we're guaranteed // to have at most one vertex for the same reason than mentioned above (only the handling @provides will make it // so that there can be more than one vertex per type). const implemVertice = otherSubgraph.verticesForType(implemTypeInSupergraph.name)[0]; if (!implemVertice) { continue; } const implemHead = copyPointers[j].copiedVertex(implemVertice); // The key goes from the implementation type to the @interfaceObject one, so the conditions // will be "fetched" on the implementation type, but `conditions` has been parsed on the // interface type, so it will use fields from the interface, not the implementation type. // So we re-parse the condition using the implementation type: this could fail, but in // that case it just mean that key is not usable. const implemType = implemVertice.type; assert(isCompositeType(implemType), () => `${implemType} should be composite since it implements ${typeInSupergraph} in the supergraph`); try { const implConditions = parseFieldSetArgument({ parentType: implemType, directive: keyApplication, validate: false, normalize: true }); builder.addEdge(implemHead, tail, new KeyResolution(), implConditions); } catch (e) { // Ignored on purpose: it just means the key is not usable on this subgraph. } } } } } }, e => { // Handling @requires if (e.transition.kind === 'FieldCollection') { const type = e.head.type; const field = e.transition.definition; assert(isCompositeType(type), () => `Non composite type "${type}" should not have field collection edge ${e}`); for (const requiresApplication of field.appliedDirectivesOf(requireDirective)) { const conditions = parseFieldSetArgument({ parentType: type, directive: requiresApplication, normalize: true }); const head = copyPointers[i].copiedVertex(e.head); // We rely on the fact that the edge indexes will be the same in the copied builder. But there is no real reason for // this to not be the case at this point so... const copiedEdge = builder.edge(head, e.index); copiedEdge.addToConditions(conditions); } } return true; // Always traverse edges } ); } /** * Handling progressive overrides here. For each progressive @override * application (with a label), we want to update the edges to the overridden * field within the "to" and "from" subgraphs with their respective override * condition (the label and a T/F value). The "from" subgraph will have an * override condition of `false`, whereas the "to" subgraph will have an * override condition of `true`. */ const subgraphsByName = new Map(subgraphs.map((s) => [s.name, s])); for (const [i, toSubgraph] of subgraphs.entries()) { const subgraphSchema = schemas[i]; const subgraphMetadata = federationMetadata(subgraphSchema); assert(subgraphMetadata, `Subgraph ${i} is not a valid federation subgraph`); for (const application of subgraphMetadata.overrideDirective().applications()) { const { from, label } = application.arguments(); if (!label) continue; const fromSubgraph = subgraphsByName.get(from); assert(fromSubgraph, () => `Subgraph ${from} not found`); function updateEdgeWithOverrideCondition(subgraph: QueryGraph, label: string, condition: boolean) { const field = application.parent; assert(field instanceof NamedSchemaElement, () => `@override should have been on a field, got ${field}`); const typeName = field.parent.name; const [vertex, ...unexpectedAdditionalVertices] = subgraph.verticesForType(typeName); assert(vertex && unexpectedAdditionalVertices.length === 0, () => `Subgraph ${subgraph.name} should have exactly one vertex for type ${typeName}`); const subgraphEdges = subgraph.outEdges(vertex); for (const edge of subgraphEdges) { if ( edge.transition.kind === "FieldCollection" && edge.transition.definition.name === field.name ) { const head = copyPointers[subgraphs.indexOf(subgraph)].copiedVertex(vertex); const copiedEdge = builder.edge(head, edge.index); copiedEdge.overrideCondition = { label, condition, }; } } } updateEdgeWithOverrideCondition(toSubgraph, label, true); updateEdgeWithOverrideCondition(fromSubgraph, label, false); } } /** * Now we'll handle instances of @fromContext. For each argument with @fromContext, I want to add its corresponding * context conditions to the edge corresponding to the argument's field */ const subgraphToArgs: Map<string, string[]> = new Map(); const subgraphToArgIndices: Map<string, Map<string, string>> = new Map(); for (const [i, subgraph] of subgraphs.entries()) { const subgraphSchema = schemas[i]; const subgraphMetadata = federationMetadata(subgraphSchema); assert(subgraphMetadata, `Subgraph ${i} is not a valid federation subgraph`); const contextNameToTypes: Map<string, Set<string>> = new Map(); for (const application of subgraphMetadata.contextDirective().applications()) { const { name: context } = application.arguments(); if (contextNameToTypes.has(context)) { contextNameToTypes.get(context)!.add(application.parent.name); } else { contextNameToTypes.set(context, new Set([application.parent.name])); } } const coordinateMap: Map<string, ContextCondition[]> = new Map(); for (const application of subgraphMetadata.fromContextDirective().applications()) { const { field } = application.arguments(); const { context, selection } = parseContext(field); assert(context, () => `FieldValue has invalid format. Context not found ${field}`); assert(selection, () => `FieldValue has invalid format. Selection not found ${field}`); const namedParameter = application.parent.name; const argCoordinate = application.parent.coordinate; const args = subgraphToArgs.get(subgraph.name) ?? []; args.push(argCoordinate); subgraphToArgs.set(subgraph.name, args); const fieldCoordinate = application.parent.parent.coordinate; const typesWithContextSet = contextNameToTypes.get(context); assert(typesWithContextSet, () => `Context ${context} is never set in subgraph`); const z = coordinateMap.get(fieldCoordinate); if (z) { z.push({ namedParameter, coordinate: argCoordinate, context, selection, typesWithContextSet, subgraphName: subgraph.name, argType: application.parent.type }); } else { coordinateMap.set(fieldCoordinate, [{ namedParameter, coordinate: argCoordinate, context, selection, typesWithContextSet, subgraphName: subgraph.name, argType: application.parent.type }]); } } simpleTraversal( subgraph, _v => { return undefined; }, e => { if (e.head.type.kind === 'ObjectType' && e.transition.kind === 'FieldCollection') { const coordinate = `${e.head.type.name}.${e.transition.definition.name}`; const requiredContexts = coordinateMap.get(coordinate); if (requiredContexts) { const headInSupergraph = copyPointers[i].copiedVertex(e.head); assert(headInSupergraph, () => `Vertex for type ${e.head.type.name} not found in supergraph`); const edgeInSupergraph = builder.edge(headInSupergraph, e.index); edgeInSupergraph.addToContextConditions(requiredContexts); } } return true; } ); } // add contextual argument maps to builder for (const [i, subgraph] of subgraphs.entries()) { const subgraphName = subgraph.name; const args = subgraphToArgs.get(subgraph.name); if (args) { args.sort(); const argToIndex = new Map(); for (let idx=0; idx < args.length; idx++) { argToIndex.set(args[idx], `contextualArgument_${i+1}_${idx}`); } subgraphToArgIndices.set(subgraphName, argToIndex); } } builder.setContextMaps(subgraphToArgs, subgraphToArgIndices); // Now we handle @provides let provideId = 0; for (const [i, subgraph] of subgraphs.entries()) { const subgraphSchema = schemas[i]; const subgraphMetadata = federationMetadata(subgraphSchema); assert(subgraphMetadata, `Subgraph ${i} is not a valid federation subgraph`); const providesDirective = subgraphMetadata.providesDirective(); simpleTraversal( subgraph, _ => undefined, e => { // Handling @provides if (e.transition.kind === 'FieldCollection') { const type = e.head.type; const field = e.transition.definition; assert(isCompositeType(type), () => `Non composite type "${type}" should not have field collection edge ${e}`); for (const providesApplication of field.appliedDirectivesOf(providesDirective)) { ++provideId; const fieldType = baseType(field.type!); assert(isCompositeType(fieldType), () => `Invalid @provide on field "${field}" whose type "${fieldType}" is not a composite type`) const provided = parseFieldSetArgument({ parentType: fieldType, directive: providesApplication }); const head = copyPointers[i].copiedVertex(e.head); const tail = copyPointers[i].copiedVertex(e.tail); // We rely on the fact that the edge indexes will be the same in the copied builder. But there is no real reason for // this to not be the case at this point so... const copiedEdge = builder.edge(head, e.index); // We make a copy of the `fieldType` vertex (with all the same edges), and we change this particular edge to point to the // new copy. From that, we can add all the provides edges to the copy. const copiedTail = builder.makeCopy(tail, provideId); builder.updateEdgeTail(copiedEdge, copiedTail); addProvidesEdges(subgraphSchema, builder, copiedTail, provided, provideId); } } return true; // Always traverse edges } ); } // We now need to finish handling @interfaceObject types. More precisely, there is cases where only a/some implementation(s) // of a interface are queried, and that could apply to an interface that is an @interfaceObject in some sugraph. Consider // the following example: // ```graphql // type Query { // getIs: [I] // } // // type I @key(fields: "id") @interfaceObject { // id: ID! // x: Int // } // ``` // where we suppose that `I` has some implementations say, `A`, `B` and `C`, in some other subgraph. // Now, consider query: // ```graphql // { // getIs { // ... on B { // x // } // } // } // ``` // So here, we query `x` which the subgraph provides, but we only do so for one of the impelementation. // So in that case, we essentially need to figure out the `__typename` first (of more precisely, we need // to know the real __typename "eventually"; we could theoretically query `x` first, and then get the __typename // to know if we should keep the result or discard it, and that could be more efficient in certain case, // but as we don't know both 1) if `x` is expansive to resolve and 2) what the ratio of results from `getIs` // will be `B` versus some other implementation, it is "safer" to get the __typename first and only resolve `x` // when we need to). // // Long story short, to solve this, we create edges from @interfaceObject types to themselves for every implementation // types of the interface: those edges will be taken when we try to take a `... on B` condition, and those edge // have __typename as a condition, forcing to find __typename in another subgraph first. for (const [i, subgraph] of subgraphs.entries()) { const subgraphSchema = schemas[i]; const subgraphMetadata = federationMetadata(subgraphSchema); assert(subgraphMetadata, `Subgraph ${i} is not a valid federation subgraph`); const interfaceObjectDirective = subgraphMetadata.interfaceObjectDirective(); for (const application of interfaceObjectDirective.applications()) { const type = application.parent; assert(isObjectType(type), '@interfaceObject should have been on an object type'); const vertex = copyPointers[i].copiedVertex(subgraph.verticesForType(type.name)[0]); const supergraphItf = supergraph.type(type.name); assert(supergraphItf && isInterfaceType(supergraphItf), () => `${type} has @interfaceObject in subgraph but has kind ${supergraphItf?.kind} in supergraph`) const condition = selectionSetOfElement(new Field(type.typenameField()!)); for (const implementation of supergraphItf.possibleRuntimeTypes()) { builder.addEdge(vertex, vertex, new InterfaceObjectFakeDownCast(type, implementation.name), condition); } } } return builder.build(FEDERATED_GRAPH_ROOT_SOURCE, forQueryPlanning); } function addProvidesEdges(schema: Schema, builder: GraphBuilder, from: Vertex, provided: SelectionSet, provideId: number) { const stack: [Vertex, SelectionSet][] = [[from, provided]]; const source = from.source; while (stack.length > 0) { const [v, selectionSet] = stack.pop()!; // We reverse the selections to cancel the reversing that the stack does. for (const selection of selectionSet.selectionsInReverseOrder()) { const element = selection.element; if (element.kind == 'Field') { const fieldDef = element.definition; const existingEdge = builder.edg