Skip to content

Commit

Permalink
Enforces input coercion rules.
Browse files Browse the repository at this point in the history
Before this diff, bad input to arguments and variables was often ignored and replaced with `null` rather than rejected. Now that `null` has a semantic meaning, and thanks to some recent changes to the spec (graphql/graphql-spec#221) - changes are necessary in order to enforce the stricter coercion rules.

This diff does the following:

* Implements the CoerceArgumentValues as described in the spec.
* Implements the CoerceVariablesValues as described in the spec.
* Alters valueFromAST and coerceValue (dual functions) to strictly enforce coercion, returning `undefined` implicitly when they fail to do so. It also fixes issues where undefined returns were being ignored as items in a list or fields in an input object.
  • Loading branch information
leebyron committed Nov 1, 2016
1 parent 2b717f1 commit 6dce0c9
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 107 deletions.
12 changes: 6 additions & 6 deletions src/execution/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -458,8 +458,8 @@ function shouldIncludeNode(
);
if (skipAST) {
const { if: skipIf } = getArgumentValues(
GraphQLSkipDirective.args,
skipAST.arguments,
GraphQLSkipDirective,
skipAST,
exeContext.variableValues
);
if (skipIf === true) {
Expand All @@ -473,8 +473,8 @@ function shouldIncludeNode(
);
if (includeAST) {
const { if: includeIf } = getArgumentValues(
GraphQLIncludeDirective.args,
includeAST.arguments,
GraphQLIncludeDirective,
includeAST,
exeContext.variableValues
);
if (includeIf === false) {
Expand Down Expand Up @@ -563,8 +563,8 @@ function resolveField(
// variables scope to fulfill any variable references.
// TODO: find a way to memoize, in case this field is within a List type.
const args = getArgumentValues(
fieldDef.args,
fieldAST.arguments,
fieldDef,
fieldAST,
exeContext.variableValues
);

Expand Down
232 changes: 150 additions & 82 deletions src/execution/values.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* of patent rights can be found in the PATENTS file in the same directory.
*/

import { forEach, isCollection } from 'iterall';
import { createIterator, isCollection } from 'iterall';

import { GraphQLError } from '../error';
import invariant from '../jsutils/invariant';
Expand All @@ -18,6 +18,8 @@ import keyMap from '../jsutils/keyMap';
import { typeFromAST } from '../utilities/typeFromAST';
import { valueFromAST } from '../utilities/valueFromAST';
import { isValidJSValue } from '../utilities/isValidJSValue';
import { isValidLiteralValue } from '../utilities/isValidLiteralValue';
import * as Kind from '../language/kinds';
import { print } from '../language/printer';
import {
isInputType,
Expand All @@ -27,9 +29,18 @@ import {
GraphQLList,
GraphQLNonNull,
} from '../type/definition';
import type { GraphQLInputType, GraphQLArgument } from '../type/definition';
import type {
GraphQLInputType,
GraphQLFieldDefinition
} from '../type/definition';
import type { GraphQLDirective } from '../type/directives';
import type { GraphQLSchema } from '../type/schema';
import type { Argument, VariableDefinition } from '../language/ast';
import type {
Field,
Directive,
Variable,
VariableDefinition,
} from '../language/ast';


/**
Expand All @@ -42,84 +53,113 @@ export function getVariableValues(
definitionASTs: Array<VariableDefinition>,
inputs: { [key: string]: mixed }
): { [key: string]: mixed } {
return definitionASTs.reduce((values, defAST) => {
const varName = defAST.variable.name.value;
values[varName] = getVariableValue(schema, defAST, inputs[varName]);
return values;
}, {});
}
const coercedValues = Object.create(null);
for (let i = 0; i < definitionASTs.length; i++) {
const definitionAST = definitionASTs[i];
const varName = definitionAST.variable.name.value;
let varType = typeFromAST(schema, definitionAST.type);
if (!isInputType(varType)) {
throw new GraphQLError(
`Variable "$${varName}" expected value of type ` +
`"${String(varType)}" which cannot be used as an input type.`,
[ definitionAST.type ]
);
}
varType = ((varType: any): GraphQLInputType);

const value = inputs[varName];
if (isInvalid(value)) {
const defaultValue = definitionAST.defaultValue;
if (!isInvalid(defaultValue)) {
coercedValues[varName] = valueFromAST(defaultValue, varType);
}
if (varType instanceof GraphQLNonNull) {
throw new GraphQLError(
`Variable "$${varName}" of required type ` +
`"${String(varType)}" was not provided.`,
[ definitionAST ]
);
}
} else {
const coercedValue = coerceValue(varType, value);
if (isInvalid(coercedValue)) {
const errors = isValidJSValue(value, varType);
const message = errors ? '\n' + errors.join('\n') : '';
throw new GraphQLError(
`Variable "${varName}" got invalid value ` +
`${JSON.stringify(value)}.${message}`,
[ definitionAST ]
);
}
coercedValues[varName] = coercedValue;
}
}
return coercedValues;
}

/**
* Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes.
*/
export function getArgumentValues(
argDefs: ?Array<GraphQLArgument>,
argASTs: ?Array<Argument>,
def: GraphQLFieldDefinition | GraphQLDirective,
node: Field | Directive,
variableValues?: ?{ [key: string]: mixed }
): { [key: string]: mixed } {
const argDefs = def.args;
const argASTs = node.arguments;
if (!argDefs || !argASTs) {
return {};
}
const coercedValues = Object.create(null);
const argASTMap = keyMap(argASTs, arg => arg.name.value);
return argDefs.reduce((result, argDef) => {
for (let i = 0; i < argDefs.length; i++) {
const argDef = argDefs[i];
const name = argDef.name;
const valueAST = argASTMap[name] ? argASTMap[name].value : null;
let value = valueFromAST(valueAST, argDef.type, variableValues);
if (isInvalid(value)) {
value = argDef.defaultValue;
}
if (!isInvalid(value)) {
result[name] = value;
}
return result;
}, {});
}


/**
* Given a variable definition, and any value of input, return a value which
* adheres to the variable definition, or throw an error.
*/
function getVariableValue(
schema: GraphQLSchema,
definitionAST: VariableDefinition,
input: mixed
): mixed {
const type = typeFromAST(schema, definitionAST.type);
const variable = definitionAST.variable;
if (!type || !isInputType(type)) {
throw new GraphQLError(
`Variable "$${variable.name.value}" expected value of type ` +
`"${print(definitionAST.type)}" which cannot be used as an input type.`,
[ definitionAST ]
);
}
const inputType = ((type: any): GraphQLInputType);
const errors = isValidJSValue(input, inputType);
if (!errors.length) {
if (isInvalid(input)) {
const defaultValue = definitionAST.defaultValue;
if (defaultValue) {
return valueFromAST(defaultValue, inputType);
const argType = argDef.type;
const argumentAST = argASTMap[name];
const defaultValue = argDef.defaultValue;
if (!argumentAST) {
if (!isInvalid(defaultValue)) {
coercedValues[name] = defaultValue;
} else if (argType instanceof GraphQLNonNull) {
throw new GraphQLError(
`Argument "${name}" of required type ` +
`"${String(argType)}" was not provided.`,
[ node ]
);
}
} else if (argumentAST.value.kind === Kind.VARIABLE) {
const variableName = (argumentAST.value: Variable).name.value;
if (variableValues && !isInvalid(variableValues[variableName])) {
// Note: this does not check that this variable value is correct.
// This assumes that this query has been validated and the variable
// usage here is of the correct type.
coercedValues[name] = variableValues[variableName];
} else if (!isInvalid(defaultValue)) {
coercedValues[name] = defaultValue;
} else if (argType instanceof GraphQLNonNull) {
throw new GraphQLError(
`Argument "${name}" of required type "${String(argType)}" was ` +
`provided the variable "$${variableName}" without a runtime value.`,
[ argumentAST.value ]
);
}
} else {
const valueAST = argumentAST.value;
const coercedValue = valueFromAST(valueAST, argType, variableValues);
if (isInvalid(coercedValue)) {
const errors = isValidLiteralValue(argType, valueAST);
const message = errors ? '\n' + errors.join('\n') : '';
throw new GraphQLError(
`Argument "${name}" got invalid value ${print(valueAST)}.${message}`,
[ def ]
);
}
coercedValues[name] = coercedValue;
}
return coerceValue(inputType, input);
}
if (isNullish(input)) {
throw new GraphQLError(
`Variable "$${variable.name.value}" of required type ` +
`"${print(definitionAST.type)}" was not provided.`,
[ definitionAST ]
);
}
const message = errors ? '\n' + errors.join('\n') : '';
throw new GraphQLError(
`Variable "$${variable.name.value}" got invalid value ` +
`${JSON.stringify(input)}.${message}`,
[ definitionAST ]
);
return coercedValues;
}

/**
Expand All @@ -135,42 +175,66 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed {
return coerceValue(type.ofType, _value);
}

if (_value === null) {
return null;
if (isInvalid(_value)) {
// Intentionally teturn no value rather than the value null.
return;
}

if (isInvalid(_value)) {
return undefined;
if (_value === null) {
// Intentionally return the value null.
return null;
}

if (type instanceof GraphQLList) {
const itemType = type.ofType;
if (isCollection(_value)) {
const coercedValues = [];
forEach((_value: any), item => {
coercedValues.push(coerceValue(itemType, item));
});
const valueIter = createIterator(_value);
if (!valueIter) {
return; // Intentionally return no value.
}
let step;
while (!(step = valueIter.next()).done) {
const itemValue = coerceValue(itemType, step.value);
if (isInvalid(itemValue)) {
return; // Intentionally return no value.
}
coercedValues.push(itemValue);
}
return coercedValues;
}
const coercedValue = coerceValue(itemType, _value);
if (isInvalid(coercedValue)) {
return; // Intentionally return no value.
}
return [ coerceValue(itemType, _value) ];
}

if (type instanceof GraphQLInputObjectType) {
if (typeof _value !== 'object' || _value === null) {
return null;
return; // Intentionally return no value.
}
const coercedObj = Object.create(null);
const fields = type.getFields();
return Object.keys(fields).reduce((obj, fieldName) => {
const fieldNames = Object.keys(fields);
for (let i = 0; i < fieldNames.length; i++) {
const fieldName = fieldNames[i];
const field = fields[fieldName];
let fieldValue = coerceValue(field.type, _value[fieldName]);
if (isInvalid(fieldValue)) {
fieldValue = field.defaultValue;
if (isInvalid(_value[fieldName])) {
if (!isInvalid(field.defaultValue)) {
coercedObj[fieldName] = field.defaultValue;
} else if (field.type instanceof GraphQLNonNull) {
return; // Intentionally return no value.
}
continue;
}
if (!isInvalid(fieldValue)) {
obj[fieldName] = fieldValue;
const fieldValue = coerceValue(field.type, _value[fieldName]);
if (isInvalid(fieldValue)) {
return; // Intentionally return no value.
}
return obj;
}, {});
coercedObj[fieldName] = fieldValue;
}
return coercedObj;
}

invariant(
Expand All @@ -179,7 +243,11 @@ function coerceValue(type: GraphQLInputType, value: mixed): mixed {
);

const parsed = type.parseValue(_value);
if (!isNullish(parsed)) {
return parsed;
if (isNullish(parsed)) {
// null or invalid values represent a failure to parse correctly,
// in which case no value is returned.
return;
}

return parsed;
}
4 changes: 2 additions & 2 deletions src/utilities/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,8 @@ function getDeprecationReason(directives: ?Array<Directive>): ?string {
return;
}
const { reason } = getArgumentValues(
GraphQLDeprecatedDirective.args,
deprecatedAST.arguments
GraphQLDeprecatedDirective,
deprecatedAST
);
return (reason: any);
}
Expand Down
Loading

0 comments on commit 6dce0c9

Please sign in to comment.