entity-diff
Version:
A simple entity diff generator.
242 lines (176 loc) • 6.48 kB
text/typescript
import { copySkeleton, isArray, isObject } from "./utils";
export enum DiffType {
NEW = "NEW",
MODIFIED = "MODIFIED",
REMOVED = "REMOVED",
ARRAY = "ARRAY",
}
interface AuditProps {
ignore?: string[];
options?: AuditKeyOptions[];
}
export interface AuditKeyOptions {
key: string;
title?: string;
customFormatter?: (object: any) => string;
arrayOptions?: {
key?: string;
name?: string;
}
}
export interface Diff {
key: string;
from?: string;
to?: string;
details?: Diff[];
type?: DiffType;
}
export class Audit {
private ignore: string[];
private options: AuditKeyOptions[];
constructor({ ignore, options }: AuditProps = { ignore: [], options: [] }) {
this.ignore = ignore ?? [];
this.options = options ?? [];
}
public diff(from: any, to: any): Diff[] {
const root: Diff[] = [];
return Object.keys({ ...from, ...to })
.filter(key => this.hasDiff(from, to, key))
.reduce((diffs, key) => this.deepDiffs(
diffs,
from[key],
to[key],
key), root);
}
private deepDiffs(diffs: Diff[], from: any, to: any, key: string) {
if (isArray(from, to)) {
return this.addArrayDiff(diffs, from, to, key);
}
if (isObject(from, to)) {
return this.addObjectDiff(diffs, from, to, key)
}
return this.addSimpleAudit(diffs, from, to, key);
}
private addSimpleAudit(diffs: Diff[], from: any, to: any, key: string): Diff[] {
const options = this.findKeyOptions(key);
let diff: Diff = {
key: options?.title ?? key,
from: from ?? null,
to: to ?? null
};
if (options?.customFormatter && from) {
diff.from = options.customFormatter(from);
}
if (options?.customFormatter && to) {
diff.to = options.customFormatter(to);
}
diffs.push(diff);
return diffs;
}
private addObjectDiff(diffs: Diff[], from: any, to: any, key: string): Diff[] {
const objectDiffs = this.diff(from, to);
if (objectDiffs && objectDiffs.length) {
const options = this.findKeyOptions(key);
diffs.push({
key: options?.title ?? key,
type: this.diffTypeDiscover(from, to),
details: objectDiffs,
});
}
return diffs;
}
private addArrayDiff(diffs: Diff[], from: any, to: any, key: string): Diff[] {
const arrayDiffs = this.arrayDetails(from, to, key);
if (arrayDiffs && arrayDiffs.length) {
const options = this.findKeyOptions(key);
diffs.push({
key: options?.title ?? key,
type: DiffType.ARRAY,
details: arrayDiffs,
});
}
return diffs;
}
private arrayDetails(from: any, to: any, key: string): Diff[] {
if (!from) {
from = copySkeleton(from);
}
const modified = this.findModifiedDiffs(from, to, key);
const add = this.findDiffsFromLeftToRight(to, from, key, DiffType.NEW);
const remove = this.findDiffsFromLeftToRight(from, to, key, DiffType.REMOVED);
return [
...modified,
...add,
...remove,
];
}
private findModifiedDiffs(right: any[], left: any[], key: string): Diff[] {
const root: Diff[] = [];
return right.reduce((modified: Diff[], item) => {
const options = this.findKeyOptions(key);
if (options?.arrayOptions) {
const leftItem = this.findItemInAnotherArray(left, item, options.arrayOptions.key ?? "id");
if (leftItem && item !== leftItem) {
const diffs = this.diff(item, leftItem);
if (diffs && diffs.length) {
const diff: Diff = {
key: item[options?.arrayOptions?.name] ?? key,
type: DiffType.MODIFIED,
details: diffs,
}
return [...modified, diff];
}
}
}
return modified;
}, root);
}
private findDiffsFromLeftToRight(right: any[], left: any[], key: string, type: DiffType) {
const root: Diff[] = [];
return right.reduce((modified: Diff[], item) => {
const options = this.findKeyOptions(key);
if (options?.arrayOptions) {
const notExists = this.notExistInAnotherArray(left, item, options.arrayOptions.key ?? "id");
let diffs: Diff[] = [];
if (notExists && type === DiffType.NEW) {
diffs = this.diff(copySkeleton(item), item);
}
if (notExists && type === DiffType.REMOVED) {
diffs = this.diff(item, copySkeleton(item));
}
if (diffs.length) {
const diff: Diff = {
key: item[options?.arrayOptions?.name] ?? key,
type,
details: diffs,
}
return [...modified, diff];
}
}
return modified;
}, root)
}
private findKeyOptions(key: string): AuditKeyOptions | undefined {
return this.options.find(({ key: option }) => option === key);
}
private findItemInAnotherArray(array: any[], itemFromAnother: any, key: string): any {
return array.find(item => item[key] === itemFromAnother[key]);
}
private notExistInAnotherArray(array: any[], itemFromAnother: any, key: string): boolean {
return array.every(item => item[key] !== itemFromAnother[key]);
}
private hasDiff(from: any, to: any, key: string) {
return (from[key] !== to[key]) && !this.ignore.includes(key);
}
private diffTypeDiscover(from: any, to: any): DiffType {
if ((from !== null) && (to !== null)) {
return DiffType.MODIFIED
}
if ((from !== null) && (to === null)) {
return DiffType.REMOVED
}
if ((from === null) && (to !== null)) {
return DiffType.NEW
}
}
}