graphql-compose
Version:
GraphQL schema builder from different data sources with middleware extensions.
605 lines (524 loc) • 16.9 kB
Flow
/* @flow */
/* eslint-disable no-use-before-define, class-methods-use-this, no-unused-vars, no-param-reassign */
import objectPath from 'object-path';
import {
GraphQLInt,
GraphQLFloat,
GraphQLString,
GraphQLBoolean,
GraphQLScalarType,
GraphQLID,
GraphQLList,
GraphQLNonNull,
GraphQLEnumType,
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLInterfaceType,
GraphQLDirective,
GraphQLSkipDirective,
GraphQLIncludeDirective,
GraphQLDeprecatedDirective,
isOutputType,
isInputType,
valueFromAST,
} from 'graphql';
import { parse, parseType } from 'graphql/language/parser';
import {
LIST_TYPE,
NON_NULL_TYPE,
DOCUMENT,
SCHEMA_DEFINITION,
SCALAR_TYPE_DEFINITION,
OBJECT_TYPE_DEFINITION,
INTERFACE_TYPE_DEFINITION,
ENUM_TYPE_DEFINITION,
UNION_TYPE_DEFINITION,
INPUT_OBJECT_TYPE_DEFINITION,
DIRECTIVE_DEFINITION,
NAMED_TYPE,
} from 'graphql/language/kinds';
import { getDescription } from 'graphql/utilities/buildASTSchema';
import keyValMap from 'graphql/jsutils/keyValMap';
import invariant from 'graphql/jsutils/invariant';
import find from 'graphql/jsutils/find';
import { getArgumentValues } from 'graphql/execution/values';
import type {
DocumentNode,
ObjectTypeDefinitionNode,
InterfaceTypeDefinitionNode,
TypeNode,
NamedTypeNode,
DirectiveNode,
InputValueDefinitionNode,
EnumTypeDefinitionNode,
InputObjectTypeDefinitionNode,
} from 'graphql/language/ast';
import GraphQLJSON from './type/json';
import GraphQLDate from './type/date';
import type {
GraphQLType,
GraphQLNamedType,
GraphQLOutputType,
GraphQLInputType,
GraphQLNullableType,
GraphQLFieldConfigMap,
GraphQLFieldConfig,
GraphQLFieldConfigArgumentMap,
GraphQLArgumentConfig,
GraphQLInputFieldConfigMap,
GraphQLInputFieldConfig,
TypeDefinitionString,
TypeNameString,
TypeWrappedString,
} from './definition';
import TypeComposer from './typeComposer';
import InputTypeComposer from './inputTypeComposer';
import Resolver from './resolver';
const RegexpOutputTypeDefinition = /type\s[^{]+\{[^}]+\}/im;
const RegexpInputTypeDefinition = /input\s[^{]+\{[^}]+\}/im;
const RegexpEnumTypeDefinition = /enum\s[^{]+\{[^}]+\}/im;
class TypeMapper {
map: Map<string, GraphQLNamedType>;
constructor() {
this.map = new Map();
this.addBasicScalarTypes();
}
get(name: string): ?GraphQLNamedType {
return this.map.get(name);
}
set(name: string, type: GraphQLNamedType) {
this.map.set(name, type);
}
has(name: string): boolean {
return this.map.has(name);
}
delete(name: string): boolean {
return this.map.delete(name);
}
keys(): Iterator<string> {
// eslint-disable-line no-undef
return this.map.keys();
}
addBasicScalarTypes() {
this.set('String', GraphQLString);
this.set('Float', GraphQLFloat);
this.set('Int', GraphQLInt);
this.set('Boolean', GraphQLBoolean);
this.set('ID', GraphQLID);
this.set('JSON', GraphQLJSON);
this.set('Json', GraphQLJSON);
this.set('Date', GraphQLDate);
}
getWrapped(str: TypeWrappedString | TypeNameString): ?GraphQLType {
const inputTypeAST: TypeNode = parseType(str);
return typeFromAST(inputTypeAST);
}
createType(str: TypeDefinitionString): ?GraphQLNamedType {
const astDocument: DocumentNode = parse(str);
if (objectPath.get(astDocument, 'kind') !== 'Document') {
throw new Error(
'You should provide correct type syntax. ' +
"Eg. createType('type IntRange { min: Int, max: Int }')"
);
}
const types = parseTypes(astDocument);
const type = types[0];
if (type) {
this.set(type.name, type);
return type;
}
return undefined;
}
convertOutputFieldConfig<TSource, TContext>(
fieldConfig: GraphQLFieldConfig<TSource, TContext>,
fieldName: string,
typeName: string
): GraphQLFieldConfig<TSource, TContext> {
let wrapWithList = false;
if (Array.isArray(fieldConfig)) {
fieldConfig = {
type: fieldConfig,
};
}
if (Array.isArray(fieldConfig.type)) {
if (fieldConfig.type.length !== 1) {
throw new Error(
`${typeName}.${fieldName} can accept Array exact with one output type definition`
);
}
wrapWithList = true;
fieldConfig.type = fieldConfig.type[0];
}
if (fieldConfig instanceof TypeComposer) {
return { type: fieldConfig.getType() };
}
if (fieldConfig instanceof Resolver) {
return fieldConfig.getFieldConfig();
}
if (fieldConfig instanceof InputTypeComposer || fieldConfig.type instanceof InputTypeComposer) {
throw new Error(
`You cannot provide InputTypeComposer to the field '${typeName}.${fieldName}'. It should be OutputType.`
);
}
// $FlowFixMe
if (typeof fieldConfig === 'string' || isOutputType(fieldConfig)) {
fieldConfig = {
// $FlowFixMe
type: fieldConfig,
};
}
if (typeof fieldConfig.type === 'string') {
const fieldTypeDef = fieldConfig.type;
if (RegexpInputTypeDefinition.test(fieldTypeDef)) {
throw new Error(
`${typeName}.${fieldName} should be OutputType, but got input type definition '${fieldTypeDef}'`
);
}
const type = RegexpOutputTypeDefinition.test(fieldTypeDef) ||
RegexpEnumTypeDefinition.test(fieldTypeDef)
? this.createType(fieldTypeDef)
: this.getWrapped(fieldTypeDef);
if (!type) {
throw new Error(
`${typeName}.${fieldName} can not conver to OutputType following string: '${fieldTypeDef}'`
);
}
// $FlowFixMe
fieldConfig.type = type;
if (!isOutputType(type)) {
throw new Error(
`${typeName}.${fieldName} provided incorrect output type '${fieldTypeDef}'`
);
}
} else if (fieldConfig.type instanceof TypeComposer) {
fieldConfig.type = fieldConfig.type.getType();
}
if (fieldConfig.args) {
fieldConfig.args = this.convertArgConfigMap(fieldConfig.args, fieldName, typeName);
}
if (wrapWithList) {
// $FlowFixMe
fieldConfig.type = new GraphQLList(fieldConfig.type);
}
return fieldConfig;
}
convertOutputFieldConfigMap<TSource, TContext>(
fields: GraphQLFieldConfigMap<TSource, TContext>,
typeName: string
): GraphQLFieldConfigMap<TSource, TContext> {
Object.keys(fields).forEach(name => {
fields[name] = this.convertOutputFieldConfig(fields[name], name, typeName); // eslint-disable-line
});
return fields;
}
convertArgConfig(
argConfig: GraphQLArgumentConfig | string,
argName: string,
fieldName: string,
typeName: string
): GraphQLArgumentConfig {
let wrapWithList = false;
if (Array.isArray(argConfig)) {
argConfig = {
type: argConfig,
};
}
// $FlowFixMe
if (Array.isArray(argConfig.type)) {
if (argConfig.type.length !== 1) {
throw new Error(
`${typeName}.${fieldName}@${argName} can accept Array exact with one input type definition`
);
}
wrapWithList = true;
// $FlowFixMe
argConfig.type = argConfig.type[0];
}
if (argConfig instanceof InputTypeComposer) {
return { type: argConfig.getType() };
}
if (argConfig instanceof TypeComposer || argConfig.type instanceof TypeComposer) {
throw new Error(
`You cannot provide TypeComposer to the arg '${typeName}.${fieldName}.@${argName}'. It should be InputType.`
);
}
// $FlowFixMe
if (typeof argConfig === 'string' || isInputType(argConfig)) {
argConfig = {
// $FlowFixMe
type: argConfig,
};
}
if (typeof argConfig.type === 'string') {
const argTypeDef = argConfig.type;
if (RegexpOutputTypeDefinition.test(argTypeDef)) {
throw new Error(
`${typeName}.${fieldName}@${argName} should be InputType, but got output type definition '${argTypeDef}'`
);
}
const type = RegexpInputTypeDefinition.test(argTypeDef) ||
RegexpEnumTypeDefinition.test(argTypeDef)
? this.createType(argTypeDef)
: this.getWrapped(argTypeDef);
if (!isInputType(type)) {
throw new Error(
`${typeName}.${fieldName}@${argName} provided incorrect input type '${argTypeDef}'`
);
}
// $FlowFixMe
argConfig.type = type;
} else if (argConfig.type instanceof InputTypeComposer) {
argConfig.type = argConfig.type.getType();
}
if (wrapWithList) {
// $FlowFixMe
argConfig.type = new GraphQLList(argConfig.type);
}
return argConfig;
}
convertArgConfigMap(
argsConfigMap: GraphQLFieldConfigArgumentMap,
fieldName: string,
typeName: string
): GraphQLFieldConfigArgumentMap {
if (argsConfigMap) {
Object.keys(argsConfigMap).forEach(argName => {
argsConfigMap[argName] = this.convertArgConfig(
// eslint-disable-line
argsConfigMap[argName],
argName,
fieldName,
typeName
);
});
}
return argsConfigMap;
}
convertInputFieldConfig(
fieldConfig: GraphQLInputFieldConfig,
fieldName: string,
typeName: string
): GraphQLInputFieldConfig {
let wrapWithList = false;
if (Array.isArray(fieldConfig)) {
fieldConfig = {
type: fieldConfig,
};
}
if (Array.isArray(fieldConfig.type)) {
if (fieldConfig.type.length !== 1) {
throw new Error(
`${typeName}.${fieldName} can accept Array exact with one input type definition`
);
}
wrapWithList = true;
fieldConfig.type = fieldConfig.type[0];
}
if (fieldConfig instanceof InputTypeComposer) {
return { type: fieldConfig.getType() };
}
if (fieldConfig instanceof TypeComposer || fieldConfig.type instanceof TypeComposer) {
throw new Error(
`You cannot provide TypeComposer to the field '${typeName}.${fieldName}'. It should be InputType.`
);
}
// $FlowFixMe
if (typeof fieldConfig === 'string' || isInputType(fieldConfig)) {
fieldConfig = {
// $FlowFixMe
type: fieldConfig,
};
}
if (typeof fieldConfig.type === 'string') {
const fieldTypeDef = fieldConfig.type;
if (RegexpOutputTypeDefinition.test(fieldTypeDef)) {
throw new Error(
`${typeName}.${fieldName} should be InputType, but got output type definition '${fieldTypeDef}'`
);
}
const type = RegexpInputTypeDefinition.test(fieldTypeDef) ||
RegexpEnumTypeDefinition.test(fieldTypeDef)
? this.createType(fieldTypeDef)
: this.getWrapped(fieldTypeDef);
if (!isInputType(type)) {
throw new Error(`${typeName}.${fieldName} provided incorrect input type '${fieldTypeDef}'`);
}
// $FlowFixMe
fieldConfig.type = type;
} else if (fieldConfig.type instanceof InputTypeComposer) {
fieldConfig.type = fieldConfig.type.getType();
}
if (wrapWithList) {
// $FlowFixMe
fieldConfig.type = new GraphQLList(fieldConfig.type);
}
return fieldConfig;
}
convertInputFieldConfigMap(
fields: GraphQLInputFieldConfigMap,
typeName: string
): GraphQLInputFieldConfigMap {
Object.keys(fields).forEach(name => {
fields[name] = this.convertInputFieldConfig(fields[name], name, typeName); // eslint-disable-line
});
return fields;
}
}
const typeMapper = new TypeMapper();
export default typeMapper;
function parseTypes(astDocument: DocumentNode): Array<GraphQLNamedType> {
const types = [];
for (let i = 0; i < astDocument.definitions.length; i++) {
const def = astDocument.definitions[i];
types[i] = makeSchemaDef(def);
}
return types;
}
function typeFromAST(inputTypeAST: TypeNode): ?GraphQLType {
let innerType;
if (inputTypeAST.kind === LIST_TYPE) {
innerType = typeFromAST(inputTypeAST.type);
return innerType && new GraphQLList(innerType);
}
if (inputTypeAST.kind === NON_NULL_TYPE) {
innerType = typeFromAST(inputTypeAST.type);
return innerType && new GraphQLNonNull(((innerType: any): GraphQLNullableType));
}
invariant(inputTypeAST.kind === NAMED_TYPE, 'Must be a named type.');
return typeMapper.get(inputTypeAST.name.value);
}
function typeDefNamed(typeName: string): GraphQLNamedType {
const type = typeMapper.get(typeName);
if (type) {
return type;
}
throw new Error(`Cannot find type with name '${typeName}' in TypeMapper.`);
}
function makeSchemaDef(def) {
if (!def) {
throw new Error('def must be defined');
}
switch (def.kind) {
case OBJECT_TYPE_DEFINITION:
return makeTypeDef(def);
// case INTERFACE_TYPE_DEFINITION:
// return makeInterfaceDef(def);
case ENUM_TYPE_DEFINITION:
return makeEnumDef(def);
// case UNION_TYPE_DEFINITION:
// return makeUnionDef(def);
// case SCALAR_TYPE_DEFINITION:
// return makeScalarDef(def);
case INPUT_OBJECT_TYPE_DEFINITION:
return makeInputObjectDef(def);
default:
throw new Error(`Type kind "${def.kind}" not supported.`);
}
}
function makeInputValues(values: Array<InputValueDefinitionNode>) {
return keyValMap(
values,
value => value.name.value,
value => {
const type = produceInputType(value.type);
return {
type,
description: getDescription(value),
defaultValue: valueFromAST(value.defaultValue, type),
};
}
);
}
function makeFieldDefMap(def: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode) {
return keyValMap(
def.fields,
field => field.name.value,
field => ({
type: produceOutputType(field.type),
description: getDescription(field),
args: makeInputValues(field.arguments),
deprecationReason: getDeprecationReason(field.directives),
})
);
}
function makeEnumDef(def: EnumTypeDefinitionNode) {
const enumType = new GraphQLEnumType({
name: def.name.value,
description: getDescription(def),
values: keyValMap(
def.values,
enumValue => enumValue.name.value,
enumValue => ({
description: getDescription(enumValue),
deprecationReason: getDeprecationReason(enumValue.directives),
})
),
});
return enumType;
}
function makeInputObjectDef(def: InputObjectTypeDefinitionNode) {
return new GraphQLInputObjectType({
name: def.name.value,
description: getDescription(def),
fields: () => makeInputValues(def.fields),
});
}
function getNamedTypeAST(typeAST: TypeNode): NamedTypeNode {
let namedType = typeAST;
while (namedType.kind === LIST_TYPE || namedType.kind === NON_NULL_TYPE) {
namedType = namedType.type;
}
return namedType;
}
function buildWrappedType(innerType: GraphQLType, inputTypeAST: TypeNode): GraphQLType {
if (inputTypeAST.kind === LIST_TYPE) {
return new GraphQLList(buildWrappedType(innerType, inputTypeAST.type));
}
if (inputTypeAST.kind === NON_NULL_TYPE) {
const wrappedType = buildWrappedType(innerType, inputTypeAST.type);
invariant(!(wrappedType instanceof GraphQLNonNull), 'No nesting nonnull.');
return new GraphQLNonNull(wrappedType);
}
return innerType;
}
function produceOutputType(typeAST: TypeNode): GraphQLOutputType {
const type = produceType(typeAST);
invariant(isOutputType(type), 'Expected Output type.');
return (type: any);
}
function produceType(typeAST: TypeNode): GraphQLType {
const typeName = getNamedTypeAST(typeAST).name.value;
const typeDef = typeDefNamed(typeName);
return buildWrappedType(typeDef, typeAST);
}
function produceInputType(typeAST: TypeNode): GraphQLInputType {
const type = produceType(typeAST);
invariant(isInputType(type), 'Expected Input type.');
return (type: any);
}
function produceInterfaceType(typeAST: TypeNode): GraphQLInterfaceType {
const type = produceType(typeAST);
invariant(type instanceof GraphQLInterfaceType, 'Expected Object type.');
return type;
}
function makeImplementedInterfaces(def: ObjectTypeDefinitionNode) {
return def.interfaces && def.interfaces.map(iface => produceInterfaceType(iface));
}
function makeTypeDef(def: ObjectTypeDefinitionNode) {
const typeName = def.name.value;
return new GraphQLObjectType({
name: typeName,
description: getDescription(def),
fields: () => makeFieldDefMap(def),
interfaces: () => makeImplementedInterfaces(def),
});
}
function getDeprecationReason(directives: ?Array<DirectiveNode>): ?string {
const deprecatedAST = directives &&
find(directives, directive => directive.name.value === GraphQLDeprecatedDirective.name);
if (!deprecatedAST) {
return;
}
const { reason } = getArgumentValues(GraphQLDeprecatedDirective, deprecatedAST);
return (reason: any); // eslint-disable-line
}