Skip to content

Commit

Permalink
[RFC] schema enforced interface adherance.
Browse files Browse the repository at this point in the history
This adds an invariant step in the Schema ctor for enforcing that all Object types in the Schema properly adhere to the Interface types they claim to implement.

This effectively implements http://facebook.github.io/graphql/#sec-Object-type-validation
  • Loading branch information
leebyron committed Aug 14, 2015
1 parent d3188d3 commit 73401ed
Show file tree
Hide file tree
Showing 4 changed files with 387 additions and 22 deletions.
285 changes: 285 additions & 0 deletions src/type/__tests__/validation.js
Expand Up @@ -1175,3 +1175,288 @@ describe('Type System: NonNull must accept GraphQL types', () => {
});

});


describe('Objects must adhere to Interface they implement', () => {

it('accepts an Object which implements an Interface', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString }
}
}
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString }
}
}
}
});

return schemaWithFieldType(AnotherObject);
}).not.to.throw();
});

it('accepts an Object which implements an Interface along with more fields', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
}
}
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
}
},
anotherfield: { type: GraphQLString }
}
});

return schemaWithFieldType(AnotherObject);
}).not.to.throw();
});

it('rejects an Object which implements an Interface field along with more arguments', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
}
}
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
anotherInput: { type: GraphQLString },
}
}
}
});

return schemaWithFieldType(AnotherObject);
}).to.throw(
'AnotherInterface.field does not define argument "anotherInput" but ' +
'AnotherObject.field provides it.'
);
});

it('rejects an Object missing an Interface field', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
}
}
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
anotherfield: { type: GraphQLString }
}
});

return schemaWithFieldType(AnotherObject);
}).to.throw(
'"AnotherInterface" expects field "field" but ' +
'"AnotherObject" does not provide it.'
);
});

it('rejects an Object with an incorrectly typed Interface field', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
}
}
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: {
type: SomeScalarType,
args: {
input: { type: GraphQLString },
}
}
}
});

return schemaWithFieldType(AnotherObject);
}).to.throw(
'AnotherInterface.field expects type "String" but ' +
'AnotherObject.field provides type "SomeScalar".'
);
});

it('rejects an Object missing an Interface argument', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
}
}
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: {
type: GraphQLString,
}
}
});

return schemaWithFieldType(AnotherObject);
}).to.throw(
'AnotherInterface.field expects argument "input" but ' +
'AnotherObject.field does not provide it.'
);
});

it('rejects an Object with an incorrectly typed Interface argument', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: {
type: GraphQLString,
args: {
input: { type: GraphQLString },
}
}
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: {
type: GraphQLString,
args: {
input: { type: SomeScalarType },
}
}
}
});

return schemaWithFieldType(AnotherObject);
}).to.throw(
'AnotherInterface.field(input:) expects type "String" but ' +
'AnotherObject.field(input:) provides type "SomeScalar".'
);
});

it('accepts an Object with an equivalently modified Interface field type', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: { type: new GraphQLNonNull(new GraphQLList(GraphQLString)) }
}
});

return schemaWithFieldType(AnotherObject);
}).not.to.throw();
});

it('rejects an Object with a differently modified Interface field type', () => {
expect(() => {
var AnotherInterface = new GraphQLInterfaceType({
name: 'AnotherInterface',
resolveType: () => null,
fields: {
field: { type: GraphQLString }
}
});

var AnotherObject = new GraphQLObjectType({
name: 'AnotherObject',
interfaces: [ AnotherInterface ],
fields: {
field: { type: new GraphQLNonNull(GraphQLString) }
}
});

return schemaWithFieldType(AnotherObject);
}).to.throw(
'AnotherInterface.field expects type "String" but ' +
'AnotherObject.field provides type "String!".'
);
});

});
81 changes: 81 additions & 0 deletions src/type/schema.js
Expand Up @@ -66,6 +66,16 @@ export class GraphQLSchema {
this.getMutationType(),
__Schema
].reduce(typeMapReducer, {});

// Enforce correct interface implementations
Object.keys(this._typeMap).forEach(typeName => {
var type = this._typeMap[typeName];
if (type instanceof GraphQLObjectType) {
type.getInterfaces().forEach(
iface => assertObjectImplementsInterface(type, iface)
);
}
});
}

getQueryType(): GraphQLObjectType {
Expand Down Expand Up @@ -147,3 +157,74 @@ function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap {

return reducedMap;
}

function assertObjectImplementsInterface(
object: GraphQLObjectType,
iface: GraphQLInterfaceType
): void {
var objectFieldMap = object.getFields();
var ifaceFieldMap = iface.getFields();

// Assert each interface field is implemented.
Object.keys(ifaceFieldMap).forEach(fieldName => {
var objectField = objectFieldMap[fieldName];
var ifaceField = ifaceFieldMap[fieldName];

// Assert interface field exists on object.
invariant(
objectField,
`"${iface}" expects field "${fieldName}" but "${object}" does not ` +
`provide it.`
);

// Assert interface field type matches object field type. (invariant)
invariant(
isEqualType(ifaceField.type, objectField.type),
`${iface}.${fieldName} expects type "${ifaceField.type}" but ` +
`${object}.${fieldName} provides type "${objectField.type}".`
);

// Assert each interface field arg is implemented.
ifaceField.args.forEach(ifaceArg => {
var argName = ifaceArg.name;
var objectArg = find(objectField.args, arg => arg.name === argName);

// Assert interface field arg exists on object field.
invariant(
objectArg,
`${iface}.${fieldName} expects argument "${argName}" but ` +
`${object}.${fieldName} does not provide it.`
);

// Assert interface field arg type matches object field arg type.
// (invariant)
invariant(
isEqualType(ifaceArg.type, objectArg.type),
`${iface}.${fieldName}(${argName}:) expects type "${ifaceArg.type}" ` +
`but ${object}.${fieldName}(${argName}:) provides ` +
`type "${objectArg.type}".`
);
});

// Assert argument set invariance.
objectField.args.forEach(objectArg => {
var argName = objectArg.name;
var ifaceArg = find(ifaceField.args, arg => arg.name === argName);
invariant(
ifaceArg,
`${iface}.${fieldName} does not define argument "${argName}" but ` +
`${object}.${fieldName} provides it.`
);
});
});
}

function isEqualType(typeA: GraphQLType, typeB: GraphQLType): boolean {
if (typeA instanceof GraphQLNonNull && typeB instanceof GraphQLNonNull) {
return isEqualType(typeA.ofType, typeB.ofType);
}
if (typeA instanceof GraphQLList && typeB instanceof GraphQLList) {
return isEqualType(typeA.ofType, typeB.ofType);
}
return typeA === typeB;
}

0 comments on commit 73401ed

Please sign in to comment.