hadron-document
Version:
Hadron Document
482 lines • 16.1 kB
JavaScript
'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