diff --git a/src/execution/values.js b/src/execution/values.js index 3b6728254ba..173f04c65ae 100644 --- a/src/execution/values.js +++ b/src/execution/values.js @@ -17,6 +17,7 @@ import type { GraphQLSchema } from '../type/schema'; import type { GraphQLField } from '../type/definition'; import type { GraphQLDirective } from '../type/directives'; import { isInputType, isNonNullType } from '../type/definition'; +import { getCoercedDefaultValue } from '../type/defaultValues'; import { typeFromAST } from '../utilities/typeFromAST'; import { valueFromAST } from '../utilities/valueFromAST'; @@ -173,8 +174,11 @@ export function getArgumentValues( const argumentNode = argNodeMap[name]; if (!argumentNode) { - if (argDef.defaultValue !== undefined) { - coercedValues[name] = argDef.defaultValue; + if (argDef.defaultValue) { + coercedValues[name] = getCoercedDefaultValue( + argDef.defaultValue, + argDef.type, + ); } else if (isNonNullType(argType)) { throw new GraphQLError( `Argument "${name}" of required type "${inspect(argType)}" ` + @@ -194,8 +198,11 @@ export function getArgumentValues( variableValues == null || !hasOwnProperty(variableValues, variableName) ) { - if (argDef.defaultValue !== undefined) { - coercedValues[name] = argDef.defaultValue; + if (argDef.defaultValue) { + coercedValues[name] = getCoercedDefaultValue( + argDef.defaultValue, + argDef.type, + ); } else if (isNonNullType(argType)) { throw new GraphQLError( `Argument "${name}" of required type "${inspect(argType)}" ` + diff --git a/src/index.d.ts b/src/index.d.ts index 317602293e5..1b6753a6b93 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -150,6 +150,7 @@ export { GraphQLArgumentConfig, GraphQLArgumentExtensions, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, diff --git a/src/index.js b/src/index.js index b46f10cd3a0..97842702e94 100644 --- a/src/index.js +++ b/src/index.js @@ -146,6 +146,7 @@ export type { GraphQLArgument, GraphQLArgumentConfig, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumValue, diff --git a/src/type/__tests__/definition-test.js b/src/type/__tests__/definition-test.js index 809e7007d34..b6d3f545cc7 100644 --- a/src/type/__tests__/definition-test.js +++ b/src/type/__tests__/definition-test.js @@ -818,6 +818,73 @@ describe('Type System: Input Objects', () => { ); }); }); + + describe('Input Object fields may have default values', () => { + it('accepts an Input Object type with a default value', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + f: { type: ScalarType, defaultValue: 3 }, + }, + }); + expect(inputObjType.getFields()).to.deep.equal({ + f: { + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: { + value: 3, + literal: undefined, + }, + deprecationReason: undefined, + extensions: undefined, + astNode: undefined, + }, + }); + }); + + it('accepts an Input Object type with a default value literal', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + f: { + type: ScalarType, + defaultValueLiteral: { kind: 'IntValue', value: '3' }, + }, + }, + }); + expect(inputObjType.getFields()).to.deep.equal({ + f: { + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: { + value: undefined, + literal: { kind: 'IntValue', value: '3' }, + }, + deprecationReason: undefined, + extensions: undefined, + astNode: undefined, + }, + }); + }); + + it('rejects an Input Object type with potentially conflicting default values', () => { + const inputObjType = new GraphQLInputObjectType({ + name: 'SomeInputObject', + fields: { + f: { + type: ScalarType, + defaultValue: 3, + defaultValueLiteral: { kind: 'IntValue', value: '3' }, + }, + }, + }); + expect(() => inputObjType.getFields()).to.throw( + 'f has both a defaultValue and a defaultValueLiteral property, but only one must be provided.', + ); + }); + }); }); describe('Type System: List', () => { diff --git a/src/type/__tests__/predicate-test.js b/src/type/__tests__/predicate-test.js index 899752864cd..2ac688b4c8b 100644 --- a/src/type/__tests__/predicate-test.js +++ b/src/type/__tests__/predicate-test.js @@ -69,6 +69,7 @@ import { assertNamedType, getNullableType, getNamedType, + defineInputValue, } from '../definition'; const ObjectType = new GraphQLObjectType({ name: 'Object', fields: {} }); @@ -562,19 +563,14 @@ describe('Type predicates', () => { }); describe('isRequiredInput', () => { - function buildArg(config: {| + function buildArg({ + type, + defaultValue, + }: {| type: GraphQLInputType, defaultValue?: mixed, |}): GraphQLArgument { - return { - name: 'someArg', - type: config.type, - description: undefined, - defaultValue: config.defaultValue, - deprecationReason: null, - extensions: undefined, - astNode: undefined, - }; + return defineInputValue({ type, defaultValue }, 'someArg'); } it('returns true for required arguments', () => { @@ -608,19 +604,14 @@ describe('Type predicates', () => { expect(isRequiredInput(optArg4)).to.equal(false); }); - function buildInputField(config: {| + function buildInputField({ + type, + defaultValue, + }: {| type: GraphQLInputType, defaultValue?: mixed, |}): GraphQLInputField { - return { - name: 'someInputField', - type: config.type, - description: undefined, - defaultValue: config.defaultValue, - deprecationReason: null, - extensions: undefined, - astNode: undefined, - }; + return defineInputValue({ type, defaultValue }, 'someInputField'); } it('returns true for required input field', () => { diff --git a/src/type/defaultValues.d.ts b/src/type/defaultValues.d.ts new file mode 100644 index 00000000000..94dbdb6722a --- /dev/null +++ b/src/type/defaultValues.d.ts @@ -0,0 +1,9 @@ +import { GraphQLInputType, GraphQLDefaultValueUsage } from './definition'; + +/** + * @internal + */ +export function getCoercedDefaultValue( + usage: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): unknown; diff --git a/src/type/defaultValues.js b/src/type/defaultValues.js new file mode 100644 index 00000000000..9c6816d3e20 --- /dev/null +++ b/src/type/defaultValues.js @@ -0,0 +1,29 @@ +import { invariant } from '../jsutils/invariant'; + +import { valueFromAST } from '../utilities/valueFromAST'; + +import type { GraphQLInputType, GraphQLDefaultValueUsage } from './definition'; + +/** + * @internal + */ +export function getCoercedDefaultValue( + usage: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): mixed { + if (usage.value !== undefined) { + return usage.value; + } + // Memoize the result of coercing the default value in a hidden field. + let coercedValue = (usage: any)._memoizedCoercedValue; + // istanbul ignore else (memoized case) + if (coercedValue === undefined) { + coercedValue = valueFromAST(usage.literal, type); + invariant( + coercedValue !== undefined, + 'Literal cannot be converted to value for this type', + ); + (usage: any)._memoizedCoercedValue = coercedValue; + } + return coercedValue; +} diff --git a/src/type/definition.d.ts b/src/type/definition.d.ts index f07202d91af..0acd9b3d4e7 100644 --- a/src/type/definition.d.ts +++ b/src/type/definition.d.ts @@ -23,6 +23,7 @@ import { FieldNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, ScalarTypeExtensionNode, UnionTypeExtensionNode, EnumTypeExtensionNode, @@ -575,12 +576,17 @@ export interface GraphQLInputValue { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: unknown; + defaultValue: Maybe; deprecationReason: Maybe; extensions: Maybe>; astNode: Maybe; } +export interface GraphQLDefaultValueUsage { + value: unknown; + literal: Maybe; +} + export interface GraphQLInputValueConfig { description?: Maybe; type: GraphQLInputType; diff --git a/src/type/definition.js b/src/type/definition.js index c16433d5b31..226ef414d09 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -41,6 +41,7 @@ import type { FieldNode, FragmentDefinitionNode, ValueNode, + ConstValueNode, } from '../language/ast'; import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped'; @@ -971,11 +972,22 @@ export function defineInputValue( !('resolve' in config), `${name} has a resolve property, but inputs cannot define resolvers.`, ); + let defaultValue; + if (config.defaultValue !== undefined || config.defaultValueLiteral) { + devAssert( + config.defaultValue === undefined || !config.defaultValueLiteral, + `${name} has both a defaultValue and a defaultValueLiteral property, but only one must be provided.`, + ); + defaultValue = { + value: config.defaultValue, + literal: config.defaultValueLiteral, + }; + } return { name, description: config.description, type: config.type, - defaultValue: config.defaultValue, + defaultValue, deprecationReason: config.deprecationReason, extensions: config.extensions && toObjMap(config.extensions), astNode: config.astNode, @@ -991,7 +1003,8 @@ export function inputValueToConfig( return { description: inputValue.description, type: inputValue.type, - defaultValue: inputValue.defaultValue, + defaultValue: inputValue.defaultValue?.value, + defaultValueLiteral: inputValue.defaultValue?.literal, deprecationReason: inputValue.deprecationReason, extensions: inputValue.extensions, astNode: inputValue.astNode, @@ -1002,16 +1015,22 @@ export type GraphQLInputValue = {| name: string, description: ?string, type: GraphQLInputType, - defaultValue: mixed, + defaultValue: ?GraphQLDefaultValueUsage, deprecationReason: ?string, extensions: ?ReadOnlyObjMap, astNode: ?InputValueDefinitionNode, |}; +export type GraphQLDefaultValueUsage = {| + value: mixed, + literal: ?ConstValueNode, +|}; + export type GraphQLInputValueConfig = {| description?: ?string, type: GraphQLInputType, defaultValue?: mixed, + defaultValueLiteral?: ?ConstValueNode, deprecationReason?: ?string, extensions?: ?ReadOnlyObjMapLike, astNode?: ?InputValueDefinitionNode, diff --git a/src/type/index.d.ts b/src/type/index.d.ts index 365256dfd1d..996f644ced6 100644 --- a/src/type/index.d.ts +++ b/src/type/index.d.ts @@ -82,6 +82,7 @@ export { GraphQLArgumentConfig, GraphQLArgumentExtensions, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumTypeExtensions, diff --git a/src/type/index.js b/src/type/index.js index 6bf0943f228..eb175f866e7 100644 --- a/src/type/index.js +++ b/src/type/index.js @@ -136,6 +136,7 @@ export type { GraphQLArgument, GraphQLArgumentConfig, GraphQLInputValue, + GraphQLDefaultValueUsage, GraphQLInputValueConfig, GraphQLEnumTypeConfig, GraphQLEnumValue, diff --git a/src/type/introspection.js b/src/type/introspection.js index f0bce5838a0..84899f1ac0f 100644 --- a/src/type/introspection.js +++ b/src/type/introspection.js @@ -384,8 +384,15 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ 'A GraphQL-formatted string representing the default value for this input value.', resolve(inputValue) { const { type, defaultValue } = inputValue; - const valueAST = astFromValue(defaultValue, type); - return valueAST ? print(valueAST) : null; + if (!defaultValue) { + return null; + } + let literal = defaultValue.literal; + if (!literal) { + literal = astFromValue(defaultValue.value, type); + invariant(literal, 'Invalid default value'); + } + return print(literal); }, }, isDeprecated: { diff --git a/src/utilities/TypeInfo.js b/src/utilities/TypeInfo.js index 10225d17549..2455309867e 100644 --- a/src/utilities/TypeInfo.js +++ b/src/utilities/TypeInfo.js @@ -209,7 +209,7 @@ export class TypeInfo { } } this._argument = argDef; - this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined); + this._defaultValueStack.push(argDef?.defaultValue?.value); this._inputTypeStack.push(isInputType(argType) ? argType : undefined); break; } @@ -233,9 +233,7 @@ export class TypeInfo { inputFieldType = inputField.type; } } - this._defaultValueStack.push( - inputField ? inputField.defaultValue : undefined, - ); + this._defaultValueStack.push(inputField?.defaultValue?.value); this._inputTypeStack.push( isInputType(inputFieldType) ? inputFieldType : undefined, ); diff --git a/src/utilities/__tests__/astFromValue-test.js b/src/utilities/__tests__/astFromValue-test.js index 3641f00227e..99dc3fdb5ae 100644 --- a/src/utilities/__tests__/astFromValue-test.js +++ b/src/utilities/__tests__/astFromValue-test.js @@ -51,6 +51,8 @@ describe('astFromValue', () => { kind: 'BooleanValue', value: false, }); + + expect(astFromValue(null, NonNullBoolean)).to.equal(null); }); it('converts Int values to Int ASTs', () => { diff --git a/src/utilities/__tests__/buildClientSchema-test.js b/src/utilities/__tests__/buildClientSchema-test.js index d3d0be8899e..504a990c2a1 100644 --- a/src/utilities/__tests__/buildClientSchema-test.js +++ b/src/utilities/__tests__/buildClientSchema-test.js @@ -441,6 +441,7 @@ describe('Type System: build schema from introspection', () => { } type Query { + defaultID(intArg: ID = "123"): String defaultInt(intArg: Int = 30): String defaultList(listArg: [Int] = [1, 2, 3]): String defaultObject(objArg: Geo = {lat: 37.485, lon: -122.148}): String @@ -596,6 +597,28 @@ describe('Type System: build schema from introspection', () => { expect(result.data).to.deep.equal({ foo: 'bar' }); }); + it('can use client schema for execution if resolvers are added', () => { + const schema = buildSchema(` + type Query { + foo(bar: String = "abc"): String + } + `); + + const introspection = introspectionFromSchema(schema); + const clientSchema = buildClientSchema(introspection); + + const QueryType: GraphQLObjectType = (clientSchema.getType('Query'): any); + QueryType.getFields().foo.resolve = (_value, args) => args.bar; + + const result = graphqlSync({ + schema: clientSchema, + source: '{ foo }', + }); + + expect(result.data).to.deep.equal({ foo: 'abc' }); + expect(result.data).to.deep.equal({ foo: 'abc' }); + }); + it('can build invalid schema', () => { const schema = buildSchema('type Query', { assumeValid: true }); diff --git a/src/utilities/__tests__/valueFromAST-test.js b/src/utilities/__tests__/valueFromAST-test.js index a4443927770..187472d0d60 100644 --- a/src/utilities/__tests__/valueFromAST-test.js +++ b/src/utilities/__tests__/valueFromAST-test.js @@ -188,6 +188,21 @@ describe('valueFromAST', () => { ); }); + it('uses default values for unprovided fields', () => { + const type = new GraphQLInputObjectType({ + name: 'TestInput', + fields: { + int: { type: GraphQLInt, defaultValue: 42 }, + float: { + type: GraphQLFloat, + defaultValueLiteral: { kind: 'FloatValue', value: '3.14' }, + }, + }, + }); + + expectValueFrom('{}', type).to.deep.equal({ int: 42, float: 3.14 }); + }); + const testInputObj = new GraphQLInputObjectType({ name: 'TestInput', fields: { diff --git a/src/utilities/astFromValue.js b/src/utilities/astFromValue.js index 5621659f73a..af9b9c74a6d 100644 --- a/src/utilities/astFromValue.js +++ b/src/utilities/astFromValue.js @@ -3,7 +3,7 @@ import { invariant } from '../jsutils/invariant'; import { isObjectLike } from '../jsutils/isObjectLike'; import { isIterableObject } from '../jsutils/isIterableObject'; -import type { ValueNode } from '../language/ast'; +import type { ConstValueNode } from '../language/ast'; import { Kind } from '../language/kinds'; import type { GraphQLInputType } from '../type/definition'; @@ -37,13 +37,15 @@ import { * | null | NullValue | * */ -export function astFromValue(value: mixed, type: GraphQLInputType): ?ValueNode { +export function astFromValue( + value: mixed, + type: GraphQLInputType, +): ?ConstValueNode { if (isNonNullType(type)) { - const astValue = astFromValue(value, type.ofType); - if (astValue?.kind === Kind.NULL) { + if (value === null) { return null; } - return astValue; + return astFromValue(value, type.ofType); } // only explicit null, not undefined, NaN diff --git a/src/utilities/buildClientSchema.js b/src/utilities/buildClientSchema.js index 487ee6d16e2..29daeb282cb 100644 --- a/src/utilities/buildClientSchema.js +++ b/src/utilities/buildClientSchema.js @@ -3,7 +3,7 @@ import { devAssert } from '../jsutils/devAssert'; import { keyValMap } from '../jsutils/keyValMap'; import { isObjectLike } from '../jsutils/isObjectLike'; -import { parseValue } from '../language/parser'; +import { parseConstValue } from '../language/parser'; import type { GraphQLSchemaValidationOptions } from '../type/schema'; import type { @@ -47,7 +47,6 @@ import type { IntrospectionTypeRef, IntrospectionNamedTypeRef, } from './getIntrospectionQuery'; -import { valueFromAST } from './valueFromAST'; /** * Build a GraphQLSchema for use by client tools. @@ -367,14 +366,13 @@ export function buildClientSchema( ); } - const defaultValue = - inputValueIntrospection.defaultValue != null - ? valueFromAST(parseValue(inputValueIntrospection.defaultValue), type) - : undefined; return { description: inputValueIntrospection.description, type, - defaultValue, + defaultValueLiteral: + inputValueIntrospection.defaultValue != null + ? parseConstValue(inputValueIntrospection.defaultValue) + : undefined, deprecationReason: inputValueIntrospection.deprecationReason, }; } diff --git a/src/utilities/coerceInputValue.js b/src/utilities/coerceInputValue.js index 9c9546fbc2e..31896064f50 100644 --- a/src/utilities/coerceInputValue.js +++ b/src/utilities/coerceInputValue.js @@ -17,6 +17,7 @@ import { isListType, isNonNullType, } from '../type/definition'; +import { getCoercedDefaultValue } from '../type/defaultValues'; type OnErrorCB = ( path: $ReadOnlyArray, @@ -102,8 +103,11 @@ function coerceInputValueImpl( const fieldValue = inputValue[field.name]; if (fieldValue === undefined) { - if (field.defaultValue !== undefined) { - coercedValue[field.name] = field.defaultValue; + if (field.defaultValue) { + coercedValue[field.name] = getCoercedDefaultValue( + field.defaultValue, + field.type, + ); } else if (isNonNullType(field.type)) { const typeStr = inspect(field.type); onError( diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 9c55417d95e..9c2261a691d 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -79,8 +79,6 @@ import { GraphQLInputObjectType, } from '../type/definition'; -import { valueFromAST } from './valueFromAST'; - type Options = {| ...GraphQLSchemaValidationOptions, @@ -494,7 +492,7 @@ export function extendSchemaImpl( configMap[node.name.value] = { type, description: node.description?.value, - defaultValue: valueFromAST(node.defaultValue, type), + defaultValueLiteral: node.defaultValue, deprecationReason: getDeprecationReason(node), astNode: node, }; diff --git a/src/utilities/findBreakingChanges.js b/src/utilities/findBreakingChanges.js index cc7ffe74433..629ace0151d 100644 --- a/src/utilities/findBreakingChanges.js +++ b/src/utilities/findBreakingChanges.js @@ -17,6 +17,7 @@ import type { GraphQLObjectType, GraphQLInterfaceType, GraphQLInputObjectType, + GraphQLDefaultValueUsage, } from '../type/definition'; import { isSpecifiedScalarType } from '../type/scalars'; import { @@ -402,18 +403,19 @@ function findArgChanges( `${oldType.name}.${oldField.name} arg ${oldArg.name} has changed type from ` + `${String(oldArg.type)} to ${String(newArg.type)}.`, }); - } else if (oldArg.defaultValue !== undefined) { - if (newArg.defaultValue === undefined) { + } else if (oldArg.defaultValue) { + if (!newArg.defaultValue) { schemaChanges.push({ type: DangerousChangeType.ARG_DEFAULT_VALUE_CHANGE, description: `${oldType.name}.${oldField.name} arg ${oldArg.name} defaultValue was removed.`, }); } else { + const newArgDefaultValue = newArg.defaultValue; // Since we looking only for client's observable changes we should // compare default values in the same representation as they are // represented inside introspection. - const oldValueStr = stringifyValue(oldArg.defaultValue, oldArg.type); - const newValueStr = stringifyValue(newArg.defaultValue, newArg.type); + const oldValueStr = printDefaultValue(oldArg.defaultValue, oldArg.type); + const newValueStr = printDefaultValue(newArgDefaultValue, newArg.type); if (oldValueStr !== newValueStr) { schemaChanges.push({ @@ -533,10 +535,15 @@ function typeKindName(type: GraphQLNamedType): string { invariant(false, 'Unexpected type: ' + inspect((type: empty))); } -function stringifyValue(value: mixed, type: GraphQLInputType): string { - const ast = astFromValue(value, type); - invariant(ast != null); - +function printDefaultValue( + defaultValue: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): string { + let ast = defaultValue.literal; + if (!ast) { + ast = astFromValue(defaultValue.value, type); + invariant(ast); + } const sortedAST = visit(ast, { ObjectValue(objectNode) { // Make a copy since sort mutates array diff --git a/src/utilities/printSchema.js b/src/utilities/printSchema.js index cc41725368a..bd254ee3967 100644 --- a/src/utilities/printSchema.js +++ b/src/utilities/printSchema.js @@ -258,10 +258,14 @@ function printArgs( } function printInputValue(arg: GraphQLInputField): string { - const defaultAST = astFromValue(arg.defaultValue, arg.type); let argDecl = arg.name + ': ' + String(arg.type); - if (defaultAST) { - argDecl += ` = ${print(defaultAST)}`; + if (arg.defaultValue) { + let literal = arg.defaultValue.literal; + if (!literal) { + literal = astFromValue(arg.defaultValue.value, arg.type); + invariant(literal, 'Invalid default value'); + } + argDecl += ` = ${print(literal)}`; } return argDecl + printDeprecated(arg.deprecationReason); } diff --git a/src/utilities/valueFromAST.js b/src/utilities/valueFromAST.js index 258976462b9..19081ea96e0 100644 --- a/src/utilities/valueFromAST.js +++ b/src/utilities/valueFromAST.js @@ -111,8 +111,13 @@ export function valueFromAST( for (const field of Object.values(type.getFields())) { const fieldNode = fieldNodes[field.name]; if (!fieldNode || isMissingVariable(fieldNode.value, variables)) { - if (field.defaultValue !== undefined) { - coercedObj[field.name] = field.defaultValue; + if (field.defaultValue) { + // NOTE: This is an inlined version of "getCoercedDefaultValue" which + // cannot be used directly since it would cause a circular dependency. + coercedObj[field.name] = + field.defaultValue.value !== undefined + ? field.defaultValue.value + : valueFromAST(field.defaultValue.literal, field.type); } else if (isNonNullType(field.type)) { return; // Invalid: intentionally return no value. }