Skip to content

Commit 408bcda

Browse files
committed
validateSchema: validate Input Objects self-references
1 parent 71d96d7 commit 408bcda

File tree

2 files changed

+174
-0
lines changed

2 files changed

+174
-0
lines changed

src/type/__tests__/validation-test.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,120 @@ describe('Type System: Input Objects must have fields', () => {
736736
]);
737737
});
738738

739+
it('accepts an Input Object with breakable circular reference', () => {
740+
const schema = buildSchema(`
741+
type Query {
742+
field(arg: SomeInputObject): String
743+
}
744+
745+
input SomeInputObject {
746+
self: SomeInputObject
747+
arrayOfSelf: [SomeInputObject]
748+
nonNullArrayOfSelf: [SomeInputObject]!
749+
nonNullArrayOfNonNullSelf: [SomeInputObject!]!
750+
intermediateSelf: AnotherInputObject
751+
}
752+
753+
input AnotherInputObject {
754+
parent: SomeInputObject
755+
}
756+
`);
757+
758+
expect(validateSchema(schema)).to.deep.equal([]);
759+
});
760+
761+
it('rejects an Input Object with non-breakable circular reference', () => {
762+
const schema = buildSchema(`
763+
type Query {
764+
field(arg: SomeInputObject): String
765+
}
766+
767+
input SomeInputObject {
768+
nonNullSelf: SomeInputObject!
769+
}
770+
`);
771+
772+
expect(validateSchema(schema)).to.deep.equal([
773+
{
774+
message:
775+
'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "nonNullSelf".',
776+
locations: [{ line: 7, column: 9 }],
777+
},
778+
]);
779+
});
780+
781+
it('rejects Input Objects with non-breakable circular reference spread across them', () => {
782+
const schema = buildSchema(`
783+
type Query {
784+
field(arg: SomeInputObject): String
785+
}
786+
787+
input SomeInputObject {
788+
startLoop: AnotherInputObject!
789+
}
790+
791+
input AnotherInputObject {
792+
nextInLoop: YetAnotherInputObject!
793+
}
794+
795+
input YetAnotherInputObject {
796+
closeLoop: SomeInputObject!
797+
}
798+
`);
799+
800+
expect(validateSchema(schema)).to.deep.equal([
801+
{
802+
message:
803+
'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.nextInLoop.closeLoop".',
804+
locations: [
805+
{ line: 7, column: 9 },
806+
{ line: 11, column: 9 },
807+
{ line: 15, column: 9 },
808+
],
809+
},
810+
]);
811+
});
812+
813+
it('rejects Input Objects with multiple non-breakable circular reference', () => {
814+
const schema = buildSchema(`
815+
type Query {
816+
field(arg: SomeInputObject): String
817+
}
818+
819+
input SomeInputObject {
820+
startLoop: AnotherInputObject!
821+
}
822+
823+
input AnotherInputObject {
824+
closeLoop: SomeInputObject!
825+
startSecondLoop: YetAnotherInputObject!
826+
}
827+
828+
input YetAnotherInputObject {
829+
closeSecondLoop: AnotherInputObject!
830+
nonNullSelf: YetAnotherInputObject!
831+
}
832+
`);
833+
834+
expect(validateSchema(schema)).to.deep.equal([
835+
{
836+
message:
837+
'Cannot reference Input Object "SomeInputObject" within itself through a series of non-null fields: "startLoop.closeLoop".',
838+
locations: [{ line: 7, column: 9 }, { line: 11, column: 9 }],
839+
},
840+
{
841+
message:
842+
'Cannot reference Input Object "AnotherInputObject" within itself through a series of non-null fields: "startSecondLoop.closeSecondLoop".',
843+
locations: [{ line: 12, column: 9 }, { line: 16, column: 9 }],
844+
},
845+
{
846+
message:
847+
'Cannot reference Input Object "YetAnotherInputObject" within itself through a series of non-null fields: "nonNullSelf".',
848+
locations: [{ line: 17, column: 9 }],
849+
},
850+
]);
851+
});
852+
739853
it('rejects an Input Object type with incorrectly typed fields', () => {
740854
const schema = buildSchema(`
741855
type Query {

src/type/validate.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
isEnumType,
2424
isInputObjectType,
2525
isNamedType,
26+
isNonNullType,
2627
isInputType,
2728
isOutputType,
2829
isRequiredArgument,
@@ -221,6 +222,9 @@ function validateName(
221222
}
222223

223224
function validateTypes(context: SchemaValidationContext): void {
225+
const validateInputObjectCircularRefs = createInputObjectCircularRefsValidator(
226+
context,
227+
);
224228
const typeMap = context.schema.getTypeMap();
225229
for (const type of objectValues(typeMap)) {
226230
// Ensure all provided types are in fact GraphQL type.
@@ -255,6 +259,9 @@ function validateTypes(context: SchemaValidationContext): void {
255259
} else if (isInputObjectType(type)) {
256260
// Ensure Input Object fields are valid.
257261
validateInputFields(context, type);
262+
263+
// Ensure Input Objects do not contain non-nullable circular references
264+
validateInputObjectCircularRefs(type);
258265
}
259266
}
260267
}
@@ -525,6 +532,59 @@ function validateInputFields(
525532
}
526533
}
527534

535+
function createInputObjectCircularRefsValidator(
536+
context: SchemaValidationContext,
537+
) {
538+
// Modified copy of algorithm from 'src/validation/rules/NoFragmentCycles.js'.
539+
// Tracks already visited types to maintain O(N) and to ensure that cycles
540+
// are not redundantly reported.
541+
const visitedTypes = Object.create(null);
542+
543+
// Array of types nodes used to produce meaningful errors
544+
const fieldPath = [];
545+
546+
// Position in the type path
547+
const fieldPathIndexByTypeName = Object.create(null);
548+
549+
return detectCycleRecursive;
550+
551+
// This does a straight-forward DFS to find cycles.
552+
// It does not terminate when a cycle was found but continues to explore
553+
// the graph to find all possible cycles.
554+
function detectCycleRecursive(inputObj: GraphQLInputObjectType) {
555+
if (visitedTypes[inputObj.name]) {
556+
return;
557+
}
558+
559+
visitedTypes[inputObj.name] = true;
560+
fieldPathIndexByTypeName[inputObj.name] = fieldPath.length;
561+
562+
const fields = objectValues(inputObj.getFields());
563+
for (const field of fields) {
564+
if (isNonNullType(field.type) && isInputObjectType(field.type.ofType)) {
565+
const fieldType = field.type.ofType;
566+
const cycleIndex = fieldPathIndexByTypeName[fieldType.name];
567+
568+
fieldPath.push(field);
569+
if (cycleIndex === undefined) {
570+
detectCycleRecursive(fieldType);
571+
} else {
572+
const cyclePath = fieldPath.slice(cycleIndex);
573+
const fieldNames = cyclePath.map(fieldObj => fieldObj.name);
574+
context.reportError(
575+
`Cannot reference Input Object "${fieldType.name}" within itself ` +
576+
`through a series of non-null fields: "${fieldNames.join('.')}".`,
577+
cyclePath.map(fieldObj => fieldObj.astNode),
578+
);
579+
}
580+
fieldPath.pop();
581+
}
582+
}
583+
584+
fieldPathIndexByTypeName[inputObj.name] = undefined;
585+
}
586+
}
587+
528588
type SDLDefinedObject<T, K> = {
529589
+astNode: ?T,
530590
+extensionASTNodes?: ?$ReadOnlyArray<K>,

0 commit comments

Comments
 (0)