heal.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import { GraphQLList, GraphQLNonNull, isNamedType, isObjectType, isInterfaceType, isUnionType, isInputObjectType, isLeafType, isListType, isNonNullType, } from 'graphql';
  2. // Update any references to named schema types that disagree with the named
  3. // types found in schema.getTypeMap().
  4. //
  5. // healSchema and its callers (visitSchema/visitSchemaDirectives) all modify the schema in place.
  6. // Therefore, private variables (such as the stored implementation map and the proper root types)
  7. // are not updated.
  8. //
  9. // If this causes issues, the schema could be more aggressively healed as follows:
  10. //
  11. // healSchema(schema);
  12. // const config = schema.toConfig()
  13. // const healedSchema = new GraphQLSchema({
  14. // ...config,
  15. // query: schema.getType('<desired new root query type name>'),
  16. // mutation: schema.getType('<desired new root mutation type name>'),
  17. // subscription: schema.getType('<desired new root subscription type name>'),
  18. // });
  19. //
  20. // One can then also -- if necessary -- assign the correct private variables to the initial schema
  21. // as follows:
  22. // Object.assign(schema, healedSchema);
  23. //
  24. // These steps are not taken automatically to preserve backwards compatibility with graphql-tools v4.
  25. // See https://github.com/ardatan/graphql-tools/issues/1462
  26. //
  27. // They were briefly taken in v5, but can now be phased out as they were only required when other
  28. // areas of the codebase were using healSchema and visitSchema more extensively.
  29. //
  30. export function healSchema(schema) {
  31. healTypes(schema.getTypeMap(), schema.getDirectives());
  32. return schema;
  33. }
  34. export function healTypes(originalTypeMap, directives) {
  35. const actualNamedTypeMap = Object.create(null);
  36. // If any of the .name properties of the GraphQLNamedType objects in
  37. // schema.getTypeMap() have changed, the keys of the type map need to
  38. // be updated accordingly.
  39. for (const typeName in originalTypeMap) {
  40. const namedType = originalTypeMap[typeName];
  41. if (namedType == null || typeName.startsWith('__')) {
  42. continue;
  43. }
  44. const actualName = namedType.name;
  45. if (actualName.startsWith('__')) {
  46. continue;
  47. }
  48. if (actualNamedTypeMap[actualName] != null) {
  49. console.warn(`Duplicate schema type name ${actualName} found; keeping the existing one found in the schema`);
  50. continue;
  51. }
  52. actualNamedTypeMap[actualName] = namedType;
  53. // Note: we are deliberately leaving namedType in the schema by its
  54. // original name (which might be different from actualName), so that
  55. // references by that name can be healed.
  56. }
  57. // Now add back every named type by its actual name.
  58. for (const typeName in actualNamedTypeMap) {
  59. const namedType = actualNamedTypeMap[typeName];
  60. originalTypeMap[typeName] = namedType;
  61. }
  62. // Directive declaration argument types can refer to named types.
  63. for (const decl of directives) {
  64. decl.args = decl.args.filter(arg => {
  65. arg.type = healType(arg.type);
  66. return arg.type !== null;
  67. });
  68. }
  69. for (const typeName in originalTypeMap) {
  70. const namedType = originalTypeMap[typeName];
  71. // Heal all named types, except for dangling references, kept only to redirect.
  72. if (!typeName.startsWith('__') && typeName in actualNamedTypeMap) {
  73. if (namedType != null) {
  74. healNamedType(namedType);
  75. }
  76. }
  77. }
  78. for (const typeName in originalTypeMap) {
  79. if (!typeName.startsWith('__') && !(typeName in actualNamedTypeMap)) {
  80. delete originalTypeMap[typeName];
  81. }
  82. }
  83. function healNamedType(type) {
  84. if (isObjectType(type)) {
  85. healFields(type);
  86. healInterfaces(type);
  87. return;
  88. }
  89. else if (isInterfaceType(type)) {
  90. healFields(type);
  91. if ('getInterfaces' in type) {
  92. healInterfaces(type);
  93. }
  94. return;
  95. }
  96. else if (isUnionType(type)) {
  97. healUnderlyingTypes(type);
  98. return;
  99. }
  100. else if (isInputObjectType(type)) {
  101. healInputFields(type);
  102. return;
  103. }
  104. else if (isLeafType(type)) {
  105. return;
  106. }
  107. throw new Error(`Unexpected schema type: ${type}`);
  108. }
  109. function healFields(type) {
  110. const fieldMap = type.getFields();
  111. for (const [key, field] of Object.entries(fieldMap)) {
  112. field.args
  113. .map(arg => {
  114. arg.type = healType(arg.type);
  115. return arg.type === null ? null : arg;
  116. })
  117. .filter(Boolean);
  118. field.type = healType(field.type);
  119. if (field.type === null) {
  120. delete fieldMap[key];
  121. }
  122. }
  123. }
  124. function healInterfaces(type) {
  125. if ('getInterfaces' in type) {
  126. const interfaces = type.getInterfaces();
  127. interfaces.push(...interfaces
  128. .splice(0)
  129. .map(iface => healType(iface))
  130. .filter(Boolean));
  131. }
  132. }
  133. function healInputFields(type) {
  134. const fieldMap = type.getFields();
  135. for (const [key, field] of Object.entries(fieldMap)) {
  136. field.type = healType(field.type);
  137. if (field.type === null) {
  138. delete fieldMap[key];
  139. }
  140. }
  141. }
  142. function healUnderlyingTypes(type) {
  143. const types = type.getTypes();
  144. types.push(...types
  145. .splice(0)
  146. .map(t => healType(t))
  147. .filter(Boolean));
  148. }
  149. function healType(type) {
  150. // Unwrap the two known wrapper types
  151. if (isListType(type)) {
  152. const healedType = healType(type.ofType);
  153. return healedType != null ? new GraphQLList(healedType) : null;
  154. }
  155. else if (isNonNullType(type)) {
  156. const healedType = healType(type.ofType);
  157. return healedType != null ? new GraphQLNonNull(healedType) : null;
  158. }
  159. else if (isNamedType(type)) {
  160. // If a type annotation on a field or an argument or a union member is
  161. // any `GraphQLNamedType` with a `name`, then it must end up identical
  162. // to `schema.getType(name)`, since `schema.getTypeMap()` is the source
  163. // of truth for all named schema types.
  164. // Note that new types can still be simply added by adding a field, as
  165. // the official type will be undefined, not null.
  166. const officialType = originalTypeMap[type.name];
  167. if (officialType && type !== officialType) {
  168. return officialType;
  169. }
  170. }
  171. return type;
  172. }
  173. }