From 2e2c9b1fc06abc4da3f02c42b8012c6af0be94ac Mon Sep 17 00:00:00 2001 From: Mikhail Novikov Date: Mon, 11 Dec 2017 16:09:06 +0200 Subject: [PATCH] Simplify API --- package.json | 2 +- src/Interfaces.ts | 23 ++ src/stitching/mergeSchemas.ts | 519 ++++++++++++++++++------------ src/stitching/schemaRecreation.ts | 53 +-- src/stitching/typeFromAST.ts | 76 +++-- src/test/testMergeSchemas.ts | 156 ++++++++- 6 files changed, 549 insertions(+), 280 deletions(-) diff --git a/package.json b/package.json index f84beafd098..5272654d2f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-tools", - "version": "2.14.0", + "version": "2.15.0-alpha.1", "description": "Useful tools to create and manipulate GraphQL schemas.", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/Interfaces.ts b/src/Interfaces.ts index e22490c7c1f..a3af8d8dabc 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -8,6 +8,7 @@ import { GraphQLIsTypeOfFn, GraphQLTypeResolver, GraphQLScalarType, + GraphQLNamedType, DocumentNode, } from 'graphql'; @@ -29,12 +30,14 @@ export interface IResolverOptions { export type MergeInfo = { delegate: ( + schemaName: string, type: 'query' | 'mutation' | 'subscription', fieldName: string, args: { [key: string]: any }, context: { [key: string]: any }, info: GraphQLResolveInfo, ) => any; + getSubSchema: (schemaName: string) => GraphQLSchema; }; export type IFieldResolver = ( @@ -119,3 +122,23 @@ export interface IMockServer { vars?: { [key: string]: any }, ) => Promise; } + +export type MergeTypeCandidate = { + schemaName: string; + schema?: GraphQLSchema; + type: GraphQLNamedType; +}; + +export type TypeWithResolvers = { + type: GraphQLNamedType; + resolvers?: IResolvers; +}; + +export type VisitTypeResult = GraphQLNamedType | TypeWithResolvers | null; + +export type VisitType = ( + name: string, + candidates: Array, +) => VisitTypeResult; + +export type ResolveType = (type: T) => T; diff --git a/src/stitching/mergeSchemas.ts b/src/stitching/mergeSchemas.ts index 8cbc632001f..2b1269cfadf 100644 --- a/src/stitching/mergeSchemas.ts +++ b/src/stitching/mergeSchemas.ts @@ -1,24 +1,35 @@ import { DocumentNode, GraphQLField, - GraphQLFieldMap, GraphQLInputObjectType, GraphQLNamedType, GraphQLObjectType, GraphQLResolveInfo, - GraphQLScalarType, + GraphQLInterfaceType, GraphQLSchema, GraphQLType, - buildASTSchema, + GraphQLString, + GraphQLNonNull, + GraphQLList, extendSchema, getNamedType, isCompositeType, isNamedType, parse, + InlineFragmentNode, + Kind, + GraphQLScalarType, } from 'graphql'; -import TypeRegistry from './TypeRegistry'; -import { IResolvers, MergeInfo, IFieldResolver } from '../Interfaces'; -import isEmptyObject from '../isEmptyObject'; +import { + IResolvers, + MergeInfo, + IFieldResolver, + VisitType, + MergeTypeCandidate, + TypeWithResolvers, + VisitTypeResult, + ResolveType, +} from '../Interfaces'; import { extractExtensionDefinitions, addResolveFunctionsToSchema, @@ -28,216 +39,188 @@ import { fieldMapToFieldConfigMap, } from './schemaRecreation'; import delegateToSchema from './delegateToSchema'; -import typeFromAST from './typeFromAST'; - -const backcompatOptions = { commentDescriptions: true }; +import typeFromAST, { GetType } from './typeFromAST'; export default function mergeSchemas({ schemas, - onTypeConflict, + visitType, resolvers, }: { - schemas: Array; - onTypeConflict?: ( - left: GraphQLNamedType, - right: GraphQLNamedType, - ) => GraphQLNamedType; + schemas: Array<{ name: string; schema: string | GraphQLSchema }>; + visitType?: VisitType; resolvers?: IResolvers | ((mergeInfo: MergeInfo) => IResolvers); }): GraphQLSchema { - if (!onTypeConflict) { - onTypeConflict = defaultOnTypeConflict; - } - let queryFields: GraphQLFieldMap = {}; - let mutationFields: GraphQLFieldMap = {}; - let subscriptionFields: GraphQLFieldMap = {}; + const allSchemas: { [name: string]: GraphQLSchema } = {}; + const typeCandidates: { [name: string]: Array } = {}; + const types: { [name: string]: GraphQLNamedType } = {}; + const extensions: Array = []; + const fragments = {}; - const typeRegistry = new TypeRegistry(); + if (!visitType) { + visitType = defaultVisitType; + } - const mergeInfo: MergeInfo = createMergeInfo(typeRegistry); + const resolveType = createResolveType(name => { + if (types[name] === undefined) { + throw new Error(`Can't find type ${name}.`); + } + return types[name]; + }); - const actualSchemas: Array = []; - const typeFragments: Array = []; - const extensions: Array = []; - let fullResolvers: IResolvers = {}; + const createNamedStub: GetType = (name, type) => { + let constructor: any; + if (type === 'object') { + constructor = GraphQLObjectType; + } else if (type === 'interface') { + constructor = GraphQLInterfaceType; + } else { + constructor = GraphQLInputObjectType; + } + return new constructor({ + name, + fields: { + __fake: { + type: GraphQLString, + }, + }, + }); + }; - schemas.forEach(schema => { - if (schema instanceof GraphQLSchema) { - actualSchemas.push(schema); - } else if (typeof schema === 'string') { - let parsedSchemaDocument = parse(schema); - try { - // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 - const actualSchema = (buildASTSchema as any)( - parsedSchemaDocument, - backcompatOptions, - ); - actualSchemas.push(actualSchema); - } catch (e) { - typeFragments.push(parsedSchemaDocument); + schemas.forEach(subSchema => { + if (subSchema.schema instanceof GraphQLSchema) { + const schema = subSchema.schema; + allSchemas[subSchema.name] = schema; + const queryType = schema.getQueryType(); + const mutationType = schema.getMutationType(); + const subscriptionType = schema.getSubscriptionType(); + addTypeCandidate(typeCandidates, 'Query', { + schemaName: subSchema.name, + schema, + type: queryType, + }); + if (mutationType) { + addTypeCandidate(typeCandidates, 'Mutation', { + schemaName: subSchema.name, + schema, + type: mutationType, + }); } - parsedSchemaDocument = extractExtensionDefinitions(parsedSchemaDocument); - if (parsedSchemaDocument.definitions.length > 0) { - extensions.push(parsedSchemaDocument); + if (subscriptionType) { + addTypeCandidate(typeCandidates, 'Subscription', { + schemaName: subSchema.name, + schema, + type: subscriptionType, + }); } - } - }); - - actualSchemas.forEach(schema => { - typeRegistry.addSchema(schema); - const queryType = schema.getQueryType(); - const mutationType = schema.getMutationType(); - const subscriptionType = schema.getSubscriptionType(); - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach(typeName => { - const type: GraphQLType = typeMap[typeName]; - if ( - isNamedType(type) && - getNamedType(type).name.slice(0, 2) !== '__' && - type !== queryType && - type !== mutationType && - type !== subscriptionType - ) { - let newType; - if (isCompositeType(type) || type instanceof GraphQLInputObjectType) { - newType = recreateCompositeType(schema, type, typeRegistry); - } else { - newType = getNamedType(type); + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach(typeName => { + const type: GraphQLNamedType = typeMap[typeName]; + if ( + isNamedType(type) && + getNamedType(type).name.slice(0, 2) !== '__' && + type !== queryType && + type !== mutationType && + type !== subscriptionType + ) { + addTypeCandidate(typeCandidates, type.name, { + schemaName: subSchema.name, + schema, + type: type, + }); } - if (newType instanceof GraphQLObjectType) { - delete newType.isTypeOf; + }); + } else if (typeof subSchema.schema === 'string') { + let parsedSchemaDocument = parse(subSchema.schema); + parsedSchemaDocument.definitions.forEach(def => { + const type = typeFromAST(def, createNamedStub); + if (type) { + addTypeCandidate(typeCandidates, type.name, { + schemaName: subSchema.name, + type: type, + }); } - typeRegistry.addType(newType.name, newType, onTypeConflict); - } - }); + }); - Object.keys(queryType.getFields()).forEach(name => { - if (!fullResolvers.Query) { - fullResolvers.Query = {}; - } - fullResolvers.Query[name] = createDelegatingResolver( - mergeInfo, - 'query', - name, + const extensionsDocument = extractExtensionDefinitions( + parsedSchemaDocument, ); - }); - - queryFields = { - ...queryFields, - ...queryType.getFields(), - }; - - if (mutationType) { - if (!fullResolvers.Mutation) { - fullResolvers.Mutation = {}; + if (extensionsDocument.definitions.length > 0) { + extensions.push(extensionsDocument); } - Object.keys(mutationType.getFields()).forEach(name => { - fullResolvers.Mutation[name] = createDelegatingResolver( - mergeInfo, - 'mutation', - name, - ); - }); - - mutationFields = { - ...mutationFields, - ...mutationType.getFields(), - }; + } else { + throw new Error(`Invalid schema ${subSchema.name}`); } + }); - if (subscriptionType) { - if (!fullResolvers.Subscription) { - fullResolvers.Subscription = {}; - } - Object.keys(subscriptionType.getFields()).forEach(name => { - fullResolvers.Subscription[name] = { - subscribe: createDelegatingResolver(mergeInfo, 'subscription', name), - }; - }); + let generatedResolvers = {}; - subscriptionFields = { - ...subscriptionFields, - ...subscriptionType.getFields(), - }; + Object.keys(typeCandidates).forEach(typeName => { + const resultType: VisitTypeResult = visitType( + typeName, + typeCandidates[typeName], + ); + if (resultType === null) { + types[typeName] = null; + } else { + let type: GraphQLNamedType; + let typeResolvers: IResolvers; + if (isNamedType(resultType)) { + type = resultType; + } else if ((resultType).type) { + type = (resultType).type; + typeResolvers = (resultType).resolvers; + } else { + throw new Error('Invalid `visitType` result for type "${typeName}"'); + } + let newType: GraphQLType; + if (isCompositeType(type) || type instanceof GraphQLInputObjectType) { + newType = recreateCompositeType(type, resolveType); + } else { + newType = getNamedType(type); + } + types[typeName] = newType; + if (typeResolvers) { + generatedResolvers[typeName] = typeResolvers; + } } }); - typeFragments.forEach(document => { - document.definitions.forEach(def => { - const type = typeFromAST(typeRegistry, def); - if (type) { - typeRegistry.addType(type.name, type, onTypeConflict); - } - }); + let mergedSchema = new GraphQLSchema({ + query: types.Query as GraphQLObjectType, + mutation: types.Mutation as GraphQLObjectType, + subscription: types.Subscription as GraphQLObjectType, + types: Object.keys(types).map(key => types[key]), }); - let passedResolvers = {}; - if (resolvers) { - if (typeof resolvers === 'function') { - passedResolvers = resolvers(mergeInfo); - } else { - passedResolvers = { ...resolvers }; - } - } + extensions.forEach(extension => { + mergedSchema = (extendSchema as any)(mergedSchema, extension, { + commentDescriptions: true, + }); + }); - Object.keys(passedResolvers).forEach(typeName => { - const type = passedResolvers[typeName]; + Object.keys(resolvers).forEach(typeName => { + const type = resolvers[typeName]; if (type instanceof GraphQLScalarType) { return; } Object.keys(type).forEach(fieldName => { const field = type[fieldName]; if (field.fragment) { - typeRegistry.addFragment(typeName, fieldName, field.fragment); + fragments[typeName] = fragments[typeName] || {}; + fragments[typeName][fieldName] = parseFragmentToInlineFragment( + field.fragment, + ); } }); }); - fullResolvers = mergeDeep(fullResolvers, passedResolvers); - - const query = new GraphQLObjectType({ - name: 'Query', - fields: () => fieldMapToFieldConfigMap(queryFields, typeRegistry), - }); - - let mutation; - if (!isEmptyObject(mutationFields)) { - mutation = new GraphQLObjectType({ - name: 'Mutation', - fields: () => fieldMapToFieldConfigMap(mutationFields, typeRegistry), - }); - } - - let subscription; - if (!isEmptyObject(subscriptionFields)) { - subscription = new GraphQLObjectType({ - name: 'Subscription', - fields: () => fieldMapToFieldConfigMap(subscriptionFields, typeRegistry), - }); - } - - typeRegistry.addType('Query', query); - typeRegistry.addType('Mutation', mutation); - typeRegistry.addType('Subscription', subscription); - - let mergedSchema = new GraphQLSchema({ - query, - mutation, - subscription, - types: typeRegistry.getAllTypes(), - }); - - extensions.forEach(extension => { - // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 - mergedSchema = (extendSchema as any)( - mergedSchema, - extension, - backcompatOptions, - ); - }); - - addResolveFunctionsToSchema(mergedSchema, fullResolvers); + addResolveFunctionsToSchema( + mergedSchema, + mergeDeep(generatedResolvers, resolvers), + ); + const mergeInfo = createMergeInfo(allSchemas, fragments); forEachField(mergedSchema, field => { if (field.resolve) { const fieldResolver = field.resolve; @@ -251,29 +234,59 @@ export default function mergeSchemas({ return mergedSchema; } -function defaultOnTypeConflict( - left: GraphQLNamedType, - right: GraphQLNamedType, -): GraphQLNamedType { - return left; +function createResolveType( + getType: (name: string, type: GraphQLType) => GraphQLType | null, +): ResolveType { + const resolveType = (type: T): T => { + if (type instanceof GraphQLList) { + const innerType = resolveType(type.ofType); + if (innerType === null) { + return null; + } else { + return new GraphQLList(innerType) as T; + } + } else if (type instanceof GraphQLNonNull) { + const innerType = resolveType(type.ofType); + if (innerType === null) { + return null; + } else { + return new GraphQLNonNull(innerType) as T; + } + } else if (isNamedType(type)) { + return getType(getNamedType(type).name, type) as T; + } else { + return type; + } + }; + return resolveType; } -function createMergeInfo(typeRegistry: TypeRegistry): MergeInfo { +function createMergeInfo( + schemas: { [name: string]: GraphQLSchema }, + fragmentReplacements: { + [name: string]: { [fieldName: string]: InlineFragmentNode }; + }, +): MergeInfo { return { + getSubSchema(schemaName: string): GraphQLSchema { + const schema = schemas[schemaName]; + if (!schema) { + throw new Error(`No subschema named ${schemaName}.`); + } + return schema; + }, delegate( + schemaName: string, operation: 'query' | 'mutation' | 'subscription', fieldName: string, args: { [key: string]: any }, context: { [key: string]: any }, info: GraphQLResolveInfo, ): any { - const schema = typeRegistry.getSchemaByField(operation, fieldName); + const schema = schemas[schemaName]; if (!schema) { - throw new Error( - `Cannot find subschema for root field ${operation}.${fieldName}`, - ); + throw new Error(`No subschema named ${schemaName}.`); } - const fragmentReplacements = typeRegistry.fragmentReplacements; return delegateToSchema( schema, fragmentReplacements, @@ -288,15 +301,46 @@ function createMergeInfo(typeRegistry: TypeRegistry): MergeInfo { } function createDelegatingResolver( - mergeInfo: MergeInfo, + schemaName: string, operation: 'query' | 'mutation' | 'subscription', fieldName: string, ): IFieldResolver { return (root, args, context, info) => { - return mergeInfo.delegate(operation, fieldName, args, context, info); + return info.mergeInfo.delegate( + schemaName, + operation, + fieldName, + args, + context, + info, + ); }; } +type FieldIteratorFn = ( + fieldDef: GraphQLField, + typeName: string, + fieldName: string, +) => void; + +function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach(typeName => { + const type = typeMap[typeName]; + + if ( + !getNamedType(type).name.startsWith('__') && + type instanceof GraphQLObjectType + ) { + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + const field = fields[fieldName]; + fn(field, typeName, fieldName); + }); + } + }); +} + function isObject(item: any): Boolean { return item && typeof item === 'object' && !Array.isArray(item); } @@ -319,26 +363,81 @@ function mergeDeep(target: any, source: any): any { return output; } -type FieldIteratorFn = ( - fieldDef: GraphQLField, - typeName: string, - fieldName: string, -) => void; +function parseFragmentToInlineFragment( + definitions: string, +): InlineFragmentNode { + const document = parse(definitions); + for (const definition of document.definitions) { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + return { + kind: Kind.INLINE_FRAGMENT, + typeCondition: definition.typeCondition, + selectionSet: definition.selectionSet, + }; + } + } + throw new Error('Could not parse fragment'); +} -function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void { - const typeMap = schema.getTypeMap(); - Object.keys(typeMap).forEach(typeName => { - const type = typeMap[typeName]; +function addTypeCandidate( + typeCandidates: { [name: string]: Array }, + name: string, + typeCandidate: MergeTypeCandidate, +) { + if (!typeCandidates[name]) { + typeCandidates[name] = []; + } + typeCandidates[name].push(typeCandidate); +} - if ( - !getNamedType(type).name.startsWith('__') && - type instanceof GraphQLObjectType - ) { - const fields = type.getFields(); - Object.keys(fields).forEach(fieldName => { - const field = fields[fieldName]; - fn(field, typeName, fieldName); - }); +const defaultVisitType: VisitType = ( + name: string, + candidates: Array, +) => { + const resolveType = createResolveType((_, type) => type); + if (name === 'Query' || name === 'Mutation' || name === 'Subscription') { + let fields = {}; + let operationName: 'query' | 'mutation' | 'subscription'; + switch (name) { + case 'Query': + operationName = 'query'; + break; + case 'Mutation': + operationName = 'mutation'; + break; + case 'Subscription': + operationName = 'subscription'; + break; + default: + break; } - }); -} + const resolvers = {}; + candidates.forEach(({ type: candidateType, schemaName }) => { + const candidateFields = (candidateType as GraphQLObjectType).getFields(); + fields = { ...fields, ...candidateFields }; + Object.keys(candidateFields).forEach(fieldName => { + if (name === 'Subscription') { + resolvers[fieldName] = { + subscribe: candidateFields[fieldName].subscribe, + }; + } else { + resolvers[fieldName] = createDelegatingResolver( + schemaName, + operationName, + fieldName, + ); + } + }); + }); + const type = new GraphQLObjectType({ + name, + fields: fieldMapToFieldConfigMap(fields, resolveType), + }); + return { + type, + resolvers, + }; + } else { + return candidates[candidates.length - 1].type; + } +}; diff --git a/src/stitching/schemaRecreation.ts b/src/stitching/schemaRecreation.ts index 0e4e3983850..f3468270043 100644 --- a/src/stitching/schemaRecreation.ts +++ b/src/stitching/schemaRecreation.ts @@ -14,17 +14,15 @@ import { GraphQLInputObjectType, GraphQLInterfaceType, GraphQLObjectType, - GraphQLSchema, GraphQLUnionType, } from 'graphql'; -import TypeRegistry from './TypeRegistry'; +import { ResolveType } from '../Interfaces'; import resolveFromParentTypename from './resolveFromParentTypename'; import defaultMergedResolver from './defaultMergedResolver'; export function recreateCompositeType( - schema: GraphQLSchema, type: GraphQLCompositeType | GraphQLInputObjectType, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLCompositeType | GraphQLInputObjectType { if (type instanceof GraphQLObjectType) { const fields = type.getFields(); @@ -33,9 +31,8 @@ export function recreateCompositeType( return new GraphQLObjectType({ name: type.name, description: type.description, - isTypeOf: type.isTypeOf, - fields: () => fieldMapToFieldConfigMap(fields, registry), - interfaces: () => interfaces.map(iface => registry.resolveType(iface)), + fields: () => fieldMapToFieldConfigMap(fields, resolveType), + interfaces: () => interfaces.map(iface => resolveType(iface)), }); } else if (type instanceof GraphQLInterfaceType) { const fields = type.getFields(); @@ -43,7 +40,7 @@ export function recreateCompositeType( return new GraphQLInterfaceType({ name: type.name, description: type.description, - fields: () => fieldMapToFieldConfigMap(fields, registry), + fields: () => fieldMapToFieldConfigMap(fields, resolveType), resolveType: (parent, context, info) => resolveFromParentTypename(parent, info.schema), }); @@ -51,8 +48,7 @@ export function recreateCompositeType( return new GraphQLUnionType({ name: type.name, description: type.description, - types: () => - type.getTypes().map(unionMember => registry.resolveType(unionMember)), + types: () => type.getTypes().map(unionMember => resolveType(unionMember)), resolveType: (parent, context, info) => resolveFromParentTypename(parent, info.schema), }); @@ -60,7 +56,8 @@ export function recreateCompositeType( return new GraphQLInputObjectType({ name: type.name, description: type.description, - fields: () => inputFieldMapToFieldConfigMap(type.getFields(), registry), + fields: () => + inputFieldMapToFieldConfigMap(type.getFields(), resolveType), }); } else { throw new Error(`Invalid type ${type}`); @@ -69,22 +66,26 @@ export function recreateCompositeType( export function fieldMapToFieldConfigMap( fields: GraphQLFieldMap, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLFieldConfigMap { const result: GraphQLFieldConfigMap = {}; Object.keys(fields).forEach(name => { - result[name] = fieldToFieldConfig(fields[name], registry); + const field = fields[name]; + const type = resolveType(field.type); + if (type !== null) { + result[name] = fieldToFieldConfig(fields[name], resolveType); + } }); return result; } function fieldToFieldConfig( field: GraphQLField, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLFieldConfig { return { - type: registry.resolveType(field.type), - args: argsToFieldConfigArgumentMap(field.args, registry), + type: resolveType(field.type), + args: argsToFieldConfigArgumentMap(field.args, resolveType), resolve: defaultMergedResolver, description: field.description, deprecationReason: field.deprecationReason, @@ -93,11 +94,11 @@ function fieldToFieldConfig( function argsToFieldConfigArgumentMap( args: Array, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLFieldConfigArgumentMap { const result: GraphQLFieldConfigArgumentMap = {}; args.forEach(arg => { - const [name, def] = argumentToArgumentConfig(arg, registry); + const [name, def] = argumentToArgumentConfig(arg, resolveType); result[name] = def; }); return result; @@ -105,12 +106,12 @@ function argsToFieldConfigArgumentMap( function argumentToArgumentConfig( argument: GraphQLArgument, - registry: TypeRegistry, + resolveType: ResolveType, ): [string, GraphQLArgumentConfig] { return [ argument.name, { - type: registry.resolveType(argument.type), + type: resolveType(argument.type), defaultValue: argument.defaultValue, description: argument.description, }, @@ -119,21 +120,25 @@ function argumentToArgumentConfig( function inputFieldMapToFieldConfigMap( fields: GraphQLInputFieldMap, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLInputFieldConfigMap { const result: GraphQLInputFieldConfigMap = {}; Object.keys(fields).forEach(name => { - result[name] = inputFieldToFieldConfig(fields[name], registry); + const field = fields[name]; + const type = resolveType(field.type); + if (type !== null) { + result[name] = inputFieldToFieldConfig(fields[name], resolveType); + } }); return result; } function inputFieldToFieldConfig( field: GraphQLInputField, - registry: TypeRegistry, + resolveType: ResolveType, ): GraphQLInputFieldConfig { return { - type: registry.resolveType(field.type), + type: resolveType(field.type), defaultValue: field.defaultValue, description: field.description, }; diff --git a/src/stitching/typeFromAST.ts b/src/stitching/typeFromAST.ts index 63458ab7a2d..ebeb30bea9d 100644 --- a/src/stitching/typeFromAST.ts +++ b/src/stitching/typeFromAST.ts @@ -23,60 +23,60 @@ import { UnionTypeDefinitionNode, valueFromAST, } from 'graphql'; -// -// TODO put back import once PR is merged -// https://github.com/graphql/graphql-js/pull/1165 -// import { getDescription } from 'graphql/utilities/buildASTSchema'; +import resolveFromParentType from './resolveFromParentTypename'; const backcompatOptions = { commentDescriptions: true }; -import resolveFromParentType from './resolveFromParentTypename'; -import TypeRegistry from './TypeRegistry'; +export type GetType = ( + name: string, + // this is a hack + type: 'object' | 'interface' | 'input', +) => GraphQLObjectType | GraphQLInputObjectType | GraphQLInterfaceType; export default function typeFromAST( - typeRegistry: TypeRegistry, node: DefinitionNode, + getType: GetType, ): GraphQLNamedType | null { switch (node.kind) { case Kind.OBJECT_TYPE_DEFINITION: - return makeObjectType(typeRegistry, node); + return makeObjectType(node, getType); case Kind.INTERFACE_TYPE_DEFINITION: - return makeInterfaceType(typeRegistry, node); + return makeInterfaceType(node, getType); case Kind.ENUM_TYPE_DEFINITION: - return makeEnumType(typeRegistry, node); + return makeEnumType(node, getType); case Kind.UNION_TYPE_DEFINITION: - return makeUnionType(typeRegistry, node); + return makeUnionType(node, getType); case Kind.SCALAR_TYPE_DEFINITION: - return makeScalarType(typeRegistry, node); + return makeScalarType(node, getType); case Kind.INPUT_OBJECT_TYPE_DEFINITION: - return makeInputObjectType(typeRegistry, node); + return makeInputObjectType(node, getType); default: return null; } } function makeObjectType( - typeRegistry: TypeRegistry, node: ObjectTypeDefinitionNode, + getType: GetType, ): GraphQLObjectType { return new GraphQLObjectType({ name: node.name.value, - fields: () => makeFields(typeRegistry, node.fields), + fields: () => makeFields(node.fields, getType), interfaces: () => node.interfaces.map( - iface => typeRegistry.getType(iface.name.value) as GraphQLInterfaceType, + iface => getType(iface.name.value, 'interface') as GraphQLInterfaceType, ), description: getDescription(node, backcompatOptions), }); } function makeInterfaceType( - typeRegistry: TypeRegistry, node: InterfaceTypeDefinitionNode, + getType: GetType, ): GraphQLInterfaceType { return new GraphQLInterfaceType({ name: node.name.value, - fields: () => makeFields(typeRegistry, node.fields), + fields: () => makeFields(node.fields, getType), description: getDescription(node, backcompatOptions), resolveType: (parent, context, info) => resolveFromParentType(parent, info.schema), @@ -84,8 +84,8 @@ function makeInterfaceType( } function makeEnumType( - typeRegistry: TypeRegistry, node: EnumTypeDefinitionNode, + getType: GetType, ): GraphQLEnumType { const values = {}; node.values.forEach(value => { @@ -101,14 +101,14 @@ function makeEnumType( } function makeUnionType( - typeRegistry: TypeRegistry, node: UnionTypeDefinitionNode, + getType: GetType, ): GraphQLUnionType { return new GraphQLUnionType({ name: node.name.value, types: () => node.types.map( - type => resolveType(typeRegistry, type) as GraphQLObjectType, + type => resolveType(type, getType, 'object') as GraphQLObjectType, ), description: getDescription(node, backcompatOptions), resolveType: (parent, context, info) => @@ -117,8 +117,8 @@ function makeUnionType( } function makeScalarType( - typeRegistry: TypeRegistry, node: ScalarTypeDefinitionNode, + getType: GetType, ): GraphQLScalarType { return new GraphQLScalarType({ name: node.name.value, @@ -134,38 +134,32 @@ function makeScalarType( } function makeInputObjectType( - typeRegistry: TypeRegistry, node: InputObjectTypeDefinitionNode, + getType: GetType, ): GraphQLInputObjectType { return new GraphQLInputObjectType({ name: node.name.value, - fields: () => makeValues(typeRegistry, node.fields), + fields: () => makeValues(node.fields, getType), description: getDescription(node, backcompatOptions), }); } -function makeFields( - typeRegistry: TypeRegistry, - nodes: Array, -) { +function makeFields(nodes: Array, getType: GetType) { const result = {}; nodes.forEach(node => { result[node.name.value] = { - type: resolveType(typeRegistry, node.type), - args: makeValues(typeRegistry, node.arguments), + type: resolveType(node.type, getType, 'object'), + args: makeValues(node.arguments, getType), description: getDescription(node, backcompatOptions), }; }); return result; } -function makeValues( - typeRegistry: TypeRegistry, - nodes: Array, -) { +function makeValues(nodes: Array, getType: GetType) { const result = {}; nodes.forEach(node => { - const type = resolveType(typeRegistry, node.type) as GraphQLInputType; + const type = resolveType(node.type, getType, 'input') as GraphQLInputType; result[node.name.value] = { type, defaultValue: valueFromAST(node.defaultValue, type), @@ -175,14 +169,18 @@ function makeValues( return result; } -function resolveType(typeRegistry: TypeRegistry, node: TypeNode): GraphQLType { +function resolveType( + node: TypeNode, + getType: GetType, + type: 'object' | 'interface' | 'input', +): GraphQLType { switch (node.kind) { case Kind.LIST_TYPE: - return new GraphQLList(resolveType(typeRegistry, node.type)); + return new GraphQLList(resolveType(node.type, getType, type)); case Kind.NON_NULL_TYPE: - return new GraphQLNonNull(resolveType(typeRegistry, node.type)); + return new GraphQLNonNull(resolveType(node.type, getType, type)); default: - return typeRegistry.getType(node.name.value); + return getType(node.name.value, type); } } diff --git a/src/test/testMergeSchemas.ts b/src/test/testMergeSchemas.ts index 01df3ef40a7..f56c1179ac9 100644 --- a/src/test/testMergeSchemas.ts +++ b/src/test/testMergeSchemas.ts @@ -10,6 +10,7 @@ import { parse, ExecutionResult, } from 'graphql'; +import { VisitType } from '../Interfaces'; import mergeSchemas from '../stitching/mergeSchemas'; import { propertySchema as localPropertySchema, @@ -124,12 +125,30 @@ testCombinations.forEach(async combination => { mergedSchema = mergeSchemas({ schemas: [ - propertySchema, - bookingSchema, - scalarTest, - enumTest, - linkSchema, - localSubscriptionSchema, + { + name: 'Property', + schema: propertySchema, + }, + { + name: 'Booking', + schema: bookingSchema, + }, + { + name: 'ScalarTest', + schema: scalarTest, + }, + { + name: 'EnumTest', + schema: enumTest, + }, + { + name: 'LinkSchema', + schema: linkSchema, + }, + { + name: 'LocalSubscription', + schema: localSubscriptionSchema, + }, ], resolvers: { TestScalar: new GraphQLScalarType({ @@ -147,6 +166,7 @@ testCombinations.forEach(async combination => { fragment: 'fragment PropertyFragment on Property { id }', resolve(parent, args, context, info) { return info.mergeInfo.delegate( + 'Booking', 'query', 'bookingsByPropertyId', { @@ -164,6 +184,7 @@ testCombinations.forEach(async combination => { fragment: 'fragment BookingFragment on Booking { propertyId }', resolve(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', { @@ -179,6 +200,7 @@ testCombinations.forEach(async combination => { property: { resolve(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', { @@ -196,6 +218,7 @@ testCombinations.forEach(async combination => { }, delegateInterfaceTest(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'interfaceTest', { @@ -207,6 +230,7 @@ testCombinations.forEach(async combination => { }, delegateArgumentTest(parent, args, context, info) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', { @@ -227,6 +251,7 @@ testCombinations.forEach(async combination => { resolve(parent, args, context, info) { if (args.id.startsWith('p')) { return info.mergeInfo.delegate( + 'Property', 'query', 'propertyById', args, @@ -235,6 +260,7 @@ testCombinations.forEach(async combination => { ); } else if (args.id.startsWith('b')) { return info.mergeInfo.delegate( + 'Booking', 'query', 'bookingById', args, @@ -243,6 +269,7 @@ testCombinations.forEach(async combination => { ); } else if (args.id.startsWith('c')) { return info.mergeInfo.delegate( + 'Booking', 'query', 'customerById', args, @@ -256,6 +283,7 @@ testCombinations.forEach(async combination => { }, async nodes(parent, args, context, info) { const bookings = await info.mergeInfo.delegate( + 'Booking', 'query', 'bookings', {}, @@ -263,6 +291,7 @@ testCombinations.forEach(async combination => { info, ); const properties = await info.mergeInfo.delegate( + 'Property', 'query', 'properties', {}, @@ -1786,3 +1815,118 @@ bookingById(id: $b1) { }); }); }); + +describe('mergeSchema options', () => { + describe('should filter types', () => { + let schema: GraphQLSchema; + + before(async () => { + const bookingSchema = await remoteBookingSchema; + const createTypeFilteringVisitTypes = ( + typeNames: Array, + ): VisitType => { + return (name, candidates) => { + if ( + ['ID', 'String', 'DateTime'].includes(name) || + typeNames.includes(name) + ) { + return candidates[candidates.length - 1].type; + } else { + return null; + } + }; + }; + schema = mergeSchemas({ + schemas: [ + { + name: 'Booking', + schema: bookingSchema, + }, + { + name: 'Selector', + schema: ` + type Query { + bookingById(id: ID!): Booking + }, + `, + }, + ], + visitType: createTypeFilteringVisitTypes(['Query', 'Booking']), + resolvers: { + Query: { + bookingById(parent, args, context, info) { + return info.mergeInfo.delegate( + 'Booking', + 'query', + 'bookingById', + args, + context, + info, + ); + }, + }, + }, + }); + }); + + it('should work normally', async () => { + const result = await graphql( + schema, + ` + query { + bookingById(id: "b1") { + id + propertyId + startTime + endTime + } + } + `, + ); + + expect(result).to.deep.equal({ + data: { + bookingById: { + endTime: '2016-06-03', + id: 'b1', + propertyId: 'p1', + startTime: '2016-05-04', + }, + }, + }); + }); + + it('should error on removed types', async () => { + const result = await graphql( + schema, + ` + query { + bookingById(id: "b1") { + id + propertyId + startTime + endTime + customer { + id + } + } + } + `, + ); + expect(result).to.deep.equal({ + errors: [ + { + locations: [ + { + column: 15, + line: 8, + }, + ], + message: 'Cannot query field "customer" on type "Booking".', + path: undefined, + }, + ], + }); + }); + }); +});