From dbd11186fed5a3346f3548a5733e67bd65ee9e7e Mon Sep 17 00:00:00 2001
From: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
Date: Fri, 17 Jun 2022 22:53:49 +0300
Subject: [PATCH 1/5] subscribe: fix missing path on unknown field error

---
 src/execution/__tests__/subscribe-test.ts |  1 +
 src/execution/execute.ts                  | 36 ++++++++++++-----------
 2 files changed, 20 insertions(+), 17 deletions(-)

diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts
index 4852d86ad3..3046afc883 100644
--- a/src/execution/__tests__/subscribe-test.ts
+++ b/src/execution/__tests__/subscribe-test.ts
@@ -424,6 +424,7 @@ describe('Subscription Initialization Phase', () => {
         {
           message: 'The subscription field "unknownField" is not defined.',
           locations: [{ line: 1, column: 16 }],
+          path: ['unknownField'],
         },
       ],
     });
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index be0323ca76..60d8fa0a77 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -1127,27 +1127,29 @@ function executeSubscription(
     rootType,
     operation.selectionSet,
   );
-  const [responseName, fieldNodes] = [...rootFields.entries()][0];
-  const fieldName = fieldNodes[0].name.value;
-  const fieldDef = schema.getField(rootType, fieldName);
-
-  if (!fieldDef) {
-    throw new GraphQLError(
-      `The subscription field "${fieldName}" is not defined.`,
-      { nodes: fieldNodes },
-    );
-  }
 
+  const [responseName, fieldNodes] = [...rootFields.entries()][0];
   const path = addPath(undefined, responseName, rootType.name);
-  const info = buildResolveInfo(
-    exeContext,
-    fieldDef,
-    fieldNodes,
-    rootType,
-    path,
-  );
 
   try {
+    const fieldName = fieldNodes[0].name.value;
+    const fieldDef = schema.getField(rootType, fieldName);
+
+    if (!fieldDef) {
+      throw new GraphQLError(
+        `The subscription field "${fieldName}" is not defined.`,
+        { nodes: fieldNodes },
+      );
+    }
+
+    const info = buildResolveInfo(
+      exeContext,
+      fieldDef,
+      fieldNodes,
+      rootType,
+      path,
+    );
+
     // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification.
     // It differs from "ResolveFieldValue" due to providing a different `resolveFn`.
 

From a1b86acb7461703012e846f8ba2cc35db5e812e0 Mon Sep 17 00:00:00 2001
From: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
Date: Sat, 18 Jun 2022 00:33:28 +0300
Subject: [PATCH 2/5] step 1

---
 src/execution/execute.ts | 39 +++++++++++++++++++++------------------
 1 file changed, 21 insertions(+), 18 deletions(-)

diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 60d8fa0a77..3f917164af 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -231,6 +231,10 @@ function buildResponse(
   return errors.length === 0 ? { data } : { errors, data };
 }
 
+function buildErrorResponse(error: GraphQLError) {
+  return { errors: [error] };
+}
+
 /**
  * Constructs a ExecutionContext object from the arguments passed to
  * execute, which we will pass throughout the other execution methods.
@@ -1094,29 +1098,22 @@ export function createSourceEventStream(
     return { errors: exeContext };
   }
 
-  try {
-    const eventStream = executeSubscription(exeContext);
-    if (isPromise(eventStream)) {
-      return eventStream.then(undefined, (error) => ({ errors: [error] }));
-    }
-
-    return eventStream;
-  } catch (error) {
-    return { errors: [error] };
-  }
+  return executeSubscription(exeContext);
 }
 
 function executeSubscription(
   exeContext: ExecutionContext,
-): PromiseOrValue<AsyncIterable<unknown>> {
+): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
   const { schema, fragments, operation, variableValues, rootValue } =
     exeContext;
 
   const rootType = schema.getSubscriptionType();
   if (rootType == null) {
-    throw new GraphQLError(
-      'Schema is not configured to execute subscription operation.',
-      { nodes: operation },
+    return buildErrorResponse(
+      new GraphQLError(
+        'Schema is not configured to execute subscription operation.',
+        { nodes: operation },
+      ),
     );
   }
 
@@ -1168,14 +1165,20 @@ function executeSubscription(
     const result = resolveFn(rootValue, args, contextValue, info);
 
     if (isPromise(result)) {
-      return result.then(assertEventStream).then(undefined, (error) => {
-        throw locatedError(error, fieldNodes, pathToArray(path));
-      });
+      return result
+        .then(assertEventStream)
+        .then(undefined, (error) =>
+          buildErrorResponse(
+            locatedError(error, fieldNodes, pathToArray(path)),
+          ),
+        );
     }
 
     return assertEventStream(result);
   } catch (error) {
-    throw locatedError(error, fieldNodes, pathToArray(path));
+    return buildErrorResponse(
+      locatedError(error, fieldNodes, pathToArray(path)),
+    );
   }
 }
 

From 974d92dbd982268e16ee8023a44e38a69d30d1cc Mon Sep 17 00:00:00 2001
From: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
Date: Sat, 18 Jun 2022 00:33:53 +0300
Subject: [PATCH 3/5] step 2

---
 src/execution/execute.ts | 31 ++++++++++++++++++-------------
 1 file changed, 18 insertions(+), 13 deletions(-)

diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 3f917164af..660eb5afa8 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -346,31 +346,36 @@ function executeOperation(
     rootType,
     operation.selectionSet,
   );
-  const path = undefined;
-
-  const { rootValue } = exeContext;
 
   switch (operation.operation) {
     case OperationTypeNode.QUERY:
-      return executeFields(exeContext, rootType, rootValue, path, rootFields);
+      return executeRootFields(exeContext, rootType, rootFields, false);
     case OperationTypeNode.MUTATION:
-      return executeFieldsSerially(
-        exeContext,
-        rootType,
-        rootValue,
-        path,
-        rootFields,
-      );
+      return executeRootFields(exeContext, rootType, rootFields, true);
     case OperationTypeNode.SUBSCRIPTION:
       // TODO: deprecate `subscribe` and move all logic here
       // Temporary solution until we finish merging execute and subscribe together
-      return executeFields(exeContext, rootType, rootValue, path, rootFields);
+      return executeRootFields(exeContext, rootType, rootFields, false);
   }
 }
 
+function executeRootFields(
+  exeContext: ExecutionContext,
+  rootType: GraphQLObjectType,
+  rootFields: Map<string, ReadonlyArray<FieldNode>>,
+  executeSerially: boolean,
+): PromiseOrValue<ObjMap<unknown>> {
+  const { rootValue } = exeContext;
+  const path = undefined;
+
+  return executeSerially
+    ? executeFieldsSerially(exeContext, rootType, rootValue, path, rootFields)
+    : executeFields(exeContext, rootType, rootValue, path, rootFields);
+}
+
 /**
  * Implements the "Executing selection sets" section of the spec
- * for fields that must be executed serially.
+ * for root fields that must be executed serially.
  */
 function executeFieldsSerially(
   exeContext: ExecutionContext,

From f5c9d66a06bd99261f7e14306262e3c21b1b74af Mon Sep 17 00:00:00 2001
From: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
Date: Sat, 18 Jun 2022 16:18:12 +0300
Subject: [PATCH 4/5] step3

---
 src/execution/__tests__/executor-test.ts |  3 -
 src/execution/execute.ts                 | 74 ++++++++++++------------
 2 files changed, 38 insertions(+), 39 deletions(-)

diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts
index 60b203dc05..7d22cdac72 100644
--- a/src/execution/__tests__/executor-test.ts
+++ b/src/execution/__tests__/executor-test.ts
@@ -871,7 +871,6 @@ describe('Execute: Handles basic execution tasks', () => {
     expectJSON(
       executeSync({ schema, document, operationName: 'Q' }),
     ).toDeepEqual({
-      data: null,
       errors: [
         {
           message: 'Schema is not configured to execute query operation.',
@@ -883,7 +882,6 @@ describe('Execute: Handles basic execution tasks', () => {
     expectJSON(
       executeSync({ schema, document, operationName: 'M' }),
     ).toDeepEqual({
-      data: null,
       errors: [
         {
           message: 'Schema is not configured to execute mutation operation.',
@@ -895,7 +893,6 @@ describe('Execute: Handles basic execution tasks', () => {
     expectJSON(
       executeSync({ schema, document, operationName: 'S' }),
     ).toDeepEqual({
-      data: null,
       errors: [
         {
           message:
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 660eb5afa8..baea6ad10b 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -174,34 +174,7 @@ export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
     return { errors: exeContext };
   }
 
-  // Return a Promise that will eventually resolve to the data described by
-  // The "Response" section of the GraphQL specification.
-  //
-  // If errors are encountered while executing a GraphQL field, only that
-  // field and its descendants will be omitted, and sibling fields will still
-  // be executed. An execution which encounters errors will still result in a
-  // resolved Promise.
-  //
-  // Errors from sub-fields of a NonNull type may propagate to the top level,
-  // at which point we still log the error and null the parent field, which
-  // in this case is the entire response.
-  try {
-    const { operation } = exeContext;
-    const result = executeOperation(exeContext, operation);
-    if (isPromise(result)) {
-      return result.then(
-        (data) => buildResponse(data, exeContext.errors),
-        (error) => {
-          exeContext.errors.push(error);
-          return buildResponse(null, exeContext.errors);
-        },
-      );
-    }
-    return buildResponse(result, exeContext.errors);
-  } catch (error) {
-    exeContext.errors.push(error);
-    return buildResponse(null, exeContext.errors);
-  }
+  return executeOperation(exeContext, exeContext.operation);
 }
 
 /**
@@ -330,12 +303,14 @@ export function buildExecutionContext(
 function executeOperation(
   exeContext: ExecutionContext,
   operation: OperationDefinitionNode,
-): PromiseOrValue<ObjMap<unknown>> {
+): PromiseOrValue<ExecutionResult> {
   const rootType = exeContext.schema.getRootType(operation.operation);
   if (rootType == null) {
-    throw new GraphQLError(
-      `Schema is not configured to execute ${operation.operation} operation.`,
-      { nodes: operation },
+    return buildErrorResponse(
+      new GraphQLError(
+        `Schema is not configured to execute ${operation.operation} operation.`,
+        { nodes: operation },
+      ),
     );
   }
 
@@ -364,13 +339,40 @@ function executeRootFields(
   rootType: GraphQLObjectType,
   rootFields: Map<string, ReadonlyArray<FieldNode>>,
   executeSerially: boolean,
-): PromiseOrValue<ObjMap<unknown>> {
+): PromiseOrValue<ExecutionResult> {
   const { rootValue } = exeContext;
   const path = undefined;
 
-  return executeSerially
-    ? executeFieldsSerially(exeContext, rootType, rootValue, path, rootFields)
-    : executeFields(exeContext, rootType, rootValue, path, rootFields);
+  // Return a Promise that will eventually resolve to the data described by
+  // The "Response" section of the GraphQL specification.
+  //
+  // If errors are encountered while executing a GraphQL field, only that
+  // field and its descendants will be omitted, and sibling fields will still
+  // be executed. An execution which encounters errors will still result in a
+  // resolved Promise.
+  //
+  // Errors from sub-fields of a NonNull type may propagate to the top level,
+  // at which point we still log the error and null the parent field, which
+  // in this case is the entire response.
+  try {
+    const data = executeSerially
+      ? executeFieldsSerially(exeContext, rootType, rootValue, path, rootFields)
+      : executeFields(exeContext, rootType, rootValue, path, rootFields);
+
+    if (isPromise(data)) {
+      return data.then(
+        (resolvedData) => buildResponse(resolvedData, exeContext.errors),
+        (error) => {
+          exeContext.errors.push(error);
+          return buildResponse(null, exeContext.errors);
+        },
+      );
+    }
+    return buildResponse(data, exeContext.errors);
+  } catch (error) {
+    exeContext.errors.push(error);
+    return { errors: exeContext.errors, data: null };
+  }
 }
 
 /**

From 086a4f72ab03d6ce5d1b03e6952dc962c73c5344 Mon Sep 17 00:00:00 2001
From: Ivan Goncharov <ivan.goncharov.ua@gmail.com>
Date: Tue, 21 Jun 2022 17:11:35 +0300
Subject: [PATCH 5/5] temp

---
 src/execution/execute.ts | 837 +++++++++++++++++++++++++++------------
 1 file changed, 588 insertions(+), 249 deletions(-)

diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index baea6ad10b..54aa300b25 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -165,16 +165,38 @@ export interface ExecutionArgs {
  * a GraphQLError will be thrown immediately explaining the invalid input.
  */
 export function execute(args: ExecutionArgs): PromiseOrValue<ExecutionResult> {
-  // If a valid execution context cannot be created due to incorrect arguments,
+  // If a valid ExecutableRequest cannot be created due to incorrect arguments,
   // a "Response" with only errors is returned.
-  const exeContext = buildExecutionContext(args);
+  const makeExecutableRequestReturn = makeExecutableRequest(
+    args.schema,
+    args.document,
+    args.operationName,
+    {
+      fieldResolver: args.fieldResolver,
+      typeResolver: args.typeResolver,
+      subscribeFieldResolver: args.subscribeFieldResolver,
+    }
+  );
 
-  // Return early errors if execution context failed.
-  if (!('schema' in exeContext)) {
-    return { errors: exeContext };
+  if (makeExecutableRequestReturn.errors !== undefined) {
+    return makeExecutableRequestReturn;
   }
+  const executableRequest = makeExecutableRequestReturn.result;
 
-  return executeOperation(exeContext, exeContext.operation);
+
+  const coerceVariableValuesReturn = executableRequest.coerceVariableValues(
+    args.variableValues,
+  );
+  if (coerceVariableValuesReturn.errors !== undefined) {
+    return coerceVariableValuesReturn;
+  }
+  const coerceVariableValues = coerceVariableValuesReturn.result;
+
+  return executableRequest.executeOperation(
+    coerceVariableValues,
+    args.contextValue,
+    args.rootValue,
+  );
 }
 
 /**
@@ -193,45 +215,32 @@ export function executeSync(args: ExecutionArgs): ExecutionResult {
   return result;
 }
 
-/**
- * Given a completed execution context and data, build the `{ errors, data }`
- * response defined by the "Response" section of the GraphQL specification.
- */
-function buildResponse(
-  data: ObjMap<unknown> | null,
-  errors: ReadonlyArray<GraphQLError>,
-): ExecutionResult {
-  return errors.length === 0 ? { data } : { errors, data };
+interface GraphQLExecutionPlanOptions {
+  fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
+  typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
+  subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
 }
 
-function buildErrorResponse(error: GraphQLError) {
-  return { errors: [error] };
+interface ExecutionPlan {
+  schema: GraphQLSchema;
+  operation: OperationDefinitionNode;
+  fragments: ObjMap<FragmentDefinitionNode>;
+  rootType: GraphQLObjectType;
+  fieldResolver: GraphQLFieldResolver<any, any>;
+  typeResolver: GraphQLTypeResolver<any, any>;
+  subscribeFieldResolver: GraphQLFieldResolver<any, any>;
 }
 
-/**
- * Constructs a ExecutionContext object from the arguments passed to
- * execute, which we will pass throughout the other execution methods.
- *
- * Throws a GraphQLError if a valid execution context cannot be created.
- *
- * TODO: consider no longer exporting this function
- * @internal
- */
-export function buildExecutionContext(
-  args: ExecutionArgs,
-): ReadonlyArray<GraphQLError> | ExecutionContext {
-  const {
-    schema,
-    document,
-    rootValue,
-    contextValue,
-    variableValues: rawVariableValues,
-    operationName,
-    fieldResolver,
-    typeResolver,
-    subscribeFieldResolver,
-  } = args;
+type ResultOrGraphQLErrors<T> =
+  | { errors: ReadonlyArray<GraphQLError>; result?: never }
+  | { result: T; errors?: never };
 
+export function makeExecutionPlan(
+  schema: GraphQLSchema,
+  document: DocumentNode,
+  operationName: Maybe<string>,
+  options: GraphQLExecutionPlanOptions = {},
+): ResultOrGraphQLErrors<ExecutionPlan> {
   // If the schema used for execution is invalid, throw an error.
   assertValidSchema(schema);
 
@@ -242,11 +251,11 @@ export function buildExecutionContext(
       case Kind.OPERATION_DEFINITION:
         if (operationName == null) {
           if (operation !== undefined) {
-            return [
+            return buildErrorResponse(
               new GraphQLError(
                 'Must provide operation name if query contains multiple operations.',
               ),
-            ];
+            );
           }
           operation = definition;
         } else if (definition.name?.value === operationName) {
@@ -263,48 +272,14 @@ export function buildExecutionContext(
 
   if (!operation) {
     if (operationName != null) {
-      return [new GraphQLError(`Unknown operation named "${operationName}".`)];
+      return buildErrorResponse(
+        new GraphQLError(`Unknown operation named "${operationName}".`),
+      );
     }
-    return [new GraphQLError('Must provide an operation.')];
-  }
-
-  // FIXME: https://github.com/graphql/graphql-js/issues/2203
-  /* c8 ignore next */
-  const variableDefinitions = operation.variableDefinitions ?? [];
-
-  const coercedVariableValues = getVariableValues(
-    schema,
-    variableDefinitions,
-    rawVariableValues ?? {},
-    { maxErrors: 50 },
-  );
-
-  if (coercedVariableValues.errors) {
-    return coercedVariableValues.errors;
+    return buildErrorResponse(new GraphQLError('Must provide an operation.'));
   }
 
-  return {
-    schema,
-    fragments,
-    rootValue,
-    contextValue,
-    operation,
-    variableValues: coercedVariableValues.coerced,
-    fieldResolver: fieldResolver ?? defaultFieldResolver,
-    typeResolver: typeResolver ?? defaultTypeResolver,
-    subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
-    errors: [],
-  };
-}
-
-/**
- * Implements the "Executing operations" section of the spec.
- */
-function executeOperation(
-  exeContext: ExecutionContext,
-  operation: OperationDefinitionNode,
-): PromiseOrValue<ExecutionResult> {
-  const rootType = exeContext.schema.getRootType(operation.operation);
+  const rootType = schema.getRootType(operation.operation);
   if (rootType == null) {
     return buildErrorResponse(
       new GraphQLError(
@@ -314,67 +289,301 @@ function executeOperation(
     );
   }
 
-  const rootFields = collectFields(
-    exeContext.schema,
-    exeContext.fragments,
-    exeContext.variableValues,
+  const result = {
+    schema,
+    operation,
+    fragments,
     rootType,
-    operation.selectionSet,
+    fieldResolver: options.fieldResolver ?? defaultFieldResolver,
+    typeResolver: options.typeResolver ?? defaultTypeResolver,
+    subscribeFieldResolver:
+      options.subscribeFieldResolver ?? defaultFieldResolver,
+  };
+  return { result };
+}
+
+/**
+ * Constructs a ExecutableRequest object
+ */
+export function makeExecutableRequest(
+  schema: GraphQLSchema,
+  document: DocumentNode,
+  operationName: Maybe<string>,
+  options: GraphQLExecutionPlanOptions = {},
+): ResultOrGraphQLErrors<ExecutableRequest> {
+  const makeExecutionPlanReturn = makeExecutionPlan(
+    schema,
+    document,
+    operationName,
+    options,
   );
 
-  switch (operation.operation) {
+  if (makeExecutionPlanReturn.errors !== undefined) {
+    return makeExecutionPlanReturn;
+  }
+  const executionPlan = makeExecutionPlanReturn.result;
+
+  switch (executionPlan.operation.operation) {
     case OperationTypeNode.QUERY:
-      return executeRootFields(exeContext, rootType, rootFields, false);
+      return { result: new ExecutableQueryRequestImpl(executionPlan) };
     case OperationTypeNode.MUTATION:
-      return executeRootFields(exeContext, rootType, rootFields, true);
+      return { result: new ExecutableMutationRequestImpl(executionPlan) };
     case OperationTypeNode.SUBSCRIPTION:
-      // TODO: deprecate `subscribe` and move all logic here
-      // Temporary solution until we finish merging execute and subscribe together
-      return executeRootFields(exeContext, rootType, rootFields, false);
+      return { result: new ExecutableSubscriptionRequestImpl(executionPlan) };
   }
 }
 
-function executeRootFields(
-  exeContext: ExecutionContext,
-  rootType: GraphQLObjectType,
-  rootFields: Map<string, ReadonlyArray<FieldNode>>,
-  executeSerially: boolean,
-): PromiseOrValue<ExecutionResult> {
-  const { rootValue } = exeContext;
-  const path = undefined;
-
-  // Return a Promise that will eventually resolve to the data described by
-  // The "Response" section of the GraphQL specification.
-  //
-  // If errors are encountered while executing a GraphQL field, only that
-  // field and its descendants will be omitted, and sibling fields will still
-  // be executed. An execution which encounters errors will still result in a
-  // resolved Promise.
-  //
-  // Errors from sub-fields of a NonNull type may propagate to the top level,
-  // at which point we still log the error and null the parent field, which
-  // in this case is the entire response.
-  try {
-    const data = executeSerially
-      ? executeFieldsSerially(exeContext, rootType, rootValue, path, rootFields)
-      : executeFields(exeContext, rootType, rootValue, path, rootFields);
-
-    if (isPromise(data)) {
-      return data.then(
-        (resolvedData) => buildResponse(resolvedData, exeContext.errors),
-        (error) => {
-          exeContext.errors.push(error);
-          return buildResponse(null, exeContext.errors);
-        },
-      );
+class CoercedVariableValues {
+  coercedValues: { [variable: string]: unknown };
+
+  constructor(objMap: { [variable: string]: unknown }) {
+    this.coercedValues = objMap;
+  }
+}
+
+export type ExecutableRequest =
+  | ExecutableQueryRequest
+  | ExecutableMutationRequest
+  | ExecutableSubscriptionRequest;
+
+export interface ExecutableQueryRequest {
+  operationType: OperationTypeNode.QUERY;
+
+  coerceVariableValues: (
+    rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>,
+  ) => ResultOrGraphQLErrors<CoercedVariableValues>;
+
+  /**
+   * Implements the "ExecuteQuery" algorithm described in the GraphQL specification.
+   */
+  executeOperation: (
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ) => PromiseOrValue<ExecutionResult>;
+}
+
+export interface ExecutableMutationRequest {
+  operationType: OperationTypeNode.MUTATION;
+
+  coerceVariableValues: (
+    rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>,
+  ) => ResultOrGraphQLErrors<CoercedVariableValues>;
+
+  /**
+   * Implements the "ExecuteMutation" algorithm described in the GraphQL specification.
+   */
+  executeOperation: (
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ) => PromiseOrValue<ExecutionResult>;
+}
+
+export interface ExecutableSubscriptionRequest {
+  operationType: OperationTypeNode.SUBSCRIPTION;
+
+  coerceVariableValues: (
+    rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>,
+  ) => ResultOrGraphQLErrors<CoercedVariableValues>;
+
+  /**
+   * Implements the "ExecuteSubscription" algorithm described in the GraphQL specification.
+   */
+  executeOperation: (
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ) => PromiseOrValue<ExecutionResult>;
+
+  /**
+   * Implements the "CreateSourceEventStream" algorithm described in the
+   * GraphQL specification, resolving the subscription source event stream.
+   *
+   * Returns a Promise which resolves to either an AsyncIterable (if successful)
+   * or an ExecutionResult (error). The promise will be rejected if the schema or
+   * other arguments to this function are invalid, or if the resolved event stream
+   * is not an async iterable.
+   *
+   * If the client-provided arguments to this function do not result in a
+   * compliant subscription, a GraphQL Response (ExecutionResult) with
+   * descriptive errors and no data will be returned.
+   *
+   * If the the source stream could not be created due to faulty subscription
+   * resolver logic or underlying systems, the promise will resolve to a single
+   * ExecutionResult containing `errors` and no `data`.
+   *
+   * If the operation succeeded, the promise resolves to the AsyncIterable for the
+   * event stream returned by the resolver.
+   *
+   * A Source Event Stream represents a sequence of events, each of which triggers
+   * a GraphQL execution for that event.
+   *
+   * This may be useful when hosting the stateful subscription service in a
+   * different process or machine than the stateless GraphQL execution engine,
+   * or otherwise separating these two steps. For more on this, see the
+   * "Supporting Subscriptions at Scale" information in the GraphQL specification.
+   */
+  createSourceEventStream: (
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ) => PromiseOrValue<ResultOrGraphQLErrors<AsyncIterable<unknown>>>;
+
+  mapSourceToResponse: (
+    stream: AsyncIterable<unknown>,
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+  ) => PromiseOrValue<AsyncGenerator<ExecutionResult, void, void>>;
+
+  /**
+   * Implements the "ExecuteSubscriptionEvent" algorithm described in the GraphQL specification.
+   */
+  executeSubscriptionEvent: (
+    event: unknown,
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+  ) => PromiseOrValue<ExecutionResult>;
+}
+
+class ExecutableRequestImpl {
+  schema: GraphQLSchema;
+  operation: OperationDefinitionNode;
+  fragments: ObjMap<FragmentDefinitionNode>;
+  rootType: GraphQLObjectType;
+  fieldResolver: GraphQLFieldResolver<any, any>;
+  typeResolver: GraphQLTypeResolver<any, any>;
+  subscribeFieldResolver: GraphQLFieldResolver<any, any>;
+
+  constructor(executionPlan: ExecutionPlan) {
+    this.schema = executionPlan.schema;
+    this.operation = executionPlan.operation;
+    this.fragments = executionPlan.fragments;
+    this.rootType = executionPlan.rootType;
+    this.fieldResolver = executionPlan.fieldResolver;
+    this.typeResolver = executionPlan.typeResolver;
+    this.subscribeFieldResolver = executionPlan.subscribeFieldResolver;
+  }
+
+  coerceVariableValues(
+    rawVariableValues: Maybe<{ readonly [variable: string]: unknown }>,
+  ): ResultOrGraphQLErrors<CoercedVariableValues> {
+    // FIXME: https://github.com/graphql/graphql-js/issues/2203
+    /* c8 ignore next */
+    const variableDefinitions = this.operation.variableDefinitions ?? [];
+
+    const getVariableValuesReturn = getVariableValues(
+      this.schema,
+      variableDefinitions,
+      rawVariableValues ?? {},
+      { maxErrors: 50 },
+    );
+
+    if (getVariableValuesReturn.errors !== undefined) {
+      return getVariableValuesReturn;
+    }
+    return {
+      result: new CoercedVariableValues(getVariableValuesReturn.coerced),
+    };
+  }
+
+  _buildExecutionContext(
+    variableValues: CoercedVariableValues,
+    contextValue: unknown,
+    rootValue: unknown,
+  ): ExecutionContext {
+    return {
+      schema: this.schema,
+      fragments: this.fragments,
+      rootValue,
+      contextValue,
+      operation: this.operation,
+      variableValues: variableValues.coercedValues,
+      fieldResolver: this.fieldResolver,
+      typeResolver: this.typeResolver,
+      subscribeFieldResolver: this.subscribeFieldResolver,
+      errors: [],
+    };
+  }
+
+  _getRootFields(variableValues: CoercedVariableValues) {
+    return collectFields(
+      this.schema,
+      this.fragments,
+      variableValues.coercedValues,
+      this.rootType,
+      this.operation.selectionSet,
+    );
+  }
+
+  _executeRootFields(
+    variableValues: CoercedVariableValues,
+    contextValue: unknown,
+    rootValue: unknown,
+    executeSerially: boolean,
+  ): PromiseOrValue<ExecutionResult> {
+    const exeContext = this._buildExecutionContext(
+      variableValues,
+      contextValue,
+      rootValue,
+    );
+    const rootFields = this._getRootFields(variableValues);
+    const path = undefined;
+
+    // Return a Promise that will eventually resolve to the data described by
+    // The "Response" section of the GraphQL specification.
+    //
+    // If errors are encountered while executing a GraphQL field, only that
+    // field and its descendants will be omitted, and sibling fields will still
+    // be executed. An execution which encounters errors will still result in a
+    // resolved Promise.
+    //
+    // Errors from sub-fields of a NonNull type may propagate to the top level,
+    // at which point we still log the error and null the parent field, which
+    // in this case is the entire response.
+    try {
+      const data = executeSerially
+        ? executeFieldsSerially(
+            exeContext,
+            this.rootType,
+            rootValue,
+            path,
+            rootFields,
+          )
+        : executeFields(exeContext, this.rootType, rootValue, path, rootFields);
+
+      if (isPromise(data)) {
+        return data.then(
+          (resolvedData) => buildResponse(resolvedData, exeContext.errors),
+          (error) => {
+            exeContext.errors.push(error);
+            return buildResponse(null, exeContext.errors);
+          },
+        );
+      }
+      return buildResponse(data, exeContext.errors);
+    } catch (error) {
+      exeContext.errors.push(error);
+      return { errors: exeContext.errors, data: null };
     }
-    return buildResponse(data, exeContext.errors);
-  } catch (error) {
-    exeContext.errors.push(error);
-    return { errors: exeContext.errors, data: null };
   }
 }
 
+/**
+ * Given a completed execution context and data, build the `{ errors, data }`
+ * response defined by the "Response" section of the GraphQL specification.
+ */
+function buildResponse(
+  data: ObjMap<unknown> | null,
+  errors: ReadonlyArray<GraphQLError>,
+): ExecutionResult {
+  return errors.length === 0 ? { data } : { errors, data };
+}
+
+function buildErrorResponse(error: GraphQLError) {
+  return { errors: [error] };
+}
+
 /**
  * Implements the "Executing selection sets" section of the spec
  * for root fields that must be executed serially.
@@ -1030,163 +1239,293 @@ export function subscribe(
 ): PromiseOrValue<
   AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
 > {
-  const resultOrStream = createSourceEventStream(args);
+  const makeExecutableRequestReturn = makeExecutableRequest(
+    args.schema,
+    args.document,
+    args.operationName,
+    {
+      fieldResolver: args.fieldResolver,
+      typeResolver: args.typeResolver,
+      subscribeFieldResolver: args.subscribeFieldResolver,
+    }
+  );
+
+  if (makeExecutableRequestReturn.errors !== undefined) {
+    return makeExecutableRequestReturn;
+  }
+  const executableRequest = makeExecutableRequestReturn.result;
 
-  if (isPromise(resultOrStream)) {
-    return resultOrStream.then((resolvedResultOrStream) =>
-      mapSourceToResponse(resolvedResultOrStream, args),
+  if (executableRequest.operationType !== OperationTypeNode.SUBSCRIPTION) {
+    throw new TypeError(
+      'Can not execute `createSourceEventStream` on queries or mutations.',
     );
   }
 
-  return mapSourceToResponse(resultOrStream, args);
-}
+  const coerceVariableValuesReturn = executableRequest.coerceVariableValues(
+    args.variableValues,
+  );
+  if (coerceVariableValuesReturn.errors !== undefined) {
+    return coerceVariableValuesReturn;
+  }
+  const coerceVariableValues = coerceVariableValuesReturn.result;
 
-function mapSourceToResponse(
-  resultOrStream: ExecutionResult | AsyncIterable<unknown>,
-  args: ExecutionArgs,
-): PromiseOrValue<
-  AsyncGenerator<ExecutionResult, void, void> | ExecutionResult
-> {
-  if (!isAsyncIterable(resultOrStream)) {
-    return resultOrStream;
+  const createSourceEventStreamReturn =
+    executableRequest.createSourceEventStream(
+      coerceVariableValues,
+      args.contextValue,
+      args.rootValue,
+    );
+
+  if (isPromise(createSourceEventStreamReturn)) {
+    return createSourceEventStreamReturn.then(
+      (resolvedCreateSourceEventStreamReturn) => {
+        if (resolvedCreateSourceEventStreamReturn.errors !== undefined) {
+          return resolvedCreateSourceEventStreamReturn;
+        }
+        return executableRequest.mapSourceToResponse(
+          resolvedCreateSourceEventStreamReturn.result,
+          coerceVariableValues,
+          args.contextValue,
+        );
+      },
+    );
   }
 
-  // For each payload yielded from a subscription, map it over the normal
-  // GraphQL `execute` function, with `payload` as the rootValue.
-  // This implements the "MapSourceToResponseEvent" algorithm described in
-  // the GraphQL specification. The `execute` function provides the
-  // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
-  // "ExecuteQuery" algorithm, for which `execute` is also used.
-  return mapAsyncIterator(resultOrStream, (payload: unknown) =>
-    execute({
-      ...args,
-      rootValue: payload,
-    }),
+  if (createSourceEventStreamReturn.errors !== undefined) {
+    return createSourceEventStreamReturn;
+  }
+  return executableRequest.mapSourceToResponse(
+    createSourceEventStreamReturn.result,
+    coerceVariableValues,
+    args.contextValue,
   );
 }
 
-/**
- * Implements the "CreateSourceEventStream" algorithm described in the
- * GraphQL specification, resolving the subscription source event stream.
- *
- * Returns a Promise which resolves to either an AsyncIterable (if successful)
- * or an ExecutionResult (error). The promise will be rejected if the schema or
- * other arguments to this function are invalid, or if the resolved event stream
- * is not an async iterable.
- *
- * If the client-provided arguments to this function do not result in a
- * compliant subscription, a GraphQL Response (ExecutionResult) with
- * descriptive errors and no data will be returned.
- *
- * If the the source stream could not be created due to faulty subscription
- * resolver logic or underlying systems, the promise will resolve to a single
- * ExecutionResult containing `errors` and no `data`.
- *
- * If the operation succeeded, the promise resolves to the AsyncIterable for the
- * event stream returned by the resolver.
- *
- * A Source Event Stream represents a sequence of events, each of which triggers
- * a GraphQL execution for that event.
- *
- * This may be useful when hosting the stateful subscription service in a
- * different process or machine than the stateless GraphQL execution engine,
- * or otherwise separating these two steps. For more on this, see the
- * "Supporting Subscriptions at Scale" information in the GraphQL specification.
- */
+/** @deprecated Please use `ExecutableSubscriptionRequest.createSourceEventStream` instead. */
 export function createSourceEventStream(
   args: ExecutionArgs,
 ): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
-  // If a valid execution context cannot be created due to incorrect arguments,
+  // If a valid ExecutableSubscriptionRequest cannot be created due to incorrect arguments,
   // a "Response" with only errors is returned.
-  const exeContext = buildExecutionContext(args);
+  const makeExecutableRequestReturn = makeExecutableRequest(
+    args.schema,
+    args.document,
+    args.operationName,
+    {
+      fieldResolver: args.fieldResolver,
+      typeResolver: args.typeResolver,
+      subscribeFieldResolver: args.subscribeFieldResolver,
+    }
+  );
 
-  // Return early errors if execution context failed.
-  if (!('schema' in exeContext)) {
-    return { errors: exeContext };
+  // Return early errors if execution request failed.
+  if (makeExecutableRequestReturn.errors !== undefined) {
+    return makeExecutableRequestReturn;
   }
+  const executableRequest = makeExecutableRequestReturn.result;
 
-  return executeSubscription(exeContext);
-}
+  if (executableRequest.operationType !== OperationTypeNode.SUBSCRIPTION) {
+    throw new TypeError(
+      'Can not execute `createSourceEventStream` on queries or mutations.',
+    );
+  }
 
-function executeSubscription(
-  exeContext: ExecutionContext,
-): PromiseOrValue<AsyncIterable<unknown> | ExecutionResult> {
-  const { schema, fragments, operation, variableValues, rootValue } =
-    exeContext;
+  const coerceVariableValuesReturn = executableRequest.coerceVariableValues(
+    args.variableValues,
+  );
+  if (coerceVariableValuesReturn.errors !== undefined) {
+    return coerceVariableValuesReturn;
+  }
+  const coerceVariableValues = coerceVariableValuesReturn.result;
 
-  const rootType = schema.getSubscriptionType();
-  if (rootType == null) {
-    return buildErrorResponse(
-      new GraphQLError(
-        'Schema is not configured to execute subscription operation.',
-        { nodes: operation },
-      ),
+  const createSourceEventStreamReturn =
+    executableRequest.createSourceEventStream(
+      coerceVariableValues,
+      args.contextValue,
+      args.rootValue,
+    );
+
+  if (isPromise(createSourceEventStreamReturn)) {
+    return createSourceEventStreamReturn.then(
+      (resolvedCreateSourceEventStreamReturn) => {
+        if (resolvedCreateSourceEventStreamReturn.errors !== undefined) {
+          return resolvedCreateSourceEventStreamReturn;
+        }
+        return resolvedCreateSourceEventStreamReturn.result;
+      },
     );
   }
 
-  const rootFields = collectFields(
-    schema,
-    fragments,
-    variableValues,
-    rootType,
-    operation.selectionSet,
-  );
+  if (createSourceEventStreamReturn.errors !== undefined) {
+    return createSourceEventStreamReturn;
+  }
+  return createSourceEventStreamReturn.result;
+}
 
-  const [responseName, fieldNodes] = [...rootFields.entries()][0];
-  const path = addPath(undefined, responseName, rootType.name);
+class ExecutableQueryRequestImpl
+  extends ExecutableRequestImpl
+  implements ExecutableQueryRequest
+{
+  operationType: OperationTypeNode.QUERY;
 
-  try {
-    const fieldName = fieldNodes[0].name.value;
-    const fieldDef = schema.getField(rootType, fieldName);
+  constructor(executionPlan: ExecutionPlan) {
+    super(executionPlan);
+    this.operationType = OperationTypeNode.QUERY;
+  }
 
-    if (!fieldDef) {
-      throw new GraphQLError(
-        `The subscription field "${fieldName}" is not defined.`,
-        { nodes: fieldNodes },
-      );
-    }
+  executeOperation(
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ): PromiseOrValue<ExecutionResult> {
+    return this._executeRootFields(
+      variableValues,
+      contextValue,
+      rootValue,
+      false,
+    );
+  }
+}
 
-    const info = buildResolveInfo(
-      exeContext,
-      fieldDef,
-      fieldNodes,
-      rootType,
-      path,
+class ExecutableMutationRequestImpl
+  extends ExecutableRequestImpl
+  implements ExecutableMutationRequest
+{
+  operationType: OperationTypeNode.MUTATION;
+
+  constructor(executionPlan: ExecutionPlan) {
+    super(executionPlan);
+    this.operationType = OperationTypeNode.MUTATION;
+  }
+
+  executeOperation(
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ): PromiseOrValue<ExecutionResult> {
+    return this._executeRootFields(
+      variableValues,
+      contextValue,
+      rootValue,
+      true,
     );
+  }
+}
 
-    // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification.
-    // It differs from "ResolveFieldValue" due to providing a different `resolveFn`.
+class ExecutableSubscriptionRequestImpl
+  extends ExecutableRequestImpl
+  implements ExecutableSubscriptionRequest
+{
+  operationType: OperationTypeNode.SUBSCRIPTION;
 
-    // Build a JS object of arguments from the field.arguments AST, using the
-    // variables scope to fulfill any variable references.
-    const args = getArgumentValues(fieldDef, fieldNodes[0], variableValues);
+  constructor(executionPlan: ExecutionPlan) {
+    super(executionPlan);
+    this.operationType = OperationTypeNode.SUBSCRIPTION;
+  }
 
-    // The resolve function's optional third argument is a context value that
-    // is provided to every resolve function within an execution. It is commonly
-    // used to represent an authenticated user, or request-specific caches.
-    const contextValue = exeContext.contextValue;
+  executeOperation(
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ): PromiseOrValue<ExecutionResult> {
+    // TODO: deprecate `subscribe` and move all logic here
+    // Temporary solution until we finish merging execute and subscribe together
+    return this._executeRootFields(
+      variableValues,
+      contextValue,
+      rootValue,
+      false,
+    );
+  }
 
-    // Call the `subscribe()` resolver or the default resolver to produce an
-    // AsyncIterable yielding raw payloads.
-    const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver;
-    const result = resolveFn(rootValue, args, contextValue, info);
+  createSourceEventStream(
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+    rootValue?: unknown,
+  ): PromiseOrValue<ResultOrGraphQLErrors<AsyncIterable<unknown>>> {
+    const exeContext = this._buildExecutionContext(
+      variableValues,
+      contextValue,
+      rootValue,
+    );
+    const rootFields = this._getRootFields(variableValues);
+    const [responseName, fieldNodes] = [...rootFields.entries()][0];
+    const path = addPath(undefined, responseName, this.rootType.name);
 
-    if (isPromise(result)) {
-      return result
-        .then(assertEventStream)
-        .then(undefined, (error) =>
-          buildErrorResponse(
-            locatedError(error, fieldNodes, pathToArray(path)),
-          ),
+    try {
+      const fieldName = fieldNodes[0].name.value;
+      const fieldDef = this.schema.getField(this.rootType, fieldName);
+
+      if (!fieldDef) {
+        throw new GraphQLError(
+          `The subscription field "${fieldName}" is not defined.`,
+          { nodes: fieldNodes },
         );
+      }
+
+      const info = buildResolveInfo(
+        exeContext,
+        fieldDef,
+        fieldNodes,
+        this.rootType,
+        path,
+      );
+
+      // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification.
+      // It differs from "ResolveFieldValue" due to providing a different `resolveFn`.
+
+      // Build a JS object of arguments from the field.arguments AST, using the
+      // variables scope to fulfill any variable references.
+      const args = getArgumentValues(
+        fieldDef,
+        fieldNodes[0],
+        exeContext.variableValues,
+      );
+
+      // Call the `subscribe()` resolver or the default resolver to produce an
+      // AsyncIterable yielding raw payloads.
+      const resolveFn = fieldDef.subscribe ?? exeContext.subscribeFieldResolver;
+      const result = resolveFn(rootValue, args, contextValue, info);
+
+      if (isPromise(result)) {
+        return result
+          .then((resolved) => ({ result: assertEventStream(resolved) }))
+          .then(undefined, (error) => ({
+            errors: [locatedError(error, fieldNodes, pathToArray(path))],
+          }));
+      }
+
+      return { result: assertEventStream(result) };
+    } catch (error) {
+      return {
+        errors: [locatedError(error, fieldNodes, pathToArray(path))],
+      };
     }
+  }
 
-    return assertEventStream(result);
-  } catch (error) {
-    return buildErrorResponse(
-      locatedError(error, fieldNodes, pathToArray(path)),
+  mapSourceToResponse(
+    stream: AsyncIterable<unknown>,
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+  ): PromiseOrValue<AsyncGenerator<ExecutionResult, void, void>> {
+    // For each payload yielded from a subscription, map it over the normal
+    // GraphQL `execute` function, with `payload` as the rootValue.
+    // This implements the "MapSourceToResponseEvent" algorithm described in
+    // the GraphQL specification. The `execute` function provides the
+    // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the
+    // "ExecuteQuery" algorithm, for which `execute` is also used.
+    return mapAsyncIterator(stream, (event: unknown) =>
+      this.executeSubscriptionEvent(event, variableValues, contextValue),
     );
   }
+
+  executeSubscriptionEvent(
+    event: unknown,
+    variableValues: CoercedVariableValues,
+    contextValue?: unknown,
+  ): PromiseOrValue<ExecutionResult> {
+    return this._executeRootFields(variableValues, contextValue, event, false);
+  }
 }
 
 function assertEventStream(result: unknown): AsyncIterable<unknown> {