-
Notifications
You must be signed in to change notification settings - Fork 1
/
ChangeLog.ts
157 lines (147 loc) · 4.83 KB
/
ChangeLog.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { DataSerializerUtils } from '../DataSerializerUtils';
export const CHANGELOG_METADATA_KEY = Symbol('__changelog__');
export interface SerializableChangelog {
[CHANGELOG_METADATA_KEY]?: ChangeLog;
}
export class ChangeLog {
/**
* Changes
*/
protected changes: Change[] = [];
/**
* Reset the log with the assumption that the object has been saved
*/
reset(): void {
this.changes = [];
}
/**
* Add a change to the log
* @param property {string} Property key
* @param oldValue {any} Old value
* @param newValue {any} New value
*/
addChange(property: string, oldValue: any, newValue: any) {
if (oldValue === newValue) {
return;
}
this.changes.push({
property,
oldValue,
newValue,
date: new Date(),
});
}
/**
* Get the latest changes
* @returns {Change[]} Latest changes
*/
getLatestChanges(): Change[] {
// Get the changes per property
const changesPerProperty: { [key: string]: Change[] } = {};
this.changes.forEach((change) => {
if (!changesPerProperty[change.property]) {
changesPerProperty[change.property] = [];
}
changesPerProperty[change.property].push(change);
});
// Sort the changes by date
Object.keys(changesPerProperty).forEach((property) => {
changesPerProperty[property].sort((a, b) => a.date.getTime() - b.date.getTime());
});
// Filter out changes that end with the same value as the initial state
const unchangedProperties: string[] = [];
Object.keys(changesPerProperty).forEach((property) => {
const lastIndex = changesPerProperty[property].length - 1;
if (changesPerProperty[property][0].oldValue === changesPerProperty[property][lastIndex].newValue) {
unchangedProperties.push(property);
}
});
// Remove the unchanged properties
Object.keys(changesPerProperty).forEach((property) => {
changesPerProperty[property] = changesPerProperty[property].filter(
() => !unchangedProperties.includes(property),
);
});
// Aggregate all changes of each properties
const changes = Object.keys(changesPerProperty)
.map((property) => {
const lastIndex = changesPerProperty[property].length - 1;
const firstChange = changesPerProperty[property][0];
const lastChange = changesPerProperty[property][lastIndex];
if (lastChange) {
return {
property,
oldValue: firstChange.oldValue,
newValue: lastChange.newValue,
date: lastChange.date,
};
}
return undefined;
})
.filter((p) => p !== undefined);
return changes;
}
/**
* Get the deleted properties
* @returns {string[]} Deleted properties
*/
getDeletedProperties(): string[] {
return this.getLatestChanges()
.filter((change) => change.newValue === undefined)
.map((change) => change.property);
}
/**
* Get the added properties
* @returns {string[]} Added properties
*/
getAddedProperties(): string[] {
return this.getLatestChanges()
.filter((change) => change.oldValue === undefined)
.map((change) => change.property);
}
}
export interface Change {
/**
* Property name
*/
property: string;
/**
* Old value
*/
oldValue: any;
/**
* New value
*/
newValue: any;
/**
* Change date
*/
date: Date;
}
/**
* Create a change log for an object
* @param target Target object
*/
export function createChangeLog<T extends Object>(target: T): T & SerializableChangelog { // eslint-disable-line
target[CHANGELOG_METADATA_KEY] = new ChangeLog();
// Wrap all data members with a changelog to track deep changes
const metadata = DataSerializerUtils.getOwnMetadata(target.constructor);
if (metadata) {
metadata.dataMembers.forEach((member) => {
if (target[member.key] && target[member.key] instanceof Object) {
target[member.key] = createChangeLog(target[member.key]);
}
});
}
// Wrap the target in a proxy to track changes
const proxy = new Proxy(target, {
set: (obj, prop, value) => {
if (obj[prop] !== value) {
obj[CHANGELOG_METADATA_KEY].addChange(prop.toString(), obj[prop], value);
}
obj[prop] = value;
return true;
},
}) as T & SerializableChangelog;
return proxy;
}