/
visitSchema.ts
343 lines (307 loc) · 11.5 KB
/
visitSchema.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import {
GraphQLInterfaceType,
GraphQLObjectType,
GraphQLSchema,
isNamedType,
GraphQLType,
GraphQLNamedType,
GraphQLInputField,
isSchema,
isObjectType,
isInterfaceType,
isInputObjectType,
isScalarType,
isUnionType,
isEnumType,
isInputType,
GraphQLEnumValue,
GraphQLEnumType,
} from 'graphql';
import {
VisitableSchemaType,
VisitorSelector,
VisitSchemaKind,
NamedTypeVisitor,
SchemaVisitorMap,
} from '../Interfaces';
import updateEachKey from '../esUtils/updateEachKey';
import keyValMap from '../esUtils/keyValMap';
import { healSchema } from './heal';
import { SchemaVisitor } from './SchemaVisitor';
// Generic function for visiting GraphQLSchema objects.
export function visitSchema(
schema: GraphQLSchema,
// To accommodate as many different visitor patterns as possible, the
// visitSchema function does not simply accept a single instance of the
// SchemaVisitor class, but instead accepts a function that takes the
// current VisitableSchemaType object and the name of a visitor method and
// returns an array of SchemaVisitor instances that implement the visitor
// method and have an interest in handling the given VisitableSchemaType
// object. In the simplest case, this function can always return an array
// containing a single visitor object, without even looking at the type or
// methodName parameters. In other cases, this function might sometimes
// return an empty array to indicate there are no visitors that should be
// applied to the given VisitableSchemaType object. For an example of a
// visitor pattern that benefits from this abstraction, see the
// SchemaDirectiveVisitor class below.
visitorOrVisitorSelector:
| VisitorSelector
| Array<SchemaVisitor | SchemaVisitorMap>
| SchemaVisitor
| SchemaVisitorMap,
): GraphQLSchema {
const visitorSelector =
typeof visitorOrVisitorSelector === 'function'
? visitorOrVisitorSelector
: () => visitorOrVisitorSelector;
// Helper function that calls visitorSelector and applies the resulting
// visitors to the given type, with arguments [type, ...args].
function callMethod<T extends VisitableSchemaType>(
methodName: string,
type: T,
...args: Array<any>
): T | null {
let visitors = visitorSelector(type, methodName);
visitors = Array.isArray(visitors) ? visitors : [visitors];
let finalType: T | null = type;
visitors.every((visitorOrVisitorDef) => {
let newType;
if (visitorOrVisitorDef instanceof SchemaVisitor) {
newType = visitorOrVisitorDef[methodName](finalType, ...args);
} else if (
isNamedType(finalType) &&
(methodName === 'visitScalar' ||
methodName === 'visitEnum' ||
methodName === 'visitObject' ||
methodName === 'visitInputObject' ||
methodName === 'visitUnion' ||
methodName === 'visitInterface')
) {
const specifiers = getTypeSpecifiers(finalType, schema);
const typeVisitor = getVisitor(visitorOrVisitorDef, specifiers);
newType =
typeVisitor != null ? typeVisitor(finalType, schema) : undefined;
}
if (typeof newType === 'undefined') {
// Keep going without modifying type.
return true;
}
if (methodName === 'visitSchema' || isSchema(finalType)) {
throw new Error(
`Method ${methodName} cannot replace schema with ${
newType as string
}`,
);
}
if (newType === null) {
// Stop the loop and return null form callMethod, which will cause
// the type to be removed from the schema.
finalType = null;
return false;
}
// Update type to the new type returned by the visitor method, so that
// later directives will see the new type, and callMethod will return
// the final type.
finalType = newType;
return true;
});
// If there were no directives for this type object, or if all visitor
// methods returned nothing, type will be returned unmodified.
return finalType;
}
// Recursive helper function that calls any appropriate visitor methods for
// each object in the schema, then traverses the object's children (if any).
function visit<T extends VisitableSchemaType>(type: T): T | null {
if (isSchema(type)) {
// Unlike the other types, the root GraphQLSchema object cannot be
// replaced by visitor methods, because that would make life very hard
// for SchemaVisitor subclasses that rely on the original schema object.
callMethod('visitSchema', type);
const typeMap: Record<
string,
GraphQLNamedType | null
> = type.getTypeMap();
Object.entries(typeMap).forEach(([typeName, namedType]) => {
if (!typeName.startsWith('__') && namedType != null) {
// Call visit recursively to let it determine which concrete
// subclass of GraphQLNamedType we found in the type map.
// We do not use updateEachKey because we want to preserve
// deleted types in the typeMap so that other types that reference
// the deleted types can be healed.
typeMap[typeName] = visit(namedType);
}
});
return type;
}
if (isObjectType(type)) {
// Note that callMethod('visitObject', type) may not actually call any
// methods, if there are no @directive annotations associated with this
// type, or if this SchemaDirectiveVisitor subclass does not override
// the visitObject method.
const newObject = callMethod('visitObject', type);
if (newObject != null) {
visitFields(newObject);
}
return newObject;
}
if (isInterfaceType(type)) {
const newInterface = callMethod('visitInterface', type);
if (newInterface != null) {
visitFields(newInterface);
}
return newInterface;
}
if (isInputObjectType(type)) {
const newInputObject = callMethod('visitInputObject', type);
if (newInputObject != null) {
const fieldMap = newInputObject.getFields() as Record<
string,
GraphQLInputField
>;
updateEachKey(fieldMap, (field) =>
callMethod('visitInputFieldDefinition', field, {
// Since we call a different method for input object fields, we
// can't reuse the visitFields function here.
objectType: newInputObject,
}),
);
}
return newInputObject;
}
if (isScalarType(type)) {
return callMethod('visitScalar', type);
}
if (isUnionType(type)) {
return callMethod('visitUnion', type);
}
if (isEnumType(type)) {
let newEnum = callMethod('visitEnum', type);
if (newEnum != null) {
const newValues: Array<GraphQLEnumValue> = [];
updateEachKey(newEnum.getValues(), (value) => {
const newValue = callMethod('visitEnumValue', value, {
enumType: newEnum,
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (newValue) {
newValues.push(newValue);
}
});
// Recreate the enum type if any of the values changed
const valuesUpdated = newValues.some(
(value, index) => value !== newEnum.getValues()[index],
);
if (valuesUpdated) {
newEnum = new GraphQLEnumType({
...(newEnum as GraphQLEnumType).toConfig(),
values: keyValMap(
newValues,
(value) => value.name,
(value) => ({
value: value.value,
deprecationReason: value.deprecationReason,
description: value.description,
astNode: value.astNode,
}),
),
}) as GraphQLEnumType & T;
}
}
return newEnum;
}
throw new Error(`Unexpected schema type: ${(type as unknown) as string}`);
}
function visitFields(type: GraphQLObjectType | GraphQLInterfaceType) {
updateEachKey(type.getFields(), (field) => {
// It would be nice if we could call visit(field) recursively here, but
// GraphQLField is merely a type, not a value that can be detected using
// an instanceof check, so we have to visit the fields in this lexical
// context, so that TypeScript can validate the call to
// visitFieldDefinition.
const newField = callMethod('visitFieldDefinition', field, {
// While any field visitor needs a reference to the field object, some
// field visitors may also need to know the enclosing (parent) type,
// perhaps to determine if the parent is a GraphQLObjectType or a
// GraphQLInterfaceType. To obtain a reference to the parent, a
// visitor method can have a second parameter, which will be an object
// with an .objectType property referring to the parent.
objectType: type,
});
if (newField.args != null) {
updateEachKey(newField.args, (arg) =>
callMethod('visitArgumentDefinition', arg, {
// Like visitFieldDefinition, visitArgumentDefinition takes a
// second parameter that provides additional context, namely the
// parent .field and grandparent .objectType. Remember that the
// current GraphQLSchema is always available via this.schema.
field: newField,
objectType: type,
}),
);
}
return newField;
});
}
visit(schema);
// Automatically update any references to named schema types replaced
// during the traversal, so implementors don't have to worry about that.
healSchema(schema);
// Return schema for convenience, even though schema parameter has all updated types.
return schema;
}
function getTypeSpecifiers(
type: GraphQLType,
schema: GraphQLSchema,
): Array<VisitSchemaKind> {
const specifiers = [VisitSchemaKind.TYPE];
if (isObjectType(type)) {
specifiers.push(
VisitSchemaKind.COMPOSITE_TYPE,
VisitSchemaKind.OBJECT_TYPE,
);
const query = schema.getQueryType();
const mutation = schema.getMutationType();
const subscription = schema.getSubscriptionType();
if (type === query) {
specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.QUERY);
} else if (type === mutation) {
specifiers.push(VisitSchemaKind.ROOT_OBJECT, VisitSchemaKind.MUTATION);
} else if (type === subscription) {
specifiers.push(
VisitSchemaKind.ROOT_OBJECT,
VisitSchemaKind.SUBSCRIPTION,
);
}
} else if (isInputType(type)) {
specifiers.push(VisitSchemaKind.INPUT_OBJECT_TYPE);
} else if (isInterfaceType(type)) {
specifiers.push(
VisitSchemaKind.COMPOSITE_TYPE,
VisitSchemaKind.ABSTRACT_TYPE,
VisitSchemaKind.INTERFACE_TYPE,
);
} else if (isUnionType(type)) {
specifiers.push(
VisitSchemaKind.COMPOSITE_TYPE,
VisitSchemaKind.ABSTRACT_TYPE,
VisitSchemaKind.UNION_TYPE,
);
} else if (isEnumType(type)) {
specifiers.push(VisitSchemaKind.ENUM_TYPE);
} else if (isScalarType(type)) {
specifiers.push(VisitSchemaKind.SCALAR_TYPE);
}
return specifiers;
}
function getVisitor(
visitorDef: SchemaVisitorMap,
specifiers: Array<VisitSchemaKind>,
): NamedTypeVisitor | null {
let typeVisitor: NamedTypeVisitor | undefined;
const stack = [...specifiers];
while (!typeVisitor && stack.length > 0) {
const next = stack.pop();
typeVisitor = visitorDef[next] as NamedTypeVisitor;
}
return typeVisitor != null ? typeVisitor : null;
}