repo_description: A fast, local first, reactive Database for JavaScript Applications https://rxdb.info/
repo_topics: angular browser-database couchdb crdt database firebase graphql indexeddb ionic local-first localstorage nodejs nosql postgres pouchdb react-native realtime realtime-database rxdb sqlite
code: import {
      Observable,
      defer
  } from 'rxjs';
  import {
      distinctUntilChanged,
      filter,
      map,
      shareReplay,
      startWith
  } from 'rxjs/operators';
  import {
      clone,
      trimDots,
      pluginMissing,
      flatClone,
      deepEqual,
      PROMISE_RESOLVE_NULL,
      RXJS_SHARE_REPLAY_DEFAULTS,
      getProperty,
      getFromMapOrCreate,
      ensureNotFalsy
  } from './plugins/utils/index.ts';
  import {
      newRxError
  } from './rx-error.ts';
  import {
      runPluginHooks
  } from './hooks.ts';
  
  import type {
      RxDocument,
      RxCollection,
      RxDocumentData,
      RxDocumentWriteData,
      UpdateQuery,
      CRDTEntry,
      ModifyFunction
  } from './types/index.d.ts';
  import { getDocumentDataOfRxChangeEvent } from './rx-change-event.ts';
  import { overwritable } from './overwritable.ts';
  import { getSchemaByObjectPath } from './rx-schema-helper.ts';
  import { getWrittenDocumentsFromBulkWriteResponse, throwIfIsStorageWriteError } from './rx-storage-helper.ts';
  import { modifierFromPublicToInternal } from './incremental-write.ts';
  
  export const basePrototype = {
      get primaryPath() {
          const _this: RxDocument = this as any;
          if (!_this.isInstanceOfRxDocument) {
              return undefined;
          }
          return _this.collection.schema.primaryPath;
      },
      get primary() {
          const _this: RxDocument = this as any;
          if (!_this.isInstanceOfRxDocument) {
              return undefined;
          }
          return (_this._data as any)[_this.primaryPath];
      },
      get revision() {
          const _this: RxDocument = this as any;
          if (!_this.isInstanceOfRxDocument) {
              return undefined;
          }
          return _this._data._rev;
      },
      get deleted$() {
          const _this: RxDocument<any> = this as any;
          if (!_this.isInstanceOfRxDocument) {
              return undefined;
          }
          return _this.$.pipe(
              map((d: any) => d._data._deleted),
              distinctUntilChanged()
          );
      },
      get deleted$$() {
          const _this: RxDocument = this as any;
          const reactivity = _this.collection.database.getReactivityFactory();
          return reactivity.fromObservable(
              _this.deleted$,
              _this.getLatest().deleted,
              _this.collection.database
          );
      },
      get deleted() {
          const _this: RxDocument = this as any;
          if (!_this.isInstanceOfRxDocument) {
              return undefined;
          }
          return _this._data._deleted;
      },
  
      getLatest(this: RxDocument): RxDocument {
          const latestDocData = this.collection._docCache.getLatestDocumentData(this.primary);
          return this.collection._docCache.getCachedRxDocument(latestDocData);
      },
  
      /**
       * returns the observable which emits the plain-data of this document
       */
      get $(): Observable<RxDocumentData<any>> {
          const _this: RxDocument<{}, {}, {}> = this as any;
          const id = this.primary;
  
          return defer(() => {
              const latestDocData = _this.collection._docCache.getLatestDocumentData(id);
              return _this.collection.eventBulks$.pipe(
                  filter((bulk: any) => !bulk.isLocal),
                  map((bulk: any) => bulk.events.find((ev: any) => ev.documentId === id)),
                  filter((event: any) => !!event),
                  map((changeEvent: any) => getDocumentDataOfRxChangeEvent(ensureNotFalsy(changeEvent))),
                  startWith(latestDocData),
                  distinctUntilChanged((prev: RxDocumentData<any>, curr: RxDocumentData<any>) => prev._rev === curr._rev),
                  map((docData: RxDocumentData<any>) => (this as RxDocument<any>).collection._docCache.getCachedRxDocument(docData)),
              );
          }).pipe(
              shareReplay(RXJS_SHARE_REPLAY_DEFAULTS)
          );
      },
      get $$(): any {
          const _this: RxDocument = this as any;
          const reactivity = _this.collection.database.getReactivityFactory();
          return reactivity.fromObservable(
              _this.$,
              _this.getLatest()._data,
              _this.collection.database
          );
      },
  
      /**
       * returns observable of the value of the given path
       */
      get$(this: RxDocument, path: string): Observable<any> {
          if (overwritable.isDevMode()) {
              if (path.includes('.item.')) {
                  throw newRxError('DOC1', {
                      path
                  });
              }
  
              if (path === this.primaryPath) {
                  throw newRxError('DOC2');
              }
  
              // final fields cannot be modified and so also not observed
              if (this.collection.schema.finalFields.includes(path)) {
                  throw newRxError('DOC3', {
                      path
                  });
              }
  
              const schemaObj = getSchemaByObjectPath(
                  this.collection.schema.jsonSchema,
                  path
              );
  
              if (!schemaObj) {
                  throw newRxError('DOC4', {
                      path
                  });
              }
          }
  
          return this.$
              .pipe(
                  map((data: any) => getProperty(data, path)),
                  distinctUntilChanged((prev: any, curr: any) => {
                      /**
                       * Use deepEqual for non-primitive values (objects/arrays)
                       * because the default === comparison always fails across
                       * document revisions since each revision creates new object references.
                       */
                      return deepEqual(prev, curr);
                  })
              );
      },
      get$$(this: RxDocument, path: string) {
          const obs = this.get$(path);
          const reactivity = this.collection.database.getReactivityFactory();
          return reactivity.fromObservable(
              obs,
              this.getLatest().get(path),
              this.collection.database
          );
      },
  
      /**
       * populate the given path
       */
      populate(this: RxDocument, path: string): Promise<RxDocument | null> {
          const schemaObj = getSchemaByObjectPath(
              this.collection.schema.jsonSchema,
              path
          );
          /**
           * Validate the schema path BEFORE looking at the document value
           * so that invalid paths and non-ref fields surface as DOC5/DOC6
           * errors even when the value at that path happens to be falsy.
           * Previously the `!value` short-circuit below swallowed these errors.
           */
          if (!schemaObj) {
              throw newRxError('DOC5', {
                  path
              });
          }
          const ref = schemaObj.ref
              ? schemaObj.ref
              : (schemaObj.type === 'array' && schemaObj.items && (schemaObj.items as any).ref
                  ? (schemaObj.items as any).ref
                  : undefined);
          if (!ref) {
              throw newRxError('DOC6', {
                  path,
                  schemaObj
              });
          }
  
          const refCollection: RxCollection = this.collection.database.collections[ref];
          if (!refCollection) {
              throw newRxError('DOC7', {
                  ref,
                  path,
                  schemaObj
              });
          }
  
          const value = this.get(path);
          if (!value) {
              return PROMISE_RESOLVE_NULL;
          }
  
          if (schemaObj.type === 'array') {
              return refCollection.findByIds(value).exec().then(res => {
                  // Preserve the original array order of the ref ids
                  // instead of using the Map iteration order which depends
                  // on the query cache and storage return order.
                  const result = [];
                  for (let i = 0; i < value.length; i++) {
                      const doc = res.get(value[i]);
                      if (doc) {
                          result.push(doc);
                      }
                  }
                  return result as any;
              });
          } else {
              return refCollection.findOne(value).exec();
          }
      },
      /**
       * get data by objectPath
       * @hotPath Performance here is really important,
       * run some tests before changing anything.
       */
      get(this: RxDocument, objPath: string): any | null {
          return getDocumentProperty(this, objPath);
      },
  
      toJSON(this: RxDocument, withMetaFields = false) {
          if (!withMetaFields) {
              const data = flatClone(this._data);
              delete (data as any)._rev;
              delete (data as any)._attachments;
              delete (data as any)._deleted;
              delete (data as any)._meta;
              return overwritable.deepFreezeWhenDevMode(data);
          } else {
              return overwritable.deepFreezeWhenDevMode(this._data);
          }
      },
      toMutableJSON(this: RxDocument, withMetaFields = false) {
          return clone(this.toJSON(withMetaFields as any));
      },
  
      /**
       * updates document
       * @overwritten by plugin (optional)
       * @param updateObj mongodb-like syntax
       */
      update(_updateObj: UpdateQuery<any>) {
          throw pluginMissing('update');
      },
      incrementalUpdate(_updateObj: UpdateQuery<any>) {
          throw pluginMissing('update');
      },
      updateCRDT(_updateObj: CRDTEntry<any> | CRDTEntry<any>[]) {
          throw pluginMissing('crdt');
      },
      putAttachment() {
          throw pluginMissing('attachments');
      },
      putAttachmentBase64() {
          throw pluginMissing('attachments');
      },
      getAttachment() {
          throw pluginMissing('attachments');
      },
      allAttachments() {
          throw pluginMissing('attachments');
      },
      get allAttachments$() {
          throw pluginMissing('attachments');
      },
  
      async modify<RxDocType>(
          this: RxDocument<RxDocType>,
          mutationFunction: ModifyFunction<RxDocType>,
          // used by some plugins that wrap the method
          _context?: string
      ): Promise<RxDocument> {
          const oldData = this._data;
          const newData: RxDocumentData<RxDocType> = await modifierFromPublicToInternal<RxDocType>(mutationFunction)(clone(oldData)) as any;
          return this._saveData(newData, oldData) as any;
      },
  
      /**
       * runs an incremental update over the document
       * @param function that takes the document-data and returns a new data-object
       */
      incrementalModify(
          this: RxDocument,
          mutationFunction: ModifyFunction<any>,
          // used by some plugins that wrap the method
          _context?: string
      ): Promise<RxDocument> {
          return this.collection.incrementalWriteQueue.addWrite(
              this._data,
              modifierFromPublicToInternal(mutationFunction)
          ).then(result => this.collection._docCache.getCachedRxDocument(result));
      },
  
      patch<RxDocType>(
          this: RxDocument<RxDocType>,
          patch: Partial<RxDocType>
      ) {
          const oldData = this._data;
          const newData = clone(oldData);
          Object
              .entries(patch)
              .forEach(([k, v]) => {
                  (newData as any)[k] = v;
              });
          return this._saveData(newData, oldData);
      },
  
      /**
       * patches the given properties
       */
      incrementalPatch<RxDocumentType = any>(
          this: RxDocument<RxDocumentType>,
          patch: Partial<RxDocumentType>
      ): Promise<RxDocument<RxDocumentType>> {
          return this.incrementalModify((docData) => {
              Object
                  .entries(patch)
                  .forEach(([k, v]) => {
                      (docData as any)[k] = v;
                  });
              return docData;
          });
      },
  
      /**
       * saves the new document-data
       * and handles the events
       */
      async _saveData<RxDocType>(
          this: RxDocument<RxDocType>,
          newData: RxDocumentWriteData<RxDocType>,
          oldData: RxDocumentData<RxDocType>
      ): Promise<RxDocument<RxDocType>> {
          newData = flatClone(newData);
  
          // deleted documents cannot be changed
          if (this._data._deleted) {
              throw newRxError('DOC11', {
                  id: this.primary,
                  document: this
              });
          }
          await beforeDocumentUpdateWrite(this.collection, newData, oldData);
          const writeRows = [{
              previous: oldData,
              document: newData
          }];
          const writeResult = await this.collection.storageInstance.bulkWrite(writeRows, 'rx-document-save-data');
  
          const isError = writeResult.error[0];
          throwIfIsStorageWriteError(this.collection, this.primary, newData, isError);
  
          await this.collection._runHooks('post', 'save', newData, this);
          return this.collection._docCache.getCachedRxDocument(
              getWrittenDocumentsFromBulkWriteResponse(
                  this.collection.schema.primaryPath,
                  writeRows,
                  writeResult
              )[0]
          );
      },
  
      /**
       * Remove the document.
       * Notice that there is no hard delete,
       * instead deleted documents get flagged with _deleted=true.
       */
      async remove(this: RxDocument): Promise<RxDocument> {
          if (this.deleted) {
              return Promise.reject(newRxError('DOC13', {
                  document: this,
                  id: this.primary
              }));
          }
  
          const removeResult = await this.collection.bulkRemove([this]);
          if (removeResult.error.length > 0) {
              const error = removeResult.error[0];
              throwIfIsStorageWriteError(
                  this.collection,
                  this.primary,
                  this._data,
                  error
              );
          }
          return removeResult.success[0];
      },
      incrementalRemove(this: RxDocument): Promise<RxDocument> {
          return this.incrementalModify(async (docData) => {
              await this.collection._runHooks('pre', 'remove', docData, this);
              docData._deleted = true;
              return docData;
          }).then(async (newDoc) => {
              await this.collection._runHooks('post', 'remove', newDoc._data, newDoc);
              return newDoc;
          });
      },
      close() {
          throw newRxError('DOC14');
      }
  };
  
  export function createRxDocumentConstructor(proto = basePrototype) {
      const constructor = function RxDocumentConstructor(
          this: RxDocument,
          collection: RxCollection,
          docData: RxDocumentData<any>
      ) {
          this.collection = collection;
  
          // assume that this is always equal to the doc-data in the database
          this._data = docData;
  
          /**
           * @performance
           * Lazy-initialize _propertyCache only when first needed
           * instead of creating a new Map for every RxDocument,
           * since many documents (e.g. from query results) may never
           * have their properties accessed via the cache.
           */
          this._propertyCache = undefined;
  
          /**
           * because of the prototype-merge,
           * we can not use the native instanceof operator
           */
          this.isInstanceOfRxDocument = true;
      };
      constructor.prototype = proto;
      return constructor;
  }
  
  export function createWithConstructor<RxDocType>(
      constructor: any,
      collection: RxCollection<RxDocType>,
      jsonData: RxDocumentData<RxDocType>
  ): RxDocument<RxDocType> | null {
      const doc = new constructor(collection, jsonData);
      runPluginHooks('createRxDocument', doc);
      return doc;
  }
  
  export function isRxDocument(obj: any): boolean {
      return typeof obj === 'object' && obj !== null && 'isInstanceOfRxDocument' in obj;
  }
  
  
  export function beforeDocumentUpdateWrite<RxDocType>(
      collection: RxCollection<RxDocType>,
      newData: RxDocumentWriteData<RxDocType>,
      oldData: RxDocumentData<RxDocType>
  ): Promise<any> {
      /**
       * Meta values must always be merged
       * instead of overwritten.
       * This ensures that different plugins do not overwrite
       * each others meta properties.
       */
      newData._meta = Object.assign(
          {},
          oldData._meta,
          newData._meta
      );
  
      // ensure modifications are ok
      if (overwritable.isDevMode()) {
          collection.schema.validateChange(oldData, newData);
      }
      return collection._runHooks('pre', 'save', newData, oldData);
  }
  
  
  
  
  function getDocumentProperty(doc: RxDocument, objPath: string): any | null {
      /**
       * @performance Lazy-initialize _propertyCache on first access
       * to avoid creating a Map for documents that never use it.
       */
      if (!doc._propertyCache) {
          doc._propertyCache = new Map<string, any>();
      }
      return getFromMapOrCreate(
          doc._propertyCache,
          objPath,
          () => {
              const valueObj = getProperty(doc._data, objPath);
  
              // direct return if array or non-object
              if (
                  typeof valueObj !== 'object' ||
                  valueObj === null ||
                  Array.isArray(valueObj)
              ) {
                  return overwritable.deepFreezeWhenDevMode(valueObj);
              }
              const proxy = new Proxy(
                  /**
                   * In dev-mode, the _data is deep-frozen
                   * so we have to flat clone here so that
                   * the proxy can work.
                   */
                  flatClone(valueObj),
                  {
                      /**
                       * @performance is really important here
                       * because people access nested properties very often
                       * and might not be aware that this is internally using a Proxy
                       */
                      get(target, property: any) {
                          if (typeof property !== 'string') {
                              return target[property];
                          }
  
  
                          const lastChar = property.charAt(property.length - 1);
                          if (lastChar === '$') {
                              if (property.endsWith('$$')) {
                                  const key = property.slice(0, -2);
                                  return doc.get$$(trimDots(objPath + '.' + key));
                              } else {
                                  const key = property.slice(0, -1);
                                  return doc.get$(trimDots(objPath + '.' + key));
                              }
                          } else if (lastChar === '_') {
                              const key = property.slice(0, -1);
                              return doc.populate(trimDots(objPath + '.' + key));
                          } else {
  
                              /**
                               * Performance shortcut
                               * In most cases access to nested properties
                               * will only access simple values which can be directly returned
                               * without creating a new Proxy or utilizing the cache.
                               */
                              const plainValue = target[property];
                              if (
                                  typeof plainValue === 'number' ||
                                  typeof plainValue === 'string' ||
                                  typeof plainValue === 'boolean'
                              ) {
                                  return plainValue;
                              }
  
                              return getDocumentProperty(doc, trimDots(objPath + '.' + property));
                          }
                      }
                  });
              return proxy;
          }
      );
  };
  