diff --git a/src/execution/__tests__/variables-test.ts b/src/execution/__tests__/variables-test.ts index eec35c99bc..133abfaacc 100644 --- a/src/execution/__tests__/variables-test.ts +++ b/src/execution/__tests__/variables-test.ts @@ -30,6 +30,8 @@ import { import { GraphQLBoolean, GraphQLString } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; +import { valueFromASTUntyped } from '../../utilities/valueFromASTUntyped.js'; + import { executeSync, experimentalExecuteIncrementally } from '../execute.js'; import { getVariableValues } from '../values.js'; @@ -64,6 +66,16 @@ const TestComplexScalar = new GraphQLScalarType({ }, }); +const TestJSONScalar = new GraphQLScalarType({ + name: 'JSONScalar', + coerceInputValue(value) { + return value; + }, + coerceInputLiteral(value) { + return valueFromASTUntyped(value); + }, +}); + const NestedType: GraphQLObjectType = new GraphQLObjectType({ name: 'NestedType', fields: { @@ -151,6 +163,7 @@ const TestType = new GraphQLObjectType({ fieldWithNestedInputObject: fieldWithInputArg({ type: TestNestedInputObject, }), + fieldWithJSONScalarInput: fieldWithInputArg({ type: TestJSONScalar }), list: fieldWithInputArg({ type: new GraphQLList(GraphQLString) }), nested: { type: NestedType, @@ -859,6 +872,94 @@ describe('Execute: Handles inputs', () => { }); }); + // Note: the below is non-specified custom graphql-js behavior. + describe('Handles custom scalars with embedded variables', () => { + it('allows custom scalars', () => { + const result = executeQuery(` + { + fieldWithJSONScalarInput(input: { a: "foo", b: ["bar"], c: "baz" }) + } + `); + + expectJSON(result).toDeepEqual({ + data: { + fieldWithJSONScalarInput: '{ a: "foo", b: ["bar"], c: "baz" }', + }, + }); + }); + + it('allows custom scalars with non-embedded variables', () => { + const result = executeQuery( + ` + query ($input: JSONScalar) { + fieldWithJSONScalarInput(input: $input) + } + `, + { input: { a: 'foo', b: ['bar'], c: 'baz' } }, + ); + + expectJSON(result).toDeepEqual({ + data: { + fieldWithJSONScalarInput: '{ a: "foo", b: ["bar"], c: "baz" }', + }, + }); + }); + + it('allows custom scalars with embedded operation variables', () => { + const result = executeQuery( + ` + query ($input: String) { + fieldWithJSONScalarInput(input: { a: $input, b: ["bar"], c: "baz" }) + } + `, + { input: 'foo' }, + ); + + expectJSON(result).toDeepEqual({ + data: { + fieldWithJSONScalarInput: '{ a: "foo", b: ["bar"], c: "baz" }', + }, + }); + }); + + it('allows custom scalars with embedded fragment variables', () => { + const result = executeQueryWithFragmentArguments(` + { + ...JSONFragment(input: "foo") + } + fragment JSONFragment($input: String) on TestType { + fieldWithJSONScalarInput(input: { a: $input, b: ["bar"], c: "baz" }) + } + `); + + expectJSON(result).toDeepEqual({ + data: { + fieldWithJSONScalarInput: '{ a: "foo", b: ["bar"], c: "baz" }', + }, + }); + }); + + it('allows custom scalars with embedded nested fragment variables', () => { + const result = executeQueryWithFragmentArguments(` + { + ...JSONFragment(input1: "foo") + } + fragment JSONFragment($input1: String) on TestType { + ...JSONNestedFragment(input2: $input1) + } + fragment JSONNestedFragment($input2: String) on TestType { + fieldWithJSONScalarInput(input: { a: $input2, b: ["bar"], c: "baz" }) + } + `); + + expectJSON(result).toDeepEqual({ + data: { + fieldWithJSONScalarInput: '{ a: "foo", b: ["bar"], c: "baz" }', + }, + }); + }); + }); + describe('Handles lists and nullability', () => { it('allows lists to be null', () => { const doc = ` diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index bc00b413d8..80ff40f871 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -1,7 +1,8 @@ import { AccumulatorMap } from '../jsutils/AccumulatorMap.js'; -import type { ObjMap } from '../jsutils/ObjMap.js'; +import type { ObjMap, ReadOnlyObjMap } from '../jsutils/ObjMap.js'; import type { + ConstValueNode, DirectiveNode, FieldNode, FragmentDefinitionNode, @@ -25,7 +26,7 @@ import { typeFromAST } from '../utilities/typeFromAST.js'; import type { GraphQLVariableSignature } from './getVariableSignature.js'; import type { VariableValues } from './values.js'; import { - experimentalGetArgumentValues, + getArgumentValues, getDirectiveValues, getFragmentVariableValues, } from './values.js'; @@ -35,10 +36,21 @@ export interface DeferUsage { parentDeferUsage: DeferUsage | undefined; } +export interface FragmentVariableValues { + readonly sources: ReadOnlyObjMap; + readonly coerced: ReadOnlyObjMap; +} + +interface FragmentVariableValueSource { + readonly signature: GraphQLVariableSignature; + readonly value?: ConstValueNode; + readonly fragmentVariableValues?: FragmentVariableValues; +} + export interface FieldDetails { node: FieldNode; deferUsage?: DeferUsage | undefined; - fragmentVariableValues?: VariableValues | undefined; + fragmentVariableValues?: FragmentVariableValues | undefined; } export type FieldDetailsList = ReadonlyArray; @@ -168,7 +180,7 @@ function collectFieldsImpl( groupedFieldSet: AccumulatorMap, newDeferUsages: Array, deferUsage?: DeferUsage, - fragmentVariableValues?: VariableValues, + fragmentVariableValues?: FragmentVariableValues, ): void { const { schema, @@ -273,7 +285,7 @@ function collectFieldsImpl( ); const fragmentVariableSignatures = fragment.variableSignatures; - let newFragmentVariableValues: VariableValues | undefined; + let newFragmentVariableValues: FragmentVariableValues | undefined; if (fragmentVariableSignatures) { newFragmentVariableValues = getFragmentVariableValues( selection, @@ -318,7 +330,7 @@ function collectFieldsImpl( */ function getDeferUsage( variableValues: VariableValues, - fragmentVariableValues: VariableValues | undefined, + fragmentVariableValues: FragmentVariableValues | undefined, node: FragmentSpreadNode | InlineFragmentNode, parentDeferUsage: DeferUsage | undefined, ): DeferUsage | undefined { @@ -351,7 +363,7 @@ function shouldIncludeNode( context: CollectFieldsContext, node: FragmentSpreadNode | FieldNode | InlineFragmentNode, variableValues: VariableValues, - fragmentVariableValues: VariableValues | undefined, + fragmentVariableValues: FragmentVariableValues | undefined, ): boolean { const skipDirectiveNode = node.directives?.find( (directive) => directive.name.value === GraphQLSkipDirective.name, @@ -361,9 +373,9 @@ function shouldIncludeNode( return false; } const skip = skipDirectiveNode - ? experimentalGetArgumentValues( + ? getArgumentValues( + GraphQLSkipDirective, skipDirectiveNode, - GraphQLSkipDirective.args, variableValues, fragmentVariableValues, context.hideSuggestions, @@ -381,9 +393,9 @@ function shouldIncludeNode( return false; } const include = includeDirectiveNode - ? experimentalGetArgumentValues( + ? getArgumentValues( + GraphQLIncludeDirective, includeDirectiveNode, - GraphQLIncludeDirective.args, variableValues, fragmentVariableValues, context.hideSuggestions, diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 71cbdb6b42..f5215585cf 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -85,7 +85,6 @@ import type { import { DeferredFragmentRecord } from './types.js'; import type { VariableValues } from './values.js'; import { - experimentalGetArgumentValues, getArgumentValues, getDirectiveValues, getVariableValues, @@ -877,9 +876,9 @@ function executeField( // Build a JS object of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. // TODO: find a way to memoize, in case this field is within a List type. - const args = experimentalGetArgumentValues( + const args = getArgumentValues( + fieldDef, fieldDetailsList[0].node, - fieldDef.args, variableValues, fieldDetailsList[0].fragmentVariableValues, hideSuggestions, @@ -2298,6 +2297,7 @@ function executeSubscription( fieldDef, fieldNodes[0], variableValues, + fieldDetailsList[0].fragmentVariableValues, hideSuggestions, ); diff --git a/src/execution/values.ts b/src/execution/values.ts index 8e0485ae18..ac67ac3b02 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -6,8 +6,10 @@ import { printPathArray } from '../jsutils/printPathArray.js'; import { GraphQLError } from '../error/GraphQLError.js'; import type { + ArgumentNode, DirectiveNode, FieldNode, + FragmentArgumentNode, FragmentSpreadNode, VariableDefinitionNode, } from '../language/ast.js'; @@ -32,6 +34,7 @@ import { validateInputValue, } from '../utilities/validateInputValue.js'; +import type { FragmentVariableValues } from './collectFields.js'; import type { GraphQLVariableSignature } from './getVariableSignature.js'; import { getVariableSignature } from './getVariableSignature.js'; @@ -154,28 +157,35 @@ export function getFragmentVariableValues( fragmentSpreadNode: FragmentSpreadNode, fragmentSignatures: ReadOnlyObjMap, variableValues: VariableValues, - fragmentVariableValues?: Maybe, + fragmentVariableValues?: Maybe, hideSuggestions?: Maybe, -): VariableValues { - const varSignatures: Array = []; +): FragmentVariableValues { + const argumentNodes = fragmentSpreadNode.arguments ?? []; + const argNodeMap = new Map(argumentNodes.map((arg) => [arg.name.value, arg])); const sources = Object.create(null); + const coerced = Object.create(null); for (const [varName, varSignature] of Object.entries(fragmentSignatures)) { - varSignatures.push(varSignature); sources[varName] = { signature: varSignature, - value: - fragmentVariableValues?.sources[varName]?.value ?? - variableValues.sources[varName]?.value, }; - } + const argumentNode = argNodeMap.get(varName); + if (argumentNode !== undefined) { + const source = sources[varName]; + source.value = argumentNode.value; + source.fragmentVariableValues = fragmentVariableValues; + } - const coerced = experimentalGetArgumentValues( - fragmentSpreadNode, - varSignatures, - variableValues, - fragmentVariableValues, - hideSuggestions, - ); + coerceArgument( + coerced, + fragmentSpreadNode, + varName, + varSignature, + argumentNode, + variableValues, + fragmentVariableValues, + hideSuggestions, + ); + } return { sources, coerced }; } @@ -192,22 +202,7 @@ export function getArgumentValues( def: GraphQLField | GraphQLDirective, node: FieldNode | DirectiveNode, variableValues?: Maybe, - hideSuggestions?: Maybe, -): { [argument: string]: unknown } { - return experimentalGetArgumentValues( - node, - def.args, - variableValues, - undefined, - hideSuggestions, - ); -} - -export function experimentalGetArgumentValues( - node: FieldNode | DirectiveNode | FragmentSpreadNode, - argDefs: ReadonlyArray, - variableValues: Maybe, - fragmentVariableValues?: Maybe, + fragmentVariableValues?: Maybe, hideSuggestions?: Maybe, ): { [argument: string]: unknown } { const coercedValues: { [argument: string]: unknown } = {}; @@ -215,80 +210,102 @@ export function experimentalGetArgumentValues( const argumentNodes = node.arguments ?? []; const argNodeMap = new Map(argumentNodes.map((arg) => [arg.name.value, arg])); - for (const argDef of argDefs) { + for (const argDef of def.args) { const name = argDef.name; - const argType = argDef.type; - const argumentNode = argNodeMap.get(name); + coerceArgument( + coercedValues, + node, + name, + argDef, + argNodeMap.get(argDef.name), + variableValues, + fragmentVariableValues, + hideSuggestions, + ); + } + return coercedValues; +} - if (!argumentNode) { - if (isRequiredArgument(argDef)) { - // Note: ProvidedRequiredArgumentsRule validation should catch this before - // execution. This is a runtime check to ensure execution does not - // continue with an invalid argument value. - throw new GraphQLError( - // TODO: clean up the naming of isRequiredArgument(), isArgument(), and argDef if/when experimental fragment variables are merged - `Argument "${isArgument(argDef) ? argDef : argDef.name}" of required type "${argType}" was not provided.`, - { nodes: node }, - ); - } - const coercedDefaultValue = coerceDefaultValue(argDef); - if (coercedDefaultValue !== undefined) { - coercedValues[name] = coercedDefaultValue; - } - continue; +// eslint-disable-next-line @typescript-eslint/max-params +function coerceArgument( + coercedValues: ObjMap, + node: FieldNode | DirectiveNode | FragmentSpreadNode, + argName: string, + argDef: GraphQLArgument | GraphQLVariableSignature, + argumentNode: ArgumentNode | FragmentArgumentNode | undefined, + variableValues: Maybe, + fragmentVariableValues: Maybe, + hideSuggestions?: Maybe, +): void { + const argType = argDef.type; + + if (!argumentNode) { + if (isRequiredArgument(argDef)) { + // Note: ProvidedRequiredArgumentsRule validation should catch this before + // execution. This is a runtime check to ensure execution does not + // continue with an invalid argument value. + throw new GraphQLError( + // TODO: clean up the naming of isRequiredArgument(), isArgument(), and argDef if/when experimental fragment variables are merged + `Argument "${isArgument(argDef) ? argDef : argName}" of required type "${argType}" was not provided.`, + { nodes: node }, + ); + } + const coercedDefaultValue = coerceDefaultValue(argDef); + if (coercedDefaultValue !== undefined) { + coercedValues[argName] = coercedDefaultValue; } + return; + } - const valueNode = argumentNode.value; + const valueNode = argumentNode.value; - // Variables without a value are treated as if no argument was provided if - // the argument is not required. - if (valueNode.kind === Kind.VARIABLE) { - const variableName = valueNode.name.value; - const scopedVariableValues = fragmentVariableValues?.sources[variableName] - ? fragmentVariableValues - : variableValues; - if ( - (scopedVariableValues == null || - !Object.hasOwn(scopedVariableValues.coerced, variableName)) && - !isRequiredArgument(argDef) - ) { - const coercedDefaultValue = coerceDefaultValue(argDef); - if (coercedDefaultValue !== undefined) { - coercedValues[name] = coercedDefaultValue; - } - continue; + // Variables without a value are treated as if no argument was provided if + // the argument is not required. + if (valueNode.kind === Kind.VARIABLE) { + const variableName = valueNode.name.value; + const scopedVariableValues = fragmentVariableValues?.sources[variableName] + ? fragmentVariableValues + : variableValues; + if ( + (scopedVariableValues == null || + !Object.hasOwn(scopedVariableValues.coerced, variableName)) && + !isRequiredArgument(argDef) + ) { + const coercedDefaultValue = coerceDefaultValue(argDef); + if (coercedDefaultValue !== undefined) { + coercedValues[argName] = coercedDefaultValue; } + return; } - const coercedValue = coerceInputLiteral( + } + const coercedValue = coerceInputLiteral( + valueNode, + argType, + variableValues, + fragmentVariableValues, + ); + if (coercedValue === undefined) { + // Note: ValuesOfCorrectTypeRule validation should catch this before + // execution. This is a runtime check to ensure execution does not + // continue with an invalid argument value. + validateInputLiteral( valueNode, argType, + (error, path) => { + // TODO: clean up the naming of isRequiredArgument(), isArgument(), and argDef if/when experimental fragment variables are merged + error.message = `Argument "${isArgument(argDef) ? argDef : argDef.name}" has invalid value${printPathArray( + path, + )}: ${error.message}`; + throw error; + }, variableValues, fragmentVariableValues, + hideSuggestions, ); - if (coercedValue === undefined) { - // Note: ValuesOfCorrectTypeRule validation should catch this before - // execution. This is a runtime check to ensure execution does not - // continue with an invalid argument value. - validateInputLiteral( - valueNode, - argType, - (error, path) => { - // TODO: clean up the naming of isRequiredArgument(), isArgument(), and argDef if/when experimental fragment variables are merged - error.message = `Argument "${isArgument(argDef) ? argDef : argDef.name}" has invalid value${printPathArray( - path, - )}: ${error.message}`; - throw error; - }, - variableValues, - fragmentVariableValues, - hideSuggestions, - ); - /* c8 ignore next */ - invariant(false, 'Invalid argument'); - } - coercedValues[name] = coercedValue; + /* c8 ignore next */ + invariant(false, 'Invalid argument'); } - return coercedValues; + coercedValues[argName] = coercedValue; } /** @@ -306,7 +323,7 @@ export function getDirectiveValues( directiveDef: GraphQLDirective, node: { readonly directives?: ReadonlyArray | undefined }, variableValues?: Maybe, - fragmentVariableValues?: Maybe, + fragmentVariableValues?: Maybe, hideSuggestions?: Maybe, ): undefined | { [argument: string]: unknown } { const directiveNode = node.directives?.find( @@ -314,9 +331,9 @@ export function getDirectiveValues( ); if (directiveNode) { - return experimentalGetArgumentValues( + return getArgumentValues( + directiveDef, directiveNode, - directiveDef.args, variableValues, fragmentVariableValues, hideSuggestions, diff --git a/src/utilities/__tests__/replaceVariables-test.ts b/src/utilities/__tests__/replaceVariables-test.ts index 61b0780d6a..e33328f641 100644 --- a/src/utilities/__tests__/replaceVariables-test.ts +++ b/src/utilities/__tests__/replaceVariables-test.ts @@ -4,14 +4,24 @@ import { describe, it } from 'mocha'; import { invariant } from '../../jsutils/invariant.js'; import type { ReadOnlyObjMap } from '../../jsutils/ObjMap.js'; -import type { ValueNode } from '../../language/ast.js'; +import type { + FragmentArgumentNode, + FragmentSpreadNode, + ValueNode, + VariableDefinitionNode, +} from '../../language/ast.js'; +import { Kind } from '../../language/kinds.js'; import { Parser, parseValue as _parseValue } from '../../language/parser.js'; import { TokenKind } from '../../language/tokenKind.js'; import { GraphQLInt } from '../../type/scalars.js'; import { GraphQLSchema } from '../../type/schema.js'; -import { getVariableValues } from '../../execution/values.js'; +import { getVariableSignature } from '../../execution/getVariableSignature.js'; +import { + getFragmentVariableValues, + getVariableValues, +} from '../../execution/values.js'; import { replaceVariables } from '../replaceVariables.js'; @@ -20,17 +30,51 @@ function parseValue(ast: string): ValueNode { } function testVariables(variableDefs: string, inputs: ReadOnlyObjMap) { - const parser = new Parser(variableDefs, { noLocation: true }); - parser.expectToken(TokenKind.SOF); const variableValuesOrErrors = getVariableValues( new GraphQLSchema({ types: [GraphQLInt] }), - parser.parseVariableDefinitions() ?? [], + parseVariableDefinitions(variableDefs), inputs, ); invariant(variableValuesOrErrors.variableValues !== undefined); return variableValuesOrErrors.variableValues; } +function parseVariableDefinitions( + variableDefs: string, +): ReadonlyArray { + const parser = new Parser(variableDefs, { noLocation: true }); + parser.expectToken(TokenKind.SOF); + return parser.parseVariableDefinitions() ?? []; +} + +function testFragmentVariables(variableDefs: string, fragmentArgs: string) { + const schema = new GraphQLSchema({ types: [GraphQLInt] }); + const fragmentSignatures = Object.create(null); + for (const varDef of parseVariableDefinitions(variableDefs)) { + const signature = getVariableSignature(schema, varDef); + fragmentSignatures[signature.name] = signature; + } + const spread: FragmentSpreadNode = { + kind: Kind.FRAGMENT_SPREAD, + name: { kind: Kind.NAME, value: 'TestFragment' }, + arguments: parseFragmentArguments(fragmentArgs), + }; + return getFragmentVariableValues( + spread, + fragmentSignatures, + Object.create(null), + undefined, + ); +} + +function parseFragmentArguments( + fragmentArguments: string, +): ReadonlyArray { + const parser = new Parser(fragmentArguments, { noLocation: true }); + parser.expectToken(TokenKind.SOF); + return parser.parseFragmentArguments() ?? []; +} + describe('replaceVariables', () => { describe('Operation Variables', () => { it('does not change simple AST', () => { @@ -96,7 +140,7 @@ describe('replaceVariables', () => { describe('Fragment Variables', () => { it('replaces simple Fragment Variables', () => { const ast = parseValue('$var'); - const fragmentVars = testVariables('($var: Int)', { var: 123 }); + const fragmentVars = testFragmentVariables('($var: Int)', `(var: 123)`); expect(replaceVariables(ast, undefined, fragmentVars)).to.deep.equal( parseValue('123'), ); @@ -105,7 +149,7 @@ describe('replaceVariables', () => { it('replaces simple Fragment Variables even when overlapping with Operation Variables', () => { const ast = parseValue('$var'); const operationVars = testVariables('($var: Int)', { var: 123 }); - const fragmentVars = testVariables('($var: Int)', { var: 456 }); + const fragmentVars = testFragmentVariables('($var: Int)', '(var: 456)'); expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal( parseValue('456'), ); @@ -113,7 +157,7 @@ describe('replaceVariables', () => { it('replaces Fragment Variables with default values', () => { const ast = parseValue('$var'); - const fragmentVars = testVariables('($var: Int = 123)', {}); + const fragmentVars = testFragmentVariables('($var: Int = 123)', ''); expect(replaceVariables(ast, undefined, fragmentVars)).to.deep.equal( parseValue('123'), ); @@ -122,7 +166,7 @@ describe('replaceVariables', () => { it('replaces Fragment Variables with default values even when overlapping with Operation Variables', () => { const ast = parseValue('$var'); const operationVars = testVariables('($var: Int = 123)', {}); - const fragmentVars = testVariables('($var: Int = 456)', {}); + const fragmentVars = testFragmentVariables('($var: Int = 456)', ''); expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal( parseValue('456'), ); @@ -130,7 +174,7 @@ describe('replaceVariables', () => { it('replaces nested Fragment Variables', () => { const ast = parseValue('{ foo: [ $var ], bar: $var }'); - const fragmentVars = testVariables('($var: Int)', { var: 123 }); + const fragmentVars = testFragmentVariables('($var: Int)', '(var: 123)'); expect(replaceVariables(ast, undefined, fragmentVars)).to.deep.equal( parseValue('{ foo: [ 123 ], bar: 123 }'), ); @@ -139,7 +183,7 @@ describe('replaceVariables', () => { it('replaces nested Fragment Variables even when overlapping with Operation Variables', () => { const ast = parseValue('{ foo: [ $var ], bar: $var }'); const operationVars = testVariables('($var: Int)', { var: 123 }); - const fragmentVars = testVariables('($var: Int)', { var: 456 }); + const fragmentVars = testFragmentVariables('($var: Int)', '(var: 456)'); expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal( parseValue('{ foo: [ 456 ], bar: 456 }'), ); @@ -155,7 +199,7 @@ describe('replaceVariables', () => { it('replaces missing Fragment Variables with null even when overlapping with Operation Variables', () => { const ast = parseValue('$var'); const operationVars = testVariables('($var: Int)', { var: 123 }); - const fragmentVars = testVariables('($var: Int)', {}); + const fragmentVars = testFragmentVariables('($var: Int)', ''); expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal( parseValue('null'), ); @@ -171,7 +215,7 @@ describe('replaceVariables', () => { it('replaces missing Fragment Variables in lists with null even when overlapping with Operation Variables', () => { const ast = parseValue('[1, $var]'); const operationVars = testVariables('($var: Int)', { var: 123 }); - const fragmentVars = testVariables('($var: Int)', {}); + const fragmentVars = testFragmentVariables('($var: Int)', ''); expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal( parseValue('[1, null]'), ); @@ -187,7 +231,7 @@ describe('replaceVariables', () => { it('omits missing Fragment Variables from objects even when overlapping with Operation Variables', () => { const ast = parseValue('{ foo: 1, bar: $var }'); const operationVars = testVariables('($var: Int)', { var: 123 }); - const fragmentVars = testVariables('($var: Int)', {}); + const fragmentVars = testFragmentVariables('($var: Int)', ''); expect(replaceVariables(ast, operationVars, fragmentVars)).to.deep.equal( parseValue('{ foo: 1 }'), ); diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index d33b4273c6..585acdaa29 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -18,6 +18,7 @@ import { isRequiredInputField, } from '../type/definition.js'; +import type { FragmentVariableValues } from '../execution/collectFields.js'; import type { VariableValues } from '../execution/values.js'; import { replaceVariables } from './replaceVariables.js'; @@ -130,7 +131,7 @@ export function coerceInputLiteral( valueNode: ValueNode, type: GraphQLInputType, variableValues?: Maybe, - fragmentVariableValues?: Maybe, + fragmentVariableValues?: Maybe, ): unknown { if (valueNode.kind === Kind.VARIABLE) { const coercedVariableValue = getCoercedVariableValue( @@ -283,7 +284,7 @@ export function coerceInputLiteral( function getCoercedVariableValue( variableNode: VariableNode, variableValues: Maybe, - fragmentVariableValues: Maybe, + fragmentVariableValues: Maybe, ): unknown { const varName = variableNode.name.value; if (fragmentVariableValues?.sources[varName] !== undefined) { diff --git a/src/utilities/replaceVariables.ts b/src/utilities/replaceVariables.ts index fc09a290e3..d9d745fe95 100644 --- a/src/utilities/replaceVariables.ts +++ b/src/utilities/replaceVariables.ts @@ -7,6 +7,7 @@ import type { } from '../language/ast.js'; import { Kind } from '../language/kinds.js'; +import type { FragmentVariableValues } from '../execution/collectFields.js'; import type { VariableValues } from '../execution/values.js'; import { valueToLiteral } from './valueToLiteral.js'; @@ -22,30 +23,45 @@ import { valueToLiteral } from './valueToLiteral.js'; export function replaceVariables( valueNode: ValueNode, variableValues?: Maybe, - fragmentVariableValues?: Maybe, + fragmentVariableValues?: Maybe, ): ConstValueNode { switch (valueNode.kind) { case Kind.VARIABLE: { const varName = valueNode.name.value; - const scopedVariableValues = fragmentVariableValues?.sources[varName] - ? fragmentVariableValues - : variableValues; + const fragmentVariableValueSource = + fragmentVariableValues?.sources[varName]; - const scopedVariableSource = scopedVariableValues?.sources[varName]; - if (scopedVariableSource == null) { + if (fragmentVariableValueSource) { + const value = fragmentVariableValueSource.value; + if (value === undefined) { + const defaultValue = fragmentVariableValueSource.signature.default; + if (defaultValue !== undefined) { + return defaultValue.literal; + } + return { kind: Kind.NULL }; + } + return replaceVariables( + value, + variableValues, + fragmentVariableValueSource.fragmentVariableValues, + ); + } + + const variableValueSource = variableValues?.sources[varName]; + if (variableValueSource == null) { return { kind: Kind.NULL }; } - if (scopedVariableSource.value === undefined) { - const defaultValue = scopedVariableSource.signature.default; + if (variableValueSource.value === undefined) { + const defaultValue = variableValueSource.signature.default; if (defaultValue !== undefined) { return defaultValue.literal; } } return valueToLiteral( - scopedVariableSource.value, - scopedVariableSource.signature.type, + variableValueSource.value, + variableValueSource.signature.type, ) as ConstValueNode; } case Kind.OBJECT: { diff --git a/src/utilities/validateInputValue.ts b/src/utilities/validateInputValue.ts index 4b6295320a..a8d9b5c257 100644 --- a/src/utilities/validateInputValue.ts +++ b/src/utilities/validateInputValue.ts @@ -23,6 +23,7 @@ import { isRequiredInputField, } from '../type/definition.js'; +import type { FragmentVariableValues } from '../execution/collectFields.js'; import type { VariableValues } from '../execution/values.js'; import { replaceVariables } from './replaceVariables.js'; @@ -231,7 +232,7 @@ export function validateInputLiteral( type: GraphQLInputType, onError: (error: GraphQLError, path: ReadonlyArray) => void, variables?: Maybe, - fragmentVariableValues?: Maybe, + fragmentVariableValues?: Maybe, hideSuggestions?: Maybe, ): void { const context: ValidationContext = { @@ -253,7 +254,7 @@ interface ValidationContext { static: boolean; onError: (error: GraphQLError, path: ReadonlyArray) => void; variables?: Maybe; - fragmentVariableValues?: Maybe; + fragmentVariableValues?: Maybe; } function validateInputLiteralImpl( @@ -456,7 +457,14 @@ function validateInputLiteralImpl( let caughtError; try { result = type.coerceInputLiteral - ? type.coerceInputLiteral(replaceVariables(valueNode), hideSuggestions) + ? type.coerceInputLiteral( + replaceVariables( + valueNode, + context.variables, + context.fragmentVariableValues, + ), + hideSuggestions, + ) : type.parseLiteral(valueNode, undefined, hideSuggestions); } catch (error) { if (error instanceof GraphQLError) {