-
-
Notifications
You must be signed in to change notification settings - Fork 801
/
prune.ts
224 lines (195 loc) · 6.98 KB
/
prune.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import {
ASTNode,
getNamedType,
GraphQLFieldMap,
GraphQLSchema,
isEnumType,
isInputObjectType,
isInterfaceType,
isObjectType,
isScalarType,
isSpecifiedScalarType,
isUnionType,
} from 'graphql';
import { getImplementingTypes } from './get-implementing-types.js';
import { MapperKind } from './Interfaces.js';
import { mapSchema } from './mapSchema.js';
import { getRootTypes } from './rootTypes.js';
import { PruneSchemaOptions } from './types.js';
/**
* Prunes the provided schema, removing unused and empty types
* @param schema The schema to prune
* @param options Additional options for removing unused types from the schema
*/
export function pruneSchema(
schema: GraphQLSchema,
options: PruneSchemaOptions = {},
): GraphQLSchema {
const {
skipEmptyCompositeTypePruning,
skipEmptyUnionPruning,
skipPruning,
skipUnimplementedInterfacesPruning,
skipUnusedTypesPruning,
} = options;
let prunedTypes: string[] = []; // Pruned types during mapping
let prunedSchema: GraphQLSchema = schema;
do {
let visited = visitSchema(prunedSchema);
// Custom pruning was defined, so we need to pre-emptively revisit the schema accounting for this
if (skipPruning) {
const revisit = [];
for (const typeName in prunedSchema.getTypeMap()) {
if (typeName.startsWith('__')) {
continue;
}
const type = prunedSchema.getType(typeName);
// if we want to skip pruning for this type, add it to the list of types to revisit
if (type && skipPruning(type)) {
revisit.push(typeName);
}
}
visited = visitQueue(revisit, prunedSchema, visited); // visit again
}
prunedTypes = [];
prunedSchema = mapSchema(prunedSchema, {
[MapperKind.TYPE]: type => {
if (!visited.has(type.name) && !isSpecifiedScalarType(type)) {
if (
isUnionType(type) ||
isInputObjectType(type) ||
isInterfaceType(type) ||
isObjectType(type) ||
isScalarType(type)
) {
// skipUnusedTypesPruning: skip pruning unused types
if (skipUnusedTypesPruning) {
return type;
}
// skipEmptyUnionPruning: skip pruning empty unions
if (
isUnionType(type) &&
skipEmptyUnionPruning &&
!Object.keys(type.getTypes()).length
) {
return type;
}
if (isInputObjectType(type) || isInterfaceType(type) || isObjectType(type)) {
// skipEmptyCompositeTypePruning: skip pruning object types or interfaces with no fields
if (skipEmptyCompositeTypePruning && !Object.keys(type.getFields()).length) {
return type;
}
}
// skipUnimplementedInterfacesPruning: skip pruning interfaces that are not implemented by any other types
if (isInterfaceType(type) && skipUnimplementedInterfacesPruning) {
return type;
}
}
prunedTypes.push(type.name);
visited.delete(type.name);
return null;
}
return type;
},
});
} while (prunedTypes.length); // Might have empty types and need to prune again
return prunedSchema;
}
function visitSchema(schema: GraphQLSchema): Set<string> {
const queue: string[] = []; // queue of nodes to visit
// Grab the root types and start there
for (const type of getRootTypes(schema)) {
queue.push(type.name);
}
return visitQueue(queue, schema);
}
function visitQueue(
queue: string[],
schema: GraphQLSchema,
visited: Set<string> = new Set<string>(),
): Set<string> {
// Interfaces encountered that are field return types need to be revisited to add their implementations
const revisit: Map<string, boolean> = new Map<string, boolean>();
// Navigate all types starting with pre-queued types (root types)
while (queue.length) {
const typeName = queue.pop() as string;
// Skip types we already visited unless it is an interface type that needs revisiting
if (visited.has(typeName) && revisit[typeName] !== true) {
continue;
}
const type = schema.getType(typeName);
if (type) {
// Get types for union
if (isUnionType(type)) {
queue.push(...type.getTypes().map(type => type.name));
}
// If it is an interface and it is a returned type, grab all implementations so we can use proper __typename in fragments
if (isInterfaceType(type) && revisit[typeName] === true) {
queue.push(...getImplementingTypes(type.name, schema));
// No need to revisit this interface again
revisit[typeName] = false;
}
if (isEnumType(type)) {
// Visit enum values directives argument types
queue.push(
...type.getValues().flatMap(value => {
if (value.astNode) {
return getDirectivesArgumentsTypeNames(schema, value.astNode);
}
return [];
}),
);
}
// Visit interfaces this type is implementing if they haven't been visited yet
if ('getInterfaces' in type) {
// Only pushes to queue to visit but not return types
queue.push(...type.getInterfaces().map(iface => iface.name));
}
// If the type has fields visit those field types
if ('getFields' in type) {
const fields = type.getFields() as GraphQLFieldMap<any, any>;
const entries = Object.entries(fields);
if (!entries.length) {
continue;
}
for (const [, field] of entries) {
if (isObjectType(type)) {
// Visit arg types and arg directives arguments types
queue.push(
...field.args.flatMap(arg => {
const typeNames = [getNamedType(arg.type).name];
if (arg.astNode) {
typeNames.push(...getDirectivesArgumentsTypeNames(schema, arg.astNode));
}
return typeNames;
}),
);
}
const namedType = getNamedType(field.type);
queue.push(namedType.name);
if (field.astNode) {
queue.push(...getDirectivesArgumentsTypeNames(schema, field.astNode));
}
// Interfaces returned on fields need to be revisited to add their implementations
if (isInterfaceType(namedType) && !(namedType.name in revisit)) {
revisit[namedType.name] = true;
}
}
}
if (type.astNode) {
queue.push(...getDirectivesArgumentsTypeNames(schema, type.astNode));
}
visited.add(typeName); // Mark as visited (and therefore it is used and should be kept)
}
}
return visited;
}
function getDirectivesArgumentsTypeNames(
schema: GraphQLSchema,
astNode: Extract<ASTNode, { readonly directives?: any }>,
) {
return (astNode.directives ?? []).flatMap(
directive =>
schema.getDirective(directive.name.value)?.args.map(arg => getNamedType(arg.type).name) ?? [],
);
}