UNPKG

6.7 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 (actualName in actualNamedTypeMap) {
49 throw new Error(`Duplicate schema type name ${actualName}`);
50 }
51 actualNamedTypeMap[actualName] = namedType;
52 // Note: we are deliberately leaving namedType in the schema by its
53 // original name (which might be different from actualName), so that
54 // references by that name can be healed.
55 }
56 // Now add back every named type by its actual name.
57 for (const typeName in actualNamedTypeMap) {
58 const namedType = actualNamedTypeMap[typeName];
59 originalTypeMap[typeName] = namedType;
60 }
61 // Directive declaration argument types can refer to named types.
62 for (const decl of directives) {
63 decl.args = decl.args.filter(arg => {
64 arg.type = healType(arg.type);
65 return arg.type !== null;
66 });
67 }
68 for (const typeName in originalTypeMap) {
69 const namedType = originalTypeMap[typeName];
70 // Heal all named types, except for dangling references, kept only to redirect.
71 if (!typeName.startsWith('__') && typeName in actualNamedTypeMap) {
72 if (namedType != null) {
73 healNamedType(namedType);
74 }
75 }
76 }
77 for (const typeName in originalTypeMap) {
78 if (!typeName.startsWith('__') && !(typeName in actualNamedTypeMap)) {
79 delete originalTypeMap[typeName];
80 }
81 }
82 function healNamedType(type) {
83 if (isObjectType(type)) {
84 healFields(type);
85 healInterfaces(type);
86 return;
87 }
88 else if (isInterfaceType(type)) {
89 healFields(type);
90 if ('getInterfaces' in type) {
91 healInterfaces(type);
92 }
93 return;
94 }
95 else if (isUnionType(type)) {
96 healUnderlyingTypes(type);
97 return;
98 }
99 else if (isInputObjectType(type)) {
100 healInputFields(type);
101 return;
102 }
103 else if (isLeafType(type)) {
104 return;
105 }
106 throw new Error(`Unexpected schema type: ${type}`);
107 }
108 function healFields(type) {
109 const fieldMap = type.getFields();
110 for (const [key, field] of Object.entries(fieldMap)) {
111 field.args
112 .map(arg => {
113 arg.type = healType(arg.type);
114 return arg.type === null ? null : arg;
115 })
116 .filter(Boolean);
117 field.type = healType(field.type);
118 if (field.type === null) {
119 delete fieldMap[key];
120 }
121 }
122 }
123 function healInterfaces(type) {
124 if ('getInterfaces' in type) {
125 const interfaces = type.getInterfaces();
126 interfaces.push(...interfaces
127 .splice(0)
128 .map(iface => healType(iface))
129 .filter(Boolean));
130 }
131 }
132 function healInputFields(type) {
133 const fieldMap = type.getFields();
134 for (const [key, field] of Object.entries(fieldMap)) {
135 field.type = healType(field.type);
136 if (field.type === null) {
137 delete fieldMap[key];
138 }
139 }
140 }
141 function healUnderlyingTypes(type) {
142 const types = type.getTypes();
143 types.push(...types
144 .splice(0)
145 .map(t => healType(t))
146 .filter(Boolean));
147 }
148 function healType(type) {
149 // Unwrap the two known wrapper types
150 if (isListType(type)) {
151 const healedType = healType(type.ofType);
152 return healedType != null ? new GraphQLList(healedType) : null;
153 }
154 else if (isNonNullType(type)) {
155 const healedType = healType(type.ofType);
156 return healedType != null ? new GraphQLNonNull(healedType) : null;
157 }
158 else if (isNamedType(type)) {
159 // If a type annotation on a field or an argument or a union member is
160 // any `GraphQLNamedType` with a `name`, then it must end up identical
161 // to `schema.getType(name)`, since `schema.getTypeMap()` is the source
162 // of truth for all named schema types.
163 // Note that new types can still be simply added by adding a field, as
164 // the official type will be undefined, not null.
165 const officialType = originalTypeMap[type.name];
166 if (officialType && type !== officialType) {
167 return officialType;
168 }
169 }
170 return type;
171 }
172}