orm/src/filter/Filter.js

/**
 * @module flitter-orm/src/filter/Filter
 */

const Focus = require('./Focus')
const FilterProxy = require('../proxy/model/FilterProxy')
const { ObjectId } = require('mongodb')

/**
 * An accumulating MongoDB filter object.
 * @class
 */
class Filter {
    /**
     * The root query object.
     * @type {object}
     * @private
     */
    _root = {$and: []}

    constructor(model) {
        /**
         * The model in question.
         * @type: {module:flitter-orm/src/model/Model}
         */
        this._model = model
    }

    /**
     * Clone this filter into a new filter instance.
     * Ensures that modifications to the cloned filter do
     * no affect this existing filter.
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    clone() {
        const filter = new this.constructor(this._model)
        filter.absorb(this._root)
        return filter
    }

    /**
     * Focus on a particular field.
     * @param {string} field
     * @returns {module:flitter-orm/src/filter/Focus~Focus}
     */
    field(field) {
        return new Focus(this, field)
    }

    /**
     * End the filter and apply it to the model's proxy.
     * @returns {module:flitter-orm/src/proxy/model/FilterProxy~FilterProxy}
     */
    end() {
        return new FilterProxy(this._model, this)
    }

    /**
     * Asserts that a field must equal the value.
     * @param {string} field
     * @param {*} value
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    equal(field, value) {
        return this.field(field).equal(value).end()
    }

    /**
     * Asserts that a field must be less than the value.
     * @param {string} field
     * @param {*} value
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    less_than(field, value) {
        return this.field(field).less_than(value).end()
    }

    /**
     * Asserts that a field must be greater than the value.
     * @param {string} field
     * @param {*} value
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    greater_than(field, value) {
        return this.field(field).greater_than(value).end()
    }

    /**
     * Asserts that a field must be less than or equal to the value.
     * @param {string} field
     * @param {*} value
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    less_than_equal(field, value) {
        return this.field(field).less_than_equal(value).end()
    }

    /**
     * Asserts that a field must be greater than or equal to the value.
     * @param {string} field
     * @param {*} value
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    greater_than_equal(field, value) {
        return this.field(field).greater_than_equal(value).end()
    }

    /**
     * Asserts that a field must be in the array of options for the value.
     * @param {string} field
     * @param {*} value
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    in(field, value) {
        return this.field(field).in(value).end()
    }

    /**
     * Write the accumulated filter to a MongoDB compatible filter object.
     * @returns {object}
     */
    write() {
        if ( this._root.$and.length < 1 ) return {}
        return this.__exec_resolve(this._root)
    }

    /**
     * Resolve the filter object from structures to primitives.
     * @param {object} object
     * @returns {object}
     * @private
     */
    __exec_resolve(object) {
        if ( Array.isArray(object) ) {
            const return_arr = []
            for ( const elem of object ) {
                const val = typeof elem === 'function' ? elem() : elem
                return_arr.push(this.__exec_resolve(val))
            }
            return return_arr
        } else if ( typeof object === 'object' && !(object instanceof Date) && !(object instanceof ObjectId) ) {
            const return_obj = {}
            for ( const prop in object ) {
                const elem = object[prop]
                const val = typeof elem === 'function' ? elem() : elem
                return_obj[prop] = this.__exec_resolve(val)
            }
            return return_obj
        } else if ( typeof object === 'function' ) {
            return this.__exec_resolve(object())
        } else {
            return object
        }
    }

    /**
     * Merge this filter with another.
     * @param {module:flitter-orm/src/filter/Filter~Filter} other
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     */
    absorb(other) {
        if ( other._root ) {
            this._root.$and = this._root.$and.concat(other._root.$and)
        } else {
            for ( const key in other ) {
                if ( key === '$and' ) {
                    this._root.$and = this._root.$and.concat(other.$and)
                } else {
                    this._root.$and.push({ [key]: other[key] })
                }
            }
        }

        return this
    }

    /**
     * Callback to break a field's focus and merge the filters for that field into this filter.
     * @param {module:flitter-orm/src/filter/Focus~Focus} focus
     * @returns {module:flitter-orm/src/filter/Filter~Filter}
     * @private
     */
    _unfocus(focus) {
        this._root.$and.push({ [focus._field]: focus._root })
        return this
    }
}

module.exports = exports = Filter