1 |
|
2 | (function() {
|
3 | (function() {
|
4 | var _, addKeyValue, applyArrayChange, applyBranchChange, applyLeafChange, changeset, compare, compareArray, compareObject, comparePrimitives, convertArrayToObj, exports, getKey, getTypeOfObj, indexOfItemInArray, isEmbeddedKey, modifyKeyValue, parseEmbeddedKeyValue, removeKey, revertArrayChange, revertBranchChange, revertLeafChange;
|
5 | changeset = {
|
6 | VERSION: '0.1.4'
|
7 | };
|
8 | if (typeof module === 'object' && module.exports) {
|
9 | _ = require('lodash');
|
10 | module.exports = exports = changeset;
|
11 | } else {
|
12 | this.changeset = changeset;
|
13 | }
|
14 | getTypeOfObj = function(obj) {
|
15 | if (typeof obj === 'undefined') {
|
16 | return 'undefined';
|
17 | }
|
18 | if (obj === null) {
|
19 | return null;
|
20 | }
|
21 | return Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1];
|
22 | };
|
23 | getKey = function(path) {
|
24 | var ref;
|
25 | return (ref = path[path.length - 1]) != null ? ref : '$root';
|
26 | };
|
27 | compare = function(oldObj, newObj, path, embededObjKeys, keyPath) {
|
28 | var changes, diffs, typeOfNewObj, typeOfOldObj;
|
29 | changes = [];
|
30 | typeOfOldObj = getTypeOfObj(oldObj);
|
31 | typeOfNewObj = getTypeOfObj(newObj);
|
32 | if (typeOfOldObj !== typeOfNewObj) {
|
33 | changes.push({
|
34 | type: changeset.op.REMOVE,
|
35 | key: getKey(path),
|
36 | value: oldObj
|
37 | });
|
38 | changes.push({
|
39 | type: changeset.op.ADD,
|
40 | key: getKey(path),
|
41 | value: newObj
|
42 | });
|
43 | return changes;
|
44 | }
|
45 | switch (typeOfOldObj) {
|
46 | case 'Date':
|
47 | changes = changes.concat(comparePrimitives(oldObj.getTime(), newObj.getTime(), path));
|
48 | break;
|
49 | case 'Object':
|
50 | diffs = compareObject(oldObj, newObj, path, embededObjKeys, keyPath);
|
51 | if (diffs.length) {
|
52 | if (path.length) {
|
53 | changes.push({
|
54 | type: changeset.op.UPDATE,
|
55 | key: getKey(path),
|
56 | changes: diffs
|
57 | });
|
58 | } else {
|
59 | changes = changes.concat(diffs);
|
60 | }
|
61 | }
|
62 | break;
|
63 | case 'Array':
|
64 | changes = changes.concat(compareArray(oldObj, newObj, path, embededObjKeys, keyPath));
|
65 | break;
|
66 | case 'Function':
|
67 | break;
|
68 | default:
|
69 | changes = changes.concat(comparePrimitives(oldObj, newObj, path));
|
70 | }
|
71 | return changes;
|
72 | };
|
73 | compareObject = function(oldObj, newObj, path, embededObjKeys, keyPath, skipPath) {
|
74 | var addedKeys, changes, deletedKeys, diffs, i, intersectionKeys, j, k, l, len, len1, len2, newKeyPath, newObjKeys, newPath, oldObjKeys;
|
75 | if (skipPath == null) {
|
76 | skipPath = false;
|
77 | }
|
78 | changes = [];
|
79 | oldObjKeys = Object.keys(oldObj);
|
80 | newObjKeys = Object.keys(newObj);
|
81 | intersectionKeys = _.intersection(oldObjKeys, newObjKeys);
|
82 | for (i = 0, len = intersectionKeys.length; i < len; i++) {
|
83 | k = intersectionKeys[i];
|
84 | newPath = path.concat([k]);
|
85 | newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
86 | diffs = compare(oldObj[k], newObj[k], newPath, embededObjKeys, newKeyPath);
|
87 | if (diffs.length) {
|
88 | changes = changes.concat(diffs);
|
89 | }
|
90 | }
|
91 | addedKeys = _.difference(newObjKeys, oldObjKeys);
|
92 | for (j = 0, len1 = addedKeys.length; j < len1; j++) {
|
93 | k = addedKeys[j];
|
94 | newPath = path.concat([k]);
|
95 | newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
96 | changes.push({
|
97 | type: changeset.op.ADD,
|
98 | key: getKey(newPath),
|
99 | value: newObj[k]
|
100 | });
|
101 | }
|
102 | deletedKeys = _.difference(oldObjKeys, newObjKeys);
|
103 | for (l = 0, len2 = deletedKeys.length; l < len2; l++) {
|
104 | k = deletedKeys[l];
|
105 | newPath = path.concat([k]);
|
106 | newKeyPath = skipPath ? keyPath : keyPath.concat([k]);
|
107 | changes.push({
|
108 | type: changeset.op.REMOVE,
|
109 | key: getKey(newPath),
|
110 | value: oldObj[k]
|
111 | });
|
112 | }
|
113 | return changes;
|
114 | };
|
115 | compareArray = function(oldObj, newObj, path, embededObjKeys, keyPath) {
|
116 | var diffs, indexedNewObj, indexedOldObj, ref, uniqKey;
|
117 | uniqKey = (ref = embededObjKeys != null ? embededObjKeys[keyPath.join('.')] : void 0) != null ? ref : '$index';
|
118 | indexedOldObj = convertArrayToObj(oldObj, uniqKey);
|
119 | indexedNewObj = convertArrayToObj(newObj, uniqKey);
|
120 | diffs = compareObject(indexedOldObj, indexedNewObj, path, embededObjKeys, keyPath, true);
|
121 | if (diffs.length) {
|
122 | return [
|
123 | {
|
124 | type: changeset.op.UPDATE,
|
125 | key: getKey(path),
|
126 | embededKey: uniqKey,
|
127 | changes: diffs
|
128 | }
|
129 | ];
|
130 | } else {
|
131 | return [];
|
132 | }
|
133 | };
|
134 | convertArrayToObj = function(arr, uniqKey) {
|
135 | var index, obj, value;
|
136 | obj = {};
|
137 | if (uniqKey !== '$index') {
|
138 | obj = _.indexBy(arr, uniqKey);
|
139 | } else {
|
140 | for (index in arr) {
|
141 | value = arr[index];
|
142 | obj[index] = value;
|
143 | }
|
144 | }
|
145 | return obj;
|
146 | };
|
147 | comparePrimitives = function(oldObj, newObj, path) {
|
148 | var changes;
|
149 | changes = [];
|
150 | if (oldObj !== newObj) {
|
151 | changes.push({
|
152 | type: changeset.op.UPDATE,
|
153 | key: getKey(path),
|
154 | value: newObj,
|
155 | oldValue: oldObj
|
156 | });
|
157 | }
|
158 | return changes;
|
159 | };
|
160 | isEmbeddedKey = function(key) {
|
161 | return /\$.*=/gi.test(key);
|
162 | };
|
163 | removeKey = function(obj, key, embededKey) {
|
164 | var index;
|
165 | if (Array.isArray(obj)) {
|
166 | if (embededKey !== '$index' || !obj[key]) {
|
167 | index = indexOfItemInArray(obj, embededKey, key);
|
168 | }
|
169 | return obj.splice(index != null ? index : key, 1);
|
170 | } else {
|
171 | return delete obj[key];
|
172 | }
|
173 | };
|
174 | indexOfItemInArray = function(arr, key, value) {
|
175 | var index, item;
|
176 | for (index in arr) {
|
177 | item = arr[index];
|
178 | if (key === '$index') {
|
179 | if (item === value) {
|
180 | return index;
|
181 | }
|
182 | } else if (item[key] === value) {
|
183 | return index;
|
184 | }
|
185 | }
|
186 | return -1;
|
187 | };
|
188 | modifyKeyValue = function(obj, key, value) {
|
189 | return obj[key] = value;
|
190 | };
|
191 | addKeyValue = function(obj, key, value) {
|
192 | if (Array.isArray(obj)) {
|
193 | return obj.push(value);
|
194 | } else {
|
195 | return obj[key] = value;
|
196 | }
|
197 | };
|
198 | parseEmbeddedKeyValue = function(key) {
|
199 | var uniqKey, value;
|
200 | uniqKey = key.substring(1, key.indexOf('='));
|
201 | value = key.substring(key.indexOf('=') + 1);
|
202 | return {
|
203 | uniqKey: uniqKey,
|
204 | value: value
|
205 | };
|
206 | };
|
207 | applyLeafChange = function(obj, change, embededKey) {
|
208 | var key, type, value;
|
209 | type = change.type, key = change.key, value = change.value;
|
210 | switch (type) {
|
211 | case changeset.op.ADD:
|
212 | return addKeyValue(obj, key, value);
|
213 | case changeset.op.UPDATE:
|
214 | return modifyKeyValue(obj, key, value);
|
215 | case changeset.op.REMOVE:
|
216 | return removeKey(obj, key, embededKey);
|
217 | }
|
218 | };
|
219 | applyArrayChange = function(arr, change) {
|
220 | var element, i, len, ref, results, subchange;
|
221 | ref = change.changes;
|
222 | results = [];
|
223 | for (i = 0, len = ref.length; i < len; i++) {
|
224 | subchange = ref[i];
|
225 | if ((subchange.value != null) || subchange.type === changeset.op.REMOVE) {
|
226 | results.push(applyLeafChange(arr, subchange, change.embededKey));
|
227 | } else {
|
228 | if (change.embededKey === '$index') {
|
229 | element = arr[+subchange.key];
|
230 | } else {
|
231 | element = _.find(arr, function(el) {
|
232 | return el[change.embededKey] === subchange.key;
|
233 | });
|
234 | }
|
235 | results.push(changeset.applyChanges(element, subchange.changes));
|
236 | }
|
237 | }
|
238 | return results;
|
239 | };
|
240 | applyBranchChange = function(obj, change) {
|
241 | if (Array.isArray(obj)) {
|
242 | return applyArrayChange(obj, change);
|
243 | } else {
|
244 | return changeset.applyChanges(obj[change.key], change.changes);
|
245 | }
|
246 | };
|
247 | revertLeafChange = function(obj, change, embededKey) {
|
248 | var key, oldValue, type, value;
|
249 | type = change.type, key = change.key, value = change.value, oldValue = change.oldValue;
|
250 | switch (type) {
|
251 | case changeset.op.ADD:
|
252 | return removeKey(obj, key, embededKey);
|
253 | case changeset.op.UPDATE:
|
254 | return modifyKeyValue(obj, key, oldValue);
|
255 | case changeset.op.REMOVE:
|
256 | return addKeyValue(obj, key, value);
|
257 | }
|
258 | };
|
259 | revertArrayChange = function(arr, change) {
|
260 | var element, i, len, ref, results, subchange;
|
261 | ref = change.changes;
|
262 | results = [];
|
263 | for (i = 0, len = ref.length; i < len; i++) {
|
264 | subchange = ref[i];
|
265 | if ((subchange.value != null) || subchange.type === changeset.op.REMOVE) {
|
266 | results.push(revertLeafChange(arr, subchange, change.embededKey));
|
267 | } else {
|
268 | if (change.embededKey === '$index') {
|
269 | element = arr[+subchange.key];
|
270 | } else {
|
271 | element = _.find(arr, function(el) {
|
272 | return el[change.embededKey] === subchange.key;
|
273 | });
|
274 | }
|
275 | results.push(changeset.revertChanges(element, subchange.changes));
|
276 | }
|
277 | }
|
278 | return results;
|
279 | };
|
280 | revertBranchChange = function(obj, change) {
|
281 | if (Array.isArray(obj)) {
|
282 | return revertArrayChange(obj, change);
|
283 | } else {
|
284 | return changeset.revertChanges(obj[change.key], change.changes);
|
285 | }
|
286 | };
|
287 | changeset.diff = function(oldObj, newObj, embededObjKeys) {
|
288 | return compare(oldObj, newObj, [], embededObjKeys, []);
|
289 | };
|
290 | changeset.applyChanges = function(obj, changesets) {
|
291 | var change, i, len, results;
|
292 | results = [];
|
293 | for (i = 0, len = changesets.length; i < len; i++) {
|
294 | change = changesets[i];
|
295 | if ((change.value != null) || change.type === changeset.op.REMOVE) {
|
296 | results.push(applyLeafChange(obj, change, change.embededKey));
|
297 | } else {
|
298 | results.push(applyBranchChange(obj[change.key], change));
|
299 | }
|
300 | }
|
301 | return results;
|
302 | };
|
303 | changeset.revertChanges = function(obj, changeset) {
|
304 | var change, i, len, ref, results;
|
305 | ref = changeset.reverse();
|
306 | results = [];
|
307 | for (i = 0, len = ref.length; i < len; i++) {
|
308 | change = ref[i];
|
309 | if (!change.changes) {
|
310 | results.push(revertLeafChange(obj, change));
|
311 | } else {
|
312 | results.push(revertBranchChange(obj[change.key], change));
|
313 | }
|
314 | }
|
315 | return results;
|
316 | };
|
317 | changeset.op = {
|
318 | REMOVE: 'remove',
|
319 | ADD: 'add',
|
320 | UPDATE: 'update'
|
321 | };
|
322 | })();
|
323 |
|
324 | }).call(this);
|