Source: environment/Environment.js

import VersionList from '../version/VersionList';
import {ucwords} from "../utils";

/**
 * An Environment instance holds all metadata about the environment,
 * such as whether it is experimental, available to the public, or if the environment
 * includes both electron and recorder modules. You can use the Environment instance
 * to add/remove modules, get the version lists for a module, and more.
 */
class Environment {
  constructor(props = {}) {
    if (!props.value) {
      throw new Error('Environment value required, none provided');
    }

    // set defaults
    this.value = props.value;
    this.label = props.label || ucwords(props.value.split('-').join(' '));
    this.description = props.description;
    this.modules = props.modules || [];
    this.target = props.target;
    this.origin = props.origin;
    this.experimental = props.experiment === undefined ? true : props.experimental;
    this.stagingApi = props.stagingApi === undefined ? false : props.stagingApi;
    this.public = props.public === undefined ? false : props.public;
    this.static = props.static === undefined ? false : props.static;
    this.parent = props.parent;
    this.userIds = props.userIds

    // override using inherited props
    for (let key of Object.keys(props)) {
      this[key] = props[key];
    }
  }

  /**
   * Return the environment value.
   * @returns {string}
   */
  getValue () {
    return this.value;
  }

  /**
   * Return the human readable environment label.
   * @returns {string}
   */
  getLabel () {
    return this.label;
  }

  /**
   * Get the {@link Environment} value property for this environment's parent.
   * @returns {*}
   */
  getParent () {
    return this.parent;
  }

  /**
   * Returns the description field, empty by default but modifiable via the build panel / build server API.
   * @returns {*}
   */
  getDescription () {
    return this.description;
  }

  /**
   * Returns the user IDs array.
   *
   * @returns {Array} An array of user IDs.
   */
  getUserIds () {
    return this.userIds
  }

  /**
   * Adds a user ID to the array of user IDs.
   *
   * @param {string} userId - The user ID to add.
   * @returns {Environment} The current instance.
   */
  addUserId (userId) {
    if (this.userIds === undefined) {
      this.userIds = []
    } else if (this.userIds.some(id => id == userId)) {
      return this
    }
    this.userIds.push(userId)
    return this
  }

  /**
   * Removes a user ID from the array of user IDs.
   *
   * @param {string} userId - The user ID to remove.
   * @returns {Environment} The current instance.
   */
  removeUserId (userId) {
    this.userIds = this.userIds.filter(id => id !== userId)
    return this
  }

  /**
   * Sets the array of user IDs.
   *
   * @param {Array} userIds - An array of user IDs.
   * @returns {Environment} The current instance.
   */
  setUserIds (userIds) {
    this.userIds = userIds
    return this
  }

  /**
   * Checks if a given user ID is allowed.
   * Use loose comparison for string vs int id.
   *
   * @param {string} userId - The user ID to check.
   * @returns {boolean} `true` if the user ID is allowed, `false` otherwise.
   */
  isUserIdAllowed (userId) {
    if (this.userIds === undefined) {
      return true
    }
    return this.userIds.some(id => id == userId)
  }


  /**
   * Return the supported modules.
   * @returns {string[]}
   */
  getModules () {
    return this.modules;
  }

  /**
   * Return true if the environment has a {@link VersionList} for the specified module.
   *
   * This is used to determine if the app should download both electron and recorder updates for
   * each environment. Some environments don't need a recorder branch, in which case it can default
   * to sandbox in most cases.
   *
   * @param module
   * @returns {boolean}
   */
  includesModule (module) {
    return this.modules.includes(module.toLowerCase());
  }

  /**
   * Add a module and perform a type safety check to prevent corrupt additions.
   * @param module
   * @returns {Environment}
   */
  addModule (module) {
    if (typeof module !== 'string') {
      throw new Error('Cannot add module value that is not a string to environments')
    }
    if (this.includesModule(module)) {
      console.warn(`Could not add duplicate module to ${this.value}: ${module}`);
      return this;
    }
    this.modules.push(module.toLowerCase());
    return this;
  }

  /**
   * Remove a module from the {@link Environment}.
   * @param module
   * @returns {Environment}
   */
  removeModule (module) {
    if (typeof module !== 'string') {
      throw new Error('Cannot remove module value that is not a string from environments')
    }
    if (!this.includesModule(module)) {
      console.warn(`Could not remove module from ${this.value}, module is already not included: ${module}`);
      return this;
    }
    this.modules = this.modules.filter(mod => mod !== module);
    return this;
  }

  /**
   * Return true if the {@link Environment} has a target override to control the storage path.
   * @returns {boolean}
   */
  hasTarget () {
    return this.target !== undefined && this.target !== null && typeof target === 'string';
  }

  /**
   * Return the target storage path (if any).
   * @returns {string}
   */
  getTarget () {
    return this.target || this.value;
  }

  /**
   * Return the origin branch hash (if any).
   * @returns {string}
   */
  getOrigin () {
    return this.origin;
  }

  /**
   * Return true if the {@link Environment} is experimental.
   * This is true for all feature-based dynamic environments & static sandboxes.
   * @returns {boolean}
   */
  isExperimental () {
    return this.experimental === true;
  }

  usesStagingApi () {
    return this.stagingApi === true;
  }

  /**
   * Return true if the {@link Environment} is publicly accessible.
   *
   * Environments like sandbox, and any non-static branches should
   * essentially _never_ be public.
   * @returns {boolean}
   */
  isPublic () {
    return this.public === true;
  }

  /**
   * Return true if the {@link Environment} is static.
   * A static environment is immutable -- it will never be deleted by
   * webhooks and they are not deployed based on feature branches.
   *
   * A non-static environment should be seen as ephemeral -- it will be
   * deleted when the correlating PR is closed or merged.
   *
   * @returns {boolean}
   */
  isStatic () {
    return this.static === true;
  }

  /**
   * Set an {@link Environment}'s experimental state to on/off (true/false).
   * If an environment is experimental, it will behave like a sandbox environment.
   * @param experimental
   * @returns {Environment}
   */
  setExperimental (experimental) {
    this.experiment = experimental;
    return this;
  }

  setStagingApi (stagingApi) {
    this.stagingApi = stagingApi;
    return this;
  }

  /**
   * Set an {@link Environment}'s public state.
   * Cannot set a public environment to non-public.
   *
   * Non-public builds should never be discoverable or distributed to
   * end-users for stability and confidentiality purposes.
   * @param isPublic
   * @returns {Environment}
   */
  setPublic (isPublic) {
    if (this.public && !isPublic) {
      throw new Error('Cannot set a public environment as non-public')
    }
    this.public = isPublic;
    return this;
  }

  /**
   * Set an {@link Environment}'s static state.
   *
   * Static environments will never be removed or unpublished by CI/CD. They will
   * only be continuously updated.
   *
   * Non-static environments will be treated as dynamic/ephemeral, and clients
   * should expect such behavior.
   * @param isStatic
   * @returns {Environment}
   */
  setStatic (isStatic) {
    if (this.static && !isStatic) {
      throw new Error('Cannot set a static environment as non-static');
    }
    this.static = isStatic;
    return this;
  }

  /**
   * Set the description for the environment.
   * @param description
   * @returns {Environment}
   */
  setDescription (description) {
    this.description = description;
    return this;
  }

  /**
   * Return the {@link VersionList} from this environment for the specified app module.
   * @param module
   * @param url
   * @param fetchOpts
   * @returns {Promise<VersionList>}
   */
  async getVersionList (module, {url, fetchOpts = {}} = {}) {
    const versionList = new VersionList({
      host: `cdn.medal.tv/${this.getTarget()}/${module}`.split('//').join('/'),
      environment: this.getValue(),
      module,
    })
    return versionList.load(url, fetchOpts);
  }

  /**
   * Convert to a parsed JSON object.
   * @returns {Object}
   */
  toJson () {
    return JSON.parse(JSON.stringify(this))
  }
}

export default Environment;