Skip to content

Commit 9b7029a

Browse files
JoviDeCroockyaacovCR
authored andcommitted
chore: collect fields alternative (#3)
* patch * commit progress * this should be spec compliant * another alternative * linting * update tests * add test that Matt mentioned * remove only
1 parent 513d172 commit 9b7029a

File tree

5 files changed

+243
-133
lines changed

5 files changed

+243
-133
lines changed

src/execution/__tests__/variables-test.ts

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,11 +1198,12 @@ describe('Execute: Handles inputs', () => {
11981198
fieldWithNullableStringInput(input: $value)
11991199
}
12001200
`);
1201-
expect(result).to.deep.equal({
1202-
data: {
1203-
fieldWithNullableStringInput: null,
1204-
},
1205-
});
1201+
1202+
expect(result).to.have.property('errors');
1203+
expect(result.errors).to.have.length(1);
1204+
expect(result.errors?.at(0)?.message).to.match(
1205+
/Argument "value" of required type "String!"/,
1206+
);
12061207
});
12071208

12081209
it('when the definition has a default and is provided', () => {
@@ -1237,22 +1238,42 @@ describe('Execute: Handles inputs', () => {
12371238
});
12381239
});
12391240

1240-
it('when the definition has a non-nullable default and is provided null', () => {
1241+
it('when a definition has a default, is not provided, and spreads another fragment', () => {
12411242
const result = executeQueryWithFragmentArguments(`
12421243
query {
1243-
...a(value: null)
1244+
...a
12441245
}
1245-
fragment a($value: String! = "B") on TestType {
1246-
fieldWithNullableStringInput(input: $value)
1246+
fragment a($a: String! = "B") on TestType {
1247+
...b(b: $a)
1248+
}
1249+
fragment b($b: String!) on TestType {
1250+
fieldWithNonNullableStringInput(input: $b)
12471251
}
12481252
`);
12491253
expect(result).to.deep.equal({
12501254
data: {
1251-
fieldWithNullableStringInput: 'null',
1255+
fieldWithNonNullableStringInput: '"B"',
12521256
},
12531257
});
12541258
});
12551259

1260+
it('when the definition has a non-nullable default and is provided null', () => {
1261+
const result = executeQueryWithFragmentArguments(`
1262+
query {
1263+
...a(value: null)
1264+
}
1265+
fragment a($value: String! = "B") on TestType {
1266+
fieldWithNullableStringInput(input: $value)
1267+
}
1268+
`);
1269+
1270+
expect(result).to.have.property('errors');
1271+
expect(result.errors).to.have.length(1);
1272+
expect(result.errors?.at(0)?.message).to.match(
1273+
/Argument "value" of non-null type "String!"/,
1274+
);
1275+
});
1276+
12561277
it('when the definition has no default and is not provided', () => {
12571278
const result = executeQueryWithFragmentArguments(`
12581279
query {
@@ -1303,6 +1324,27 @@ describe('Execute: Handles inputs', () => {
13031324
});
13041325
});
13051326

1327+
it ('when a fragment-variable is shadowed by an intermediate fragment-spread but defined in the operation-variables', () => {
1328+
const result = executeQueryWithFragmentArguments(`
1329+
query($x: String = "A") {
1330+
...a
1331+
}
1332+
fragment a($x: String) on TestType {
1333+
...b
1334+
}
1335+
1336+
fragment b on TestType {
1337+
fieldWithNullableStringInput(input: $x)
1338+
}
1339+
`);
1340+
expect(result).to.deep.equal({
1341+
data: {
1342+
fieldWithNullableStringInput:
1343+
'"A"',
1344+
},
1345+
});
1346+
});
1347+
13061348
it('when a fragment is used with different args', () => {
13071349
const result = executeQueryWithFragmentArguments(`
13081350
query($x: String = "Hello") {

src/execution/collectFields.ts

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@ import {
2222
} from '../type/directives.js';
2323
import type { GraphQLSchema } from '../type/schema.js';
2424

25-
import { substituteFragmentArguments } from '../utilities/substituteFragmentArguments.js';
2625
import { typeFromAST } from '../utilities/typeFromAST.js';
2726

28-
import { getDirectiveValues } from './values.js';
27+
import { getArgumentValuesFromSpread, getDirectiveValues } from './values.js';
2928

3029
export interface DeferUsage {
3130
label: string | undefined;
@@ -35,6 +34,7 @@ export interface DeferUsage {
3534
export interface FieldDetails {
3635
node: FieldNode;
3736
deferUsage: DeferUsage | undefined;
37+
fragmentVariableValues?: ObjMap<unknown> | undefined;
3838
}
3939

4040
export type FieldGroup = ReadonlyArray<FieldDetails>;
@@ -44,10 +44,10 @@ export type GroupedFieldSet = ReadonlyMap<string, FieldGroup>;
4444
interface CollectFieldsContext {
4545
schema: GraphQLSchema;
4646
fragments: ObjMap<FragmentDefinitionNode>;
47-
variableValues: { [variable: string]: unknown };
4847
operation: OperationDefinitionNode;
4948
runtimeType: GraphQLObjectType;
5049
visitedFragmentNames: Set<string>;
50+
variableValues: { [variable: string]: unknown };
5151
}
5252

5353
/**
@@ -74,8 +74,8 @@ export function collectFields(
7474
const context: CollectFieldsContext = {
7575
schema,
7676
fragments,
77-
variableValues,
7877
runtimeType,
78+
variableValues,
7979
operation,
8080
visitedFragmentNames: new Set(),
8181
};
@@ -85,6 +85,7 @@ export function collectFields(
8585
operation.selectionSet,
8686
groupedFieldSet,
8787
newDeferUsages,
88+
variableValues,
8889
);
8990
return { groupedFieldSet, newDeferUsages };
9091
}
@@ -114,8 +115,8 @@ export function collectSubfields(
114115
const context: CollectFieldsContext = {
115116
schema,
116117
fragments,
117-
variableValues,
118118
runtimeType: returnType,
119+
variableValues,
119120
operation,
120121
visitedFragmentNames: new Set(),
121122
};
@@ -130,6 +131,7 @@ export function collectSubfields(
130131
node.selectionSet,
131132
subGroupedFieldSet,
132133
newDeferUsages,
134+
undefined,
133135
fieldDetail.deferUsage,
134136
);
135137
}
@@ -141,31 +143,35 @@ export function collectSubfields(
141143
};
142144
}
143145

146+
// eslint-disable-next-line max-params
144147
function collectFieldsImpl(
145148
context: CollectFieldsContext,
146149
selectionSet: SelectionSetNode,
147150
groupedFieldSet: AccumulatorMap<string, FieldDetails>,
148151
newDeferUsages: Array<DeferUsage>,
152+
fragmentVariableValues?: ObjMap<unknown>,
149153
deferUsage?: DeferUsage,
150154
): void {
151155
const {
152156
schema,
153157
fragments,
154-
variableValues,
155158
runtimeType,
159+
variableValues,
156160
operation,
157161
visitedFragmentNames,
158162
} = context;
159163

160164
for (const selection of selectionSet.selections) {
161165
switch (selection.kind) {
162166
case Kind.FIELD: {
163-
if (!shouldIncludeNode(variableValues, selection)) {
167+
const vars = fragmentVariableValues ?? variableValues;
168+
if (!shouldIncludeNode(vars, selection)) {
164169
continue;
165170
}
166171
groupedFieldSet.add(getFieldEntryKey(selection), {
167172
node: selection,
168173
deferUsage,
174+
fragmentVariableValues: fragmentVariableValues ?? undefined,
169175
});
170176
break;
171177
}
@@ -190,6 +196,7 @@ function collectFieldsImpl(
190196
selection.selectionSet,
191197
groupedFieldSet,
192198
newDeferUsages,
199+
fragmentVariableValues,
193200
deferUsage,
194201
);
195202
} else {
@@ -199,6 +206,7 @@ function collectFieldsImpl(
199206
selection.selectionSet,
200207
groupedFieldSet,
201208
newDeferUsages,
209+
fragmentVariableValues,
202210
newDeferUsage,
203211
);
204212
}
@@ -231,27 +239,43 @@ function collectFieldsImpl(
231239
continue;
232240
}
233241

234-
const fragmentSelectionSet = substituteFragmentArguments(
235-
fragment,
236-
selection,
237-
);
242+
// We need to introduce a concept of shadowing:
243+
//
244+
// - when a fragment defines a variable that is in the parent scope but not given
245+
// in the fragment-spread we need to look at this variable as undefined and check
246+
// whether the definition has a defaultValue, if not remove it from the variableValues.
247+
// - when a fragment does not define a variable we need to copy it over from the parent
248+
// scope as that variable can still get used in spreads later on in the selectionSet.
249+
// - when a value is passed in through the fragment-spread we need to copy over the key-value
250+
// into our variable-values.
251+
const fragmentArgValues = fragment.variableDefinitions
252+
? getArgumentValuesFromSpread(
253+
selection,
254+
schema,
255+
fragment.variableDefinitions,
256+
variableValues,
257+
fragmentVariableValues,
258+
)
259+
: undefined;
238260

239261
if (!newDeferUsage) {
240262
visitedFragmentNames.add(fragmentName);
241263
collectFieldsImpl(
242264
context,
243-
fragmentSelectionSet,
265+
fragment.selectionSet,
244266
groupedFieldSet,
245267
newDeferUsages,
268+
fragmentArgValues,
246269
deferUsage,
247270
);
248271
} else {
249272
newDeferUsages.push(newDeferUsage);
250273
collectFieldsImpl(
251274
context,
252-
fragmentSelectionSet,
275+
fragment.selectionSet,
253276
groupedFieldSet,
254277
newDeferUsages,
278+
fragmentArgValues,
255279
newDeferUsage,
256280
);
257281
}

src/execution/execute.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -721,9 +721,10 @@ function executeField(
721721
// variables scope to fulfill any variable references.
722722
// TODO: find a way to memoize, in case this field is within a List type.
723723
const args = getArgumentValues(
724-
fieldDef,
725724
fieldGroup[0].node,
725+
fieldDef.args,
726726
exeContext.variableValues,
727+
fieldGroup[0].fragmentVariableValues,
727728
);
728729

729730
// The resolve function's optional third argument is a context value that
@@ -1028,7 +1029,7 @@ function getStreamUsage(
10281029
const stream = getDirectiveValues(
10291030
GraphQLStreamDirective,
10301031
fieldGroup[0].node,
1031-
exeContext.variableValues,
1032+
fieldGroup[0].fragmentVariableValues ?? exeContext.variableValues,
10321033
);
10331034

10341035
if (!stream) {
@@ -2051,7 +2052,12 @@ function executeSubscription(
20512052

20522053
// Build a JS object of arguments from the field.arguments AST, using the
20532054
// variables scope to fulfill any variable references.
2054-
const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues);
2055+
const args = getArgumentValues(
2056+
fieldNodes[0],
2057+
fieldDef.args,
2058+
variableValues,
2059+
undefined,
2060+
);
20552061

20562062
// The resolve function's optional third argument is a context value that
20572063
// is provided to every resolve function within an execution. It is commonly

0 commit comments

Comments
 (0)