api/emulations/rdf.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 rdflib = require('rdflib');
const errConst = require('../../error_const');
const makeError = require('../../native/_error.js');
const { EXPOSE_AS_EXPERIMENTAL_API } = require('../../helpers');

const JSON_LD_MIME_TYPE = 'application/ld+json';
const RDF_GRAPH_ID = '@id';

/**
* Represents an RDF node resource named by an absolute URI property
* @typedef {Object} NamedNode
* @property {String} termType=NamedNode NamedNode
* @property {String} value Absolute URI
* @property {String} uri Absolute URI
*/

/**
* Represents a [blank node](https://en.wikipedia.org/wiki/Blank_node} RDF resource
* @typedef {Object} BlankNode
* @property {String} termType=BlankNode BlankNode
* @property {String} value=n0 n0
* @property {String} id=n0 n0
*/

/**
* Represents an literal RDF node resource such as a noun or date-time
* @typedef {Object} LiteralNode
* @property {String} termType=LiteralNode LiteralNode
* @property {String} value Either string or XSD datatype
* @property {String} [lang] Optional i18n language tag
* @property {NamedNode} [datatype]
*/

/**
* Experimental RDF emulation on top of a {@link MutableData}
* @hideconstructor
*/
class RDF {
  constructor(mData) {
    this.mData = mData;
    this.graphStore = rdflib.graph();
    this.id = undefined;
    this.vocabs = {
      LDP: this.namespace('http://www.w3.org/ns/ldp#'),
      RDF: this.namespace('http://www.w3.org/2000/01/this-schema#'),
      RDFS: this.namespace('http://www.w3.org/1999/02/22-this-syntax-ns#'),
      FOAF: this.namespace('http://xmlns.com/foaf/0.1/'),
      OWL: this.namespace('http://www.w3.org/2002/07/owl#'),
      DCTERMS: this.namespace('http://purl.org/dc/terms/'),
      SAFETERMS: this.namespace('http://safenetwork.org/safevocab/')
    };
  }
  /**
   * Sets ID for graph store
   * @param {String} id ID representing current in-memory graph store.
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *     const rdf = await mData.emulateAs('RDF');
   *     const id = rdf.sym("safe://pluto.astronomy");
   *     rdf.setId(id.uri);
   *     console.log(rdf.id);
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  setId(id) {
    this.id = id;
  }

  /**
   * Fetch the RDF data stored in the underlying MutableData on the network
   * and load it in memory to allow manipulating triples before commit them again.
   *
   * @param {Array<String>} ids list of RDF graph IDs to use as a filter for fetching
   * graphs, e.g. ['safe://mywebid.mypubname', 'safe://mypubname']
   * @param {Boolean} [toDecrypt=false] flag to decrypt the data being fetched
   *
   * @throws {ERR_SERIALISING_DESERIALISING|MISSING_RDF_ID}
   * @returns {Promise<Array>}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   *
   * const ids = [];
   * const toEncrypt = false;
   * const toDecrypt = false;
   * const asyncFn = async () => {
   *     try {
   *         const rdf = await mData.emulateAs('RDF');
   *         const id = rdf.sym("safe://pluto.astronomy");
   *         const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *         const triples = [
   *             {
   *                 predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *                 object : DBP('Pluto')
   *             },
   *             {
   *                 predicate : rdf.sym("http://dbpedia.org/property/atmosphereComposition"),
   *                 object : DBP("Methane")
   *             },
   *             {
   *                 predicate : rdf.vocabs.RDF('type'),
   *                 object : DBP("Dwarf_planet")
   *             },
   *             {
   *                 predicate : rdf.sym("http://dbpedia.org/ontology/discovered"),
   *                 object : literalNode
   *             }
   *         ];
   *         // The subject of each triple is the same in this example
   *         triples.forEach( triple => rdf.add(id, triple.predicate, triple.object) );
   *         const nameAndTag = await rdf.commit(toEncrypt);
   *         const entryGraphArray = await rdf.nowOrWhenFetched(ids, toDecrypt);
   *     } catch(err) {
   *         throw err;
   *     }
   * };
   */
  async nowOrWhenFetched(ids, toDecrypt = false) {
    let entriesList = [];
    let entries;

    if (ids && ids.length > 0) {
      const graphsToFetch = (!Array.isArray(ids)) ? [ids] : ids;
      // TODO: support a list of more than one id
      // Promise.all(ids.map(async (e) => {
      const serialisedGraph = await this.mData.get(graphsToFetch[0]);
      entriesList.push({ key: graphsToFetch[0], value: serialisedGraph });
      // }));
    } else {
      entries = await this.mData.getEntries();
      entriesList = await entries.listEntries();
    }

    if (entriesList.length === 0) return;

    let id;
    const validGraphs = await entriesList.reduce(async (graphs, entry) => {
      const reducedGraphs = await graphs;
      let keyStr = entry.key.toString();
      let valueStr = entry.value.buf.toString();
      if (toDecrypt && valueStr.length > 0) {
        try {
          const decryptedKey = await this.mData.decrypt(entry.key);
          keyStr = decryptedKey.toString();
          const decryptedValue = await this.mData.decrypt(entry.value.buf);
          valueStr = decryptedValue.toString();
        } catch (error) {
          if (error.code !== errConst.ERR_SERIALISING_DESERIALISING.code) {
            console.warn('Error decrypting MutableData entry in rdf.nowOrWhenFetched()');
          }
          // ok, let's then assume the entry is not encrypted
          // this maybe temporary, just for backward compatibility,
          // but in the future we should always expect them to be encrpyted
        }
      }

      // If the entry was soft-deleted skip it, or if it's not
      // an RDF graph entry also ignore it
      if (valueStr.length === 0 || !keyStr.startsWith('safe://')) {
        return reducedGraphs;
      }

      if (!id) {
        // FIXME: we need to know which is the main graph in a deterministic way
        // perhaps when we have XOR-URLs we will be able to check a match between
        // this MD's location and the @id value which will be set to the XOR-URL.
        id = JSON.parse(valueStr)[RDF_GRAPH_ID];
      }

      reducedGraphs.push(valueStr);
      return reducedGraphs;
    }, Promise.resolve([]));

    const entriesGraphs = await Promise.all(validGraphs);

    if (!id) {
      // This simply means that none of the existing entries are RDF graphs.
      // We throw the error and it's up to the caller to decide
      // what to do in such an scenario
      throw makeError(errConst.MISSING_RDF_ID.code, errConst.MISSING_RDF_ID.msg);
    }

    return Promise.all(entriesGraphs.map((graph) => this.parse(graph, JSON_LD_MIME_TYPE, id)));
  }

  /**
   * Creates resource namespace prefix and returns function to create {@link NamedNode}.
   * @param {String} uri Absolute namespace URI
   * @returns {function(string): NamedNode}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *     const rdf = await mData.emulateAs('RDF');
   *     const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *     const dwarfPlanetNodeResource = DBP("Dwarf_planet");
   *     const formerPlanetNodeResource = DBP("Former_planet");
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  namespace(uri) { // eslint-disable-line class-methods-use-this
    return rdflib.Namespace(uri);
  }

  /**
   * @param {String|Number} value Literal value
   * @param {String} languageOrDatatype Either i18n language tag or XSD URI data type
   * @returns {LiteralNode}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *     const rdf = await mData.emulateAs('RDF');
   *     const discoveryDate = new Date("18 Feb 1930");
   *     const dateTimeDataType = "http://www.w3.org/2001/XMLSchema#dateTime";
   *     let literalNode = rdf.literal(discoveryDate.toISOString(), dateTimeDataType);
   *     console.log( JSON.stringify(literalNode) );
   *
   *     // Alternatively
   *     literalNode = rdf.literal("Aardvark", "en-US");
   *     console.log( JSON.stringify(literalNode) );
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  literal(value, languageOrDatatype) { // eslint-disable-line class-methods-use-this
    return rdflib.literal(value, languageOrDatatype);
  }

  collection(nodes) { // eslint-disable-line class-methods-use-this
    return new rdflib.Collection(nodes);
  }

  /**
   * @returns {BlankNode}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *     const rdf = await mData.emulateAs('RDF');
   *     const blankNode = rdf.bnode();
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  bnode() { // eslint-disable-line class-methods-use-this
    return rdflib.blankNode();
  }

  /**
   * Creates an RDF resource identified by a URI
   * @returns {NamedNode}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *     const rdf = await mData.emulateAs('RDF');
   *     const predicateUri = "http://dbpedia.org/ontology/discovered";
   *     const predicateResource = rdf.sym(predicateUri);
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  sym(uri) { // eslint-disable-line class-methods-use-this
    return rdflib.sym(uri);
  }

  /**
   * Retrieves one node, the first to match any combination of subject, predicate, or object.
   * A null value is a wildcard.
   * @param {NamedNode|BlankNode|null} [subject] https://www.w3.org/TR/rdf-schema/#ch_subject
   * @param {NamedNode|null} [predicate] https://www.w3.org/TR/rdf-schema/#ch_predicate
   * @param {LiteralNode|NamedNode|null} [object] https://www.w3.org/TR/rdf-schema/#ch_object
   * @param {NamedNode} [provenance] https://www.w3.org/TR/2014/REC-n-quads-20140225/#sec-intro
   * @returns {LiteralNode|BlankNode|NamedNode} Single node resource
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triples = [
   *           {
   *               predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *               object : DBP('Pluto')
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/property/atmosphereComposition"),
   *               object : DBP("Methane")
   *           },
   *           {
   *               predicate : rdf.vocabs.RDF('type'),
   *               object : DBP("Dwarf_planet")
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/ontology/discovered"),
   *               object : literalNode
   *           }
   *       ];
   *       // The subject of each triple is the same in this example
   *       triples.forEach( triple => rdf.add(id, triple.predicate, triple.object) );
   *       const result = rdf.any(subject, predicate, object);
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  any(subject, predicate, object, provenance) {
    return this.graphStore.any(subject, predicate, object, provenance);
  }

  /**
   * Retrieves all nodes matching any combination of subject, predicate, or object.
   * A null value is a wildcard.
   * @param {NamedNode|BlankNode|null} [subject] https://www.w3.org/TR/rdf-schema/#ch_subject
   * @param {NamedNode|null} [predicate] https://www.w3.org/TR/rdf-schema/#ch_predicate
   * @param {LiteralNode|NamedNode|null} [object] https://www.w3.org/TR/rdf-schema/#ch_object
   * @param {NamedNode} [provenance] https://www.w3.org/TR/2014/REC-n-quads-20140225/#sec-intro
   * @returns {Array} Turtle document
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triples = [
   *           {
   *               predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *               object : DBP('Pluto')
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/property/atmosphereComposition"),
   *               object : DBP("Methane")
   *           },
   *           {
   *               predicate : rdf.vocabs.RDF('type'),
   *               object : DBP("Dwarf_planet")
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/ontology/discovered"),
   *               object : literalNode
   *           }
   *       ];
   *       // The subject of each triple is the same in this example
   *       triples.forEach( triple => rdf.add(id, triple.predicate, triple.object) );
   *       const result = rdf.each(subject, predicate, object);
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  each(subject, predicate, object, provenance) {
    return this.graphStore.each(subject, predicate, object, provenance);
  }

  /**
   * Retrieves all statements matching any combination of subject, predicate, or object.
   * A null value is a wildcard.
   * @param {NamedNode|BlankNode|null} [subject] https://www.w3.org/TR/rdf-schema/#ch_subject
   * @param {NamedNode|null} [predicate] https://www.w3.org/TR/rdf-schema/#ch_predicate
   * @param {LiteralNode|NamedNode|null} [object] https://www.w3.org/TR/rdf-schema/#ch_object
   * @param {NamedNode} [provenance] https://www.w3.org/TR/2014/REC-n-quads-20140225/#sec-intro
   * @returns {Array}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triples = [
   *           {
   *               predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *               object : DBP('Pluto')
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/property/atmosphereComposition"),
   *               object : DBP("Methane")
   *           },
   *           {
   *               predicate : rdf.vocabs.RDF('type'),
   *               object : DBP("Dwarf_planet")
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/ontology/discovered"),
   *               object : literalNode
   *           }
   *       ];
   *       // The subject of each triple is the same in this example
   *       triples.forEach( triple => rdf.add(id, triple.predicate, triple.object) );
   *       const result = rdf.statementsMatching(subject, predicate, object);
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  statementsMatching(subject, predicate, object, provenance) {
    return this.graphStore.statementsMatching(subject, predicate, object, provenance);
  }

  /**
   * Remove all statements matching any combination of subject, predicate or object
   * @param {NamedNode|BlankNode|null} [subject] https://www.w3.org/TR/rdf-schema/#ch_subject
   * @param {NamedNode|null} [predicate] https://www.w3.org/TR/rdf-schema/#ch_predicate
   * @param {LiteralNode|NamedNode|null} [object] https://www.w3.org/TR/rdf-schema/#ch_object
   * @param {NamedNode} [provenance] https://www.w3.org/TR/2014/REC-n-quads-20140225/#sec-intro
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triples = [
   *           {
   *               predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *               object : DBP('Pluto')
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/property/atmosphereComposition"),
   *               object : DBP("Methane")
   *           },
   *           {
   *               predicate : rdf.vocabs.RDF('type'),
   *               object : DBP("Dwarf_planet")
   *           },
   *           {
   *               predicate : rdf.sym("http://dbpedia.org/ontology/discovered"),
   *               object : literalNode
   *           }
   *       ];
   *       // The subject of each triple is the same in this example
   *       triples.forEach( triple => rdf.add(id, triple.predicate, triple.object) );
   *       rdf.removeMany(subject, predicate, object);
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  removeMany(subject, predicate, object, provenance) {
    return this.graphStore.removeMany(subject, predicate, object, provenance);
  }

  /**
   * Parse serialised RDF data and place into graph store
   * @param {String} data Serialised RDF
   * @param {String} mimeType
   * @param {String} id Arbitrary absolute URI to identify graph
   * @returns {Promise<String>} RDF document according to mime type
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   *   const mimeType = "text/turtle";
   *   const asyncFn = async () => {
   *       try {
   *           const rdf = await mData.emulateAs('RDF');
   *           const id = rdf.sym("safe://pluto.astronomy");
   *           const serialised = `
   *           \@prefix : <#>.
   *           \@prefix ont: <http://dbpedia.org/ontology/>.
   *           \@prefix XML: <http://www.w3.org/2001/XMLSchema#>.
   *           \@prefix pro: <http://dbpedia.org/property/>.
   *           \@prefix res: <http://dbpedia.org/resource/>.
   *           \@prefix n0: <http://www.w3.org/1999/02/22-this-syntax-ns#>.
   *           \@prefix thi: <http://www.w3.org/2000/01/this-schema#>.
   *
   *           <>
   *               ont:discovered "1930-02-18T08:00:00.000Z"^^XML:dateTime;
   *               pro:atmosphereComposition res:Methane;
   *               n0:isDefinedBy res:Pluto;
   *               thi:type res:Dwarf_planet.
   *           `;
   *           var parsedData = await rdf.parse(serialised, mimeType, id);
   *       } catch(err) {
   *           throw err;
   *       }
   *       console.log(parsedData);
   *   };
   */
  parse(data, mimeType, id) {
    return new Promise((resolve, reject) => {
      const cb = (err, parsed) => {
        if (err) {
          return reject(err);
        }
        this.setId(id);
        resolve(parsed);
      };

      // since we provide a callback then parse becomes async
      rdflib.parse(data, this.graphStore, id, mimeType, cb);
    });
  }

  /**
   * Adds RDF statement to graph
   * @param {NamedNode|BlankNode|null} [subject] https://www.w3.org/TR/rdf-schema/#ch_subject
   * @param {NamedNode|null} [predicate] https://www.w3.org/TR/rdf-schema/#ch_predicate
   * @param {LiteralNode|NamedNode|null} [object] https://www.w3.org/TR/rdf-schema/#ch_object
   * @param {NamedNode} [provenance] https://www.w3.org/TR/2014/REC-n-quads-20140225/#sec-intro
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const asyncFn = async () => {
   *   try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triple = {
   *           predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *           object : DBP('Pluto')
   *       };
   *       rdf.add(id, triple.predicate, triple.object)
   *   } catch (err) {
   *     throw err;
   *   }
   * };
   */
  add(subject, predicate, object, provenance) {
    this.graphStore.add(subject, predicate, object, provenance);
  }

  /**
   * Serialise RDF data
   * @param {String} mimeType
   * @returns {Promise<String>} RDF document according to mime type
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const mimeType = "text/turtle";
   * const asyncFn = async () => {
   *     try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triple = {
   *           predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *           object : DBP('Pluto')
   *       };
   *       rdf.add(id, triple.predicate, triple.object)
   *       const serialised = await rdf.serialise(mimeType);
   *     } catch(err) {
   *         throw err;
   *     }
   *     console.log(serialised);
   * };
   */
  async serialise(mimeType) {
    return new Promise((resolve, reject) => {
      const cb = (err, parsed) => {
        if (err) {
          return reject(err);
        }
        resolve(parsed);
      };
      // TODO: serialise it with compact when it's jsonld. This is
      // currently not possible as it's not supporrted by rdflib.js
      rdflib.serialize(null, this.graphStore, this.id, mimeType, cb);
    });
  }

  /**
   * Commit the RDF document to the underlying MutableData on the network
   *
   * @param {Boolean} [toEncrypt=false] flag to encrypt the data to be committed
   * @throws {ERR_SERIALISING_DESERIALISING}
   * @returns {Promise<NameAndTag>}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const toEncrypt = false;
   * const asyncFn = async () => {
   *     try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triple = {
   *           predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *           object : DBP('Pluto')
   *       };
   *       rdf.add(id, triple.predicate, triple.object)
   *       const nameAndTag = await rdf.commit(toEncrypt);
   *     } catch(err) {
   *         throw err;
   *     }
   * };
   */
  async commit(toEncrypt = false) {
    const serialJsonLd = await this.serialise(JSON_LD_MIME_TYPE);
    const graphs = JSON.parse(serialJsonLd);
    const entries = await this.mData.getEntries();
    const entriesList = await entries.listEntries();
    const mutation = await this.mData.app.mutableData.newMutation();
    const mData = this.mData;
    const graphPromises = graphs.map(async (graph) => {
      const unencryptedKey = graph[RDF_GRAPH_ID];
      let key = unencryptedKey;
      let match = false;

      // find the current graph in the entries list and remove it
      // (before replacing via the rdf graph) this is to be able to remove any
      // remaining entries (not readded via rdf) as they have been
      // removed from this graph.
      await Promise.all(entriesList.map(async (entry, i) => {
        if (!entry || !entry.key || match) return;

        let keyToCheck = entry.key.toString();

        if (toEncrypt) {
          try {
            const decryptedKey = await mData.decrypt(entry.key);
            keyToCheck = decryptedKey.toString();
          } catch (error) {
            if (error.code !== errConst.ERR_SERIALISING_DESERIALISING.code) {
              console.warn('Error decrypting MutableData entry in rdf.commit():', error);
            }
            // ok, let's then assume the entry is not encrypted
            // this maybe temporary, just for backward compatibility,
            // but in the future we should always expect them to be encrpyted
          }
        }

        if (unencryptedKey === keyToCheck) {
          delete entriesList[i];
          match = entry;
        }
      }));

      let stringifiedGraph = JSON.stringify(graph);
      if (toEncrypt) {
        key = await mData.encryptKey(key);
        stringifiedGraph = await mData.encryptValue(stringifiedGraph);
      }

      if (match) {
        return mutation.update(key, stringifiedGraph, match.value.version + 1);
      }
      return mutation.insert(key, stringifiedGraph);
    });

    await Promise.all(graphPromises);

    // remove RDF entries which are not present in new RDF
    await entriesList.forEach(async (entry) => {
      if (entry) {
        let keyToCheck = entry.key.toString();

        if (toEncrypt) {
          try {
            const decryptedKey = await mData.decrypt(entry.key);
            keyToCheck = decryptedKey.toString();
          } catch (error) {
            if (error.code !== errConst.ERR_SERIALISING_DESERIALISING.code) {
              console.warn('Error decrypting MutableData entry in rdf.commit():', error);
            }
            // ok, let's then assume the entry is not encrypted
            // this maybe temporary, just for backward compatibility,
            // but in the future we should always expect them to be encrpyted
          }
        }

        if (keyToCheck.startsWith('safe://')) {
          await mutation.delete(entry.key, entry.value.version + 1);
        }
      }
    });

    await this.mData.applyEntriesMutation(mutation);
    const nameAndTag = await this.mData.getNameAndTag();
    return nameAndTag;
  }

  /**
  * Append the triples to the RDF document into the underlying MutableData on the network
  * @returns {Promise<NameAndTag>}
   * @example
   * // Assumes {@link MutableData} interface has been obtained
   * const toEncrypt = false;
   * const asyncFn = async () => {
   *     try {
   *       const rdf = await mData.emulateAs('RDF');
   *       const id = rdf.sym("safe://pluto.astronomy");
   *       const subject = id;
   *       const predicate = null;
   *       const object = null;
   *       const DBP = rdf.namespace( 'http://dbpedia.org/resource/' );
   *       const triple = {
   *           predicate : rdf.vocabs.RDFS('isDefinedBy'),
   *           object : DBP('Pluto')
   *       };
   *       rdf.add(id, triple.predicate, triple.object)
   *       let nameAndTag = await rdf.commit(toEncrypt);
   *       rdf.add(id, triple.predicate, triple.object)
   *       const newTriple = {
   *           predicate : rdf.vocabs.RDF('type'),
   *           object : DBP("Dwarf_planet")
   *       };
   *       rdf.add(id, newTriple.predicate, newTriple.object)
   *       nameAndTag = await rdf.append();
   *     } catch(err) {
   *         throw err;
   *     }
   * };
  */
  async append() {
    // TODO: this currently only supports adding graphs with different ID
    const serialJsonLd = await this.serialise(JSON_LD_MIME_TYPE);
    const graphs = JSON.parse(serialJsonLd);
    const mutation = await this.mData.app.mutableData.newMutation();
    graphs.forEach((e) => {
      const key = e[RDF_GRAPH_ID];
      const stringifiedGraph = JSON.stringify(e);
      mutation.insert(key, stringifiedGraph);
    });

    await this.mData.applyEntriesMutation(mutation);
    const nameAndTag = await this.mData.getNameAndTag();
    return nameAndTag;
  }
}

class rdfEmulationFactory {
  /**
  * @private
  * Instantiate the RDF emulation layer wrapping a MutableData instance,
  * hiding the whole RDF emulation class behind the experimental API flag
  *
  * @param {MutableData} mData the MutableData to wrap around
  */
  constructor(mData) {
    /* eslint-disable camelcase, prefer-arrow-callback */
    return EXPOSE_AS_EXPERIMENTAL_API.call(mData.app, function RDF_Emulation() {
      return new RDF(mData);
    });
  }
}

module.exports = rdfEmulationFactory;