Source: lib/partialupdate/PartialUpdateBuilder.js

'use strict';

const ALLOWED_OPERATIONS = [
  '$add',
  '$and',
  '$currentDate',
  '$dec',
  '$inc',
  '$max',
  '$min',
  '$mul',
  '$or',
  '$pop',
  '$push',
  '$put',
  '$remove',
  '$rename',
  '$replace',
  '$set',
  '$shift',
  '$unshift',
  '$xor',
];

const UpdateOperation = require('./UpdateOperation');
const deprecated = require('../util/deprecated');

/**
 * @alias partialupdate.PartialUpdateBuilder<T>
 */
class PartialUpdateBuilder {
  /**
   * @param {json} operations
   */
  constructor(operations) {
    /** @type {UpdateOperation[]} */
    this.operations = [];
    if (operations) {
      this.addOperations(operations);
    }
  }

  /**
   * Sets a field to a given value
   *
   * @param {string} field The field to set
   * @param {*} value The value to set to
   * @return {this}
   */
  set(field, value) {
    let val = value;
    if (val instanceof Set) {
      val = Array.from(val);
    } else if (val instanceof Map) {
      const newValue = {};
      val.forEach((v, k) => {
        newValue[k] = v;
      });
      val = newValue;
    }

    return this.addOperation(field, '$set', val);
  }

  /**
   * Increments a field by a given value
   *
   * @param {string} field The field to increment
   * @param {number=} by The number to increment by, defaults to 1
   * @return {this}
   */
  inc(field, by) {
    return this.addOperation(field, '$inc', typeof by === 'number' ? by : 1);
  }

  /**
   * Decrements a field by a given value
   *
   * @param {string} field The field to decrement
   * @param {number=} by The number to decrement by, defaults to 1
   * @return {this}
   */
  dec(field, by) {
    return this.increment(field, typeof by === 'number' ? -by : -1);
  }

  /**
   * Multiplies a field by a given number
   *
   * @param {string} field The field to multiply
   * @param {number} multiplicator The number to multiply by
   * @return {this}
   */
  mul(field, multiplicator) {
    if (typeof multiplicator !== 'number') {
      throw new Error('Multiplicator must be a number.');
    }

    return this.addOperation(field, '$mul', multiplicator);
  }

  /**
   * Divides a field by a given number
   *
   * @param {string} field The field to divide
   * @param {number} divisor The number to divide by
   * @return {this}
   */
  div(field, divisor) {
    if (typeof divisor !== 'number') {
      throw new Error('Divisor must be a number.');
    }

    return this.addOperation(field, '$mul', 1 / divisor);
  }

  /**
   * Sets the highest possible value of a field
   *
   * @param {string} field The field to compare with
   * @param {number} value The highest possible value
   * @return {this}
   */
  min(field, value) {
    if (typeof value !== 'number') {
      throw new Error('Value must be a number');
    }

    return this.addOperation(field, '$min', value);
  }

  /**
   * Sets the smallest possible value of a field
   *
   * @param {string} field The field to compare with
   * @param {number} value The smalles possible value
   * @return {this}
   */
  max(field, value) {
    if (typeof value !== 'number') {
      throw new Error('Value must be a number');
    }

    return this.addOperation(field, '$max', value);
  }

  /**
   * Removes an item from an array or map
   *
   * @param {string} field The field to perform the operation on
   * @param {*} item The item to add
   * @return {this}
   */
  remove(field, item) {
    return this.addOperation(field, '$remove', item);
  }

  /**
   * Puts an item from an array or map
   *
   * @param {string} field The field to perform the operation on
   * @param {string|object} key The map key to put the value to or an object of arguments
   * @param {*} [value] The value to put if a key was used
   * @return {this}
   */
  put(field, key, value) {
    const obj = {};
    if (typeof key === 'string' || typeof key === 'number') {
      obj[key] = value;
    } else if (typeof key === 'object') {
      Object.assign(obj, key);
    }

    return this.addOperation(field, '$put', obj);
  }

  /**
   * Pushes an item into a list
   *
   * @param {string} field The field to perform the operation on
   * @param {*} item The item to add
   * @return {this}
   */
  push(field, item) {
    return this.addOperation(field, '$push', item);
  }

  /**
   * Unshifts an item into a list
   *
   * @param {string} field The field to perform the operation on
   * @param {*} item The item to add
   * @return {this}
   */
  unshift(field, item) {
    return this.addOperation(field, '$unshift', item);
  }

  /**
   * Pops the last item out of a list
   *
   * @param {string} field The field to perform the operation on
   * @return {this}
   */
  pop(field) {
    return this.addOperation(field, '$pop');
  }

  /**
   * Shifts the first item out of a list
   *
   * @param {string} field The field to perform the operation on
   * @return {this}
   */
  shift(field) {
    return this.addOperation(field, '$shift');
  }

  /**
   * Adds an item to a set
   *
   * @param {string} field The field to perform the operation on
   * @param {*} item The item to add
   * @return {this}
   */
  add(field, item) {
    return this.addOperation(field, '$add', item);
  }

  /**
   * Replaces an item at a given index
   *
   * @param {string} path The path to perform the operation on
   * @param {number} index The index where the item will be replaced
   * @param {*} item The item to replace with
   * @return {this}
   */
  replace(path, index, item) {
    if (this.hasOperationOnPath(path)) {
      throw new Error(`You cannot update ${path} multiple times`);
    }

    return this.addOperation(`${path}.${index}`, '$replace', item);
  }

  /**
   * Sets a datetime field to the current moment
   *
   * @param {string} field The field to perform the operation on
   * @return {this}
   */
  currentDate(field) {
    return this.addOperation(field, '$currentDate');
  }

  /**
   * Performs a bitwise AND on a path
   *
   * @param {string} path The path to perform the operation on
   * @param {number} bitmask The bitmask taking part in the operation
   * @return {this}
   */
  and(path, bitmask) {
    return this.addOperation(path, '$and', bitmask);
  }

  /**
   * Performs a bitwise OR on a path
   *
   * @param {string} path The path to perform the operation on
   * @param {number} bitmask The bitmask taking part in the operation
   * @return {this}
   */
  or(path, bitmask) {
    return this.addOperation(path, '$or', bitmask);
  }

  /**
   * Performs a bitwise XOR on a path
   *
   * @param {string} path The path to perform the operation on
   * @param {number} bitmask The bitmask taking part in the operation
   * @return {this}
   */
  xor(path, bitmask) {
    return this.addOperation(path, '$xor', bitmask);
  }

  /**
   * Renames a field
   *
   * @param {string} oldPath The old field name
   * @param {string} newPath The new field name
   * @return {this}
   */
  rename(oldPath, newPath) {
    return this.addOperation(oldPath, '$rename', newPath);
  }

  /**
   * Returns a JSON representation of this partial update
   *
   * @return {json}
   */
  toJSON() {
    return this.operations.reduce((json, operation) => {
      const obj = {};
      obj[operation.path] = operation.value;

      json[operation.name] = Object.assign({}, json[operation.name], obj);

      return json;
    }, {});
  }

  /**
   * Executes the partial update
   *
   * @return {Promise<T>} The promise resolves when the partial update has been executed successfully
   * @abstract
   */
  execute() {
    throw new Error('Cannot call "execute" on abstract PartialUpdateBuilder');
  }

  /**
   * Adds an update operation on the partial update
   *
   * @param {string} path The path which gets modified by the operation
   * @param {string} operator The operator of the operation to add
   * @param {*} [value] The value used to execute the operation
   * @return {this}
   * @private
   */
  addOperation(path, operator, value) {
    if (typeof path !== 'string') {
      throw new Error('Path must be a string');
    }

    if (ALLOWED_OPERATIONS.indexOf(operator) === -1) {
      throw new Error('Operation invalid: ' + operator);
    }

    if (this.hasOperationOnPath(path)) {
      throw new Error(`You cannot update ${path} multiple times`);
    }

    // Check for illegal values
    if (typeof value === 'number') {
      if (Number.isNaN(value)) {
        throw new Error('NaN is not a supported value');
      }
      if (!Number.isFinite(value)) {
        throw new Error('Infinity is not a supported value');
      }
    }

    // Add the new operation
    const normalizedValue = typeof value === 'undefined' ? null : value;
    const updateOperation = new UpdateOperation(operator, path, normalizedValue);
    this.operations.push(updateOperation);

    return this;
  }

  /**
   * Adds initial operations
   *
   * @param {json} json
   * @private
   */
  addOperations(json) {
    Object.keys(json).forEach((key) => {
      const pathValueDictionary = json[key];
      Object.keys(pathValueDictionary).forEach((path) => {
        const value = pathValueDictionary[path];
        this.operations.push(new UpdateOperation(key, path, value));
      });
    });
  }

  /**
   * Checks whether an operation on the field exists already
   *
   * @param {string} path The path where the operation is executed on
   * @return {boolean} True, if the operation does exist
   * @private
   */
  hasOperationOnPath(path) {
    return this.operations.some(op => op.path === path);
  }
}

// aliases
Object.assign(PartialUpdateBuilder.prototype, /** @lends partialupdate.PartialUpdateBuilder<T>.prototype */ {
  /**
   * Increments a field by a given value
   *
   * @method
   * @param {string} field The field to increment
   * @param {number=} by The number to increment by, defaults to 1
   * @return {this}
   */
  increment: PartialUpdateBuilder.prototype.inc,

  /**
   * Decrements a field by a given value
   *
   * @method
   * @param {string} field The field to decrement
   * @param {number=} by The number to decrement by, defaults to 1
   * @return {this}
   */
  decrement: PartialUpdateBuilder.prototype.dec,

  /**
   * Multiplies a field by a given number
   *
   * @method
   * @param {string} field The field to multiply
   * @param {number} multiplicator The number to multiply by
   * @return {this}
   */
  multiply: PartialUpdateBuilder.prototype.mul,

  /**
   * Divides a field by a given number
   *
   * @method
   * @param {string} field The field to divide
   * @param {number} divisor The number to divide by
   * @return {this}
   */
  divide: PartialUpdateBuilder.prototype.div,

  /**
   * Sets the highest possible value of a field
   *
   * @method
   * @param {string} field The field to compare with
   * @param {number} value The highest possible value
   * @return {this}
   */
  atMost: PartialUpdateBuilder.prototype.min,

  /**
   * Sets the smallest possible value of a field
   *
   * @method
   * @param {string} field The field to compare with
   * @param {number} value The smalles possible value
   * @return {this}
   */
  atLeast: PartialUpdateBuilder.prototype.max,

  /**
   * Sets a datetime field to the current moment
   *
   * @method
   * @param {string} field The field to perform the operation on
   * @return {this}
   */
  toNow: PartialUpdateBuilder.prototype.currentDate,
});

deprecated(PartialUpdateBuilder.prototype, '_operations', 'operations');
deprecated(PartialUpdateBuilder.prototype, '_addOperation', 'addOperation');
deprecated(PartialUpdateBuilder.prototype, '_hasOperationOnPath', 'hasOperationOnPath');

module.exports = PartialUpdateBuilder;