/** @category Values */
import { invariant } from '../jsutils/invariant.ts';
import type { Maybe } from '../jsutils/Maybe.ts';
import type { ObjMap, ReadOnlyObjMap } from '../jsutils/ObjMap.ts';
import { printPathArray } from '../jsutils/printPathArray.ts';
import { ensureGraphQLError } from '../error/ensureGraphQLError.ts';
import { GraphQLError } from '../error/GraphQLError.ts';
import type {
ArgumentNode,
DirectiveNode,
FieldNode,
FragmentArgumentNode,
FragmentSpreadNode,
VariableDefinitionNode,
} from '../language/ast.ts';
import { Kind } from '../language/kinds.ts';
import type { GraphQLArgument, GraphQLField } from '../type/definition.ts';
import {
isArgument,
isNonNullType,
isRequiredArgument,
} from '../type/definition.ts';
import type { GraphQLDirective } from '../type/directives.ts';
import type { GraphQLSchema } from '../type/schema.ts';
import { validateDefaultInput } from '../type/validate.ts';
import {
coerceDefaultValue,
coerceInputLiteral,
coerceInputValue,
} from '../utilities/coerceInputValue.ts';
import {
validateInputLiteral,
validateInputValue,
} from '../utilities/validateInputValue.ts';
import type {
FragmentVariableValues,
FragmentVariableValueSource,
} from './collectFields.ts';
import type { GraphQLVariableSignature } from './getVariableSignature.ts';
import { getVariableSignature } from './getVariableSignature.ts';
/**
* Coerced variable values prepared for execution.
*
* The `coerced` map contains runtime values keyed by variable name. The
* `sources` map records whether each value came from request input, an operation
* default, or a fragment-variable default so utilities can preserve defaults
* when replacing variables in literals.
*/
export interface VariableValues {
/** Source metadata for each variable value keyed by variable name. */
readonly sources: ReadOnlyObjMap<VariableValueSource>;
/** Coerced runtime variable values keyed by variable name. */
readonly coerced: ReadOnlyObjMap<unknown>;
}
interface VariableValueSource {
readonly signature: GraphQLVariableSignature;
readonly value?: unknown;
}
type VariableValuesOrErrors =
| { variableValues: VariableValues; errors?: never }
| { errors: ReadonlyArray<GraphQLError>; variableValues?: never };
/**
* Prepares an object map of variableValues of the correct type based on the
* provided variable definitions and arbitrary input. If the input cannot be
* parsed to match the variable definitions, GraphQLError values are returned.
*
* Note: Returned maps use null prototypes to avoid collisions with
* Object prototype properties.
* @param schema - GraphQL schema to use.
* @param varDefNodes - The variable definition AST nodes to coerce.
* @param inputs - The runtime variable values keyed by variable name.
* @param options - Optional configuration for this operation.
* @param [options.maxErrors] - Maximum number of coercion errors to report.
* @param [options.hideSuggestions] - Whether suggestion text should be omitted
* from errors.
* @returns Coerced variable values with source metadata, or request errors.
* @example
* ```ts
* // Coerce provided variables and apply operation defaults.
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { getVariableValues } from 'graphql/execution';
*
* const schema = buildSchema(`
* type Query {
* reviews(stars: Int!, limit: Int = 10): [String]
* }
* `);
* const document = parse(`
* query ($stars: Int!, $limit: Int = 10) {
* reviews(stars: $stars, limit: $limit)
* }
* `);
* const operation = document.definitions[0];
*
* const result = getVariableValues(schema, operation.variableDefinitions, {
* stars: '5',
* });
*
* assert('variableValues' in result);
*
* result.variableValues.coerced; // => { stars: 5, limit: 10 }
* ```
* @example
* ```ts
* // This variant uses maxErrors to cap reported coercion errors.
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { getVariableValues } from 'graphql/execution';
*
* const schema = buildSchema(`
* input ReviewInput {
* stars: Int!
* }
*
* type Query {
* review(input: ReviewInput!): String
* }
* `);
* const document = parse(`
* query ($first: ReviewInput!, $second: ReviewInput!) {
* first: review(input: $first)
* second: review(input: $second)
* }
* `);
* const operation = document.definitions[0];
*
* const result = getVariableValues(
* schema,
* operation.variableDefinitions,
* { first: { stars: 'bad' }, second: { stars: 'also bad' } },
* { maxErrors: 1 },
* );
*
* assert('errors' in result);
*
* result.errors.length; // => 2
* result.errors[1].message; // matches /error limit reached/
* ```
*/
export function getVariableValues(
schema: GraphQLSchema,
varDefNodes: ReadonlyArray<VariableDefinitionNode>,
inputs: { readonly [variable: string]: unknown },
options?: { maxErrors?: number; hideSuggestions?: boolean },
): VariableValuesOrErrors {
const errors: Array<GraphQLError> = [];
const maxErrors = options?.maxErrors;
try {
const variableValues = coerceVariableValues(
schema,
varDefNodes,
inputs,
(error) => {
if (maxErrors != null && errors.length >= maxErrors) {
throw new GraphQLError(
'Too many errors processing variables, error limit reached. Execution aborted.',
);
}
errors.push(error);
},
options?.hideSuggestions,
);
if (errors.length === 0) {
return { variableValues };
}
} catch (error) {
errors.push(ensureGraphQLError(error));
}
return { errors };
}
function coerceVariableValues(
schema: GraphQLSchema,
varDefNodes: ReadonlyArray<VariableDefinitionNode>,
inputs: { readonly [variable: string]: unknown },
onError: (error: GraphQLError) => void,
hideSuggestions?: Maybe<boolean>,
): VariableValues {
const sources: ObjMap<VariableValueSource> = Object.create(null);
const coerced: ObjMap<unknown> = Object.create(null);
for (const varDefNode of varDefNodes) {
const varSignature = getVariableSignature(schema, varDefNode);
if (varSignature instanceof GraphQLError) {
onError(varSignature);
continue;
}
const { name: varName, type: varType } = varSignature;
const value = Object.hasOwn(inputs, varName) ? inputs[varName] : undefined;
if (value === undefined) {
sources[varName] = { signature: varSignature };
if (varDefNode.defaultValue) {
maybeUseDefaultValue(
coerced,
varName,
varSignature,
(error, path) => {
onError(
new GraphQLError(
`Variable "$${varName}" has invalid default value${printPathArray(path)}: ${error.message}`,
{ nodes: varDefNode },
),
);
},
hideSuggestions,
);
continue;
} else if (!isNonNullType(varType)) {
// Non-provided values for nullable variables are omitted.
continue;
}
} else {
sources[varName] = { signature: varSignature, value };
}
const coercedValue = coerceInputValue(value, varType);
if (coercedValue !== undefined) {
coerced[varName] = coercedValue;
} else {
validateInputValue(
value,
varType,
(error, path) => {
onError(
new GraphQLError(
`Variable "$${varName}" has invalid value${printPathArray(path)}: ${
error.message
}`,
{ nodes: varDefNode, originalError: error },
),
);
},
hideSuggestions,
);
}
}
return { sources, coerced };
}
function maybeUseDefaultValue(
coercedValues: ObjMap<unknown>,
name: string,
inputValue: GraphQLArgument | GraphQLVariableSignature,
onError: (error: GraphQLError, path: ReadonlyArray<string | number>) => void,
hideSuggestions?: Maybe<boolean>,
): void {
try {
// coerceDefaultValue assumes validation has already rejected invalid
// defaults. If validation was skipped, invalid defaults or nested input
// field defaults can throw here; recover with validation-style errors below.
const coercedDefaultValue = coerceDefaultValue(inputValue);
if (coercedDefaultValue !== undefined) {
coercedValues[name] = coercedDefaultValue;
}
} catch (error) {
const defaultInput = inputValue.default;
// Defensive: coerceDefaultValue should only throw while coercing a default.
/* node:coverage ignore next 3 */
if (defaultInput === undefined) {
throw error;
}
// Prefer validation's user-facing errors for invalid defaults.
let reportedValidationError = false;
validateDefaultInput(
defaultInput,
inputValue.type,
(defaultError, path) => {
reportedValidationError = true;
onError(defaultError, path);
},
hideSuggestions,
);
if (!reportedValidationError) {
// The default itself validated, so coercion failed while applying a nested
// input field default. Surface the original coercion error.
onError(ensureGraphQLError(error), []);
}
}
}
/** @internal */
export function getFragmentVariableValues(
fragmentSpreadNode: FragmentSpreadNode,
fragmentSignatures: ReadOnlyObjMap<GraphQLVariableSignature>,
variableValues: VariableValues,
fragmentVariableValues?: Maybe<FragmentVariableValues>,
hideSuggestions?: Maybe<boolean>,
): FragmentVariableValues {
const argumentNodes = fragmentSpreadNode.arguments ?? [];
const argNodeMap = new Map(argumentNodes.map((arg) => [arg.name.value, arg]));
const sources: ObjMap<FragmentVariableValueSource> = Object.create(null);
const coerced: ObjMap<unknown> = Object.create(null);
for (const [varName, varSignature] of Object.entries(fragmentSignatures)) {
const argumentNode = argNodeMap.get(varName);
if (argumentNode !== undefined) {
sources[varName] =
fragmentVariableValues == null
? { signature: varSignature, value: argumentNode.value }
: {
signature: varSignature,
value: argumentNode.value,
fragmentVariableValues,
};
} else {
sources[varName] = {
signature: varSignature,
};
}
coerceArgument(
coerced,
fragmentSpreadNode,
varName,
varSignature,
argumentNode,
variableValues,
fragmentVariableValues,
hideSuggestions,
);
}
return { sources, coerced };
}
/**
* Prepares an object map of argument values given a list of argument
* definitions and list of argument AST nodes.
*
* Note: Returned value uses a null prototype to avoid collisions with
* JavaScript's own property names.
* @param def - Field or directive definition that declares the arguments.
* @param node - Field or directive AST node supplying argument literals.
* @param variableValues - Operation variable values returned by getVariableValues.
* @param fragmentVariableValues - Fragment variable values for the current fragment scope.
* @param hideSuggestions - Whether suggestion text should be omitted from errors.
* @returns A map of coerced argument values.
* @example
* ```ts
* // Read literal argument values and defaults.
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { getArgumentValues } from 'graphql/execution';
*
* const schema = buildSchema(`
* type Query {
* reviews(stars: Int!, limit: Int = 10): [String]
* }
* `);
* const fieldDef = schema.getQueryType().getFields().reviews;
* const document = parse('{ reviews(stars: 5) }');
* const fieldNode = document.definitions[0].selectionSet.selections[0];
*
* getArgumentValues(fieldDef, fieldNode); // => { stars: 5, limit: 10 }
* ```
* @example
* ```ts
* // This variant resolves argument values from operation variables.
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
* import { getArgumentValues, getVariableValues } from 'graphql/execution';
*
* const schema = buildSchema(`
* type Query {
* reviews(stars: Int!): [String]
* }
* `);
* const fieldDef = schema.getQueryType().getFields().reviews;
* const document = parse('query ($stars: Int!) { reviews(stars: $stars) }');
* const operation = document.definitions[0];
* const fieldNode = document.definitions[0].selectionSet.selections[0];
* const variables = getVariableValues(schema, operation.variableDefinitions, {
* stars: '5',
* });
*
* assert('variableValues' in variables);
*
* getArgumentValues(fieldDef, fieldNode, variables.variableValues); // => { stars: 5 }
* getArgumentValues(fieldDef, fieldNode); // throws an error
* ```
*/
export function getArgumentValues(
def: GraphQLField<unknown, unknown> | GraphQLDirective,
node: FieldNode | DirectiveNode,
variableValues?: Maybe<VariableValues>,
fragmentVariableValues?: Maybe<FragmentVariableValues>,
hideSuggestions?: Maybe<boolean>,
): ObjMap<unknown> {
const coercedValues: ObjMap<unknown> = Object.create(null);
const argumentNodes = node.arguments ?? [];
const argNodeMap = new Map(argumentNodes.map((arg) => [arg.name.value, arg]));
for (const argDef of def.args) {
const name = argDef.name;
coerceArgument(
coercedValues,
node,
name,
argDef,
argNodeMap.get(argDef.name),
variableValues,
fragmentVariableValues,
hideSuggestions,
);
}
return coercedValues;
}
/** @internal */
// eslint-disable-next-line max-params
function coerceArgument(
coercedValues: ObjMap<unknown>,
node: FieldNode | DirectiveNode | FragmentSpreadNode,
argName: string,
argDef: GraphQLArgument | GraphQLVariableSignature,
argumentNode: ArgumentNode | FragmentArgumentNode | undefined,
variableValues: Maybe<VariableValues>,
fragmentVariableValues: Maybe<FragmentVariableValues>,
hideSuggestions?: Maybe<boolean>,
): void {
const argType = argDef.type;
const onArgDefaultValueError = (
error: GraphQLError,
path: ReadonlyArray<string | number>,
): never => {
throw new GraphQLError(
`${printArgumentOrFragmentVariable(argDef, node)} has invalid default value${printPathArray(path)}: ${error.message}`,
{ nodes: node },
);
};
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
`${printArgumentOrFragmentVariable(argDef, node)} of required type "${argType}" was not provided.`,
{ nodes: node },
);
}
maybeUseDefaultValue(
coercedValues,
argName,
argDef,
onArgDefaultValueError,
hideSuggestions,
);
return;
}
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)
) {
maybeUseDefaultValue(
coercedValues,
argName,
argDef,
onArgDefaultValueError,
hideSuggestions,
);
return;
}
}
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 = `${printArgumentOrFragmentVariable(argDef, node)} has invalid value${printPathArray(
path,
)}: ${error.message}`;
throw error;
},
variableValues,
fragmentVariableValues,
hideSuggestions,
);
/* node:coverage ignore next */
invariant(false, 'Invalid argument');
}
coercedValues[argName] = coercedValue;
}
// TODO: clean up the naming of isRequiredArgument(), isArgument(), and argDef if/when experimental fragment variables are merged
function printArgumentOrFragmentVariable(
argDef: GraphQLArgument | GraphQLVariableSignature,
node: FieldNode | DirectiveNode | FragmentSpreadNode,
): string {
return isArgument(argDef)
? `Argument "${argDef}"`
: `Variable "$${argDef.name}" defined by fragment "${node.name.value}"`;
}
/**
* Prepares an object map of argument values given a directive definition
* and a AST node which may contain directives. Optionally also accepts a map
* of variable values.
*
* If the directive does not exist on the node, returns undefined.
*
* Note: Returned value uses a null prototype to avoid collisions with
* JavaScript's own property names.
* @param directiveDef - Directive definition to read argument definitions from.
* @param node - AST node that may contain directives.
* @param node.directives - The directives on the AST node.
* @param variableValues - Operation variable values returned by getVariableValues.
* @param fragmentVariableValues - Fragment variable values for the current fragment scope.
* @param hideSuggestions - Whether suggestion text should be omitted from errors.
* @returns A map of coerced directive argument values, or undefined when absent.
* @example
* ```ts
* // Read literal directive arguments from a node.
* import { parse } from 'graphql/language';
* import { GraphQLSkipDirective } from 'graphql/type';
* import { getDirectiveValues } from 'graphql/execution';
*
* const document = parse('{ name @skip(if: true) }');
* const fieldNode = document.definitions[0].selectionSet.selections[0];
*
* getDirectiveValues(GraphQLSkipDirective, fieldNode); // => { if: true }
* ```
* @example
* ```ts
* // This variant resolves directive arguments from variables and handles absent directives.
* import assert from 'node:assert';
* import { parse } from 'graphql/language';
* import { GraphQLIncludeDirective } from 'graphql/type';
* import { buildSchema } from 'graphql/utilities';
* import { getDirectiveValues, getVariableValues } from 'graphql/execution';
*
* const schema = buildSchema('type Query { name: String }');
* const document = parse(
* 'query ($includeName: Boolean!) { name @include(if: $includeName) }',
* );
* const operation = document.definitions[0];
* const fieldNode = document.definitions[0].selectionSet.selections[0];
* const variables = getVariableValues(schema, operation.variableDefinitions, {
* includeName: false,
* });
*
* assert('variableValues' in variables);
*
* getDirectiveValues(
* GraphQLIncludeDirective,
* fieldNode,
* variables.variableValues,
* ); // => { if: false }
* getDirectiveValues(GraphQLIncludeDirective, { directives: [] }); // => undefined
* ```
*/
export function getDirectiveValues(
directiveDef: GraphQLDirective,
node: { readonly directives?: ReadonlyArray<DirectiveNode> | undefined },
variableValues?: Maybe<VariableValues>,
fragmentVariableValues?: Maybe<FragmentVariableValues>,
hideSuggestions?: Maybe<boolean>,
): undefined | ObjMap<unknown> {
const directiveNode = node.directives?.find(
(directive) => directive.name.value === directiveDef.name,
);
if (directiveNode) {
return getArgumentValues(
directiveDef,
directiveNode,
variableValues,
fragmentVariableValues,
hideSuggestions,
);
}
}