graphql
Version:
A Query Language and Runtime which can target any service.
511 lines (454 loc) • 15.6 kB
Flow
/**
* Copyright (c) 2015-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 objectValues from '../polyfills/objectValues';
import invariant from '../jsutils/invariant';
import keyMap from '../jsutils/keyMap';
import keyValMap from '../jsutils/keyValMap';
import type { ObjMap } from '../jsutils/ObjMap';
import { valueFromAST } from './valueFromAST';
import { assertValidSDL } from '../validation/validate';
import blockStringValue from '../language/blockStringValue';
import { TokenKind } from '../language/lexer';
import { parse } from '../language/parser';
import type { ParseOptions } from '../language/parser';
import type { Source } from '../language/source';
import { getDirectiveValues } from '../execution/values';
import { Kind } from '../language/kinds';
import type {
DocumentNode,
NameNode,
TypeNode,
NamedTypeNode,
SchemaDefinitionNode,
TypeDefinitionNode,
ScalarTypeDefinitionNode,
ObjectTypeDefinitionNode,
FieldDefinitionNode,
InputValueDefinitionNode,
InterfaceTypeDefinitionNode,
UnionTypeDefinitionNode,
EnumTypeDefinitionNode,
EnumValueDefinitionNode,
InputObjectTypeDefinitionNode,
DirectiveDefinitionNode,
StringValueNode,
Location,
} from '../language/ast';
import { isTypeDefinitionNode } from '../language/predicates';
import type { DirectiveLocationEnum } from '../language/directiveLocation';
import type {
GraphQLType,
GraphQLNamedType,
GraphQLFieldConfig,
GraphQLArgumentConfig,
GraphQLEnumValueConfig,
GraphQLInputFieldConfig,
} from '../type/definition';
import {
GraphQLScalarType,
GraphQLObjectType,
GraphQLInterfaceType,
GraphQLUnionType,
GraphQLEnumType,
GraphQLInputObjectType,
GraphQLList,
GraphQLNonNull,
} from '../type/definition';
import {
GraphQLDirective,
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLDeprecatedDirective,
} from '../type/directives';
import { introspectionTypes } from '../type/introspection';
import { specifiedScalarTypes } from '../type/scalars';
import { GraphQLSchema } from '../type/schema';
import type { GraphQLSchemaValidationOptions } from '../type/schema';
export type BuildSchemaOptions = {
...GraphQLSchemaValidationOptions,
/**
* Descriptions are defined as preceding string literals, however an older
* experimental version of the SDL supported preceding comments as
* descriptions. Set to true to enable this deprecated behavior.
* This option is provided to ease adoption and will be removed in v16.
*
* Default: false
*/
commentDescriptions?: boolean,
/**
* Set to true to assume the SDL is valid.
*
* Default: false
*/
assumeValidSDL?: boolean,
};
/**
* This takes the ast of a schema document produced by the parse function in
* src/language/parser.js.
*
* If no schema definition is provided, then it will look for types named Query
* and Mutation.
*
* Given that AST it constructs a GraphQLSchema. The resulting schema
* has no resolve methods, so execution will use default resolvers.
*
* Accepts options as a second argument:
*
* - commentDescriptions:
* Provide true to use preceding comments as the description.
*
*/
export function buildASTSchema(
documentAST: DocumentNode,
options?: BuildSchemaOptions,
): GraphQLSchema {
invariant(
documentAST && documentAST.kind === Kind.DOCUMENT,
'Must provide valid Document AST',
);
if (!options || !(options.assumeValid || options.assumeValidSDL)) {
assertValidSDL(documentAST);
}
let schemaDef: ?SchemaDefinitionNode;
const nodeMap: ObjMap<TypeDefinitionNode> = Object.create(null);
const directiveDefs: Array<DirectiveDefinitionNode> = [];
for (const def of documentAST.definitions) {
if (def.kind === Kind.SCHEMA_DEFINITION) {
schemaDef = def;
} else if (isTypeDefinitionNode(def)) {
nodeMap[def.name.value] = def;
} else if (def.kind === Kind.DIRECTIVE_DEFINITION) {
directiveDefs.push(def);
}
}
const operationTypes = schemaDef
? getOperationTypes(schemaDef)
: {
query: nodeMap.Query,
mutation: nodeMap.Mutation,
subscription: nodeMap.Subscription,
};
const definitionBuilder = new ASTDefinitionBuilder(
nodeMap,
options,
typeName => {
throw new Error(`Type "${typeName}" not found in document.`);
},
);
const directives = directiveDefs.map(def =>
definitionBuilder.buildDirective(def),
);
// If specified directives were not explicitly declared, add them.
if (!directives.some(directive => directive.name === 'skip')) {
directives.push(GraphQLSkipDirective);
}
if (!directives.some(directive => directive.name === 'include')) {
directives.push(GraphQLIncludeDirective);
}
if (!directives.some(directive => directive.name === 'deprecated')) {
directives.push(GraphQLDeprecatedDirective);
}
// Note: While this could make early assertions to get the correctly
// typed values below, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
return new GraphQLSchema({
query: operationTypes.query
? (definitionBuilder.buildType(operationTypes.query): any)
: null,
mutation: operationTypes.mutation
? (definitionBuilder.buildType(operationTypes.mutation): any)
: null,
subscription: operationTypes.subscription
? (definitionBuilder.buildType(operationTypes.subscription): any)
: null,
types: objectValues(nodeMap).map(node => definitionBuilder.buildType(node)),
directives,
astNode: schemaDef,
assumeValid: options && options.assumeValid,
allowedLegacyNames: options && options.allowedLegacyNames,
});
function getOperationTypes(schema: SchemaDefinitionNode) {
const opTypes = {};
for (const operationType of schema.operationTypes) {
opTypes[operationType.operation] = operationType.type;
}
return opTypes;
}
}
type TypeDefinitionsMap = ObjMap<TypeDefinitionNode>;
type TypeResolver = (typeName: string) => GraphQLNamedType;
export class ASTDefinitionBuilder {
_typeDefinitionsMap: TypeDefinitionsMap;
_options: ?BuildSchemaOptions;
_resolveType: TypeResolver;
_cache: ObjMap<GraphQLNamedType>;
constructor(
typeDefinitionsMap: TypeDefinitionsMap,
options: ?BuildSchemaOptions,
resolveType: TypeResolver,
) {
this._typeDefinitionsMap = typeDefinitionsMap;
this._options = options;
this._resolveType = resolveType;
// Initialize to the GraphQL built in scalars and introspection types.
this._cache = keyMap(
specifiedScalarTypes.concat(introspectionTypes),
type => type.name,
);
}
buildType(node: NamedTypeNode | TypeDefinitionNode): GraphQLNamedType {
const typeName = node.name.value;
if (!this._cache[typeName]) {
if (node.kind === Kind.NAMED_TYPE) {
const defNode = this._typeDefinitionsMap[typeName];
this._cache[typeName] = defNode
? this._makeSchemaDef(defNode)
: this._resolveType(node.name.value);
} else {
this._cache[typeName] = this._makeSchemaDef(node);
}
}
return this._cache[typeName];
}
_buildWrappedType(typeNode: TypeNode): GraphQLType {
if (typeNode.kind === Kind.LIST_TYPE) {
return GraphQLList(this._buildWrappedType(typeNode.type));
}
if (typeNode.kind === Kind.NON_NULL_TYPE) {
return GraphQLNonNull(
// Note: GraphQLNonNull constructor validates this type
(this._buildWrappedType(typeNode.type): any),
);
}
return this.buildType(typeNode);
}
buildDirective(directive: DirectiveDefinitionNode): GraphQLDirective {
const locations = directive.locations.map(
({ value }) => ((value: any): DirectiveLocationEnum),
);
return new GraphQLDirective({
name: directive.name.value,
description: getDescription(directive, this._options),
locations,
args: keyByNameNode(directive.arguments || [], arg => this.buildArg(arg)),
astNode: directive,
});
}
buildField(field: FieldDefinitionNode): GraphQLFieldConfig<mixed, mixed> {
return {
// Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
type: (this._buildWrappedType(field.type): any),
description: getDescription(field, this._options),
args: keyByNameNode(field.arguments || [], arg => this.buildArg(arg)),
deprecationReason: getDeprecationReason(field),
astNode: field,
};
}
buildArg(value: InputValueDefinitionNode): GraphQLArgumentConfig {
// Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
const type: any = this._buildWrappedType(value.type);
return {
type,
description: getDescription(value, this._options),
defaultValue: valueFromAST(value.defaultValue, type),
astNode: value,
};
}
buildInputField(value: InputValueDefinitionNode): GraphQLInputFieldConfig {
// Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
const type: any = this._buildWrappedType(value.type);
return {
type,
description: getDescription(value, this._options),
defaultValue: valueFromAST(value.defaultValue, type),
astNode: value,
};
}
buildEnumValue(value: EnumValueDefinitionNode): GraphQLEnumValueConfig {
return {
description: getDescription(value, this._options),
deprecationReason: getDeprecationReason(value),
astNode: value,
};
}
_makeSchemaDef(astNode: TypeDefinitionNode): GraphQLNamedType {
switch (astNode.kind) {
case Kind.OBJECT_TYPE_DEFINITION:
return this._makeTypeDef(astNode);
case Kind.INTERFACE_TYPE_DEFINITION:
return this._makeInterfaceDef(astNode);
case Kind.ENUM_TYPE_DEFINITION:
return this._makeEnumDef(astNode);
case Kind.UNION_TYPE_DEFINITION:
return this._makeUnionDef(astNode);
case Kind.SCALAR_TYPE_DEFINITION:
return this._makeScalarDef(astNode);
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
return this._makeInputObjectDef(astNode);
default:
throw new Error(`Type kind "${astNode.kind}" not supported.`);
}
}
_makeTypeDef(astNode: ObjectTypeDefinitionNode) {
const interfaceNodes = astNode.interfaces;
const fieldNodes = astNode.fields;
// Note: While this could make assertions to get the correctly typed
// values below, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
const interfaces =
interfaceNodes && interfaceNodes.length > 0
? () => interfaceNodes.map(ref => (this.buildType(ref): any))
: [];
const fields =
fieldNodes && fieldNodes.length > 0
? () => keyByNameNode(fieldNodes, field => this.buildField(field))
: Object.create(null);
return new GraphQLObjectType({
name: astNode.name.value,
description: getDescription(astNode, this._options),
interfaces,
fields,
astNode,
});
}
_makeInterfaceDef(astNode: InterfaceTypeDefinitionNode) {
const fieldNodes = astNode.fields;
const fields =
fieldNodes && fieldNodes.length > 0
? () => keyByNameNode(fieldNodes, field => this.buildField(field))
: Object.create(null);
return new GraphQLInterfaceType({
name: astNode.name.value,
description: getDescription(astNode, this._options),
fields,
astNode,
});
}
_makeEnumDef(astNode: EnumTypeDefinitionNode) {
const valueNodes = astNode.values || [];
return new GraphQLEnumType({
name: astNode.name.value,
description: getDescription(astNode, this._options),
values: keyByNameNode(valueNodes, value => this.buildEnumValue(value)),
astNode,
});
}
_makeUnionDef(astNode: UnionTypeDefinitionNode) {
const typeNodes = astNode.types;
// Note: While this could make assertions to get the correctly typed
// values below, that would throw immediately while type system
// validation with validateSchema() will produce more actionable results.
const types =
typeNodes && typeNodes.length > 0
? () => typeNodes.map(ref => (this.buildType(ref): any))
: [];
return new GraphQLUnionType({
name: astNode.name.value,
description: getDescription(astNode, this._options),
types,
astNode,
});
}
_makeScalarDef(astNode: ScalarTypeDefinitionNode) {
return new GraphQLScalarType({
name: astNode.name.value,
description: getDescription(astNode, this._options),
astNode,
serialize: value => value,
});
}
_makeInputObjectDef(def: InputObjectTypeDefinitionNode) {
const { fields } = def;
return new GraphQLInputObjectType({
name: def.name.value,
description: getDescription(def, this._options),
fields: fields
? () => keyByNameNode(fields, field => this.buildInputField(field))
: Object.create(null),
astNode: def,
});
}
}
function keyByNameNode<T: { +name: NameNode }, V>(
list: $ReadOnlyArray<T>,
valFn: (item: T) => V,
): ObjMap<V> {
return keyValMap(list, ({ name }) => name.value, valFn);
}
/**
* Given a field or enum value node, returns the string value for the
* deprecation reason.
*/
function getDeprecationReason(
node: EnumValueDefinitionNode | FieldDefinitionNode,
): ?string {
const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node);
return deprecated && (deprecated.reason: any);
}
/**
* Given an ast node, returns its string description.
* @deprecated: provided to ease adoption and will be removed in v16.
*
* Accepts options as a second argument:
*
* - commentDescriptions:
* Provide true to use preceding comments as the description.
*
*/
export function getDescription(
node: { +description?: StringValueNode, +loc?: Location },
options: ?BuildSchemaOptions,
): void | string {
if (node.description) {
return node.description.value;
}
if (options && options.commentDescriptions) {
const rawValue = getLeadingCommentBlock(node);
if (rawValue !== undefined) {
return blockStringValue('\n' + rawValue);
}
}
}
function getLeadingCommentBlock(node): void | string {
const loc = node.loc;
if (!loc) {
return;
}
const comments = [];
let token = loc.startToken.prev;
while (
token &&
token.kind === TokenKind.COMMENT &&
token.next &&
token.prev &&
token.line + 1 === token.next.line &&
token.line !== token.prev.line
) {
const value = String(token.value);
comments.push(value);
token = token.prev;
}
return comments.reverse().join('\n');
}
/**
* A helper function to build a GraphQLSchema directly from a source
* document.
*/
export function buildSchema(
source: string | Source,
options?: BuildSchemaOptions & ParseOptions,
): GraphQLSchema {
return buildASTSchema(parse(source, options), options);
}