Skip to content

Commit

Permalink
feat(merge): make fragment merging type-aware
Browse files Browse the repository at this point in the history
  • Loading branch information
benjie committed Jun 25, 2020
1 parent 9d04c2a commit 3ab04fb
Showing 1 changed file with 114 additions and 34 deletions.
148 changes: 114 additions & 34 deletions packages/graphiql/src/utility/mergeAst.ts
@@ -1,65 +1,145 @@
/**
* Copyright (c) 2019 GraphQL Contributors.
* Copyright (c) 2020 GraphQL Contributors.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {
DocumentNode,
FieldNode,
FragmentDefinitionNode,
visit,
GraphQLOutputType,
GraphQLSchema,
SelectionNode,
} from 'graphql';
TypeInfo,
getNamedType,
visit,
visitWithTypeInfo,
} from "graphql";

export function uniqueBy(
export function uniqueBy<T>(
array: readonly SelectionNode[],
iteratee: (item: any) => any,
iteratee: (item: FieldNode) => T
) {
const FilteredMap = new Map();
const result = [];
const FilteredMap = new Map<T, FieldNode>();
const result: SelectionNode[] = [];
for (const item of array) {
const uniqeValue = iteratee(item);
if (!FilteredMap.has(uniqeValue)) {
FilteredMap.set(uniqeValue, true);
if (item.kind === "Field") {
const uniqueValue = iteratee(item);
const existing = FilteredMap.get(uniqueValue);
if (item.directives && item.directives.length) {
// Cannot inline fields with directives (yet)
result.push(itemClone);
} else if (existing && existing.selectionSet && item.selectionSet) {
// Merge the selection sets
existing.selectionSet.selections = [
...existing.selectionSet.selections,
...item.selectionSet.selections,
];
} else if (!existing) {
const itemClone = { ...item };
FilteredMap.set(uniqueValue, itemClone);
result.push(itemClone);
}
} else {
result.push(item);
}
}
return result;
}

export function inlineRelevantFragmentSpreads(
fragmentDefinitions: {
[key: string]: FragmentDefinitionNode | undefined;
},
selectionSetType: GraphQLOutputType,
selections: readonly SelectionNode[]
): readonly SelectionNode[] {
const selectionSetTypeName = getNamedType(selectionSetType).name;
const outputSelections = [];
for (let selection of selections) {
if (selection.kind === "FragmentSpread") {
const fragmentDefinition = fragmentDefinitions[selection.name.value];
if (fragmentDefinition) {
const { typeCondition, directives, selectionSet } = fragmentDefinition;
selection = {
kind: "InlineFragment",
typeCondition,
directives,
selectionSet,
};
}
}
if (
selection.kind === "InlineFragment" &&
// Cannot inline if there are directives
(!selection.directives || selection.directives?.length === 0)
) {
const fragmentTypeName = selection.typeCondition
? selection.typeCondition.name.value
: null;
if (!fragmentTypeName || fragmentTypeName === selectionSetTypeName) {
outputSelections.push(
...inlineRelevantFragmentSpreads(
fragmentDefinitions,
selectionSetType,
selection.selectionSet.selections
)
);
continue;
}
}
outputSelections.push(selection);
}
return outputSelections;
}

/**
* Given a document AST, inline all named fragment definitions
* Given a document AST, inline all named fragment definitions.
*/
export default function mergeAST(documentAST: DocumentNode): DocumentNode {
export default function mergeAST(
documentAST: DocumentNode,
schema: GraphQLSchema
): DocumentNode {
const typeInfo = new TypeInfo(schema);
const fragmentDefinitions: {
[key: string]: FragmentDefinitionNode;
[key: string]: FragmentDefinitionNode | undefined;
} = Object.create(null);

for (const definition of documentAST.definitions) {
if (definition.kind === 'FragmentDefinition') {
if (definition.kind === "FragmentDefinition") {
fragmentDefinitions[definition.name.value] = definition;
}
}

return visit(documentAST, {
FragmentSpread(node) {
return {
...fragmentDefinitions[node.name.value],
kind: 'InlineFragment',
};
},
SelectionSet(node) {
return {
...node,
selections: uniqueBy(
node.selections,
selection => selection.name.value,
),
};
},
FragmentDefinition() {
return null;
},
});
return visit(
documentAST,
visitWithTypeInfo(typeInfo, {
SelectionSet(node) {
const selectionSetType = typeInfo.getParentType();
let { selections } = node;

if (selectionSetType) {
selections = inlineRelevantFragmentSpreads(
fragmentDefinitions,
selectionSetType,
selections
);
}

selections = uniqueBy(selections, selection =>
selection.alias ? selection.alias.value : selection.name.value
);

return {
...node,
selections,
};
},
FragmentDefinition() {
return null;
},
})
);
}

0 comments on commit 3ab04fb

Please sign in to comment.