UNPKG

6.78 kBJavaScriptView Raw
1import { 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//
30export function healSchema(schema) {
31 healTypes(schema.getTypeMap(), schema.getDirectives());
32 return schema;
33}
34export 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}