UNPKG

23.6 kBJavaScriptView Raw
1'use strict';
2
3const EventEmitter = require('eventemitter3');
4const keys = require('lodash.keys');
5const isObject = require('lodash.isplainobject');
6const isArray = require('lodash.isarray');
7const isEqual = require('lodash.isequal');
8const isString = require('lodash.isstring');
9const includes = require('lodash.includes');
10const ObjectGenerator = require('./object-generator');
11const TypeChecker = require('hadron-type-checker');
12const uuid = require('uuid');
13
14const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS';
15
16/**
17 * The event constant.
18 */
19const Events = {
20 'Added': 'Element::Added',
21 'Edited': 'Element::Edited',
22 'Removed': 'Element::Removed',
23 'Reverted': 'Element::Reverted',
24 'Converted': 'Element::Converted',
25 'Invalid': 'Element::Invalid',
26 'Valid': 'Element::Valid'
27};
28
29/**
30 * Id field constant.
31 */
32const ID = '_id';
33
34/**
35 * Types that are not editable.
36 */
37const UNEDITABLE_TYPES = [
38 'Binary',
39 'Code',
40 'MinKey',
41 'MaxKey',
42 'Timestamp',
43 'BSONRegExp',
44 'Undefined',
45 'Null'
46];
47
48/**
49 * Curly brace constant.
50 */
51const CURLY = '{';
52
53/**
54 * Bracket constant.
55 */
56const BRACKET = '[';
57
58/**
59 * Regex to match an array or object string.
60 */
61const ARRAY_OR_OBJECT = /^(\[|\{)(.+)(\]|\})$/;
62
63/**
64 * Represents an element in a document.
65 */
66class Element extends EventEmitter {
67 /**
68 * Bulk edit the element. Can accept JSON strings.
69 *
70 * @param {String} value - The JSON string value.
71 */
72 bulkEdit(value) {
73 if (value.match(ARRAY_OR_OBJECT)) {
74 this.edit(JSON.parse(value));
75 this._bubbleUp(Events.Converted);
76 } else {
77 this.edit(value);
78 }
79 }
80
81 /**
82 * Cancel any modifications to the element.
83 */
84 cancel() {
85 if (this.elements) {
86 for (let element of this.elements) {
87 element.cancel();
88 }
89 }
90 if (this.isModified()) {
91 this.revert();
92 }
93 }
94
95 /**
96 * Create the element.
97 *
98 * @param {String} key - The key.
99 * @param {Object} value - The value.
100 * @param {Boolean} added - Is the element a new 'addition'?
101 * @param {Element} parent - The parent element.
102 * @param {Element} previousElement - The previous element in the list.
103 * @param {Element} nextElement - The next element in the list.
104 */
105 constructor(key, value, added, parent, previousElement, nextElement) {
106 super();
107 this.uuid = uuid.v4();
108 this.key = key;
109 this.currentKey = key;
110 this.parent = parent;
111 this.previousElement = previousElement;
112 this.nextElement = nextElement;
113 this.added = added;
114 this.removed = false;
115 this.type = TypeChecker.type(value);
116 this.currentType = this.type;
117 this.setValid();
118
119 if (this._isExpandable(value)) {
120 this.elements = this._generateElements(value);
121 this.originalExpandableValue = value;
122 } else {
123 this.value = value;
124 this.currentValue = value;
125 }
126 }
127
128 /**
129 * Edit the element.
130 *
131 * @param {Object} value - The new value.
132 */
133 edit(value) {
134 this.currentType = TypeChecker.type(value);
135 if (this._isExpandable(value) && !this._isExpandable(this.currentValue)) {
136 this.currentValue = null;
137 this.elements = this._generateElements(value);
138 } else if (!this._isExpandable(value) && this.elements) {
139 this.currentValue = value;
140 this.elements = undefined;
141 } else {
142 this.currentValue = value;
143 }
144 this.setValid();
145 this._bubbleUp(Events.Edited);
146 }
147
148 /**
149 * Get an element by its key.
150 *
151 * @param {String} key - The key name.
152 *
153 * @returns {Element} The element.
154 */
155 get(key) {
156 return this.elements ? this.elements.get(key) : undefined;
157 }
158
159 /**
160 * Get an element by its index.
161 *
162 * @param {Number} i - The index.
163 *
164 * @returns {Element} The element.
165 */
166 at(i) {
167 return this.elements ? this.elements.at(i) : undefined;
168 }
169
170 /**
171 * Go to the next edit.
172 *
173 * Will check if the value is either { or [ and take appropriate action.
174 *
175 * @returns {Element} The next element.
176 */
177 next() {
178 if (this.currentValue === CURLY) {
179 return this._convertToEmptyObject();
180 } else if (this.currentValue === BRACKET) {
181 return this._convertToEmptyArray();
182 }
183 return this._next();
184 }
185
186 /**
187 * Rename the element. Update the parent's mapping if available.
188 *
189 * @param {String} key - The new key.
190 */
191 rename(key) {
192 if (this.parent !== undefined) {
193 const elm = this.parent.elements._map[this.currentKey];
194 delete this.parent.elements._map[this.currentKey];
195 this.parent.elements._map[key] = elm;
196 }
197
198 this.currentKey = key;
199 this._bubbleUp(Events.Edited);
200 }
201
202 /**
203 * Generate the javascript object for this element.
204 *
205 * @returns {Object} The javascript object.
206 */
207 generateObject() {
208 if (this.currentType === 'Array') {
209 return ObjectGenerator.generateArray(this.elements);
210 }
211 if (this.elements) {
212 return ObjectGenerator.generate(this.elements);
213 }
214 return this.currentValue;
215 }
216
217 /**
218 * Generate the javascript object representing the original values
219 * for this element (pre-element removal, renaming, editing).
220 *
221 * @returns {Object} The javascript object.
222 */
223 generateOriginalObject() {
224 if (this.type === 'Array') {
225 const originalElements = this._generateElements(this.originalExpandableValue);
226 return ObjectGenerator.generateOriginalArray(originalElements);
227 }
228 if (this.type === 'Object') {
229 const originalElements = this._generateElements(this.originalExpandableValue);
230 return ObjectGenerator.generateOriginal(originalElements);
231 }
232
233 return this.value;
234 }
235
236 /**
237 * Insert an element after the provided element. If this element is an array,
238 * then ignore the key specified by the caller and use the correct index.
239 * Update the keys of the rest of the elements in the LinkedList.
240 *
241 * @param {Element} element - The element to insert after.
242 * @param {String} key - The key.
243 * @param {Object} value - The value.
244 *
245 * @returns {Element} The new element.
246 */
247 insertAfter(element, key, value) {
248 if (this.currentType === 'Array') {
249 if (element.currentKey === '') {
250 this.elements.handleEmptyKeys(element);
251 }
252 key = element.currentKey + 1;
253 }
254 var newElement = this.elements.insertAfter(element, key, value, true, this);
255 if (this.currentType === 'Array') {
256 this.elements.updateKeys(newElement, 1);
257 }
258 this._bubbleUp(Events.Added);
259 return newElement;
260 }
261
262 /**
263 * Add a new element to this element.
264 *
265 * @param {String | Number} key - The element key.
266 * @param {Object} value - The value.
267 *
268 * @returns {Element} The new element.
269 */
270 insertEnd(key, value) {
271 if (this.currentType === 'Array') {
272 this.elements.flush();
273 key = 0;
274 if (this.elements.lastElement) {
275 if (this.elements.lastElement.currentKey === '') {
276 this.elements.handleEmptyKeys(this.elements.lastElement);
277 }
278 key = this.elements.lastElement.currentKey + 1;
279 }
280 }
281 var newElement = this.elements.insertEnd(key, value, true, this);
282 this._bubbleUp(Events.Added);
283 return newElement;
284 }
285
286 /**
287 * Insert a placeholder element at the end of the element.
288 *
289 * @returns {Element} The placeholder element.
290 */
291 insertPlaceholder() {
292 var newElement = this.elements.insertEnd('', '', true, this);
293 this._bubbleUp(Events.Added);
294 return newElement;
295 }
296
297 /**
298 * Is the element a newly added element?
299 *
300 * @returns {Boolean} If the element is newly added.
301 */
302 isAdded() {
303 return this.added || (this.parent && this.parent.isAdded());
304 }
305
306 /**
307 * Is the element blank?
308 *
309 * @returns {Boolean} If the element is blank.
310 */
311 isBlank() {
312 return this.currentKey === '' && this.currentValue === '';
313 }
314
315 /**
316 * Does the element have a valid value for the current type?
317 *
318 * @returns {Boolean} If the value is valid.
319 */
320 isCurrentTypeValid() {
321 return this.currentTypeValid;
322 }
323
324 /**
325 * Set the element as valid.
326 */
327 setValid() {
328 this.currentTypeValid = true;
329 this.invalidTypeMessage = undefined;
330 this._bubbleUp(Events.Valid, this.uuid);
331 }
332
333 /**
334 * Set the element as invalid.
335 *
336 * @param {Object} value - The value.
337 * @param {String} newType - The new type.
338 * @param {String} message - The error message.
339 */
340 setInvalid(value, newType, message) {
341 this.currentValue = value;
342 this.currentType = newType;
343 this.currentTypeValid = false;
344 this.invalidTypeMessage = message;
345 this._bubbleUp(Events.Invalid, this.uuid);
346 }
347
348 /**
349 * Determine if the key is a duplicate.
350 *
351 * @param {String} value - The value to check.
352 *
353 * @returns {Boolean} If the key is a duplicate.
354 */
355 isDuplicateKey(value) {
356 if (value === this.currentKey) {
357 return false;
358 }
359 for (let element of this.parent.elements) {
360 if (element.currentKey === value) {
361 return true;
362 }
363 }
364 return false;
365 }
366
367 /**
368 * Determine if the element is edited - returns true if
369 * the key or value changed. Does not count array values whose keys have
370 * changed as edited.
371 *
372 * @returns {Boolean} If the element is edited.
373 */
374 isEdited() {
375 return (this.isRenamed() ||
376 !this._valuesEqual() ||
377 this.type !== this.currentType) &&
378 !this.isAdded();
379 }
380
381 /**
382 * Check for value equality.
383
384 * @returns {Boolean} If the value is equal.
385 */
386 _valuesEqual() {
387 if (this.currentType === 'Date' && isString(this.currentValue)) {
388 return isEqual(this.value, new Date(this.currentValue));
389 } else if (this.currentType === 'ObjectId' && isString(this.currentValue)) {
390 return this._isObjectIdEqual();
391 }
392 return isEqual(this.value, this.currentValue);
393 }
394
395 _isObjectIdEqual() {
396 try {
397 return this.value.toHexString() === this.currentValue;
398 } catch (_) {
399 return false;
400 }
401 }
402
403 /**
404 * Is the element the last in the elements.
405 *
406 * @returns {Boolean} If the element is last.
407 */
408 isLast() {
409 return this.parent.elements.lastElement === this;
410 }
411
412 /**
413 * Determine if the element is renamed.
414 *
415 * @returns {Boolean} If the element was renamed.
416 */
417 isRenamed() {
418 let keyChanged = false;
419 if (!this.parent || this.parent.isRoot() || this.parent.currentType === 'Object') {
420 keyChanged = (this.key !== this.currentKey);
421 }
422
423 return keyChanged;
424 }
425
426 /**
427 * Can changes to the elemnt be reverted?
428 *
429 * @returns {Boolean} If the element can be reverted.
430 */
431 isRevertable() {
432 return this.isEdited() || this.isRemoved();
433 }
434
435 /**
436 * Can the element be removed?
437 *
438 * @returns {Boolean} If the element can be removed.
439 */
440 isRemovable() {
441 return !this.parent.isRemoved();
442 }
443
444 /**
445 * Can no action be taken on the element?
446 *
447 * @returns {Boolean} If no action can be taken.
448 */
449 isNotActionable() {
450 return (this.key === ID && !this.isAdded()) || !this.isRemovable();
451 }
452
453 /**
454 * Determine if the value is editable.
455 *
456 * @returns {Boolean} If the value is editable.
457 */
458 isValueEditable() {
459 return this.isKeyEditable() && !includes(UNEDITABLE_TYPES, this.currentType);
460 }
461
462 /**
463 * Determine if the key of the parent element is editable.
464 *
465 * @returns {Boolean} If the parent's key is editable.
466 */
467 isParentEditable() {
468 if (this.parent && !this.parent.isRoot()) {
469 return this.parent.isKeyEditable();
470 }
471 return true;
472 }
473
474 /**
475 * Determine if the key is editable.
476 *
477 * @returns {Boolean} If the key is editable.
478 */
479 isKeyEditable() {
480 return this.isParentEditable() && (this.isAdded() || (this.currentKey !== ID));
481 }
482
483 /**
484 * Determine if the element is modified at all.
485 *
486 * @returns {Boolean} If the element is modified.
487 */
488 isModified() {
489 if (this.elements) {
490 for (let element of this.elements) {
491 if (element.isModified()) {
492 return true;
493 }
494 }
495 }
496 return this.isAdded() || this.isEdited() || this.isRemoved();
497 }
498
499 /**
500 * Is the element flagged for removal?
501 *
502 * @returns {Boolean} If the element is flagged for removal.
503 */
504 isRemoved() {
505 return this.removed;
506 }
507
508 /**
509 * Elements themselves are not the root.
510 *
511 * @returns {false} Always false.
512 */
513 isRoot() {
514 return false;
515 }
516
517 /**
518 * Flag the element for removal.
519 */
520 remove() {
521 this.revert();
522 this.removed = true;
523 this._bubbleUp(Events.Removed);
524 }
525
526 /**
527 * Revert the changes to the element.
528 */
529 revert() {
530 if (this.isAdded()) {
531 if (this.parent && this.parent.currentType === 'Array') {
532 this.parent.elements.updateKeys(this, -1);
533 }
534 this.parent.elements.remove(this);
535 this.parent.emit(Events.Removed);
536 this.parent = null;
537 } else {
538 if (this.originalExpandableValue) {
539 this.elements = this._generateElements(this.originalExpandableValue);
540 this.currentValue = undefined;
541 } else {
542 if (this.currentValue === null && this.value !== null) {
543 this.elements = null;
544 } else {
545 this._removeAddedElements();
546 }
547 this.currentValue = this.value;
548 }
549 this.currentKey = this.key;
550 this.currentType = this.type;
551 this.removed = false;
552 }
553 this.setValid();
554 this._bubbleUp(Events.Reverted);
555 }
556
557 /**
558 * Fire and bubble up the event.
559 *
560 * @param {Event} evt - The event.
561 * @param {*} data - Optional.
562 */
563 _bubbleUp(evt, data) {
564 this.emit(evt, data);
565 var element = this.parent;
566 if (element) {
567 if (element.isRoot()) {
568 element.emit(evt, data);
569 } else {
570 element._bubbleUp(evt, data);
571 }
572 }
573 }
574
575 /**
576 * Convert this element to an empty object.
577 */
578 _convertToEmptyObject() {
579 this.edit({});
580 this.insertPlaceholder();
581 }
582
583 /**
584 * Convert to an empty array.
585 */
586 _convertToEmptyArray() {
587 this.edit([]);
588 this.insertPlaceholder();
589 }
590
591 /**
592 * Is the element empty?
593 *
594 * @param {Element} element - The element to check.
595 *
596 * @returns {Boolean} If the element is empty.
597 */
598 _isElementEmpty(element) {
599 return element && element.isAdded() && element.isBlank();
600 }
601
602 /**
603 * Check if the value is expandable.
604 *
605 * @param {Object} value - The value to check.
606 *
607 * @returns {Boolean} If the value is expandable.
608 */
609 _isExpandable(value) {
610 return isObject(value) || isArray(value);
611 }
612
613 /**
614 * Generates a sequence of child elements.
615 *
616 * @param {Object} object - The object to generate from.
617 *
618 * @returns {Array} The elements.
619 */
620 _generateElements(object) {
621 return new LinkedList(this, object); // eslint-disable-line no-use-before-define
622 }
623
624 /**
625 * Get the key for the element.
626 *
627 * @param {String} key
628 * @param {Number} index
629 *
630 * @returns {String|Number} The index if the type is an array, or the key.
631 */
632 _key(key, index) {
633 return this.currentType === 'Array' ? index : key;
634 }
635
636 /**
637 * Add a new element to the parent.
638 */
639 _next() {
640 if (!this._isElementEmpty(this.nextElement) && !this._isElementEmpty(this)) {
641 this.parent.insertAfter(this, '', '');
642 }
643 }
644
645 /**
646 * Removes the added elements from the element.
647 */
648 _removeAddedElements() {
649 if (this.elements) {
650 for (let element of this.elements) {
651 if (element.isAdded()) {
652 this.elements.remove(element);
653 }
654 }
655 }
656 }
657}
658
659/**
660 * Represents a doubly linked list.
661 */
662class LinkedList {
663 /**
664 * Get the element at the provided index.
665 *
666 * @param {Integer} index - The index.
667 *
668 * @returns {Element} The matching element.
669 */
670 at(index) {
671 this.flush();
672 if (!Number.isInteger(index)) {
673 return undefined;
674 }
675
676 var element = this.firstElement;
677 for (var i = 0; i < index; i++) {
678 if (!element) {
679 return undefined;
680 }
681 element = element.nextElement;
682 }
683 return element === null ? undefined : element;
684 }
685
686 get(key) {
687 this.flush();
688 return this._map[key];
689 }
690
691 // Instantiate the new doubly linked list.
692 constructor(doc, originalDoc) {
693 this.firstElement = null;
694 this.lastElement = null;
695 this.doc = doc;
696 this.originalDoc = originalDoc;
697 this.keys = keys(this.originalDoc);
698 if (this.doc.currentType === 'Array') {
699 this.keys = this.keys.map(k => parseInt(k, 10));
700 }
701 this.size = this.keys.length;
702 this.loaded = 0;
703 this._map = {};
704 }
705
706 /**
707 * Insert data after the provided element.
708 *
709 * @param {Element} element - The element to insert after.
710 * @param {String} key - The element key.
711 * @param {Object} value - The element value.
712 * @param {Boolean} added - If the element is new.
713 * @param {Object} parent - The parent.
714 *
715 * @returns {Element} The inserted element.
716 */
717 insertAfter(element, key, value, added, parent) {
718 this.flush();
719 return this._insertAfter(element, key, value, added, parent);
720 }
721
722 /**
723 * Update the currentKey of each element if array elements.
724 *
725 * @param {Element} element - The element to insert after.
726 * @param {Number} add - 1 if adding a new element, -1 if removing.
727 */
728 updateKeys(element, add) {
729 this.flush();
730 while (element.nextElement) {
731 element.nextElement.currentKey += add;
732 element = element.nextElement;
733 }
734 }
735
736 /**
737 * If an element is added after a placeholder, convert that placeholder
738 * into an empty element with the correct key.
739 *
740 * @param {Element} element - The placeholder element.
741 */
742 handleEmptyKeys(element) {
743 if (element.currentKey === '') {
744 let e = element;
745 while (e.currentKey === '') {
746 if (!e.previousElement) {
747 e.currentKey = 0;
748 break;
749 } else {
750 e = e.previousElement;
751 }
752 }
753 while (e.nextElement) {
754 e.nextElement.currentKey = e.currentKey + 1;
755 e = e.nextElement;
756 }
757 }
758 }
759
760 /**
761 * Insert data before the provided element.
762 *
763 * @param {Element} element - The element to insert before.
764 * @param {String} key - The element key.
765 * @param {Object} value - The element value.
766 * @param {Boolean} added - If the element is new.
767 * @param {Object} parent - The parent.
768 *
769 * @returns {Element} The inserted element.
770 */
771 insertBefore(element, key, value, added, parent) {
772 this.flush();
773 return this._insertBefore(element, key, value, added, parent);
774 }
775
776 /**
777 * Insert data at the beginning of the list.
778 *
779 * @param {String} key - The element key.
780 * @param {Object} value - The element value.
781 * @param {Boolean} added - If the element is new.
782 * @param {Object} parent - The parent.
783 *
784 * @returns {Element} The data element.
785 */
786 insertBeginning(key, value, added, parent) {
787 this.flush();
788 return this._insertBeginning(key, value, added, parent);
789 }
790
791 /**
792 * Insert data at the end of the list.
793 *
794 * @param {String} key - The element key.
795 * @param {Object} value - The element value.
796 * @param {Boolean} added - If the element is new.
797 * @param {Object} parent - The parent.
798 *
799 * @returns {Element} The data element.
800 */
801 insertEnd(key, value, added, parent) {
802 this.flush();
803 if (!this.lastElement) {
804 return this.insertBeginning(key, value, added, parent);
805 }
806 return this.insertAfter(this.lastElement, key, value, added, parent);
807 }
808
809 flush() {
810 if (this.loaded < this.size) {
811 // Only iterate from the loaded index to the size.
812 for (let element of this) {
813 if (element && element.elements) {
814 element.elements.flush();
815 }
816 }
817 }
818 }
819
820 /**
821 * Get an iterator for the list.
822 *
823 * @returns {Iterator} The iterator.
824 */
825 [Symbol.iterator]() {
826 let currentElement;
827 let index = 0;
828 return {
829 next: () => {
830 if (this._needsLazyLoad(index)) {
831 const key = this.keys[index];
832 index += 1;
833 currentElement = this._lazyInsertEnd(key);
834 return { value: currentElement };
835 } else if (this._needsStandardIteration(index)) {
836 if (currentElement) {
837 currentElement = currentElement.nextElement;
838 } else {
839 currentElement = this.firstElement;
840 }
841 if (currentElement) {
842 index += 1;
843 return { value: currentElement };
844 }
845 return { done: true };
846 }
847 return { done: true };
848 }
849 };
850 }
851
852 _needsLazyLoad(index) {
853 return (index === 0 && this.loaded === 0 && this.size > 0) ||
854 (this.loaded <= index && index < this.size);
855 }
856
857 _needsStandardIteration(index) {
858 return (this.loaded > 0 && index < this.loaded && index < this.size);
859 }
860
861 /**
862 * Insert on the end of the list lazily.
863 *
864 * @param {String} key - The key.
865 *
866 * @returns {Element} The inserted element.
867 */
868 _lazyInsertEnd(key) {
869 this.size -= 1;
870 return this._insertEnd(key, this.originalDoc[key], this.doc.cloned, this.doc);
871 }
872
873 _insertEnd(key, value, added, parent) {
874 if (!this.lastElement) {
875 return this._insertBeginning(key, value, added, parent);
876 }
877 return this._insertAfter(this.lastElement, key, value, added, parent);
878 }
879
880 _insertBefore(element, key, value, added, parent) {
881 var newElement = new Element(key, value, added, parent, element.previousElement, element);
882 if (element.previousElement) {
883 element.previousElement.nextElement = newElement;
884 } else {
885 this.firstElement = newElement;
886 }
887 element.previousElement = newElement;
888 this._map[newElement.key] = newElement;
889 this.size += 1;
890 this.loaded += 1;
891 return newElement;
892 }
893
894 _insertBeginning(key, value, added, parent) {
895 if (!this.firstElement) {
896 var element = new Element(key, value, added, parent, null, null);
897 this.firstElement = this.lastElement = element;
898 this.size += 1;
899 this.loaded += 1;
900 this._map[element.key] = element;
901 return element;
902 }
903 const newElement = this.insertBefore(this.firstElement, key, value, added, parent);
904 this._map[newElement.key] = newElement;
905 return newElement;
906 }
907
908 _insertAfter(element, key, value, added, parent) {
909 var newElement = new Element(key, value, added, parent, element, element.nextElement);
910 if (element.nextElement) {
911 element.nextElement.previousElement = newElement;
912 } else {
913 this.lastElement = newElement;
914 }
915 element.nextElement = newElement;
916 this._map[newElement.key] = newElement;
917 this.size += 1;
918 this.loaded += 1;
919 return newElement;
920 }
921
922 /**
923 * Remove the element from the list.
924 *
925 * @param {Element} element - The element to remove.
926 *
927 * @returns {DoublyLinkedList} The list with the element removed.
928 */
929 remove(element) {
930 this.flush();
931 if (element.previousElement) {
932 element.previousElement.nextElement = element.nextElement;
933 } else {
934 this.firstElement = element.nextElement;
935 }
936 if (element.nextElement) {
937 element.nextElement.previousElement = element.previousElement;
938 } else {
939 this.lastElement = element.previousElement;
940 }
941 element.nextElement = element.previousElement = null;
942 delete this._map[element.currentKey];
943 this.size -= 1;
944 this.loaded -= 1;
945 return this;
946 }
947}
948
949module.exports = Element;
950module.exports.LinkedList = LinkedList;
951module.exports.Events = Events;
952module.exports.DATE_FORMAT = DATE_FORMAT;