diff --git a/src/execution/__tests__/executor-test.ts b/src/execution/__tests__/executor-test.ts index f5e55b9ef8..29332bffc2 100644 --- a/src/execution/__tests__/executor-test.ts +++ b/src/execution/__tests__/executor-test.ts @@ -908,6 +908,29 @@ describe('Execute: Handles basic execution tasks', () => { expect(result).to.deep.equal({ data: { a: 'b' } }); }); + it('resolves to an error if schema does not support operation', () => { + const schema = new GraphQLSchema({ assumeValid: true }); + + const document = parse(` + query Q { __typename } + mutation M { __typename } + subscription S { __typename } + `); + + // FIXME: errors should be wrapped into ExecutionResult + expect(() => + executeSync({ schema, document, operationName: 'Q' }), + ).to.throw('Schema is not configured to execute query operation.'); + + expect(() => + executeSync({ schema, document, operationName: 'M' }), + ).to.throw('Schema is not configured to execute mutation operation.'); + + expect(() => + executeSync({ schema, document, operationName: 'S' }), + ).to.throw('Schema is not configured to execute subscription operation.'); + }); + it('correct field ordering despite execution order', async () => { const schema = new GraphQLSchema({ query: new GraphQLObjectType({ diff --git a/src/execution/__tests__/subscribe-test.ts b/src/execution/__tests__/subscribe-test.ts index 097535259d..5d20d7a42f 100644 --- a/src/execution/__tests__/subscribe-test.ts +++ b/src/execution/__tests__/subscribe-test.ts @@ -367,6 +367,22 @@ describe('Subscription Initialization Phase', () => { ); }); + it('resolves to an error if schema does not support subscriptions', async () => { + const schema = new GraphQLSchema({ query: DummyQueryType }); + const document = parse('subscription { unknownField }'); + + const result = await subscribe({ schema, document }); + expectJSON(result).to.deep.equal({ + errors: [ + { + message: + 'Schema is not configured to execute subscription operation.', + locations: [{ line: 1, column: 1 }], + }, + ], + }); + }); + it('resolves to an error for unknown subscription field', async () => { const schema = new GraphQLSchema({ query: DummyQueryType, diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 5337d55908..7fbab33bfc 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -51,8 +51,6 @@ import { isNonNullType, } from '../type/definition'; -import { getOperationRootType } from '../utilities/getOperationRootType'; - import { getVariableValues, getArgumentValues } from './values'; import { collectFields, @@ -336,12 +334,19 @@ function executeOperation( operation: OperationDefinitionNode, rootValue: unknown, ): PromiseOrValue | null> { - const type = getOperationRootType(exeContext.schema, operation); - const fields = collectFields( + const rootType = exeContext.schema.getRootType(operation.operation); + if (rootType == null) { + throw new GraphQLError( + `Schema is not configured to execute ${operation.operation} operation.`, + operation, + ); + } + + const rootFields = collectFields( exeContext.schema, exeContext.fragments, exeContext.variableValues, - type, + rootType, operation.selectionSet, ); @@ -353,8 +358,14 @@ function executeOperation( try { const result = operation.operation === 'mutation' - ? executeFieldsSerially(exeContext, type, rootValue, path, fields) - : executeFields(exeContext, type, rootValue, path, fields); + ? executeFieldsSerially( + exeContext, + rootType, + rootValue, + path, + rootFields, + ) + : executeFields(exeContext, rootType, rootValue, path, rootFields); if (isPromise(result)) { return result.then(undefined, (error) => { exeContext.errors.push(error); diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts index 1d19eaf7ea..7f93d57911 100644 --- a/src/execution/subscribe.ts +++ b/src/execution/subscribe.ts @@ -11,8 +11,6 @@ import type { DocumentNode } from '../language/ast'; import type { GraphQLSchema } from '../type/schema'; import type { GraphQLFieldResolver } from '../type/definition'; -import { getOperationRootType } from '../utilities/getOperationRootType'; - import type { ExecutionArgs, ExecutionResult, @@ -194,16 +192,24 @@ async function executeSubscription( ): Promise { const { schema, fragments, operation, variableValues, rootValue } = exeContext; - const type = getOperationRootType(schema, operation); - const fields = collectFields( + + const rootType = schema.getSubscriptionType(); + if (rootType == null) { + throw new GraphQLError( + 'Schema is not configured to execute subscription operation.', + operation, + ); + } + + const rootFields = collectFields( schema, fragments, variableValues, - type, + rootType, operation.selectionSet, ); - const [responseName, fieldNodes] = [...fields.entries()][0]; - const fieldDef = getFieldDef(schema, type, fieldNodes[0]); + const [responseName, fieldNodes] = [...rootFields.entries()][0]; + const fieldDef = getFieldDef(schema, rootType, fieldNodes[0]); if (!fieldDef) { const fieldName = fieldNodes[0].name.value; @@ -213,8 +219,14 @@ async function executeSubscription( ); } - const path = addPath(undefined, responseName, type.name); - const info = buildResolveInfo(exeContext, fieldDef, fieldNodes, type, path); + const path = addPath(undefined, responseName, rootType.name); + const info = buildResolveInfo( + exeContext, + fieldDef, + fieldNodes, + rootType, + path, + ); try { // Implements the "ResolveFieldEventStream" algorithm from GraphQL specification. diff --git a/src/type/schema.ts b/src/type/schema.ts index 869327c36d..98de60c53a 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -9,6 +9,7 @@ import type { Maybe } from '../jsutils/Maybe'; import type { GraphQLError } from '../error/GraphQLError'; import type { + OperationTypeNode, SchemaDefinitionNode, SchemaExtensionNode, } from '../language/ast'; @@ -275,6 +276,17 @@ export class GraphQLSchema { return this._subscriptionType; } + getRootType(operation: OperationTypeNode): Maybe { + switch (operation) { + case 'query': + return this.getQueryType(); + case 'mutation': + return this.getMutationType(); + case 'subscription': + return this.getSubscriptionType(); + } + } + getTypeMap(): TypeMap { return this._typeMap; } diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index bd0d23971d..e95efe0b96 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -169,19 +169,8 @@ export class TypeInfo { this._directive = schema.getDirective(node.name.value); break; case Kind.OPERATION_DEFINITION: { - let type: unknown; - switch (node.operation) { - case 'query': - type = schema.getQueryType(); - break; - case 'mutation': - type = schema.getMutationType(); - break; - case 'subscription': - type = schema.getSubscriptionType(); - break; - } - this._typeStack.push(isObjectType(type) ? type : undefined); + const rootType = schema.getRootType(node.operation); + this._typeStack.push(isObjectType(rootType) ? rootType : undefined); break; } case Kind.INLINE_FRAGMENT: diff --git a/src/utilities/__tests__/getOperationRootType-test.ts b/src/utilities/__tests__/getOperationRootType-test.ts index f730e7810a..99292d15d2 100644 --- a/src/utilities/__tests__/getOperationRootType-test.ts +++ b/src/utilities/__tests__/getOperationRootType-test.ts @@ -40,7 +40,7 @@ function getOperationNode(doc: DocumentNode): OperationDefinitionNode { return operationNode; } -describe('getOperationRootType', () => { +describe('Deprecated - getOperationRootType', () => { it('Gets a Query type for an unnamed OperationDefinitionNode', () => { const testSchema = new GraphQLSchema({ query: queryType, diff --git a/src/utilities/getOperationRootType.ts b/src/utilities/getOperationRootType.ts index 039cefaa40..7738ec14e9 100644 --- a/src/utilities/getOperationRootType.ts +++ b/src/utilities/getOperationRootType.ts @@ -10,6 +10,8 @@ import type { GraphQLObjectType } from '../type/definition'; /** * Extracts the root type of the operation from the schema. + * + * @deprecated Please use `GraphQLSchema.getRootType` instead. Will be removed in v17 */ export function getOperationRootType( schema: GraphQLSchema,