Source: lib/util/Metadata.js

'use strict';

const error = require('../error');
const Acl = require('../Acl');
const Lockable = require('./Lockable');
const deprecated = require('./deprecated');

/**
 * The Metadata instance tracks the state of an object and checks if the object state was changed since last
 * load/update. The metadata keeps therefore the state of:
 * - in which state the object currently is
 * - which db managed the instance
 * - the metadata of the object (id, version, bucket)
 * - which is the owning object (root object) of an embedded object
 *
 * {@link util.Metadata#get(object)} can be used on any managed object to retrieve the metadata of the root object
 *
 * @alias util.Metadata
 * @extends util.Lockable
 */
class Metadata extends Lockable {
  /**
   * Creates a metadata instance for the given type and object instance
   *
   * @param {metamodel.ManagedType} type The type of the object
   * @param {*} object The object instance of the type
   * @return {*} The created metadata for the object
   */
  static create(type, object) {
    let meta;
    if (type.isEntity) {
      meta = new Metadata(object, type);
    } else if (type.isEmbeddable) {
      meta = {
        type,
        readAccess() {
          const metadata = this.root && this.root._metadata;
          if (metadata) {
            metadata.readAccess();
          }
        },
        writeAccess() {
          const metadata = this.root && this.root._metadata;
          if (metadata) {
            metadata.writeAccess();
          }
        },
      };
    } else {
      throw new Error('Illegal type ' + type);
    }

    return meta;
  }

  /**
   * Returns the metadata of the managed object
   * @param {binding.Managed} managed
   * @return {util.Metadata}
   */
  static get(managed) {
    return managed._metadata;
  }

  /**
   * @type EntityManager
   */
  get db() {
    if (this.entityManager) {
      return this.entityManager;
    }

    this.entityManager = require('../baqend'); // eslint-disable-line global-require
    return this.entityManager;
  }

  /**
   * @param db {EntityManager}
   */
  set db(db) {
    if (!this.entityManager) {
      this.entityManager = db;
    } else {
      throw new Error('DB has already been set.');
    }
  }

  /**
   * @type string
   * @readonly
   */
  get bucket() {
    return this.type.name;
  }

  /**
   * @type string
   * @readonly
   */
  get key() {
    if (!this.decodedKey && this.id) {
      const index = this.id.lastIndexOf('/');
      this.decodedKey = decodeURIComponent(this.id.substring(index + 1));
    }
    return this.decodedKey;
  }

  /**
   * @param {string} value
   */
  set key(value) {
    const val = value + '';

    if (this.id) {
      throw new Error('The id can\'t be set twice.');
    }

    this.id = '/db/' + this.bucket + '/' + encodeURIComponent(val);
    this.decodedKey = val;
  }

  /**
   * Indicates if this object already belongs to an db
   * <code>true</code> if this object belongs already to an db otherwise <code>false</code>
   * @type boolean
   * @readonly
   */
  get isAttached() {
    return !!this.entityManager;
  }

  /**
   * Indicates if this object is represents a db object, but was not loaded up to now
   * @type boolean
   * @readonly
   */
  get isAvailable() {
    return this.state > Metadata.Type.UNAVAILABLE;
  }

  /**
   * Indicates if this object represents the state of the db and was not modified in any manner
   * @type boolean
   * @readonly
   */
  get isPersistent() {
    return this.state === Metadata.Type.PERSISTENT;
  }

  /**
   * Indicates that this object was modified and the object was not written back to the db
   * @type boolean
   * @readonly
   */
  get isDirty() {
    return this.state === Metadata.Type.DIRTY;
  }

  /**
   * @param {binding.Entity} entity
   * @param {metamodel.ManagedType} type
   */
  constructor(entity, type) {
    super();

    /**
     * @type binding.Entity
     * @private
     */
    this.root = entity;
    this.state = Metadata.Type.DIRTY;
    this.enabled = true;
    /** @type string */
    this.id = null;
    /** @type number */
    this.version = null;
    /** @type metamodel.ManagedType */
    this.type = type;
    /** @type Acl */
    this.acl = new Acl(this);
  }

  /**
   * Enable/Disable state change tracking of this object
   * @param {boolean} newStateTrackingState The new change tracking state
   * @return {void}
   */
  enable(newStateTrackingState) {
    this.enabled = newStateTrackingState;
  }

  /**
   * Signals that the object will be accessed by a read
   *
   * Ensures that the object was loaded already.
   *
   * @return {void}
   */
  readAccess() {
    if (this.enabled) {
      if (!this.isAvailable) {
        throw new error.PersistentError('This object ' + this.id + ' is not available.');
      }
    }
  }

  /**
   * Signals that the object will be accessed by a write
   *
   * Ensures that the object was loaded already and marks the object as dirty.
   *
   * @return {void}
   */
  writeAccess() {
    if (this.enabled) {
      if (!this.isAvailable) {
        throw new error.PersistentError('This object ' + this.id + ' is not available.');
      }

      this.setDirty();
    }
  }

  /**
   * Indicates that the associated object isn't available
   * @return {void}
   */
  setUnavailable() {
    this.state = Metadata.Type.UNAVAILABLE;
  }

  /**
   * Indicates that the associated object is not stale
   *
   * An object is stale if it correlates the database state and is not modified by the user.
   *
   * @return {void}
   */
  setPersistent() {
    this.state = Metadata.Type.PERSISTENT;
  }

  /**
   * Indicates the the object is modified by the user
   * @return {void}
   */
  setDirty() {
    this.state = Metadata.Type.DIRTY;
  }

  /**
   * Indicates the the object is removed
   * @return {void}
   */
  setRemoved() {
    // mark the object only as dirty if it was already available
    if (this.isAvailable) {
      this.setDirty();
      this.version = null;
    }
  }

  /**
   * Converts the object to an JSON-Object
   * @param {Object|boolean} [options=false] to json options by default excludes the metadata
   * @param {boolean} [options.excludeMetadata=false] Excludes the metadata form the serialized json
   * @param {number} [options.depth=0] Includes up to depth referenced objects into the serialized json
   * @param {boolean} [options.persisting=false] indicates if the current state will be persisted.
   *  Used to update the internal change tracking state of collections and mark the object persistent if its true
   * @return {json} JSON-Object
   * @deprecated
   */
  getJson(options) {
    return this.type.toJsonValue(this, this.root, options);
  }

  /**
   * Sets the object content from json
   * @param {json} json The updated json content
   * @param {Object=} options The options used to apply the json
   * @param {boolean} [options.persisting=false] indicates if the current state will be persisted.
   * Used to update the internal change tracking state of collections and mark the object persistent or dirty afterwards
   * @param {boolean} [options.onlyMetadata=false} Indicates if only the metadata should be updated
   * @param {boolean} {options.updateMetadataOnly=false} Indicates if only the metadata should be updated
   * @return {void}
   * @deprecated
   */
  setJson(json, options) {
    this.type.fromJsonValue(this, json, this.root, options);
  }
}

/**
 * @enum {number}
 */
Metadata.Type = {
  UNAVAILABLE: -1,
  PERSISTENT: 0,
  DIRTY: 1,
};

deprecated(Metadata.prototype, '_root', 'root');
deprecated(Metadata.prototype, '_state', 'state');
deprecated(Metadata.prototype, '_enabled', 'enabled');

module.exports = Metadata;