Skip to content

Commit

Permalink
Add new 'GraphQLSchema.getField' method (#3605)
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanGoncharov authored May 27, 2022
1 parent 4f8864e commit 69e1554
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 74 deletions.
42 changes: 2 additions & 40 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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];
}
5 changes: 2 additions & 3 deletions src/execution/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
buildExecutionContext,
buildResolveInfo,
execute,
getFieldDef,
} from './execute';
import { mapAsyncIterator } from './mapAsyncIterator';
import { getArgumentValues } from './values';
Expand Down Expand Up @@ -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 },
Expand Down
110 changes: 110 additions & 0 deletions src/type/__tests__/schema-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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', () => {
Expand Down
45 changes: 44 additions & 1 deletion src/type/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { OperationTypeNode } from '../language/ast';

import type {
GraphQLAbstractType,
GraphQLCompositeType,
GraphQLField,
GraphQLInterfaceType,
GraphQLNamedType,
GraphQLObjectType,
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
34 changes: 4 additions & 30 deletions src/utilities/TypeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}

/**
Expand Down
Loading

0 comments on commit 69e1554

Please sign in to comment.