Repository: graphql/graphql-js
Pull request #3605 (2022-05-25 17:02:38)

Add new 'GraphQLSchema.getField' method
Motivation: generalize it and remove code dublication in two places and
also allow to use as public API outside of graphql-js

IvanGoncharov: > @IvanGoncharov Please, see benchmark results here: [graphql/graphql-js/runs/6598090837?check_suite_focus=true#step:6:1](https://github.com/graphql/graphql-js/runs/6598090837?check_suite_focus=true#step:6:1)

Sadly, I need to figure out a way to run benchmark reliably on CI after #3604 
But here is data from my machine (HEAD is this PR.
<img width="584" alt="image" src="https://user-images.githubusercontent.com/8336157/170347563-bedfc3de-4b18-4728-bde3-81bdb6362652.png">

So performance slightly increase by ~1% due to using switch instead of multiple `if`.

Patch:
diff --git a/src/execution/execute.ts b/src/execution/execute.ts
index 3261f511..6dc42461 100644
--- a/src/execution/execute.ts
+++ b/src/execution/execute.ts
@@ -44,11 +44,6 @@ import {
   isNonNullType,
   isObjectType,
 } from '../type/definition';
-import {
-  SchemaMetaFieldDef,
-  TypeMetaFieldDef,
-  TypeNameMetaFieldDef,
-} from '../type/introspection';
 import type { GraphQLSchema } from '../type/schema';
 import { assertValidSchema } from '../type/validate';
 
@@ -481,7 +476,8 @@ function executeField(
   fieldNodes: ReadonlyArray<FieldNode>,
   path: Path,
 ): PromiseOrValue<unknown> {
-  const fieldDef = getFieldDef(exeContext.schema, parentType, fieldNodes[0]);
+  const fieldName = fieldNodes[0].name.value;
+  const fieldDef = exeContext.schema.getField(parentType, fieldName);
   if (!fieldDef) {
     return;
   }
@@ -1013,37 +1009,3 @@ export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
       return property;
     }
   };
-
-/**
- * This method looks up the field on the given type definition.
- * It has special casing for the three introspection fields,
- * __schema, __type and __typename. __typename is special because
- * it can always be queried as a field, even in situations where no
- * other fields are allowed, like on a Union. __schema and __type
- * could get automatically added to the query type, but that would
- * require mutating type definitions, which would cause issues.
- *
- * @internal
- */
-export function getFieldDef(
-  schema: GraphQLSchema,
-  parentType: GraphQLObjectType,
-  fieldNode: FieldNode,
-): Maybe<GraphQLField<unknown, unknown>> {
-  const fieldName = fieldNode.name.value;
-
-  if (
-    fieldName === SchemaMetaFieldDef.name &&
-    schema.getQueryType() === parentType
-  ) {
-    return SchemaMetaFieldDef;
-  } else if (
-    fieldName === TypeMetaFieldDef.name &&
-    schema.getQueryType() === parentType
-  ) {
-    return TypeMetaFieldDef;
-  } else if (fieldName === TypeNameMetaFieldDef.name) {
-    return TypeNameMetaFieldDef;
-  }
-  return parentType.getFields()[fieldName];
-}
diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts
index 0b240b3f..7a04480b 100644
--- a/src/execution/subscribe.ts
+++ b/src/execution/subscribe.ts
@@ -22,7 +22,6 @@ import {
   buildExecutionContext,
   buildResolveInfo,
   execute,
-  getFieldDef,
 } from './execute';
 import { mapAsyncIterator } from './mapAsyncIterator';
 import { getArgumentValues } from './values';
@@ -199,10 +198,10 @@ async function executeSubscription(
     operation.selectionSet,
   );
   const [responseName, fieldNodes] = [...rootFields.entries()][0];
-  const fieldDef = getFieldDef(schema, rootType, fieldNodes[0]);
+  const fieldName = fieldNodes[0].name.value;
+  const fieldDef = schema.getField(rootType, fieldName);
 
   if (!fieldDef) {
-    const fieldName = fieldNodes[0].name.value;
     throw new GraphQLError(
       `The subscription field "${fieldName}" is not defined.`,
       { nodes: fieldNodes },
diff --git a/src/type/schema.ts b/src/type/schema.ts
index 6b44e1fc..7cc576a1 100644
--- a/src/type/schema.ts
+++ b/src/type/schema.ts
@@ -16,6 +16,8 @@ import { OperationTypeNode } from '../language/ast';
 
 import type {
   GraphQLAbstractType,
+  GraphQLCompositeType,
+  GraphQLField,
   GraphQLInterfaceType,
   GraphQLNamedType,
   GraphQLObjectType,
@@ -30,7 +32,12 @@ import {
 } from './definition';
 import type { GraphQLDirective } from './directives';
 import { isDirective, specifiedDirectives } from './directives';
-import { __Schema } from './introspection';
+import {
+  __Schema,
+  SchemaMetaFieldDef,
+  TypeMetaFieldDef,
+  TypeNameMetaFieldDef,
+} from './introspection';
 
 /**
  * Test if the given value is a GraphQL schema.
@@ -350,6 +357,42 @@ export class GraphQLSchema {
     return this.getDirectives().find((directive) => directive.name === name);
   }
 
+  /**
+   * This method looks up the field on the given type definition.
+   * It has special casing for the three introspection fields, `__schema`,
+   * `__type` and `__typename`.
+   *
+   * `__typename` is special because it can always be queried as a field, even
+   * in situations where no other fields are allowed, like on a Union.
+   *
+   * `__schema` and `__type` could get automatically added to the query type,
+   * but that would require mutating type definitions, which would cause issues.
+   */
+  getField(
+    parentType: GraphQLCompositeType,
+    fieldName: string,
+  ): GraphQLField<unknown, unknown> | undefined {
+    switch (fieldName) {
+      case SchemaMetaFieldDef.name:
+        return this.getQueryType() === parentType
+          ? SchemaMetaFieldDef
+          : undefined;
+      case TypeMetaFieldDef.name:
+        return this.getQueryType() === parentType
+          ? TypeMetaFieldDef
+          : undefined;
+      case TypeNameMetaFieldDef.name:
+        return TypeNameMetaFieldDef;
+    }
+
+    // this function is part "hot" path inside executor and check presence
+    // of 'getFields' is faster than to use `!isUnionType`
+    if ('getFields' in parentType) {
+      return parentType.getFields()[fieldName];
+    }
+    return undefined;
+  }
+
   toConfig(): GraphQLSchemaNormalizedConfig {
     return {
       description: this.description,
diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts
index e72dfb01..aff849ff 100644
--- a/src/utilities/TypeInfo.ts
+++ b/src/utilities/TypeInfo.ts
@@ -23,17 +23,11 @@ import {
   isEnumType,
   isInputObjectType,
   isInputType,
-  isInterfaceType,
   isListType,
   isObjectType,
   isOutputType,
 } from '../type/definition';
 import type { GraphQLDirective } from '../type/directives';
-import {
-  SchemaMetaFieldDef,
-  TypeMetaFieldDef,
-  TypeNameMetaFieldDef,
-} from '../type/introspection';
 import type { GraphQLSchema } from '../type/schema';
 
 import { typeFromAST } from './typeFromAST';
@@ -293,36 +287,16 @@ export class TypeInfo {
 
 type GetFieldDefFn = (
   schema: GraphQLSchema,
-  parentType: GraphQLType,
+  parentType: GraphQLCompositeType,
   fieldNode: FieldNode,
 ) => Maybe<GraphQLField<unknown, unknown>>;
 
-/**
- * Not exactly the same as the executor's definition of getFieldDef, in this
- * statically evaluated environment we do not always have an Object type,
- * and need to handle Interface and Union types.
- */
 function getFieldDef(
   schema: GraphQLSchema,
-  parentType: GraphQLType,
+  parentType: GraphQLCompositeType,
   fieldNode: FieldNode,
-): Maybe<GraphQLField<unknown, unknown>> {
-  const name = fieldNode.name.value;
-  if (
-    name === SchemaMetaFieldDef.name &&
-    schema.getQueryType() === parentType
-  ) {
-    return SchemaMetaFieldDef;
-  }
-  if (name === TypeMetaFieldDef.name && schema.getQueryType() === parentType) {
-    return TypeMetaFieldDef;
-  }
-  if (name === TypeNameMetaFieldDef.name && isCompositeType(parentType)) {
-    return TypeNameMetaFieldDef;
-  }
-  if (isObjectType(parentType) || isInterfaceType(parentType)) {
-    return parentType.getFields()[name];
-  }
+) {
+  return schema.getField(parentType, fieldNode.name.value);
 }
 
 /**


Test patch:
diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts
index 8a31b50a..652bfeea 100644
--- a/src/type/__tests__/schema-test.ts
+++ b/src/type/__tests__/schema-test.ts
@@ -7,14 +7,21 @@ import { DirectiveLocation } from '../../language/directiveLocation';
 
 import { printSchema } from '../../utilities/printSchema';
 
+import type { GraphQLCompositeType } from '../definition';
 import {
   GraphQLInputObjectType,
   GraphQLInterfaceType,
   GraphQLList,
   GraphQLObjectType,
   GraphQLScalarType,
+  GraphQLUnionType,
 } from '../definition';
 import { GraphQLDirective } from '../directives';
+import {
+  SchemaMetaFieldDef,
+  TypeMetaFieldDef,
+  TypeNameMetaFieldDef,
+} from '../introspection';
 import { GraphQLBoolean, GraphQLInt, GraphQLString } from '../scalars';
 import { GraphQLSchema } from '../schema';
 
@@ -319,6 +326,109 @@ describe('Type System: Schema', () => {
     );
   });
 
+  describe('getField', () => {
+    const petType = new GraphQLInterfaceType({
+      name: 'Pet',
+      fields: {
+        name: { type: GraphQLString },
+      },
+    });
+
+    const catType = new GraphQLObjectType({
+      name: 'Cat',
+      interfaces: [petType],
+      fields: {
+        name: { type: GraphQLString },
+      },
+    });
+
+    const dogType = new GraphQLObjectType({
+      name: 'Dog',
+      interfaces: [petType],
+      fields: {
+        name: { type: GraphQLString },
+      },
+    });
+
+    const catOrDog = new GraphQLUnionType({
+      name: 'CatOrDog',
+      types: [catType, dogType],
+    });
+
+    const queryType = new GraphQLObjectType({
+      name: 'Query',
+      fields: {
+        catOrDog: { type: catOrDog },
+      },
+    });
+
+    const mutationType = new GraphQLObjectType({
+      name: 'Mutation',
+      fields: {},
+    });
+
+    const subscriptionType = new GraphQLObjectType({
+      name: 'Subscription',
+      fields: {},
+    });
+
+    const schema = new GraphQLSchema({
+      query: queryType,
+      mutation: mutationType,
+      subscription: subscriptionType,
+    });
+
+    function expectField(parentType: GraphQLCompositeType, name: string) {
+      return expect(schema.getField(parentType, name));
+    }
+
+    it('returns known fields', () => {
+      expectField(petType, 'name').to.equal(petType.getFields().name);
+      expectField(catType, 'name').to.equal(catType.getFields().name);
+
+      expectField(queryType, 'catOrDog').to.equal(
+        queryType.getFields().catOrDog,
+      );
+    });
+
+    it('returns `undefined` for unknown fields', () => {
+      expectField(catOrDog, 'name').to.equal(undefined);
+
+      expectField(queryType, 'unknown').to.equal(undefined);
+      expectField(petType, 'unknown').to.equal(undefined);
+      expectField(catType, 'unknown').to.equal(undefined);
+      expectField(catOrDog, 'unknown').to.equal(undefined);
+    });
+
+    it('handles introspection fields', () => {
+      expectField(queryType, '__typename').to.equal(TypeNameMetaFieldDef);
+      expectField(mutationType, '__typename').to.equal(TypeNameMetaFieldDef);
+      expectField(subscriptionType, '__typename').to.equal(
+        TypeNameMetaFieldDef,
+      );
+
+      expectField(petType, '__typename').to.equal(TypeNameMetaFieldDef);
+      expectField(catType, '__typename').to.equal(TypeNameMetaFieldDef);
+      expectField(dogType, '__typename').to.equal(TypeNameMetaFieldDef);
+      expectField(catOrDog, '__typename').to.equal(TypeNameMetaFieldDef);
+
+      expectField(queryType, '__type').to.equal(TypeMetaFieldDef);
+      expectField(queryType, '__schema').to.equal(SchemaMetaFieldDef);
+    });
+
+    it('returns `undefined` for introspection fields in wrong location', () => {
+      expect(schema.getField(petType, '__type')).to.equal(undefined);
+      expect(schema.getField(dogType, '__type')).to.equal(undefined);
+      expect(schema.getField(mutationType, '__type')).to.equal(undefined);
+      expect(schema.getField(subscriptionType, '__type')).to.equal(undefined);
+
+      expect(schema.getField(petType, '__schema')).to.equal(undefined);
+      expect(schema.getField(dogType, '__schema')).to.equal(undefined);
+      expect(schema.getField(mutationType, '__schema')).to.equal(undefined);
+      expect(schema.getField(subscriptionType, '__schema')).to.equal(undefined);
+    });
+  });
+
   describe('Validity', () => {
     describe('when not assumed valid', () => {
       it('configures the schema to still needing validation', () => {
diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts
index 40a0d4b4..154ee251 100644
--- a/src/utilities/__tests__/TypeInfo-test.ts
+++ b/src/utilities/__tests__/TypeInfo-test.ts
@@ -33,9 +33,13 @@ const testSchema = buildSchema(`
     name(surname: Boolean): String
   }
 
+  union HumanOrAlien = Human | Alien
+
   type QueryRoot {
     human(id: ID): Human
     alien: Alien
+    humanOrAlien: HumanOrAlien
+    pet: Pet
   }
 
   schema {
@@ -146,6 +150,62 @@ describe('visitWithTypeInfo', () => {
     expect(visitorArgs).to.deep.equal(wrappedVisitorArgs);
   });
 
+  it('supports introspection fields', () => {
+    const typeInfo = new TypeInfo(testSchema);
+
+    const ast = parse(`
+      {
+        __typename
+        __type(name: "Cat") { __typename }
+        __schema {
+          __typename # in object type
+        }
+        humanOrAlien {
+          __typename # in union type
+        }
+        pet {
+          __typename # in interface type
+        }
+        someUnknownType {
+          __typename # unknown
+        }
+        pet {
+          __type # unknown
+          __schema # unknown
+        }
+      }
+    `);
+
+    const visitedFields: Array<[string | undefined, string | undefined]> = [];
+    visit(
+      ast,
+      visitWithTypeInfo(typeInfo, {
+        Field() {
+          const typeName = typeInfo.getParentType()?.name;
+          const fieldName = typeInfo.getFieldDef()?.name;
+          visitedFields.push([typeName, fieldName]);
+        },
+      }),
+    );
+
+    expect(visitedFields).to.deep.equal([
+      ['QueryRoot', '__typename'],
+      ['QueryRoot', '__type'],
+      ['__Type', '__typename'],
+      ['QueryRoot', '__schema'],
+      ['__Schema', '__typename'],
+      ['QueryRoot', 'humanOrAlien'],
+      ['HumanOrAlien', '__typename'],
+      ['QueryRoot', 'pet'],
+      ['Pet', '__typename'],
+      ['QueryRoot', undefined],
+      [undefined, undefined],
+      ['QueryRoot', 'pet'],
+      ['Pet', undefined],
+      ['Pet', undefined],
+    ]);
+  });
+
   it('maintains type info during visit', () => {
     const visited: Array<any> = [];