Skip to content

Instantly share code, notes, and snippets.

@jarwol
Created January 13, 2017 20:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jarwol/9e7e5341a5992d7b299187bce2869f46 to your computer and use it in GitHub Desktop.
Save jarwol/9e7e5341a5992d7b299187bce2869f46 to your computer and use it in GitHub Desktop.
GraphQL diff() and merge() extensions
'use strict';
/**
* This module extends the graphql.js schema and types by adding diff and merge functions.
*/
(function () {
const GraphQLSchema = require('graphql/type/schema').GraphQLSchema,
GraphQLObjectType = require('graphql/type/definition').GraphQLObjectType,
GraphQLScalarType = require('graphql/type/definition').GraphQLScalarType,
GraphQLUnionType = require('graphql/type/definition').GraphQLUnionType,
GraphQLEnumType = require('graphql/type/definition').GraphQLEnumType,
GraphQLNonNull = require('graphql/type/definition').GraphQLNonNull,
GraphQLList = require('graphql/type/definition').GraphQLList,
GraphQLInterfaceType = require('graphql/type/definition').GraphQLInterfaceType,
format = require('util').format,
cloneDeep = require('lodash.clonedeep');
// Diff extensions
GraphQLSchema.prototype.diff = diffSchema;
GraphQLObjectType.prototype.diff = GraphQLInterfaceType.prototype.diff = diffObjectTypes;
GraphQLEnumType.prototype.diff = diffEnumTypes;
GraphQLScalarType.prototype.diff = diffScalarTypes;
GraphQLUnionType.prototype.diff = diffUnionTypes;
// Merge extensions
GraphQLSchema.prototype.merge = mergeSchema;
GraphQLObjectType.prototype.merge = GraphQLInterfaceType.prototype.merge = mergeObjectTypes;
GraphQLList.prototype.merge = GraphQLNonNull.prototype.merge = GraphQLScalarType.prototype.merge = GraphQLEnumType.prototype.merge = overwriteType;
/*****************************************************
* DIFF
*****************************************************/
/**
* Reports differences between this GraphQLSchema and another one by diffing all of the types.
* @param other another GraphQLSchema
* @returns {Array} list of descriptions of the differences between the schemas
*/
function diffSchema(other) {
let diffs = [];
if (!other || !(other instanceof GraphQLSchema)) {
throw new TypeError('Cannot diff with null/undefined or non-GraphQLSchema object.');
}
for (let key in this.getTypeMap()) {
const thisType = this.getTypeMap()[key];
const otherType = other.getTypeMap()[key];
diffs = diffs.concat(thisType.diff(otherType));
}
for (let key in other.getTypeMap()) {
const thisType = this.getTypeMap()[key];
if (!thisType) {
diffs.push('Type missing from this schema: ' + key);
}
}
return diffs;
}
/**
* Reports differences between this GraphQLScalarType and another.
* @param other another GraphQLScalarType
* @returns {Array} list of descriptions of the differences between the GraphQLScalarTypes
*/
function diffScalarTypes(other) {
if (!other) {
return ['Type missing from other schema: ' + this.name];
}
if (!(other instanceof GraphQLScalarType)) {
return [format('Type mismatch: `%s: GraphQLScalarType vs. %s: %s`', this.name, other.name, other.constructor.name)];
}
if (this.name !== other.name) {
return ['Type name difference: ' + this.name + ' | ' + other.name];
}
if (this.description != other.description) {
diffs.push(format('Description diff on type %s: `"%s" vs. "%s"`', this.name, this.description, other.description));
}
return [];
}
/**
* Reports differences between this GraphQLEnumType and another. The name and enum values are compared.
* @param other another GraphQLEnumType
* @returns {Array} list of descriptions of the differences
*/
function diffEnumTypes(other) {
let diffs = [];
if (!other) {
return ['Type missing from other schema: ' + this.name];
}
if (this.constructor.name !== other.constructor.name) {
return [format('Type mismatch: `%s: %s vs. %s: %s`', this.name, this.constructor.name, other.name, other.constructor.name)];
}
if (this.name !== other.name) {
return ['Type name difference: ' + this.name + ' | ' + other.name];
}
if (this.description != other.description) {
diffs.push(format('Description diff on type %s: `"%s" vs. "%s"`', this.name, this.description, other.description));
}
for (let i = 0; i < this.getValues().length; i++) {
if (!enumEquals(this.getValues()[i], other.getValues()[i])) {
diffs.push('Enum values differ in type ' + this.name);
}
}
return diffs;
}
/**
* Reports differences between this GraphQLUnionType and another.
* @param other another GraphQLUnionType
* @returns {Array} list of descriptions of the differences
*/
function diffUnionTypes(other) {
if (!other) {
return ['Type missing from other schema: ' + this.name];
}
if (this.constructor.name !== other.constructor.name) {
return [format('Type mismatch: `%s: %s vs. %s: %s`', this.name, this.constructor.name, other.name, other.constructor.name)];
}
if (this.name !== other.name) {
return ['Type name difference: ' + this.name + ' vs. ' + other.name];
}
if (this.description != other.description) {
diffs.push(format('Description diff on type %s: `"%s" vs. "%s"`', this.name, this.description, other.description));
}
const thisType = this.getTypes().map(type => {
return type.name;
}).sort().join(' | ');
const otherType = other.getTypes().map(type => {
return type.name;
}).sort().join(' | ');
if (thisType !== otherType) {
return [format('Difference in union type %s: `%s vs. %s`', this.name, thisType, otherType)];
}
return [];
}
/**
* Reports differences between this GraphQLObjectType or GraphQLInterfaceType and another. Fields and implemented interfaces are compared.
* @param other another GraphQLObjectType or GraphQLInterfaceType
* @returns {Array} list of descriptions of the differences
*/
function diffObjectTypes(other) {
let diffs = [];
if (!other) {
return ['Type missing from other schema: ' + this.name];
}
if (this.constructor.name !== other.constructor.name) {
return [format('Type mismatch: `%s: %s vs. %s: %s`', this.name, this.constructor.name, other.name, other.constructor.name)];
}
if (this.name !== other.name) {
diffs.push('Type name difference: ' + this.name + ' vs. ' + other.name);
}
if (this.description != other.description) {
diffs.push(format('Description diff on type %s: `"%s" vs. "%s"`', this.name, this.description, other.description));
}
diffs = diffs.concat(diffFields(this, other));
if (this instanceof GraphQLObjectType) {
diffs = diffs.concat(diffInterfaces(this, other)).concat(diffInterfaces(other, this));
}
return diffs;
}
function diffFields(thisType, otherType) {
let diffs = [];
for (let key in thisType.getFields()) {
if (thisType.getFields().hasOwnProperty(key)) {
const thisField = thisType.getFields()[key];
const otherField = otherType.getFields()[key];
if (!otherField) {
diffs.push(format('Field missing from other: `%s.%s`', thisType.name, getFieldString(thisField)));
continue;
}
const thisFieldString = getFieldString(thisField);
const otherFieldString = getFieldString(otherField);
if (thisFieldString !== otherFieldString) {
diffs.push(format('Field diff on type %s: `%s vs. %s`', thisType.name, thisFieldString, otherFieldString));
}
if (thisField.description != otherField.description) {
diffs.push(format('Description diff on field %s.%s: `"%s" vs. "%s"`', thisType.name, key, thisField.description, otherField.description));
}
diffs = diffs.concat(diffArgDescriptions(thisType.name, thisField, otherField));
}
}
for (let key in otherType.getFields()) {
if (otherType.getFields().hasOwnProperty(key)) {
const thisField = thisType.getFields()[key];
const otherField = otherType.getFields()[key];
if (!thisField) {
diffs.push(format('Field missing from this: `%s.%s`', thisType.name, getFieldString(otherField)));
}
}
}
return diffs;
}
function diffArgDescriptions(typeName, thisField, otherField) {
const thisArgs = new Map(thisField.args.map(arg => [arg.name, arg]));
return otherField.args.map(arg => {
if (thisArgs.has(arg.name)) {
const thisDescription = thisArgs.get(arg.name).description;
const otherDescription = arg.description;
if (thisDescription !== otherDescription) {
return format('Description diff on argument %s.%s(%s): `"%s" vs. "%s"`', typeName, thisField.name, arg.name, thisDescription, otherDescription);
}
}
return null;
}).filter(str => !!str);
}
function diffInterfaces(thisType, otherType) {
let match = true;
for (let i = 0; i < thisType.getInterfaces().length; i++) {
match = otherType.getInterfaces().some(item => {
return item.name === thisType.getInterfaces()[i].name;
});
if (!match) {
break;
}
}
if (!match) {
return [format('Interface diff on type %s: `%s vs. %s`', thisType.name, thisType.getInterfaces().join(', '), otherType.getInterfaces().join(', '))];
}
return [];
}
function enumEquals(val1, val2) {
return val1.value === val2.value && val1.name === val2.name && val1.isDeprecated === val2.isDeprecated
&& val1.description === val2.description && val1.deprecationReason === val2.deprecationReason;
}
function getFieldString(field) {
return field.name + getArgsString(field) + ': ' + field.type.toString();
}
function getArgsString(field) {
if (!field.args.length) {
return '';
}
return '(' + field.args.map(arg => {
const defaultVal = arg.defaultValue ? ' = ' + arg.defaultValue : '';
return arg.name + ': ' + arg.type.toString() + defaultVal;
}).join(', ') + ')';
}
/*****************************************************
* MERGE
*****************************************************/
/**
* Merge this GraphQLSchema with another one. This schema's types and fields take precedence over other's.
* Does not modify either schema, but instead returns a new one.
* @param other another GraphQLSchema to merge with this one
* @returns {GraphQLSchema} new GraphQLSchema representing this merged with other
*/
function mergeSchema(other) {
if (!other || !(other instanceof GraphQLSchema)) {
throw new TypeError('Cannot merge with null/undefined or non-GraphQLSchema object.');
}
const merged = cloneDeep(this);
for (let key in this.getTypeMap()) {
const thisType = this.getTypeMap()[key];
const otherType = other.getTypeMap()[key];
merged._typeMap[key] = thisType.merge(otherType);
}
for (let key in other.getTypeMap()) {
const thisType = this.getTypeMap()[key];
const otherType = other.getTypeMap()[key];
if (!thisType) {
merged._typeMap[key] = otherType;
}
}
return merged;
}
/**
* Returns other type if it exists, otherwise this.
* @param other another GraphQL type object to merge with this
* @returns {GraphQLList|GraphQLNonNull|GraphQLScalarType|GraphQLEnum}
*/
function overwriteType(other) {
return other || this;
}
/**
* Merges another GraphQLObjectType or GraphQLInterfaceType with this one by taking the union of all fields in both types, overwriting this type's
* fields with the other's if there are conflicts. For GraphQLObjectTypes, implemented interfaces are also merged.
* @param other another GraphQL type to merge with this one
* @returns {GraphQLObjectType|GraphQLInterfaceType}
*/
function mergeObjectTypes(other) {
if (!other) {
return this;
}
if (this.constructor.name !== other.constructor.name) {
throw new TypeError(format('Cannot merge with different base type. this: %s, other: %s.', this.constructor.name, other.constructor.name));
}
const mergedType = cloneDeep(this);
const otherFields = Object.keys(other.getFields());
for (let i = 0; i < otherFields.length; i++) {
const key = otherFields[i];
if (other.getFields().hasOwnProperty(key)) {
mergedType._fields[key] = other.getFields()[key];
}
}
if (this instanceof GraphQLObjectType) {
mergedType._interfaces = Array.from(new Set(this.getInterfaces().concat(other.getInterfaces())));
}
return mergedType;
}
})();
@somil-nagarro
Copy link

Here using mergeObjectTypes method, we are overriding the rightType values to leftType instead of availing both. In this case query for left type will be error prone, as fields are replaced with rightType fields.
leftType._fields[key] = rightTYpe.getFields()[key];

can we create union of both type and return so query result wont be error prone.

Another issue is, if the rightType getfields contains any graphQL object then how to copy that graphql object to leftType schema available at run time. As at run time all right type have not yet merged with left type.

Ex: RightType
Type A {
product: Product
}

type Product {
name: String
}

leftType
Type A {
id: Int
}

while merging in onTypeConflict it gives error of 'No such type Product' error.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment