Skip to content

Commit 01f10a7

Browse files
committed
Reconcile GraphQLObjectType .name properties with schema.getTypeMap().
This automatically guarantees the following invariant holds for all named types in the schema: schema.getType(name).name === name This reconciliation falls into the category of "healing" because it doesn't require any input from the implementor of the schema visitor, and strictly improves the consistency of the final schema.
1 parent a07ca3f commit 01f10a7

File tree

2 files changed

+105
-3
lines changed

2 files changed

+105
-3
lines changed

src/schemaVisitor.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ export function visitSchema(
289289
return schema;
290290
}
291291

292+
type NamedTypeMap = {
293+
[key: string]: GraphQLNamedType;
294+
};
295+
292296
// Update any references to named schema types that disagree with the named
293297
// types found in schema.getTypeMap().
294298
export function healSchema(schema: GraphQLSchema) {
@@ -297,12 +301,40 @@ export function healSchema(schema: GraphQLSchema) {
297301

298302
function heal(type: VisitableSchemaType) {
299303
if (type instanceof GraphQLSchema) {
300-
each(type.getTypeMap(), (namedType, typeName) => {
301-
if (! typeName.startsWith('__')) {
302-
heal(namedType);
304+
const originalTypeMap: NamedTypeMap = type.getTypeMap();
305+
const actualNamedTypeMap: NamedTypeMap = Object.create(null);
306+
307+
// If any of the .name properties of the GraphQLNamedType objects in
308+
// schema.getTypeMap() have changed, the keys of the type map need to
309+
// be updated accordingly.
310+
311+
each(originalTypeMap, (namedType, typeName) => {
312+
if (typeName.startsWith('__')) {
313+
return;
303314
}
315+
316+
const actualName = namedType.name;
317+
if (actualName.startsWith('__')) {
318+
return;
319+
}
320+
321+
if (hasOwn.call(actualNamedTypeMap, actualName)) {
322+
throw new Error(`Duplicate schema type name ${actualName}`);
323+
}
324+
325+
actualNamedTypeMap[actualName] = namedType;
326+
327+
// Note: we are deliberately leaving namedType in the schema by its
328+
// original name (which might be different from actualName), so that
329+
// references by that name can be healed.
330+
});
331+
332+
// Now add back every named type by its actual name.
333+
each(actualNamedTypeMap, (namedType, typeName) => {
334+
originalTypeMap[typeName] = namedType;
304335
});
305336

337+
// Directive declaration argument types can refer to named types.
306338
each(type.getDirectives(), decl => {
307339
if (decl.args) {
308340
each(decl.args, arg => {
@@ -311,6 +343,21 @@ export function healSchema(schema: GraphQLSchema) {
311343
}
312344
});
313345

346+
each(originalTypeMap, (namedType, typeName) => {
347+
if (! typeName.startsWith('__')) {
348+
heal(namedType);
349+
}
350+
});
351+
352+
updateEachKey(originalTypeMap, (namedType, typeName) => {
353+
// Dangling references to renamed types should remain in the schema
354+
// during healing, but must be removed now, so that the following
355+
// invariant holds for all names: schema.getType(name).name === name
356+
if (! hasOwn.call(actualNamedTypeMap, typeName)) {
357+
return null;
358+
}
359+
});
360+
314361
} else if (type instanceof GraphQLObjectType) {
315362
healFields(type);
316363
each(type.getInterfaces(), iface => heal(iface));

src/test/testDirectives.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
GraphQLNonNull,
2828
GraphQLList,
2929
GraphQLUnionType,
30+
GraphQLInt,
3031
} from 'graphql';
3132

3233
const typeDefs = `
@@ -1065,6 +1066,12 @@ describe('@directives', () => {
10651066
}
10661067
});
10671068
assert.strictEqual(found, true);
1069+
1070+
// Make sure that the Person type was actually removed.
1071+
assert.strictEqual(
1072+
typeof schema.getType('Person'),
1073+
'undefined'
1074+
);
10681075
});
10691076

10701077
it('can remove enum values', () => {
@@ -1093,4 +1100,52 @@ describe('@directives', () => {
10931100
['DOG_YEARS', 'PERSON_YEARS']
10941101
);
10951102
});
1103+
1104+
it('can swap names of GraphQLNamedType objects', () => {
1105+
const schema = makeExecutableSchema({
1106+
typeDefs: `
1107+
type Query {
1108+
people: [Person]
1109+
}
1110+
1111+
type Person @rename(to: "Human") {
1112+
heightInInches: Int
1113+
}
1114+
1115+
scalar Date
1116+
1117+
type Human @rename(to: "Person") {
1118+
born: Date
1119+
}`,
1120+
1121+
directives: {
1122+
rename: class extends SchemaDirectiveVisitor {
1123+
public visitObject(object: GraphQLObjectType) {
1124+
object.name = this.args.to;
1125+
}
1126+
}
1127+
}
1128+
});
1129+
1130+
const Human = schema.getType('Human') as GraphQLObjectType;
1131+
assert.strictEqual(Human.name, 'Human');
1132+
assert.strictEqual(
1133+
Human.getFields().heightInInches.type,
1134+
GraphQLInt,
1135+
);
1136+
1137+
const Person = schema.getType('Person') as GraphQLObjectType;
1138+
assert.strictEqual(Person.name, 'Person');
1139+
assert.strictEqual(
1140+
Person.getFields().born.type,
1141+
schema.getType('Date') as GraphQLScalarType,
1142+
);
1143+
1144+
const Query = schema.getType('Query') as GraphQLObjectType;
1145+
const peopleType = Query.getFields().people.type as GraphQLList<GraphQLObjectType>;
1146+
assert.strictEqual(
1147+
peopleType.ofType,
1148+
Human
1149+
);
1150+
});
10961151
});

0 commit comments

Comments
 (0)