@finnair/diff
Version:
Object Diff Based on Paths And Valudes of an Object
97 lines (96 loc) • 3.45 kB
JavaScript
import { Path } from '@finnair/path';
export const defaultDiffFilter = (_path, value) => value !== undefined;
const OBJECT = Object.freeze({});
const ARRAY = Object.freeze([]);
const primitiveTypes = {
'boolean': true,
'number': true,
'string': true,
'bigint': true,
'symbol': true,
};
function isPrimitive(value) {
return value === null || value === undefined || !!primitiveTypes[typeof value];
}
export class Diff {
config;
constructor(config) {
this.config = config;
}
allPaths(value) {
const paths = new Set();
this.collectPathsAndValues(value, (node) => {
paths.add(node.path.toJSON());
});
return paths;
}
changedPaths(a, b) {
return new Set(this.changeset(a, b).keys());
}
changeset(a, b) {
const changeset = new Map();
const aMap = this.pathsAndValues(a);
this.collectPathsAndValues(b, (bNode) => {
const path = bNode.path;
const bValue = bNode.value;
const pathStr = path.toJSON();
if (aMap.has(pathStr)) {
const aNode = aMap.get(pathStr);
const aValue = aNode?.value;
aMap.delete(pathStr);
if (!this.isEqual(aValue, bValue, path)) {
changeset.set(pathStr, { path, oldValue: aValue, newValue: this.getNewValue(bValue) });
}
}
else {
changeset.set(pathStr, { path, newValue: this.getNewValue(bValue) });
}
}, Path.ROOT);
aMap.forEach((node, pathStr) => changeset.set(pathStr, { path: node.path, oldValue: node.value }));
return changeset;
}
pathsAndValues(value) {
const map = new Map();
this.collectPathsAndValues(value, (node) => map.set(node.path.toJSON(), { path: node.path, value: node.value }), Path.ROOT);
return map;
}
collectPathsAndValues(value, collector, path = Path.ROOT) {
if ((this.config?.filter ?? defaultDiffFilter)(path, value)) {
if (isPrimitive(value) || this.config?.isPrimitive?.(value, path)) {
collector({ path, value });
}
else if (typeof value === 'object') {
if (Array.isArray(value)) {
if (this.config?.includeObjects) {
collector({ path, value: ARRAY });
}
value.forEach((element, index) => this.collectPathsAndValues(element, collector, path.index(index)));
}
else if (value.constructor === Object) {
if (this.config?.includeObjects) {
collector({ path, value: OBJECT });
}
Object.keys(value).forEach((key) => this.collectPathsAndValues(value[key], collector, path.property(key)));
}
else {
throw new Error(`only primitives, arrays and plain objects are supported, got "${value.constructor.name}"`);
}
}
}
}
isEqual(a, b, path) {
if (a === b) {
return true;
}
return !!this.config?.isEqual?.(a, b, path);
}
getNewValue(value) {
if (value === OBJECT) {
return {};
}
if (value === ARRAY) {
return [];
}
return value;
}
}