-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
merge-ast.ts
156 lines (146 loc) · 4.25 KB
/
merge-ast.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import {
DocumentNode,
FieldNode,
FragmentDefinitionNode,
GraphQLOutputType,
GraphQLSchema,
SelectionNode,
TypeInfo,
getNamedType,
visit,
visitWithTypeInfo,
ASTVisitor,
Kind,
} from 'graphql';
function uniqueBy<T>(
array: readonly SelectionNode[],
iteratee: (item: FieldNode) => T,
) {
const FilteredMap = new Map<T, FieldNode>();
const result: SelectionNode[] = [];
for (const item of array) {
if (item.kind === 'Field') {
const uniqueValue = iteratee(item);
const existing = FilteredMap.get(uniqueValue);
if (item.directives?.length) {
// Cannot inline fields with directives (yet)
const itemClone = { ...item };
result.push(itemClone);
} else if (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;
}
function inlineRelevantFragmentSpreads(
fragmentDefinitions: {
[key: string]: FragmentDefinitionNode | undefined;
},
selections: readonly SelectionNode[],
selectionSetType?: GraphQLOutputType | null,
): readonly SelectionNode[] {
const selectionSetTypeName = selectionSetType
? getNamedType(selectionSetType).name
: null;
const outputSelections = [];
const seenSpreads: string[] = [];
for (let selection of selections) {
if (selection.kind === 'FragmentSpread') {
const fragmentName = selection.name.value;
if (!selection.directives || selection.directives.length === 0) {
if (seenSpreads.includes(fragmentName)) {
/* It's a duplicate - skip it! */
continue;
} else {
seenSpreads.push(fragmentName);
}
}
const fragmentDefinition = fragmentDefinitions[selection.name.value];
if (fragmentDefinition) {
const { typeCondition, directives, selectionSet } = fragmentDefinition;
selection = {
kind: Kind.INLINE_FRAGMENT,
typeCondition,
directives,
selectionSet,
};
}
}
if (
selection.kind === Kind.INLINE_FRAGMENT &&
// 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,
selection.selectionSet.selections,
selectionSetType,
),
);
continue;
}
}
outputSelections.push(selection);
}
return outputSelections;
}
/**
* Given a document AST, inline all named fragment definitions.
*/
export function mergeAst(
documentAST: DocumentNode,
schema?: GraphQLSchema | null,
): DocumentNode {
// If we're given the schema, we can simplify even further by resolving object
// types vs unions/interfaces
const typeInfo = schema ? new TypeInfo(schema) : null;
const fragmentDefinitions: {
[key: string]: FragmentDefinitionNode | undefined;
} = Object.create(null);
for (const definition of documentAST.definitions) {
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
fragmentDefinitions[definition.name.value] = definition;
}
}
const visitors: ASTVisitor = {
SelectionSet(node: any) {
const selectionSetType = typeInfo ? typeInfo.getParentType() : null;
let { selections } = node;
selections = inlineRelevantFragmentSpreads(
fragmentDefinitions,
selections,
selectionSetType,
);
selections = uniqueBy(selections, selection =>
selection.alias ? selection.alias.value : selection.name.value,
);
return {
...node,
selections,
};
},
FragmentDefinition() {
return null;
},
};
return visit(
documentAST,
typeInfo ? visitWithTypeInfo(typeInfo, visitors) : visitors,
);
}