Skip to content

Commit 157aada

Browse files
JoviDeCroockmjmahoneyaacovCR
authored
feat: add experimental support for parsing fragment arguments (#4015)
This is a rebase of #3847 This implements execution of Fragment Arguments, and more specifically visiting, parsing and printing of fragment-spreads with arguments and fragment definitions with variables, as described by the spec changes in graphql/graphql-spec#1081. There are a few amendments in terms of execution and keying the fragment-spreads, these are reflected in mjmahone/graphql-spec#3 The purpose is to be able to independently review all the moving parts, the stacked PR's will contain mentions of open feedback that was present at the time. - [execution changes](JoviDeCroock#2) - [TypeInfo & validation changes](JoviDeCroock#4) - [validation changes in isolation](JoviDeCroock#5) CC @mjmahone the original author --------- Co-authored-by: mjmahone <[email protected]> Co-authored-by: Yaacov Rydzinski <[email protected]>
1 parent 426b017 commit 157aada

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2236
-247
lines changed

src/execution/__tests__/variables-test.ts

+400-3
Large diffs are not rendered by default.

src/execution/collectFields.ts

+64-14
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,39 @@ import type { GraphQLSchema } from '../type/schema.js';
2424

2525
import { typeFromAST } from '../utilities/typeFromAST.js';
2626

27-
import { getDirectiveValues } from './values.js';
27+
import type { GraphQLVariableSignature } from './getVariableSignature.js';
28+
import { experimentalGetArgumentValues, getDirectiveValues } from './values.js';
2829

2930
export interface DeferUsage {
3031
label: string | undefined;
3132
parentDeferUsage: DeferUsage | undefined;
3233
}
3334

35+
export interface FragmentVariables {
36+
signatures: ObjMap<GraphQLVariableSignature>;
37+
values: ObjMap<unknown>;
38+
}
39+
3440
export interface FieldDetails {
3541
node: FieldNode;
36-
deferUsage: DeferUsage | undefined;
42+
deferUsage?: DeferUsage | undefined;
43+
fragmentVariables?: FragmentVariables | undefined;
3744
}
3845

3946
export type FieldGroup = ReadonlyArray<FieldDetails>;
4047

4148
export type GroupedFieldSet = ReadonlyMap<string, FieldGroup>;
4249

50+
export interface FragmentDetails {
51+
definition: FragmentDefinitionNode;
52+
variableSignatures?: ObjMap<GraphQLVariableSignature> | undefined;
53+
}
54+
4355
interface CollectFieldsContext {
4456
schema: GraphQLSchema;
45-
fragments: ObjMap<FragmentDefinitionNode>;
57+
fragments: ObjMap<FragmentDetails>;
4658
variableValues: { [variable: string]: unknown };
59+
fragmentVariableValues?: FragmentVariables;
4760
operation: OperationDefinitionNode;
4861
runtimeType: GraphQLObjectType;
4962
visitedFragmentNames: Set<string>;
@@ -60,7 +73,7 @@ interface CollectFieldsContext {
6073
*/
6174
export function collectFields(
6275
schema: GraphQLSchema,
63-
fragments: ObjMap<FragmentDefinitionNode>,
76+
fragments: ObjMap<FragmentDetails>,
6477
variableValues: { [variable: string]: unknown },
6578
runtimeType: GraphQLObjectType,
6679
operation: OperationDefinitionNode,
@@ -101,7 +114,7 @@ export function collectFields(
101114
// eslint-disable-next-line max-params
102115
export function collectSubfields(
103116
schema: GraphQLSchema,
104-
fragments: ObjMap<FragmentDefinitionNode>,
117+
fragments: ObjMap<FragmentDetails>,
105118
variableValues: { [variable: string]: unknown },
106119
operation: OperationDefinitionNode,
107120
returnType: GraphQLObjectType,
@@ -140,12 +153,14 @@ export function collectSubfields(
140153
};
141154
}
142155

156+
// eslint-disable-next-line max-params
143157
function collectFieldsImpl(
144158
context: CollectFieldsContext,
145159
selectionSet: SelectionSetNode,
146160
groupedFieldSet: AccumulatorMap<string, FieldDetails>,
147161
newDeferUsages: Array<DeferUsage>,
148162
deferUsage?: DeferUsage,
163+
fragmentVariables?: FragmentVariables,
149164
): void {
150165
const {
151166
schema,
@@ -159,18 +174,19 @@ function collectFieldsImpl(
159174
for (const selection of selectionSet.selections) {
160175
switch (selection.kind) {
161176
case Kind.FIELD: {
162-
if (!shouldIncludeNode(variableValues, selection)) {
177+
if (!shouldIncludeNode(selection, variableValues, fragmentVariables)) {
163178
continue;
164179
}
165180
groupedFieldSet.add(getFieldEntryKey(selection), {
166181
node: selection,
167182
deferUsage,
183+
fragmentVariables,
168184
});
169185
break;
170186
}
171187
case Kind.INLINE_FRAGMENT: {
172188
if (
173-
!shouldIncludeNode(variableValues, selection) ||
189+
!shouldIncludeNode(selection, variableValues, fragmentVariables) ||
174190
!doesFragmentConditionMatch(schema, selection, runtimeType)
175191
) {
176192
continue;
@@ -179,6 +195,7 @@ function collectFieldsImpl(
179195
const newDeferUsage = getDeferUsage(
180196
operation,
181197
variableValues,
198+
fragmentVariables,
182199
selection,
183200
deferUsage,
184201
);
@@ -190,6 +207,7 @@ function collectFieldsImpl(
190207
groupedFieldSet,
191208
newDeferUsages,
192209
deferUsage,
210+
fragmentVariables,
193211
);
194212
} else {
195213
newDeferUsages.push(newDeferUsage);
@@ -199,6 +217,7 @@ function collectFieldsImpl(
199217
groupedFieldSet,
200218
newDeferUsages,
201219
newDeferUsage,
220+
fragmentVariables,
202221
);
203222
}
204223

@@ -210,42 +229,60 @@ function collectFieldsImpl(
210229
const newDeferUsage = getDeferUsage(
211230
operation,
212231
variableValues,
232+
fragmentVariables,
213233
selection,
214234
deferUsage,
215235
);
216236

217237
if (
218238
!newDeferUsage &&
219239
(visitedFragmentNames.has(fragName) ||
220-
!shouldIncludeNode(variableValues, selection))
240+
!shouldIncludeNode(selection, variableValues, fragmentVariables))
221241
) {
222242
continue;
223243
}
224244

225245
const fragment = fragments[fragName];
226246
if (
227247
fragment == null ||
228-
!doesFragmentConditionMatch(schema, fragment, runtimeType)
248+
!doesFragmentConditionMatch(schema, fragment.definition, runtimeType)
229249
) {
230250
continue;
231251
}
252+
253+
const fragmentVariableSignatures = fragment.variableSignatures;
254+
let newFragmentVariables: FragmentVariables | undefined;
255+
if (fragmentVariableSignatures) {
256+
newFragmentVariables = {
257+
signatures: fragmentVariableSignatures,
258+
values: experimentalGetArgumentValues(
259+
selection,
260+
Object.values(fragmentVariableSignatures),
261+
variableValues,
262+
fragmentVariables,
263+
),
264+
};
265+
}
266+
232267
if (!newDeferUsage) {
233268
visitedFragmentNames.add(fragName);
234269
collectFieldsImpl(
235270
context,
236-
fragment.selectionSet,
271+
fragment.definition.selectionSet,
237272
groupedFieldSet,
238273
newDeferUsages,
239274
deferUsage,
275+
newFragmentVariables,
240276
);
241277
} else {
242278
newDeferUsages.push(newDeferUsage);
243279
collectFieldsImpl(
244280
context,
245-
fragment.selectionSet,
281+
fragment.definition.selectionSet,
246282
groupedFieldSet,
247283
newDeferUsages,
248284
newDeferUsage,
285+
newFragmentVariables,
249286
);
250287
}
251288
break;
@@ -262,10 +299,16 @@ function collectFieldsImpl(
262299
function getDeferUsage(
263300
operation: OperationDefinitionNode,
264301
variableValues: { [variable: string]: unknown },
302+
fragmentVariables: FragmentVariables | undefined,
265303
node: FragmentSpreadNode | InlineFragmentNode,
266304
parentDeferUsage: DeferUsage | undefined,
267305
): DeferUsage | undefined {
268-
const defer = getDirectiveValues(GraphQLDeferDirective, node, variableValues);
306+
const defer = getDirectiveValues(
307+
GraphQLDeferDirective,
308+
node,
309+
variableValues,
310+
fragmentVariables,
311+
);
269312

270313
if (!defer) {
271314
return;
@@ -291,10 +334,16 @@ function getDeferUsage(
291334
* directives, where `@skip` has higher precedence than `@include`.
292335
*/
293336
function shouldIncludeNode(
294-
variableValues: { [variable: string]: unknown },
295337
node: FragmentSpreadNode | FieldNode | InlineFragmentNode,
338+
variableValues: { [variable: string]: unknown },
339+
fragmentVariables: FragmentVariables | undefined,
296340
): boolean {
297-
const skip = getDirectiveValues(GraphQLSkipDirective, node, variableValues);
341+
const skip = getDirectiveValues(
342+
GraphQLSkipDirective,
343+
node,
344+
variableValues,
345+
fragmentVariables,
346+
);
298347
if (skip?.if === true) {
299348
return false;
300349
}
@@ -303,6 +352,7 @@ function shouldIncludeNode(
303352
GraphQLIncludeDirective,
304353
node,
305354
variableValues,
355+
fragmentVariables,
306356
);
307357
if (include?.if === false) {
308358
return false;

src/execution/execute.ts

+26-8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { isAsyncIterable } from '../jsutils/isAsyncIterable.js';
55
import { isIterableObject } from '../jsutils/isIterableObject.js';
66
import { isObjectLike } from '../jsutils/isObjectLike.js';
77
import { isPromise } from '../jsutils/isPromise.js';
8+
import { mapValue } from '../jsutils/mapValue.js';
89
import type { Maybe } from '../jsutils/Maybe.js';
910
import { memoize3 } from '../jsutils/memoize3.js';
1011
import type { ObjMap } from '../jsutils/ObjMap.js';
@@ -20,7 +21,6 @@ import { locatedError } from '../error/locatedError.js';
2021
import type {
2122
DocumentNode,
2223
FieldNode,
23-
FragmentDefinitionNode,
2424
OperationDefinitionNode,
2525
} from '../language/ast.js';
2626
import { OperationTypeNode } from '../language/ast.js';
@@ -53,12 +53,14 @@ import { buildExecutionPlan } from './buildExecutionPlan.js';
5353
import type {
5454
DeferUsage,
5555
FieldGroup,
56+
FragmentDetails,
5657
GroupedFieldSet,
5758
} from './collectFields.js';
5859
import {
5960
collectFields,
6061
collectSubfields as _collectSubfields,
6162
} from './collectFields.js';
63+
import { getVariableSignature } from './getVariableSignature.js';
6264
import { buildIncrementalResponse } from './IncrementalPublisher.js';
6365
import { mapAsyncIterable } from './mapAsyncIterable.js';
6466
import type {
@@ -74,6 +76,7 @@ import type {
7476
} from './types.js';
7577
import { DeferredFragmentRecord } from './types.js';
7678
import {
79+
experimentalGetArgumentValues,
7780
getArgumentValues,
7881
getDirectiveValues,
7982
getVariableValues,
@@ -132,7 +135,7 @@ const collectSubfields = memoize3(
132135
*/
133136
export interface ExecutionContext {
134137
schema: GraphQLSchema;
135-
fragments: ObjMap<FragmentDefinitionNode>;
138+
fragments: ObjMap<FragmentDetails>;
136139
rootValue: unknown;
137140
contextValue: unknown;
138141
operation: OperationDefinitionNode;
@@ -462,7 +465,7 @@ export function buildExecutionContext(
462465
assertValidSchema(schema);
463466

464467
let operation: OperationDefinitionNode | undefined;
465-
const fragments: ObjMap<FragmentDefinitionNode> = Object.create(null);
468+
const fragments: ObjMap<FragmentDetails> = Object.create(null);
466469
for (const definition of document.definitions) {
467470
switch (definition.kind) {
468471
case Kind.OPERATION_DEFINITION:
@@ -479,9 +482,18 @@ export function buildExecutionContext(
479482
operation = definition;
480483
}
481484
break;
482-
case Kind.FRAGMENT_DEFINITION:
483-
fragments[definition.name.value] = definition;
485+
case Kind.FRAGMENT_DEFINITION: {
486+
let variableSignatures;
487+
if (definition.variableDefinitions) {
488+
variableSignatures = Object.create(null);
489+
for (const varDef of definition.variableDefinitions) {
490+
const signature = getVariableSignature(schema, varDef);
491+
variableSignatures[signature.name] = signature;
492+
}
493+
}
494+
fragments[definition.name.value] = { definition, variableSignatures };
484495
break;
496+
}
485497
default:
486498
// ignore non-executable definitions
487499
}
@@ -737,10 +749,11 @@ function executeField(
737749
// Build a JS object of arguments from the field.arguments AST, using the
738750
// variables scope to fulfill any variable references.
739751
// TODO: find a way to memoize, in case this field is within a List type.
740-
const args = getArgumentValues(
741-
fieldDef,
752+
const args = experimentalGetArgumentValues(
742753
fieldGroup[0].node,
754+
fieldDef.args,
743755
exeContext.variableValues,
756+
fieldGroup[0].fragmentVariables,
744757
);
745758

746759
// The resolve function's optional third argument is a context value that
@@ -823,7 +836,10 @@ export function buildResolveInfo(
823836
parentType,
824837
path,
825838
schema: exeContext.schema,
826-
fragments: exeContext.fragments,
839+
fragments: mapValue(
840+
exeContext.fragments,
841+
(fragment) => fragment.definition,
842+
),
827843
rootValue: exeContext.rootValue,
828844
operation: exeContext.operation,
829845
variableValues: exeContext.variableValues,
@@ -1046,6 +1062,7 @@ function getStreamUsage(
10461062
GraphQLStreamDirective,
10471063
fieldGroup[0].node,
10481064
exeContext.variableValues,
1065+
fieldGroup[0].fragmentVariables,
10491066
);
10501067

10511068
if (!stream) {
@@ -1074,6 +1091,7 @@ function getStreamUsage(
10741091
const streamedFieldGroup: FieldGroup = fieldGroup.map((fieldDetails) => ({
10751092
node: fieldDetails.node,
10761093
deferUsage: undefined,
1094+
fragmentVariables: fieldDetails.fragmentVariables,
10771095
}));
10781096

10791097
const streamUsage = {

src/execution/getVariableSignature.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { GraphQLError } from '../error/GraphQLError.js';
2+
3+
import type { VariableDefinitionNode } from '../language/ast.js';
4+
import { print } from '../language/printer.js';
5+
6+
import { isInputType } from '../type/definition.js';
7+
import type { GraphQLInputType, GraphQLSchema } from '../type/index.js';
8+
9+
import { typeFromAST } from '../utilities/typeFromAST.js';
10+
import { valueFromAST } from '../utilities/valueFromAST.js';
11+
12+
/**
13+
* A GraphQLVariableSignature is required to coerce a variable value.
14+
*
15+
* Designed to have comparable interface to GraphQLArgument so that
16+
* getArgumentValues() can be reused for fragment arguments.
17+
* */
18+
export interface GraphQLVariableSignature {
19+
name: string;
20+
type: GraphQLInputType;
21+
defaultValue: unknown;
22+
}
23+
24+
export function getVariableSignature(
25+
schema: GraphQLSchema,
26+
varDefNode: VariableDefinitionNode,
27+
): GraphQLVariableSignature | GraphQLError {
28+
const varName = varDefNode.variable.name.value;
29+
const varType = typeFromAST(schema, varDefNode.type);
30+
31+
if (!isInputType(varType)) {
32+
// Must use input types for variables. This should be caught during
33+
// validation, however is checked again here for safety.
34+
const varTypeStr = print(varDefNode.type);
35+
return new GraphQLError(
36+
`Variable "$${varName}" expected value of type "${varTypeStr}" which cannot be used as an input type.`,
37+
{ nodes: varDefNode.type },
38+
);
39+
}
40+
41+
return {
42+
name: varName,
43+
type: varType,
44+
defaultValue: valueFromAST(varDefNode.defaultValue, varType),
45+
};
46+
}

0 commit comments

Comments
 (0)