Skip to content

Commit ba19546

Browse files
committed
Detect uninhabitable input types formed by OneOf and non-OneOf cycles
Rework the circular references validation to detect input object types that cannot be provided a finite value. This covers: - Self-recursive OneOf types (e.g. input A @OneOf { a: A }) - Mixed OneOf/non-OneOf cycles with no escape path - Standard non-null circular references (existing behavior preserved) Rename algorithms to match spec terminology: - InputObjectHasUnbreakableCycle (was InputObjectCanBeProvidedAFiniteValue) - InputFieldTypeHasUnbreakableCycle (was FieldTypeCanBeProvidedAFiniteValue)
1 parent 994512e commit ba19546

File tree

2 files changed

+277
-189
lines changed

2 files changed

+277
-189
lines changed

src/type/__tests__/validation-test.ts

Lines changed: 135 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -924,7 +924,7 @@ describe('Type System: Input Objects must have fields', () => {
924924
expectJSON(validateSchema(schema)).toDeepEqual([
925925
{
926926
message:
927-
'Invalid circular reference. The Input Object SomeInputObject references itself in the non-null field SomeInputObject.nonNullSelf.',
927+
'Input Object SomeInputObject references itself via the required fields: SomeInputObject.nonNullSelf.',
928928
locations: [{ line: 7, column: 9 }],
929929
},
930930
]);
@@ -952,13 +952,31 @@ describe('Type System: Input Objects must have fields', () => {
952952
expectJSON(validateSchema(schema)).toDeepEqual([
953953
{
954954
message:
955-
'Invalid circular reference. The Input Object SomeInputObject references itself via the non-null fields: SomeInputObject.startLoop, AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop.',
955+
'Input Object SomeInputObject references itself via the required fields: SomeInputObject.startLoop, AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop.',
956956
locations: [
957957
{ line: 7, column: 9 },
958958
{ line: 11, column: 9 },
959959
{ line: 15, column: 9 },
960960
],
961961
},
962+
{
963+
message:
964+
'Input Object AnotherInputObject references itself via the required fields: AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop, SomeInputObject.startLoop.',
965+
locations: [
966+
{ line: 11, column: 9 },
967+
{ line: 15, column: 9 },
968+
{ line: 7, column: 9 },
969+
],
970+
},
971+
{
972+
message:
973+
'Input Object YetAnotherInputObject references itself via the required fields: YetAnotherInputObject.closeLoop, SomeInputObject.startLoop, AnotherInputObject.nextInLoop.',
974+
locations: [
975+
{ line: 15, column: 9 },
976+
{ line: 7, column: 9 },
977+
{ line: 11, column: 9 },
978+
],
979+
},
962980
]);
963981
});
964982

@@ -986,24 +1004,28 @@ describe('Type System: Input Objects must have fields', () => {
9861004
expectJSON(validateSchema(schema)).toDeepEqual([
9871005
{
9881006
message:
989-
'Invalid circular reference. The Input Object SomeInputObject references itself via the non-null fields: SomeInputObject.startLoop, AnotherInputObject.closeLoop.',
1007+
'Input Object SomeInputObject references itself via the required fields: SomeInputObject.startLoop, AnotherInputObject.closeLoop.',
9901008
locations: [
9911009
{ line: 7, column: 9 },
9921010
{ line: 11, column: 9 },
9931011
],
9941012
},
9951013
{
9961014
message:
997-
'Invalid circular reference. The Input Object AnotherInputObject references itself via the non-null fields: AnotherInputObject.startSecondLoop, YetAnotherInputObject.closeSecondLoop.',
1015+
'Input Object AnotherInputObject references itself via the required fields: AnotherInputObject.closeLoop, SomeInputObject.startLoop.',
9981016
locations: [
999-
{ line: 12, column: 9 },
1000-
{ line: 16, column: 9 },
1017+
{ line: 11, column: 9 },
1018+
{ line: 7, column: 9 },
10011019
],
10021020
},
10031021
{
10041022
message:
1005-
'Invalid circular reference. The Input Object YetAnotherInputObject references itself in the non-null field YetAnotherInputObject.nonNullSelf.',
1006-
locations: [{ line: 17, column: 9 }],
1023+
'Input Object YetAnotherInputObject references itself via the required fields: YetAnotherInputObject.closeSecondLoop, AnotherInputObject.closeLoop, SomeInputObject.startLoop.',
1024+
locations: [
1025+
{ line: 16, column: 9 },
1026+
{ line: 11, column: 9 },
1027+
{ line: 7, column: 9 },
1028+
],
10071029
},
10081030
]);
10091031
});
@@ -2409,173 +2431,202 @@ describe('Type System: OneOf Input Object fields must be nullable', () => {
24092431
});
24102432
});
24112433

2412-
describe('Type System: OneOf Input Objects must be inhabitable', () => {
2434+
describe('Type System: Input Objects must not have unbreakable cycles', () => {
24132435
it('accepts a OneOf Input Object with a scalar field', () => {
24142436
const schema = buildSchema(`
24152437
type Query {
2416-
test(arg: A): String
2438+
test(arg: A): Int
24172439
}
24182440
24192441
input A @oneOf {
2420-
a: String
2421-
b: Int
2442+
a: Int
24222443
}
24232444
`);
24242445
expectJSON(validateSchema(schema)).toDeepEqual([]);
24252446
});
24262447

2427-
it('accepts a OneOf Input Object with an enum field', () => {
2448+
it('accepts a OneOf Input Object with a recursive list field', () => {
24282449
const schema = buildSchema(`
24292450
type Query {
2430-
test(arg: A): String
2451+
test(arg: A): Int
24312452
}
24322453
2433-
enum Color { RED GREEN BLUE }
2434-
24352454
input A @oneOf {
2436-
a: Color
2455+
a: [A!]
24372456
}
24382457
`);
24392458
expectJSON(validateSchema(schema)).toDeepEqual([]);
24402459
});
24412460

2442-
it('accepts a OneOf Input Object with a list field', () => {
2461+
it('accepts a OneOf Input Object referencing a non-OneOf input object', () => {
24432462
const schema = buildSchema(`
24442463
type Query {
2445-
test(arg: A): String
2464+
test(arg: A): Int
24462465
}
24472466
24482467
input A @oneOf {
2449-
a: [A]
2468+
b: B
2469+
}
2470+
2471+
input B {
2472+
x: Int
24502473
}
24512474
`);
24522475
expectJSON(validateSchema(schema)).toDeepEqual([]);
24532476
});
24542477

2455-
it('accepts a OneOf Input Object referencing a non-OneOf input object', () => {
2478+
it('accepts a OneOf/OneOf cycle with a scalar escape', () => {
24562479
const schema = buildSchema(`
24572480
type Query {
2458-
test(arg: A): String
2481+
test(arg: A): Int
24592482
}
24602483
24612484
input A @oneOf {
2462-
a: RegularInput
2485+
b: B
2486+
escape: Int
24632487
}
24642488
2465-
input RegularInput {
2466-
x: String
2489+
input B @oneOf {
2490+
a: A
24672491
}
24682492
`);
24692493
expectJSON(validateSchema(schema)).toDeepEqual([]);
24702494
});
24712495

2472-
it('accepts a OneOf Input Object with at least one escape field', () => {
2496+
it('accepts a OneOf/non-OneOf cycle with a nullable escape', () => {
24732497
const schema = buildSchema(`
24742498
type Query {
2475-
test(arg: A): String
2499+
test(arg: A): Int
24762500
}
24772501
24782502
input A @oneOf {
24792503
b: B
2480-
escape: String
24812504
}
24822505
2483-
input B @oneOf {
2506+
input B {
24842507
a: A
24852508
}
24862509
`);
24872510
expectJSON(validateSchema(schema)).toDeepEqual([]);
24882511
});
24892512

2490-
it('accepts mutually referencing OneOf types where one has a scalar escape', () => {
2513+
it('rejects a self-referencing OneOf type with no escapes', () => {
24912514
const schema = buildSchema(`
24922515
type Query {
2493-
test(arg: A): String
2516+
test(arg: A): Int
24942517
}
24952518
24962519
input A @oneOf {
2497-
b: B
2498-
}
2499-
2500-
input B @oneOf {
2501-
a: A
2502-
escape: Int
2520+
self: A
25032521
}
25042522
`);
2505-
expectJSON(validateSchema(schema)).toDeepEqual([]);
2523+
expectJSON(validateSchema(schema)).toDeepEqual([
2524+
{
2525+
message:
2526+
'Input Object A references itself via the required fields: A.self.',
2527+
locations: [{ line: 7, column: 9 }],
2528+
},
2529+
]);
25062530
});
25072531

2508-
it('accepts a OneOf referencing a non-OneOf which references back', () => {
2532+
it('rejects a mixed OneOf/non-OneOf cycle with no escapes', () => {
25092533
const schema = buildSchema(`
25102534
type Query {
2511-
test(arg: A): String
2535+
test(arg: A): Int
25122536
}
25132537
25142538
input A @oneOf {
2515-
b: RegularInput
2539+
b: B
25162540
}
25172541
2518-
input RegularInput {
2519-
back: A
2542+
input B {
2543+
a: A!
25202544
}
25212545
`);
2522-
expectJSON(validateSchema(schema)).toDeepEqual([]);
2546+
expectJSON(validateSchema(schema)).toDeepEqual([
2547+
{
2548+
message:
2549+
'Input Object A references itself via the required fields: A.b, B.a.',
2550+
locations: [
2551+
{ line: 7, column: 9 },
2552+
{ line: 11, column: 9 },
2553+
],
2554+
},
2555+
{
2556+
message:
2557+
'Input Object B references itself via the required fields: B.a, A.b.',
2558+
locations: [
2559+
{ line: 11, column: 9 },
2560+
{ line: 7, column: 9 },
2561+
],
2562+
},
2563+
]);
25232564
});
25242565

2525-
it('accepts a OneOf with multiple fields where one escapes through chained OneOf types', () => {
2566+
it('accepts a OneOf/non-OneOf with scalar escape', () => {
25262567
const schema = buildSchema(`
25272568
type Query {
2528-
test(arg: A): String
2569+
test(arg: A): Int
25292570
}
25302571
25312572
input A @oneOf {
25322573
b: B
2533-
c: C
2574+
escape: Int
25342575
}
25352576
2536-
input B @oneOf {
2537-
a: A
2577+
input B {
2578+
a: A!
25382579
}
2580+
`);
2581+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2582+
});
25392583

2540-
input C @oneOf {
2584+
it('accepts a non-OneOf/non-OneOf cycle with a nullable escape', () => {
2585+
const schema = buildSchema(`
2586+
type Query {
2587+
test(arg: A): Int
2588+
}
2589+
2590+
input A {
2591+
b: B!
2592+
}
2593+
2594+
input B {
25412595
a: A
2542-
escape: String
25432596
}
25442597
`);
25452598
expectJSON(validateSchema(schema)).toDeepEqual([]);
25462599
});
25472600

2548-
it('rejects a closed subgraph of one OneOf type', () => {
2601+
it('accepts a non-OneOf/non-OneOf cycle with a list escape', () => {
25492602
const schema = buildSchema(`
25502603
type Query {
2551-
test(arg: A): String
2604+
test(arg: A): Int
25522605
}
25532606
2554-
input A @oneOf {
2555-
self: A
2607+
input A {
2608+
b: [B!]!
2609+
}
2610+
2611+
input B {
2612+
a: A!
25562613
}
25572614
`);
2558-
expectJSON(validateSchema(schema)).toDeepEqual([
2559-
{
2560-
message:
2561-
'OneOf Input Object A must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.',
2562-
locations: [{ line: 6, column: 7 }],
2563-
},
2564-
]);
2615+
expectJSON(validateSchema(schema)).toDeepEqual([]);
25652616
});
25662617

2567-
it('rejects a closed subgraph of multiple OneOf types', () => {
2618+
it('rejects a larger mixed OneOf/non-OneOf cycle with no escapes', () => {
25682619
const schema = buildSchema(`
25692620
type Query {
2570-
test(arg: A): String
2621+
test(arg: A): Int
25712622
}
25722623
25732624
input A @oneOf {
25742625
b: B
25752626
}
25762627
2577-
input B @oneOf {
2578-
c: C
2628+
input B {
2629+
c: C!
25792630
}
25802631
25812632
input C @oneOf {
@@ -2585,18 +2636,30 @@ describe('Type System: OneOf Input Objects must be inhabitable', () => {
25852636
expectJSON(validateSchema(schema)).toDeepEqual([
25862637
{
25872638
message:
2588-
'OneOf Input Object A must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.',
2589-
locations: [{ line: 6, column: 7 }],
2639+
'Input Object A references itself via the required fields: A.b, B.c, C.a.',
2640+
locations: [
2641+
{ line: 7, column: 9 },
2642+
{ line: 11, column: 9 },
2643+
{ line: 15, column: 9 },
2644+
],
25902645
},
25912646
{
25922647
message:
2593-
'OneOf Input Object B must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.',
2594-
locations: [{ line: 10, column: 7 }],
2648+
'Input Object B references itself via the required fields: B.c, C.a, A.b.',
2649+
locations: [
2650+
{ line: 11, column: 9 },
2651+
{ line: 15, column: 9 },
2652+
{ line: 7, column: 9 },
2653+
],
25952654
},
25962655
{
25972656
message:
2598-
'OneOf Input Object C must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.',
2599-
locations: [{ line: 14, column: 7 }],
2657+
'Input Object C references itself via the required fields: C.a, A.b, B.c.',
2658+
locations: [
2659+
{ line: 15, column: 9 },
2660+
{ line: 7, column: 9 },
2661+
{ line: 11, column: 9 },
2662+
],
26002663
},
26012664
]);
26022665
});

0 commit comments

Comments
 (0)