prune.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import { getNamedType, isObjectType, isInterfaceType, isUnionType, isInputObjectType, isSpecifiedScalarType, isScalarType, isEnumType, } from 'graphql';
  2. import { mapSchema } from './mapSchema.js';
  3. import { MapperKind } from './Interfaces.js';
  4. import { getRootTypes } from './rootTypes.js';
  5. import { 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. */
  11. export 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. }
  70. function 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. }
  78. function 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. }
  151. function 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. }