Skip to content

Commit 615c05a

Browse files
committed
validateSchema: validate Input Objects self-references
1 parent f373fed commit 615c05a

File tree

2 files changed

+173
-0
lines changed

2 files changed

+173
-0
lines changed

src/type/__tests__/validation-test.js

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

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

src/type/validate.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ function validateName(
228228
}
229229

230230
function validateTypes(context: SchemaValidationContext): void {
231+
const validateInputObjectCircularRefs = createInputObjectCircularRefsValidator(
232+
context,
233+
);
231234
const typeMap = context.schema.getTypeMap();
232235
for (const type of objectValues(typeMap)) {
233236
// Ensure all provided types are in fact GraphQL type.
@@ -262,6 +265,9 @@ function validateTypes(context: SchemaValidationContext): void {
262265
} else if (isInputObjectType(type)) {
263266
// Ensure Input Object fields are valid.
264267
validateInputFields(context, type);
268+
269+
// Ensure Input Objects do not contain non-nullable circular references
270+
validateInputObjectCircularRefs(type);
265271
}
266272
}
267273
}
@@ -554,6 +560,59 @@ function validateInputFields(
554560
}
555561
}
556562

563+
function createInputObjectCircularRefsValidator(
564+
context: SchemaValidationContext,
565+
) {
566+
// Modified copy of algorithm from 'src/validation/rules/NoFragmentCycles.js'.
567+
// Tracks already visited types to maintain O(N) and to ensure that cycles
568+
// are not redundantly reported.
569+
const visitedTypes = Object.create(null);
570+
571+
// Array of types nodes used to produce meaningful errors
572+
const fieldPath = [];
573+
574+
// Position in the type path
575+
const fieldPathIndexByTypeName = Object.create(null);
576+
577+
return detectCycleRecursive;
578+
579+
// This does a straight-forward DFS to find cycles.
580+
// It does not terminate when a cycle was found but continues to explore
581+
// the graph to find all possible cycles.
582+
function detectCycleRecursive(inputObj: GraphQLInputObjectType) {
583+
if (visitedTypes[inputObj.name]) {
584+
return;
585+
}
586+
587+
visitedTypes[inputObj.name] = true;
588+
fieldPathIndexByTypeName[inputObj.name] = fieldPath.length;
589+
590+
const fields = objectValues(inputObj.getFields());
591+
for (const field of fields) {
592+
if (isNonNullType(field.type) && isInputObjectType(field.type.ofType)) {
593+
const fieldType = field.type.ofType;
594+
const cycleIndex = fieldPathIndexByTypeName[fieldType.name];
595+
596+
fieldPath.push(field);
597+
if (cycleIndex === undefined) {
598+
detectCycleRecursive(fieldType);
599+
} else {
600+
const cyclePath = fieldPath.slice(cycleIndex);
601+
const fieldNames = cyclePath.map(fieldObj => fieldObj.name);
602+
context.reportError(
603+
`Cannot reference Input Object "${fieldType.name}" within itself ` +
604+
`through a series of non-null fields: "${fieldNames.join('.')}".`,
605+
cyclePath.map(fieldObj => fieldObj.astNode),
606+
);
607+
}
608+
fieldPath.pop();
609+
}
610+
}
611+
612+
fieldPathIndexByTypeName[inputObj.name] = undefined;
613+
}
614+
}
615+
557616
type SDLDefinedObject<T, K> = {
558617
+astNode: ?T,
559618
+extensionASTNodes?: ?$ReadOnlyArray<K>,

0 commit comments

Comments
 (0)