api/mutable.js

// Copyright 2018 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under
// the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT> or
// the Modified BSD license <LICENSE-BSD or https://opensource.org/licenses/BSD-3-Clause>,
// at your option.
//
// This file may not be copied, modified, or distributed except according to those terms.
//
// Please review the Licences for the specific language governing permissions and limitations
// relating to use of the SAFE Network Software.


const h = require('../helpers');
const lib = require('../native/lib');
const t = require('../native/types');
const emulations = require('./emulations');
const { PubSignKey } = require('./crypto');
const consts = require('../consts');
const errConst = require('../error_const');
const makeError = require('../native/_error.js');
const { ONLY_IF_EXPERIMENTAL_API_ENABLED } = require('../helpers');
const multihash = require('multihashes');
const CID = require('cids');

const CONSTANTS = consts.pubConsts;

/**
 * Holds the permissions of a {@link MutableData} object
 * @hideconstructor
 */
class Permissions extends h.NetworkObject {

  /**
   * Total number of permission entries
   * @returns {Promise<Number>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *       const mData = await app.mutableData.newRandomPublic(15001);
   *       await mData.quickSetup({});
   *       const perms = await mData.getPermissions();
   *       const length = await perms.len();
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  len() {
    return lib.mdata_permissions_len(this.app.connection, this.ref);
  }

  /**
  * @private
  * used by autoref to clean the reference
  * @param {SAFEApp} app
  * @param {handle} ref
  */
  static free() {
    return lib.mdata_permissions_free(this.app.connection, this.ref);
  }

  /**
   * Lookup the permissions of a specifc signing key
   * @param {PubSignKey|CONSTANTS.USER_ANYONE} [signKey=CONSTANTS.USER_ANYONE] The key to lookup
   * @returns {Promise<Object>} The permission set for that key
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *        const pubSignKey = await app.crypto.getAppPubSignKey();
   *        const perms = await mData.getPermissions();
   *        const permSet = await perms.getPermissionsSet(pubSignKey)
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  getPermissionSet(signKey) {
    return lib.mdata_permissions_get(this.app.connection,
                                     this.ref,
                                     signKey ? signKey.ref : CONSTANTS.USER_ANYONE);
  }

  /**
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * Insert a new permission set mapped to a specifc key. Directly commits
   * to the network.
   * Requires the 'ManagePermissions' permission for the app.
   * @param {PubSignKey|CONSTANTS.USER_ANYONE} [signKey=CONSTANTS.USER_ANYONE] the key to map to
   * @param {Object} permissionSet The permission set to insert
   * @returns {Promise} Resolves when finished
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *        const pubSignKey = await app.crypto.getAppPubSignKey();
   *        const perms = await mData.getPermissions();
   *        const pmSet = ['Insert', 'ManagePermissions'];
   *        await perms.insertPermissionSet(pubSignKey, pmSet)
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  insertPermissionSet(signKey, permissionSet) {
    return lib.mdata_permissions_insert(this.app.connection,
                                        this.ref,
                                        signKey
                                          ? signKey.ref
                                          : CONSTANTS.USER_ANYONE,
                                        permissionSet);
  }

  /**
   * Return the list of all associated permission sets.
   * @returns {Promise<Array>} the list of permission sets
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *        const mData = await app.mutableData.newRandomPublic(15001);
   *        await mData.quickSetup({});
   *        const perms = await mData.getPermissions();
   *        const permSetsArray = await perms.listPermissionSets();
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  listPermissionSets() {
    return lib.mdata_list_permission_sets(this.app.connection, this.ref)
        .then((permSets) => permSets.map((userPermSet) =>
          ({ signKey: new PubSignKey(this.app, userPermSet.signKey),
            permSet: userPermSet.permSet
          })
        ));
  }

}

/**
 * Creates mutation actions to be applied to {@link MutableData}
 * @hideconstructor
 */
class EntryMutationTransaction extends h.NetworkObject {

  /**
  * @private
  * used by autoref to clean the reference
  * @param {SAFEApp} app
  * @param {handle} ref
  */
  static free(app, ref) {
    return lib.mdata_entry_actions_free(app.connection, ref);
  }


  /**
   * Creates an action to store a new key/value entry. Does not commit to network.
   *
   * @param {(String|Buffer)} keyName
   * @param {(String|Buffer)} value
   * @returns {Promise} Resolves when complete
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *        const mData = await app.mutableData.newRandomPublic(15001);
   *        await mData.quickSetup({});
   *        const entries = await mData.getEntries();
   *        const keyName = 'surname';
   *        const value = 'Turing';
   *        await entries.insert(keyName, value);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  insert(keyName, value) {
    return lib.mdata_entry_actions_insert(
      this.app.connection,
      this.ref,
      keyName,
      value
    );
  }

  /**
   * Creates an action to delete an existing entry. Does not commit to network.
   *
   * @param {(String|Buffer)} keyName the key you want to delete
   * @param {Number} version The version successor, to confirm you are
   *        actually asking for the correct {@link MutableData} version.
   * @returns {Promise} Resolves when complete
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *        const mData = await app.mutableData.newRandomPublic(15001);
   *        await mData.quickSetup({ surname: 'Turing' });
   *        const version = await mData.getVersion();
   *        const entries = await mData.getEntries();
   *        const keyName = 'dnaChecksum';
   *        await entries.delete(keyName, version + 1);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  delete(keyName, version) {
    return lib.mdata_entry_actions_delete(
      this.app.connection,
      this.ref,
      keyName,
      version
    );
  }

  /**
   * Creates an action to update an existing entry. Does not commit to network.
   *
   * @param {(String|Buffer)} keyName
   * @param {(String|Buffer)} value
   * @param {Number} version The version successor, to confirm you are
   *        actually asking for the correct {@link MutableData} version.
   * @returns {Promise} Resolves when complete
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *        const mData = await app.mutableData.newRandomPublic(15001);
   *        await mData.quickSetup({ surname: 'Turing' });
   *        const version = await mData.getVersion();
   *        const entries = await mData.getEntries();
   *        const keyName = 'street_address';
   *        const value = '7297 Highfield Road';
   *        await entries.update(keyName, value, version + 1);
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  update(keyName, value, version) {
    return lib.mdata_entry_actions_update(
      this.app.connection,
      this.ref,
      keyName,
      value,
      version
    );
  }
}


/**
 * {@link MutableData} {@link Entries} operations
 * @hideconstructor
 */
class Entries extends h.NetworkObject {

  /**
   * Get the total number of entries in the {@link MutableData}
   * @returns {Promise<Number>} number of entries
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *       const mData = await app.mutableData.newRandomPublic(15001);
   *       await mData.quickSetup({ surname: 'Turing' });
   *       const perms = await mData.getPermissions();
   *       const length = await perms.len();
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  len() {
    return lib.mdata_entries_len(this.app.connection, this.ref);
  }

  /**
  * @private
  * used by autoref to clean the mdata keys reference
  * @param {SAFEApp} app
  * @param {handle} ref
  */
  static free(app, ref) {
    return lib.mdata_entries_free(app.connection, ref);
  }

  /**
   * Look up the value of a specific key
   *
   * @param {String} keyName the key to lookup
   * @returns {Promise<ValueVersion>} the entry's value and the current version
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *         await mData.quickSetup({ surname: 'Turing' });
   *         const entries = await mData.getEntries();
   *         const value = await entries.get('surname')
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  get(keyName) {
    return lib.mdata_entries_get(this.app.connection, this.ref, keyName);
  }

  /**
   * Get a list with the entries contained in this {@link MutableData}
   * @returns {Promise<Array>} the entries list
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const entries = await mData.getEntries();
   *         const  entriesArray = await entries.listEntries();
   *         entriesArray.forEach((entry) => {
   *           const key = entry.key.toString();
   *           const value = entry.value.buf.toString();
   *           console.log('Key: ', key);
   *           console.log('Value: ', value);
   *         });
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  listEntries() {
    return lib.mdata_list_entries(this.app.connection, this.ref);
  }

  /**
   * Insert a new entry. Once you call `MutableData.put` with this entry,
   * it will fail if the entry already exists or the current app doesn't have the
   * permissions to edit that {@link MutableData}.
   *
   * @param {(String|Buffer)} keyName
   * @param {(String|Buffer)} value
   * @returns {Promise} Resolves when complete
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *         await mData.quickSetup({});
   *         const entries = await mData.getEntries();
   *         const  entriesArray = await entries.insert('given_name', 'Alan');
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  insert(keyName, value) {
    return lib.mdata_entries_insert(this.app.connection, this.ref, keyName, value);
  }

  /**
   * Create a new mutation transaction for the entries
   * @return {Promise<EntryMutationTransaction>} Mutation transaction interface
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *         await mData.quickSetup({});
   *         const entries = await mData.getEntries();
   *         const  mutationIntertace = await entries.mutate();
   *     } catch(err) {
   *       throw err;
   *     }
   * };
   */
  mutate() {
    return lib.mdata_entry_actions_new(this.app.connection)
            .then((r) => h.autoref(new EntryMutationTransaction(this.app, r)));
  }
}

/**
* @typedef {Object} ValueVersion
* @property {Buffer} buf the buffer with the value
* @property {Number} version the version
* Holds the informatation of a value of a {@link MutableData}
*/

/**
* @typedef {Object} NameAndTag
* @property {Buffer} name - the XoR-name/address on the network
* @property {Number} typeTag - the type tag
* @property {String} xorUrl - `safe://` URL representing XOR address of {@link MutableData}, hashed with SHA3-256, and encoded as base32
*/

/**
 * @hideconstructor
 */
class MutableData extends h.NetworkObject {
  /**
   * Easily set up and commit a new {@link MutableData} with
   * the app having full-access permissions (and no other).
   * The name and description parameters are metadata for the {@link MutableData} which
   * can be used to identify what this {@link MutableData} contains.
   * The metadata is particularly used by the Authenticator when another
   * application has requested mutation permissions on this {@link MutableData},
   * so the user can make a better decision to either allow or deny such a
   * request based on this information.
   *
   * @param {Object} data a key-value payload it should
   *        create the data with
   * @param {(String|Buffer)} name A descriptive metadata name for the {@link MutableData}
   * @param {(String|Buffer)} description
   * A detailed metadata description for the {@link MutableData} content
   *
   * @returns {Promise<MutableData>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     let mData = app.mutableData.newRandomPublic(tagtype);
   *     const entries = {
   *         key1: 'value1',
   *         key2: 'value2'
   *     };
   *     const name = 'My MutableData';
   *     const description = "To store my app\'s data";
   *     mData = await mData.quickSetup(entries, name, description);
   * };
   */
  quickSetup(data, name, description) {
    const pmSet = ['Insert', 'Update', 'Delete', 'ManagePermissions'];

    return this.app.mutableData.newEntries()
      .then((entries) => {
        if (!data) {
          return entries;
        }
        return Promise.all(Object.getOwnPropertyNames(data).map((key) =>
          entries.insert(key, data[key]))).then(() => entries);
      })
      .then((entries) => {
        if (!name && !description) {
          return entries;
        }
        const userMetadata = new t.UserMetadata({ name, description });
        return lib.mdata_encode_metadata(userMetadata)
          .then((encodedMeta) => entries.insert(CONSTANTS.MD_METADATA_KEY, encodedMeta))
          .then(() => entries);
      })
      .then((entries) => this.app.crypto.getAppPubSignKey()
        .then((key) => this.app.mutableData.newPermissions()
            .then((pm) => pm.insertPermissionSet(key, pmSet)
              .then(() => this.put(pm, entries))))
      )
      .then(() => this);
  }

  /**
   * Set the metadata information in the {@link MutableData}. Note this can be used
   * only if the {@link MutableData} was already committed to the network, .i.e either
   * with `put`, with `quickSetup`, or if it is an already existing {@link MutableData}
   * just fetched from the network.
   * The metadata is particularly used by the Authenticator when another
   * application has requested mutation permissions on this {@link MutableData},
   * displaying this information to the user, so the user can make a better
   * decision to either allow or deny such a request based on it.
   *
   * @param {(String|Buffer)} name A descriptive name for the {@link MutableData}
   * @param {(String|Buffer)} description A detailed description for the {@link MutableData} content
   *
   * @returns {Promise} Resolves once finished
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     const name = 'Mutable data name';
   *     const description = 'Mutable data description';
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({});
   *         await mData.setMetadata(name, description);
   *     } catch (err) {
   *         return err;
   *     }
   * };
   */
  setMetadata(name, description) {
    const userMetadata = new t.UserMetadata({ name, description });
    return lib.mdata_encode_metadata(userMetadata)
      .then((encodedMeta) => this.app.mutableData.newMutation()
        .then((mut) => this.get(CONSTANTS.MD_METADATA_KEY)
          .then((metadata) => mut.update(CONSTANTS.MD_METADATA_KEY,
                                          encodedMeta, metadata.version + 1)
            , () => mut.insert(CONSTANTS.MD_METADATA_KEY, encodedMeta)
          )
          .then(() => this.applyEntriesMutation(mut))
        ));
  }

  /**
   * Encrypt an entry key value for a private {@link MutableData}.
   * If the {@link MutableData} is public, the same, unencrypted, value is returned.
   *
   * @param {(String|Buffer)} key
   * @returns {Promise<Buffer>} The encrypted entry key
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
             const mData = await app.mutableData.newRandomPrivate(15001);
   *         const encryptedKey = await mData.encryptKey('key1')
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  encryptKey(key) {
    return lib.mdata_info_encrypt_entry_key(this.ref, key);
  }

  /**
   * Encrypt an entry value for a private {@link MutableData}.
   * If the {@link MutableData} is public, the same, unencrypted, value is returned.
   *
   * @param {(String|Buffer)} value
   * @returns {Promise<Buffer>} The encrypted entry value
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
             const mData = await app.mutableData.newRandomPrivate(15001);
   *         const encryptedValue = await mData.encryptValue('value1')
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  encryptValue(value) {
    return lib.mdata_info_encrypt_entry_value(this.ref, value);
  }

  /**
   * Decrypt the entry key/value provided as parameter with the encryption key
   * contained in a private {@link MutableData}.
   *
   * @param {(String|Buffer)} value
   * @returns {Promise<Buffer>} The decrypted value
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         const encryptedValue = await mData.encryptValue('value1')
   *         const decryptedValue = await mData.decrypt(encryptedKey)
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  decrypt(value) {
    return lib.mdata_info_decrypt(this.ref, value);
  }

  /**
   * Look up the name, tag, and XOR-URL of the {@link MutableData} as required to look it
   * up on the network.
   *
   * @returns {Promise<NameAndTag>} The XOR name and type tag. If the
   * experimental APIs are enabled the XOR-URL is also returned in the object.
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         const  nameAndTag = await mData.getNameAndTag();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getNameAndTag() {
    // If the experimental apis are enabled we also return the XOR-URL
    const xorUrl = ONLY_IF_EXPERIMENTAL_API_ENABLED.call(this.app, () => {
      const address = Buffer.from(this.ref.name);
      const encodedHash = multihash.encode(address, consts.CID_HASH_FN);
      const newCid = new CID(consts.CID_VERSION, consts.CID_DEFAULT_CODEC, encodedHash);
      const cidStr = newCid.toBaseEncodedString(consts.CID_BASE_ENCODING);
      return `safe://${cidStr}:${this.ref.typeTag}`;
    });

    return {
      name: this.ref.name,
      typeTag: this.ref.typeTag,
      xorUrl
    };
  }

  /**
   * Look up the mutable data object version on the network
   *
   * @returns {Promise<Number>} Current version
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         const version = await mData.getVersion();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getVersion() {
    return lib.mdata_get_version(this.app.connection, this.ref);
  }

  /**
   * Look up the value of a specific key
   *
   * @returns {Promise<ValueVersion>} the entry value and its current version
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         const entryValue = await mData.get('key1');
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  get(key) {
    return lib.mdata_get_value(this.app.connection, this.ref, key);
  }

  /**
   * Commit this {@link MutableData} to the network.
   * @param {Permission|CONSTANTS.MD_PERMISSION_EMPTY} permissions
   * the permissions to create the mutable data with
   * @param {Entries|CONSTANTS.MD_ENTRIES_EMPTY} entries
   * data entries to create the mutable data with
   * @returns {Promise}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         const perms = await mData.newPermissions();
   *         const pmSet = ['Insert', 'Update', 'Delete', 'ManagePermissions'];
   *         const pubSignKey = await app.crypto.getAppPubSignKey();
   *         await perms.insertPermissionsSet(pubSignKey, pmSet);
   *         const entries = await mData.newEntries();
   *         await entries.insert('key1', 'value1');
   *         await mData.put(perms, entries)
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  put(permissions, entries) {
    return lib.mdata_put(this.app.connection,
                          this.ref,
                          permissions ? permissions.ref : CONSTANTS.MD_PERMISSION_EMPTY,
                          entries ? entries.ref : CONSTANTS.MD_ENTRIES_EMPTY);
  }

  /**
   * Get a Handle to the entries associated with this {@link MutableData}
   * @returns {Promise<Entries>} the entries representation object
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({});
   *         const entries = mData.getEntries();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getEntries() {
    return lib.mdata_entries(this.app.connection, this.ref)
      .then((r) => h.autoref(new Entries(this.app, r)));
  }

  /**
   * Get a list with the keys contained in this {@link MutableData}
   * @returns {Promise<Array>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const entryKeysArray = mData.getKeys();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getKeys() {
    return lib.mdata_list_keys(this.app.connection, this.ref);
  }

  /**
   * Get the list of values contained in this {@link MutableData}
   * @returns {Promise<Array>} the list of values
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const entryValuesArray = mData.getValues();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getValues() {
    return lib.mdata_list_values(this.app.connection, this.ref);
  }

  /**
   * Get an interface to the permissions associated with this {@link MutableData}
   * @returns {Permissions} The permissions interface object
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const permissionsInterface = mData.getPermissions();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getPermissions() {
    return lib.mdata_list_permissions(this.app.connection, this.ref)
      .then((r) => h.autoref(new Permissions(this.app, r, this)));
  }

  /**
   * Get an interface to the permissions associated with this {@link MutableData} for
   * a specific signing key
   * @param {PubSignKey|CONSTANTS.USER_ANYONE} [signKey=CONSTANTS.USER_ANYONE]
   * @returns {Promise<Object>} Permissions set associated to the signing key
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const signKey = await app.crypto.getAppPubSignKey();
   *         const permissionSet = await mData.getUserPermissions(signKey);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getUserPermissions(signKey) {
    return lib.mdata_list_user_permissions(this.app.connection, this.ref,
                                                      signKey
                                                        ? signKey.ref
                                                        : CONSTANTS.USER_ANYONE);
  }

  /**
   * Delete the permissions of a specifc key. Directly commits to the network.
   * Requires 'ManagePermissions' permission for the app.
   * @param {PubSignKey|CONSTANTS.USER_ANYONE} [signKey=CONSTANTS.USER_ANYONE] the key to lookup for
   * @param {Number} version The version successor, to confirm you are
   *        actually asking for the right one
   * @returns {Promise} once finished
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const version = await mData.getVersion();
   *         const signKey = await app.crypto.getAppPubSignKey();
   *         const permissionSet = mData.delUserPermissions(signKey, version + 1);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  delUserPermissions(signKey, version) {
    return lib.mdata_del_user_permissions(this.app.connection,
                                          this.ref,
                                          signKey
                                            ? signKey.ref
                                            : CONSTANTS.USER_ANYONE,
                                          version);
  }

  /**
   * Set the permissions of a specifc key. Directly commits to the network.
   * Requires 'ManagePermissions' permission for the app.
   * @param {PubSignKey|CONSTANTS.USER_ANYONE} [signKey=CONSTANTS.USER_ANYONE] the key to lookup for
   * @param {PermissionSet} permissionSet The permission set to set to
   * @param {Number} version the version successor, to confirm you are
   *        actually asking for the right one
   * @returns {Promise} resolves once finished
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const version = await mData.getVersion();
   *         const pmSet = ['Insert'];
   *         const permissionSet = await mData.setUserPermissions(
   *             safe.CONSTANTS.USER_ANYONE, pmSet, version + 1
   *         );
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  setUserPermissions(signKey, permissionSet, version) {
    return lib.mdata_set_user_permissions(this.app.connection,
                                          this.ref,
                                          signKey
                                            ? signKey.ref
                                            : CONSTANTS.USER_ANYONE,
                                          permissionSet || [],
                                          version);
  }

  /**
   * Commit the transaction to the network
   * @param {EntryMutationTransaction} mutations the Mutations you want to apply
   * @return {Promise} Resolves once finished
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const mutation = await app.mutableData.newMutation();
   *         await mutation.insert('key2', 'value2')
   *         await mData.applyEntriesMutation(mutation);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  applyEntriesMutation(mutations) {
    return lib.mdata_mutate_entries(this.app.connection, this.ref, mutations.ref);
  }

  /**
   * Serialise the current {@link MutableData}
   * @returns {Promise<String>} The serialilsed version of the {@link MutableData}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const serialisedMD = await mData.serialise();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  serialise() {
    return lib.mdata_info_serialise(this.ref);
  }

  /**
   * Get serialised size of current {@link MutableData}
   * @returns {Promise<Number>} The serialilsed size of the {@link MutableData}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ key1: 'value1', key2: 'value2' });
   *         const serialisedSize = await mData.getSerialisedSize();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  getSerialisedSize() {
    return lib.mdata_serialised_size(this.app.connection, this.ref);
  }

  /**
   * Wrap this {@link MutableData} into a known abstraction. Currently only known: `NFS`
   * @param {String} eml - name of the emulation
   * @returns {Emulation} the Emulation you are asking for
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const emulationOptions = {
   *     nfs   : 'NFS',
   *     rdf   : 'RDF',
   *     webid : 'WebId'
   * };
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *         await mData.quickSetup({ });
   *         const nfs = await mData.emulateAs(emulationOptions.nfs)
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  emulateAs(eml) {
    return new emulations[eml.toUpperCase()](this);
  }
}

/**
 * API to initialise new {@link MutableData}, {@link Permissions},
 * {@link Entries}, or {@link EntryMutationTransaction} instances.
 */
class MutableDataInterface {
  /**
  * @hideconstructor
  * Create a new MutableData
  * @param {SAFEApp} app instance this is bound to
  */
  constructor(app) {
    this.app = app;
  }

  /**
   * Create a new private {@link MutableData} at a random address. Entrie can be encrypted.
   * @param {Number} typeTag
   * @throws {TYPE_TAG_NAN}
   * @returns {Promise<MutableData>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPrivate(15001);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  newRandomPrivate(typeTag) {
    if (!typeTag || !Number.isInteger(typeTag)) {
      throw makeError(errConst.TYPE_TAG_NAN.code, errConst.TYPE_TAG_NAN.msg);
    }
    return lib.mdata_info_random_private(typeTag)
          .then((mDataInfo) => this.wrapMdata(mDataInfo));
  }


  /**
   * Create a new public {@link MutableData} at a random address
   * @param {Number} typeTag
   * @throws {TYPE_TAG_NAN}
   * @returns {Promise<MutableData>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  newRandomPublic(typeTag) {
    if (!typeTag || !Number.isInteger(typeTag)) {
      throw makeError(errConst.TYPE_TAG_NAN.code, errConst.TYPE_TAG_NAN.msg);
    }
    return lib.mdata_info_random_public(typeTag)
          .then((mDataInfo) => this.wrapMdata(mDataInfo));
  }

  /**
   * Initiate a private{@link MutableData} at the given address. Entries can be encrypted.
   * @param {Buffer|String} name 32-byte name is the network address
   * @param {Number} typeTag
   * @param {Buffer|String} secKey
   * @param {Buffer|String} nonce
   * @returns {Promise<MutableData>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const name = await app.crypto.sha3Hash('1010101010101');
   *         const encKeyPair = await app.crypto.generateEncKeyPair();
   *         const secKey = encKeyPair.secEncKey;
   *         const nonce = await app.crypto.generateNonce()
   *         const mData = await app.mutableData.newPrivate(name, 15002, secKey, nonce);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  newPrivate(name, typeTag, secKey, nonce) {
    return lib.mdata_info_new_private(name, typeTag, secKey, nonce)
          .then((mDataInfo) => this.wrapMdata(mDataInfo));
  }

  /**
   * Initiate a public {@link MutableData} at the given address
   * @param {Buffer|String} 32-byte name is the network address
   * @param {Number} typeTag
   * @returns {Promise<MutableData>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const name = await app.crypto.sha3Hash('1010101010101');
   *         const mData = await app.mutableData.newPublic(name, 15002);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  newPublic(name, typeTag) {
    const mDataInfo = lib.makeMDataInfoObj({ name, type_tag: typeTag });
    return Promise.resolve(this.wrapMdata(mDataInfo));
  }

  /**
   * Create a new Permissions object.
   * @returns {Promise<Permissions>} Permissions interface
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *         const permissions = await app.mutableData.newPermissions();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  newPermissions() {
    return lib.mdata_permissions_new(this.app.connection)
        .then((r) => h.autoref(new Permissions(this.app, r)));
  }

  /**
   * Create a new {@link EntryMutationTransaction} object.
   * @returns {Promise<EntryMutationTransaction>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *         const permissions = await app.mutableData.newMutation();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  newMutation() {
    return lib.mdata_entry_actions_new(this.app.connection)
        .then((r) => h.autoref(new EntryMutationTransaction(this.app, r)));
  }

  /**
   * Create a new Entries object.
   * @returns {Promise<Entries>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         const mData = await app.mutableData.newRandomPublic(15001);
   *         const permissions = await app.mutableData.newEntries();
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  newEntries() {
    return lib.mdata_entries_new(this.app.connection)
        .then((r) => h.autoref(new Entries(this.app, r)));
  }

  /**
   * Create a new {@link MutuableData} object from serialised format
   * @returns {Promise<MutableData>}
   * @example
   * // Assumes {@link initialiseApp|SAFEApp} interface has been obtained
   * const asyncFn = async () => {
   *     try {
   *         let mData = await app.mutableData.newRandomPublic(15001);
   *         await mData.quickSetup({ });
   *         const serialisedMD = await mData.serialise();
   *         mData = await app.mutableData.fromSerial(serialisedMD);
   *     } catch (err) {
   *         throw err;
   *     }
   * };
   */
  fromSerial(serial) {
    return lib.mdata_info_deserialise(serial)
        .then((mDataInfo) => this.wrapMdata(mDataInfo));
  }

  /**
  * @private
  * Helper to create a new autorefence MutableData for a given
  * mdata reference from the native layer
  *
  * @param {handle} mDataInfo - the native handle
  * @returns {MutableData} - wrapped
  */
  wrapMdata(mDataInfo) {
    return new MutableData(this.app, mDataInfo);
  }

}

module.exports = MutableDataInterface;