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

import Conf from 'conf';
import path from 'path';
import { getAppDataPath } from './fileHandling.js';
import eventHandler from '../../../frontend/backend-api/event-system/EventHandler.js';

/**
 * Creates a new conf store that is used to store the current state of something
 *
 * @param {String} storeName the name of the store referencing the thing we want to store
 * @param {undefined|Object|Array} defaultVal the default layout of the store that is used when creating it
 * @param {Boolean} noStorage used to signal if the store is supposed to be stored in the Storage subdirectory
 */
function getStore(storeName, defaultVal, subDir = 'Storage') {
  let appDir = getAppDataPath();
  const storageDir = path.join(appDir, subDir);

  // different structure for role mappings
  if (storeName === 'roleMappings')
    return new Conf({
      configName: storeName,
      cwd: storageDir,
      defaults: { [storeName]: { users: {} } },
    });

  return new Conf({
    configName: storeName,
    cwd: storageDir,
    defaults: defaultVal ? defaultVal : { [storeName]: [] },
  });
}

// contains all stores we need
const stores = {};

// usually all storages are inside the 'storageDir'
stores.capabilities = { store: getStore('capabilities') };
stores.processes = { store: getStore('processes') };
stores.instances = { store: getStore('instances') };
stores.machines = { store: getStore('machines') };
resetMachines();
stores.environmentProfiles = { store: getStore('environmentProfiles') };
stores.environmentConfig = { store: getStore('environmentConfig', { environmentConfig: {} }) };
stores.config = { store: getStore('config', {}, 'Config') }; // true => store directly in app/root dir
stores.userPreferences = { store: getStore('userPreferences', {}) };
stores.resources = { store: getStore('resources') };
stores.shares = { store: getStore('shares') };
stores.roles = { store: getStore('roles') };
stores.roleMappings = { store: getStore('roleMappings') };

/**
 * Gets the value of the store with the given name
 *
 * @param {String} store the name of the store we want to get
 */
function get(storeName) {
  // The config store has a different layout from the others
  /**
   * ConfigStore: { key1: val1, ... }
   * Others: { storeName: [storeVals] }
   *
   */
  if (storeName === 'config' || storeName === 'roleMappings') {
    return stores[storeName].store.get();
  } else {
    return stores[storeName].store.get(storeName);
  }
}

function set(storeName, key, data, userId) {
  const oldData = storeName === 'config' ? get(storeName)[key] : get(storeName);
  if (data === undefined) {
    stores[storeName].store.delete(key);
  } else {
    if (userId) {
      stores[storeName].store.set(`${key}.${userId}`, data);
    } else {
      stores[storeName].store.set(key, data);
    }
  }
  eventHandler.dispatch(`store_${storeName}Changed`, { oldData, data, key });
}

/**
 * Allows the manipulation of specific objects in the given store
 * BEWARE: Don't use this on the config store
 *
 * @param {string} storeName
 * @param {object} idUpdateMap object with key value pairs
 * @param {string} key: the id of the object we want to change
 * @param {object} value: the new value for the object we want to change
 */
function updateByIds(storeName, idUpdateMap) {
  const state = get(storeName);
  const oldState = [...state];

  const updates = {};

  Object.entries(idUpdateMap).forEach(([id, update]) => {
    const index = state.findIndex((object) => object.id === id);
    if (index < 0) {
      // do nothing if the object doesn't exist
      return;
    }
    updates[id] = { oldState: state[index], newState: update };

    // overwrite the old object with the updated one
    state[index] = update;
  });

  if (Object.keys(updates).length) {
    stores[storeName].store.set(storeName, state);
    eventHandler.dispatch(`store_${storeName}Updated`, { oldData: oldState, data: state, updates });
  }
}

/**
 * Returns singular form of a stores name (machines => machine)
 *
 * @param {String} storeName plural
 * @returns {Strings} - singular
 */
function getSingular(storeName) {
  switch (storeName) {
    case 'processes':
      return 'process';
    case 'capabilities':
      return 'capability';
    default:
      return storeName.slice(0, storeName.length - 1);
  }
}

/**
 * Function to add an element to the stores which store elements with ids (machines, processes, etc.) (NOT! config)
 *
 * Does check if element with the same id is already in store
 *
 * @param {String} storeName the name of the store we want to add to
 * @param {Object} newElement the object we want to add
 * @returns {Boolean} - if adding was possible (don't add if element with same id exists)
 */
function add(storeName, newElement) {
  let state = get(storeName);

  const sameId = state.find((el) => el.id === newElement.id);

  if (sameId) {
    return false;
  } else {
    state = [...state, newElement];

    stores[storeName].store.set(storeName, state);

    // get the singular form of the store (machines => machine)
    const elementName = getSingular(storeName);

    eventHandler.dispatch(`store_${elementName}_added`, { [elementName]: newElement });
    return true;
  }
}

/**
 * Function to set an element in dictionary format
 *
 * @param {String} storeName the name of the store we want to add to
 * @param {Object} newItem the object we want to add
 */
function setDictElement(storeName, newItem) {
  stores[storeName].store.set(newItem);
}

/**
 * Function to remove an element from a store (store has to have property like with add)
 *
 * @param {String} storeName name of the store we want to remove from
 * @param {String} elementId id of the element we want to remove
 */
function remove(storeName, elementId) {
  let state = get(storeName);

  const elementIndex = state.findIndex((el) => el.id === elementId);

  if (elementIndex > -1) {
    state.splice(elementIndex, 1);

    stores[storeName].store.set(storeName, state);

    // get the singular form of the store (machines => machine)
    const elementName = getSingular(storeName);

    eventHandler.dispatch(`store_${elementName}_removed`, { [`${elementName}Id`]: elementId });
  }
}

/**
 * Function to remove an element from a store in dictionary format
 *
 * @param {String} storeName name of the store we want to remove from
 * @param {String} elementId id of the element we want to remove
 * @param {String} itemId only if nested item in element should be removed
 */
function removeDictElement(storeName, elementId, itemId = null) {
  let state = get(storeName);

  if (state[elementId]) {
    if (itemId) {
      delete state[elementId][itemId];
    } else {
      delete state[elementId];
    }

    stores[storeName].store.set(storeName, state);
  }
}

/**
 * Function to update a single value inside one of the stores
 *
 * @param {String} storeName the name of the store we want to update something in
 * @param {String} elementId the id of the element we want to update
 * @param {Object} updatedInfo the info we want to overwrite the current one with
 */
function update(storeName, elementId, updatedInfo) {
  const state = get(storeName);

  const index = state.findIndex((el) => el.id === elementId);

  if (index < 0) {
    // do nothing if the object doesn't exist
    return;
  }
  // overwrite the old object with the updated one
  state[index] = updatedInfo;

  stores[storeName].store.set(storeName, state);

  // get the singular form of the store (machines => machine)
  const elementName = getSingular(storeName);

  eventHandler.dispatch(`store_${elementName}_updated`, { oldId: elementId, updatedInfo });
}

function getById(storeName, id) {
  return get(storeName).find((el) => el.id === id);
}

function resetMachines() {
  const machines = get('machines');

  const resetMachines = machines
    .map((d) => ({ ...d, status: 'DISCONNECTED' }))
    .map((d) => ({ ...d, ip: d.ip || (d.location && d.location.replace('http://', '')) }));

  set('machines', 'machines', resetMachines);
}

export function getStorePath(storeName) {
  return stores[storeName].store._options.cwd;
}

export default {
  get,
  set,
  add,
  setDictElement,
  remove,
  removeDictElement,
  update,
  getById,
  updateByIds,
  getStorePath,
};