Skip to content

Commit

Permalink
Reconcile GraphQLObjectType .name properties with schema.getTypeMap().
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
benjamn committed Mar 14, 2018
1 parent a07ca3f commit 01f10a7
Show file tree
Hide file tree
Showing 2 changed files with 105 additions and 3 deletions.
53 changes: 50 additions & 3 deletions src/schemaVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ export function visitSchema(
return schema;
}

type NamedTypeMap = {
[key: string]: GraphQLNamedType;
};

// Update any references to named schema types that disagree with the named
// types found in schema.getTypeMap().
export function healSchema(schema: GraphQLSchema) {
Expand All @@ -297,12 +301,40 @@ export function healSchema(schema: GraphQLSchema) {

function heal(type: VisitableSchemaType) {
if (type instanceof GraphQLSchema) {
each(type.getTypeMap(), (namedType, typeName) => {
if (! typeName.startsWith('__')) {
heal(namedType);
const originalTypeMap: NamedTypeMap = type.getTypeMap();
const actualNamedTypeMap: NamedTypeMap = Object.create(null);

// If any of the .name properties of the GraphQLNamedType objects in
// schema.getTypeMap() have changed, the keys of the type map need to
// be updated accordingly.

each(originalTypeMap, (namedType, typeName) => {
if (typeName.startsWith('__')) {
return;
}

const actualName = namedType.name;
if (actualName.startsWith('__')) {
return;
}

if (hasOwn.call(actualNamedTypeMap, actualName)) {
throw new Error(`Duplicate schema type name ${actualName}`);
}

actualNamedTypeMap[actualName] = namedType;

// Note: we are deliberately leaving namedType in the schema by its
// original name (which might be different from actualName), so that
// references by that name can be healed.
});

// Now add back every named type by its actual name.
each(actualNamedTypeMap, (namedType, typeName) => {
originalTypeMap[typeName] = namedType;
});

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

each(originalTypeMap, (namedType, typeName) => {
if (! typeName.startsWith('__')) {
heal(namedType);
}
});

updateEachKey(originalTypeMap, (namedType, typeName) => {
// Dangling references to renamed types should remain in the schema
// during healing, but must be removed now, so that the following
// invariant holds for all names: schema.getType(name).name === name
if (! hasOwn.call(actualNamedTypeMap, typeName)) {
return null;
}
});

} else if (type instanceof GraphQLObjectType) {
healFields(type);
each(type.getInterfaces(), iface => heal(iface));
Expand Down
55 changes: 55 additions & 0 deletions src/test/testDirectives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
GraphQLNonNull,
GraphQLList,
GraphQLUnionType,
GraphQLInt,
} from 'graphql';

const typeDefs = `
Expand Down Expand Up @@ -1065,6 +1066,12 @@ describe('@directives', () => {
}
});
assert.strictEqual(found, true);

// Make sure that the Person type was actually removed.
assert.strictEqual(
typeof schema.getType('Person'),
'undefined'
);
});

it('can remove enum values', () => {
Expand Down Expand Up @@ -1093,4 +1100,52 @@ describe('@directives', () => {
['DOG_YEARS', 'PERSON_YEARS']
);
});

it('can swap names of GraphQLNamedType objects', () => {
const schema = makeExecutableSchema({
typeDefs: `
type Query {
people: [Person]
}
type Person @rename(to: "Human") {
heightInInches: Int
}
scalar Date
type Human @rename(to: "Person") {
born: Date
}`,

directives: {
rename: class extends SchemaDirectiveVisitor {
public visitObject(object: GraphQLObjectType) {
object.name = this.args.to;
}
}
}
});

const Human = schema.getType('Human') as GraphQLObjectType;
assert.strictEqual(Human.name, 'Human');
assert.strictEqual(
Human.getFields().heightInInches.type,
GraphQLInt,
);

const Person = schema.getType('Person') as GraphQLObjectType;
assert.strictEqual(Person.name, 'Person');
assert.strictEqual(
Person.getFields().born.type,
schema.getType('Date') as GraphQLScalarType,
);

const Query = schema.getType('Query') as GraphQLObjectType;
const peopleType = Query.getFields().people.type as GraphQLList<GraphQLObjectType>;
assert.strictEqual(
peopleType.ofType,
Human
);
});
});

0 comments on commit 01f10a7

Please sign in to comment.