Skip to content

Commit

Permalink
Update gateway to build & use new Query Plan format (apollographql/ap…
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeDawkins authored Aug 31, 2020
1 parent 1063419 commit cd7f939
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 90 deletions.
92 changes: 79 additions & 13 deletions gateway-js/src/QueryPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@ import {
FragmentDefinitionNode,
GraphQLSchema,
OperationDefinitionNode,
SelectionSetNode,
VariableDefinitionNode,
Kind,
SelectionNode as GraphQLJSSelectionNode,
} from 'graphql';
import { astSerializer, queryPlanSerializer } from './snapshotSerializers';
import prettyFormat from 'pretty-format';
import { queryPlanSerializer, astSerializer } from './snapshotSerializers';

export type ResponsePath = (string | number)[];

export type FragmentMap = { [fragmentName: string]: FragmentDefinitionNode };

export interface QueryPlan {
kind: 'QueryPlan';
node?: PlanNode;
}

export type OperationContext = {
schema: GraphQLSchema;
operation: OperationDefinitionNode;
fragments: FragmentMap;
};

export interface QueryPlan {
kind: 'QueryPlan';
node?: PlanNode;
}

export type PlanNode = SequenceNode | ParallelNode | FetchNode | FlattenNode;

export interface SequenceNode {
Expand All @@ -38,20 +38,86 @@ export interface ParallelNode {
export interface FetchNode {
kind: 'Fetch';
serviceName: string;
selectionSet: SelectionSetNode;
variableUsages?: { [name: string]: VariableDefinitionNode };
requires?: SelectionSetNode;
internalFragments: Set<FragmentDefinitionNode>;
source: string;
variableUsages?: string[];
requires?: QueryPlanSelectionNode[];
operation: string;
}

export interface FlattenNode {
kind: 'Flatten';
path: ResponsePath;
node: 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;
};
2 changes: 1 addition & 1 deletion gateway-js/src/__tests__/executeQueryPlan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('executeQueryPlan', () => {
request: {
variables: {},
},
};
} as GraphQLRequestContext;
}

describe(`errors`, () => {
Expand Down
37 changes: 1 addition & 36 deletions gateway-js/src/__tests__/queryPlanCucumber.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,9 @@ features.forEach((feature) => {
options
);

const serializedPlan = JSON.parse(JSON.stringify(queryPlan, serializeQueryPlanNode));
const parsedExpectedPlan = JSON.parse(expectedQueryPlan);

expect(serializedPlan).toEqual(parsedExpectedPlan);
expect(queryPlan).toEqual(parsedExpectedPlan);
})
}

Expand All @@ -69,37 +68,3 @@ features.forEach((feature) => {
});
});
});

const serializeQueryPlanNode = (k: string , v: any) => {
switch(k){
case "selectionSet":
case "internalFragments":
case "loc":
case "arguments":
case "directives":
case "source":
return undefined;
case "kind":
if(v === Kind.SELECTION_SET) return undefined;
return v;
case "variableUsages":
// TODO check this
return Object.keys(v);
case "typeCondition":
return v.name.value;
case "name":
return v.value;
case "requires":
return v?.selections;
default:
// replace source with operation
if(v?.kind === "Fetch"){
return { ...v, operation: v.source };
}
// replace selectionSet with selections[]
if(v?.kind === Kind.INLINE_FRAGMENT){
return { ...v, selections: v.selectionSet.selections }
}
return v;
}
}
11 changes: 6 additions & 5 deletions gateway-js/src/buildQueryPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
QueryPlan,
ResponsePath,
OperationContext,
trimSelectionNodes,
FragmentMap,
} from './QueryPlan';
import { getFieldDef, getResponseName } from './utilities/graphql';
Expand Down Expand Up @@ -101,6 +102,8 @@ export function buildQueryPlan(
return {
kind: 'QueryPlan',
node: nodes.length
// if an operation is a mutation, we run the root fields in sequence,
// otherwise we run them in parallel
? flatWrap(isMutation ? 'Sequence' : 'Parallel', nodes)
: undefined,
};
Expand Down Expand Up @@ -144,11 +147,9 @@ function executionNodeForGroup(
const fetchNode: FetchNode = {
kind: 'Fetch',
serviceName,
selectionSet,
requires,
variableUsages,
internalFragments,
source: stripIgnoredCharacters(print(operation)),
requires: requires ? trimSelectionNodes(requires?.selections) : undefined,
variableUsages: Object.keys(variableUsages),
operation: stripIgnoredCharacters(print(operation)),
};

const node: PlanNode =
Expand Down
29 changes: 15 additions & 14 deletions gateway-js/src/executeQueryPlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
execute,
GraphQLError,
Kind,
SelectionSetNode,
TypeNameMetaFieldDef,
GraphQLFieldResolver,
} from 'graphql';
Expand All @@ -20,9 +19,11 @@ import {
QueryPlan,
ResponsePath,
OperationContext,
QueryPlanSelectionNode,
QueryPlanFieldNode,
getResponseName
} from './QueryPlan';
import { deepMerge } from './utilities/deepMerge';
import { getResponseName } from './utilities/graphql';

export type ServiceMap = {
[serviceName: string]: GraphQLDataSource;
Expand Down Expand Up @@ -207,7 +208,7 @@ async function executeFetch<TContext>(

let variables = Object.create(null);
if (fetch.variableUsages) {
for (const variableName of Object.keys(fetch.variableUsages)) {
for (const variableName of fetch.variableUsages) {
const providedVariables = context.requestContext.request.variables;
if (
providedVariables &&
Expand All @@ -221,7 +222,7 @@ async function executeFetch<TContext>(
if (!fetch.requires) {
const dataReceivedFromService = await sendOperation(
context,
fetch.source,
fetch.operation,
variables,
);

Expand All @@ -248,7 +249,7 @@ async function executeFetch<TContext>(

const dataReceivedFromService = await sendOperation(
context,
fetch.source,
fetch.operation,
{ ...variables, representations },
);

Expand Down Expand Up @@ -388,7 +389,7 @@ async function executeFetch<TContext>(
*/
function executeSelectionSet(
source: Record<string, any> | null,
selectionSet: SelectionSetNode,
selections: QueryPlanSelectionNode[],
): Record<string, any> | null {

// If the underlying service has returned null for the parent (source)
Expand All @@ -399,23 +400,23 @@ function executeSelectionSet(

const result: Record<string, any> = Object.create(null);

for (const selection of selectionSet.selections) {
for (const selection of selections) {
switch (selection.kind) {
case Kind.FIELD:
const responseName = getResponseName(selection);
const selectionSet = selection.selectionSet;
const responseName = getResponseName(selection as QueryPlanFieldNode);
const selections = (selection as QueryPlanFieldNode).selections;

if (typeof source[responseName] === 'undefined') {
throw new Error(`Field "${responseName}" was not found in response.`);
}
if (Array.isArray(source[responseName])) {
result[responseName] = source[responseName].map((value: any) =>
selectionSet ? executeSelectionSet(value, selectionSet) : value,
selections ? executeSelectionSet(value, selections) : value,
);
} else if (selectionSet) {
} else if (selections) {
result[responseName] = executeSelectionSet(
source[responseName],
selectionSet,
selections,
);
} else {
result[responseName] = source[responseName];
Expand All @@ -427,10 +428,10 @@ function executeSelectionSet(
const typename = source && source['__typename'];
if (!typename) continue;

if (typename === selection.typeCondition.name.value) {
if (typename === selection.typeCondition) {
deepMerge(
result,
executeSelectionSet(source, selection.selectionSet),
executeSelectionSet(source, selection.selections),
);
}
break;
Expand Down
Loading

0 comments on commit cd7f939

Please sign in to comment.