Skip to content

Commit 9cab1b5

Browse files
committed
refactor: extract request normalization out of buildExecutionContext
existing flow hid the execution request spec portion within the execution context object setup -- the algorithm steps belong within the function itself.
1 parent 1895122 commit 9cab1b5

File tree

2 files changed

+139
-54
lines changed

2 files changed

+139
-54
lines changed

src/execution/__tests__/subscribe-test.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,24 @@ import { describe, it } from 'mocha';
44
import { expectJSON } from '../../__testUtils__/expectJSON';
55
import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick';
66

7+
import type { ObjMap } from '../../jsutils/ObjMap';
78
import { invariant } from '../../jsutils/invariant';
89
import { isAsyncIterable } from '../../jsutils/isAsyncIterable';
910

11+
import type {
12+
FragmentDefinitionNode,
13+
OperationDefinitionNode,
14+
} from '../../language/ast';
1015
import { parse } from '../../language/parser';
1116

1217
import { GraphQLSchema } from '../../type/schema';
1318
import { GraphQLList, GraphQLObjectType } from '../../type/definition';
1419
import { GraphQLInt, GraphQLString, GraphQLBoolean } from '../../type/scalars';
1520

16-
import type { ExecutionContext } from '../execute';
1721
import {
1822
buildExecutionContext,
1923
createSourceEventStream,
24+
getNormalizedExecutableDefinitions,
2025
subscribe,
2126
} from '../execute';
2227

@@ -420,10 +425,14 @@ describe('Subscription Initialization Phase', () => {
420425
const document = parse('subscription { foo }');
421426
const result = await subscribe({ schema, document });
422427

423-
const exeContext = buildExecutionContext({
424-
schema,
425-
document,
426-
}) as ExecutionContext;
428+
const { operation, fragments } =
429+
getNormalizedExecutableDefinitions(document);
430+
const exeContext = buildExecutionContext(
431+
{ schema, document },
432+
operation as OperationDefinitionNode,
433+
fragments as ObjMap<FragmentDefinitionNode>,
434+
{},
435+
);
427436
expect(await createSourceEventStream(exeContext)).to.deep.equal(result);
428437
return result;
429438
}

src/execution/execute.ts

Lines changed: 125 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ const collectSubfields = memoize3(
102102
* 3) inline fragment "spreads" e.g. `...on Type { a }`
103103
*/
104104

105+
type NormalizedExecutableDocument =
106+
| {
107+
errors: ReadonlyArray<GraphQLError>;
108+
operation?: never;
109+
fragments?: never;
110+
}
111+
| {
112+
operation: OperationDefinitionNode;
113+
fragments: ObjMap<FragmentDefinitionNode>;
114+
errors?: never;
115+
};
116+
105117
/**
106118
* Data that must be available at all points during query execution.
107119
*
@@ -197,22 +209,59 @@ export interface SubscriptionArgs extends ExecutionArgs {}
197209
* rather than a promise that resolves to the ExecutionResult with the errors.
198210
*
199211
*/
200-
export function executeRequest(
201-
args: ExecutionArgs,
202-
):
212+
export function executeRequest({
213+
schema,
214+
document,
215+
rootValue,
216+
contextValue,
217+
variableValues,
218+
operationName,
219+
disableSubscription,
220+
fieldResolver,
221+
typeResolver,
222+
subscribeFieldResolver,
223+
}: ExecutionArgs):
203224
| ExecutionResult
204225
| Promise<ExecutionResult | AsyncGenerator<ExecutionResult, void, void>> {
205-
const exeContext = buildExecutionContext(args);
226+
// If arguments are missing or incorrect, throw an error.
227+
assertValidExecutionArguments(schema, document, variableValues);
206228

207-
// Return early errors if execution context failed.
208-
if (!('schema' in exeContext)) {
209-
return { errors: exeContext };
229+
// If an error is encountered while selecting an operation, return it.
230+
const normalizedExecutableDocument = getNormalizedExecutableDefinitions(
231+
document,
232+
operationName,
233+
);
234+
if (normalizedExecutableDocument.errors) {
235+
return { errors: normalizedExecutableDocument.errors };
210236
}
211237

212-
if (
213-
!args.disableSubscription &&
214-
exeContext.operation.operation === 'subscription'
215-
) {
238+
const { operation, fragments } = normalizedExecutableDocument;
239+
240+
// If errors are encountered while coercing variable values, return them.
241+
const coercedVariableValues = getCoercedVariableValues(
242+
schema,
243+
operation,
244+
variableValues,
245+
);
246+
if (coercedVariableValues.errors) {
247+
return { errors: coercedVariableValues.errors };
248+
}
249+
250+
// Set up the execution context
251+
const exeContext = {
252+
schema,
253+
fragments,
254+
rootValue,
255+
contextValue,
256+
operation,
257+
coercedVariableValues: coercedVariableValues.coerced,
258+
fieldResolver: fieldResolver ?? defaultFieldResolver,
259+
typeResolver: typeResolver ?? defaultTypeResolver,
260+
subscribeFieldResolver,
261+
errors: [],
262+
};
263+
264+
if (!disableSubscription && operation.operation === 'subscription') {
216265
return executeSubscription(exeContext);
217266
}
218267

@@ -326,41 +375,31 @@ export function assertValidExecutionArguments(
326375
}
327376

328377
/**
329-
* Constructs a ExecutionContext object from the arguments passed to
330-
* executeRequest, which we will pass throughout the other execution methods.
378+
* Normalizes executable definitions within a document based on the given
379+
* operation name.
331380
*
332-
* Throws a GraphQLError if a valid execution context cannot be created.
381+
* Returns a GraphQLError if a single matching operation cannot be found.
333382
*
334383
* @internal
335384
*/
336-
export function buildExecutionContext(
337-
args: ExecutionArgs,
338-
): ReadonlyArray<GraphQLError> | ExecutionContext {
339-
const {
340-
schema,
341-
document,
342-
rootValue,
343-
contextValue,
344-
variableValues,
345-
operationName,
346-
fieldResolver,
347-
typeResolver,
348-
subscribeFieldResolver,
349-
} = args;
350-
assertValidExecutionArguments(schema, document, variableValues);
351-
385+
export function getNormalizedExecutableDefinitions(
386+
document: DocumentNode,
387+
operationName?: Maybe<string>,
388+
): NormalizedExecutableDocument {
352389
let operation: OperationDefinitionNode | undefined;
353390
const fragments: ObjMap<FragmentDefinitionNode> = Object.create(null);
354391
for (const definition of document.definitions) {
355392
switch (definition.kind) {
356393
case Kind.OPERATION_DEFINITION:
357394
if (operationName == null) {
358395
if (operation !== undefined) {
359-
return [
360-
new GraphQLError(
361-
'Must provide operation name if query contains multiple operations.',
362-
),
363-
];
396+
return {
397+
errors: [
398+
new GraphQLError(
399+
'Must provide operation name if query contains multiple operations.',
400+
),
401+
],
402+
};
364403
}
365404
operation = definition;
366405
} else if (definition.name?.value === operationName) {
@@ -375,34 +414,71 @@ export function buildExecutionContext(
375414

376415
if (!operation) {
377416
if (operationName != null) {
378-
return [new GraphQLError(`Unknown operation named "${operationName}".`)];
417+
return {
418+
errors: [
419+
new GraphQLError(`Unknown operation named "${operationName}".`),
420+
],
421+
};
379422
}
380-
return [new GraphQLError('Must provide an operation.')];
423+
return { errors: [new GraphQLError('Must provide an operation.')] };
381424
}
382425

426+
return {
427+
operation,
428+
fragments,
429+
};
430+
}
431+
432+
/**
433+
* Gets coerced variable values based on a given schema and operation.
434+
*
435+
* A thin wrapper around getVariableValues.
436+
*
437+
* @internal
438+
*/
439+
function getCoercedVariableValues(
440+
schema: GraphQLSchema,
441+
operation: OperationDefinitionNode,
442+
variableValues: Maybe<{ readonly [variable: string]: unknown }>,
443+
) {
383444
// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
384445
const variableDefinitions = operation.variableDefinitions ?? [];
385446

386-
const coercedVariableValues = getVariableValues(
387-
schema,
388-
variableDefinitions,
389-
variableValues ?? {},
390-
{
391-
maxErrors: 50,
392-
},
393-
);
447+
return getVariableValues(schema, variableDefinitions, variableValues ?? {}, {
448+
maxErrors: 50,
449+
});
450+
}
394451

395-
if (coercedVariableValues.errors) {
396-
return coercedVariableValues.errors;
397-
}
452+
/**
453+
* Constructs a ExecutionContext object from the arguments passed to
454+
* executeRequest, the normalized executable definitions, and the coerced
455+
* variable values. The ExecutionContext will be passed throughout the
456+
* other execution methods.
457+
*
458+
* @internal
459+
*/
460+
export function buildExecutionContext(
461+
args: ExecutionArgs,
462+
operation: OperationDefinitionNode,
463+
fragments: ObjMap<FragmentDefinitionNode>,
464+
coercedVariableValues: { [variable: string]: unknown },
465+
): ExecutionContext {
466+
const {
467+
schema,
468+
rootValue,
469+
contextValue,
470+
fieldResolver,
471+
typeResolver,
472+
subscribeFieldResolver,
473+
} = args;
398474

399475
return {
400476
schema,
401477
fragments,
402478
rootValue,
403479
contextValue,
404480
operation,
405-
coercedVariableValues: coercedVariableValues.coerced,
481+
coercedVariableValues,
406482
fieldResolver: fieldResolver ?? defaultFieldResolver,
407483
typeResolver: typeResolver ?? defaultTypeResolver,
408484
subscribeFieldResolver,

0 commit comments

Comments
 (0)