From 0f7e88adb326a64fe0a7cbf956eb1d2c92d94d9b Mon Sep 17 00:00:00 2001 From: Dmitrii Samsonov Date: Thu, 9 Jul 2026 13:20:57 +0200 Subject: [PATCH] add fragment loader support to variables/args processing --- .../src/__tests__/execute.test.ts | 122 ++++++ .../supermassive/src/executeWithoutSchema.ts | 101 +++-- packages/supermassive/src/types.ts | 7 +- .../src/utilities/requestSchemaFragment.ts | 38 ++ packages/supermassive/src/values.ts | 412 +++++++++++++----- 5 files changed, 527 insertions(+), 153 deletions(-) create mode 100644 packages/supermassive/src/utilities/requestSchemaFragment.ts diff --git a/packages/supermassive/src/__tests__/execute.test.ts b/packages/supermassive/src/__tests__/execute.test.ts index d1edaa8d0..91dd5b700 100644 --- a/packages/supermassive/src/__tests__/execute.test.ts +++ b/packages/supermassive/src/__tests__/execute.test.ts @@ -677,4 +677,126 @@ describe("executeWithoutSchema - regression tests", () => { const events = await drainExecution(result); expect(events).toMatchSnapshot(); }); + + interface MissingInputTypeTestCase { + name: string; + document: string; + vars?: Record; + partialSchema: string; + fullSchema: string; + } + + const partialDownloadFileSchema = ` + type Mutation { + downloadFile(input: FileCoordinates!): DownloadFileResult + } + + type DownloadFileResult { + downloadUrl: String! + }`; + + const fullDownloadFileSchema = ` + type Mutation { + downloadFile(input: FileCoordinates!): DownloadFileResult + } + + input FileCoordinates { + name: String! + options: FileDownloadOptions + } + + input FileDownloadOptions { + compression: CompressionMode + } + + enum CompressionMode { + NONE + RAR + ZIP + } + + type DownloadFileResult { + downloadUrl: String! + }`; + + const missingInputTypeCases: MissingInputTypeTestCase[] = [ + { + name: "missing input variable type", + document: `mutation DownloadFile($input: FileCoordinates!) { + downloadFile(input: $input) { downloadUrl } + }`, + vars: { input: { name: "test-file" } }, + partialSchema: partialDownloadFileSchema, + fullSchema: fullDownloadFileSchema, + }, + { + name: "missing nested input variable type", + document: `mutation DownloadFile($input: FileCoordinates!) { + downloadFile(input: $input) { downloadUrl } + }`, + vars: { input: { name: "test-file", options: { compression: "NONE" } } }, + partialSchema: ` + type Mutation { + downloadFile(input: FileCoordinates!): DownloadFileResult + } + + input FileCoordinates { + name: String! + options: FileDownloadOptions + } + + type DownloadFileResult { + downloadUrl: String! + }`, + fullSchema: fullDownloadFileSchema, + }, + { + name: "missing inline argument type", + document: `mutation DownloadFile { + downloadFile(input: {name: "bar-file", options:{ compression: "RAR" }}) { downloadUrl } + }`, + partialSchema: partialDownloadFileSchema, + fullSchema: fullDownloadFileSchema, + }, + ]; + + test.each(missingInputTypeCases)( + "$name", + async ({ + document, + vars, + fullSchema, + partialSchema, + }: MissingInputTypeTestCase) => { + expect.assertions(1); + + const fullDefinitions = encodeASTSchema(parse(fullSchema))[0]; + const partialDefinitions = encodeASTSchema(parse(partialSchema))[0]; + + const schemaFragment = { + schemaId: "test", + definitions: partialDefinitions, + resolvers: { + Mutation: { + downloadFile: () => { + return { downloadUrl: "hi" }; + }, + }, + }, + }; + + const parsedDocument = parse(document); + const result = await executeWithoutSchema({ + document: parsedDocument, + variableValues: vars, + schemaFragment, + schemaFragmentLoader: (currentFragment, _context, req) => { + currentFragment.definitions = fullDefinitions; + return Promise.resolve({ mergedFragment: currentFragment }); + }, + }); + + expect(result).toEqual({ data: { downloadFile: { downloadUrl: "hi" } } }); + }, + ); }); diff --git a/packages/supermassive/src/executeWithoutSchema.ts b/packages/supermassive/src/executeWithoutSchema.ts index 340c4523c..8aa01217c 100644 --- a/packages/supermassive/src/executeWithoutSchema.ts +++ b/packages/supermassive/src/executeWithoutSchema.ts @@ -41,7 +41,6 @@ import type { IncrementalExecutionResult, SchemaFragment, SchemaFragmentLoader, - SchemaFragmentRequest, } from "./types"; import { getArgumentValues, @@ -52,6 +51,7 @@ import type { ExecutionHooks } from "./hooks/types"; import { arraysAreEqual } from "./utilities/array"; import { isAsyncIterable } from "./jsutils/isAsyncIterable"; import { mapAsyncIterator } from "./utilities/mapAsyncIterator"; +import { requestSchemaFragment } from "./utilities/requestSchemaFragment"; import { GraphQLStreamDirective } from "./schema/directives"; import { memoize3 } from "./jsutils/memoize3"; import { @@ -140,6 +140,13 @@ export function executeWithoutSchema( // If a valid execution context cannot be created due to incorrect arguments, // a "Response" with only errors is returned. const exeContext = buildExecutionContext(args); + if (isPromise(exeContext)) { + return exeContext.then((ctx) => + "schemaFragment" in ctx + ? executeOperationWithBeforeHook(ctx) + : { errors: ctx }, + ); + } // Return early errors if execution context failed. if (!("schemaFragment" in exeContext)) { @@ -178,13 +185,13 @@ export function assertValidExecutionArguments( */ function buildExecutionContext( args: ExecutionWithoutSchemaArgs, -): Array | ExecutionContext { +): PromiseOrValue | ExecutionContext> { const { schemaFragment, schemaFragmentLoader, document, rootValue, - contextValue, + contextValue: initialContextValue, buildContextValue, variableValues, operationName, @@ -232,29 +239,25 @@ function buildExecutionContext( // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const variableDefinitions = operation.variableDefinitions ?? []; + const contextValue = buildContextValue + ? buildContextValue(initialContextValue) + : initialContextValue; const coercedVariableValues = getVariableValues( - schemaFragment, + { contextValue, schemaFragment, schemaFragmentLoader }, variableDefinitions, variableValues ?? {}, { maxErrors: 50 }, ); - if (coercedVariableValues.errors) { - return coercedVariableValues.errors; - } - - return { + const exeContext: Omit = { schemaFragment, schemaFragmentLoader, fragments, rootValue, - contextValue: buildContextValue - ? buildContextValue(contextValue) - : contextValue, + contextValue, buildContextValue, operation, - variableValues: coercedVariableValues.coerced, fieldResolver: fieldResolver ?? defaultFieldResolver, typeResolver: typeResolver ?? defaultTypeResolver, subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver, @@ -263,6 +266,28 @@ function buildExecutionContext( subsequentPayloads: new Set(), enablePerEventContext: enablePerEventContext ?? true, }; + + if (isPromise(coercedVariableValues)) { + return coercedVariableValues.then((r) => { + if (r.errors) { + return r.errors; + } + + return { + ...exeContext, + variableValues: r.coerced, + }; + }); + } + + if (coercedVariableValues.errors) { + return coercedVariableValues.errors; + } + + return { + ...exeContext, + variableValues: coercedVariableValues.coerced, + }; } function buildPerEventExecutionContext( @@ -609,32 +634,6 @@ function executeField( }); } -function requestSchemaFragment( - exeContext: ExecutionContext, - request: SchemaFragmentRequest, -): PromiseOrValue { - if (!exeContext.schemaFragmentLoader) { - return; - } - const currentSchemaId = exeContext.schemaFragment.schemaId; - return exeContext - .schemaFragmentLoader( - exeContext.schemaFragment, - exeContext.contextValue, - request, - ) - .then(({ mergedFragment, mergedContextValue }) => { - if (currentSchemaId !== mergedFragment.schemaId) { - throw new Error( - `Cannot use new schema fragment: old and new fragments describe different schemas:` + - ` ${currentSchemaId} vs. ${mergedFragment.schemaId}`, - ); - } - exeContext.contextValue = mergedContextValue ?? exeContext.contextValue; - exeContext.schemaFragment = mergedFragment; - }); -} - /** * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. @@ -822,7 +821,11 @@ function runSubscriptionResolver( result = hookContext.then((context) => { hookContext = context; - return resolveFn(rootValue, args, contextValue, info); + return isPromise(args) + ? args.then((resolvedArgs) => + resolveFn(rootValue, resolvedArgs, contextValue, info), + ) + : resolveFn(rootValue, args, contextValue, info); }); } } @@ -830,7 +833,11 @@ function runSubscriptionResolver( // Call the `subscribe()` resolver or the default resolver to produce an // AsyncIterable yielding raw payloads. if (result === undefined) { - result = resolveFn(rootValue, args, contextValue, info); + result = isPromise(args) + ? args.then((resolvedArgs) => + resolveFn(rootValue, resolvedArgs, contextValue, info), + ) + : resolveFn(rootValue, args, contextValue, info); } if (isPromise(result)) { @@ -1150,10 +1157,18 @@ function resolveAndCompleteField( return null; } - return resolveFn(source, args, contextValue, info); + return isPromise(args) + ? args.then((resolvedArgs) => + resolveFn(source, resolvedArgs, contextValue, info), + ) + : resolveFn(source, args, contextValue, info); }); } else { - result = resolveFn(source, args, contextValue, info); + result = isPromise(args) + ? args.then((resolvedArgs) => + resolveFn(source, resolvedArgs, contextValue, info), + ) + : resolveFn(source, args, contextValue, info); } let completed; diff --git a/packages/supermassive/src/types.ts b/packages/supermassive/src/types.ts index 457134685..cae45e565 100644 --- a/packages/supermassive/src/types.ts +++ b/packages/supermassive/src/types.ts @@ -277,6 +277,10 @@ export type SchemaFragment = { operationTypes?: OperationTypes; }; +export type SchemaFragmentForInputTypeRequest = { + kind: "InputType"; + typeName: string; +}; export type SchemaFragmentForReturnTypeRequest = { kind: "ReturnType"; parentTypeName: TypeName; @@ -290,7 +294,8 @@ export type SchemaFragmentForRuntimeTypeRequest = { export type SchemaFragmentRequest = | SchemaFragmentForReturnTypeRequest - | SchemaFragmentForRuntimeTypeRequest; + | SchemaFragmentForRuntimeTypeRequest + | SchemaFragmentForInputTypeRequest; export type SchemaFragmentLoaderResult = { mergedFragment: SchemaFragment; diff --git a/packages/supermassive/src/utilities/requestSchemaFragment.ts b/packages/supermassive/src/utilities/requestSchemaFragment.ts new file mode 100644 index 000000000..69b5d2aa1 --- /dev/null +++ b/packages/supermassive/src/utilities/requestSchemaFragment.ts @@ -0,0 +1,38 @@ +import type { + SchemaFragment, + SchemaFragmentLoader, + SchemaFragmentRequest, +} from "../types"; +import type { PromiseOrValue } from "../jsutils/PromiseOrValue"; + +export interface SchemaFragmentLoaderContext { + contextValue: unknown; + schemaFragment: SchemaFragment; + schemaFragmentLoader?: SchemaFragmentLoader; +} + +export function requestSchemaFragment( + exeContext: SchemaFragmentLoaderContext, + request: SchemaFragmentRequest, +): PromiseOrValue { + if (!exeContext.schemaFragmentLoader) { + return; + } + const currentSchemaId = exeContext.schemaFragment.schemaId; + return exeContext + .schemaFragmentLoader( + exeContext.schemaFragment, + exeContext.contextValue, + request, + ) + .then(({ mergedFragment, mergedContextValue }) => { + if (currentSchemaId !== mergedFragment.schemaId) { + throw new Error( + `Cannot use new schema fragment: old and new fragments describe different schemas:` + + ` ${currentSchemaId} vs. ${mergedFragment.schemaId}`, + ); + } + exeContext.contextValue = mergedContextValue ?? exeContext.contextValue; + exeContext.schemaFragment = mergedFragment; + }); +} diff --git a/packages/supermassive/src/values.ts b/packages/supermassive/src/values.ts index 5a14eb3bc..3d37a526d 100644 --- a/packages/supermassive/src/values.ts +++ b/packages/supermassive/src/values.ts @@ -6,6 +6,7 @@ import { DirectiveNode, FieldNode, VariableDefinitionNode, + ArgumentNode, } from "graphql"; import { inspect } from "./jsutils/inspect"; import { printPathArray } from "./jsutils/printPathArray"; @@ -20,15 +21,23 @@ import { isDefined, isInputType, getDirectiveDefinitionArgs, + InputValueDefinition, } from "./schema/definition"; +import { typeNameFromReference } from "./schema/reference"; import { valueFromAST } from "./utilities/valueFromAST"; import { coerceInputValue } from "./utilities/coerceInputValue"; +import { + type SchemaFragmentLoaderContext, + requestSchemaFragment, +} from "./utilities/requestSchemaFragment"; import { inspectTypeReference, isNonNullType, typeReferenceFromNode, } from "./schema/reference"; -import type { SchemaFragment } from "./types"; +import { PromiseOrValue } from "graphql/jsutils/PromiseOrValue"; +import { isPromise } from "./jsutils/isPromise"; +import { SchemaFragment } from "./types"; type CoercedVariableValues = | { errors: Array; coerced?: never } @@ -46,16 +55,16 @@ type CoercedVariableValues = * @internal */ export function getVariableValues( - schemaFragment: SchemaFragment, + context: SchemaFragmentLoaderContext, varDefNodes: ReadonlyArray, inputs: { [variable: string]: unknown }, options?: { maxErrors?: number }, -): CoercedVariableValues { +): PromiseOrValue { const errors: GraphQLError[] = []; const maxErrors = options?.maxErrors; try { const coerced = coerceVariableValues( - schemaFragment, + context, varDefNodes, inputs, (error) => { @@ -70,7 +79,9 @@ export function getVariableValues( ); if (errors.length === 0) { - return { coerced }; + return isPromise(coerced) + ? coerced.then((result) => ({ coerced: result })) + : { coerced }; } } catch (error) { errors.push(error as GraphQLError); @@ -80,61 +91,152 @@ export function getVariableValues( } function coerceVariableValues( - schemaFragment: SchemaFragment, + context: SchemaFragmentLoaderContext, varDefNodes: ReadonlyArray, inputs: { [variable: string]: unknown }, onError: (error: GraphQLError) => void, -): { [variable: string]: unknown } { +): PromiseOrValue<{ [variable: string]: unknown }> { const coercedValues: { [variable: string]: unknown } = {}; + const coercionResults = []; + let hasPromises = false; for (const varDefNode of varDefNodes) { - const varName = varDefNode.variable.name.value; - const varTypeReference = typeReferenceFromNode(varDefNode.type); + const r = coerceVariableValueWithFragmentLoader( + context, + varDefNode, + inputs, + onError, + ); + coercionResults.push(r); - if (!isInputType(schemaFragment.definitions, varTypeReference)) { - // Must use input types for variables. This should be caught during - // validation, however is checked again here for safety. - const varTypeStr = inspectTypeReference(varTypeReference); - onError( - locatedError( - `Variable "$${varName}" expected value of type "${varTypeStr}" which cannot be used as an input type.`, - [varDefNode.type], - ), + if (isPromise(r)) { + hasPromises = true; + } + } + + return hasPromises + ? Promise.all(coercionResults).then((results) => + results.reduce(reduceVariableCoercionResults, coercedValues), + ) + : (coercionResults as CoercionResult[]).reduce( + reduceVariableCoercionResults, + coercedValues, ); - continue; +} + +function reduceVariableCoercionResults( + coercedValues: Record, + result: CoercionResult, +): Record { + if (isCoercedValue(result)) { + coercedValues[result.varName] = result.value; + } + return coercedValues; +} + +type CoercedValue = { varName: string; value: unknown }; +type CoercionError = { varName: string; error: GraphQLError }; +type CoercionResult = CoercedValue | CoercionError; + +function isCoercedValue(r: CoercionResult): r is CoercedValue { + return "value" in r; +} + +function coerceVariableValueWithFragmentLoader( + context: SchemaFragmentLoaderContext, + varDefNode: Readonly, + inputs: { [variable: string]: unknown }, + onError: (error: GraphQLError) => void, +): PromiseOrValue { + const { schemaFragment } = context; + const varTypeReference = typeReferenceFromNode(varDefNode.type); + if (!isDefined(schemaFragment.definitions, varTypeReference)) { + const typeName = typeNameFromReference(varTypeReference); + // FIXME: this call needs to resolve all the nested types as well or it could still break down the chain + const loading = requestSchemaFragment(context, { + kind: "InputType", + typeName, + }); + + if (!loading) { + const varName = varDefNode.variable.name.value; + const error = locatedError( + `Type "${typeName}" for variable "${varName}" is missing.`, + [varDefNode.type], + ); + onError(error); + return { varName, error }; } - if (!hasOwnProperty(inputs, varName)) { - if (varDefNode.defaultValue) { - coercedValues[varName] = valueFromAST( + return loading.then(() => + coerceVariableValue(context.schemaFragment, varDefNode, inputs, onError), + ); + } + + return coerceVariableValue( + context.schemaFragment, + varDefNode, + inputs, + onError, + ); +} + +function coerceVariableValue( + schemaFragment: SchemaFragment, + varDefNode: Readonly, + inputs: { [variable: string]: unknown }, + onError: (error: GraphQLError) => void, +): CoercionResult { + const varName = varDefNode.variable.name.value; + const varTypeReference = typeReferenceFromNode(varDefNode.type); + + if (!isInputType(schemaFragment.definitions, varTypeReference)) { + // Must use input types for variables. This should be caught during + // validation, however is checked again here for safety. + const varTypeStr = inspectTypeReference(varTypeReference); + const error = locatedError( + `Variable "$${varName}" expected value of type "${varTypeStr}" which cannot be used as an input type.`, + [varDefNode.type], + ); + onError(error); + return { varName, error }; + } + + if (!hasOwnProperty(inputs, varName)) { + if (varDefNode.defaultValue) { + return { + varName, + value: valueFromAST( varDefNode.defaultValue, varTypeReference, schemaFragment, - ); - } else if (isNonNullType(varTypeReference)) { - const varTypeStr = inspectTypeReference(varTypeReference); - onError( - locatedError( - `Variable "$${varName}" of required type "${varTypeStr}" was not provided.`, - [varDefNode], - ), - ); - } - continue; - } - - const value = inputs[varName]; - if (value === null && isNonNullType(varTypeReference)) { - const varTypeStr = inspectTypeReference(varTypeReference); - onError( - locatedError( - `Variable "$${varName}" of non-null type "${varTypeStr}" must not be null.`, - [varDefNode], ), + }; + } else if (isNonNullType(varTypeReference)) { + const varTypeStr = inspectTypeReference(varTypeReference); + const error = locatedError( + `Variable "$${varName}" of required type "${varTypeStr}" was not provided.`, + [varDefNode], ); - continue; + onError(error); + return { varName, error }; } + // FIXME: probablt should return with undefined value here + } + + const value = inputs[varName]; + if (value === null && isNonNullType(varTypeReference)) { + const varTypeStr = inspectTypeReference(varTypeReference); + const error = locatedError( + `Variable "$${varName}" of non-null type "${varTypeStr}" must not be null.`, + [varDefNode], + ); + onError(error); + return { varName, error }; + } - coercedValues[varName] = coerceInputValue( + return { + varName, + value: coerceInputValue( value, varTypeReference, schemaFragment, @@ -146,10 +248,8 @@ function coerceVariableValues( } onError(locatedError(prefix + "; " + error.message, [varDefNode])); }, - ); - } - - return coercedValues; + ), + }; } function isFieldDefinition( @@ -173,19 +273,21 @@ export function getArgumentValues( exeContext: ExecutionContext, def: FieldDefinition | DirectiveDefinitionTuple, node: FieldNode | DirectiveNode, -): { [argument: string]: unknown } { - const definitions = exeContext.schemaFragment.definitions; - const coercedValues: { [argument: string]: unknown } = {}; +): PromiseOrValue<{ [argument: string]: unknown }> { + const coercedValues: Record = {}; const argumentDefs = isFieldDefinition(def, node) ? getFieldDefinitionArgs(def) : getDirectiveDefinitionArgs(def); + if (!argumentDefs) { return coercedValues; } + // istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203') const argumentNodes = node.arguments ?? []; const argNodeMap = new Map(argumentNodes.map((arg) => [arg.name.value, arg])); - + let hasPromises = false; + const coercionResults = []; for (const [name, argumentDef] of Object.entries(argumentDefs)) { const argumentNode = argNodeMap.get(name); const argumentTypeRef = getInputValueTypeReference(argumentDef); @@ -204,72 +306,153 @@ export function getArgumentValues( continue; } - if (!isDefined(definitions, argumentTypeRef)) { - throw locatedError( - `Could not find type for argument ${name} in ${node.kind} ${node.name.value}`, - [argumentNode], - ); - } + const { + schemaFragment, + schemaFragmentLoader, + contextValue, + variableValues, + } = exeContext; - // if (!schemaTypes.isInputType(argumentTypeRef)) { - // const type = schemaTypes.printTypeRef(argumentTypeRef); - // throw locatedError( - // `Argument "$${name}" expected value of type "${type}" which cannot be used as an input type.`, - // [argumentNode], - // ); - // } - - const valueNode = argumentNode.value; - let isNull = valueNode.kind === Kind.NULL; - - if (valueNode.kind === Kind.VARIABLE) { - const variableName = valueNode.name.value; - if ( - exeContext.variableValues == null || - !hasOwnProperty(exeContext.variableValues, variableName) - ) { - if (defaultValue !== undefined) { - coercedValues[name] = defaultValue; - } else if (isNonNullType(argumentTypeRef)) { - const type = inspectTypeReference(argumentTypeRef); - throw locatedError( - `Argument "${name}" of required type "${type}" ` + - `was provided the variable "$${variableName}" which was not provided a runtime value.`, - [valueNode], - ); - } - continue; - } - isNull = exeContext.variableValues[variableName] == null; + const r = getArgumentValueWithFragmentLoader( + { contextValue, schemaFragment, schemaFragmentLoader }, + argumentNode, + argumentDef, + variableValues, + ); + + coercionResults.push(r); + + if (isPromise(r)) { + hasPromises = true; } + } - if (isNull && isNonNullType(argumentTypeRef)) { - const type = inspectTypeReference(argumentTypeRef); - throw locatedError( - `Argument "${name}" of non-null type "${type}" must not be null."`, - [valueNode], + return hasPromises + ? Promise.all(coercionResults).then((results) => + results.reduce(reduceArgCoercionResults, coercedValues), + ) + : (coercionResults as ArgCoercionResult[]).reduce( + reduceArgCoercionResults, + coercedValues, ); +} + +type ArgValue = { argName: string; value: unknown }; +type ArgError = { argName: string; error: GraphQLError }; +type ArgCoercionResult = ArgValue | ArgError; + +function isArgValue(r: ArgCoercionResult): r is ArgValue { + return "value" in r; +} + +function reduceArgCoercionResults( + coercedValues: Record, + result: ArgCoercionResult, +): Record { + if (isArgValue(result)) { + coercedValues[result.argName] = result.value; + } + return coercedValues; +} + +function getArgumentValueWithFragmentLoader( + context: SchemaFragmentLoaderContext, + argNode: ArgumentNode, + argDef: InputValueDefinition, + variableValues: { [variable: string]: unknown }, +): PromiseOrValue { + const argName = argNode.name.value; + const argumentTypeRef = getInputValueTypeReference(argDef); + + const { schemaFragment } = context; + if (!isDefined(schemaFragment.definitions, argumentTypeRef)) { + const typeName = typeNameFromReference(argumentTypeRef); + // FIXME: this call needs to resolve all the nested types as well or it could still break down the chain + const loading = requestSchemaFragment(context, { + kind: "InputType", + typeName, + }); + + if (!loading) { + const error = locatedError( + `Type "${typeName}" for argument "${argName}" is missing.`, + [argNode], + ); + return { argName, error }; } - const coercedValue = valueFromAST( - valueNode, - argumentTypeRef, - exeContext.schemaFragment, - exeContext.variableValues, + return loading.then(() => + coerceArgumentValue(context, argNode, argDef, variableValues), ); - 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. - throw locatedError( - `Argument "${name}" has invalid value ${print(valueNode)}.`, - [valueNode], - ); + } + + return coerceArgumentValue(context, argNode, argDef, variableValues); +} + +function coerceArgumentValue( + context: SchemaFragmentLoaderContext, + argNode: ArgumentNode, + argDef: InputValueDefinition, + variableValues: { [variable: string]: unknown }, +) { + const argumentTypeRef = getInputValueTypeReference(argDef); + + const argName = argNode.name.value; + const valueNode = argNode.value; + let isNull = valueNode.kind === Kind.NULL; + + if (valueNode.kind === Kind.VARIABLE) { + const variableName = valueNode.name.value; + const defaultValue = getInputDefaultValue(argDef); + if ( + variableValues == null || + !hasOwnProperty(variableValues, variableName) + ) { + if (defaultValue !== undefined) { + return { argName, value: defaultValue }; + } + if (isNonNullType(argumentTypeRef)) { + const type = inspectTypeReference(argumentTypeRef); + const error = locatedError( + `Argument "${name}" of required type "${type}" ` + + `was provided the variable "$${variableName}" which was not provided a runtime value.`, + [valueNode], + ); + + return { argName, error }; + } + // FIXME: what to do here? check original implementation } - coercedValues[name] = coercedValue; + isNull = variableValues[variableName] == null; } - return coercedValues; + if (isNull && isNonNullType(argumentTypeRef)) { + const type = inspectTypeReference(argumentTypeRef); + const error = locatedError( + `Argument "${name}" of non-null type "${type}" must not be null."`, + [valueNode], + ); + return { argName, error }; + } + + const coercedValue = valueFromAST( + valueNode, + argumentTypeRef, + context.schemaFragment, + variableValues, + ); + 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. + const error = locatedError( + `Argument "${argName}" has invalid value ${print(valueNode)}.`, + [valueNode], + ); + return { argName, error }; + } + + return { argName, value: coercedValue }; } /** @@ -296,7 +479,18 @@ export function getDirectiveValues( ); if (directiveNode) { - return getArgumentValues(exeContext, directiveDef, directiveNode); + const argValues = getArgumentValues( + exeContext, + directiveDef, + directiveNode, + ); + + // FIXME: check if schemaLoader should load types for directives + if (isPromise(argValues)) { + throw locatedError("FIXME:getDirectiveValues", node.directives); + } + + return argValues; } }