diff --git a/.eslintrc b/.eslintrc index 9d0b76ea4a..6b09ba8770 100644 --- a/.eslintrc +++ b/.eslintrc @@ -181,7 +181,7 @@ "no-unneeded-ternary": 2, "no-unreachable": 2, "no-unused-expressions": 2, - "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], + "no-unused-vars": [2, {"vars": "all", "args": "after-used", "argsIgnorePattern": "^_"}], "no-use-before-define": 0, "no-useless-call": 2, "no-useless-escape": 2, diff --git a/src/index.js b/src/index.js index 26d86086c4..fa04a8f393 100644 --- a/src/index.js +++ b/src/index.js @@ -330,9 +330,12 @@ export { // Create a GraphQLType from a GraphQL language AST. typeFromAST, - // Create a JavaScript value from a GraphQL language AST. + // Create a JavaScript value from a GraphQL language AST with a Type. valueFromAST, + // Create a JavaScript value from a GraphQL language AST without a Type. + valueFromASTUntyped, + // Create a GraphQL language AST from a JavaScript value. astFromValue, diff --git a/src/type/definition.js b/src/type/definition.js index d8ea027037..c41d994ca5 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -8,10 +8,11 @@ */ import invariant from '../jsutils/invariant'; -import isNullish from '../jsutils/isNullish'; +import isInvalid from '../jsutils/isInvalid'; import type {ObjMap} from '../jsutils/ObjMap'; import * as Kind from '../language/kinds'; import { assertValidName } from '../utilities/assertValidName'; +import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped'; import type { ScalarTypeDefinitionNode, ObjectTypeDefinitionNode, @@ -339,27 +340,30 @@ export class GraphQLScalarType { } // Determines if an internal value is valid for this type. - // Equivalent to checking for if the parsedValue is nullish. isValidValue(value: mixed): boolean { - return !isNullish(this.parseValue(value)); + return !isInvalid(this.parseValue(value)); } // Parses an externally provided value to use as an input. parseValue(value: mixed): mixed { const parser = this._scalarConfig.parseValue; - return parser && !isNullish(value) ? parser(value) : undefined; + if (isInvalid(value)) { + return undefined; + } + return parser ? parser(value) : value; } // Determines if an internal value is valid for this type. - // Equivalent to checking for if the parsedLiteral is nullish. - isValidLiteral(valueNode: ValueNode): boolean { - return !isNullish(this.parseLiteral(valueNode)); + isValidLiteral(valueNode: ValueNode, variables: ?ObjMap): boolean { + return !isInvalid(this.parseLiteral(valueNode, variables)); } // Parses an externally provided literal value to use as an input. - parseLiteral(valueNode: ValueNode): mixed { + parseLiteral(valueNode: ValueNode, variables: ?ObjMap): mixed { const parser = this._scalarConfig.parseLiteral; - return parser ? parser(valueNode) : undefined; + return parser ? + parser(valueNode, variables) : + valueFromASTUntyped(valueNode, variables); } toString(): string { @@ -381,7 +385,10 @@ export type GraphQLScalarTypeConfig = { astNode?: ?ScalarTypeDefinitionNode; serialize: (value: mixed) => ?TExternal; parseValue?: (value: mixed) => ?TInternal; - parseLiteral?: (valueNode: ValueNode) => ?TInternal; + parseLiteral?: ( + valueNode: ValueNode, + variables: ?ObjMap, + ) => ?TInternal; }; @@ -952,12 +959,14 @@ export class GraphQLEnumType/* */ { } } - isValidLiteral(valueNode: ValueNode): boolean { + isValidLiteral(valueNode: ValueNode, _variables: ?ObjMap): boolean { + // Note: variables will be resolved to a value before calling this function. return valueNode.kind === Kind.ENUM && this._getNameLookup()[valueNode.value] !== undefined; } - parseLiteral(valueNode: ValueNode): ?any/* T */ { + parseLiteral(valueNode: ValueNode, _variables: ?ObjMap): ?any/* T */ { + // Note: variables will be resolved to a value before calling this function. if (valueNode.kind === Kind.ENUM) { const enumValue = this._getNameLookup()[valueNode.value]; if (enumValue) { diff --git a/src/type/scalars.js b/src/type/scalars.js index 35ae96368c..46d636b771 100644 --- a/src/type/scalars.js +++ b/src/type/scalars.js @@ -53,7 +53,7 @@ export const GraphQLInt = new GraphQLScalarType({ return num; } } - return null; + return undefined; } }); @@ -83,7 +83,7 @@ export const GraphQLFloat = new GraphQLScalarType({ parseLiteral(ast) { return ast.kind === Kind.FLOAT || ast.kind === Kind.INT ? parseFloat(ast.value) : - null; + undefined; } }); @@ -105,7 +105,7 @@ export const GraphQLString = new GraphQLScalarType({ serialize: coerceString, parseValue: coerceString, parseLiteral(ast) { - return ast.kind === Kind.STRING ? ast.value : null; + return ast.kind === Kind.STRING ? ast.value : undefined; } }); @@ -115,7 +115,7 @@ export const GraphQLBoolean = new GraphQLScalarType({ serialize: Boolean, parseValue: Boolean, parseLiteral(ast) { - return ast.kind === Kind.BOOLEAN ? ast.value : null; + return ast.kind === Kind.BOOLEAN ? ast.value : undefined; } }); @@ -132,6 +132,6 @@ export const GraphQLID = new GraphQLScalarType({ parseLiteral(ast) { return ast.kind === Kind.STRING || ast.kind === Kind.INT ? ast.value : - null; + undefined; } }); diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 912f717f66..805fef52e8 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -473,6 +473,22 @@ describe('Schema Builder', () => { expect(output).to.equal(body); }); + it('Custom scalar argument field with default', () => { + const body = dedent` + schema { + query: Hello + } + + scalar CustomScalar + + type Hello { + str(int: CustomScalar = 2): String + } + `; + const output = cycleOutput(body); + expect(output).to.equal(body); + }); + it('Simple type with mutation', () => { const body = dedent` schema { diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index 33bd16146b..f97a6d2c86 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -341,6 +341,30 @@ describe('Type System: build schema from introspection', () => { await testSchema(schema); }); + it('builds a schema with default value on custom scalar field', async () => { + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'ArgFields', + fields: { + testField: { + type: GraphQLString, + args: { + testArg: { + type: new GraphQLScalarType({ + name: 'CustomScalar', + serialize: value => value + }), + defaultValue: 'default' + } + } + } + } + }) + }); + + await testSchema(schema); + }); + it('builds a schema with an enum', async () => { const foodEnum = new GraphQLEnumType({ name: 'Food', diff --git a/src/utilities/__tests__/valueFromASTUntyped-test.js b/src/utilities/__tests__/valueFromASTUntyped-test.js new file mode 100644 index 0000000000..0b7ae5815b --- /dev/null +++ b/src/utilities/__tests__/valueFromASTUntyped-test.js @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2017, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { describe, it } from 'mocha'; +import { expect } from 'chai'; +import { valueFromASTUntyped } from '../valueFromASTUntyped'; +import { parseValue } from '../../language'; + +describe('valueFromASTUntyped', () => { + + function testCase(valueText, expected) { + expect( + valueFromASTUntyped(parseValue(valueText)) + ).to.deep.equal(expected); + } + + function testCaseWithVars(valueText, variables, expected) { + expect( + valueFromASTUntyped(parseValue(valueText), variables) + ).to.deep.equal(expected); + } + + it('parses simple values', () => { + testCase('null', null); + testCase('true', true); + testCase('false', false); + testCase('123', 123); + testCase('123.456', 123.456); + testCase('"abc123"', 'abc123'); + }); + + it('parses lists of values', () => { + testCase('[true, false]', [ true, false ]); + testCase('[true, 123.45]', [ true, 123.45 ]); + testCase('[true, null]', [ true, null ]); + testCase('[true, ["foo", 1.2]]', [ true, [ 'foo', 1.2 ] ]); + }); + + it('parses input objects', () => { + testCase( + '{ int: 123, bool: false }', + { int: 123, bool: false } + ); + testCase( + '{ foo: [ { bar: "baz"} ] }', + { foo: [ { bar: 'baz'} ] } + ); + }); + + it('parses enum values as plain strings', () => { + testCase('TEST_ENUM_VALUE', 'TEST_ENUM_VALUE'); + testCase('[TEST_ENUM_VALUE]', [ 'TEST_ENUM_VALUE' ]); + }); + + it('parses variables', () => { + testCaseWithVars('$testVariable', { testVariable: 'foo' }, 'foo'); + testCaseWithVars('[$testVariable]', { testVariable: 'foo' }, [ 'foo' ]); + testCaseWithVars( + '{a:[$testVariable]}', + { testVariable: 'foo' }, + { a: [ 'foo' ] } + ); + testCaseWithVars('$testVariable', { testVariable: null }, null); + testCaseWithVars('$testVariable', {}, undefined); + }); + +}); diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 5888a48f2c..45ce79d238 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -446,13 +446,7 @@ export function buildASTSchema( name: def.name.value, description: getDescription(def, options), astNode: def, - serialize: () => null, - // Note: validation calls the parse functions to determine if a - // literal value is correct. Returning null would cause use of custom - // scalars to always fail validation. Returning false causes them to - // always pass validation. - parseValue: () => false, - parseLiteral: () => false, + serialize: value => value, }); } diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 443e64629b..dcbbc3b04e 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -236,13 +236,7 @@ export function buildClientSchema( return new GraphQLScalarType({ name: scalarIntrospection.name, description: scalarIntrospection.description, - serialize: id => id, - // Note: validation calls the parse functions to determine if a - // literal value is correct. Returning null would cause use of custom - // scalars to always fail validation. Returning false causes them to - // always pass validation. - parseValue: () => false, - parseLiteral: () => false, + serialize: value => value, }); } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 19eb2dba26..c5ce95d4c5 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -506,12 +506,6 @@ export function extendSchema( description: getDescription(typeNode, options), astNode: typeNode, serialize: id => id, - // Note: validation calls the parse functions to determine if a - // literal value is correct. Returning null would cause use of custom - // scalars to always fail validation. Returning false causes them to - // always pass validation. - parseValue: () => false, - parseLiteral: () => false, }); } diff --git a/src/utilities/index.js b/src/utilities/index.js index ce8c87cf42..abb57a1c21 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -55,9 +55,12 @@ export { // Create a GraphQLType from a GraphQL language AST. export { typeFromAST } from './typeFromAST'; -// Create a JavaScript value from a GraphQL language AST. +// Create a JavaScript value from a GraphQL language AST with a type. export { valueFromAST } from './valueFromAST'; +// Create a JavaScript value from a GraphQL language AST without a type. +export { valueFromASTUntyped } from './valueFromASTUntyped'; + // Create a GraphQL language AST from a JavaScript value. export { astFromValue } from './astFromValue'; diff --git a/src/utilities/isValidLiteralValue.js b/src/utilities/isValidLiteralValue.js index 5ac46a356f..450ef7a70b 100644 --- a/src/utilities/isValidLiteralValue.js +++ b/src/utilities/isValidLiteralValue.js @@ -109,7 +109,7 @@ export function isValidLiteralValue( ); // Scalars determine if a literal values is valid. - if (!type.isValidLiteral(valueNode)) { + if (!type.isValidLiteral(valueNode, null)) { return [ `Expected type "${type.name}", found ${print(valueNode)}.` ]; } diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index d29d5b9325..0e4dad6d59 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -9,7 +9,6 @@ import keyMap from '../jsutils/keyMap'; import invariant from '../jsutils/invariant'; -import isNullish from '../jsutils/isNullish'; import isInvalid from '../jsutils/isInvalid'; import type {ObjMap} from '../jsutils/ObjMap'; import * as Kind from '../language/kinds'; @@ -151,14 +150,13 @@ export function valueFromAST( 'Must be input type' ); - const parsed = type.parseLiteral(valueNode); - if (isNullish(parsed) && !type.isValidLiteral(valueNode)) { - // Invalid values represent a failure to parse correctly, in which case - // no value is returned. - return; + // Scalar and Enum values implement methods which perform this translation. + if (type.isValidLiteral(valueNode, variables)) { + return type.parseLiteral(valueNode, variables); } - return parsed; + // Invalid values represent a failure to parse correctly, in which case + // no value is returned. } // Returns true if the provided valueNode is a variable which is not defined diff --git a/src/utilities/valueFromASTUntyped.js b/src/utilities/valueFromASTUntyped.js new file mode 100644 index 0000000000..a8b4d0b472 --- /dev/null +++ b/src/utilities/valueFromASTUntyped.js @@ -0,0 +1,64 @@ +/* @flow */ +/** + * Copyright (c) 2017, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import keyValMap from '../jsutils/keyValMap'; +import isInvalid from '../jsutils/isInvalid'; +import type { ObjMap } from '../jsutils/ObjMap'; +import * as Kind from '../language/kinds'; +import type { ValueNode } from '../language/ast'; + +/** + * Produces a JavaScript value given a GraphQL Value AST. + * + * Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value + * will reflect the provided GraphQL value AST. + * + * | GraphQL Value | JavaScript Value | + * | -------------------- | ---------------- | + * | Input Object | Object | + * | List | Array | + * | Boolean | Boolean | + * | String / Enum | String | + * | Int / Float | Number | + * | Null | null | + * + */ +export function valueFromASTUntyped( + valueNode: ValueNode, + variables?: ?ObjMap, +): mixed { + switch (valueNode.kind) { + case Kind.NULL: + return null; + case Kind.INT: + return parseInt(valueNode.value, 10); + case Kind.FLOAT: + return parseFloat(valueNode.value); + case Kind.STRING: + case Kind.ENUM: + case Kind.BOOLEAN: + return valueNode.value; + case Kind.LIST: + return valueNode.values.map(node => valueFromASTUntyped(node, variables)); + case Kind.OBJECT: + return keyValMap( + valueNode.fields, + field => field.name.value, + field => valueFromASTUntyped(field.value, variables), + ); + case Kind.VARIABLE: + const variableName = valueNode.name.value; + return variables && !isInvalid(variables[variableName]) ? + variables[variableName] : + undefined; + default: + throw new Error('Unexpected value kind: ' + (valueNode.kind: empty)); + } +}