graphql
Version:
A Query Language and Runtime which can target any service.
317 lines (298 loc) • 9.68 kB
Flow
// @flow strict
import find from '../polyfills/find';
import { Kind } from '../language/kinds';
import { type ASTNode, type FieldNode } from '../language/ast';
import { type GraphQLSchema } from '../type/schema';
import { type GraphQLDirective } from '../type/directives';
import {
type GraphQLType,
type GraphQLInputType,
type GraphQLOutputType,
type GraphQLCompositeType,
type GraphQLField,
type GraphQLArgument,
type GraphQLInputField,
type GraphQLEnumValue,
isObjectType,
isInterfaceType,
isEnumType,
isInputObjectType,
isListType,
isCompositeType,
isInputType,
isOutputType,
getNullableType,
getNamedType,
} from '../type/definition';
import {
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
} from '../type/introspection';
import { typeFromAST } from './typeFromAST';
/**
* TypeInfo is a utility class which, given a GraphQL schema, can keep track
* of the current field and type definitions at any point in a GraphQL document
* AST during a recursive descent by calling `enter(node)` and `leave(node)`.
*/
export class TypeInfo {
_schema: GraphQLSchema;
_typeStack: Array<?GraphQLOutputType>;
_parentTypeStack: Array<?GraphQLCompositeType>;
_inputTypeStack: Array<?GraphQLInputType>;
_fieldDefStack: Array<?GraphQLField<mixed, mixed>>;
_defaultValueStack: Array<?mixed>;
_directive: ?GraphQLDirective;
_argument: ?GraphQLArgument;
_enumValue: ?GraphQLEnumValue;
_getFieldDef: typeof getFieldDef;
constructor(
schema: GraphQLSchema,
// NOTE: this experimental optional second parameter is only needed in order
// to support non-spec-compliant codebases. You should never need to use it.
// It may disappear in the future.
getFieldDefFn?: typeof getFieldDef,
// Initial type may be provided in rare cases to facilitate traversals
// beginning somewhere other than documents.
initialType?: GraphQLType,
): void {
this._schema = schema;
this._typeStack = [];
this._parentTypeStack = [];
this._inputTypeStack = [];
this._fieldDefStack = [];
this._defaultValueStack = [];
this._directive = null;
this._argument = null;
this._enumValue = null;
this._getFieldDef = getFieldDefFn || getFieldDef;
if (initialType) {
if (isInputType(initialType)) {
this._inputTypeStack.push(initialType);
}
if (isCompositeType(initialType)) {
this._parentTypeStack.push(initialType);
}
if (isOutputType(initialType)) {
this._typeStack.push(initialType);
}
}
}
getType(): ?GraphQLOutputType {
if (this._typeStack.length > 0) {
return this._typeStack[this._typeStack.length - 1];
}
}
getParentType(): ?GraphQLCompositeType {
if (this._parentTypeStack.length > 0) {
return this._parentTypeStack[this._parentTypeStack.length - 1];
}
}
getInputType(): ?GraphQLInputType {
if (this._inputTypeStack.length > 0) {
return this._inputTypeStack[this._inputTypeStack.length - 1];
}
}
getParentInputType(): ?GraphQLInputType {
if (this._inputTypeStack.length > 1) {
return this._inputTypeStack[this._inputTypeStack.length - 2];
}
}
getFieldDef(): ?GraphQLField<mixed, mixed> {
if (this._fieldDefStack.length > 0) {
return this._fieldDefStack[this._fieldDefStack.length - 1];
}
}
getDefaultValue(): ?mixed {
if (this._defaultValueStack.length > 0) {
return this._defaultValueStack[this._defaultValueStack.length - 1];
}
}
getDirective(): ?GraphQLDirective {
return this._directive;
}
getArgument(): ?GraphQLArgument {
return this._argument;
}
getEnumValue(): ?GraphQLEnumValue {
return this._enumValue;
}
enter(node: ASTNode) {
const schema = this._schema;
// Note: many of the types below are explicitly typed as "mixed" to drop
// any assumptions of a valid schema to ensure runtime types are properly
// checked before continuing since TypeInfo is used as part of validation
// which occurs before guarantees of schema and document validity.
switch (node.kind) {
case Kind.SELECTION_SET: {
const namedType: mixed = getNamedType(this.getType());
this._parentTypeStack.push(
isCompositeType(namedType) ? namedType : undefined,
);
break;
}
case Kind.FIELD: {
const parentType = this.getParentType();
let fieldDef;
let fieldType: mixed;
if (parentType) {
fieldDef = this._getFieldDef(schema, parentType, node);
if (fieldDef) {
fieldType = fieldDef.type;
}
}
this._fieldDefStack.push(fieldDef);
this._typeStack.push(isOutputType(fieldType) ? fieldType : undefined);
break;
}
case Kind.DIRECTIVE:
this._directive = schema.getDirective(node.name.value);
break;
case Kind.OPERATION_DEFINITION: {
let type: mixed;
if (node.operation === 'query') {
type = schema.getQueryType();
} else if (node.operation === 'mutation') {
type = schema.getMutationType();
} else if (node.operation === 'subscription') {
type = schema.getSubscriptionType();
}
this._typeStack.push(isObjectType(type) ? type : undefined);
break;
}
case Kind.INLINE_FRAGMENT:
case Kind.FRAGMENT_DEFINITION: {
const typeConditionAST = node.typeCondition;
const outputType: mixed = typeConditionAST
? typeFromAST(schema, typeConditionAST)
: getNamedType(this.getType());
this._typeStack.push(isOutputType(outputType) ? outputType : undefined);
break;
}
case Kind.VARIABLE_DEFINITION: {
const inputType: mixed = typeFromAST(schema, node.type);
this._inputTypeStack.push(
isInputType(inputType) ? inputType : undefined,
);
break;
}
case Kind.ARGUMENT: {
let argDef;
let argType: mixed;
const fieldOrDirective = this.getDirective() || this.getFieldDef();
if (fieldOrDirective) {
argDef = find(
fieldOrDirective.args,
arg => arg.name === node.name.value,
);
if (argDef) {
argType = argDef.type;
}
}
this._argument = argDef;
this._defaultValueStack.push(argDef ? argDef.defaultValue : undefined);
this._inputTypeStack.push(isInputType(argType) ? argType : undefined);
break;
}
case Kind.LIST: {
const listType: mixed = getNullableType(this.getInputType());
const itemType: mixed = isListType(listType)
? listType.ofType
: listType;
// List positions never have a default value.
this._defaultValueStack.push(undefined);
this._inputTypeStack.push(isInputType(itemType) ? itemType : undefined);
break;
}
case Kind.OBJECT_FIELD: {
const objectType: mixed = getNamedType(this.getInputType());
let inputFieldType: GraphQLInputType | void;
let inputField: GraphQLInputField | void;
if (isInputObjectType(objectType)) {
inputField = objectType.getFields()[node.name.value];
if (inputField) {
inputFieldType = inputField.type;
}
}
this._defaultValueStack.push(
inputField ? inputField.defaultValue : undefined,
);
this._inputTypeStack.push(
isInputType(inputFieldType) ? inputFieldType : undefined,
);
break;
}
case Kind.ENUM: {
const enumType: mixed = getNamedType(this.getInputType());
let enumValue;
if (isEnumType(enumType)) {
enumValue = enumType.getValue(node.value);
}
this._enumValue = enumValue;
break;
}
}
}
leave(node: ASTNode) {
switch (node.kind) {
case Kind.SELECTION_SET:
this._parentTypeStack.pop();
break;
case Kind.FIELD:
this._fieldDefStack.pop();
this._typeStack.pop();
break;
case Kind.DIRECTIVE:
this._directive = null;
break;
case Kind.OPERATION_DEFINITION:
case Kind.INLINE_FRAGMENT:
case Kind.FRAGMENT_DEFINITION:
this._typeStack.pop();
break;
case Kind.VARIABLE_DEFINITION:
this._inputTypeStack.pop();
break;
case Kind.ARGUMENT:
this._argument = null;
this._defaultValueStack.pop();
this._inputTypeStack.pop();
break;
case Kind.LIST:
case Kind.OBJECT_FIELD:
this._defaultValueStack.pop();
this._inputTypeStack.pop();
break;
case Kind.ENUM:
this._enumValue = null;
break;
}
}
}
/**
* 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,
fieldNode: FieldNode,
): ?GraphQLField<mixed, mixed> {
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];
}
}