model/ModelCollection.js

import { isValidUid, isArray, checkType } from '../lib/check'
import { throwError } from '../lib/utils'
import Model from './Model'
import ModelDefinition from './ModelDefinition'
import Pager from '../pager/Pager'

function throwIfContainsOtherThanModelObjects(values) {
    if (values && values[Symbol.iterator]) {
        const toCheck = [...values]
        toCheck.forEach(value => {
            if (!(value instanceof Model)) {
                throwError(
                    'Values of a ModelCollection must be instances of Model'
                )
            }
        })
    }
}

function throwIfContainsModelWithoutUid(values) {
    if (values && values[Symbol.iterator]) {
        const toCheck = [...values]
        toCheck.forEach(value => {
            if (!isValidUid(value.id)) {
                throwError(
                    'Can not add a Model without id to a ModelCollection'
                )
            }
        })
    }
}

/**
 * Collection of `Model` objects that can be interacted upon. Can contain a pager object to easily navigate
 * pages within the system.
 *
 * @memberof module:model
 */
class ModelCollection {
    /**
     * @constructor
     *
     * @param {ModelDefinition} modelDefinition The `ModelDefinition` that this collection is for. This defines the type of models that
     * are allowed to be added to the collection.
     * @param {Model[]} values Initial values that should be added to the collection.
     * @param {Object} pagerData Object with pager data. This object contains data that will be put into the `Pager` instance.
     *
     * @description
     *
     * Creates a new `ModelCollection` object based on the passed `modelDefinition`. Additionally values can be added by passing
     * `Model` objects in the `values` parameter. The collection also exposes a pager object which can be used to navigate through
     * the pages in the collection. For more information see the `Pager` class.
     */
    constructor(modelDefinition, values, pagerData) {
        checkType(modelDefinition, ModelDefinition)
        /**
         * @property {ModelDefinition} modelDefinition The `ModelDefinition` that this collection is for. This defines the type of models that
         * are allowed to be added to the collection.
         */
        this.modelDefinition = modelDefinition

        /**
         * @property {Pager} pager Pager object that is created from the pagerData that was passed when the collection was constructed. If no pager data was present
         * the pager will have default values.
         */
        this.pager = new Pager(pagerData, modelDefinition)

        // We can not extend the Map object right away in v8 contexts.
        this.valuesContainerMap = new Map()
        this[Symbol.iterator] = this.valuesContainerMap[Symbol.iterator].bind(
            this.valuesContainerMap
        )

        throwIfContainsOtherThanModelObjects(values)
        throwIfContainsModelWithoutUid(values)

        // Add the values separately as not all Iterators return the same values
        if (isArray(values)) {
            values.forEach(value =>
                this.valuesContainerMap.set(value.id, value)
            )
        }
    }

    /**
     * @property {Number} size The number of Model objects that are in the collection.
     *
     * @description
     * Contains the number of Model objects that are in this collection. If the collection is a collection with a pager. This
     * does not take into account all the items in the database. Therefore when a pager is present on the collection
     * the size will return the items on that page. To get the total number of items consult the pager.
     */
    get size() {
        return this.valuesContainerMap.size
    }

    /**
     * Adds a Model instance to the collection. The model is checked if it is a correct instance of `Model` and if it has
     * a valid id. A valid id is a uid string of 11 alphanumeric characters.
     *
     * @param {Model} value Model instance to add to the collection.
     * @returns {ModelCollection} Returns itself for chaining purposes.
     *
     * @throws {Error} When the passed value is not an instance of `Model`
     * @throws {Error} Throws error when the passed value does not have a valid id.
     */
    add(value) {
        throwIfContainsOtherThanModelObjects([value])
        throwIfContainsModelWithoutUid([value])

        this.set(value.id, value)
        return this
    }

    /**
     * If working with the Map type object is inconvenient this method can be used to return the values
     * of the collection as an Array object.
     *
     * @returns {Array} Returns the values of the collection as an array.
     */
    toArray() {
        const resultArray = []

        this.forEach(model => {
            resultArray.push(model)
        })

        return resultArray
    }

    static create(modelDefinition, values, pagerData) {
        return new ModelCollection(modelDefinition, values, pagerData)
    }

    static throwIfContainsOtherThanModelObjects(value) {
        return throwIfContainsOtherThanModelObjects(value)
    }

    static throwIfContainsModelWithoutUid(value) {
        return throwIfContainsModelWithoutUid(value)
    }

    /**
     * Clear the collection and remove all it's values.
     *
     * @returns {this} Returns itself for chaining purposes;
     */
    // TODO: Reset the pager?
    clear() {
        return this.valuesContainerMap.clear.call(this.valuesContainerMap)
    }

    delete(...args) {
        return this.valuesContainerMap.delete.call(
            this.valuesContainerMap,
            ...args
        )
    }

    entries() {
        return this.valuesContainerMap.entries.call(this.valuesContainerMap)
    }

    // FIXME: This calls the forEach function with the values Map and not with the ModelCollection as the third argument
    forEach(...args) {
        return this.valuesContainerMap.forEach.call(
            this.valuesContainerMap,
            ...args
        )
    }

    get(...args) {
        return this.valuesContainerMap.get.call(
            this.valuesContainerMap,
            ...args
        )
    }

    has(...args) {
        return this.valuesContainerMap.has.call(
            this.valuesContainerMap,
            ...args
        )
    }

    keys() {
        return this.valuesContainerMap.keys.call(this.valuesContainerMap)
    }

    set(...args) {
        return this.valuesContainerMap.set.call(
            this.valuesContainerMap,
            ...args
        )
    }

    values() {
        return this.valuesContainerMap.values.call(this.valuesContainerMap)
    }
}

export default ModelCollection