daemonraco/dfdb

View on GitHub
src/includes/condition.dfdb.ts

Summary

Maintainability
A
55 mins
Test Coverage
/**
 * @file condition.dfdb.ts
 * @author Alejandro D. Simi
 */

/**
 * List of known types of conditions.
 * @enum ConditionTypes
 */
export enum ConditionTypes {
    Exact,
    GreaterOrEqual,
    GreaterThan,
    Ignored,
    In,
    Like,
    LowerOrEqual,
    LowerThan,
    NotIn,
    RegEx,
    Wrong
};

/**
 * This simple class checks how a list of contitions should be specified.
 * @type ConditionsList
 */
export type ConditionsList = Array<Condition>;

/**
 * Standard way to specify simple conditions.
 * @type SimpleConditionsList
 */
export type SimpleConditionsList = { [name: string]: any };

/**
 * This class represents all types of search conditions and how they evaluate
 * values.
 *
 * @class Condition
 */
export class Condition {
    //
    // Signature for dynamic method association.
    [key: string]: any;
    //
    // Protected class constants.
    protected static readonly Keywords: { [name: string]: ConditionTypes } = {
        '$exact': ConditionTypes.Exact,
        '$ge': ConditionTypes.GreaterOrEqual,
        '$gt': ConditionTypes.GreaterThan,
        '$in': ConditionTypes.In,
        '$le': ConditionTypes.LowerOrEqual,
        '$like': ConditionTypes.Like,
        '$lt': ConditionTypes.LowerThan,
        '$notIn': ConditionTypes.NotIn,
        '$regex': ConditionTypes.RegEx
    };
    protected static readonly KeywordsAliases: { [name: string]: string } = {
        '=': '$exact',
        '>=': '$ge',
        '>': '$gt',
        '<=': '$le',
        '<': '$lt',
        '*': '$like',
        '$partial': '$like',
        '$regexp': '$regex'
    };
    protected static readonly PrimitiveTypes: string[] = ['boolean', 'number'];
    //
    // Protected properties.
    protected _data: any = null;
    protected _field: string = null;
    protected _type: ConditionTypes = null;
    protected _typeName: string = null;
    //
    // Constructors
    /**
     * @protected
     * @constructor
     */
    protected constructor(conditionType: ConditionTypes, field: string, data: any) {
        //
        // Shortcuts.
        this._type = conditionType;
        this._field = field;
        this._data = data;
        //
        // Setting pointer methods.
        this._typeName = ConditionTypes[this._type];
        this.cleanData = this[`cleanData${this._typeName}`];
        this.validate = this[`validate${this._typeName}`];
        //
        // Cleaning data.
        this.cleanData();
    }
    //
    // Public pointer methods.
    /**
     * This point will hold the logic to validate a value when this object
     * completes its contruction.
     *
     * @method validate
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    public validate: (value: any) => boolean = null;
    //
    // Protected pointer methods.
    /**
     * This point will hold the logic to check condition data when this object
     * completes its contruction.
     *
     * @method cleanData
     */
    protected cleanData: () => void;
    //
    // Public methods.
    /**
     * Field name to which this condition is associated.
     *
     * @method field
     * @returns {string} Returns a field name.
     */
    public field(): string {
        return this._field;
    }
    /**
     * This methods provides a proper value for string auto-castings.
     *
     * @method toString
     * @returns {string} Returns a simple string identifying this condition.
     */
    public toString = (): string => {
        return `condition:${this._field}[${this._typeName}]`;
    }
    //
    // Protected methods.
    /**
     * This method holds the logic to prepare a condition's internal value to be
     * matched exactly in later validations.
     *
     * @protected
     * @method cleanDataExact
     */
    protected cleanDataExact(): void {
        this._data = `${this._data['$exact']}`.toLowerCase();
    }
    /**
     * This method holds the logic to prepare a condition's internal value for
     * later 'greater than or equal' validations.
     *
     * @protected
     * @method cleanDataGreaterOrEqual
     */
    protected cleanDataGreaterOrEqual(): void {
        if (Condition.PrimitiveTypes.indexOf(typeof this._data) < 0) {
            this._data = `${this._data['$ge']}`.toLowerCase();
        }
    }
    /**
     * This method holds the logic to prepare a condition's internal value for
     * later 'greater than' validations.
     *
     * @protected
     * @method cleanDataGreaterThan
     */
    protected cleanDataGreaterThan(): void {
        if (Condition.PrimitiveTypes.indexOf(typeof this._data) < 0) {
            this._data = `${this._data['$gt']}`.toLowerCase();
        }
    }
    /**
     * There's no need to prepare a condition's internal value when it always
     * returns TRUE.
     *
     * @protected
     * @method cleanDataIgnored
     */
    protected cleanDataIgnored(): void {
        this._data = null;
    }
    /**
     * This method holds the logic to prepare a condition's internal value to be
     * used as pool in later validations.
     *
     * @protected
     * @method cleanDataIn
     */
    protected cleanDataIn(): void {
        this._data = Array.isArray(this._data['$in']) ? this._data['$in'] : [];
        this._data = this._data.map((v: any) => `${v}`.toLowerCase());
    }
    /**
     * This method holds the logic to prepare a condition's internal value to be
     * partially matched in later validations.
     *
     * @protected
     * @method cleanDataLike
     */
    protected cleanDataLike(): void {
        if (typeof this._data === 'object') {
            this._data = `${this._data['$like']}`.toLowerCase();
        } else {
            this._data = `${this._data}`.toLowerCase();
        }
    }
    /**
     * This method holds the logic to prepare a condition's internal value for
     * later 'lower than or equal' validations.
     *
     * @protected
     * @method cleanDataLowerOrEqual
     */
    protected cleanDataLowerOrEqual(): void {
        if (Condition.PrimitiveTypes.indexOf(typeof this._data) < 0) {
            this._data = `${this._data['$le']}`.toLowerCase();
        }
    }
    /**
     * This method holds the logic to prepare a condition's internal value for
     * later 'lower than' validations.
     *
     * @protected
     * @method cleanDataLowerThan
     */
    protected cleanDataLowerThan(): void {
        if (Condition.PrimitiveTypes.indexOf(typeof this._data) < 0) {
            this._data = `${this._data['$lt']}`.toLowerCase();
        }
    }
    /**
     * This method holds the logic to prepare a condition's internal value to be
     * used as pool in later validations.
     *
     * @protected
     * @method cleanDataNotIn
     */
    protected cleanDataNotIn(): void {
        this._data = Array.isArray(this._data['$notIn']) ? this._data['$notIn'] : [];
        this._data = this._data.map((v: any) => `${v}`.toLowerCase());
    }
    /**
     * This method holds the logic to prepare a condition's internal value to be
     * matched as regular expression in later validations.
     *
     * @protected
     * @method cleanDataRegEx
     */
    protected cleanDataRegEx(): void {
        if (typeof this._data['$regex'] !== 'undefined' && this._data['$regex'] instanceof RegExp) {
            this._data = new RegExp(this._data['$regex'], 'i');
        } else {
            this.cleanDataWrong();
        }
    }
    /**
     * There's no need to prepare a condition's internal value when it always
     * returns FALSE.
     *
     * @protected
     * @method cleanDataWrong
     */
    protected cleanDataWrong(): void {
        this._data = null;
    }
    /**
     * This method holds the logic to validate if a value is another (both values
     * are interpreted as strings).
     *
     * @protected
     * @method validateExact
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateExact(value: any): boolean {
        return this._data == `${value}`.toLowerCase();
    }
    /**
     * This method holds the logic to validate if a value greater than or equal to
     * the one configure (both values are interpreted as strings unless they are
     * primitive types).
     *
     * @protected
     * @method validateGreaterOrEqual
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateGreaterOrEqual(value: any): boolean {
        return Condition.PrimitiveTypes.indexOf(typeof value) < 0 ? `${value}`.toLowerCase() >= this._data : value >= this._data;
    }
    /**
     * This method holds the logic to validate if a value greater than the one
     * configure (both values are interpreted as strings unless they are primitive
     * types).
     *
     * @protected
     * @method validateGreaterThan
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateGreaterThan(value: any): boolean {
        return Condition.PrimitiveTypes.indexOf(typeof value) < 0 ? `${value}`.toLowerCase() > this._data : value > this._data;
    }
    /**
     * This method is used to always accept values.
     *
     * @protected
     * @method validateIgnored
     * @param {any} value Provided for compatibility.
     * @returns {boolean} Always returns TRUE.
     */
    protected validateIgnored(value: any): boolean {
        return true;
    }
    /**
     * This method holds the logic to validate if a value is among others in a
     * list (both values are interpreted as strings).
     *
     * @protected
     * @method validateIn
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateIn(value: any): boolean {
        return this._data.indexOf(`${value}`.toLowerCase()) > -1;
    }
    /**
     * This method holds the logic to validate if a value is on is inside another
     * (both values are interpreted as strings).
     *
     * @protected
     * @method validateLike
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateLike(value: any): boolean {
        return `${value}`.toLowerCase().indexOf(this._data) > -1;
    }
    /**
     * This method holds the logic to validate if a value lower than or equal to
     * the one configure (both values are interpreted as strings unless they are
     * primitive types).
     *
     * @protected
     * @method validateLowerOrEqual
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateLowerOrEqual(value: any): boolean {
        return Condition.PrimitiveTypes.indexOf(typeof value) < 0 ? `${value}`.toLowerCase() <= this._data : value <= this._data;
    }
    /**
     * This method holds the logic to validate if a value lower than the one
     * configure (both values are interpreted as strings unless they are primitive
     * types).
     *
     * @protected
     * @method validateLowerThan
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateLowerThan(value: any): boolean {
        return Condition.PrimitiveTypes.indexOf(typeof value) < 0 ? `${value}`.toLowerCase() < this._data : value < this._data;
    }
    /**
     * This method holds the logic to validate if a value is among others in a
     * list (both values are interpreted as strings).
     *
     * @protected
     * @method validateNotIn
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateNotIn(value: any): boolean {
        return this._data.indexOf(`${value}`.toLowerCase()) < 0;
    }
    /**
     * This method holds the logic to validate if a value matches a regular
     * expression.
     *
     * @protected
     * @method validateRegEx
     * @param {any} value Value to check.
     * @returns {boolean} Returns TRUE when it checks out.
     */
    protected validateRegEx(value: any): boolean {
        return `${value}`.toLowerCase().match(this._data) !== null;
    }
    /**
     * This method is used to always reject values.
     *
     * @protected
     * @method validateWrong
     * @param {any} value Provided for compatibility.
     * @returns {boolean} Always returns FALSE.
     */
    protected validateWrong(value: any): boolean {
        return false;
    }
    //
    // Public class methods.
    /**
     * This method takes a condition data/configuration and returns the proper
     * object to validate it.
     *
     * @static
     * @method BuildCondition
     * @param {string} field Field to which the condition will be associated to.
     * @param {any} conf Configuration to analyse while building a condition.
     * @returns {Condition} Return a condition object.
     */
    public static BuildCondition(field: string, conf: any): Condition {
        //
        // Default values.
        let type: ConditionTypes = ConditionTypes.Like;
        //
        // Is it a complex specification?
        if (typeof conf === 'object' && !Array.isArray(conf)) {
            //
            // Cleaning aliases.
            Object.keys(conf).forEach((key: string) => {
                const newKey: string = typeof Condition.KeywordsAliases[key] !== 'undefined' ? Condition.KeywordsAliases[key] : null;
                if (newKey) {
                    conf[newKey] = conf[key];
                    delete conf[key];
                }
            });
            //
            // Is there someting to use? Otherwise it's ignored and always
            // validates as valid.
            const keys = Object.keys(conf);
            if (keys.length > 0) {
                //
                // First should say how to preceed. Does it? Otherwise it always
                // validates as insvalid.
                if (typeof Condition.Keywords[keys[0]] !== 'undefined') {
                    type = Condition.Keywords[keys[0]];
                } else {
                    type = ConditionTypes.Wrong;
                }
            } else {
                type = ConditionTypes.Ignored;
            }
        }
        //
        // Building the right condition.
        return new Condition(type, field, conf);
    }
    /**
     * This method takes a simple list of conditions and builds a list of search
     * condition objects.
     *
     * @static
     * @method BuildConditionsSet
     * @param {SimpleConditionsList} conditions Simple list of conditions.
     * @returns {ConditionsList} Returns a list of search conditions.
     */
    public static BuildConditionsSet(conditions: SimpleConditionsList): ConditionsList {
        //
        // Default values.
        let out: ConditionsList = [];
        //
        // Can it be used to build a list of conditions?
        if (typeof conditions === 'object' && !Array.isArray(conditions)) {
            //
            // Building and adding each condition.
            Object.keys(conditions).forEach((key: string) => {
                //
                // Is it a list of multiple conditions for the same field?
                if (typeof conditions[key] === 'object' && !Array.isArray(conditions[key]) && Object.keys(conditions[key]).length > 0) {
                    //
                    // Separating each key as a different condition.
                    Object.keys(conditions[key]).forEach((subKey: string) => {
                        const subConf: any = {};
                        subConf[subKey] = conditions[key][subKey];
                        out.push(Condition.BuildCondition(key, subConf));
                    });
                } else {
                    //
                    // At this point it should be added as a single condition.
                    out.push(Condition.BuildCondition(key, conditions[key]));
                }
            });
        }

        return out;
    }
}