Skip to content

Commit 9615e64

Browse files
committed
use { data, errors } so no mutating exeContext
1 parent 86d33b4 commit 9615e64

File tree

1 file changed

+92
-47
lines changed

1 file changed

+92
-47
lines changed

src/execution/execute.js

+92-47
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ export type ExecutionContext = {
9999
operation: OperationDefinitionNode,
100100
variableValues: { [variable: string]: mixed },
101101
fieldResolver: GraphQLFieldResolver<any, any>,
102-
errors: Array<GraphQLError>,
103102
};
104103

105104
/**
@@ -113,6 +112,23 @@ export type ExecutionResult = {
113112
data?: ObjMap<mixed>,
114113
};
115114

115+
/**
116+
* A partial result of GraphQL execution. `data` and `errors` as above.
117+
* "NP" = "Not Promise".
118+
*/
119+
export type ExecutionPartialResultNP<T> = {
120+
errors?: $ReadOnlyArray<GraphQLError>,
121+
data?: T | null,
122+
};
123+
124+
/**
125+
* A partial result of GraphQL execution. `data` and `errors` as above.
126+
* This version might be a promise.
127+
*/
128+
export type ExecutionPartialResult<T> = MaybePromise<
129+
ExecutionPartialResultNP<T>,
130+
>;
131+
116132
export type ExecutionArgs = {|
117133
schema: GraphQLSchema,
118134
document: DocumentNode,
@@ -218,24 +234,21 @@ function executeImpl(
218234
// be executed. An execution which encounters errors will still result in a
219235
// resolved Promise.
220236
const data = executeOperation(context, context.operation, rootValue);
221-
return buildResponse(context, data);
237+
return buildResponse(data);
222238
}
223239

224240
/**
225-
* Given a completed execution context and data, build the { errors, data }
226-
* response defined by the "Response" section of the GraphQL specification.
241+
* Strip out `errors` if empty.
227242
*/
228-
function buildResponse(
229-
context: ExecutionContext,
230-
data: MaybePromise<ObjMap<mixed> | null>,
231-
) {
232-
const promise = getPromise(data);
243+
function buildResponse(result: ExecutionPartialResult<mixed>) {
244+
const promise = getPromise(result);
233245
if (promise) {
234-
return promise.then(resolved => buildResponse(context, resolved));
246+
return promise.then(buildResponse);
235247
}
236-
return context.errors.length === 0
237-
? { data }
238-
: { errors: context.errors, data };
248+
if (result.data && (!result.errors || !result.errors.length)) {
249+
return { data: result.data };
250+
}
251+
return result;
239252
}
240253

241254
/**
@@ -370,7 +383,6 @@ export function buildExecutionContext(
370383
operation,
371384
variableValues,
372385
fieldResolver: fieldResolver || defaultFieldResolver,
373-
errors,
374386
};
375387
}
376388

@@ -381,7 +393,7 @@ function executeOperation(
381393
exeContext: ExecutionContext,
382394
operation: OperationDefinitionNode,
383395
rootValue: mixed,
384-
): MaybePromise<ObjMap<mixed> | null> {
396+
): ExecutionPartialResult<mixed> {
385397
const type = getOperationRootType(exeContext.schema, operation);
386398
const fields = collectFields(
387399
exeContext,
@@ -405,15 +417,14 @@ function executeOperation(
405417
: executeFields(exeContext, type, rootValue, path, fields);
406418
const promise = getPromise(result);
407419
if (promise) {
408-
return promise.then(undefined, error => {
409-
exeContext.errors.push(error);
410-
return Promise.resolve(null);
411-
});
420+
return promise.then(undefined, error => ({
421+
data: null,
422+
errors: [error],
423+
}));
412424
}
413425
return result;
414426
} catch (error) {
415-
exeContext.errors.push(error);
416-
return null;
427+
return { data: null, errors: [error] };
417428
}
418429
}
419430

@@ -468,8 +479,8 @@ function executeFieldsSerially(
468479
sourceValue: mixed,
469480
path: ResponsePath | void,
470481
fields: ObjMap<Array<FieldNode>>,
471-
): MaybePromise<ObjMap<mixed>> {
472-
return promiseReduce(
482+
): ExecutionPartialResult<mixed> {
483+
const finalResult = promiseReduce(
473484
Object.keys(fields),
474485
(results, responseName) => {
475486
const fieldNodes = fields[responseName];
@@ -481,7 +492,7 @@ function executeFieldsSerially(
481492
fieldNodes,
482493
fieldPath,
483494
);
484-
if (result === undefined) {
495+
if (result === null) {
485496
return results;
486497
}
487498
const promise = getPromise(result);
@@ -496,6 +507,8 @@ function executeFieldsSerially(
496507
},
497508
Object.create(null),
498509
);
510+
const promise = getPromise(finalResult);
511+
return promise ? promise.then(mergeEPRs) : mergeEPRs((finalResult: any));
499512
}
500513

501514
/**
@@ -508,7 +521,7 @@ function executeFields(
508521
sourceValue: mixed,
509522
path: ResponsePath | void,
510523
fields: ObjMap<Array<FieldNode>>,
511-
): MaybePromise<ObjMap<mixed>> {
524+
): ExecutionPartialResult<mixed> {
512525
let containsPromise = false;
513526

514527
const finalResults = Object.keys(fields).reduce((results, responseName) => {
@@ -521,7 +534,7 @@ function executeFields(
521534
fieldNodes,
522535
fieldPath,
523536
);
524-
if (result === undefined) {
537+
if (result === null) {
525538
return results;
526539
}
527540
results[responseName] = result;
@@ -533,14 +546,31 @@ function executeFields(
533546

534547
// If there are no promises, we can just return the object
535548
if (!containsPromise) {
536-
return finalResults;
549+
return mergeEPRs(finalResults);
537550
}
538551

539552
// Otherwise, results is a map from field name to the result
540553
// of resolving that field, which is possibly a promise. Return
541554
// a promise that will return this same map, but with any
542555
// promises replaced with the values they resolved to.
543-
return promiseForObject(finalResults);
556+
return promiseForObject(finalResults).then(mergeEPRs);
557+
}
558+
559+
function mergeEPRs(
560+
eprMap: ObjMap<ExecutionPartialResultNP<mixed>>,
561+
): ExecutionPartialResultNP<mixed> {
562+
const keys = Object.keys(eprMap);
563+
const values = keys.map(name => eprMap[name]);
564+
return values.reduce(
565+
(merged, value, i) => {
566+
merged.data[keys[i]] = value.data;
567+
if (value.errors && value.errors.length) {
568+
merged.errors.push(...value.errors);
569+
}
570+
return merged;
571+
},
572+
{ data: Object.create(null), errors: [] },
573+
);
544574
}
545575

546576
/**
@@ -684,13 +714,13 @@ function resolveField(
684714
source: mixed,
685715
fieldNodes: $ReadOnlyArray<FieldNode>,
686716
path: ResponsePath,
687-
): mixed {
717+
): ExecutionPartialResult<mixed> | null {
688718
const fieldNode = fieldNodes[0];
689719
const fieldName = fieldNode.name.value;
690720

691721
const fieldDef = getFieldDef(exeContext.schema, parentType, fieldName);
692722
if (!fieldDef) {
693-
return;
723+
return null;
694724
}
695725

696726
const resolveFn = fieldDef.resolve || exeContext.fieldResolver;
@@ -795,7 +825,7 @@ function completeValueCatchingError(
795825
info: GraphQLResolveInfo,
796826
path: ResponsePath,
797827
result: mixed,
798-
): mixed {
828+
): ExecutionPartialResult<mixed> {
799829
// If the field type is non-nullable, then it is resolved without any
800830
// protection from errors, however it still properly locates the error.
801831
if (isNonNullType(returnType)) {
@@ -826,17 +856,16 @@ function completeValueCatchingError(
826856
// the rejection error and resolve to null.
827857
// Note: we don't rely on a `catch` method, but we do expect "thenable"
828858
// to take a second callback for the error case.
829-
return promise.then(undefined, error => {
830-
exeContext.errors.push(error);
831-
return Promise.resolve(null);
832-
});
859+
return promise.then(undefined, error => ({
860+
data: null,
861+
errors: [error],
862+
}));
833863
}
834864
return completed;
835865
} catch (error) {
836866
// If `completeValueWithLocatedError` returned abruptly (threw an error),
837867
// log the error and return null.
838-
exeContext.errors.push(error);
839-
return null;
868+
return { data: null, errors: [error] };
840869
}
841870
}
842871

@@ -849,7 +878,7 @@ function completeValueWithLocatedError(
849878
info: GraphQLResolveInfo,
850879
path: ResponsePath,
851880
result: mixed,
852-
): mixed {
881+
): ExecutionPartialResult<mixed> {
853882
try {
854883
const completed = completeValue(
855884
exeContext,
@@ -909,7 +938,7 @@ function completeValue(
909938
info: GraphQLResolveInfo,
910939
path: ResponsePath,
911940
result: mixed,
912-
): mixed {
941+
): ExecutionPartialResult<mixed> {
913942
// If result is a Promise, apply-lift over completeValue.
914943
const promise = getPromise(result);
915944
if (promise) {
@@ -934,7 +963,7 @@ function completeValue(
934963
path,
935964
result,
936965
);
937-
if (completed === null) {
966+
if (completed.data === null) {
938967
throw new Error(
939968
`Cannot return null for non-nullable field ${info.parentType.name}.${
940969
info.fieldName
@@ -946,7 +975,7 @@ function completeValue(
946975

947976
// If result value is null-ish (null, undefined, or NaN) then return null.
948977
if (isNullish(result)) {
949-
return null;
978+
return { data: null };
950979
}
951980

952981
// If field type is List, complete each item in the list with the inner type
@@ -964,7 +993,7 @@ function completeValue(
964993
// If field type is a leaf type, Scalar or Enum, serialize to a valid value,
965994
// returning null if serialization is not possible.
966995
if (isLeafType(returnType)) {
967-
return completeLeafValue(returnType, result);
996+
return { data: completeLeafValue(returnType, result) };
968997
}
969998

970999
// If field type is an abstract type, Interface or Union, determine the
@@ -1012,7 +1041,7 @@ function completeListValue(
10121041
info: GraphQLResolveInfo,
10131042
path: ResponsePath,
10141043
result: mixed,
1015-
): mixed {
1044+
): ExecutionPartialResult<mixed> {
10161045
invariant(
10171046
isCollection(result),
10181047
`Expected Iterable, but did not find one for field ${
@@ -1044,7 +1073,23 @@ function completeListValue(
10441073
completedResults.push(completedItem);
10451074
});
10461075

1047-
return containsPromise ? Promise.all(completedResults) : completedResults;
1076+
return containsPromise
1077+
? Promise.all(completedResults).then(flattenEPRs)
1078+
: flattenEPRs(completedResults);
1079+
}
1080+
1081+
function flattenEPRs(
1082+
eprs: $ReadOnlyArray<ExecutionPartialResultNP<mixed>>,
1083+
): ExecutionPartialResultNP<mixed> {
1084+
const errors = [];
1085+
const data = [];
1086+
forEach((eprs: any), item => {
1087+
data.push(item.data);
1088+
if (item.errors && item.errors.length) {
1089+
errors.push(...item.errors);
1090+
}
1091+
});
1092+
return { data, errors };
10481093
}
10491094

10501095
/**
@@ -1074,7 +1119,7 @@ function completeAbstractValue(
10741119
info: GraphQLResolveInfo,
10751120
path: ResponsePath,
10761121
result: mixed,
1077-
): mixed {
1122+
): ExecutionPartialResult<mixed> {
10781123
const runtimeType = returnType.resolveType
10791124
? returnType.resolveType(result, exeContext.contextValue, info)
10801125
: defaultResolveTypeFn(result, exeContext.contextValue, info, returnType);
@@ -1163,7 +1208,7 @@ function completeObjectValue(
11631208
info: GraphQLResolveInfo,
11641209
path: ResponsePath,
11651210
result: mixed,
1166-
): mixed {
1211+
): ExecutionPartialResult<mixed> {
11671212
// If there is an isTypeOf predicate function, call it with the
11681213
// current result. If isTypeOf returns false, then raise an error rather
11691214
// than continuing execution.
@@ -1220,7 +1265,7 @@ function collectAndExecuteSubfields(
12201265
info: GraphQLResolveInfo,
12211266
path: ResponsePath,
12221267
result: mixed,
1223-
): mixed {
1268+
): ExecutionPartialResult<mixed> {
12241269
// Collect sub-fields to execute to complete this value.
12251270
const subFieldNodes = collectSubfields(exeContext, returnType, fieldNodes);
12261271
return executeFields(exeContext, returnType, result, path, subFieldNodes);

0 commit comments

Comments
 (0)