Source: engine/universal/distribution/src/database/db.js

const { data } = require('@proceed/system');
const {
  getDefinitionsName,
  getDefinitionsId,
  getDeploymentMethod,
  getProcessIds,
} = require('@proceed/bpmn-helper');

const { validateProcess } = require('./processInfoValidation.js');

const prefix = 'process_';

/**
 * Function that filters the User Tasks HTML fileNames from the general process information object
 *
 * @param {Object} processInfo contains all known information for a process
 * @param {Boolean} imported indicates if we want the HTML for the process or for the processes imported by the process
 * @returns {Array} - array containing the fileNames for all User-Task data we have
 */
function filterHTML(processInfo, imported) {
  let prefix = 'html_';

  if (imported) {
    prefix = `import_${prefix}`;
  }

  return Object.keys(processInfo)
    .filter((key) => key.startsWith(prefix))
    .map((key) => key.substring(prefix.length, key.length));
}

/**
 * Function that filters the imported process descripions from the general process information object
 *
 * @param {Object} processInfo contains all known information for a process
 * @returns {Object} - mapping from definitionId of imported process definition to actual definition
 */
function filterImportedProcesses(processInfo) {
  return Object.keys(processInfo)
    .filter((key) => key.startsWith('import_'))
    .filter((key) => !key.includes('html_'))
    .reduce((curr, key) => {
      const importedDefinitionId = key.substring(7, key.length);
      curr[importedDefinitionId] = processInfo[key];
      return curr;
    }, {});
}

module.exports = {
  /**
   * Checks if the file with process information exists
   *
   * @param {String} definitionId name of the file the definition of the process is stored in
   * @returns {Boolean} - indicates if the file exists or not
   */
  async isProcessExisting(definitionId) {
    const result = await data.read(`processes/${definitionId}`);
    if (result === null) {
      return false;
    }
    return true;
  },

  /**
   * Returns true if the process is valid and complete
   * e.g. there are definitions for all imported processes
   *
   * @param {String} definitionId name of the file the definition of the process is stored in
   * @returns {Boolean} - indicates if the process is valid and can be executed
   */
  async isProcessValid(definitionId) {
    //check if the process was already validated, yes? => just return true
    const process = await data.read(definitionId);

    if (process.validated && JSON.parse(process.validated)) {
      return true;
    }

    // get all known assets for the process
    const knownUserTaskFiles = filterHTML(process, false);
    const knownImportedUserTaskFiles = filterHTML(process, true);
    const knownImports = filterImportedProcesses(process);

    // assert if we have all required assets for the process to be executed
    const result = await validateProcess(
      process.bpmn,
      knownUserTaskFiles,
      knownImports,
      knownImportedUserTaskFiles
    );

    // set flag in the process file that the validity check was done and succesful
    if (result === true) {
      await data.write(`${definitionId}/validated`, JSON.stringify(true));
    }

    return result;
  },

  /**
   * Saves a process definition under the given definitionid with some metadata
   *
   * @param {String} definitionId
   * @param {String} bpmn the process definition
   */
  async saveProcessDefinition(definitionId, bpmn) {
    const processIds = await getProcessIds(bpmn);

    if (processIds.length > 1) {
      throw new Error('Only process definitions containing one process allowed!');
    }

    await data.write(`${definitionId}/bpmn`, bpmn);
    await data.write(`${definitionId}/validated`, JSON.stringify(false));
    await data.write(`${definitionId}/deploymentDate`, JSON.stringify(Date.now()));
    await data.write(`processes/${definitionId}`, JSON.stringify(processIds[0]));
  },

  /**
   * Get list with all processes known to this machine
   *
   * @returns {Object} - mapping from definitionIds to the process ids of the processes contained in the files
   */
  async getAllProcesses() {
    const processes = await data.read('processes');
    return processes || {};
  },

  /**
   * Gets the BPMN of the process
   *
   * @param {String} definitionId
   * @returns {String} - the process definition
   */
  async getProcess(definitionId) {
    if (!(await this.isProcessExisting(definitionId))) {
      throw new Error('Process with given definitionId does not exist!');
    }
    const process = await data.read(`${definitionId}/bpmn`);
    return process;
  },

  /**
   * Gets the definition and additional information (e.g. date of deployment, id, name, method of deployment) for a process
   *
   * @param {String} processDefinitionId
   * @returns {Object} - { bpmn, deploymentDate, definitionId, definitionName, deploymentMethod }
   */
  async getProcessInfo(processDefinitionId) {
    const process = await data.read(`${processDefinitionId}`);

    if (!process) {
      throw new Error('Process with given definitionId does not exist!');
    }

    const { bpmn, deploymentDate } = {
      bpmn: process.bpmn,
      deploymentDate: JSON.parse(process.deploymentDate),
    };

    const definitionId = await getDefinitionsId(bpmn);
    const definitionName = await getDefinitionsName(bpmn);
    const deploymentMethod = await getDeploymentMethod(bpmn);

    return { bpmn, deploymentDate, definitionId, definitionName, deploymentMethod };
  },

  /**
   * Removes the information stored about a process
   *
   * @param {String} definitionId
   */
  async deleteProcess(definitionId) {
    await data.delete(definitionId);
    await data.delete(`processes/${definitionId}`);
  },

  /**
   * Saves the definition about a process imported by another process in the file of the importing process
   *
   * @param {String} definitionId name of the file the process that is importing is stored in
   * @param {String} importedDefinitionId name of the file the imported process definition would be stored under
   * @param {String} importedBpmn process definition of the imported process
   */
  async saveImportedProcessDefinition(definitionId, importedDefinitionId, importedBpmn) {
    if (!importedBpmn) {
      throw new Error('Process definition content must not be empty!');
    }

    await data.write(`${definitionId}/import_${importedDefinitionId}`, importedBpmn);
  },

  /**
   * Get all processes imported by the process stored in the file with the given name
   *
   * @param {String} definitionId
   * @returns {Object} - mapping from imported process definitionids to the definitions of the processes
   */
  async getImportedProcesses(definitionId) {
    const process = await data.read(`${definitionId}`);
    const importedProcesses = filterImportedProcesses(process);
    return importedProcesses;
  },

  /**
   * Gets a specific imported process with the given definitionId for the importing process with the given definitionId
   *
   * @param {String} definitionId
   * @param {String} importedDefinitionId
   * @returns {String} - the definition of the imported process
   */
  async getImportedProcess(definitionId, importedDefinitionId) {
    const importedProcesses = await this.getImportedProcesses(definitionId);

    if (!importedProcesses) {
      return;
    }

    return importedProcesses[importedDefinitionId];
  },

  /**
   * Saves the HTML for a specific user task in a specific process stored in the file with the given definitionId
   *
   * @param {String} definitionId
   * @param {String} fileName the fileName as given in the proceed:fileName attribute of the user task
   * @param {String} html
   * @param {Boolean} imported indicator if the user task is located in a process imported by the main process
   */
  async saveHTMLString(definitionId, fileName, html, imported) {
    if (!(await this.isProcessExisting(definitionId))) {
      throw new Error('Process with given ID does not exist!');
    }

    if (!html) {
      throw new Error('HTML content must not be empty!');
    }

    let prefix = 'html_';

    if (imported) {
      prefix = `import_${prefix}`;
    }

    await data.write(`${definitionId}/${prefix}${fileName}`, html);
  },

  /**
   * Gets the html for a specific user task in the process stored under the given definitionId
   *
   * @param {String} definitionId
   * @param {String} fileName the fileName as given in the proceed:fileName attribute of the user task
   * @param {Boolean} imported indicator if the user task is located in a process imported by the main process
   */
  async getHTML(definitionId, fileName, imported) {
    let prefix = 'html_';

    if (imported) {
      prefix = `import_${prefix}`;
    }

    const html = await data.read(`${definitionId}/${prefix}${fileName}`);

    if (!html) {
      throw new Error("No HTML found. Either the process or the html doesn't seem to exist.");
    }

    return html;
  },

  /**
   * Get the fileNames for all the user task data given for the process stored under the given definitionId
   *
   * @param {String} definitionId
   * @param {String} imported indicates if we want to know the user tasks in the imported processes or the ones in the main process
   * @returns {Array} - array containing the ids for all (imported) user tasks
   */
  async getAllUserTasks(definitionId, imported) {
    // TODO: use asterisk notation for html ids
    const process = await data.read(`${definitionId}`);

    if (!process) {
      throw new Error('Process with given ID does not exist!');
    }

    const htmlFileNames = filterHTML(process, imported);
    return htmlFileNames;
  },

  /**
   * Stores the instance information for process instances that ended to make them available even after restarting the engine
   *
   * @param {String} definitionId
   * @param {String} instanceInfo
   */
  async archiveInstance(definitionId, instanceId, instanceInfo) {
    data.write(`${definitionId}/archived_instance_${instanceId}`, JSON.stringify(instanceInfo));
  },

  async getArchivedInstances(definitionId) {
    const process = await data.read(definitionId);

    if (!process) {
      throw new Error('Process with given ID does not exist!');
    }

    return Object.keys(process)
      .filter((key) => key.startsWith('archived_instance_'))
      .reduce((curr, key) => {
        const instanceId = key.substring(18, key.length);
        curr[instanceId] = JSON.parse(process[key]);
        return curr;
      }, {});
  },

  /**
   * Adds an attribute to process data
   *
   * @param {String} definitionId id of the process to add the attribute to
   * @param {String} key key under which the attribute is supposed to be stored
   * @param {String} value attribute value
   */
  async addProcessAttribute(definitionId, key, value) {
    await data.write(`${definitionId}/${key}`, value);
  },

  /**
   * Returns the value of a specific process attribute
   *
   * @param {String} definitionId id of the process
   * @param {String} key key under which the attribute is supposed to be stored
   * @returns {String} the attribute value
   */
  async getProcessAttribute(definitionId, key) {
    return await data.read(`${definitionId}/${key}`);
  },
};