@graphql-inspector/core
Version:
Tooling for GraphQL. Compare GraphQL Schemas, check documents, find breaking changes, find similar types.
176 lines (175 loc) • 5.42 kB
JavaScript
export function keyMap(list, keyFn) {
return list.reduce((map, item) => {
map[keyFn(item)] = item;
return map;
}, Object.create(null));
}
export function isEqual(a, b) {
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length)
return false;
for (let index = 0; index < a.length; index++) {
if (!isEqual(a[index], b[index])) {
return false;
}
}
return true;
}
if (a && b && typeof a === 'object' && typeof b === 'object') {
const aRecord = a;
const bRecord = b;
const aKeys = Object.keys(aRecord);
const bKeys = Object.keys(bRecord);
if (aKeys.length !== bKeys.length)
return false;
for (const key of aKeys) {
if (!isEqual(aRecord[key], bRecord[key])) {
return false;
}
}
return true;
}
return a === b || (!a && !b);
}
export function isNotEqual(a, b) {
return !isEqual(a, b);
}
export function isVoid(a) {
return typeof a === 'undefined' || a === null;
}
export function diffArrays(a, b) {
return a.filter(c => !b.some(d => isEqual(d, c)));
}
function extractName(name) {
if (typeof name === 'string') {
return name;
}
return name.value;
}
export function compareLists(oldList, newList, callbacks) {
const oldMap = keyMap(oldList, ({ name }) => extractName(name));
const newMap = keyMap(newList, ({ name }) => extractName(name));
const added = [];
const removed = [];
const mutual = [];
for (const oldItem of oldList) {
const newItem = newMap[extractName(oldItem.name)] ?? null;
if (newItem === null) {
removed.push(oldItem);
}
else {
mutual.push({
newVersion: newItem,
oldVersion: oldItem ?? null,
});
}
}
for (const newItem of newList) {
if (oldMap[extractName(newItem.name)] === undefined) {
added.push(newItem);
}
}
if (callbacks) {
if (callbacks.onRemoved) {
for (const item of removed) {
callbacks.onRemoved(item);
}
}
if (callbacks.onAdded) {
for (const item of added) {
callbacks.onAdded(item);
}
}
if (callbacks.onMutual) {
for (const item of mutual) {
callbacks.onMutual(item);
}
}
}
return {
added,
removed,
mutual,
};
}
/**
* This is special because directives can be repeated and a name alone is not enough
* to identify whether or not an instance was changed, added, or removed.
* The best option is to assume the order is the same, and treat the changes as they come.
* So `type T @foo` to `type T @foo(f: 'bar') @foo` would be adding an argument `f: 'bar'` and
* then adding a new directive `@foo`. Rather than adding `@foo(f: 'bar')`
*/
export function compareDirectiveLists(oldList, newList, callbacks) {
// collect all the usages, in order, by name for the old and new version of the schema
const oldMap = keyMapList(oldList, ({ name }) => extractName(name));
const newMap = keyMapList(newList, ({ name }) => extractName(name));
const added = [];
const removed = [];
const mutual = [];
for (const oldItem of oldList) {
// check if the oldItem exists in the new schema
const newItems = newMap[extractName(oldItem.name)] ?? null;
// if not, then it's been removed
if (newItems === null) {
removed.push(oldItem);
}
else {
// if so, then consider this a mutual change, and remove it from the list of newItems to avoid counting it in the future
const [newItem, ...rest] = newItems;
if (rest.length > 0) {
newMap[extractName(oldItem.name)] = rest;
}
else {
delete newMap[extractName(oldItem.name)];
}
mutual.push({
newVersion: newItem,
oldVersion: oldItem ?? null,
});
}
}
for (const newItem of newList) {
const existingItems = oldMap[extractName(newItem.name)] ?? null;
if (existingItems === null) {
added.push(newItem);
}
else {
const [_, ...rest] = existingItems;
if (rest.length > 0) {
oldMap[extractName(newItem.name)] = rest;
}
else {
delete oldMap[extractName(newItem.name)];
}
}
}
if (callbacks) {
if (callbacks.onRemoved) {
for (const item of removed) {
callbacks.onRemoved(item);
}
}
if (callbacks.onAdded) {
for (const item of added) {
callbacks.onAdded(item);
}
}
if (callbacks.onMutual) {
for (const item of mutual) {
callbacks.onMutual(item);
}
}
}
return {
added,
removed,
mutual,
};
}
export function keyMapList(list, keyFn) {
return list.reduce((map, item) => {
const key = keyFn(item);
map[key] = [...(map[key] ?? []), item];
return map;
}, Object.create(null));
}