Skip to content

Commit

Permalink
Introduce mergeSchemas
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Feb 12, 2019
1 parent 85b1fe4 commit 89554cd
Show file tree
Hide file tree
Showing 18 changed files with 678 additions and 39 deletions.
3 changes: 2 additions & 1 deletion src/epoxy/index.ts
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';
32 changes: 32 additions & 0 deletions src/epoxy/merge-schemas.ts
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
})
}
79 changes: 79 additions & 0 deletions src/epoxy/typedefs-mergers/directives.ts
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;
}
12 changes: 12 additions & 0 deletions src/epoxy/typedefs-mergers/enum-values.ts
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))),
];
}
20 changes: 20 additions & 0 deletions src/epoxy/typedefs-mergers/enum.ts
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;

}
33 changes: 33 additions & 0 deletions src/epoxy/typedefs-mergers/fields.ts
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;
}
26 changes: 26 additions & 0 deletions src/epoxy/typedefs-mergers/input-type.ts
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;
}
25 changes: 25 additions & 0 deletions src/epoxy/typedefs-mergers/interface.ts
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;
}
12 changes: 12 additions & 0 deletions src/epoxy/typedefs-mergers/merge-named-type-array.ts
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))),
];
}
52 changes: 52 additions & 0 deletions src/epoxy/typedefs-mergers/merge-nodes.ts
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;
}, {});
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ interface Config {
useSchemaDefinition?: boolean;
}

export function mergeGraphQLSchemas(types: Array<string | Source | DocumentNode | GraphQLSchema>, config?: Partial<Config>): DocumentNode {
export function mergeTypeDefs(types: Array<string | Source | DocumentNode | GraphQLSchema>, config?: Partial<Config>): DocumentNode {
return {
kind: 'Document',
definitions: mergeGraphQLTypes(types, {
Expand Down
24 changes: 24 additions & 0 deletions src/epoxy/typedefs-mergers/type.ts
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;
}
22 changes: 22 additions & 0 deletions src/epoxy/typedefs-mergers/union.ts
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;
}
Loading

0 comments on commit 89554cd

Please sign in to comment.