diff --git a/src/execution/values.ts b/src/execution/values.ts index 346abc38e9..7ce5bca3b3 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -23,6 +23,7 @@ import { typeFromAST } from '../utilities/typeFromAST'; import { coerceInputValue, coerceInputLiteral, + coerceDefaultValue, } from '../utilities/coerceInputValue'; type CoercedVariableValues = @@ -177,8 +178,11 @@ export function getArgumentValues( const argumentNode = argNodeMap[name]; if (!argumentNode) { - if (argDef.defaultValue !== undefined) { - coercedValues[name] = argDef.defaultValue; + if (argDef.defaultValue) { + coercedValues[name] = coerceDefaultValue( + argDef.defaultValue, + argDef.type, + ); } else if (isNonNullType(argType)) { throw new GraphQLError( `Argument ${argDef} of required type ${argType} was not provided.`, @@ -197,8 +201,11 @@ export function getArgumentValues( variableValues == null || !hasOwnProperty(variableValues, variableName) ) { - if (argDef.defaultValue !== undefined) { - coercedValues[name] = argDef.defaultValue; + if (argDef.defaultValue) { + coercedValues[name] = coerceDefaultValue( + argDef.defaultValue, + argDef.type, + ); } else if (isNonNullType(argType)) { throw new GraphQLError( `Argument ${argDef} of required type ${argType} ` + diff --git a/src/index.ts b/src/index.ts index 183d249a89..702fc5d108 100644 --- a/src/index.ts +++ b/src/index.ts @@ -181,6 +181,7 @@ export type { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, + GraphQLDefaultValueUsage, } from './type/index'; /** Parse and operate on GraphQL language source files. */ diff --git a/src/type/__tests__/definition-test.ts b/src/type/__tests__/definition-test.ts index 80c17c7f40..287ab112e5 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -826,6 +826,69 @@ 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: { + coordinate: 'SomeInputObject.f', + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: { value: 3 }, + 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: { + coordinate: 'SomeInputObject.f', + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: { 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/definition.ts b/src/type/definition.ts index 09cb797413..df6372378a 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -21,6 +21,7 @@ import { print } from '../language/printer'; import type { FieldNode, ValueNode, + ConstValueNode, OperationDefinitionNode, FragmentDefinitionNode, ScalarTypeDefinitionNode, @@ -964,6 +965,7 @@ export interface GraphQLArgumentConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: unknown; + defaultValueLiteral?: ConstValueNode; deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; @@ -1049,7 +1051,7 @@ export class GraphQLArgument extends GraphQLSchemaElement { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: unknown; + defaultValue: GraphQLDefaultValueUsage | undefined; deprecationReason: Maybe; extensions: Maybe>; astNode: Maybe; @@ -1064,7 +1066,7 @@ export class GraphQLArgument extends GraphQLSchemaElement { this.name = name; this.description = config.description; this.type = config.type; - this.defaultValue = config.defaultValue; + this.defaultValue = defineDefaultValue(coordinate, config); this.deprecationReason = config.deprecationReason; this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; @@ -1074,7 +1076,8 @@ export class GraphQLArgument extends GraphQLSchemaElement { return { description: this.description, type: this.type, - defaultValue: this.defaultValue, + defaultValue: this.defaultValue?.value, + defaultValueLiteral: this.defaultValue?.literal, deprecationReason: this.deprecationReason, extensions: this.extensions, astNode: this.astNode, @@ -1090,6 +1093,26 @@ export type GraphQLFieldMap = ObjMap< GraphQLField >; +export type GraphQLDefaultValueUsage = + | { value: unknown; literal?: never } + | { literal: ConstValueNode; value?: never }; + +function defineDefaultValue( + coordinate: string, + config: GraphQLArgumentConfig | GraphQLInputFieldConfig, +): GraphQLDefaultValueUsage | undefined { + if (config.defaultValue === undefined && !config.defaultValueLiteral) { + return; + } + devAssert( + !(config.defaultValue !== undefined && config.defaultValueLiteral), + `${coordinate} has both a defaultValue and a defaultValueLiteral property, but only one must be provided.`, + ); + return config.defaultValueLiteral + ? { literal: config.defaultValueLiteral } + : { value: config.defaultValue }; +} + /** * Custom extensions * @@ -1694,6 +1717,7 @@ export interface GraphQLInputFieldConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: unknown; + defaultValueLiteral?: ConstValueNode; deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; @@ -1705,7 +1729,7 @@ export class GraphQLInputField extends GraphQLSchemaElement { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: unknown; + defaultValue: GraphQLDefaultValueUsage | undefined; deprecationReason: Maybe; extensions: Maybe>; astNode: Maybe; @@ -1726,7 +1750,7 @@ export class GraphQLInputField extends GraphQLSchemaElement { this.name = name; this.description = config.description; this.type = config.type; - this.defaultValue = config.defaultValue; + this.defaultValue = defineDefaultValue(coordinate, config); this.deprecationReason = config.deprecationReason; this.extensions = config.extensions && toObjMap(config.extensions); this.astNode = config.astNode; @@ -1736,7 +1760,8 @@ export class GraphQLInputField extends GraphQLSchemaElement { return { description: this.description, type: this.type, - defaultValue: this.defaultValue, + defaultValue: this.defaultValue?.value, + defaultValueLiteral: this.defaultValue?.literal, extensions: this.extensions, astNode: this.astNode, }; diff --git a/src/type/index.ts b/src/type/index.ts index f4c419bb66..8ef0c1f07e 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -116,6 +116,7 @@ export type { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, + GraphQLDefaultValueUsage, } from './definition'; export { diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 6dfd193f5e..fd4f81c15b 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -384,8 +384,13 @@ 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; + } + const literal = + defaultValue.literal ?? astFromValue(defaultValue.value, type); + invariant(literal, 'Invalid default value'); + return print(literal); }, }, isDeprecated: { diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index f9ba319ce6..322b961986 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -17,6 +17,7 @@ import type { GraphQLArgument, GraphQLInputField, GraphQLEnumValue, + GraphQLDefaultValueUsage, } from '../type/definition'; import { isObjectType, @@ -49,7 +50,7 @@ export class TypeInfo { private _parentTypeStack: Array>; private _inputTypeStack: Array>; private _fieldDefStack: Array>>; - private _defaultValueStack: Array>; + private _defaultValueStack: Array; private _directive: Maybe; private _argument: Maybe; private _enumValue: Maybe; @@ -119,7 +120,7 @@ export class TypeInfo { } } - getDefaultValue(): Maybe { + getDefaultValue(): GraphQLDefaultValueUsage | undefined { if (this._defaultValueStack.length > 0) { return this._defaultValueStack[this._defaultValueStack.length - 1]; } diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index 448ac2bcdb..478f4c824c 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -444,6 +444,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 @@ -599,6 +600,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') as 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__/coerceInputValue-test.ts b/src/utilities/__tests__/coerceInputValue-test.ts index c76bf0cc5d..145c231972 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -24,7 +24,11 @@ import { GraphQLInputObjectType, } from '../../type/definition'; -import { coerceInputValue, coerceInputLiteral } from '../coerceInputValue'; +import { + coerceInputValue, + coerceInputLiteral, + coerceDefaultValue, +} from '../coerceInputValue'; interface CoerceResult { value: unknown; @@ -610,10 +614,14 @@ describe('coerceInputLiteral', () => { name: 'TestInput', fields: { int: { type: GraphQLInt, defaultValue: 42 }, + float: { + type: GraphQLFloat, + defaultValueLiteral: { kind: 'FloatValue', value: '3.14' }, + }, }, }); - test('{}', type, { int: 42 }); + test('{}', type, { int: 42, float: 3.14 }); }); const testInputObj = new GraphQLInputObjectType({ @@ -681,3 +689,26 @@ describe('coerceInputLiteral', () => { }); }); }); + +describe('coerceDefaultValue', () => { + it('memoizes coercion', () => { + const parseValueCalls: any = []; + + const spyScalar = new GraphQLScalarType({ + name: 'SpyScalar', + parseValue(value) { + parseValueCalls.push(value); + return value; + }, + }); + + const defaultValueUsage = { + literal: { kind: 'StringValue', value: 'hello' }, + } as const; + expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello'); + + // Call a second time + expect(coerceDefaultValue(defaultValueUsage, spyScalar)).to.equal('hello'); + expect(parseValueCalls).to.deep.equal(['hello']); + }); +}); diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index 6ba9cff68e..7158f7bd0f 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -4,7 +4,7 @@ import { isObjectLike } from '../jsutils/isObjectLike'; import { isIterableObject } from '../jsutils/isIterableObject'; import type { Maybe } from '../jsutils/Maybe'; -import type { ValueNode } from '../language/ast'; +import type { ConstValueNode } from '../language/ast'; import { Kind } from '../language/kinds'; import type { GraphQLInputType } from '../type/definition'; @@ -41,7 +41,7 @@ import { export function astFromValue( value: unknown, type: GraphQLInputType, -): Maybe { +): Maybe { if (isNonNullType(type)) { const astValue = astFromValue(value, type.ofType); if (astValue?.kind === Kind.NULL) { diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 1bfe2e6901..abd186b028 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -47,7 +47,6 @@ import type { IntrospectionTypeRef, IntrospectionNamedTypeRef, } from './getIntrospectionQuery'; -import { coerceInputLiteral } from './coerceInputValue'; /** * Build a GraphQLSchema for use by client tools. @@ -366,17 +365,13 @@ export function buildClientSchema( ); } - const defaultValue = - inputValueIntrospection.defaultValue != null - ? coerceInputLiteral( - parseConstValue(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.ts b/src/utilities/coerceInputValue.ts index 021194b0c0..9c25caf62f 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -14,7 +14,10 @@ import { isIterableObject } from '../jsutils/isIterableObject'; import { GraphQLError } from '../error/GraphQLError'; -import type { GraphQLInputType } from '../type/definition'; +import type { + GraphQLInputType, + GraphQLDefaultValueUsage, +} from '../type/definition'; import { isLeafType, assertLeafType, @@ -111,8 +114,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] = coerceDefaultValue( + field.defaultValue, + field.type, + ); } else if (isNonNullType(field.type)) { const typeStr = inspect(field.type); onError( @@ -279,8 +285,11 @@ export function coerceInputLiteral( if (isRequiredInputField(field)) { return; // Invalid: intentionally return no value. } - if (field.defaultValue !== undefined) { - coercedValue[field.name] = field.defaultValue; + if (field.defaultValue) { + coercedValue[field.name] = coerceDefaultValue( + field.defaultValue, + field.type, + ); } } else { const fieldValue = coerceInputLiteral( @@ -317,3 +326,23 @@ function isMissingVariable( (variables == null || variables[valueNode.name.value] === undefined) ); } + +/** + * @internal + */ +export function coerceDefaultValue( + defaultValue: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): unknown { + // Memoize the result of coercing the default value in a hidden field. + let coercedValue = (defaultValue as any)._memoizedCoercedValue; + // istanbul ignore else (memoized case) + if (coercedValue === undefined) { + coercedValue = defaultValue.literal + ? coerceInputLiteral(defaultValue.literal, type) + : defaultValue.value; + invariant(coercedValue !== undefined); + (defaultValue as any)._memoizedCoercedValue = coercedValue; + } + return coercedValue; +} diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 2597998478..7ec9671905 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -80,8 +80,6 @@ import { GraphQLInputObjectType, } from '../type/definition'; -import { coerceInputLiteral } from './coerceInputValue'; - interface Options extends GraphQLSchemaValidationOptions { /** * Set to true to assume the SDL is valid. @@ -491,9 +489,7 @@ export function extendSchemaImpl( argConfigMap[arg.name.value] = { type, description: arg.description?.value, - defaultValue: arg.defaultValue - ? coerceInputLiteral(arg.defaultValue, type) - : undefined, + defaultValueLiteral: arg.defaultValue, deprecationReason: getDeprecationReason(arg), astNode: arg, }; @@ -520,9 +516,7 @@ export function extendSchemaImpl( inputFieldMap[field.name.value] = { type, description: field.description?.value, - defaultValue: field.defaultValue - ? coerceInputLiteral(field.defaultValue, type) - : undefined, + defaultValueLiteral: field.defaultValue, deprecationReason: getDeprecationReason(field), astNode: field, }; diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index e79fd8043a..fe058d0fd8 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -17,6 +17,7 @@ import type { GraphQLObjectType, GraphQLInterfaceType, GraphQLInputObjectType, + GraphQLDefaultValueUsage, } from '../type/definition'; import { isSpecifiedScalarType } from '../type/scalars'; import { @@ -527,10 +528,12 @@ function typeKindName(type: GraphQLNamedType): string { invariant(false, 'Unexpected type: ' + inspect(type)); } -function stringifyValue(value: unknown, type: GraphQLInputType): string { - const ast = astFromValue(value, type); - invariant(ast != null); - +function stringifyValue( + defaultValue: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): string { + const ast = defaultValue.literal ?? 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.ts b/src/utilities/printSchema.ts index 42d48c6a6c..0f76080c62 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -259,10 +259,13 @@ 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) { + const literal = + arg.defaultValue.literal ?? + astFromValue(arg.defaultValue.value, arg.type); + invariant(literal, 'Invalid default value'); + argDecl += ` = ${print(literal)}`; } return argDecl + printDeprecated(arg.deprecationReason); } diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index 93bf43d391..bf47f2fd52 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -25,6 +25,7 @@ import type { GraphQLField, GraphQLArgument, GraphQLEnumValue, + GraphQLDefaultValueUsage, } from '../type/definition'; import { TypeInfo, visitWithTypeInfo } from '../utilities/TypeInfo'; @@ -33,7 +34,7 @@ type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode; interface VariableUsage { readonly node: VariableNode; readonly type: Maybe; - readonly defaultValue: Maybe; + readonly defaultValue: GraphQLDefaultValueUsage | undefined; } /** diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts index 9312f88548..e7b948c39f 100644 --- a/src/validation/rules/VariablesInAllowedPositionRule.ts +++ b/src/validation/rules/VariablesInAllowedPositionRule.ts @@ -7,7 +7,10 @@ import type { ValueNode } from '../../language/ast'; import type { ASTVisitor } from '../../language/visitor'; import type { GraphQLSchema } from '../../type/schema'; -import type { GraphQLType } from '../../type/definition'; +import type { + GraphQLType, + GraphQLDefaultValueUsage, +} from '../../type/definition'; import { isNonNullType } from '../../type/definition'; import { typeFromAST } from '../../utilities/typeFromAST'; @@ -79,7 +82,7 @@ function allowedVariableUsage( varType: GraphQLType, varDefaultValue: Maybe, locationType: GraphQLType, - locationDefaultValue: Maybe, + locationDefaultValue: GraphQLDefaultValueUsage | undefined, ): boolean { if (isNonNullType(locationType) && !isNonNullType(varType)) { const hasNonNullVariableDefaultValue =