Skip to content

Commit fdb733e

Browse files
committed
Preserve sources of variable values
Depends on #3074 By way of introducing type `VariableValues`, allows `getVariableValues` to return both the coerced values as well as the original sources, which are then made available in `ExecutionContext`. While variable sources are not used directly here, they're used directly in #3065. This PR is pulled out as a pre-req to aid review
1 parent 2ee1da8 commit fdb733e

11 files changed

+116
-62
lines changed

src/execution/execute.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
GraphQLObjectType,
2424
} from '../type/definition';
2525

26+
import { VariableValues } from './values';
27+
2628
/**
2729
* Terminology
2830
*
@@ -55,7 +57,7 @@ export interface ExecutionContext {
5557
contextValue: unknown;
5658
fragments: ObjMap<FragmentDefinitionNode>;
5759
operation: OperationDefinitionNode;
58-
variableValues: { [key: string]: unknown };
60+
variableValues: VariableValues;
5961
fieldResolver: GraphQLFieldResolver<any, any>;
6062
typeResolver: GraphQLTypeResolver<any, any>;
6163
errors: Array<GraphQLError>;

src/execution/execute.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
import { typeFromAST } from '../utilities/typeFromAST';
6161
import { getOperationRootType } from '../utilities/getOperationRootType';
6262

63+
import type { VariableValues } from './values';
6364
import {
6465
getVariableValues,
6566
getArgumentValues,
@@ -98,7 +99,7 @@ export type ExecutionContext = {|
9899
rootValue: mixed,
99100
contextValue: mixed,
100101
operation: OperationDefinitionNode,
101-
variableValues: { [variable: string]: mixed, ... },
102+
variableValues: VariableValues,
102103
fieldResolver: GraphQLFieldResolver<any, any>,
103104
typeResolver: GraphQLTypeResolver<any, any>,
104105
errors: Array<GraphQLError>,
@@ -679,7 +680,7 @@ export function buildResolveInfo(
679680
fragments: exeContext.fragments,
680681
rootValue: exeContext.rootValue,
681682
operation: exeContext.operation,
682-
variableValues: exeContext.variableValues,
683+
variableValues: exeContext.variableValues.coerced,
683684
};
684685
}
685686

src/execution/values.d.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Maybe } from '../jsutils/Maybe';
2-
import { ObjMap } from '../jsutils/ObjMap';
2+
import { ReadOnlyObjMap } from '../jsutils/ObjMap';
33

44
import { GraphQLError } from '../error/GraphQLError';
55
import {
@@ -10,11 +10,20 @@ import {
1010

1111
import { GraphQLDirective } from '../type/directives';
1212
import { GraphQLSchema } from '../type/schema';
13-
import { GraphQLField } from '../type/definition';
13+
import { GraphQLField, GraphQLInputType } from '../type/definition';
14+
15+
export type VariableValues = {
16+
readonly sources: ReadOnlyObjMap<{
17+
readonly variable: VariableDefinitionNode;
18+
readonly type: GraphQLInputType;
19+
readonly value: unknown;
20+
}>;
21+
readonly coerced: ReadOnlyObjMap<unknown>;
22+
};
1423

1524
type CoercedVariableValues =
1625
| { errors: ReadonlyArray<GraphQLError>; coerced?: never }
17-
| { errors?: never; coerced: { [key: string]: unknown } };
26+
| { errors?: never; coerced: VariableValues };
1827

1928
/**
2029
* Prepares an object map of variableValues of the correct type based on the
@@ -43,7 +52,7 @@ export function getVariableValues(
4352
export function getArgumentValues(
4453
def: GraphQLField<unknown, unknown> | GraphQLDirective,
4554
node: FieldNode | DirectiveNode,
46-
variableValues?: Maybe<ObjMap<unknown>>,
55+
variableValues?: Maybe<VariableValues>,
4756
): { [key: string]: unknown };
4857

4958
/**
@@ -62,5 +71,5 @@ export function getDirectiveValues(
6271
node: {
6372
readonly directives?: ReadonlyArray<DirectiveNode>;
6473
},
65-
variableValues?: Maybe<ObjMap<unknown>>,
74+
variableValues?: Maybe<VariableValues>,
6675
): undefined | { [key: string]: unknown };

src/execution/values.js

+31-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { ObjMap } from '../jsutils/ObjMap';
2-
import { keyMap } from '../jsutils/keyMap';
1+
import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from '../jsutils/ObjMap';
32
import { inspect } from '../jsutils/inspect';
3+
import { keyMap } from '../jsutils/keyMap';
44
import { printPathArray } from '../jsutils/printPathArray';
55

66
import { GraphQLError } from '../error/GraphQLError';
@@ -14,7 +14,7 @@ import { Kind } from '../language/kinds';
1414
import { print } from '../language/printer';
1515

1616
import type { GraphQLSchema } from '../type/schema';
17-
import type { GraphQLField } from '../type/definition';
17+
import type { GraphQLInputType, GraphQLField } from '../type/definition';
1818
import type { GraphQLDirective } from '../type/directives';
1919
import { isInputType, isNonNullType } from '../type/definition';
2020
import { getCoercedDefaultValue } from '../type/defaultValues';
@@ -23,9 +23,18 @@ import { typeFromAST } from '../utilities/typeFromAST';
2323
import { valueFromAST } from '../utilities/valueFromAST';
2424
import { coerceInputValue } from '../utilities/coerceInputValue';
2525

26+
export type VariableValues = {|
27+
+sources: ReadOnlyObjMap<{|
28+
+variable: VariableDefinitionNode,
29+
+type: GraphQLInputType,
30+
+value: mixed,
31+
|}>,
32+
+coerced: ReadOnlyObjMap<mixed>,
33+
|};
34+
2635
type CoercedVariableValues =
2736
| {| errors: $ReadOnlyArray<GraphQLError> |}
28-
| {| coerced: { [variable: string]: mixed, ... } |};
37+
| {| coerced: VariableValues |};
2938

3039
/**
3140
* Prepares an object map of variableValues of the correct type based on the
@@ -41,7 +50,7 @@ type CoercedVariableValues =
4150
export function getVariableValues(
4251
schema: GraphQLSchema,
4352
varDefNodes: $ReadOnlyArray<VariableDefinitionNode>,
44-
inputs: { +[variable: string]: mixed, ... },
53+
inputs: ReadOnlyObjMapLike<mixed>,
4554
options?: {| maxErrors?: number |},
4655
): CoercedVariableValues {
4756
const errors = [];
@@ -74,10 +83,11 @@ export function getVariableValues(
7483
function coerceVariableValues(
7584
schema: GraphQLSchema,
7685
varDefNodes: $ReadOnlyArray<VariableDefinitionNode>,
77-
inputs: { +[variable: string]: mixed, ... },
86+
inputs: ReadOnlyObjMapLike<mixed>,
7887
onError: (error: GraphQLError) => void,
79-
): { [variable: string]: mixed, ... } {
80-
const coercedValues = {};
88+
): VariableValues {
89+
const sources = Object.create(null);
90+
const coerced = Object.create(null);
8191
for (const varDefNode of varDefNodes) {
8292
const varName = varDefNode.variable.name.value;
8393
const varType = typeFromAST(schema, varDefNode.type);
@@ -96,7 +106,12 @@ function coerceVariableValues(
96106

97107
if (!hasOwnProperty(inputs, varName)) {
98108
if (varDefNode.defaultValue) {
99-
coercedValues[varName] = valueFromAST(varDefNode.defaultValue, varType);
109+
sources[varName] = {
110+
variable: varDefNode,
111+
type: varType,
112+
value: undefined,
113+
};
114+
coerced[varName] = valueFromAST(varDefNode.defaultValue, varType);
100115
} else if (isNonNullType(varType)) {
101116
const varTypeStr = inspect(varType);
102117
onError(
@@ -121,7 +136,8 @@ function coerceVariableValues(
121136
continue;
122137
}
123138

124-
coercedValues[varName] = coerceInputValue(
139+
sources[varName] = { variable: varDefNode, type: varType, value };
140+
coerced[varName] = coerceInputValue(
125141
value,
126142
varType,
127143
(path, invalidValue, error) => {
@@ -144,7 +160,7 @@ function coerceVariableValues(
144160
);
145161
}
146162

147-
return coercedValues;
163+
return { sources, coerced };
148164
}
149165

150166
/**
@@ -160,7 +176,7 @@ function coerceVariableValues(
160176
export function getArgumentValues(
161177
def: GraphQLField<mixed, mixed> | GraphQLDirective,
162178
node: FieldNode | DirectiveNode,
163-
variableValues?: ?ObjMap<mixed>,
179+
variableValues?: ?VariableValues,
164180
): { [argument: string]: mixed, ... } {
165181
const coercedValues = {};
166182

@@ -196,7 +212,7 @@ export function getArgumentValues(
196212
const variableName = valueNode.name.value;
197213
if (
198214
variableValues == null ||
199-
!hasOwnProperty(variableValues, variableName)
215+
variableValues.coerced[variableName] === undefined
200216
) {
201217
if (argDef.defaultValue) {
202218
coercedValues[name] = getCoercedDefaultValue(
@@ -212,7 +228,7 @@ export function getArgumentValues(
212228
}
213229
continue;
214230
}
215-
isNull = variableValues[variableName] == null;
231+
isNull = variableValues.coerced[variableName] == null;
216232
}
217233

218234
if (isNull && isNonNullType(argType)) {
@@ -252,7 +268,7 @@ export function getArgumentValues(
252268
export function getDirectiveValues(
253269
directiveDef: GraphQLDirective,
254270
node: { +directives?: $ReadOnlyArray<DirectiveNode>, ... },
255-
variableValues?: ?ObjMap<mixed>,
271+
variableValues?: ?VariableValues,
256272
): void | { [argument: string]: mixed, ... } {
257273
// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
258274
const directiveNode = node.directives?.find(

src/type/definition.d.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Maybe } from '../jsutils/Maybe';
55

66
import { PromiseOrValue } from '../jsutils/PromiseOrValue';
77
import { Path } from '../jsutils/Path';
8-
import { ObjMap } from '../jsutils/ObjMap';
8+
import { ObjMap, ReadOnlyObjMap } from '../jsutils/ObjMap';
99

1010
import {
1111
ScalarTypeDefinitionNode,
@@ -345,7 +345,7 @@ export type GraphQLScalarValueParser<TInternal> = (
345345
) => Maybe<TInternal>;
346346
export type GraphQLScalarLiteralParser<TInternal> = (
347347
valueNode: ValueNode,
348-
variables: Maybe<ObjMap<unknown>>,
348+
variables: Maybe<ReadOnlyObjMap<unknown>>,
349349
) => Maybe<TInternal>;
350350

351351
export interface GraphQLScalarTypeConfig<TInternal, TExternal> {
@@ -487,7 +487,7 @@ export interface GraphQLResolveInfo {
487487
readonly fragments: ObjMap<FragmentDefinitionNode>;
488488
readonly rootValue: unknown;
489489
readonly operation: OperationDefinitionNode;
490-
readonly variableValues: { [variableName: string]: unknown };
490+
readonly variableValues: ReadOnlyObjMap<unknown>;
491491
}
492492

493493
/**
@@ -790,7 +790,7 @@ export class GraphQLEnumType {
790790
parseValue(value: unknown): Maybe<any>;
791791
parseLiteral(
792792
valueNode: ValueNode,
793-
_variables: Maybe<ObjMap<unknown>>,
793+
_variables: Maybe<ReadOnlyObjMap<unknown>>,
794794
): Maybe<any>;
795795

796796
toConfig(): GraphQLEnumTypeConfig & {

src/type/definition.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ export type GraphQLScalarValueParser<TInternal> = (
641641
642642
export type GraphQLScalarLiteralParser<TInternal> = (
643643
valueNode: ValueNode,
644-
variables: ?ObjMap<mixed>,
644+
variables: ?ReadOnlyObjMap<mixed>,
645645
) => ?TInternal;
646646
647647
export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
@@ -909,7 +909,7 @@ export type GraphQLResolveInfo = {|
909909
+fragments: ObjMap<FragmentDefinitionNode>,
910910
+rootValue: mixed,
911911
+operation: OperationDefinitionNode,
912-
+variableValues: { [variable: string]: mixed, ... },
912+
+variableValues: ReadOnlyObjMap<mixed>,
913913
|};
914914

915915
export type GraphQLFieldConfig<
@@ -1349,7 +1349,10 @@ export class GraphQLEnumType /* <T> */ {
13491349
return enumValue.value;
13501350
}
13511351

1352-
parseLiteral(valueNode: ValueNode, _variables: ?ObjMap<mixed>): ?any /* T */ {
1352+
parseLiteral(
1353+
valueNode: ValueNode,
1354+
_variables: ?ReadOnlyObjMap<mixed>,
1355+
): ?any /* T */ {
13531356
// Note: variables will be resolved to a value before calling this function.
13541357
if (valueNode.kind !== Kind.ENUM) {
13551358
const valueStr = print(valueNode);

src/utilities/__tests__/valueFromAST-test.js

+40-19
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { expect } from 'chai';
22
import { describe, it } from 'mocha';
33

4-
import type { ObjMap } from '../../jsutils/ObjMap';
4+
import type { ReadOnlyObjMap } from '../../jsutils/ObjMap';
55
import { invariant } from '../../jsutils/invariant';
66
import { identityFunc } from '../../jsutils/identityFunc';
77

8-
import { parseValue } from '../../language/parser';
8+
import { parseValue, Parser } from '../../language/parser';
99

1010
import type { GraphQLInputType } from '../../type/definition';
1111
import {
@@ -22,16 +22,32 @@ import {
2222
GraphQLEnumType,
2323
GraphQLInputObjectType,
2424
} from '../../type/definition';
25+
import { GraphQLSchema } from '../../type/schema';
26+
27+
import { getVariableValues } from '../../execution/values';
2528

2629
import { valueFromAST } from '../valueFromAST';
2730

2831
describe('valueFromAST', () => {
2932
function expectValueFrom(
3033
valueText: string,
3134
type: GraphQLInputType,
32-
variables?: ObjMap<mixed>,
35+
variableDefs?: string,
36+
variableValues?: ReadOnlyObjMap<mixed>,
3337
) {
3438
const ast = parseValue(valueText);
39+
let variables;
40+
if (variableValues && variableDefs !== undefined) {
41+
const parser = new Parser(variableDefs);
42+
parser.expectToken('<SOF>');
43+
const coercedVariables = getVariableValues(
44+
new GraphQLSchema({}),
45+
parser.parseVariableDefinitions(),
46+
variableValues,
47+
);
48+
invariant(coercedVariables.coerced);
49+
variables = coercedVariables.coerced;
50+
}
3551
const value = valueFromAST(ast, type, variables);
3652
return expect(value);
3753
}
@@ -239,38 +255,43 @@ describe('valueFromAST', () => {
239255
});
240256

241257
it('accepts variable values assuming already coerced', () => {
242-
expectValueFrom('$var', GraphQLBoolean, {}).to.equal(undefined);
243-
expectValueFrom('$var', GraphQLBoolean, { var: true }).to.equal(true);
244-
expectValueFrom('$var', GraphQLBoolean, { var: null }).to.equal(null);
245-
expectValueFrom('$var', nonNullBool, { var: null }).to.equal(undefined);
258+
expectValueFrom('$var', GraphQLBoolean).to.equal(undefined);
259+
expectValueFrom('$var', GraphQLBoolean, '($var: Boolean)', {
260+
var: true,
261+
}).to.equal(true);
262+
expectValueFrom('$var', GraphQLBoolean, '($var: Boolean)', {
263+
var: null,
264+
}).to.equal(null);
265+
expectValueFrom('$var', nonNullBool, '($var: Boolean)', {
266+
var: null,
267+
}).to.equal(undefined);
246268
});
247269

248270
it('asserts variables are provided as items in lists', () => {
249-
expectValueFrom('[ $foo ]', listOfBool, {}).to.deep.equal([null]);
250-
expectValueFrom('[ $foo ]', listOfNonNullBool, {}).to.equal(undefined);
251-
expectValueFrom('[ $foo ]', listOfNonNullBool, {
271+
expectValueFrom('[ $foo ]', listOfBool).to.deep.equal([null]);
272+
expectValueFrom('[ $foo ]', listOfNonNullBool).to.equal(undefined);
273+
expectValueFrom('[ $foo ]', listOfNonNullBool, '($foo: Boolean)', {
252274
foo: true,
253275
}).to.deep.equal([true]);
254276
// Note: variables are expected to have already been coerced, so we
255277
// do not expect the singleton wrapping behavior for variables.
256-
expectValueFrom('$foo', listOfNonNullBool, { foo: true }).to.equal(true);
257-
expectValueFrom('$foo', listOfNonNullBool, { foo: [true] }).to.deep.equal([
258-
true,
259-
]);
278+
expectValueFrom('$foo', listOfNonNullBool, '($foo: Boolean)', {
279+
foo: true,
280+
}).to.equal(true);
281+
expectValueFrom('$foo', listOfNonNullBool, '($foo: [Boolean])', {
282+
foo: [true],
283+
}).to.deep.equal([true]);
260284
});
261285

262286
it('omits input object fields for unprovided variables', () => {
263287
expectValueFrom(
264288
'{ int: $foo, bool: $foo, requiredBool: true }',
265289
testInputObj,
266-
{},
267290
).to.deep.equal({ int: 42, requiredBool: true });
268291

269-
expectValueFrom('{ requiredBool: $foo }', testInputObj, {}).to.equal(
270-
undefined,
271-
);
292+
expectValueFrom('{ requiredBool: $foo }', testInputObj).to.equal(undefined);
272293

273-
expectValueFrom('{ requiredBool: $foo }', testInputObj, {
294+
expectValueFrom('{ requiredBool: $foo }', testInputObj, '($foo: Boolean)', {
274295
foo: true,
275296
}).to.deep.equal({
276297
int: 42,

0 commit comments

Comments
 (0)