From 04398572cc8b8d3737b4f6c382060e3b85cb8d25 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Mon, 10 May 2021 11:44:12 -0700 Subject: [PATCH] Preserve defaultValue literals Fixes #3051 This change solves the problem of default values defined via SDL not always resolving correctly through introspection by preserving the original GraphQL literal in the schema definition. This changes argument and input field definitions `defaultValue` field from just the "value" to a new `GraphQLDefaultValueUsage` type which contains either or both "value" and "literal" fields. Since either of these fields may be set, new functions for resolving a value or literal from either have been added - `getLiteralDefaultValue` and `getCoercedDefaultValue` - these replace uses that either assumed a set value or were manually converting a value back to a literal. Here is the flow for how a default value defined in an SDL would be converted into a functional schema and back to an SDL: **Before this change:** ``` (SDL) --parse-> (AST) --coerceInputLiteral--> (defaultValue config) --valueToAST--> (AST) --print --> (SDL) ``` `coerceInputLiteral` performs coercion which is a one-way function, and `valueToAST` is unsafe and set to be deprecated in #3049. **After this change:** ``` (SDL) --parse-> (defaultValue literal config) --print --> (SDL) ``` --- src/execution/getVariableSignature.ts | 13 +++-- src/execution/values.ts | 22 +++++-- src/index.ts | 1 + src/type/__tests__/definition-test.ts | 58 +++++++++++++++++++ src/type/__tests__/predicate-test.ts | 10 +++- src/type/definition.ts | 37 ++++++++++-- src/type/index.ts | 1 + src/type/introspection.ts | 9 ++- src/utilities/TypeInfo.ts | 5 +- .../__tests__/buildClientSchema-test.ts | 23 ++++++++ .../__tests__/coerceInputValue-test.ts | 36 +++++++++++- src/utilities/buildClientSchema.ts | 13 ++--- src/utilities/coerceInputValue.ts | 37 ++++++++++-- src/utilities/extendSchema.ts | 10 +--- src/utilities/findBreakingChanges.ts | 8 ++- src/utilities/printSchema.ts | 9 ++- src/utilities/valueFromAST.ts | 2 +- src/validation/ValidationContext.ts | 3 +- .../rules/VariablesInAllowedPositionRule.ts | 7 ++- 19 files changed, 247 insertions(+), 57 deletions(-) diff --git a/src/execution/getVariableSignature.ts b/src/execution/getVariableSignature.ts index d23c310eb5..8f9fd4a5d2 100644 --- a/src/execution/getVariableSignature.ts +++ b/src/execution/getVariableSignature.ts @@ -4,9 +4,12 @@ import type { VariableDefinitionNode } from '../language/ast.js'; import { print } from '../language/printer.js'; import { isInputType } from '../type/definition.js'; -import type { GraphQLInputType, GraphQLSchema } from '../type/index.js'; +import type { + GraphQLDefaultValueUsage, + GraphQLInputType, + GraphQLSchema, +} from '../type/index.js'; -import { coerceInputLiteral } from '../utilities/coerceInputValue.js'; import { typeFromAST } from '../utilities/typeFromAST.js'; /** @@ -18,7 +21,7 @@ import { typeFromAST } from '../utilities/typeFromAST.js'; export interface GraphQLVariableSignature { name: string; type: GraphQLInputType; - defaultValue: unknown; + defaultValue: GraphQLDefaultValueUsage | undefined; } export function getVariableSignature( @@ -43,8 +46,6 @@ export function getVariableSignature( return { name: varName, type: varType, - defaultValue: defaultValue - ? coerceInputLiteral(varDefNode.defaultValue, varType) - : undefined, + defaultValue: defaultValue ? { literal: defaultValue } : undefined, }; } diff --git a/src/execution/values.ts b/src/execution/values.ts index c153a616a5..5ac0c7656c 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -20,6 +20,7 @@ import type { GraphQLDirective } from '../type/directives.js'; import type { GraphQLSchema } from '../type/schema.js'; import { + coerceDefaultValue, coerceInputLiteral, coerceInputValue, } from '../utilities/coerceInputValue.js'; @@ -90,8 +91,11 @@ function coerceVariableValues( const { name: varName, type: varType } = varSignature; if (!Object.hasOwn(inputs, varName)) { - if (varDefNode.defaultValue) { - coercedValues[varName] = varSignature.defaultValue; + if (varSignature.defaultValue) { + coercedValues[varName] = coerceDefaultValue( + varSignature.defaultValue, + varType, + ); } else if (isNonNullType(varType)) { const varTypeStr = inspect(varType); onError( @@ -173,8 +177,11 @@ export function experimentalGetArgumentValues( const argumentNode = argNodeMap.get(name); if (argumentNode == null) { - 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 "${name}" of required type "${inspect(argType)}" ` + @@ -197,8 +204,11 @@ export function experimentalGetArgumentValues( scopedVariableValues == null || !Object.hasOwn(scopedVariableValues, 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 "${name}" of required type "${inspect(argType)}" ` + diff --git a/src/index.ts b/src/index.ts index cffd892db5..27ec4301d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -199,6 +199,7 @@ export type { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, + GraphQLDefaultValueUsage, } from './type/index.js'; // 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 9a491fc31f..c5e6ebf38a 100644 --- a/src/type/__tests__/definition-test.ts +++ b/src/type/__tests__/definition-test.ts @@ -4,6 +4,7 @@ import { describe, it } from 'mocha'; import { identityFunc } from '../../jsutils/identityFunc.js'; import { inspect } from '../../jsutils/inspect.js'; +import { Kind } from '../../language/kinds.js'; import { parseValue } from '../../language/parser.js'; import type { GraphQLNullableType, GraphQLType } from '../definition.js'; @@ -581,6 +582,63 @@ describe('Type System: Input Objects', () => { 'not used anymore', ); }); + + 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().f).to.deep.include({ + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: { value: 3 }, + deprecationReason: undefined, + extensions: {}, + 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: Kind.INT, value: '3' }, + }, + }, + }); + expect(inputObjType.getFields().f).to.deep.include({ + name: 'f', + description: undefined, + type: ScalarType, + defaultValue: { literal: { kind: 'IntValue', value: '3' } }, + deprecationReason: undefined, + extensions: {}, + 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: Kind.INT, value: '3' }, + }, + }, + }); + expect(() => inputObjType.getFields()).to.throw( + 'Argument "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.ts b/src/type/__tests__/predicate-test.ts index c1e1fb3059..a71ce8c012 100644 --- a/src/type/__tests__/predicate-test.ts +++ b/src/type/__tests__/predicate-test.ts @@ -574,7 +574,10 @@ describe('Type predicates', () => { name: 'someArg', type: config.type, description: undefined, - defaultValue: config.defaultValue, + defaultValue: + config.defaultValue !== undefined + ? { value: config.defaultValue } + : undefined, deprecationReason: null, extensions: Object.create(null), astNode: undefined, @@ -622,7 +625,10 @@ describe('Type predicates', () => { name: 'someInputField', type: config.type, description: undefined, - defaultValue: config.defaultValue, + defaultValue: + config.defaultValue !== undefined + ? { value: config.defaultValue } + : undefined, deprecationReason: null, extensions: Object.create(null), astNode: undefined, diff --git a/src/type/definition.ts b/src/type/definition.ts index b0e783b691..4ebb5ae928 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -16,6 +16,7 @@ import { toObjMap } from '../jsutils/toObjMap.js'; import { GraphQLError } from '../error/GraphQLError.js'; import type { + ConstValueNode, EnumTypeDefinitionNode, EnumTypeExtensionNode, EnumValueDefinitionNode, @@ -799,7 +800,7 @@ export function defineArguments( name: assertName(argName), description: argConfig.description, type: argConfig.type, - defaultValue: argConfig.defaultValue, + defaultValue: defineDefaultValue(argName, argConfig), deprecationReason: argConfig.deprecationReason, extensions: toObjMap(argConfig.extensions), astNode: argConfig.astNode, @@ -833,7 +834,8 @@ export function argsToArgsConfig( (arg) => ({ description: arg.description, type: arg.type, - defaultValue: arg.defaultValue, + defaultValue: arg.defaultValue?.value, + defaultValueLiteral: arg.defaultValue?.literal, deprecationReason: arg.deprecationReason, extensions: arg.extensions, astNode: arg.astNode, @@ -946,6 +948,7 @@ export interface GraphQLArgumentConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: unknown; + defaultValueLiteral?: ConstValueNode | undefined; deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; @@ -971,7 +974,7 @@ export interface GraphQLArgument { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: unknown; + defaultValue: GraphQLDefaultValueUsage | undefined; deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; @@ -985,6 +988,26 @@ export type GraphQLFieldMap = ObjMap< GraphQLField >; +export type GraphQLDefaultValueUsage = + | { value: unknown; literal?: never } + | { literal: ConstValueNode; value?: never }; + +export function defineDefaultValue( + argName: string, + config: GraphQLArgumentConfig | GraphQLInputFieldConfig, +): GraphQLDefaultValueUsage | undefined { + if (config.defaultValue === undefined && !config.defaultValueLiteral) { + return; + } + devAssert( + !(config.defaultValue !== undefined && config.defaultValueLiteral), + `Argument "${argName}" 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 * @@ -1538,7 +1561,8 @@ export class GraphQLInputObjectType { const fields = mapValue(this.getFields(), (field) => ({ description: field.description, type: field.type, - defaultValue: field.defaultValue, + defaultValue: field.defaultValue?.value, + defaultValueLiteral: field.defaultValue?.literal, deprecationReason: field.deprecationReason, extensions: field.extensions, astNode: field.astNode, @@ -1572,7 +1596,7 @@ function defineInputFieldMap( name: assertName(fieldName), description: fieldConfig.description, type: fieldConfig.type, - defaultValue: fieldConfig.defaultValue, + defaultValue: defineDefaultValue(fieldName, fieldConfig), deprecationReason: fieldConfig.deprecationReason, extensions: toObjMap(fieldConfig.extensions), astNode: fieldConfig.astNode, @@ -1613,6 +1637,7 @@ export interface GraphQLInputFieldConfig { description?: Maybe; type: GraphQLInputType; defaultValue?: unknown; + defaultValueLiteral?: ConstValueNode | undefined; deprecationReason?: Maybe; extensions?: Maybe>; astNode?: Maybe; @@ -1624,7 +1649,7 @@ export interface GraphQLInputField { name: string; description: Maybe; type: GraphQLInputType; - defaultValue: unknown; + defaultValue: GraphQLDefaultValueUsage | undefined; deprecationReason: Maybe; extensions: Readonly; astNode: Maybe; diff --git a/src/type/index.ts b/src/type/index.ts index 6617a7bfe1..dfbca75ad0 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -119,6 +119,7 @@ export type { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, + GraphQLDefaultValueUsage, } from './definition.js'; export { diff --git a/src/type/introspection.ts b/src/type/introspection.ts index e6e121e1cc..b72b162cbb 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -407,8 +407,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 != null, 'Invalid default value'); + return print(literal); }, }, isDeprecated: { diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index 75a4eab9ff..e49733f0c2 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -14,6 +14,7 @@ import { getEnterLeaveForKind } from '../language/visitor.js'; import type { GraphQLArgument, GraphQLCompositeType, + GraphQLDefaultValueUsage, GraphQLEnumValue, GraphQLField, GraphQLInputField, @@ -53,7 +54,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; @@ -124,7 +125,7 @@ export class TypeInfo { return this._fieldDefStack.at(-1); } - getDefaultValue(): Maybe { + getDefaultValue(): GraphQLDefaultValueUsage | undefined { return this._defaultValueStack.at(-1); } diff --git a/src/utilities/__tests__/buildClientSchema-test.ts b/src/utilities/__tests__/buildClientSchema-test.ts index b6b433626d..0ad292d03a 100644 --- a/src/utilities/__tests__/buildClientSchema-test.ts +++ b/src/utilities/__tests__/buildClientSchema-test.ts @@ -439,6 +439,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 @@ -609,6 +610,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 15467e8456..78dede7b81 100644 --- a/src/utilities/__tests__/coerceInputValue-test.ts +++ b/src/utilities/__tests__/coerceInputValue-test.ts @@ -5,6 +5,7 @@ import { identityFunc } from '../../jsutils/identityFunc.js'; import { invariant } from '../../jsutils/invariant.js'; import type { ObjMap } from '../../jsutils/ObjMap.js'; +import { Kind } from '../../language/kinds.js'; import { parseValue } from '../../language/parser.js'; import { print } from '../../language/printer.js'; @@ -24,7 +25,11 @@ import { GraphQLString, } from '../../type/scalars.js'; -import { coerceInputLiteral, coerceInputValue } from '../coerceInputValue.js'; +import { + coerceDefaultValue, + coerceInputLiteral, + coerceInputValue, +} from '../coerceInputValue.js'; interface CoerceResult { value: unknown; @@ -716,10 +721,14 @@ describe('coerceInputLiteral', () => { name: 'TestInput', fields: { int: { type: GraphQLInt, defaultValue: 42 }, + float: { + type: GraphQLFloat, + defaultValueLiteral: { kind: Kind.FLOAT, value: '3.14' }, + }, }, }); - test('{}', type, { int: 42 }); + test('{}', type, { int: 42, float: 3.14 }); }); const testInputObj = new GraphQLInputObjectType({ @@ -807,3 +816,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: Kind.STRING, 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/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index c109b18fa7..f1f52e8a52 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -32,7 +32,6 @@ import { specifiedScalarTypes } from '../type/scalars.js'; import type { GraphQLSchemaValidationOptions } from '../type/schema.js'; import { GraphQLSchema } from '../type/schema.js'; -import { coerceInputLiteral } from './coerceInputValue.js'; import type { IntrospectionDirective, IntrospectionEnumType, @@ -374,17 +373,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 88c97c8405..f912fea4e4 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -15,7 +15,10 @@ import { GraphQLError } from '../error/GraphQLError.js'; import type { ValueNode, VariableNode } from '../language/ast.js'; import { Kind } from '../language/kinds.js'; -import type { GraphQLInputType } from '../type/definition.js'; +import type { + GraphQLDefaultValueUsage, + GraphQLInputType, +} from '../type/definition.js'; import { assertLeafType, isInputObjectType, @@ -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( @@ -326,8 +332,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( @@ -379,3 +388,21 @@ function getVariableValue( return variableValues?.[varName]; } + +/** + * @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; + if (coercedValue === undefined) { + coercedValue = defaultValue.literal + ? coerceInputLiteral(defaultValue.literal, type) + : defaultValue.value; + (defaultValue as any)._memoizedCoercedValue = coercedValue; + } + return coercedValue; +} diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index d18b53d028..f43c15330d 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -83,8 +83,6 @@ import { assertValidSDLExtension } from '../validation/validate.js'; import { getDirectiveValues } from '../execution/values.js'; -import { coerceInputLiteral } from './coerceInputValue.js'; - interface Options extends GraphQLSchemaValidationOptions { /** * Set to true to assume the SDL is valid. @@ -535,9 +533,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, }; @@ -564,9 +560,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 2921074625..e6f4f79055 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -5,6 +5,7 @@ import { keyMap } from '../jsutils/keyMap.js'; import { print } from '../language/printer.js'; import type { + GraphQLDefaultValueUsage, GraphQLEnumType, GraphQLField, GraphQLInputObjectType, @@ -534,8 +535,11 @@ function typeKindName(type: GraphQLNamedType): string { invariant(false, 'Unexpected type: ' + inspect(type)); } -function stringifyValue(value: unknown, type: GraphQLInputType): string { - const ast = astFromValue(value, type); +function stringifyValue( + defaultValue: GraphQLDefaultValueUsage, + type: GraphQLInputType, +): string { + const ast = defaultValue.literal ?? astFromValue(defaultValue.value, type); invariant(ast != null); return print(sortValueNode(ast)); } diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 7d4a5e4f5a..b02580c6fa 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 != null, 'Invalid default value'); + argDecl += ` = ${print(literal)}`; } return argDecl + printDeprecated(arg.deprecationReason); } diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index add9153680..e0ff5d8bf2 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -115,7 +115,7 @@ export function valueFromAST( const fieldNode = fieldNodes.get(field.name); if (fieldNode == null || isMissingVariable(fieldNode.value, variables)) { if (field.defaultValue !== undefined) { - coercedObj[field.name] = field.defaultValue; + coercedObj[field.name] = field.defaultValue.value; } else if (isNonNullType(field.type)) { return; // Invalid: intentionally return no value. } diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index 7f7114c4ef..d45e7a46a4 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -19,6 +19,7 @@ import { visit } from '../language/visitor.js'; import type { GraphQLArgument, GraphQLCompositeType, + GraphQLDefaultValueUsage, GraphQLEnumValue, GraphQLField, GraphQLInputType, @@ -34,7 +35,7 @@ type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode; interface VariableUsage { readonly node: VariableNode; readonly type: Maybe; - readonly defaultValue: Maybe; + readonly defaultValue: GraphQLDefaultValueUsage | undefined; readonly fragmentVariableDefinition: Maybe; } diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts index e68a595c4f..888e49d3dd 100644 --- a/src/validation/rules/VariablesInAllowedPositionRule.ts +++ b/src/validation/rules/VariablesInAllowedPositionRule.ts @@ -7,7 +7,10 @@ import type { ValueNode, VariableDefinitionNode } from '../../language/ast.js'; import { Kind } from '../../language/kinds.js'; import type { ASTVisitor } from '../../language/visitor.js'; -import type { GraphQLType } from '../../type/definition.js'; +import type { + GraphQLDefaultValueUsage, + GraphQLType, +} from '../../type/definition.js'; import { isNonNullType } from '../../type/definition.js'; import type { GraphQLSchema } from '../../type/schema.js'; @@ -95,7 +98,7 @@ function allowedVariableUsage( varType: GraphQLType, varDefaultValue: Maybe, locationType: GraphQLType, - locationDefaultValue: Maybe, + locationDefaultValue: GraphQLDefaultValueUsage | undefined, ): boolean { if (isNonNullType(locationType) && !isNonNullType(varType)) { const hasNonNullVariableDefaultValue =