UNPKG

tree-changes

Version:
184 lines (146 loc) 4.72 kB
import equal from '@gilbarbara/deep-equal'; import is from 'is-lite'; import { CompareValuesOptions, Data, Key, Options, ValidTypes, Value } from './types'; export function canHaveLength(...arguments_: any): boolean { return arguments_.every((d: unknown) => is.string(d) || is.array(d) || is.plainObject(d)); } export function checkEquality(left: Data, right: Data, value: Value) { if (!isSameType(left, right)) { return false; } if ([left, right].every(is.array)) { return !left.some(hasValue(value)) && right.some(hasValue(value)); } /* istanbul ignore else */ if ([left, right].every(is.plainObject)) { return ( !Object.entries(left).some(hasEntry(value)) && Object.entries(right).some(hasEntry(value)) ); } return right === value; } export function compareNumbers<K = Key>( previousData: Data, data: Data, options: Options<K>, ): boolean { const { actual, key, previous, type } = options; const left = nested(previousData, key); const right = nested(data, key); let changed = [left, right].every(is.number) && (type === 'increased' ? left < right : left > right); if (!is.undefined(actual)) { changed = changed && right === actual; } if (!is.undefined(previous)) { changed = changed && left === previous; } return changed; } export function compareValues<K = Key>( previousData: Data, data: Data, options: CompareValuesOptions<K>, ) { const { key, type, value } = options; const left = nested(previousData, key); const right = nested(data, key); const primary = type === 'added' ? left : right; const secondary = type === 'added' ? right : left; if (!is.nullOrUndefined(value)) { if (is.defined(primary)) { // check if nested data matches if (is.array(primary) || is.plainObject(primary)) { return checkEquality(primary, secondary, value); } } else { return equal(secondary, value); } return false; } if ([left, right].every(is.array)) { return !secondary.every(isEqualPredicate(primary)); } if ([left, right].every(is.plainObject)) { return hasExtraKeys(Object.keys(primary), Object.keys(secondary)); } return ( ![left, right].every(d => is.primitive(d) && is.defined(d)) && (type === 'added' ? !is.defined(left) && is.defined(right) : is.defined(left) && !is.defined(right)) ); } export function getIterables<K = Key>(previousData: Data, data: Data, { key }: Options<K> = {}) { let left = nested(previousData, key); let right = nested(data, key); if (!isSameType(left, right)) { throw new TypeError('Inputs have different types'); } if (!canHaveLength(left, right)) { throw new TypeError("Inputs don't have length"); } if ([left, right].every(is.plainObject)) { left = Object.keys(left); right = Object.keys(right); } return [left, right]; } export function hasEntry(input: Value) { return ([key, value]: [string, Value]) => { if (is.array(input)) { return ( equal(input, value) || input.some(d => equal(d, value) || (is.array(value) && isEqualPredicate(value)(d))) ); } /* istanbul ignore else */ if (is.plainObject(input) && input[key]) { return !!input[key] && equal(input[key], value); } return equal(input, value); }; } export function hasExtraKeys(left: string[], right: string[]): boolean { return right.some(d => !left.includes(d)); } export function hasValue(input: Value) { return (value: Value) => { if (is.array(input)) { return input.some(d => equal(d, value) || (is.array(value) && isEqualPredicate(value)(d))); } return equal(input, value); }; } export function includesOrEqualsTo<T>(previousValue: T | T[], value: T): boolean { return is.array(previousValue) ? previousValue.some(d => equal(d, value)) : equal(previousValue, value); } export function isEqualPredicate(data: unknown[]) { return (value: unknown) => data.some(d => equal(d, value)); } export function isSameType(...arguments_: ValidTypes[]): boolean { return ( arguments_.every(is.array) || arguments_.every(is.number) || arguments_.every(is.plainObject) || arguments_.every(is.string) ); } export function nested<T extends Data, K = Key>(data: T, property?: K) { /* istanbul ignore else */ if (is.plainObject(data) || is.array(data)) { /* istanbul ignore else */ if (is.string(property)) { const props: Array<any> = property.split('.'); return props.reduce((acc, d) => acc && acc[d], data); } /* istanbul ignore else */ if (is.number(property)) { return data[property]; } return data; } return data; }