Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce mergeSchemas #19

Merged
merged 4 commits into from
Feb 13, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, mergeGraphQLSchemas } from './typedefs-mergers/merge-typedefs';
export { mergeResolvers } from './resolvers-mergers/merge-resolvers';
export { mergeSchemas } from './merge-schemas';
43 changes: 43 additions & 0 deletions src/epoxy/merge-schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { GraphQLSchema, DocumentNode } from "graphql";
import { IResolvers, SchemaDirectiveVisitor, makeExecutableSchema, IResolverValidationOptions, ILogger } from "graphql-tools";
import { mergeTypeDefs } from "./typedefs-mergers/merge-typedefs";
import { asArray } from "../utils/helpers";
import { mergeResolvers } from "./resolvers-mergers/merge-resolvers";
import { extractResolversFromSchema, IResolversComposerMapping, composeResolvers } from "../utils";

export interface MergeSchemasConfig {
schemas: GraphQLSchema[];
typeDefs?: (DocumentNode | string)[] | DocumentNode | string;
resolvers?: IResolvers | IResolvers[];
resolversComposition?: IResolversComposerMapping;
schemaDirectives ?: { [directiveName: string] : typeof SchemaDirectiveVisitor };
resolverValidationOptions ?: IResolverValidationOptions;
logger?: ILogger;
}

export function mergeSchemas({
schemas,
typeDefs,
resolvers,
resolversComposition,
schemaDirectives,
resolverValidationOptions,
logger
}: MergeSchemasConfig) {
return makeExecutableSchema({
typeDefs: mergeTypeDefs([
...schemas,
...typeDefs ? asArray(typeDefs) : []
]),
resolvers: composeResolvers(
mergeResolvers([
...schemas.map(schema => extractResolversFromSchema(schema)),
...resolvers ? asArray<IResolvers>(resolvers) : []
]),
resolversComposition || {}
),
schemaDirectives,
resolverValidationOptions,
logger
})
}
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,17 @@ interface Config {
useSchemaDefinition?: boolean;
}

export function mergeGraphQLSchemas(types: Array<string | Source | DocumentNode | GraphQLSchema>, config?: Partial<Config>): DocumentNode {
export function mergeGraphQLSchemas(...args: ArgsType<typeof mergeGraphQLTypes>): ReturnType<typeof mergeGraphQLTypes> {
console.info(`
GraphQL Toolkit/Epoxy
Deprecation Notice;
'mergeGraphQLSchemas' is deprecated and will be removed in the next version.
Please use 'mergeTypeDefs' instead!
`);
return mergeGraphQLTypes(...args);
}

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