Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions src/execution/__tests__/oneof-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,11 @@ describe('Execute: Handles OneOf Input Objects', () => {
const result = executeQuery(query, rootValue);

expectJSON(result).toDeepEqual({
data: {
test: null,
},
errors: [
{
locations: [{ column: 23, line: 3 }],
locations: [{ column: 16, line: 2 }],
message:
// This type of error would be caught at validation-time
// hence the vague error message here.
'Argument "Query.test(input:)" has invalid value: Expected variable "$input" provided to type "TestInputObject!" to provide a runtime value.',
path: ['test'],
'Variable "$input" has invalid default value: OneOf Input Object "TestInputObject" must specify exactly one key.',
},
],
});
Expand Down
152 changes: 152 additions & 0 deletions src/execution/__tests__/variables-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,34 @@ const TestType = new GraphQLObjectType({
},
});

const TestTypeWithInvalidDefaultArgumentValue = new GraphQLObjectType({
name: 'TestTypeWithInvalidDefaultArgumentValue',
fields: {
fieldWithInvalidDefaultArgumentValue: fieldWithInputArg({
type: GraphQLString,
default: { value: 123 },
}),
},
});

const TestTypeWithInvalidNestedDefaultArgumentValue = new GraphQLObjectType({
name: 'TestTypeWithInvalidNestedDefaultArgumentValue',
fields: {
fieldWithInvalidNestedDefaultArgumentValue: fieldWithInputArg({
type: new GraphQLInputObjectType({
name: 'InputWithInvalidNestedFieldDefault',
fields: {
foo: {
type: GraphQLString,
default: { value: 123 },
},
},
}),
default: { value: {} },
}),
},
});

const schema = new GraphQLSchema({
query: TestType,
directives: [
Expand Down Expand Up @@ -450,6 +478,61 @@ describe('Execute: Handles inputs', () => {
});
});

it('reports invalid default values with variable definition locations', () => {
const result = executeQuery(
'query ($input: String = 123) { fieldWithNullableStringInput(input: $input) }',
);

expectJSON(result).toDeepEqual({
errors: [
{
message:
'Variable "$input" has invalid default value: String cannot represent a non string value: 123',
locations: [{ line: 1, column: 8 }],
},
],
});
});

it('includes suggestions for invalid default values', () => {
const result = executeSync({
schema,
document: parse(
'query ($input: TestInputObject = { c: "ok", aa: "x" }) { fieldWithObjectInput(input: $input) }',
),
});

expectJSON(result).toDeepEqual({
errors: [
{
message:
'Variable "$input" has invalid default value: Expected value of type "TestInputObject" not to include unknown field "aa". Did you mean "a"? Found: { c: "ok", aa: "x" }.',
locations: [{ line: 1, column: 8 }],
},
],
});
});

it('hides suggestions for invalid default values when specified', () => {
const result = executeSync({
schema,
document: parse(
'query ($input: TestInputObject = { c: "ok", aa: "x" }) { fieldWithObjectInput(input: $input) }',
),
hideSuggestions: true,
});

expectJSON(result).toDeepEqual({
errors: [
{
message:
'Variable "$input" has invalid default value: Expected value of type "TestInputObject" not to include unknown field "aa", found: { c: "ok", aa: "x" }.',
locations: [{ line: 1, column: 8 }],
},
],
});
});

it('does not use default value when provided', () => {
const result = executeQuery(
`
Expand Down Expand Up @@ -1296,6 +1379,58 @@ describe('Execute: Handles inputs', () => {
},
});
});

it('localizes invalid default value errors during execution', () => {
const schemaWithInvalidDefaultArgumentValue = new GraphQLSchema({
query: TestTypeWithInvalidDefaultArgumentValue,
assumeValid: true,
});

const result = executeSync({
schema: schemaWithInvalidDefaultArgumentValue,
document: parse('{ fieldWithInvalidDefaultArgumentValue }'),
});

expectJSON(result).toDeepEqual({
data: {
fieldWithInvalidDefaultArgumentValue: null,
},
errors: [
{
message:
'Argument "TestTypeWithInvalidDefaultArgumentValue.fieldWithInvalidDefaultArgumentValue(input:)" has invalid default value: String cannot represent a non string value: 123',
locations: [{ line: 1, column: 3 }],
path: ['fieldWithInvalidDefaultArgumentValue'],
},
],
});
});

it('localizes nested invalid field default value errors during execution', () => {
const schemaWithInvalidNestedDefaultArgumentValue = new GraphQLSchema({
query: TestTypeWithInvalidNestedDefaultArgumentValue,
assumeValid: true,
});

const result = executeSync({
schema: schemaWithInvalidNestedDefaultArgumentValue,
document: parse('{ fieldWithInvalidNestedDefaultArgumentValue }'),
});

expectJSON(result).toDeepEqual({
data: {
fieldWithInvalidNestedDefaultArgumentValue: null,
},
errors: [
{
message:
'Argument "TestTypeWithInvalidNestedDefaultArgumentValue.fieldWithInvalidNestedDefaultArgumentValue(input:)" has invalid default value: Expected value of type "String" to be valid, found: 123.',
locations: [{ line: 1, column: 3 }],
path: ['fieldWithInvalidNestedDefaultArgumentValue'],
},
],
});
});
});

describe('getVariableValues: limit maximum number of coercion errors', () => {
Expand Down Expand Up @@ -1456,6 +1591,23 @@ describe('Execute: Handles inputs', () => {
});
});

it('when the definition has an invalid default and is not provided', () => {
const result = executeQueryWithFragmentArguments(
'query { ...a } fragment a($value: String = 123) on TestType { fieldWithNullableStringInput(input: $value) }',
);

expectJSON(result).toDeepEqual({
data: null,
errors: [
{
message:
'Variable "$value" defined by fragment "a" has invalid default value: String cannot represent a non string value: 123',
locations: [{ line: 1, column: 9 }],
},
],
});
});

it('when a definition has a default, is not provided, and spreads another fragment', () => {
const result = executeQueryWithFragmentArguments(`
query {
Expand Down
90 changes: 81 additions & 9 deletions src/execution/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from '../type/definition.js';
import type { GraphQLDirective } from '../type/directives.js';
import type { GraphQLSchema } from '../type/schema.js';
import { validateDefaultInput } from '../type/validate.js';

import {
coerceDefaultValue,
Expand Down Expand Up @@ -117,7 +118,20 @@ function coerceVariableValues(
if (value === undefined) {
sources[varName] = { signature: varSignature };
if (varDefNode.defaultValue) {
coerced[varName] = coerceInputLiteral(varDefNode.defaultValue, varType);
maybeUseDefaultValue(
coerced,
varName,
varSignature,
(error, path) => {
onError(
new GraphQLError(
`Variable "$${varName}" has invalid default value${printPathArray(path)}: ${error.message}`,
{ nodes: varDefNode },
),
);
},
hideSuggestions,
);
continue;
} else if (!isNonNullType(varType)) {
// Non-provided values for nullable variables are omitted.
Expand Down Expand Up @@ -152,6 +166,49 @@ function coerceVariableValues(
return { sources, coerced };
}

function maybeUseDefaultValue(
coercedValues: ObjMap<unknown>,
name: string,
inputValue: GraphQLArgument | GraphQLVariableSignature,
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void,
hideSuggestions?: Maybe<boolean>,
): void {
try {
// coerceDefaultValue assumes validation has already rejected invalid
// defaults. If validation was skipped, invalid defaults or nested input
// field defaults can throw here; recover with validation-style errors below.
const coercedDefaultValue = coerceDefaultValue(inputValue);
if (coercedDefaultValue !== undefined) {
coercedValues[name] = coercedDefaultValue;
}
} catch (error) {
const defaultInput = inputValue.default;
// Defensive: coerceDefaultValue should only throw while coercing a default.
/* c8 ignore next 3 */
if (defaultInput === undefined) {
throw error;
}

// Prefer validation's user-facing errors for invalid defaults.
let reportedValidationError = false;
validateDefaultInput(
defaultInput,
inputValue.type,
(defaultError, path) => {
reportedValidationError = true;
onError(defaultError, path);
},
hideSuggestions,
);

if (!reportedValidationError) {
// The default itself validated, so coercion failed while applying a nested
// input field default. Surface the original coercion error.
onError(ensureGraphQLError(error), []);
}
}
}

export function getFragmentVariableValues(
fragmentSpreadNode: FragmentSpreadNode,
fragmentSignatures: ReadOnlyObjMap<GraphQLVariableSignature>,
Expand Down Expand Up @@ -236,6 +293,15 @@ function coerceArgument(
hideSuggestions?: Maybe<boolean>,
): void {
const argType = argDef.type;
const onArgDefaultValueError = (
error: GraphQLError,
path: ReadonlyArray<string | number>,
): never => {
throw new GraphQLError(
`${printArgumentOrFragmentVariable(argDef, node)} has invalid default value${printPathArray(path)}: ${error.message}`,
{ nodes: node },
);
};

if (!argumentNode) {
if (isRequiredArgument(argDef)) {
Expand All @@ -248,10 +314,13 @@ function coerceArgument(
{ nodes: node },
);
}
const coercedDefaultValue = coerceDefaultValue(argDef);
if (coercedDefaultValue !== undefined) {
coercedValues[argName] = coercedDefaultValue;
}
maybeUseDefaultValue(
coercedValues,
argName,
argDef,
onArgDefaultValueError,
hideSuggestions,
);
return;
}

Expand All @@ -269,10 +338,13 @@ function coerceArgument(
!Object.hasOwn(scopedVariableValues.coerced, variableName)) &&
!isRequiredArgument(argDef)
) {
const coercedDefaultValue = coerceDefaultValue(argDef);
if (coercedDefaultValue !== undefined) {
coercedValues[argName] = coercedDefaultValue;
}
maybeUseDefaultValue(
coercedValues,
argName,
argDef,
onArgDefaultValueError,
hideSuggestions,
);
return;
}
}
Expand Down
Loading
Loading