@apollo/query-graphs
Version:
Apollo Federation library to work with 'query graphs'
1,172 lines (1,134 loc) • 44.8 kB
text/typescript
import {
assert,
assertUnreachable,
federationMetadata,
isCompositeType,
isObjectType,
OperationElement,
possibleRuntimeTypes,
Schema,
Selection,
SelectionSet,
typenameFieldName,
} from '@apollo/federation-internals';
import {
checkOverrideCondition,
FEDERATED_GRAPH_ROOT_SOURCE,
OverrideCondition,
QueryGraph,
Vertex,
} from './querygraph';
import { SimultaneousPathsWithLazyIndirectPaths } from './graphPath';
/**
* Indirect option metadata for the complete digraph for type T. See
* {@link NonLocalSelectionsMetadata} for more information about how we group
* indirect options into complete digraphs.
*/
interface IndirectOptionsMetadata {
/**
* The members of the complete digraph for type T.
*/
sameTypeOptions: Set<Vertex>;
/**
* Any interface object types I that are reachable for at least one vertex in
* the complete digraph for type T.
*/
interfaceObjectOptions: Set<string>;
}
interface FieldTail {
/**
* The tail vertex of this field edge.
*/
tail: Vertex;
/**
* The override condition of the field, if it has one.
*/
overrideCondition?: OverrideCondition;
}
/**
* Downcasts from normal non-interface-object types, which have regular
* downcasts to their object type vertices.
*/
interface NonInterfaceObjectDowncasts {
kind: 'NonInterfaceObject';
downcasts: Map<string, Vertex>;
};
/**
* "Fake" downcasts from interface object types to object types that don't
* really exist in the subgraph schema (and hence have no vertex).
*/
interface InterfaceObjectDowncasts {
kind: 'InterfaceObject';
downcasts: Set<string>;
}
/**
* Downcasts edges to the possible runtime object types of a composite type.
*/
type ObjectTypeDowncasts =
| NonInterfaceObjectDowncasts
| InterfaceObjectDowncasts;
export class NonLocalSelectionsMetadata {
static readonly MAX_NON_LOCAL_SELECTIONS = 100_000;
/**
* When a (resolvable) @key exists on a type T in a subgraph, a key resolution
* edge is created from every subgraph's type T to that subgraph's type T.
* This similarly holds for root type resolution edges. This means that the
* vertices of type T with such a @key (or are operation root types) form a
* complete digraph in the query graph. These indirect options effectively
* occur as a group in our estimation process, so we track group members here
* per type name, and precompute units of work relative to these groups.
*
* Interface object types I in a subgraph will only sometimes create a key
* resolution edge between an implementing type T in a subgraph and that
* subgraph's type I. This means the vertices of the complete digraph for I
* are indirect options for such vertices of type T. We track any such types I
* that are reachable for at least one vertex in the complete digraph for type
* T here as well.
*/
private readonly typesToIndirectOptions =
new Map<string, IndirectOptionsMetadata>();
/**
* For vertices of a type T that aren't in their complete digraph (due to not
* having a @key), these remaining vertices will have the complete digraph of
* T (and any interface object complete digraphs) as indirect options, but
* these remaining vertices may separately have more indirect options that are
* not options for the complete digraph of T, specifically if the complete
* digraph for T has no key resolution edges to an interface object I, but
* this remaining vertex does. We keep track of such interface object types
* for those remaining vertices here.
*/
private readonly remainingVerticesToInterfaceObjectOptions =
new Map<Vertex, Set<string>>;
/**
* A map of field names to the endpoints of field query graph edges with that
* field name. Note we additionally store the progressive overrides label, if
* the edge is conditioned on it.
*/
private readonly fieldsToEndpoints =
new Map<string, Map<Vertex, FieldTail>>();
/**
* A map of type condition names to endpoints of downcast query graph edges
* with that type condition name, including fake downcasts for interface
* objects, and a non-existent edge that represents a type condition name
* equal to the parent type.
*/
private readonly inlineFragmentsToEndpoints =
new Map<string, Map<Vertex, Vertex>>();
/**
* A map of composite type vertices to their downcast edges that lead
* specifically to an object type (i.e., the possible runtime types of the
* vertex's type).
*/
private readonly verticesToObjectTypeDowncasts =
new Map<Vertex, ObjectTypeDowncasts>();
/**
* A map of field names to parent vertices whose corresponding type and schema
* can be rebased on by the field.
*/
private readonly fieldsToRebaseableParentVertices =
new Map<string, Set<Vertex>>;
/**
* A map of type condition names to parent vertices whose corresponding type
* and schema can be rebased on by an inline fragment with that type
* condition.
*/
private readonly inlineFragmentsToRebaseableParentVertices =
new Map<string, Set<Vertex>>;
constructor(graph: QueryGraph) {
this.precomputeNonLocalSelectionMetadata(graph);
}
/**
* Precompute relevant metadata about the query graph for speeding up the
* estimation of the count of non-local selections. Note that none of the
* algorithms used in this function should take any longer algorithmically as
* the rest of query graph creation (and similarly for query graph memory).
*/
private precomputeNonLocalSelectionMetadata(graph: QueryGraph) {
this.precomputeNextVertexMetadata(graph);
this.precomputeRebasingMetadata(graph);
}
private precomputeNextVertexMetadata(graph: QueryGraph) {
const verticesToInterfaceObjectOptions = new Map<Vertex, Set<string>>();
for (const edge of graph.allEdges()) {
switch (edge.transition.kind) {
case 'FieldCollection': {
// We skip selections where the tail is a non-composite type, as we'll
// never need to estimate the next vertices for such selections.
if (!isCompositeType(edge.tail.type)) {
continue;
}
const fieldName = edge.transition.definition.name;
let endpointsEntry = this.fieldsToEndpoints.get(fieldName);
if (!endpointsEntry) {
endpointsEntry = new Map();
this.fieldsToEndpoints.set(fieldName, endpointsEntry);
}
endpointsEntry.set(edge.head, {
tail: edge.tail,
overrideCondition: edge.overrideCondition
});
break;
}
case 'DownCast': {
if (isObjectType(edge.transition.castedType)) {
let downcastsEntry =
this.verticesToObjectTypeDowncasts.get(edge.head);
if (!downcastsEntry) {
downcastsEntry = {
kind: 'NonInterfaceObject',
downcasts: new Map(),
};
this.verticesToObjectTypeDowncasts.set(edge.head, downcastsEntry);
}
assert(
downcastsEntry.kind === 'NonInterfaceObject',
() => 'Unexpectedly found interface object with regular object downcasts',
);
downcastsEntry.downcasts.set(
edge.transition.castedType.name,
edge.tail
);
}
const typeConditionName = edge.transition.castedType.name;
let endpointsEntry = this.inlineFragmentsToEndpoints
.get(typeConditionName);
if (!endpointsEntry) {
endpointsEntry = new Map();
this.inlineFragmentsToEndpoints.set(
typeConditionName,
endpointsEntry
);
}
endpointsEntry.set(edge.head, edge.tail);
break;
}
case 'InterfaceObjectFakeDownCast': {
// Note that fake downcasts for interface objects are only created to
// "fake" object types.
let downcastsEntry =
this.verticesToObjectTypeDowncasts.get(edge.head);
if (!downcastsEntry) {
downcastsEntry = {
kind: 'InterfaceObject',
downcasts: new Set(),
};
this.verticesToObjectTypeDowncasts.set(edge.head, downcastsEntry);
}
assert(
downcastsEntry.kind === 'InterfaceObject',
() => 'Unexpectedly found abstract type with interface object downcasts',
);
downcastsEntry.downcasts.add(edge.transition.castedTypeName);
const typeConditionName = edge.transition.castedTypeName;
let endpointsEntry = this.inlineFragmentsToEndpoints
.get(typeConditionName);
if (!endpointsEntry) {
endpointsEntry = new Map();
this.inlineFragmentsToEndpoints.set(
typeConditionName,
endpointsEntry
);
}
endpointsEntry.set(edge.head, edge.tail);
break;
}
case 'KeyResolution':
case 'RootTypeResolution': {
const headTypeName = edge.head.type.name;
const tailTypeName = edge.tail.type.name;
if (headTypeName === tailTypeName) {
// In this case, we have a non-interface-object key resolution edge
// or a root type resolution edge. The tail must be part of the
// complete digraph for the tail's type, so we record the member.
let indirectOptionsEntry = this.typesToIndirectOptions
.get(tailTypeName);
if (!indirectOptionsEntry) {
indirectOptionsEntry = {
sameTypeOptions: new Set(),
interfaceObjectOptions: new Set(),
};
this.typesToIndirectOptions.set(
tailTypeName,
indirectOptionsEntry,
);
}
indirectOptionsEntry.sameTypeOptions.add(edge.tail);
} else {
// Otherwise, this must be an interface object key resolution edge.
// We don't know the members of the complete digraph for the head's
// type yet, so we can't set the metadata yet, and instead store the
// head to interface object type mapping in a temporary map.
let interfaceObjectOptionsEntry = verticesToInterfaceObjectOptions
.get(edge.head);
if (!interfaceObjectOptionsEntry) {
interfaceObjectOptionsEntry = new Set();
verticesToInterfaceObjectOptions.set(
edge.head,
interfaceObjectOptionsEntry,
);
}
interfaceObjectOptionsEntry.add(tailTypeName);
}
break;
}
case 'SubgraphEnteringTransition':
break;
default:
assertUnreachable(edge.transition);
}
}
// Now that we've finished computing members of the complete digraphs, we
// can properly track interface object options.
for (const [vertex, options] of verticesToInterfaceObjectOptions) {
const optionsMetadata = this.typesToIndirectOptions.get(vertex.type.name);
if (optionsMetadata) {
if (optionsMetadata.sameTypeOptions.has(vertex)) {
for (const option of options) {
optionsMetadata.interfaceObjectOptions.add(option);
}
continue;
}
}
this.remainingVerticesToInterfaceObjectOptions.set(vertex, options);
}
// The interface object options for the complete digraphs are now correct,
// but we need to subtract these from any interface object options for
// remaining vertices.
for (const [vertex, options] of this.remainingVerticesToInterfaceObjectOptions) {
const indirectOptionsMetadata = this.typesToIndirectOptions
.get(vertex.type.name);
if (!indirectOptionsMetadata) {
continue;
}
for (const option of options) {
if (indirectOptionsMetadata.interfaceObjectOptions.has(option)) {
options.delete(option);
}
}
// If this subtraction left any interface object option sets empty, we
// remove them.
if (options.size === 0) {
this.remainingVerticesToInterfaceObjectOptions.delete(vertex);
}
}
// For all composite type vertices, we pretend that there's a self-downcast
// edge for that type, as this simplifies next vertex calculation.
for (const vertex of graph.allVertices()) {
if (
vertex.source === FEDERATED_GRAPH_ROOT_SOURCE
|| !isCompositeType(vertex.type)
) {
continue;
}
const typeConditionName = vertex.type.name;
let endpointsEntry = this.inlineFragmentsToEndpoints
.get(typeConditionName);
if (!endpointsEntry) {
endpointsEntry = new Map();
this.inlineFragmentsToEndpoints.set(
typeConditionName,
endpointsEntry
);
}
endpointsEntry.set(vertex, vertex);
if (!isObjectType(vertex.type)) {
continue;
}
const metadata = federationMetadata(vertex.type.schema());
assert(
metadata,
() => 'Subgraph schema unexpectedly did not have subgraph metadata',
);
if (metadata.isInterfaceObjectType(vertex.type)) {
continue;
}
let downcastsEntry = this.verticesToObjectTypeDowncasts.get(vertex);
if (!downcastsEntry) {
downcastsEntry = {
kind: 'NonInterfaceObject',
downcasts: new Map(),
};
this.verticesToObjectTypeDowncasts.set(vertex, downcastsEntry);
}
assert(
downcastsEntry.kind === 'NonInterfaceObject',
() => 'Unexpectedly found object type with interface object downcasts in supergraph',
);
downcastsEntry.downcasts.set(typeConditionName, vertex);
}
}
private precomputeRebasingMetadata(graph: QueryGraph) {
// We need composite-types-to-vertices map by source for the federated query
// graph, so we compute that here.
const compositeTypesToVerticesBySource =
new Map<string, Map<string, Set<Vertex>>>();
for (const vertex of graph.allVertices()) {
if (
vertex.source === FEDERATED_GRAPH_ROOT_SOURCE
|| !isCompositeType(vertex.type)
) {
continue;
}
let typesToVerticesEntry = compositeTypesToVerticesBySource
.get(vertex.source);
if (!typesToVerticesEntry) {
typesToVerticesEntry = new Map();
compositeTypesToVerticesBySource.set(
vertex.source,
typesToVerticesEntry
);
}
let verticesEntry = typesToVerticesEntry.get(vertex.type.name);
if (!verticesEntry) {
verticesEntry = new Set();
typesToVerticesEntry.set(vertex.type.name, verticesEntry);
}
verticesEntry.add(vertex);
}
// For each subgraph schema, we iterate through its composite types, so that
// we can collect metadata relevant to rebasing.
for (const [source, schema] of graph.sources) {
if (source === FEDERATED_GRAPH_ROOT_SOURCE) {
continue;
}
// We pass through each composite type, recording whether the field can be
// rebased on it along with interface implements/union membership
// relationships.
const fieldsToRebaseableTypes = new Map<string, Set<string>>();
const objectTypesToImplementingCompositeTypes =
new Map<string, Set<string>>();
const metadata = federationMetadata(schema);
assert(
metadata,
() => 'Subgraph schema unexpectedly did not have subgraph metadata',
);
const fromContextDirectiveName = metadata.fromContextDirective().name;
for (const type of schema.types()) {
switch (type.kind) {
case 'ObjectType': {
// Record fields that don't contain @fromContext as being rebaseable
// (also including __typename).
for (const field of type.fields()) {
if (field.arguments().some((arg) =>
arg.hasAppliedDirective(fromContextDirectiveName)
)) {
continue;
}
let rebaseableTypesEntry =
fieldsToRebaseableTypes.get(field.name);
if (!rebaseableTypesEntry) {
rebaseableTypesEntry = new Set();
fieldsToRebaseableTypes.set(field.name, rebaseableTypesEntry);
}
rebaseableTypesEntry.add(type.name);
}
let rebaseableTypesEntry =
fieldsToRebaseableTypes.get(typenameFieldName);
if (!rebaseableTypesEntry) {
rebaseableTypesEntry = new Set();
fieldsToRebaseableTypes.set(
typenameFieldName,
rebaseableTypesEntry
);
}
rebaseableTypesEntry.add(type.name);
// Record the object type as implementing itself.
let implementingObjectTypesEntry =
objectTypesToImplementingCompositeTypes.get(type.name);
if (!implementingObjectTypesEntry) {
implementingObjectTypesEntry = new Set();
objectTypesToImplementingCompositeTypes.set(
type.name,
implementingObjectTypesEntry,
);
}
implementingObjectTypesEntry.add(type.name);
// For each implements, record the interface type as an implementing
// type.
for (const interfaceImplementation of type.interfaceImplementations()) {
implementingObjectTypesEntry.add(
interfaceImplementation.interface.name
);
}
break;
}
case 'InterfaceType': {
// Record fields that don't contain @fromContext as being rebaseable
// (also including __typename).
for (const field of type.fields()) {
if (field.arguments().some((arg) =>
arg.hasAppliedDirective(fromContextDirectiveName)
)) {
continue;
}
let rebaseableTypesEntry =
fieldsToRebaseableTypes.get(field.name);
if (!rebaseableTypesEntry) {
rebaseableTypesEntry = new Set();
fieldsToRebaseableTypes.set(field.name, rebaseableTypesEntry);
}
rebaseableTypesEntry.add(type.name);
}
let rebaseableTypesEntry =
fieldsToRebaseableTypes.get(typenameFieldName);
if (!rebaseableTypesEntry) {
rebaseableTypesEntry = new Set();
fieldsToRebaseableTypes.set(
typenameFieldName,
rebaseableTypesEntry
);
}
rebaseableTypesEntry.add(type.name);
break;
}
case 'UnionType': {
// Just record the __typename field as being rebaseable.
let rebaseableTypesEntry =
fieldsToRebaseableTypes.get(typenameFieldName);
if (!rebaseableTypesEntry) {
rebaseableTypesEntry = new Set();
fieldsToRebaseableTypes.set(
typenameFieldName,
rebaseableTypesEntry
);
}
rebaseableTypesEntry.add(type.name);
// For each member, record the union type as an implementing type.
for (const member of type.members()) {
let implementingObjectTypesEntry =
objectTypesToImplementingCompositeTypes.get(member.type.name);
if (!implementingObjectTypesEntry) {
implementingObjectTypesEntry = new Set();
objectTypesToImplementingCompositeTypes.set(
member.type.name,
implementingObjectTypesEntry,
);
}
implementingObjectTypesEntry.add(type.name);
}
break;
}
case 'ScalarType':
case 'EnumType':
case 'InputObjectType':
break;
default:
assertUnreachable(type);
}
}
// With the interface implements/union membership relationships, we can
// compute which pairs of types have at least one possible runtime type in
// their intersection, and are thus rebaseable.
const inlineFragmentsToRebaseableTypes = new Map<string, Set<string>>();
for (const implementingTypes of objectTypesToImplementingCompositeTypes.values()) {
for (const typeName of implementingTypes) {
let rebaseableTypesEntry =
inlineFragmentsToRebaseableTypes.get(typeName);
if (!rebaseableTypesEntry) {
rebaseableTypesEntry = new Set();
fieldsToRebaseableTypes.set(typeName, rebaseableTypesEntry);
}
for (const implementingType of implementingTypes) {
rebaseableTypesEntry.add(implementingType);
}
}
}
// Finally, we can compute the vertices for the rebaseable types, as we'll
// be working with those instead of types when checking whether an
// operation element can be rebased.
const compositeTypesToVertices =
compositeTypesToVerticesBySource.get(source)
?? new Map<string, Set<Vertex>>();
for (const [fieldName, types] of fieldsToRebaseableTypes) {
let rebaseableParentVerticesEntry =
this.fieldsToRebaseableParentVertices.get(fieldName);
if (!rebaseableParentVerticesEntry) {
rebaseableParentVerticesEntry = new Set();
this.fieldsToRebaseableParentVertices.set(
fieldName,
rebaseableParentVerticesEntry,
);
}
for (const type of types) {
const vertices = compositeTypesToVertices.get(type);
if (vertices) {
for (const vertex of vertices) {
rebaseableParentVerticesEntry.add(vertex);
}
}
}
}
for (const [typeConditionName, types] of inlineFragmentsToRebaseableTypes) {
let rebaseableParentVerticesEntry =
this.inlineFragmentsToRebaseableParentVertices.get(typeConditionName);
if (!rebaseableParentVerticesEntry) {
rebaseableParentVerticesEntry = new Set();
this.inlineFragmentsToRebaseableParentVertices.set(
typeConditionName,
rebaseableParentVerticesEntry,
);
}
for (const type of types) {
const vertices = compositeTypesToVertices.get(type);
if (vertices) {
for (const vertex of vertices) {
rebaseableParentVerticesEntry.add(vertex);
}
}
}
}
}
}
/**
* This calls {@link checkNonLocalSelectionsLimitExceeded} for each of the
* selections in the open branches stack; see that function's doc comment for
* more information.
*/
checkNonLocalSelectionsLimitExceededAtRoot(
stack: [Selection, SimultaneousPathsWithLazyIndirectPaths[]][],
state: NonLocalSelectionsState,
supergraphSchema: Schema,
inconsistentAbstractTypesRuntimes: Set<string>,
overrideConditions: Map<string, boolean>,
): boolean {
for (const [selection, simultaneousPaths] of stack) {
const tailVertices = new Set<Vertex>();
for (const simultaneousPath of simultaneousPaths) {
for (const path of simultaneousPath.paths) {
tailVertices.add(path.tail);
}
}
const tailVerticesInfo =
this.estimateVerticesWithIndirectOptions(tailVertices);
// Note that top-level selections aren't avoided via fully-local selection
// set optimization, so we always add them here.
if (this.updateCount(1, tailVertices.size, state)) {
return true;
}
if (selection.selectionSet) {
const selectionHasDefer = selection.hasDefer();
const nextVertices = this.estimateNextVerticesForSelection(
selection.element,
tailVerticesInfo,
state,
supergraphSchema,
overrideConditions,
);
if (this.checkNonLocalSelectionsLimitExceeded(
selection.selectionSet,
nextVertices,
selectionHasDefer,
state,
supergraphSchema,
inconsistentAbstractTypesRuntimes,
overrideConditions,
)) {
return true;
}
}
}
return false;
}
/**
* When recursing through a selection set to generate options from each
* element, there is an optimization that allows us to avoid option
* exploration if a selection set is "fully local" from all the possible
* vertices we could be at in the query graph.
*
* This function computes an approximate upper bound on the number of
* selections in a selection set that wouldn't be avoided by such an
* optimization (i.e. the "non-local" selections), and adds it to the given
* count in the state. Note that the count for a given selection set is scaled
* by an approximate upper bound on the possible number of tail vertices for
* paths ending at that selection set. If at any point, the count exceeds
* `MAX_NON_LOCAL_SELECTIONS`, then this function will return `true`.
*
* This function's code is closely related to
* `selectionIsFullyLocalFromAllVertices()` (which implements the
* aforementioned optimization). However, when it comes to traversing the
* query graph, we generally ignore the effects of edge pre-conditions and
* other optimizations to option generation for efficiency's sake, giving us
* an upper bound since the extra vertices may fail some of the checks (e.g.
* the selection set may not rebase on them).
*
* Note that this function takes in whether the parent selection of the
* selection set has @defer, as that affects whether the optimization is
* disabled for that selection set.
*/
private checkNonLocalSelectionsLimitExceeded(
selectionSet: SelectionSet,
parentVertices: NextVerticesInfo,
parentSelectionHasDefer: boolean,
state: NonLocalSelectionsState,
supergraphSchema: Schema,
inconsistentAbstractTypesRuntimes: Set<string>,
overrideConditions: Map<string, boolean>,
): boolean {
// Compute whether the selection set is non-local, and if so, add its
// selections to the count. Any of the following causes the selection set to
// be non-local.
// 1. The selection set's vertices having at least one reachable
// cross-subgraph edge.
// 2. The parent selection having @defer.
// 3. Any selection in the selection set having @defer.
// 4. Any selection in the selection set being an inline fragment whose type
// condition has inconsistent runtime types across subgraphs.
// 5. Any selection in the selection set being unable to be rebased on the
// selection set's vertices.
// 6. Any nested selection sets causing the count to be incremented.
let selectionSetIsNonLocal =
parentVertices.nextVerticesHaveReachableCrossSubgraphEdges
|| parentSelectionHasDefer;
for (const selection of selectionSet.selections()) {
const element = selection.element;
const selectionHasDefer = element.hasDefer();
const selectionHasInconsistentRuntimeTypes =
element.kind === 'FragmentElement'
&& element.typeCondition
&& inconsistentAbstractTypesRuntimes.has(element.typeCondition.name);
const oldCount = state.count;
if (selection.selectionSet) {
const nextVertices = this.estimateNextVerticesForSelection(
element,
parentVertices,
state,
supergraphSchema,
overrideConditions,
);
if (this.checkNonLocalSelectionsLimitExceeded(
selection.selectionSet,
nextVertices,
selectionHasDefer,
state,
supergraphSchema,
inconsistentAbstractTypesRuntimes,
overrideConditions,
)) {
return true;
}
}
selectionSetIsNonLocal ||= selectionHasDefer
|| selectionHasInconsistentRuntimeTypes
|| (oldCount != state.count);
}
// Determine whether the selection can be rebased on all selection set
// vertices (without indirect options). This is more expensive, so we do
// this last/only if needed. Note that we were originally calling a slightly
// modified `canAddTo()` to mimic the logic in
// `selectionIsFullyLocalFromAllVertices()`, but this ended up being rather
// expensive in practice, so an optimized version using precomputation is
// used below.
if (!selectionSetIsNonLocal && parentVertices.nextVertices.size > 0) {
outer: for (const selection of selectionSet.selections()) {
switch (selection.kind) {
case 'FieldSelection': {
// Note that while the precomputed metadata accounts for
// @fromContext, it doesn't account for checking whether the
// operation field's parent type either matches the subgraph
// schema's parent type name or is an interface type. Given current
// composition rules, this should always be the case when rebasing
// supergraph/API schema queries onto one of its subgraph schema, so
// we avoid the check here for performance.
const rebaseableParentVertices =
this.fieldsToRebaseableParentVertices
.get(selection.element.definition.name);
if (!rebaseableParentVertices) {
selectionSetIsNonLocal = true;
break outer;
}
for (const vertex of parentVertices.nextVertices) {
if (!rebaseableParentVertices.has(vertex)) {
selectionSetIsNonLocal = true;
break outer;
}
}
break;
}
case 'FragmentSelection': {
const typeConditionName = selection.element.typeCondition?.name;
if (!typeConditionName) {
// Inline fragments without type conditions can always be rebased.
continue;
}
const rebaseableParentVertices =
this.inlineFragmentsToRebaseableParentVertices
.get(typeConditionName);
if (!rebaseableParentVertices) {
selectionSetIsNonLocal = true;
break outer;
}
for (const vertex of parentVertices.nextVertices) {
if (!rebaseableParentVertices.has(vertex)) {
selectionSetIsNonLocal = true;
break outer;
}
}
break;
}
default:
assertUnreachable(selection);
}
}
}
return selectionSetIsNonLocal && this.updateCount(
selectionSet.selections().length,
parentVertices.nextVertices.size,
state,
);
}
/**
* Updates the non-local selection set count in the state, returning true if
* this causes the count to exceed `MAX_NON_LOCAL_SELECTIONS`.
*/
private updateCount(
numSelections: number,
numParentVertices: number,
state: NonLocalSelectionsState,
): boolean {
const additional_count = numSelections * numParentVertices;
const new_count = state.count + additional_count;
if (new_count > NonLocalSelectionsMetadata.MAX_NON_LOCAL_SELECTIONS) {
return true;
}
state.count = new_count;
return false;
}
/**
* In `checkNonLocalSelectionsLimitExceeded()`, when handling a given
* selection for a set of parent vertices (including indirect options), this
* function can be used to estimate an upper bound on the next vertices after
* taking the selection (also with indirect options).
*/
private estimateNextVerticesForSelection(
element: OperationElement,
parentVertices: NextVerticesInfo,
state: NonLocalSelectionsState,
supergraphSchema: Schema,
overrideConditions: Map<string, boolean>,
): NextVerticesInfo {
const selectionKey = element.kind === 'Field'
? element.definition.name
: element.typeCondition?.name;
if (!selectionKey) {
// For empty type condition, the vertices don't change.
return parentVertices;
}
let cache = state.nextVerticesCache.get(selectionKey);
if (!cache) {
cache = {
typesToNextVertices: new Map(),
remainingVerticesToNextVertices: new Map(),
};
state.nextVerticesCache.set(selectionKey, cache);
}
const nextVerticesInfo: NextVerticesInfo = {
nextVertices: new Set(),
nextVerticesHaveReachableCrossSubgraphEdges: false,
nextVerticesWithIndirectOptions: {
types: new Set(),
remainingVertices: new Set(),
}
}
for (const typeName of parentVertices.nextVerticesWithIndirectOptions.types) {
let cacheEntry = cache.typesToNextVertices.get(typeName);
if (!cacheEntry) {
const indirectOptions = this.typesToIndirectOptions.get(typeName);
assert(
indirectOptions,
() => 'Unexpectedly missing vertex information for cached type',
);
cacheEntry = this.estimateNextVerticesForSelectionWithoutCaching(
element,
indirectOptions.sameTypeOptions,
supergraphSchema,
overrideConditions,
);
cache.typesToNextVertices.set(typeName, cacheEntry);
}
this.mergeNextVerticesInfo(cacheEntry, nextVerticesInfo);
}
for (const vertex of parentVertices.nextVerticesWithIndirectOptions.remainingVertices) {
let cacheEntry = cache.remainingVerticesToNextVertices.get(vertex);
if (!cacheEntry) {
cacheEntry = this.estimateNextVerticesForSelectionWithoutCaching(
element,
[vertex],
supergraphSchema,
overrideConditions,
);
cache.remainingVerticesToNextVertices.set(vertex, cacheEntry);
}
this.mergeNextVerticesInfo(cacheEntry, nextVerticesInfo);
}
return nextVerticesInfo;
}
private mergeNextVerticesInfo(
source: NextVerticesInfo,
target: NextVerticesInfo
) {
for (const vertex of source.nextVertices) {
target.nextVertices.add(vertex);
}
target.nextVerticesHaveReachableCrossSubgraphEdges ||=
source.nextVerticesHaveReachableCrossSubgraphEdges;
this.mergeVerticesWithIndirectOptionsInfo(
source.nextVerticesWithIndirectOptions,
target.nextVerticesWithIndirectOptions,
);
}
private mergeVerticesWithIndirectOptionsInfo(
source: VerticesWithIndirectOptionsInfo,
target: VerticesWithIndirectOptionsInfo,
) {
for (const type of source.types) {
target.types.add(type);
}
for (const vertex of source.remainingVertices) {
target.remainingVertices.add(vertex);
}
}
/**
* Estimate an upper bound on the next vertices after taking the selection on
* the given parent vertices. Because we're just trying for an upper bound, we
* assume we can always take type-preserving non-collecting transitions, we
* ignore any conditions on the selection edge, and we always type-explode.
* (We do account for override conditions, which are relatively
* straightforward.)
*
* Since we're iterating through next vertices in the process, for efficiency
* sake we also compute whether there are any reachable cross-subgraph edges
* from the next vertices (without indirect options). This method assumes that
* inline fragments have type conditions.
*/
private estimateNextVerticesForSelectionWithoutCaching(
element: OperationElement,
parentVertices: Iterable<Vertex>,
supergraphSchema: Schema,
overrideConditions: Map<string, boolean>,
): NextVerticesInfo {
const nextVertices = new Set<Vertex>();
switch (element.kind) {
case 'Field': {
const fieldEndpoints = this.fieldsToEndpoints
.get(element.definition.name);
const processHeadVertex = (vertex: Vertex) => {
const fieldTail = fieldEndpoints?.get(vertex);
if (!fieldTail) {
return;
}
if (fieldTail.overrideCondition) {
if (checkOverrideCondition(
fieldTail.overrideCondition,
overrideConditions,
)) {
nextVertices.add(fieldTail.tail);
}
} else {
nextVertices.add(fieldTail.tail);
}
};
for (const vertex of parentVertices) {
// As an upper bound for efficiency sake, we consider both
// non-type-exploded and type-exploded options.
processHeadVertex(vertex);
const downcasts = this.verticesToObjectTypeDowncasts.get(vertex);
if (!downcasts) {
continue;
}
// Interface object fake downcasts only go back to the self vertex, so
// we ignore them.
if (downcasts.kind === 'NonInterfaceObject') {
for (const vertex of downcasts.downcasts.values()) {
processHeadVertex(vertex);
}
}
}
break;
}
case 'FragmentElement': {
const typeConditionName = element.typeCondition?.name;
assert(
typeConditionName,
() => 'Inline fragment unexpectedly had no type condition',
);
const inlineFragmentEndpoints = this.inlineFragmentsToEndpoints
.get(typeConditionName);
// If we end up computing runtime types for the type condition, only do
// it once.
let runtimeTypes: Set<string> | null = null;
for (const vertex of parentVertices) {
// We check whether there's already a (maybe fake) downcast edge for
// the type condition (note that we've inserted fake downcasts for
// same-type type conditions into the metadata).
const nextVertex = inlineFragmentEndpoints?.get(vertex);
if (nextVertex) {
nextVertices.add(nextVertex);
continue;
}
// If not, then we need to type explode across the possible runtime
// types (in the supergraph schema) for the type condition.
const downcasts = this.verticesToObjectTypeDowncasts.get(vertex);
if (!downcasts) {
continue;
}
if (!runtimeTypes) {
const typeInSupergraph = supergraphSchema.type(typeConditionName);
assert(
typeInSupergraph && isCompositeType(typeInSupergraph),
() => 'Type unexpectedly missing or non-composite in supergraph schema',
);
runtimeTypes = new Set<string>();
for (const type of possibleRuntimeTypes(typeInSupergraph)) {
runtimeTypes.add(type.name);
}
}
switch (downcasts.kind) {
case 'NonInterfaceObject': {
for (const [typeName, vertex] of downcasts.downcasts) {
if (runtimeTypes.has(typeName)) {
nextVertices.add(vertex);
}
}
break;
}
case 'InterfaceObject': {
for (const typeName of downcasts.downcasts) {
if (runtimeTypes.has(typeName)) {
// Note that interface object fake downcasts are self edges,
// so we're done once we find one.
nextVertices.add(vertex);
break;
}
}
break;
}
default:
assertUnreachable(downcasts);
}
}
break;
}
default:
assertUnreachable(element);
}
return this.estimateVerticesWithIndirectOptions(nextVertices);
}
/**
* Estimate the indirect options for the given next vertices, and add them to
* the given vertices. As an upper bound for efficiency's sake, we assume we
* can take any indirect option (i.e. ignore any edge conditions).
*/
private estimateVerticesWithIndirectOptions(
nextVertices: Set<Vertex>,
): NextVerticesInfo {
const nextVerticesInfo: NextVerticesInfo = {
nextVertices,
nextVerticesHaveReachableCrossSubgraphEdges: false,
nextVerticesWithIndirectOptions: {
types: new Set(),
remainingVertices: new Set(),
}
};
for (const nextVertex of nextVertices) {
nextVerticesInfo.nextVerticesHaveReachableCrossSubgraphEdges ||=
nextVertex.hasReachableCrossSubgraphEdges;
const typeName = nextVertex.type.name
const optionsMetadata = this.typesToIndirectOptions.get(typeName);
if (optionsMetadata) {
// If there's an entry in `typesToIndirectOptions` for the type, then
// the complete digraph for T is non-empty, so we add its type. If it's
// our first time seeing this type, we also add any of the complete
// digraph's interface object options.
if (
!nextVerticesInfo.nextVerticesWithIndirectOptions.types.has(typeName)
) {
nextVerticesInfo.nextVerticesWithIndirectOptions.types.add(typeName);
for (const option of optionsMetadata.interfaceObjectOptions) {
nextVerticesInfo.nextVerticesWithIndirectOptions.types.add(option);
}
}
// If the vertex is a member of the complete digraph, then we don't need
// to separately add the remaining vertex.
if (optionsMetadata.sameTypeOptions.has(nextVertex)) {
continue;
}
}
// We need to add the remaining vertex, and if its our first time seeing
// it, we also add any of its interface object options.
if (
!nextVerticesInfo.nextVerticesWithIndirectOptions.remainingVertices
.has(nextVertex)
) {
nextVerticesInfo.nextVerticesWithIndirectOptions.remainingVertices
.add(nextVertex);
const options = this.remainingVerticesToInterfaceObjectOptions
.get(nextVertex);
if (options) {
for (const option of options) {
nextVerticesInfo.nextVerticesWithIndirectOptions.types.add(option);
}
}
}
}
return nextVerticesInfo;
}
}
interface NextVerticesCache {
/**
* This is the merged next vertex info for selections on the set of vertices
* in the complete digraph for the given type T. Note that this does not merge
* in the next vertex info for any interface object options reachable from
* vertices in that complete digraph for T.
*/
typesToNextVertices: Map<string, NextVerticesInfo>,
/**
* This is the next vertex info for selections on the given vertex. Note that
* this does not merge in the next vertex info for any interface object
* options reachable from that vertex.
*/
remainingVerticesToNextVertices: Map<Vertex, NextVerticesInfo>,
}
interface NextVerticesInfo {
/**
* The next vertices after taking the selection.
*/
nextVertices: Set<Vertex>,
/**
* Whether any cross-subgraph edges are reachable from any next vertices.
*/
nextVerticesHaveReachableCrossSubgraphEdges: boolean,
/**
* These are the next vertices along with indirect options, represented
* succinctly by the types of any complete digraphs along with remaining
* vertices.
*/
nextVerticesWithIndirectOptions: VerticesWithIndirectOptionsInfo,
}
interface VerticesWithIndirectOptionsInfo {
/**
* For indirect options that are representable as complete digraphs for a type
* T, these are those types.
*/
types: Set<string>,
/**
* For any vertices of type T that aren't in their complete digraphs for type
* T, these are those vertices.
*/
remainingVertices: Set<Vertex>,
}
export class NonLocalSelectionsState {
/**
* An estimation of the number of non-local selections for the whole operation
* (where the count for a given selection set is scaled by the number of tail
* vertices at that selection set). Note this does not count selections from
* recursive query planning.
*/
count = 0;
/**
* Whenever we take a selection on a set of vertices with indirect options, we
* cache the resulting vertices here. The map key for field selections is the
* field's name and for inline fragment selections is the type condition's
* name.
*/
readonly nextVerticesCache = new Map<string, NextVerticesCache>;
}