tree-changes
Version:
Get changes between two versions of data with similar shape
136 lines (112 loc) • 3.68 kB
text/typescript
import equal from '@gilbarbara/deep-equal';
import is from 'is-lite';
import { compareNumbers, compareValues, getIterables, includesOrEqualsTo, nested } from './helpers';
import { Data, KeyType, TreeChanges, Value } from './types';
export default function treeChanges<P extends Data, D extends Data, K = KeyType<P, D>>(
previousData: P,
data: D,
): TreeChanges<K> {
if ([previousData, data].some(is.nullOrUndefined)) {
throw new Error('Missing required parameters');
}
if (![previousData, data].every(d => is.plainObject(d) || is.array(d))) {
throw new Error('Expected plain objects or array');
}
const added = (key?: K, value?: Value): boolean => {
try {
return compareValues<K>(previousData, data, { key, type: 'added', value });
} catch {
/* istanbul ignore next */
return false;
}
};
const changed = (key?: K | string, actual?: Value, previous?: Value): boolean => {
try {
const left = nested(previousData, key);
const right = nested(data, key);
const hasActual = is.defined(actual);
const hasPrevious = is.defined(previous);
if (hasActual || hasPrevious) {
const leftComparator = hasPrevious
? includesOrEqualsTo(previous, left)
: !includesOrEqualsTo(actual, left);
const rightComparator = includesOrEqualsTo(actual, right);
return leftComparator && rightComparator;
}
if ([left, right].every(is.array) || [left, right].every(is.plainObject)) {
return !equal(left, right);
}
return left !== right;
} catch {
/* istanbul ignore next */
return false;
}
};
const changedFrom = (key: K | string, previous: Value, actual?: Value): boolean => {
if (!is.defined(key)) {
return false;
}
try {
const left = nested(previousData, key);
const right = nested(data, key);
const hasActual = is.defined(actual);
return (
includesOrEqualsTo(previous, left) &&
(hasActual ? includesOrEqualsTo(actual, right) : !hasActual)
);
} catch {
/* istanbul ignore next */
return false;
}
};
const decreased = (key: K, actual?: Value, previous?: Value): boolean => {
if (!is.defined(key)) {
return false;
}
try {
return compareNumbers<K>(previousData, data, { key, actual, previous, type: 'decreased' });
} catch {
/* istanbul ignore next */
return false;
}
};
const emptied = (key?: K): boolean => {
try {
const [left, right] = getIterables(previousData, data, { key });
return !!left.length && !right.length;
} catch {
/* istanbul ignore next */
return false;
}
};
const filled = (key?: K): boolean => {
try {
const [left, right] = getIterables(previousData, data, { key });
return !left.length && !!right.length;
} catch {
/* istanbul ignore next */
return false;
}
};
const increased = (key: K, actual?: Value, previous?: Value): boolean => {
if (!is.defined(key)) {
return false;
}
try {
return compareNumbers<K>(previousData, data, { key, actual, previous, type: 'increased' });
} catch {
/* istanbul ignore next */
return false;
}
};
const removed = (key?: K, value?: Value): boolean => {
try {
return compareValues<K>(previousData, data, { key, type: 'removed', value });
} catch {
/* istanbul ignore next */
return false;
}
};
return { added, changed, changedFrom, decreased, emptied, filled, increased, removed };
}
export type { Data, KeyType, TreeChanges, Value } from './types';