UNPKG

hadron-document

Version:
482 lines • 16.1 kB
'use strict'; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Document = exports.DEFAULT_VISIBLE_ELEMENTS = void 0; const element_1 = require("./element"); const eventemitter3_1 = __importDefault(require("eventemitter3")); const bson_1 = require("bson"); const object_generator_1 = __importDefault(require("./object-generator")); const utils_1 = require("./utils"); const document_events_1 = require("./document-events"); const element_events_1 = require("./element-events"); /** * The id field. */ const ID = '_id'; exports.DEFAULT_VISIBLE_ELEMENTS = 25; /** * Represents a document. */ class Document extends eventemitter3_1.default { uuid; doc; cloned; isUpdatable; elements; type; currentType; size = null; expanded = false; maxVisibleElementsCount = exports.DEFAULT_VISIBLE_ELEMENTS; editing = false; markedForDeletion = false; // This is used to store the changed EJSON string when the document is modified // via the JSONEditor. modifiedEJSONString = null; /** * Send cancel event. */ cancel() { // Cancel will remove elements from iterator, clone it before iterating // otherwise we will skip items for (const element of Array.from(this.elements)) { element.cancel(); } this.emit(document_events_1.DocumentEvents.Cancel); } /** * Create the new document from the provided object. * * @param {Object} doc - The document. * @param {boolean} cloned - If it is a cloned document. */ constructor(doc, cloned = false) { super(); this.uuid = new bson_1.UUID().toHexString(); this.doc = doc; this.cloned = cloned || false; this.isUpdatable = true; this.type = 'Document'; this.currentType = 'Document'; this.elements = this._generateElements(); } apply(doc) { if (typeof doc?.generateObject === 'function') { doc = doc.generateObject(); } const updatedKeys = []; let prevKey = null; for (const [key, value] of Object.entries(doc)) { if (this.get(key)) { this.get(key).edit(value); } else if (prevKey) { this.insertAfter(this.get(prevKey), key, value); } else { this.insertBeginning(key, value); } prevKey = key; updatedKeys.push(key); } for (const el of [...this.elements]) { if (!updatedKeys.includes(el.currentKey)) { el.remove(); } } } preserveTypes(other) { const thisDoc = this.generateObject(); for (const key of Object.keys(thisDoc)) { const thisElement = this.get(key); const otherElement = other.get(key); if (!thisElement || !otherElement) { continue; } thisElement.preserveType(otherElement); } } /** * Adjust field types in this document based on the collection's existing * schema to prevent unintended type narrowing on insert. For example, if * a field is typed as Double in the schema but the value 5.0 was parsed * as Int32, this will cast it back to Double. * * @param schemaFields - A map of dotted field paths to objects containing * a `type` property with their observed schema type(s). Uses * mongodb-schema naming ('Double', 'Long', etc.). Only fields present * in this map are considered for type adjustment. */ preserveTypesFromSchema(schemaFields) { for (const element of this.elements) { element.preserveTypeFromSchema(schemaFields, ''); } } /** * Generate the javascript object for this document. * * @returns {Object} The javascript object. */ generateObject(options) { return object_generator_1.default.generate(this.elements, options); } /** * Generate the javascript object with the original elements in this document. * * @returns {Object} The original javascript object. */ generateOriginalObject(options) { return object_generator_1.default.generateOriginal(this.elements, options); } /** * Generate the `query` and `updateDoc` to be used in an update operation * where the update only succeeds when the changed document's elements have * not been changed in the background. * * `query` and `updateDoc` may use $getField and $setField if field names * contain either `.` or start with `$`. These operators are only available * on MongoDB 5.0+. (Note that field names starting with `$` are also only * allowed in MongoDB 5.0+.) * * @param keyInclusionOptions Specify which fields to include in the * originalFields list. * * @returns {Object} An object containing the `query` and `updateDoc` to be * used in an update operation. */ generateUpdateUnlessChangedInBackgroundQuery(opts = {}) { // Build a query that will find the document to update only if it has the // values of elements that were changed with their original value. // This query won't find the document if an updated element's value isn't // the same value as it was when it was originally loaded. const originalFieldsThatWillBeUpdated = object_generator_1.default.getQueryForOriginalKeysAndValuesForSpecifiedFields(this, opts, true); const query = { _id: this.getId(), ...originalFieldsThatWillBeUpdated, }; const updateDoc = object_generator_1.default.generateUpdateDoc(this); return { query, updateDoc, }; } /** * Get an element by its key. * * @param {String} key * * @returns {Element} The element. */ get(key) { return this.elements.get(key); } /** * Get an element by a series of segment names. * * @param {Array} path - The series of fieldnames. Cannot be empty. * * @returns {Element} The element. */ getChild(path) { if (!path) { return undefined; } let element = this.elements.get(path[0]); let i = 1; while (i < path.length) { if (element === undefined) { return undefined; } element = element.currentType === 'Array' ? element.at(path[i]) : element.get(path[i]); i++; } return element; } /** * Get the _id value for the document. * * @returns {Object} The id. */ getId() { const element = this.get(ID); return element ? element.generateObject() : null; } /** * Generate the query javascript object reflecting the elements that * are specified by the keys listed in `keys`. The values of this object are * the original values, this can be used when querying for an update based * on multiple criteria. * * @param keyInclusionOptions Specify which fields to include in the * originalFields list. * * @returns {Object} The javascript object. */ getQueryForOriginalKeysAndValuesForSpecifiedKeys(opts = {}) { return object_generator_1.default.getQueryForOriginalKeysAndValuesForSpecifiedFields(this, opts, false); } /** * Get the _id value as a string. Required if _id is not always an ObjectId. * * @returns {String} The string id. */ getStringId() { const element = this.get(ID); if (!element) { return null; } else if (element.currentType === 'Array' || element.currentType === 'Object') { return JSON.stringify(element.generateObject()); } return String(element.value); } /** * Insert a placeholder element at the end of the document. * * @returns {Element} The placeholder element. */ insertPlaceholder() { return this.insertEnd('', ''); } /** * Add a new element to this document. * * @param {String} key - The element key. * @param {Object} value - The value. * * @returns {Element} The new element. */ insertBeginning(key, value) { const newElement = this.elements.insertBeginning(key, value); newElement._bubbleUp(element_events_1.ElementEvents.Added, newElement, this); this.emit(document_events_1.DocumentEvents.VisibleElementsChanged, this); return newElement; } /** * Add a new element to this document. * * @param {String} key - The element key. * @param {Object} value - The value. * * @returns {Element} The new element. */ insertEnd(key, value) { const newElement = this.elements.insertEnd(key, value); newElement._bubbleUp(element_events_1.ElementEvents.Added, newElement, this); this.emit(document_events_1.DocumentEvents.VisibleElementsChanged, this); return newElement; } /** * Insert an element after the provided element. * * @param {Element} element - The element to insert after. * @param {String} key - The key. * @param {Object} value - The value. * * @returns {Element} The new element. */ insertAfter(element, key, value) { const newElement = this.elements.insertAfter(element, key, value); newElement?._bubbleUp(element_events_1.ElementEvents.Added, newElement, this); if (newElement && this.elements.at(this.maxVisibleElementsCount - 1) === element) { this.maxVisibleElementsCount++; } this.emit(document_events_1.DocumentEvents.VisibleElementsChanged, this); return newElement; } /** * A document always exists, is never added. * * @returns Always false. */ isAdded() { return false; } /** * Determine if the element is modified at all. * * @returns {Boolean} If the element is modified. */ isModified() { for (const element of this.elements) { if (element.isModified()) { return true; } } return false; } /** * A document is never removed * * @returns {false} Always false. */ isRemoved() { return false; } /** * The document object is always the root object. * * @returns {true} Always true. */ isRoot() { return true; } /** * Generates a sequence of elements. * * @returns {Array} The elements. */ _generateElements() { return new element_1.ElementList(this, this.doc); } /** * @deprecated Use DocumentEvents import instead */ static get Events() { return document_events_1.DocumentEvents; } /** * Parse a new Document from extended JSON input. */ static FromEJSON(input) { const parsed = bson_1.EJSON.parse(input, { relaxed: false }); return new Document(parsed); } /** * Parse multiple Document from extended JSON input. * If the input consists of only a single document without * `[array]` brackets, return an array consisting of only * that document. */ static FromEJSONArray(input) { const parsed = bson_1.EJSON.parse(input, { relaxed: false }); return Array.isArray(parsed) ? parsed.map((doc) => new Document(doc)) : [new Document(parsed)]; } /** * Convert this Document instance into a human-readable EJSON string. */ toEJSON(source = 'current', options = {}) { const obj = source === 'original' ? this.generateOriginalObject() : this.generateObject(); return (0, utils_1.objectToIdiomaticEJSON)(obj, options); } /** * Expands a document by expanding all of its fields */ expand() { this.expanded = true; for (const element of this.elements) { element.expand(true); } this.emit(document_events_1.DocumentEvents.Expanded); this.emit(document_events_1.DocumentEvents.VisibleElementsChanged, this); } /** * Collapses a document by collapsing all of its fields */ collapse() { this.expanded = false; for (const element of this.elements) { element.collapse(); } this.emit(document_events_1.DocumentEvents.Collapsed); this.emit(document_events_1.DocumentEvents.VisibleElementsChanged, this); } getVisibleElements() { return [...this.elements].slice(0, this.maxVisibleElementsCount); } setMaxVisibleElementsCount(newCount) { this.maxVisibleElementsCount = newCount; this.emit(document_events_1.DocumentEvents.VisibleElementsChanged, this); } getTotalVisibleElementsCount() { const visibleElements = this.getVisibleElements(); return visibleElements.reduce((totalVisibleChildElements, element) => { return (totalVisibleChildElements + 1 + element.getTotalVisibleElementsCount()); }, 0); } findUUIDs() { let subtype4Count = 0; let subtype3Count = 0; for (const element of this.elements) { if (element.currentType === 'Binary') { if (element.value.sub_type === 4) { subtype4Count++; } if (element.value.sub_type === 3) { subtype3Count++; } } else if (element.currentType === 'Object' || element.currentType === 'Array') { const { subtype3Count: sub3, subtype4Count: sub4 } = element.findUUIDs(); subtype3Count += sub3; subtype4Count += sub4; } } return { subtype3Count, subtype4Count }; } startEditing(elementId, field) { if (!this.editing) { this.editing = true; this.emit(document_events_1.DocumentEvents.EditingStarted, elementId, field); } } finishEditing() { if (this.editing) { this.editing = false; this.setModifiedEJSONString(null); this.emit(document_events_1.DocumentEvents.EditingFinished); } } onUpdateStart() { this.emit(document_events_1.DocumentEvents.UpdateStarted); } onUpdateSuccess(doc) { this.emit(document_events_1.DocumentEvents.UpdateSuccess, doc); this.finishEditing(); } onUpdateBlocked() { this.emit(document_events_1.DocumentEvents.UpdateBlocked); } onUpdateError(error) { this.emit(document_events_1.DocumentEvents.UpdateError, error, error.errInfo); } markForDeletion() { if (!this.markedForDeletion) { this.markedForDeletion = true; this.emit(document_events_1.DocumentEvents.MarkedForDeletion); } } finishDeletion() { if (this.markedForDeletion) { this.markedForDeletion = false; this.emit(document_events_1.DocumentEvents.DeletionFinished); } } onRemoveStart() { this.emit(document_events_1.DocumentEvents.RemoveStarted); } onRemoveSuccess() { this.emit(document_events_1.DocumentEvents.RemoveSuccess); this.finishDeletion(); } onRemoveError(error) { this.emit(document_events_1.DocumentEvents.RemoveError, error, error.errInfo); } setModifiedEJSONString(ejson) { this.modifiedEJSONString = ejson; } } exports.Document = Document; //# sourceMappingURL=document.js.map