Skip to content

Commit

Permalink
validateSchema: validate Input Objects self-references
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanGoncharov committed Jul 19, 2018
1 parent f373fed commit 615c05a
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 0 deletions.
114 changes: 114 additions & 0 deletions src/type/__tests__/validation-test.js
Expand Up @@ -728,6 +728,120 @@ describe('Type System: Input Objects must have fields', () => {
]);
});

it('accepts an Input Object with breakable circular reference', () => {
const schema = buildSchema(`
type Query {
field(arg: SomeInputObject): String
}
input SomeInputObject {
self: SomeInputObject
arrayOfSelf: [SomeInputObject]
nonNullArrayOfSelf: [SomeInputObject]!
nonNullArrayOfNonNullSelf: [SomeInputObject!]!
intermediateSelf: AnotherInputObject
}
input AnotherInputObject {
parent: SomeInputObject
}
`);

expect(validateSchema(schema)).to.deep.equal([]);
});

it('rejects an Input Object with non-breakable circular reference', () => {
const schema = buildSchema(`
type Query {
field(arg: SomeInputObject): String
}
input SomeInputObject {
nonNullSelf: SomeInputObject!
}
`);

expect(validateSchema(schema)).to.deep.equal([
{
message:
'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "nonNullSelf".',
locations: [{ line: 7, column: 9 }],
},
]);
});

it('rejects Input Objects with non-breakable circular reference spread across them', () => {
const schema = buildSchema(`
type Query {
field(arg: SomeInputObject): String
}
input SomeInputObject {
startLoop: AnotherInputObject!
}
input AnotherInputObject {
nextInLoop: YetAnotherInputObject!
}
input YetAnotherInputObject {
closeLoop: SomeInputObject!
}
`);

expect(validateSchema(schema)).to.deep.equal([
{
message:
'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.nextInLoop.closeLoop".',
locations: [
{ line: 7, column: 9 },
{ line: 11, column: 9 },
{ line: 15, column: 9 },
],
},
]);
});

it('rejects Input Objects with multiple non-breakable circular reference', () => {
const schema = buildSchema(`
type Query {
field(arg: SomeInputObject): String
}
input SomeInputObject {
startLoop: AnotherInputObject!
}
input AnotherInputObject {
closeLoop: SomeInputObject!
startSecondLoop: YetAnotherInputObject!
}
input YetAnotherInputObject {
closeSecondLoop: AnotherInputObject!
nonNullSelf: YetAnotherInputObject!
}
`);

expect(validateSchema(schema)).to.deep.equal([
{
message:
'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.closeLoop".',
locations: [{ line: 7, column: 9 }, { line: 11, column: 9 }],
},
{
message:
'Cannot reference Input Object "AnotherInputObject" within itself through a series of non-null fields: "startSecondLoop.closeSecondLoop".',
locations: [{ line: 12, column: 9 }, { line: 16, column: 9 }],
},
{
message:
'Cannot reference Input Object "YetAnotherInputObject" within itself through a series of non-null fields: "nonNullSelf".',
locations: [{ line: 17, column: 9 }],
},
]);
});

it('rejects an Input Object type with incorrectly typed fields', () => {
const schema = buildSchema(`
type Query {
Expand Down
59 changes: 59 additions & 0 deletions src/type/validate.js
Expand Up @@ -228,6 +228,9 @@ function validateName(
}

function validateTypes(context: SchemaValidationContext): void {
const validateInputObjectCircularRefs = createInputObjectCircularRefsValidator(
context,
);
const typeMap = context.schema.getTypeMap();
for (const type of objectValues(typeMap)) {
// Ensure all provided types are in fact GraphQL type.
Expand Down Expand Up @@ -262,6 +265,9 @@ function validateTypes(context: SchemaValidationContext): void {
} else if (isInputObjectType(type)) {
// Ensure Input Object fields are valid.
validateInputFields(context, type);

// Ensure Input Objects do not contain non-nullable circular references
validateInputObjectCircularRefs(type);
}
}
}
Expand Down Expand Up @@ -554,6 +560,59 @@ function validateInputFields(
}
}

function createInputObjectCircularRefsValidator(
context: SchemaValidationContext,
) {
// Modified copy of algorithm from 'src/validation/rules/NoFragmentCycles.js'.
// Tracks already visited types to maintain O(N) and to ensure that cycles
// are not redundantly reported.
const visitedTypes = Object.create(null);

// Array of types nodes used to produce meaningful errors
const fieldPath = [];

// Position in the type path
const fieldPathIndexByTypeName = Object.create(null);

return detectCycleRecursive;

// This does a straight-forward DFS to find cycles.
// It does not terminate when a cycle was found but continues to explore
// the graph to find all possible cycles.
function detectCycleRecursive(inputObj: GraphQLInputObjectType) {
if (visitedTypes[inputObj.name]) {
return;
}

visitedTypes[inputObj.name] = true;
fieldPathIndexByTypeName[inputObj.name] = fieldPath.length;

const fields = objectValues(inputObj.getFields());
for (const field of fields) {
if (isNonNullType(field.type) && isInputObjectType(field.type.ofType)) {
const fieldType = field.type.ofType;
const cycleIndex = fieldPathIndexByTypeName[fieldType.name];

fieldPath.push(field);
if (cycleIndex === undefined) {
detectCycleRecursive(fieldType);
} else {
const cyclePath = fieldPath.slice(cycleIndex);
const fieldNames = cyclePath.map(fieldObj => fieldObj.name);
context.reportError(
`Cannot reference Input Object "${fieldType.name}" within itself ` +
`through a series of non-null fields: "${fieldNames.join('.')}".`,
cyclePath.map(fieldObj => fieldObj.astNode),
);
}
fieldPath.pop();
}
}

fieldPathIndexByTypeName[inputObj.name] = undefined;
}
}

type SDLDefinedObject<T, K> = {
+astNode: ?T,
+extensionASTNodes?: ?$ReadOnlyArray<K>,
Expand Down

0 comments on commit 615c05a

Please sign in to comment.