Source: modelarray.js

const _ = require('lodash');

import _privateKey from './privatekey'

import { typecast } from './utils';
import { normalizeProperties } from './normalize'

export class ModelArray extends Array {
  /**
   * @classdesc Represents a basic array with typecasted values. Inherits <code>Array</code>
   * Clients to not need to manually create instances of this class.
   *
   * @description Clients to not need to manually create instances of this class.
   * @class
   * @param self
   * @param properties
   */
  constructor(self, properties) {
    super();

    // Store all internals.
    const _private = this[_privateKey] = {};

    // Store reference to self.
    _private._self = self;

    // Store properties (arrayType, unique, etc).
    _private._properties = properties;

    // Normalize our own properties.
    if (properties.arrayType) {
      properties.arrayType = normalizeProperties.call(self, properties.arrayType);
    }
  }

  push(...args) {
    // Values are passed through the typecast before being allowed onto the array if arrayType is set.
    // In the case of rejection, the typecast returns undefined, which is not appended to the array.
    let values;
    if (this[_privateKey]._properties.arrayType) {
      values = [].map.call(args, (value) => {
        try {
          return typecast.call(this[_privateKey]._self, value, undefined, this[_privateKey]._properties.arrayType);
        }
        catch (error) {
          // set it the root parent object
          if (this[_privateKey]._self && this[_privateKey]._self[_privateKey] && this[_privateKey]._self[_privateKey]._errors) {
            this[_privateKey]._self[_privateKey]._errors.push(error);
          }
          return undefined;
        }
      }, this);

      // remove undefined and nulls
      values = _.compact(values);
    } else {
      values = args;
    }

    if (this[_privateKey]._properties.unique) {
      values = _.difference(values, _.toArray(this));
    }

    return Array.prototype.push.apply(this, values);
  }

  concat(...args) {
    // Return new instance of SchemaArray.
    const schemaArray = new ModelArray(this[_privateKey]._self, this[_privateKey]._properties);

    // Create primitive array with all elements.
    let array = this.toArray();

    for (const i in args) {
      if (args[i].toArray) {
        args[i] = args[i].toArray();
      }
      array = array.concat(args[i]);
    }

    // Push each value in individually to typecast.
    for (const i in array) {
      schemaArray.push(array[i]);
    }

    return schemaArray;
  }

  toArray() {
    // Create new Array to hold elements.
    const array = [];

    // Loop through each element, clone if necessary.
    _.each(this, (element) => {
      // Call toObject() method if defined (this allows us to return primitive objects instead of Model instances).
      if (_.isObject(element) && _.isFunction(element.toObject)) {
        element = element.toObject();

        // If is non-SchemaType object, shallow clone so that properties modification don't have an affect on the original object.
      } else if (_.isObject(element)) {
        element = _.clone(element);
      }

      array.push(element);
    });

    return array;
  }

  set(array) {
    let ok = true;
    _.each(array, (arrayValue) => {
      try {
        typecast.call(this[_privateKey]._self, arrayValue, undefined, this[_privateKey]._properties.arrayType);
      }
      catch (error) {
        ok = false;
        // set it the root parent object
        if (this[_privateKey]._self && this[_privateKey]._self[_privateKey] && this[_privateKey]._self[_privateKey]._errors) {
          this[_privateKey]._self[_privateKey]._errors.push(error);
        }
      }
    });

    if (ok) {
      this.length = 0;
      this.push.apply(this, array);
    }
  }

  toJSON() {
    return this.toArray();
  }

  /**
   *  Used to detect instance of SchemaArray internally.
   * @returns {boolean}
   * @private
   */
  _isModelArray() {
    return true;
  }
}