Skip to content

Commit

Permalink
Validate oneof objects at execution time
Browse files Browse the repository at this point in the history
This ensures the Oneof Objects resolve to an object with exactly one
non-null entry.
  • Loading branch information
erikkessler1 committed Jan 21, 2022
1 parent cf68b04 commit c13d96a
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 2 deletions.
133 changes: 133 additions & 0 deletions src/execution/__tests__/oneof-test.ts
Expand Up @@ -37,6 +37,20 @@ function executeQuery(
return execute({ schema, document: parse(query), rootValue, variableValues });
}

async function executeQueryAsync(
query: string,
rootValue: unknown,
variableValues?: { [variable: string]: unknown },
): Promise<ExecutionResult> {
const result = await execute({
schema,
document: parse(query),
rootValue,
variableValues,
});
return result;
}

describe('Execute: Handles Oneof Input Objects and Oneof Objects', () => {
describe('Oneof Input Objects', () => {
const rootValue = {
Expand Down Expand Up @@ -140,4 +154,123 @@ describe('Execute: Handles Oneof Input Objects and Oneof Objects', () => {
});
});
});

describe('Oneof Objects', () => {
const query = `
query ($input: TestInputObject! = {a: "abc"}) {
test(input: $input) {
a
b
}
}
`;

it('works with a single, non-null value', () => {
const rootValue = {
test: {
a: null,
b: 123,
},
};
const result = executeQuery(query, rootValue);

expectJSON(result).toDeepEqual({
data: {
test: {
a: null,
b: 123,
},
},
});
});

it('works with a single, non-null, async value', async () => {
const rootValue = {
test() {
return {
a: null,
b: () => new Promise((resolve) => resolve(123)),
};
},
};
const result = await executeQueryAsync(query, rootValue);

expectJSON(result).toDeepEqual({
data: {
test: {
a: null,
b: 123,
},
},
});
});

it('errors when there are no non-null values', () => {
const rootValue = {
test: {
a: null,
b: null,
},
};
const result = executeQuery(query, rootValue);

expectJSON(result).toDeepEqual({
data: { test: null },
errors: [
{
locations: [{ column: 11, line: 3 }],
message:
'Oneof Object "TestObject" must have exactly one non-null field but got 0.',
path: ['test'],
},
],
});
});

it('errors when there are multiple non-null values', () => {
const rootValue = {
test: {
a: 'abc',
b: 456,
},
};
const result = executeQuery(query, rootValue);

expectJSON(result).toDeepEqual({
data: { test: null },
errors: [
{
locations: [{ column: 11, line: 3 }],
message:
'Oneof Object "TestObject" must have exactly one non-null field but got 2.',
path: ['test'],
},
],
});
});

it('errors when there are multiple non-null, async values', async () => {
const rootValue = {
test() {
return {
a: 'abc',
b: () => new Promise((resolve) => resolve(123)),
};
},
};
const result = await executeQueryAsync(query, rootValue);

expectJSON(result).toDeepEqual({
data: { test: null },
errors: [
{
locations: [{ column: 11, line: 3 }],
message:
'Oneof Object "TestObject" must have exactly one non-null field but got 2.',
path: ['test'],
},
],
});
});
});
});
63 changes: 61 additions & 2 deletions src/execution/execute.ts
Expand Up @@ -927,12 +927,13 @@ function completeObjectValue(
if (!resolvedIsTypeOf) {
throw invalidReturnTypeError(returnType, result, fieldNodes);
}
return executeFields(
return executeFieldsWithOneOfValidation(
exeContext,
returnType,
result,
path,
subFieldNodes,
fieldNodes,
);
});
}
Expand All @@ -942,7 +943,65 @@ function completeObjectValue(
}
}

return executeFields(exeContext, returnType, result, path, subFieldNodes);
return executeFieldsWithOneOfValidation(
exeContext,
returnType,
result,
path,
subFieldNodes,
fieldNodes,
);
}

function executeFieldsWithOneOfValidation(
exeContext: ExecutionContext,
parentType: GraphQLObjectType,
sourceValue: unknown,
path: Path | undefined,
fields: Map<string, ReadonlyArray<FieldNode>>,
fieldNodes: ReadonlyArray<FieldNode>,
): PromiseOrValue<ObjMap<unknown>> {
const value = executeFields(
exeContext,
parentType,
sourceValue,
path,
fields,
);
if (!parentType.isOneOf) {
return value;
}

if (isPromise(value)) {
return value.then((resolvedValue) => {
validateOneOfValue(resolvedValue, parentType, fieldNodes);
return resolvedValue;
});
}

validateOneOfValue(value, parentType, fieldNodes);
return value;
}

function validateOneOfValue(
value: ObjMap<unknown>,
returnType: GraphQLObjectType,
fieldNodes: ReadonlyArray<FieldNode>,
): void {
let nonNullCount = 0;

for (const field in value) {
if (value[field] !== null) {
nonNullCount += 1;
}
}

if (nonNullCount !== 1) {
throw new GraphQLError(
`Oneof Object "${returnType.name}" must have exactly one non-null field but got ${nonNullCount}.`,
fieldNodes,
);
}
}

function invalidReturnTypeError(
Expand Down

0 comments on commit c13d96a

Please sign in to comment.