diff --git a/src/index.js b/src/index.js index 2dd2bbb133..7a8a2f6981 100644 --- a/src/index.js +++ b/src/index.js @@ -189,6 +189,16 @@ export { TokenKind, DirectiveLocation, BREAK, + // Predicates + isDefinitionNode, + isExecutableDefinitionNode, + isSelectionNode, + isValueNode, + isTypeNode, + isTypeSystemDefinitionNode, + isTypeDefinitionNode, + isTypeSystemExtensionNode, + isTypeExtensionNode, } from './language'; export type { diff --git a/src/language/index.js b/src/language/index.js index e49e8917d1..8054718aa9 100644 --- a/src/language/index.js +++ b/src/language/index.js @@ -88,5 +88,17 @@ export type { InputObjectTypeExtensionNode, } from './ast'; +export { + isDefinitionNode, + isExecutableDefinitionNode, + isSelectionNode, + isValueNode, + isTypeNode, + isTypeSystemDefinitionNode, + isTypeDefinitionNode, + isTypeSystemExtensionNode, + isTypeExtensionNode, +} from './predicates'; + export { DirectiveLocation } from './directiveLocation'; export type { DirectiveLocationEnum } from './directiveLocation'; diff --git a/src/language/predicates.js b/src/language/predicates.js new file mode 100644 index 0000000000..9b62132ace --- /dev/null +++ b/src/language/predicates.js @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2018-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import type { ASTNode } from './ast'; +import { Kind } from './kinds'; + +export function isDefinitionNode(node: ASTNode): boolean %checks { + return ( + isExecutableDefinitionNode(node) || + isTypeSystemDefinitionNode(node) || + isTypeSystemExtensionNode(node) + ); +} + +export function isExecutableDefinitionNode(node: ASTNode): boolean %checks { + return ( + node.kind === Kind.OPERATION_DEFINITION || + node.kind === Kind.FRAGMENT_DEFINITION + ); +} + +export function isSelectionNode(node: ASTNode): boolean %checks { + return ( + node.kind === Kind.FIELD || + node.kind === Kind.FRAGMENT_SPREAD || + node.kind === Kind.INLINE_FRAGMENT + ); +} + +export function isValueNode(node: ASTNode): boolean %checks { + return ( + node.kind === Kind.INT || + node.kind === Kind.FLOAT || + node.kind === Kind.STRING || + node.kind === Kind.BOOLEAN || + node.kind === Kind.NULL || + node.kind === Kind.ENUM || + node.kind === Kind.LIST || + node.kind === Kind.OBJECT || + node.kind === Kind.OBJECT_FIELD + ); +} + +export function isTypeNode(node: ASTNode): boolean %checks { + return ( + node.kind === Kind.NAMED_TYPE || + node.kind === Kind.LIST_TYPE || + node.kind === Kind.NON_NULL_TYPE + ); +} + +export function isTypeSystemDefinitionNode(node: ASTNode): boolean %checks { + return ( + node.kind === Kind.SCHEMA_DEFINITION || + isTypeDefinitionNode(node) || + node.kind === Kind.DIRECTIVE_DEFINITION + ); +} + +export function isTypeDefinitionNode(node: ASTNode): boolean %checks { + return ( + node.kind === Kind.SCALAR_TYPE_DEFINITION || + node.kind === Kind.OBJECT_TYPE_DEFINITION || + node.kind === Kind.INTERFACE_TYPE_DEFINITION || + node.kind === Kind.UNION_TYPE_DEFINITION || + node.kind === Kind.ENUM_TYPE_DEFINITION || + node.kind === Kind.INPUT_OBJECT_TYPE_DEFINITION + ); +} + +export function isTypeSystemExtensionNode(node: ASTNode): boolean %checks { + return node.kind === Kind.SCHEMA_EXTENSION || isTypeExtensionNode(node); +} + +export function isTypeExtensionNode(node: ASTNode): boolean %checks { + return ( + node.kind === Kind.SCALAR_TYPE_EXTENSION || + node.kind === Kind.OBJECT_TYPE_EXTENSION || + node.kind === Kind.INTERFACE_TYPE_EXTENSION || + node.kind === Kind.UNION_TYPE_EXTENSION || + node.kind === Kind.ENUM_TYPE_EXTENSION || + node.kind === Kind.INPUT_OBJECT_TYPE_EXTENSION + ); +} diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 8b79af1c31..2319e740b3 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -40,6 +40,7 @@ import type { StringValueNode, Location, } from '../language/ast'; +import { isTypeDefinitionNode } from '../language/predicates'; import type { DirectiveLocationEnum } from '../language/directiveLocation'; @@ -132,27 +133,18 @@ export function buildASTSchema( const nodeMap: ObjMap = Object.create(null); const directiveDefs: Array = []; for (let i = 0; i < documentAST.definitions.length; i++) { - const d = documentAST.definitions[i]; - switch (d.kind) { - case Kind.SCHEMA_DEFINITION: - schemaDef = d; - break; - case Kind.SCALAR_TYPE_DEFINITION: - case Kind.OBJECT_TYPE_DEFINITION: - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.ENUM_TYPE_DEFINITION: - case Kind.UNION_TYPE_DEFINITION: - case Kind.INPUT_OBJECT_TYPE_DEFINITION: - const typeName = d.name.value; - if (nodeMap[typeName]) { - throw new Error(`Type "${typeName}" was defined more than once.`); - } - typeDefs.push(d); - nodeMap[typeName] = d; - break; - case Kind.DIRECTIVE_DEFINITION: - directiveDefs.push(d); - break; + const def = documentAST.definitions[i]; + if (def.kind === Kind.SCHEMA_DEFINITION) { + schemaDef = def; + } else if (isTypeDefinitionNode(def)) { + const typeName = def.name.value; + if (nodeMap[typeName]) { + throw new Error(`Type "${typeName}" was defined more than once.`); + } + typeDefs.push(def); + nodeMap[typeName] = def; + } else if (def.kind === Kind.DIRECTIVE_DEFINITION) { + directiveDefs.push(def); } } diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 7be0a0d2c0..ffe0f9ed40 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -56,6 +56,10 @@ import type { SchemaExtensionNode, SchemaDefinitionNode, } from '../language/ast'; +import { + isTypeDefinitionNode, + isTypeExtensionNode, +} from '../language/predicates'; type Options = {| ...GraphQLSchemaValidationOptions, @@ -125,67 +129,51 @@ export function extendSchema( for (let i = 0; i < documentAST.definitions.length; i++) { const def = documentAST.definitions[i]; - switch (def.kind) { - case Kind.SCHEMA_DEFINITION: - schemaDef = def; - break; - case Kind.SCHEMA_EXTENSION: - schemaExtensions.push(def); - break; - case Kind.OBJECT_TYPE_DEFINITION: - case Kind.INTERFACE_TYPE_DEFINITION: - case Kind.ENUM_TYPE_DEFINITION: - case Kind.UNION_TYPE_DEFINITION: - case Kind.SCALAR_TYPE_DEFINITION: - case Kind.INPUT_OBJECT_TYPE_DEFINITION: - // Sanity check that none of the defined types conflict with the - // schema's existing types. - const typeName = def.name.value; - if (schema.getType(typeName)) { - throw new GraphQLError( - `Type "${typeName}" already exists in the schema. It cannot also ` + - 'be defined in this type definition.', - [def], - ); - } - typeDefinitionMap[typeName] = def; - break; - case Kind.SCALAR_TYPE_EXTENSION: - case Kind.OBJECT_TYPE_EXTENSION: - case Kind.INTERFACE_TYPE_EXTENSION: - case Kind.ENUM_TYPE_EXTENSION: - case Kind.INPUT_OBJECT_TYPE_EXTENSION: - case Kind.UNION_TYPE_EXTENSION: - // Sanity check that this type extension exists within the - // schema's existing types. - const extendedTypeName = def.name.value; - const existingType = schema.getType(extendedTypeName); - if (!existingType) { - throw new GraphQLError( - `Cannot extend type "${extendedTypeName}" because it does not ` + - 'exist in the existing schema.', - [def], - ); - } - checkExtensionNode(existingType, def); - - const existingTypeExtensions = typeExtensionsMap[extendedTypeName]; - typeExtensionsMap[extendedTypeName] = existingTypeExtensions - ? existingTypeExtensions.concat([def]) - : [def]; - break; - case Kind.DIRECTIVE_DEFINITION: - const directiveName = def.name.value; - const existingDirective = schema.getDirective(directiveName); - if (existingDirective) { - throw new GraphQLError( - `Directive "${directiveName}" already exists in the schema. It ` + - 'cannot be redefined.', - [def], - ); - } - directiveDefinitions.push(def); - break; + if (def.kind === Kind.SCHEMA_DEFINITION) { + schemaDef = def; + } else if (def.kind === Kind.SCHEMA_EXTENSION) { + schemaExtensions.push(def); + } else if (isTypeDefinitionNode(def)) { + // Sanity check that none of the defined types conflict with the + // schema's existing types. + const typeName = def.name.value; + if (schema.getType(typeName)) { + throw new GraphQLError( + `Type "${typeName}" already exists in the schema. It cannot also ` + + 'be defined in this type definition.', + [def], + ); + } + typeDefinitionMap[typeName] = def; + } else if (isTypeExtensionNode(def)) { + // Sanity check that this type extension exists within the + // schema's existing types. + const extendedTypeName = def.name.value; + const existingType = schema.getType(extendedTypeName); + if (!existingType) { + throw new GraphQLError( + `Cannot extend type "${extendedTypeName}" because it does not ` + + 'exist in the existing schema.', + [def], + ); + } + checkExtensionNode(existingType, def); + + const existingTypeExtensions = typeExtensionsMap[extendedTypeName]; + typeExtensionsMap[extendedTypeName] = existingTypeExtensions + ? existingTypeExtensions.concat([def]) + : [def]; + } else if (def.kind === Kind.DIRECTIVE_DEFINITION) { + const directiveName = def.name.value; + const existingDirective = schema.getDirective(directiveName); + if (existingDirective) { + throw new GraphQLError( + `Directive "${directiveName}" already exists in the schema. It ` + + 'cannot be redefined.', + [def], + ); + } + directiveDefinitions.push(def); } } diff --git a/src/validation/rules/ExecutableDefinitions.js b/src/validation/rules/ExecutableDefinitions.js index 188bbbcb29..609339b2c8 100644 --- a/src/validation/rules/ExecutableDefinitions.js +++ b/src/validation/rules/ExecutableDefinitions.js @@ -10,6 +10,7 @@ import type { ASTValidationContext } from '../ValidationContext'; import { GraphQLError } from '../../error'; import { Kind } from '../../language/kinds'; +import { isExecutableDefinitionNode } from '../../language/predicates'; import type { ASTVisitor } from '../../language/visitor'; export function nonExecutableDefinitionMessage(defName: string): string { @@ -28,10 +29,7 @@ export function ExecutableDefinitions( return { Document(node) { for (const definition of node.definitions) { - if ( - definition.kind !== Kind.OPERATION_DEFINITION && - definition.kind !== Kind.FRAGMENT_DEFINITION - ) { + if (!isExecutableDefinitionNode(definition)) { context.reportError( new GraphQLError( nonExecutableDefinitionMessage(