@apollo/query-planner
Version:
Apollo Query Planner
235 lines (210 loc) • 10.2 kB
text/typescript
import {
Kind,
SelectionNode as GraphQLJSSelectionNode,
OperationTypeNode,
DocumentNode,
} from 'graphql';
import prettyFormat from 'pretty-format';
import { queryPlanSerializer, astSerializer } from './snapshotSerializers';
export type ResponsePath = (string | number)[];
export interface QueryPlan {
kind: 'QueryPlan';
node?: PlanNode | SubscriptionNode;
}
export type PlanNode = SequenceNode | ParallelNode | FetchNode | FlattenNode | DeferNode | ConditionNode;
export interface SequenceNode {
kind: 'Sequence';
nodes: PlanNode[];
}
export interface ParallelNode {
kind: 'Parallel';
nodes: PlanNode[];
}
export interface SubscriptionNode {
kind: 'Subscription';
primary: FetchNode;
rest?: PlanNode;
}
export interface FetchNode {
kind: 'Fetch';
serviceName: string;
// Optional identifier for the fetch for defer support. All fetches of a given plan will be guaranteed to have a unique `id`.
id?: string,
// If QP defer support is enabled _and_ the `serviceName` subgraph support defer, then whether `operation` contains some @defer. Unset otherwise.
hasDefers?: boolean,
variableUsages?: string[];
requires?: QueryPlanSelectionNode[];
operation: string;
operationName: string | undefined;
operationKind: OperationTypeNode;
operationDocumentNode?: DocumentNode;
// Optionally describe a number of "rewrites" that query plan executors should apply to the data that is sent as input of this fetch.
// Note that such rewrites should only impact the inputs of the fetch they are applied to (meaning that, as those inputs are collected
// from the current in-memory result, the rewrite should _not_ impact said in-memory results, only what is sent in the fetch).
inputRewrites?: FetchDataRewrite[];
// Similar, but for optional "rewrites" to apply to the data that received from a fetch (and before it is applied to the current in-memory results).
outputRewrites?: FetchDataRewrite[];
// Optional rewrites to apply to the data that is sent as input of this fetch. This is a list of rewrites that should be applied to the data
contextRewrites?: FetchDataKeyRenamer[];
}
/**
* The type of rewrites currently supported on the input/output data of fetches.
*
* A rewrite usually identifies some subpart of thedata and some action to perform on that subpart.
*/
export type FetchDataRewrite = FetchDataValueSetter | FetchDataKeyRenamer;
/**
* A rewrite that sets a value at the provided path of the data it is applied to.
*/
export interface FetchDataValueSetter {
kind: 'ValueSetter',
// Path to the key that is set by this "rewrite". It is of the form `[ 'x', '... on A', 'y' ]`, where fragments
// means that the path should only match for objects whose `__typename` is he provided type.
// If the path does not match in the data this is applied to, then this setter should not rewrite the data.
// The path starts at the top of the data it is applied to. So for instance, for fetch data inputs, the path
// start at the root of the object representing those inputs.
path: string[],
// The value to set at `path`.
setValueTo: any,
}
export interface FetchDataKeyRenamer {
kind: 'KeyRenamer'
// Same format as in `FetchDataValueSetter`, but this renames the key at the very end of this path to the
// name from `renameKeyTo`.
path: string[],
renameKeyTo: string,
}
export interface FlattenNode {
kind: 'Flatten';
path: ResponsePath;
node: PlanNode;
}
/**
* A `DeferNode` corresponds to one or more @defer at the same level of "nestedness" in the planned query.
*
* It contains a "primary block" and an array of "deferred blocks". The "primary block represents the part of
* the query that is _not_ deferred (so the part of the query up until we reach the @defer(s) this handle),
* while each "deferred block" correspond to the deferred part of one of the @defer handle by the node.
*
* Note that `DeferNode` are only generated if defer support is enabled for the query planner. Also note that
* if said support is enabled, then `DeferNode` are always generated if the query has a @defer, even if in some
* case the generated plan may not "truly" defer the underlying fetches (in cases where `defered[*].node`
* are all undefined; this currently happens because some specific case of defer cannot be handled, but could
* later also happen if we implement more advanced server-side heuristic to decide if deferring is judicious or
* not). This allow the executor of the plan to consistely send a defer-abiding multipart response to the client.
*/
export interface DeferNode {
kind: 'Defer',
// The "primary" part of a defer, that is the non-deferred part (though could be deferred itself for a nested defer).
primary: {
// The part of the original query that "selects" the data to send in that primary response (once the plan in `node` completes).
// Note that if this `DeferNode` is nested, then it must come inside the `DeferredNode` in which it is nested, and in that
// case this subselection will start that that parent `DeferredNode.path`.
// Note: this can be undefined in the rare case where everything in the original query is deferred (which is not very
// useful in practice, but not disallowed by the @defer spec at the moment).
subselection?: string,
// The plan to get all the data for that primary part. Same as for subselection: usually defined, but can be undefined
// in some corner cases where nothing is to be done in the primary part.
node?: PlanNode,
},
// The "deferred" parts of the defer (note that it's an array). Each of those deferred elements will correspond to
// a different chunk of the response to the client (after the initial non-deferred one that is).
deferred: DeferredNode[],
}
// Note that `DeferredNode` is not a "full" node in the sense that it is not part of the `PlanNode`
// type union, because it never appears alone, it is a sub-part of `DeferNode`. It is nonetheless
// useful to extract it as a named type for use in the code generating plans.
export interface DeferredNode {
// References one or more fetch node(s) (by `id`) within `primary.node`. The plan of this deferred part should not be started before all those fetches returns.
depends: {
id: string,
// If `FetchNode` pointed by `id` has `hasDefers=true` and this value is set (to the label of one of the defer of the pointed fetch), then this deferred "block"
// .
deferLabel?: string,
}[],
// The optional defer label.
label?: string,
// Path, in the query, to the @defer this corresponds to. The `subselection` starts at that `queryPath`.
// This look like: `[ 'products', '... on Book', 'reviews' ]`
queryPath: string[],
// The part of the original query that "selects" the data to send in that deferred response (once the plan in `node` completes). Will be set _unless_ `node` is a `DeferNode` itself.
subselection?: string,
// The plan to get all the data for that deferred part. Usually set, but can be undefined for a `@defer` where everything has been fetched in the "primary block", that is when
// this deferred only exists to expose what should be send to the upstream client in a deferred response, but without declaring additional fetches (which happens for @defer that
// cannot be handled through query planner and where the defer cannot be passed through to the subgraph).
node?: PlanNode,
}
export interface ConditionNode {
kind: 'Condition',
condition: string,
ifClause?: PlanNode,
elseClause?: PlanNode,
}
/**
* SelectionNodes from GraphQL-js _can_ have a FragmentSpreadNode
* but this SelectionNode is specifically typing the `requires` key
* in a built query plan, where there can't be FragmentSpreadNodes
* since that info is contained in the `FetchNode.operation`
*/
export type QueryPlanSelectionNode = QueryPlanFieldNode | QueryPlanInlineFragmentNode;
export interface QueryPlanFieldNode {
readonly kind: 'Field';
readonly alias?: string;
readonly name: string;
readonly selections?: QueryPlanSelectionNode[];
}
export interface QueryPlanInlineFragmentNode {
readonly kind: 'InlineFragment';
readonly typeCondition?: string;
readonly selections: QueryPlanSelectionNode[];
}
export function serializeQueryPlan(queryPlan: QueryPlan) {
return prettyFormat(queryPlan, {
plugins: [queryPlanSerializer, astSerializer],
});
}
export function getResponseName(node: QueryPlanFieldNode): string {
return node.alias ? node.alias : node.name;
}
/**
* Converts a GraphQL-js SelectionNode to our newly defined SelectionNode
*
* This function is used to remove the unneeded pieces of a SelectionSet's
* `.selections`. It is only ever called on a query plan's `requires` field,
* so we can guarantee there won't be any FragmentSpreads passed in. That's why
* we can ignore the case where `selection.kind === Kind.FRAGMENT_SPREAD`
*/
export const trimSelectionNodes = (
selections: readonly GraphQLJSSelectionNode[],
): QueryPlanSelectionNode[] => {
/**
* Using an array to push to instead of returning value from `selections.map`
* because TypeScript thinks we can encounter a `Kind.FRAGMENT_SPREAD` here,
* so if we mapped the array directly to the return, we'd have to `return undefined`
* from one branch of the map and then `.filter(Boolean)` on that returned
* array
*/
const remapped: QueryPlanSelectionNode[] = [];
selections.forEach((selection) => {
if (selection.kind === Kind.FIELD) {
remapped.push({
kind: Kind.FIELD,
name: selection.name.value,
selections:
selection.selectionSet &&
trimSelectionNodes(selection.selectionSet.selections),
});
}
if (selection.kind === Kind.INLINE_FRAGMENT) {
remapped.push({
kind: Kind.INLINE_FRAGMENT,
typeCondition: selection.typeCondition?.name.value,
selections: trimSelectionNodes(selection.selectionSet.selections),
});
}
});
return remapped;
};
export const isPlanNode = (node: PlanNode | SubscriptionNode | undefined): node is PlanNode => {
return !!node && node.kind !== 'Subscription';
}