Skip to content

Commit 1158cd4

Browse files
authored
Execute serially supporting sync execution. (#1198)
Refactors executeFieldsSerially to return MaybePromise Fixes #1195
1 parent 358df97 commit 1158cd4

File tree

5 files changed

+137
-58
lines changed

5 files changed

+137
-58
lines changed

src/execution/__tests__/sync-test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,17 @@ describe('Execute: synchronously when possible', () => {
3131
},
3232
},
3333
}),
34+
mutation: new GraphQLObjectType({
35+
name: 'Mutation',
36+
fields: {
37+
syncMutationField: {
38+
type: GraphQLString,
39+
resolve(rootValue) {
40+
return rootValue;
41+
},
42+
},
43+
},
44+
}),
3445
});
3546

3647
it('does not return a Promise for initial errors', () => {
@@ -61,6 +72,16 @@ describe('Execute: synchronously when possible', () => {
6172
expect(result).to.deep.equal({ data: { syncField: 'rootValue' } });
6273
});
6374

75+
it('does not return a Promise if mutation fields are all synchronous', () => {
76+
const doc = 'mutation Example { syncMutationField }';
77+
const result = execute({
78+
schema,
79+
document: parse(doc),
80+
rootValue: 'rootValue',
81+
});
82+
expect(result).to.deep.equal({ data: { syncMutationField: 'rootValue' } });
83+
});
84+
6485
it('returns a Promise if any field is asynchronous', async () => {
6586
const doc = 'query Example { syncField, asyncField }';
6687
const result = execute({

src/execution/execute.js

Lines changed: 29 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99

1010
import { forEach, isCollection } from 'iterall';
1111
import { GraphQLError, locatedError } from '../error';
12+
import getPromise from '../jsutils/getPromise';
1213
import invariant from '../jsutils/invariant';
1314
import isInvalid from '../jsutils/isInvalid';
1415
import isNullish from '../jsutils/isNullish';
1516
import memoize3 from '../jsutils/memoize3';
17+
import promiseForObject from '../jsutils/promiseForObject';
18+
import promiseReduce from '../jsutils/promiseReduce';
1619
import type { ObjMap } from '../jsutils/ObjMap';
1720
import type { MaybePromise } from '../jsutils/MaybePromise';
1821

@@ -465,33 +468,33 @@ function executeFieldsSerially(
465468
sourceValue: mixed,
466469
path: ResponsePath | void,
467470
fields: ObjMap<Array<FieldNode>>,
468-
): Promise<ObjMap<mixed>> {
469-
return Object.keys(fields).reduce(
470-
(prevPromise, responseName) =>
471-
prevPromise.then(results => {
472-
const fieldNodes = fields[responseName];
473-
const fieldPath = addPath(path, responseName);
474-
const result = resolveField(
475-
exeContext,
476-
parentType,
477-
sourceValue,
478-
fieldNodes,
479-
fieldPath,
480-
);
481-
if (result === undefined) {
482-
return results;
483-
}
484-
const promise = getPromise(result);
485-
if (promise) {
486-
return promise.then(resolvedResult => {
487-
results[responseName] = resolvedResult;
488-
return results;
489-
});
490-
}
491-
results[responseName] = result;
471+
): MaybePromise<ObjMap<mixed>> {
472+
return promiseReduce(
473+
Object.keys(fields),
474+
(results, responseName) => {
475+
const fieldNodes = fields[responseName];
476+
const fieldPath = addPath(path, responseName);
477+
const result = resolveField(
478+
exeContext,
479+
parentType,
480+
sourceValue,
481+
fieldNodes,
482+
fieldPath,
483+
);
484+
if (result === undefined) {
492485
return results;
493-
}),
494-
Promise.resolve({}),
486+
}
487+
const promise = getPromise(result);
488+
if (promise) {
489+
return promise.then(resolvedResult => {
490+
results[responseName] = resolvedResult;
491+
return results;
492+
});
493+
}
494+
results[responseName] = result;
495+
return results;
496+
},
497+
Object.create(null),
495498
);
496499
}
497500

@@ -662,24 +665,6 @@ function doesFragmentConditionMatch(
662665
return false;
663666
}
664667

665-
/**
666-
* This function transforms a JS object `ObjMap<Promise<T>>` into
667-
* a `Promise<ObjMap<T>>`
668-
*
669-
* This is akin to bluebird's `Promise.props`, but implemented only using
670-
* `Promise.all` so it will work with any implementation of ES6 promises.
671-
*/
672-
function promiseForObject<T>(object: ObjMap<Promise<T>>): Promise<ObjMap<T>> {
673-
const keys = Object.keys(object);
674-
const valuesAndPromises = keys.map(name => object[name]);
675-
return Promise.all(valuesAndPromises).then(values =>
676-
values.reduce((resolvedObject, value, i) => {
677-
resolvedObject[keys[i]] = value;
678-
return resolvedObject;
679-
}, Object.create(null)),
680-
);
681-
}
682-
683668
/**
684669
* Implements the logic to compute the key of a given field's entry
685670
*/
@@ -1346,20 +1331,6 @@ export const defaultFieldResolver: GraphQLFieldResolver<any, *> = function(
13461331
}
13471332
};
13481333

1349-
/**
1350-
* Only returns the value if it acts like a Promise, i.e. has a "then" function,
1351-
* otherwise returns void.
1352-
*/
1353-
function getPromise<T>(value: Promise<T> | mixed): Promise<T> | void {
1354-
if (
1355-
typeof value === 'object' &&
1356-
value !== null &&
1357-
typeof value.then === 'function'
1358-
) {
1359-
return (value: any);
1360-
}
1361-
}
1362-
13631334
/**
13641335
* This method looks up the field on the given type defintion.
13651336
* It has special casing for the two introspection fields, __schema

src/jsutils/getPromise.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
/**
11+
* Only returns the value if it acts like a Promise, i.e. has a "then" function,
12+
* otherwise returns void.
13+
*/
14+
export default function getPromise<T>(
15+
value: Promise<T> | mixed,
16+
): Promise<T> | void {
17+
if (
18+
typeof value === 'object' &&
19+
value !== null &&
20+
typeof value.then === 'function'
21+
) {
22+
return (value: any);
23+
}
24+
}

src/jsutils/promiseForObject.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type { ObjMap } from './ObjMap';
11+
12+
/**
13+
* This function transforms a JS object `ObjMap<Promise<T>>` into
14+
* a `Promise<ObjMap<T>>`
15+
*
16+
* This is akin to bluebird's `Promise.props`, but implemented only using
17+
* `Promise.all` so it will work with any implementation of ES6 promises.
18+
*/
19+
export default function promiseForObject<T>(
20+
object: ObjMap<Promise<T>>,
21+
): Promise<ObjMap<T>> {
22+
const keys = Object.keys(object);
23+
const valuesAndPromises = keys.map(name => object[name]);
24+
return Promise.all(valuesAndPromises).then(values =>
25+
values.reduce((resolvedObject, value, i) => {
26+
resolvedObject[keys[i]] = value;
27+
return resolvedObject;
28+
}, Object.create(null)),
29+
);
30+
}

src/jsutils/promiseReduce.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import getPromise from './getPromise';
11+
import type { MaybePromise } from './MaybePromise';
12+
13+
/**
14+
* Similar to Array.prototype.reduce(), however the reducing callback may return
15+
* a Promise, in which case reduction will continue after each promise resolves.
16+
*
17+
* If the callback does not return a Promise, then this function will also not
18+
* return a Promise.
19+
*/
20+
export default function promiseReduce<T, U>(
21+
values: $ReadOnlyArray<T>,
22+
callback: (U, T) => MaybePromise<U>,
23+
initialValue: MaybePromise<U>,
24+
): MaybePromise<U> {
25+
return values.reduce((previous, value) => {
26+
const promise = getPromise(previous);
27+
if (promise) {
28+
return promise.then(resolved => callback(resolved, value));
29+
}
30+
// Previous is not Promise<U>, so it is U.
31+
return callback((previous: any), value);
32+
}, initialValue);
33+
}

0 commit comments

Comments
 (0)