-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
678 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export { mergeGraphQLSchemas } from './schema-mergers/merge-schema'; | ||
export { mergeTypeDefs } from './typedefs-mergers/merge-typedefs'; | ||
export { mergeResolvers } from './resolvers-mergers/merge-resolvers'; | ||
export { mergeSchemas } from './merge-schemas'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { GraphQLSchema, DocumentNode } from "graphql"; | ||
import { IResolvers, SchemaDirectiveVisitor, makeExecutableSchema, IResolverValidationOptions } from "graphql-tools"; | ||
import { mergeTypeDefs } from "./typedefs-mergers/merge-typedefs"; | ||
import { asArray } from "../utils/helpers"; | ||
import { mergeResolvers } from "./resolvers-mergers/merge-resolvers"; | ||
import { extractResolversFromSchema } from "../utils"; | ||
|
||
export interface MergeSchemasConfig { | ||
schemas: GraphQLSchema[]; | ||
typeDefs?: (DocumentNode | string)[] | DocumentNode | string; | ||
resolvers?: IResolvers | IResolvers[]; | ||
schemaDirectives ?: { [directiveName: string] : typeof SchemaDirectiveVisitor }; | ||
resolverValidationOptions ?: IResolverValidationOptions; | ||
} | ||
|
||
export function mergeSchemas(config: MergeSchemasConfig) { | ||
const typeDefs = mergeTypeDefs([ | ||
...config.schemas, | ||
...config.typeDefs ? asArray(config.typeDefs) : [] | ||
]); | ||
const resolvers = mergeResolvers([ | ||
...config.schemas.map(schema => extractResolversFromSchema(schema)), | ||
...config.resolvers ? asArray<IResolvers>(config.resolvers) : [] | ||
]); | ||
|
||
return makeExecutableSchema({ | ||
typeDefs, | ||
resolvers, | ||
schemaDirectives: config.schemaDirectives, | ||
resolverValidationOptions: config.resolverValidationOptions | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { ArgumentNode, DirectiveNode } from 'graphql/language/ast'; | ||
import { DirectiveDefinitionNode, ListValueNode, NameNode, print } from 'graphql'; | ||
|
||
function directiveAlreadyExists(directivesArr: ReadonlyArray<DirectiveNode>, otherDirective: DirectiveNode): boolean { | ||
return !!directivesArr.find(directive => directive.name.value === otherDirective.name.value); | ||
} | ||
|
||
function nameAlreadyExists(name: NameNode, namesArr: ReadonlyArray<NameNode>): boolean { | ||
return namesArr.some(({ value }) => value === name.value); | ||
} | ||
|
||
function mergeArguments(a1: ArgumentNode[], a2: ArgumentNode[]): ArgumentNode[] { | ||
const result: ArgumentNode[] = [...a2]; | ||
|
||
for (const argument of a1) { | ||
const existingIndex = result.findIndex(a => a.name.value === argument.name.value); | ||
|
||
if (existingIndex > -1) { | ||
const existingArg = result[existingIndex]; | ||
|
||
if (existingArg.value.kind === 'ListValue') { | ||
(existingArg.value as any).values = [ | ||
...existingArg.value.values, | ||
...(argument.value as ListValueNode).values, | ||
]; | ||
} else { | ||
(existingArg as any).value = argument.value; | ||
} | ||
} else { | ||
result.push(argument); | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
|
||
export function mergeDirectives(d1: ReadonlyArray<DirectiveNode>, d2: ReadonlyArray<DirectiveNode>): DirectiveNode[] { | ||
const result = [...d2]; | ||
|
||
for (const directive of d1) { | ||
if (directiveAlreadyExists(result, directive)) { | ||
const existingDirectiveIndex = result.findIndex(d => d.name.value === directive.name.value); | ||
const existingDirective = result[existingDirectiveIndex]; | ||
(result[existingDirectiveIndex] as any).arguments = mergeArguments(existingDirective.arguments as any, directive.arguments as any); | ||
} else { | ||
result.push(directive); | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
|
||
function validateInputs(node: DirectiveDefinitionNode, existingNode: DirectiveDefinitionNode): void | never { | ||
const printedNode = print(node); | ||
const printedExistingNode = print(existingNode); | ||
const leaveInputs = new RegExp('(directive @\w*\d*)|( on .*$)', 'g'); | ||
const sameArguments = printedNode.replace(leaveInputs, '') === printedExistingNode.replace(leaveInputs, ''); | ||
|
||
if (!sameArguments) { | ||
throw new Error(`Unable to merge GraphQL directive "${node.name.value}". \nExisting directive: \n\t${printedExistingNode} \nReceived directive: \n\t${printedNode}`); | ||
} | ||
} | ||
|
||
export function mergeDirective(node: DirectiveDefinitionNode, existingNode?: DirectiveDefinitionNode): DirectiveDefinitionNode { | ||
if (existingNode) { | ||
|
||
validateInputs(node, existingNode); | ||
|
||
return { | ||
...node, | ||
locations: [ | ||
...existingNode.locations, | ||
...(node.locations.filter(name => !nameAlreadyExists(name, existingNode.locations))), | ||
], | ||
}; | ||
} | ||
|
||
return node; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { EnumValueDefinitionNode } from 'graphql/language/ast'; | ||
|
||
function alreadyExists(arr: ReadonlyArray<EnumValueDefinitionNode>, other: EnumValueDefinitionNode): boolean { | ||
return !!arr.find(v => v.name.value === other.name.value); | ||
} | ||
|
||
export function mergeEnumValues(first: ReadonlyArray<EnumValueDefinitionNode>, second: ReadonlyArray<EnumValueDefinitionNode>): EnumValueDefinitionNode[] { | ||
return [ | ||
...second, | ||
...(first.filter(d => !alreadyExists(second, d))), | ||
]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { EnumTypeDefinitionNode, EnumTypeExtensionNode } from 'graphql'; | ||
import { mergeDirectives } from './directives'; | ||
import { mergeEnumValues } from './enum-values'; | ||
|
||
export function mergeEnum(e1: EnumTypeDefinitionNode | EnumTypeExtensionNode, e2: EnumTypeDefinitionNode | EnumTypeExtensionNode): EnumTypeDefinitionNode | EnumTypeExtensionNode { | ||
|
||
if (e2) { | ||
return { | ||
name: e1.name, | ||
description: e1['description'] || e2['description'], | ||
kind: (e1.kind === 'EnumTypeDefinition' || e2.kind === 'EnumTypeDefinition') ? 'EnumTypeDefinition' : 'EnumTypeExtension', | ||
loc: e1.loc, | ||
directives: mergeDirectives(e1.directives, e2.directives), | ||
values: mergeEnumValues(e1.values, e2.values), | ||
} as any; | ||
} | ||
|
||
return e1; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { FieldDefinitionNode } from 'graphql/language/ast'; | ||
import { extractType } from './utils'; | ||
import { mergeDirectives } from './directives'; | ||
|
||
function fieldAlreadyExists(fieldsArr: ReadonlyArray<any>, otherField: any): boolean { | ||
const result: FieldDefinitionNode | null = fieldsArr.find(field => field.name.value === otherField.name.value); | ||
|
||
if (result) { | ||
const t1 = extractType(result.type); | ||
const t2 = extractType(otherField.type); | ||
|
||
if (t1.name.value !== t2.name.value) { | ||
throw new Error(`Field "${otherField.name.value}" already defined with a different type. Declared as "${t1.name.value}", but you tried to override with "${t2.name.value}"`); | ||
} | ||
} | ||
|
||
return !!result; | ||
} | ||
|
||
export function mergeFields<T>(f1: ReadonlyArray<T>, f2: ReadonlyArray<T>): T[] { | ||
const result: T[] = [...f2]; | ||
|
||
for (const field of f1) { | ||
if (fieldAlreadyExists(result, field)) { | ||
const existing = result.find((f: any) => f.name.value === (field as any).name.value); | ||
existing['directives'] = mergeDirectives(field['directives'], existing['directives']); | ||
} else { | ||
result.push(field); | ||
} | ||
} | ||
|
||
return result; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { InputObjectTypeDefinitionNode } from 'graphql'; | ||
import { mergeFields } from './fields'; | ||
import { mergeDirectives } from './directives'; | ||
import { InputValueDefinitionNode, InputObjectTypeExtensionNode } from 'graphql/language/ast'; | ||
|
||
export function mergeInputType( | ||
node: InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode, | ||
existingNode: InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode): InputObjectTypeDefinitionNode | InputObjectTypeExtensionNode { | ||
|
||
if (existingNode) { | ||
try { | ||
return { | ||
name: node.name, | ||
description: node['description'] || existingNode['description'], | ||
kind: (node.kind === 'InputObjectTypeDefinition' || existingNode.kind === 'InputObjectTypeDefinition') ? 'InputObjectTypeDefinition' : 'InputObjectTypeExtension', | ||
loc: node.loc, | ||
fields: mergeFields<InputValueDefinitionNode>(node.fields, existingNode.fields), | ||
directives: mergeDirectives(node.directives, existingNode.directives), | ||
} as any; | ||
} catch (e) { | ||
throw new Error(`Unable to merge GraphQL input type "${node.name.value}": ${e.message}`); | ||
} | ||
} | ||
|
||
return node; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { InterfaceTypeDefinitionNode, InterfaceTypeExtensionNode } from 'graphql'; | ||
import { mergeFields } from './fields'; | ||
import { mergeDirectives } from './directives'; | ||
|
||
export function mergeInterface( | ||
node: InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode, | ||
existingNode: InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode): InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode { | ||
|
||
if (existingNode) { | ||
try { | ||
return { | ||
name: node.name, | ||
description: node['description'] || existingNode['description'], | ||
kind: (node.kind === 'InterfaceTypeDefinition' || existingNode.kind === 'InterfaceTypeDefinition') ? 'InterfaceTypeDefinition' : 'InterfaceTypeExtension', | ||
loc: node.loc, | ||
fields: mergeFields(node.fields, existingNode.fields), | ||
directives: mergeDirectives(node.directives, existingNode.directives), | ||
} as any; | ||
} catch (e) { | ||
throw new Error(`Unable to merge GraphQL interface "${node.name.value}": ${e.message}`); | ||
} | ||
} | ||
|
||
return node; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { NamedTypeNode } from 'graphql/language/ast'; | ||
|
||
function alreadyExists(arr: ReadonlyArray<NamedTypeNode>, other: NamedTypeNode): boolean { | ||
return !!arr.find(i => i.name.value === other.name.value); | ||
} | ||
|
||
export function mergeNamedTypeArray(first: ReadonlyArray<NamedTypeNode>, second: ReadonlyArray<NamedTypeNode>): NamedTypeNode[] { | ||
return [ | ||
...second, | ||
...(first.filter(d => !alreadyExists(second, d))), | ||
]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { DefinitionNode } from 'graphql'; | ||
import { | ||
isGraphQLEnum, | ||
isGraphQLInputType, | ||
isGraphQLInterface, | ||
isGraphQLScalar, | ||
isGraphQLType, | ||
isGraphQLUnion, | ||
isGraphQLDirective, | ||
isGraphQLTypeExtension, | ||
isGraphQLInputTypeExtension, | ||
isGraphQLEnumExtension, | ||
isGraphQLUnionExtension, | ||
isGraphQLScalarExtension, | ||
isGraphQLInterfaceExtension, | ||
} from './utils'; | ||
import { mergeType } from './type'; | ||
import { mergeEnum } from './enum'; | ||
import { mergeUnion } from './union'; | ||
import { mergeInputType } from './input-type'; | ||
import { mergeInterface } from './interface'; | ||
import { mergeDirective } from './directives'; | ||
|
||
export type MergedResultMap = {[name: string]: DefinitionNode}; | ||
|
||
export function mergeGraphQLNodes(nodes: ReadonlyArray<DefinitionNode>): MergedResultMap { | ||
return nodes.reduce<MergedResultMap>((prev: MergedResultMap, nodeDefinition: DefinitionNode) => { | ||
const node = (nodeDefinition as any); | ||
|
||
if (node && node.name && node.name.value) { | ||
const name = node.name.value; | ||
|
||
if (isGraphQLType(nodeDefinition) || isGraphQLTypeExtension(nodeDefinition)) { | ||
prev[name] = mergeType(nodeDefinition, prev[name] as any); | ||
} else if (isGraphQLEnum(nodeDefinition) || isGraphQLEnumExtension(nodeDefinition)) { | ||
prev[name] = mergeEnum(nodeDefinition, prev[name] as any); | ||
} else if (isGraphQLUnion(nodeDefinition) || isGraphQLUnionExtension(nodeDefinition)) { | ||
prev[name] = mergeUnion(nodeDefinition, prev[name] as any); | ||
} else if (isGraphQLScalar(nodeDefinition) || isGraphQLScalarExtension(nodeDefinition)) { | ||
prev[name] = nodeDefinition; | ||
} else if (isGraphQLInputType(nodeDefinition) || isGraphQLInputTypeExtension(nodeDefinition)) { | ||
prev[name] = mergeInputType(nodeDefinition, prev[name] as any); | ||
} else if (isGraphQLInterface(nodeDefinition) || isGraphQLInterfaceExtension(nodeDefinition)) { | ||
prev[name] = mergeInterface(nodeDefinition, prev[name] as any); | ||
} else if (isGraphQLDirective(nodeDefinition)) { | ||
prev[name] = mergeDirective(nodeDefinition, prev[name] as any); | ||
} | ||
} | ||
|
||
return prev; | ||
}, {}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { ObjectTypeDefinitionNode, ObjectTypeExtensionNode } from 'graphql'; | ||
import { mergeFields } from './fields'; | ||
import { mergeDirectives } from './directives'; | ||
import { mergeNamedTypeArray } from './merge-named-type-array'; | ||
|
||
export function mergeType(node: ObjectTypeDefinitionNode | ObjectTypeExtensionNode, existingNode: ObjectTypeDefinitionNode | ObjectTypeExtensionNode): ObjectTypeDefinitionNode | ObjectTypeExtensionNode { | ||
if (existingNode) { | ||
try { | ||
return { | ||
name: node.name, | ||
description: node['description'] || existingNode['description'], | ||
kind: (node.kind === 'ObjectTypeDefinition' || existingNode.kind === 'ObjectTypeDefinition') ? 'ObjectTypeDefinition' : 'ObjectTypeExtension', | ||
loc: node.loc, | ||
fields: mergeFields(node.fields, existingNode.fields), | ||
directives: mergeDirectives(node.directives, existingNode.directives), | ||
interfaces: mergeNamedTypeArray(node.interfaces, existingNode.interfaces), | ||
} as any; | ||
} catch (e) { | ||
throw new Error(`Unable to merge GraphQL type "${node.name.value}": ${e.message}`); | ||
} | ||
} | ||
|
||
return node; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { UnionTypeDefinitionNode, UnionTypeExtensionNode } from 'graphql'; | ||
import { mergeDirectives } from './directives'; | ||
import { mergeNamedTypeArray } from './merge-named-type-array'; | ||
|
||
export function mergeUnion(first: UnionTypeDefinitionNode | UnionTypeExtensionNode, second: UnionTypeDefinitionNode | UnionTypeExtensionNode): UnionTypeDefinitionNode | UnionTypeExtensionNode { | ||
if (second) { | ||
return { | ||
name: first.name, | ||
description: first['description'] || second['description'], | ||
directives: mergeDirectives(first.directives, second.directives), | ||
kind: (first.kind === 'UnionTypeDefinition' || second.kind === 'UnionTypeDefinition') ? 'UnionTypeDefinition' : 'UnionTypeExtension', | ||
loc: first.loc, | ||
types: mergeNamedTypeArray(first.types, second.types), | ||
} as any; | ||
} | ||
|
||
if (first.kind === 'UnionTypeExtension') { | ||
throw new Error(`Unable to extend undefined GraphQL union: ${first.name}`); | ||
} | ||
|
||
return first; | ||
} |
Oops, something went wrong.