From 69e155474f06b7715e172d5c75c0f3e5ad819e21 Mon Sep 17 00:00:00 2001 From: Ivan Goncharov Date: Fri, 27 May 2022 22:19:40 +0300 Subject: [PATCH] Add new 'GraphQLSchema.getField' method (#3605) --- src/execution/execute.ts | 42 +-------- src/execution/subscribe.ts | 5 +- src/type/__tests__/schema-test.ts | 110 +++++++++++++++++++++++ src/type/schema.ts | 45 +++++++++- src/utilities/TypeInfo.ts | 34 +------ src/utilities/__tests__/TypeInfo-test.ts | 60 +++++++++++++ 6 files changed, 222 insertions(+), 74 deletions(-) diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 3261f51184..6dc4246178 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, path: Path, ): PromiseOrValue { - 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 = 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> { - 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 0b240b3fd7..7a04480bf4 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/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index 8a31b50ada..652bfeeae6 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/type/schema.ts b/src/type/schema.ts index 6b44e1fc1a..7cc576a18f 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 | 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 e72dfb01fb..aff849ff64 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>; -/** - * 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> { - 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); } /** diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts index 40a0d4b478..154ee2516b 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 = [];