UNPKG

8.03 kBJavaScriptView Raw
1import { getNamedType, isEnumType, isInputObjectType, isInterfaceType, isObjectType, isScalarType, isSpecifiedScalarType, isUnionType, } from 'graphql';
2import { getImplementingTypes } from './get-implementing-types.js';
3import { MapperKind } from './Interfaces.js';
4import { mapSchema } from './mapSchema.js';
5import { getRootTypes } from './rootTypes.js';
6/**
7 * Prunes the provided schema, removing unused and empty types
8 * @param schema The schema to prune
9 * @param options Additional options for removing unused types from the schema
10 */
11export function pruneSchema(schema, options = {}) {
12 const { skipEmptyCompositeTypePruning, skipEmptyUnionPruning, skipPruning, skipUnimplementedInterfacesPruning, skipUnusedTypesPruning, } = options;
13 let prunedTypes = []; // Pruned types during mapping
14 let prunedSchema = schema;
15 do {
16 let visited = visitSchema(prunedSchema);
17 // Custom pruning was defined, so we need to pre-emptively revisit the schema accounting for this
18 if (skipPruning) {
19 const revisit = [];
20 for (const typeName in prunedSchema.getTypeMap()) {
21 if (typeName.startsWith('__')) {
22 continue;
23 }
24 const type = prunedSchema.getType(typeName);
25 // if we want to skip pruning for this type, add it to the list of types to revisit
26 if (type && skipPruning(type)) {
27 revisit.push(typeName);
28 }
29 }
30 visited = visitQueue(revisit, prunedSchema, visited); // visit again
31 }
32 prunedTypes = [];
33 prunedSchema = mapSchema(prunedSchema, {
34 [MapperKind.TYPE]: type => {
35 if (!visited.has(type.name) && !isSpecifiedScalarType(type)) {
36 if (isUnionType(type) ||
37 isInputObjectType(type) ||
38 isInterfaceType(type) ||
39 isObjectType(type) ||
40 isScalarType(type)) {
41 // skipUnusedTypesPruning: skip pruning unused types
42 if (skipUnusedTypesPruning) {
43 return type;
44 }
45 // skipEmptyUnionPruning: skip pruning empty unions
46 if (isUnionType(type) &&
47 skipEmptyUnionPruning &&
48 !Object.keys(type.getTypes()).length) {
49 return type;
50 }
51 if (isInputObjectType(type) || isInterfaceType(type) || isObjectType(type)) {
52 // skipEmptyCompositeTypePruning: skip pruning object types or interfaces with no fields
53 if (skipEmptyCompositeTypePruning && !Object.keys(type.getFields()).length) {
54 return type;
55 }
56 }
57 // skipUnimplementedInterfacesPruning: skip pruning interfaces that are not implemented by any other types
58 if (isInterfaceType(type) && skipUnimplementedInterfacesPruning) {
59 return type;
60 }
61 }
62 prunedTypes.push(type.name);
63 visited.delete(type.name);
64 return null;
65 }
66 return type;
67 },
68 });
69 } while (prunedTypes.length); // Might have empty types and need to prune again
70 return prunedSchema;
71}
72function visitSchema(schema) {
73 const queue = []; // queue of nodes to visit
74 // Grab the root types and start there
75 for (const type of getRootTypes(schema)) {
76 queue.push(type.name);
77 }
78 return visitQueue(queue, schema);
79}
80function visitQueue(queue, schema, visited = new Set()) {
81 // Interfaces encountered that are field return types need to be revisited to add their implementations
82 const revisit = new Map();
83 // Navigate all types starting with pre-queued types (root types)
84 while (queue.length) {
85 const typeName = queue.pop();
86 // Skip types we already visited unless it is an interface type that needs revisiting
87 if (visited.has(typeName) && revisit[typeName] !== true) {
88 continue;
89 }
90 const type = schema.getType(typeName);
91 if (type) {
92 // Get types for union
93 if (isUnionType(type)) {
94 queue.push(...type.getTypes().map(type => type.name));
95 }
96 // If it is an interface and it is a returned type, grab all implementations so we can use proper __typename in fragments
97 if (isInterfaceType(type) && revisit[typeName] === true) {
98 queue.push(...getImplementingTypes(type.name, schema));
99 // No need to revisit this interface again
100 revisit[typeName] = false;
101 }
102 if (isEnumType(type)) {
103 // Visit enum values directives argument types
104 queue.push(...type.getValues().flatMap(value => getDirectivesArgumentsTypeNames(schema, value)));
105 }
106 // Visit interfaces this type is implementing if they haven't been visited yet
107 if ('getInterfaces' in type) {
108 // Only pushes to queue to visit but not return types
109 queue.push(...type.getInterfaces().map(iface => iface.name));
110 }
111 // If the type has fields visit those field types
112 if ('getFields' in type) {
113 const fields = type.getFields();
114 const entries = Object.entries(fields);
115 if (!entries.length) {
116 continue;
117 }
118 for (const [, field] of entries) {
119 if (isObjectType(type)) {
120 // Visit arg types and arg directives arguments types
121 queue.push(...field.args.flatMap(arg => {
122 const typeNames = [getNamedType(arg.type).name];
123 typeNames.push(...getDirectivesArgumentsTypeNames(schema, arg));
124 return typeNames;
125 }));
126 }
127 const namedType = getNamedType(field.type);
128 queue.push(namedType.name);
129 queue.push(...getDirectivesArgumentsTypeNames(schema, field));
130 // Interfaces returned on fields need to be revisited to add their implementations
131 if (isInterfaceType(namedType) && !(namedType.name in revisit)) {
132 revisit[namedType.name] = true;
133 }
134 }
135 }
136 queue.push(...getDirectivesArgumentsTypeNames(schema, type));
137 visited.add(typeName); // Mark as visited (and therefore it is used and should be kept)
138 }
139 }
140 return visited;
141}
142function getDirectivesArgumentsTypeNames(schema, directableObj) {
143 const argTypeNames = new Set();
144 if (directableObj.astNode?.directives) {
145 for (const directiveNode of directableObj.astNode.directives) {
146 const directive = schema.getDirective(directiveNode.name.value);
147 if (directive?.args) {
148 for (const arg of directive.args) {
149 const argType = getNamedType(arg.type);
150 argTypeNames.add(argType.name);
151 }
152 }
153 }
154 }
155 if (directableObj.extensions?.['directives']) {
156 for (const directiveName in directableObj.extensions['directives']) {
157 const directive = schema.getDirective(directiveName);
158 if (directive?.args) {
159 for (const arg of directive.args) {
160 const argType = getNamedType(arg.type);
161 argTypeNames.add(argType.name);
162 }
163 }
164 }
165 }
166 return [...argTypeNames];
167}