Source: management-system/src/backend/shared-electron-server/data/process.js

import eventHandler from '../../../frontend/backend-api/event-system/EventHandler.js';
import store from './store.js';
import logger from '../logging.js';
import {
  saveProcess,
  deleteProcess,
  getUserTaskIds,
  getUserTaskHTML,
  getUserTasksHTML,
  saveUserTaskHTML,
  deleteUserTaskHTML,
  getBPMN,
  updateProcess as overwriteProcess,
  getUpdatedProcessesJSON,
} from './fileHandling.js';
import helperEx from '../../../shared-frontend-backend/helpers/javascriptHelpers.js';
const { mergeIntoObject } = helperEx;
import processHelperEx from '../../../shared-frontend-backend/helpers/processHelpers.js';
const { getProcessInfo } = processHelperEx;

export let processMetaObjects = {};

/**
 * Returns all known processes in form of an array
 *
 * @returns {Array} - array containing all known processes
 */
export function getProcesses() {
  return Object.values(processMetaObjects);
}

/**
 * Throws if process with given id doesn't exist
 *
 * @param {String} processDefinitionsId
 */
export function checkIfProcessExists(processDefinitionsId) {
  if (!processMetaObjects[processDefinitionsId]) {
    throw new Error(`Process with id ${processDefinitionsId} does not exist!`);
  }
}

/**
 * Handles adding a process, makes sure all necessary information gets parsed from bpmn
 *
 * @param {String} bpmn the xml description of the process to create
 * @returns {Object} - returns an object containing the intitial process information
 */
export async function addProcess(processData) {
  const { bpmn } = processData;
  delete processData.bpmn;

  if (!bpmn) {
    throw new Error("Can't create a process without a bpmn!");
  }

  const date = new Date().toUTCString();

  // create meta info object
  const metadata = {
    inEditingBy: [],
    departments: [],
    variables: [],
    createdOn: date,
    lastEdited: date,
    type: 'process',
    ...processData,
    ...(await getProcessInfo(bpmn)),
  };

  const { id: processDefinitionsId } = metadata;

  // check if there is an id collision
  if (processMetaObjects[processDefinitionsId]) {
    throw new Error('Tried to add process with id of an existing process!');
  }

  // save process info
  processMetaObjects[processDefinitionsId] = metadata;
  // write meta data to store
  store.add('processes', removeExcessiveInformation(metadata));
  // save bpmn
  await saveProcess(processDefinitionsId, bpmn);

  eventHandler.dispatch('processAdded', { process: metadata });

  return metadata;
}

/**
 * Updates an existing process with the given bpmn
 *
 * @param {String} processDefinitionsId
 * @param {String} newBpmn
 * @returns {Object} - contains the new process meta information
 */
export async function updateProcess(processDefinitionsId, newInfo) {
  checkIfProcessExists(processDefinitionsId);

  const { bpmn: newBpmn } = newInfo;
  delete newInfo.bpmn;

  let metaChanges = {
    ...newInfo,
  };

  if (newBpmn) {
    // get new info from bpmn
    metaChanges = {
      ...metaChanges,
      ...(await getProcessInfo(newBpmn)),
    };
  }

  const newMetaData = await updateProcessMetaData(processDefinitionsId, metaChanges);

  if (newBpmn) {
    await overwriteProcess(processDefinitionsId, newBpmn);

    eventHandler.dispatch('backend_processXmlChanged', {
      definitionsId: processDefinitionsId,
      newXml: newBpmn,
    });
  }

  return newMetaData;
}

/**
 * Direct updates to process meta data, should mostly be used for internal changes (puppeteer client, electron) to avoid
 * parsing the bpmn unnecessarily
 *
 * @param {Object} processDefinitionsId
 * @param {Object} metaChanges contains the elements to change and their new values
 */
export async function updateProcessMetaData(processDefinitionsId, metaChanges) {
  checkIfProcessExists(processDefinitionsId);

  const { id: newId } = metaChanges;

  if (newId && processDefinitionsId !== newId) {
    throw new Error(`Illegal try to change id from ${processDefinitionsId} to ${newId}`);
  }

  const newMetaData = {
    ...processMetaObjects[processDefinitionsId],
    lastEdited: new Date().toUTCString(),
  };

  mergeIntoObject(newMetaData, metaChanges, true, true, true);

  // add shared_with if process is shared
  if (metaChanges.shared_with) {
    newMetaData.shared_with = metaChanges.shared_with;
  }

  // remove shared_with if not shared anymore
  if (newMetaData.shared_with && metaChanges.shared_with && metaChanges.shared_with.length === 0) {
    delete newMetaData.shared_with;
  }

  processMetaObjects[processDefinitionsId] = newMetaData;

  store.update('processes', processDefinitionsId, removeExcessiveInformation(newMetaData));

  eventHandler.dispatch('processUpdated', {
    oldId: processDefinitionsId,
    updatedInfo: newMetaData,
  });

  return newMetaData;
}

/**
 * Removes an existing process
 *
 * @param {String} processDefinitionsId
 */
export async function removeProcess(processDefinitionsId) {
  if (!processMetaObjects[processDefinitionsId]) {
    return;
  }

  // remove process directory
  await deleteProcess(processDefinitionsId);
  // remove from store
  store.remove('processes', processDefinitionsId);
  delete processMetaObjects[processDefinitionsId];

  eventHandler.dispatch('processRemoved', { processDefinitionsId });
}

/**
 * Removes information from the meta data that would not be correct after a restart
 *
 * @param {Object} processInfo the complete process meta information
 */
function removeExcessiveInformation(processInfo) {
  const newInfo = { ...processInfo };
  delete newInfo.inEditingBy;
  return newInfo;
}

/**
 * Returns the process definition for the process with the given id
 *
 * @param {String} processDefinitionsId
 * @returns {String} - the process definition
 */
export async function getProcessBpmn(processDefinitionsId) {
  checkIfProcessExists(processDefinitionsId);

  try {
    const bpmn = await getBPMN(processDefinitionsId);
    return bpmn;
  } catch (err) {
    logger.debug(`Error reading bpmn of process. Reason:\n${err}`);
    throw new Error('Unable to find process bpmn!');
  }
}

/**
 * Returns the filenames of html data for all user tasks in the given process
 *
 * @param {String} processDefinitionsId
 * @returns {Array} - array containing the filenames of the htmls of all user tasks in the process
 */
export async function getProcessUserTasks(processDefinitionsId) {
  checkIfProcessExists(processDefinitionsId);

  try {
    const userTaskIds = await getUserTaskIds(processDefinitionsId);
    return userTaskIds;
  } catch (err) {
    logger.debug(`Error reading user task ids. Reason:\n${err}`);
    throw new Error('Unable to read user task filenames');
  }
}

/**
 * Returns the html for a specific user task in a process
 *
 * @param {String} processDefinitionsId
 * @param {String} taskFileName
 * @returns {String} - the html under the given fileName
 */
export async function getProcessUserTaskHtml(processDefinitionsId, taskFileName) {
  checkIfProcessExists(processDefinitionsId);

  try {
    const userTaskHtml = await getUserTaskHTML(processDefinitionsId, taskFileName);
    return userTaskHtml;
  } catch (err) {
    logger.debug(`Error getting html of user task. Reason:\n${err}`);
    throw new Error('Unable to get html for user task!');
  }
}

/**
 * Return object mapping from user tasks fileNames to their html
 *
 * @param {String} processDefinitionsId
 * @returns {Object} - contains the html for all user tasks in the process
 */
export async function getProcessUserTasksHtml(processDefinitionsId) {
  checkIfProcessExists(processDefinitionsId);

  try {
    const userTasksHtml = await getUserTasksHTML(processDefinitionsId);
    return userTasksHtml;
  } catch (err) {
    logger.debug(`Error getting user task html. Reason:\n${err}`);
    throw new Error('Failed getting html for all user tasks');
  }
}

export async function saveProcessUserTask(processDefinitionsId, userTaskFileName, html) {
  checkIfProcessExists(processDefinitionsId);

  try {
    await saveUserTaskHTML(processDefinitionsId, userTaskFileName, html);
    eventHandler.dispatch('backend_processTaskHtmlChanged', {
      processDefinitionsId,
      userTaskFileName,
      html,
    });
  } catch (err) {
    logger.debug(`Error storing user task data. Reason:\n${err}`);
    throw new Error('Failed to store the user task data');
  }
}

/**
 * Removes a stored user task from disk
 *
 * @param {String} processDefinitionsId
 * @param {String} userTaskFileName
 */
export async function deleteProcessUserTask(processDefinitionsId, userTaskFileName) {
  checkIfProcessExists(processDefinitionsId);

  try {
    await deleteUserTaskHTML(processDefinitionsId, userTaskFileName);
    eventHandler.dispatch('backend_processTaskHtmlChanged', {
      processDefinitionsId,
      userTaskFileName,
    });
  } catch (err) {
    logger.debug(`Error removing user task html. Reason:\n${err}`);
  }
}

/**
 * Stores the id of the socket wanting to block the process from being deleted inside the process object
 *
 * @param {String} socketId
 * @param {String} processDefinitionsId
 */
export function blockProcess(socketId, processDefinitionsId) {
  checkIfProcessExists(processDefinitionsId);

  const process = { ...processMetaObjects[processDefinitionsId] };

  const blocker = { id: socketId, task: null };
  let { inEditingBy } = process;
  if (!inEditingBy) {
    inEditingBy = [blocker];
  } else {
    const existingBlocker = inEditingBy.find((b) => b.id == blocker.id);
    if (!existingBlocker) {
      inEditingBy.push(blocker);
    }
  }
  updateProcessMetaData(processDefinitionsId, { inEditingBy });
}

/**
 * Removes the id of the socket wanting to unblock the process from the process object
 *
 * @param {String} socketId
 * @param {String} processDefinitionsId
 */
export function unblockProcess(socketId, processDefinitionsId) {
  checkIfProcessExists(processDefinitionsId);

  const process = processMetaObjects[processDefinitionsId];

  if (!process.inEditingBy) {
    return;
  }

  const inEditingBy = process.inEditingBy.filter((blocker) => blocker.id !== socketId);

  updateProcessMetaData(processDefinitionsId, { inEditingBy });
}

export function blockTask(socketId, processDefinitionsId, taskId) {
  checkIfProcessExists(processDefinitionsId);

  const process = processMetaObjects[processDefinitionsId];

  if (!process.inEditingBy) {
    return;
  }

  let blocker = process.inEditingBy.find((b) => b.id === socketId);

  let { inEditingBy } = process;

  if (!blocker) {
    blocker = { id: socketId, task: taskId };
    inEditingBy.push(blocker);
  } else {
    blocker.task = taskId;
  }

  updateProcessMetaData(processDefinitionsId, { inEditingBy });
}

export function unblockTask(socketId, processDefinitionsId, taskId) {
  checkIfProcessExists(processDefinitionsId);

  const process = processMetaObjects[processDefinitionsId];

  if (!process.inEditingBy) {
    return;
  }

  let blocker = process.inEditingBy.find((b) => b.id === socketId);

  if (blocker && blocker.task === taskId) {
    blocker.task = null;

    updateProcessMetaData(processDefinitionsId, { inEditingBy: process.inEditingBy });
  }
}

/**
 * initializes the process meta information objects
 */
export async function init() {
  processMetaObjects = {};

  // get processes that were persistently stored
  const storedProcesses = store.get('processes');
  const updatedProcesses = await getUpdatedProcessesJSON(storedProcesses);
  store.set('processes', 'processes', updatedProcesses);
  const processes = updatedProcesses.map((uP) => ({ ...uP, inEditingBy: [] }));
  processes.forEach((process) => (processMetaObjects[process.id] = process));
}

init();