Skip to content

Commit 64e8899

Browse files
committed
check oneof inhabitability
1 parent 7fdd84e commit 64e8899

File tree

2 files changed

+258
-0
lines changed

2 files changed

+258
-0
lines changed

src/type/__tests__/validation-test.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2409,6 +2409,199 @@ describe('Type System: OneOf Input Object fields must be nullable', () => {
24092409
});
24102410
});
24112411

2412+
describe('Type System: OneOf Input Objects must be inhabitable', () => {
2413+
it('accepts a OneOf Input Object with a scalar field', () => {
2414+
const schema = buildSchema(`
2415+
type Query {
2416+
test(arg: A): String
2417+
}
2418+
2419+
input A @oneOf {
2420+
a: String
2421+
b: Int
2422+
}
2423+
`);
2424+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2425+
});
2426+
2427+
it('accepts a OneOf Input Object with an enum field', () => {
2428+
const schema = buildSchema(`
2429+
type Query {
2430+
test(arg: A): String
2431+
}
2432+
2433+
enum Color { RED GREEN BLUE }
2434+
2435+
input A @oneOf {
2436+
a: Color
2437+
}
2438+
`);
2439+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2440+
});
2441+
2442+
it('accepts a OneOf Input Object with a list field', () => {
2443+
const schema = buildSchema(`
2444+
type Query {
2445+
test(arg: A): String
2446+
}
2447+
2448+
input A @oneOf {
2449+
a: [A]
2450+
}
2451+
`);
2452+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2453+
});
2454+
2455+
it('accepts a OneOf Input Object referencing a non-OneOf input object', () => {
2456+
const schema = buildSchema(`
2457+
type Query {
2458+
test(arg: A): String
2459+
}
2460+
2461+
input A @oneOf {
2462+
a: RegularInput
2463+
}
2464+
2465+
input RegularInput {
2466+
x: String
2467+
}
2468+
`);
2469+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2470+
});
2471+
2472+
it('accepts a OneOf Input Object with at least one escape field', () => {
2473+
const schema = buildSchema(`
2474+
type Query {
2475+
test(arg: A): String
2476+
}
2477+
2478+
input A @oneOf {
2479+
b: B
2480+
escape: String
2481+
}
2482+
2483+
input B @oneOf {
2484+
a: A
2485+
}
2486+
`);
2487+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2488+
});
2489+
2490+
it('accepts mutually referencing OneOf types where one has a scalar escape', () => {
2491+
const schema = buildSchema(`
2492+
type Query {
2493+
test(arg: A): String
2494+
}
2495+
2496+
input A @oneOf {
2497+
b: B
2498+
}
2499+
2500+
input B @oneOf {
2501+
a: A
2502+
escape: Int
2503+
}
2504+
`);
2505+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2506+
});
2507+
2508+
it('accepts a OneOf referencing a non-OneOf which references back', () => {
2509+
const schema = buildSchema(`
2510+
type Query {
2511+
test(arg: A): String
2512+
}
2513+
2514+
input A @oneOf {
2515+
b: RegularInput
2516+
}
2517+
2518+
input RegularInput {
2519+
back: A
2520+
}
2521+
`);
2522+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2523+
});
2524+
2525+
it('accepts a OneOf with multiple fields where one escapes through chained OneOf types', () => {
2526+
const schema = buildSchema(`
2527+
type Query {
2528+
test(arg: A): String
2529+
}
2530+
2531+
input A @oneOf {
2532+
b: B
2533+
c: C
2534+
}
2535+
2536+
input B @oneOf {
2537+
a: A
2538+
}
2539+
2540+
input C @oneOf {
2541+
a: A
2542+
escape: String
2543+
}
2544+
`);
2545+
expectJSON(validateSchema(schema)).toDeepEqual([]);
2546+
});
2547+
2548+
it('rejects a closed subgraph of one OneOf type', () => {
2549+
const schema = buildSchema(`
2550+
type Query {
2551+
test(arg: A): String
2552+
}
2553+
2554+
input A @oneOf {
2555+
self: A
2556+
}
2557+
`);
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+
]);
2565+
});
2566+
2567+
it('rejects a closed subgraph of multiple OneOf types', () => {
2568+
const schema = buildSchema(`
2569+
type Query {
2570+
test(arg: A): String
2571+
}
2572+
2573+
input A @oneOf {
2574+
b: B
2575+
}
2576+
2577+
input B @oneOf {
2578+
c: C
2579+
}
2580+
2581+
input C @oneOf {
2582+
a: A
2583+
}
2584+
`);
2585+
expectJSON(validateSchema(schema)).toDeepEqual([
2586+
{
2587+
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 }],
2590+
},
2591+
{
2592+
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 }],
2595+
},
2596+
{
2597+
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 }],
2600+
},
2601+
]);
2602+
});
2603+
});
2604+
24122605
describe('Objects must adhere to Interface they implement', () => {
24132606
it('accepts an Object which implements an Interface', () => {
24142607
const schema = buildSchema(`

src/type/validate.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,8 @@ function validateTypes(context: SchemaValidationContext): void {
365365
createInputObjectNonNullCircularRefsValidator(context);
366366
const validateInputObjectDefaultValueCircularRefs =
367367
createInputObjectDefaultValueCircularRefsValidator(context);
368+
const validateOneOfInputObjectInhabitability =
369+
createOneOfInputObjectInhabitabilityValidator(context);
368370
const typeMap = context.schema.getTypeMap();
369371
for (const type of Object.values(typeMap)) {
370372
// Ensure all provided types are in fact GraphQL type.
@@ -409,6 +411,11 @@ function validateTypes(context: SchemaValidationContext): void {
409411

410412
// Ensure Input Objects do not contain invalid default value circular references.
411413
validateInputObjectDefaultValueCircularRefs(type);
414+
415+
// Ensure OneOf Input Objects are inhabitable.
416+
if (type.isOneOf) {
417+
validateOneOfInputObjectInhabitability(type);
418+
}
412419
}
413420
}
414421
}
@@ -943,6 +950,64 @@ function createInputObjectDefaultValueCircularRefsValidator(
943950
}
944951
}
945952

953+
function createOneOfInputObjectInhabitabilityValidator(
954+
context: SchemaValidationContext,
955+
): (inputObj: GraphQLInputObjectType) => void {
956+
// Tracks already validated types to maintain O(N) across top-level calls.
957+
const visitedTypes = new Set<GraphQLInputObjectType>();
958+
959+
return function validateOneOfInputObjectInhabitability(
960+
inputObj: GraphQLInputObjectType,
961+
): void {
962+
if (visitedTypes.has(inputObj)) {
963+
return;
964+
}
965+
966+
if (!isInhabitable(inputObj, new Set())) {
967+
context.reportError(
968+
`OneOf Input Object ${inputObj} must be inhabitable but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.`,
969+
inputObj.astNode,
970+
);
971+
}
972+
973+
visitedTypes.add(inputObj);
974+
};
975+
976+
function isInhabitable(
977+
inputObj: GraphQLInputObjectType,
978+
visited: ReadonlySet<GraphQLInputObjectType>,
979+
): boolean {
980+
if (visited.has(inputObj)) {
981+
return false;
982+
}
983+
984+
const nextVisited = new Set(visited);
985+
nextVisited.add(inputObj);
986+
987+
for (const field of Object.values(inputObj.getFields())) {
988+
if (isListType(field.type)) {
989+
return true;
990+
}
991+
992+
const namedType = getNamedType(field.type);
993+
994+
if (!isInputObjectType(namedType)) {
995+
return true;
996+
}
997+
998+
if (!namedType.isOneOf) {
999+
return true;
1000+
}
1001+
1002+
if (isInhabitable(namedType, nextVisited)) {
1003+
return true;
1004+
}
1005+
}
1006+
1007+
return false;
1008+
}
1009+
}
1010+
9461011
function getAllImplementsInterfaceNodes(
9471012
type: GraphQLObjectType | GraphQLInterfaceType,
9481013
iface: GraphQLInterfaceType,

0 commit comments

Comments
 (0)