Skip to content

Commit ada56fe

Browse files
authored
Validate literals in a single rule with finer precision (#1144)
This generalizes the "arguments of correct type" and "default values of correct type" to a single rule "values of correct type" which has been re-written to rely on a traversal rather than the utility function `isValidLiteralValue`. To reduce breaking scope, this does not remove that utility even though it's no longer used directly within the library. Since the default values rule included another validation rule that rule was renamed to a more apt "variable default value allowed". This also includes the original errors from custom scalars in the validation error output, solving the remainder of #821. Closes #821
1 parent 1aa12df commit ada56fe

21 files changed

+805
-644
lines changed

src/execution/__tests__/variables-test.js

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,9 @@ describe('Execute: Handles inputs', () => {
208208
errors: [
209209
{
210210
message:
211-
'Argument "input" got invalid value ["foo", "bar", "baz"].\n' +
212-
'Expected "TestInputObject", found not an object.',
211+
'Argument "input" has invalid value ["foo", "bar", "baz"].',
213212
path: ['fieldWithObjectInput'],
213+
locations: [{ line: 3, column: 39 }],
214214
},
215215
],
216216
});
@@ -981,9 +981,7 @@ describe('Execute: Handles inputs', () => {
981981
},
982982
errors: [
983983
{
984-
message:
985-
'Argument "input" got invalid value WRONG_TYPE.\n' +
986-
'Expected type "String", found WRONG_TYPE.',
984+
message: 'Argument "input" has invalid value WRONG_TYPE.',
987985
locations: [{ line: 2, column: 46 }],
988986
path: ['fieldWithDefaultArgumentValue'],
989987
},

src/execution/values.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import keyMap from '../jsutils/keyMap';
1414
import { coerceValue } from '../utilities/coerceValue';
1515
import { typeFromAST } from '../utilities/typeFromAST';
1616
import { valueFromAST } from '../utilities/valueFromAST';
17-
import { isValidLiteralValue } from '../utilities/isValidLiteralValue';
1817
import * as Kind from '../language/kinds';
1918
import { print } from '../language/printer';
2019
import { isInputType, isNonNullType } from '../type/definition';
@@ -164,10 +163,11 @@ export function getArgumentValues(
164163
const valueNode = argumentNode.value;
165164
const coercedValue = valueFromAST(valueNode, argType, variableValues);
166165
if (isInvalid(coercedValue)) {
167-
const errors = isValidLiteralValue(argType, valueNode);
168-
const message = errors ? '\n' + errors.join('\n') : '';
166+
// Note: ValuesOfCorrectType validation should catch this before
167+
// execution. This is a runtime check to ensure execution does not
168+
// continue with an invalid argument value.
169169
throw new GraphQLError(
170-
`Argument "${name}" got invalid value ${print(valueNode)}.${message}`,
170+
`Argument "${name}" has invalid value ${print(valueNode)}.`,
171171
[argumentNode.value],
172172
);
173173
}

src/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,6 @@ export {
261261
// All validation rules in the GraphQL Specification.
262262
specifiedRules,
263263
// Individual validation rules.
264-
ArgumentsOfCorrectTypeRule,
265-
DefaultValuesOfCorrectTypeRule,
266264
FieldsOnCorrectTypeRule,
267265
FragmentsOnCompositeTypesRule,
268266
KnownArgumentNamesRule,
@@ -285,7 +283,9 @@ export {
285283
UniqueInputFieldNamesRule,
286284
UniqueOperationNamesRule,
287285
UniqueVariableNamesRule,
286+
ValuesOfCorrectTypeRule,
288287
VariablesAreInputTypesRule,
288+
VariablesDefaultValueAllowedRule,
289289
VariablesInAllowedPositionRule,
290290
} from './validation';
291291

src/jsutils/orList.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
const MAX_LENGTH = 5;
11+
12+
/**
13+
* Given [ A, B, C ] return 'A, B, or C'.
14+
*/
15+
export default function orList(items: $ReadOnlyArray<string>): string {
16+
const selected = items.slice(0, MAX_LENGTH);
17+
return selected.reduce(
18+
(list, quoted, index) =>
19+
list +
20+
(selected.length > 2 ? ', ' : ' ') +
21+
(index === selected.length - 1 ? 'or ' : '') +
22+
quoted,
23+
);
24+
}

src/jsutils/quotedOrList.js

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,11 @@
77
* @flow
88
*/
99

10-
const MAX_LENGTH = 5;
10+
import orList from './orList';
1111

1212
/**
1313
* Given [ A, B, C ] return '"A", "B", or "C"'.
1414
*/
1515
export default function quotedOrList(items: $ReadOnlyArray<string>): string {
16-
const selected = items.slice(0, MAX_LENGTH);
17-
return selected
18-
.map(item => `"${item}"`)
19-
.reduce(
20-
(list, quoted, index) =>
21-
list +
22-
(selected.length > 2 ? ', ' : ' ') +
23-
(index === selected.length - 1 ? 'or ' : '') +
24-
quoted,
25-
);
16+
return orList(items.map(item => `"${item}"`));
2617
}

src/type/__tests__/enumType-test.js

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,21 @@ describe('Type System: Enum Values', () => {
161161
errors: [
162162
{
163163
message:
164-
'Argument "fromEnum" has invalid value "GREEN".' +
165-
'\nExpected type "Color", found "GREEN".',
164+
'Expected type Color, found "GREEN"; Did you mean the enum value: GREEN?',
165+
locations: [{ line: 1, column: 23 }],
166+
},
167+
],
168+
});
169+
});
170+
171+
it('does not accept values not in the enum', async () => {
172+
expect(
173+
await graphql(schema, '{ colorEnum(fromEnum: GREENISH) }'),
174+
).to.jsonEqual({
175+
errors: [
176+
{
177+
message:
178+
'Expected type Color, found GREENISH; Did you mean the enum value: GREEN?',
166179
locations: [{ line: 1, column: 23 }],
167180
},
168181
],
@@ -180,6 +193,7 @@ describe('Type System: Enum Values', () => {
180193
{
181194
message: 'Expected a value of type "Color" but received: GREEN',
182195
locations: [{ line: 1, column: 3 }],
196+
path: ['colorEnum'],
183197
},
184198
],
185199
});
@@ -189,9 +203,7 @@ describe('Type System: Enum Values', () => {
189203
expect(await graphql(schema, '{ colorEnum(fromEnum: 1) }')).to.jsonEqual({
190204
errors: [
191205
{
192-
message:
193-
'Argument "fromEnum" has invalid value 1.' +
194-
'\nExpected type "Color", found 1.',
206+
message: 'Expected type Color, found 1.',
195207
locations: [{ line: 1, column: 23 }],
196208
},
197209
],
@@ -203,9 +215,7 @@ describe('Type System: Enum Values', () => {
203215
{
204216
errors: [
205217
{
206-
message:
207-
'Argument "fromInt" has invalid value GREEN.' +
208-
'\nExpected type "Int", found GREEN.',
218+
message: 'Expected type Int, found GREEN.',
209219
locations: [{ line: 1, column: 22 }],
210220
},
211221
],

src/utilities/TypeInfo.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ export class TypeInfo {
6262
// to support non-spec-compliant codebases. You should never need to use it.
6363
// It may disappear in the future.
6464
getFieldDefFn?: typeof getFieldDef,
65+
// Initial type may be provided in rare cases to facilitate traversals
66+
// beginning somewhere other than documents.
67+
initialType?: GraphQLType,
6568
): void {
6669
this._schema = schema;
6770
this._typeStack = [];
@@ -72,6 +75,17 @@ export class TypeInfo {
7275
this._argument = null;
7376
this._enumValue = null;
7477
this._getFieldDef = getFieldDefFn || getFieldDef;
78+
if (initialType) {
79+
if (isInputType(initialType)) {
80+
this._inputTypeStack.push(initialType);
81+
}
82+
if (isCompositeType(initialType)) {
83+
this._parentTypeStack.push(initialType);
84+
}
85+
if (isOutputType(initialType)) {
86+
this._typeStack.push(initialType);
87+
}
88+
}
7589
}
7690

7791
getType(): ?GraphQLOutputType {
@@ -92,6 +106,12 @@ export class TypeInfo {
92106
}
93107
}
94108

109+
getParentInputType(): ?GraphQLInputType {
110+
if (this._inputTypeStack.length > 1) {
111+
return this._inputTypeStack[this._inputTypeStack.length - 2];
112+
}
113+
}
114+
95115
getFieldDef(): ?GraphQLField<*, *> {
96116
if (this._fieldDefStack.length > 0) {
97117
return this._fieldDefStack[this._fieldDefStack.length - 1];
@@ -183,10 +203,9 @@ export class TypeInfo {
183203
break;
184204
case Kind.LIST:
185205
const listType: mixed = getNullableType(this.getInputType());
186-
let itemType: mixed;
187-
if (isListType(listType)) {
188-
itemType = listType.ofType;
189-
}
206+
const itemType: mixed = isListType(listType)
207+
? listType.ofType
208+
: listType;
190209
this._inputTypeStack.push(isInputType(itemType) ? itemType : undefined);
191210
break;
192211
case Kind.OBJECT_FIELD:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { expect } from 'chai';
9+
import { describe, it } from 'mocha';
10+
import { isValidLiteralValue } from '../isValidLiteralValue';
11+
import { parseValue } from '../../language';
12+
import { GraphQLInt } from '../../type';
13+
14+
describe('isValidLiteralValue', () => {
15+
it('Returns no errors for a valid value', () => {
16+
expect(isValidLiteralValue(GraphQLInt, parseValue('123'))).to.deep.equal(
17+
[],
18+
);
19+
});
20+
21+
it('Returns errors for an invalid value', () => {
22+
expect(isValidLiteralValue(GraphQLInt, parseValue('"abc"'))).to.deep.equal([
23+
{
24+
message: 'Expected type Int, found "abc".',
25+
locations: [{ line: 1, column: 1 }],
26+
path: undefined,
27+
},
28+
]);
29+
});
30+
});

src/utilities/isValidLiteralValue.js

Lines changed: 18 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -7,123 +7,30 @@
77
* @flow
88
*/
99

10-
import { print } from '../language/printer';
11-
import type {
12-
ValueNode,
13-
ListValueNode,
14-
ObjectValueNode,
15-
} from '../language/ast';
16-
import * as Kind from '../language/kinds';
17-
import {
18-
isScalarType,
19-
isEnumType,
20-
isInputObjectType,
21-
isListType,
22-
isNonNullType,
23-
} from '../type/definition';
10+
import { TypeInfo } from './TypeInfo';
11+
import type { GraphQLError } from '../error/GraphQLError';
12+
import type { ValueNode } from '../language/ast';
13+
import { DOCUMENT } from '../language/kinds';
14+
import { visit, visitWithTypeInfo } from '../language/visitor';
2415
import type { GraphQLInputType } from '../type/definition';
25-
import isInvalid from '../jsutils/isInvalid';
26-
import keyMap from '../jsutils/keyMap';
16+
import { GraphQLSchema } from '../type/schema';
17+
import { ValuesOfCorrectType } from '../validation/rules/ValuesOfCorrectType';
18+
import { ValidationContext } from '../validation/validate';
2719

2820
/**
29-
* Utility for validators which determines if a value literal node is valid
30-
* given an input type.
21+
* Utility which determines if a value literal node is valid for an input type.
3122
*
32-
* Note that this only validates literal values, variables are assumed to
33-
* provide values of the correct type.
23+
* Deprecated. Rely on validation for documents containing literal values.
3424
*/
3525
export function isValidLiteralValue(
3626
type: GraphQLInputType,
3727
valueNode: ValueNode,
38-
): Array<string> {
39-
// A value must be provided if the type is non-null.
40-
if (isNonNullType(type)) {
41-
if (!valueNode || valueNode.kind === Kind.NULL) {
42-
return [`Expected "${String(type)}", found null.`];
43-
}
44-
return isValidLiteralValue(type.ofType, valueNode);
45-
}
46-
47-
if (!valueNode || valueNode.kind === Kind.NULL) {
48-
return [];
49-
}
50-
51-
// This function only tests literals, and assumes variables will provide
52-
// values of the correct type.
53-
if (valueNode.kind === Kind.VARIABLE) {
54-
return [];
55-
}
56-
57-
// Lists accept a non-list value as a list of one.
58-
if (isListType(type)) {
59-
const itemType = type.ofType;
60-
if (valueNode.kind === Kind.LIST) {
61-
return (valueNode: ListValueNode).values.reduce((acc, item, index) => {
62-
const errors = isValidLiteralValue(itemType, item);
63-
return acc.concat(
64-
errors.map(error => `In element #${index}: ${error}`),
65-
);
66-
}, []);
67-
}
68-
return isValidLiteralValue(itemType, valueNode);
69-
}
70-
71-
// Input objects check each defined field and look for undefined fields.
72-
if (isInputObjectType(type)) {
73-
if (valueNode.kind !== Kind.OBJECT) {
74-
return [`Expected "${type.name}", found not an object.`];
75-
}
76-
const fields = type.getFields();
77-
78-
const errors = [];
79-
80-
// Ensure every provided field is defined.
81-
const fieldNodes = (valueNode: ObjectValueNode).fields;
82-
fieldNodes.forEach(providedFieldNode => {
83-
if (!fields[providedFieldNode.name.value]) {
84-
errors.push(
85-
`In field "${providedFieldNode.name.value}": Unknown field.`,
86-
);
87-
}
88-
});
89-
90-
// Ensure every defined field is valid.
91-
const fieldNodeMap = keyMap(fieldNodes, fieldNode => fieldNode.name.value);
92-
Object.keys(fields).forEach(fieldName => {
93-
const result = isValidLiteralValue(
94-
fields[fieldName].type,
95-
fieldNodeMap[fieldName] && fieldNodeMap[fieldName].value,
96-
);
97-
errors.push(...result.map(error => `In field "${fieldName}": ${error}`));
98-
});
99-
100-
return errors;
101-
}
102-
103-
if (isEnumType(type)) {
104-
if (valueNode.kind !== Kind.ENUM || !type.getValue(valueNode.value)) {
105-
return [`Expected type "${type.name}", found ${print(valueNode)}.`];
106-
}
107-
108-
return [];
109-
}
110-
111-
if (isScalarType(type)) {
112-
// Scalars determine if a literal value is valid via parseLiteral().
113-
try {
114-
const parseResult = type.parseLiteral(valueNode, null);
115-
if (isInvalid(parseResult)) {
116-
return [`Expected type "${type.name}", found ${print(valueNode)}.`];
117-
}
118-
} catch (error) {
119-
const printed = print(valueNode);
120-
const message = error.message;
121-
return [`Expected type "${type.name}", found ${printed}; ${message}`];
122-
}
123-
124-
return [];
125-
}
126-
127-
/* istanbul ignore next */
128-
throw new Error(`Unknown type: ${(type: empty)}.`);
28+
): $ReadOnlyArray<GraphQLError> {
29+
const emptySchema = new GraphQLSchema({});
30+
const emptyDoc = { kind: DOCUMENT, definitions: [] };
31+
const typeInfo = new TypeInfo(emptySchema, undefined, type);
32+
const context = new ValidationContext(emptySchema, emptyDoc, typeInfo);
33+
const visitor = ValuesOfCorrectType(context);
34+
visit(valueNode, visitWithTypeInfo(typeInfo, visitor));
35+
return context.getErrors();
12936
}

0 commit comments

Comments
 (0)