UNPKG

7.71 kBJavaScriptView Raw
1import { getNamedType, isObjectType, isInterfaceType, isUnionType, isInputObjectType, isSpecifiedScalarType, isScalarType, isEnumType, } from 'graphql';
2import { mapSchema } from './mapSchema.js';
3import { MapperKind } from './Interfaces.js';
4import { getRootTypes } from './rootTypes.js';
5import { getImplementingTypes } from './get-implementing-types.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) && skipEmptyUnionPruning && !Object.keys(type.getTypes()).length) {
47 return type;
48 }
49 if (isInputObjectType(type) || isInterfaceType(type) || isObjectType(type)) {
50 // skipEmptyCompositeTypePruning: skip pruning object types or interfaces with no fields
51 if (skipEmptyCompositeTypePruning && !Object.keys(type.getFields()).length) {
52 return type;
53 }
54 }
55 // skipUnimplementedInterfacesPruning: skip pruning interfaces that are not implemented by any other types
56 if (isInterfaceType(type) && skipUnimplementedInterfacesPruning) {
57 return type;
58 }
59 }
60 prunedTypes.push(type.name);
61 visited.delete(type.name);
62 return null;
63 }
64 return type;
65 },
66 });
67 } while (prunedTypes.length); // Might have empty types and need to prune again
68 return prunedSchema;
69}
70function visitSchema(schema) {
71 const queue = []; // queue of nodes to visit
72 // Grab the root types and start there
73 for (const type of getRootTypes(schema)) {
74 queue.push(type.name);
75 }
76 return visitQueue(queue, schema);
77}
78function visitQueue(queue, schema, visited = new Set()) {
79 // Interfaces encountered that are field return types need to be revisited to add their implementations
80 const revisit = new Map();
81 // Navigate all types starting with pre-queued types (root types)
82 while (queue.length) {
83 const typeName = queue.pop();
84 // Skip types we already visited unless it is an interface type that needs revisiting
85 if (visited.has(typeName) && revisit[typeName] !== true) {
86 continue;
87 }
88 const type = schema.getType(typeName);
89 if (type) {
90 // Get types for union
91 if (isUnionType(type)) {
92 queue.push(...type.getTypes().map(type => type.name));
93 }
94 // If it is an interface and it is a returned type, grab all implementations so we can use proper __typename in fragments
95 if (isInterfaceType(type) && revisit[typeName] === true) {
96 queue.push(...getImplementingTypes(type.name, schema));
97 // No need to revisit this interface again
98 revisit[typeName] = false;
99 }
100 if (isEnumType(type)) {
101 // Visit enum values directives argument types
102 queue.push(...type.getValues().flatMap(value => {
103 if (value.astNode) {
104 return getDirectivesArgumentsTypeNames(schema, value.astNode);
105 }
106 return [];
107 }));
108 }
109 // Visit interfaces this type is implementing if they haven't been visited yet
110 if ('getInterfaces' in type) {
111 // Only pushes to queue to visit but not return types
112 queue.push(...type.getInterfaces().map(iface => iface.name));
113 }
114 // If the type has fields visit those field types
115 if ('getFields' in type) {
116 const fields = type.getFields();
117 const entries = Object.entries(fields);
118 if (!entries.length) {
119 continue;
120 }
121 for (const [, field] of entries) {
122 if (isObjectType(type)) {
123 // Visit arg types and arg directives arguments types
124 queue.push(...field.args.flatMap(arg => {
125 const typeNames = [getNamedType(arg.type).name];
126 if (arg.astNode) {
127 typeNames.push(...getDirectivesArgumentsTypeNames(schema, arg.astNode));
128 }
129 return typeNames;
130 }));
131 }
132 const namedType = getNamedType(field.type);
133 queue.push(namedType.name);
134 if (field.astNode) {
135 queue.push(...getDirectivesArgumentsTypeNames(schema, field.astNode));
136 }
137 // Interfaces returned on fields need to be revisited to add their implementations
138 if (isInterfaceType(namedType) && !(namedType.name in revisit)) {
139 revisit[namedType.name] = true;
140 }
141 }
142 }
143 if (type.astNode) {
144 queue.push(...getDirectivesArgumentsTypeNames(schema, type.astNode));
145 }
146 visited.add(typeName); // Mark as visited (and therefore it is used and should be kept)
147 }
148 }
149 return visited;
150}
151function getDirectivesArgumentsTypeNames(schema, astNode) {
152 var _a;
153 return ((_a = astNode.directives) !== null && _a !== void 0 ? _a : []).flatMap(directive => { var _a, _b; return (_b = (_a = schema.getDirective(directive.name.value)) === null || _a === void 0 ? void 0 : _a.args.map(arg => getNamedType(arg.type).name)) !== null && _b !== void 0 ? _b : []; });
154}