UNPKG

@graphql-tools/utils

Version:

Common package containing utils and types for GraphQL tools

168 lines (167 loc) • 8.03 kB
import { getNamedType, 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'; /** * 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, options = {}) { const { skipEmptyCompositeTypePruning, skipEmptyUnionPruning, skipPruning, skipUnimplementedInterfacesPruning, skipUnusedTypesPruning, } = options; let prunedTypes = []; // Pruned types during mapping let prunedSchema = 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) { const queue = []; // 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, schema, visited = new Set()) { // Interfaces encountered that are field return types need to be revisited to add their implementations const revisit = new Map(); // Navigate all types starting with pre-queued types (root types) while (queue.length) { const typeName = queue.pop(); // 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 => getDirectivesArgumentsTypeNames(schema, value))); } // 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(); 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]; typeNames.push(...getDirectivesArgumentsTypeNames(schema, arg)); return typeNames; })); } const namedType = getNamedType(field.type); queue.push(namedType.name); queue.push(...getDirectivesArgumentsTypeNames(schema, field)); // Interfaces returned on fields need to be revisited to add their implementations if (isInterfaceType(namedType) && !(namedType.name in revisit)) { revisit[namedType.name] = true; } } } queue.push(...getDirectivesArgumentsTypeNames(schema, type)); visited.add(typeName); // Mark as visited (and therefore it is used and should be kept) } } return visited; } function getDirectivesArgumentsTypeNames(schema, directableObj) { const argTypeNames = new Set(); if (directableObj.astNode?.directives) { for (const directiveNode of directableObj.astNode.directives) { const directive = schema.getDirective(directiveNode.name.value); if (directive?.args) { for (const arg of directive.args) { const argType = getNamedType(arg.type); argTypeNames.add(argType.name); } } } } if (directableObj.extensions?.['directives']) { for (const directiveName in directableObj.extensions['directives']) { const directive = schema.getDirective(directiveName); if (directive?.args) { for (const arg of directive.args) { const argType = getNamedType(arg.type); argTypeNames.add(argType.name); } } } } return [...argTypeNames]; }