Source: QueryHash.js

'use strict';

import Utils from './Utils';
import QueryHashItem from './QueryHashItem';

class QueryHash {
    /**
     * Create an instance of QueryHash
     * @Constructor
     * @public
     * @param {string|object} [data] - base to create key-value items; base64 string, query string, or plain object
     * @throws Error
     * @returns {QueryHash} this, chainable
     */
    constructor(data) {
        this._items = [];

        if (arguments.length > 1)
            throw new Error('QueryHash constructor only accepts one optional parameter.');
        else if (typeof data === 'string')
            this._fromString(data);
        else if (Object.prototype.toString.call(data) === '[object Object]')
            this.fromObject(data);
        else if (arguments.length !== 0)
            throw new Error('QueryHash constructor only accepts a query string, base64 string, or a plain object.');

        return this;
    }

    /**
     * Add a key-value pair
     * @public
     * @param {string} key - item key to add
     * @param {string|number|boolean} val - item value
     * @throws Error
     * @returns {QueryHash} this, chainable
     */
    add(key, val) {
        if (arguments.length !== 2)
            throw new Error(`QueryHash.add expects 2 parameters, ${arguments.length} given.`);

        this._items.push(new QueryHashItem(key, val));

        return this;
    }

    /**
     * Remove items matching the given key
     * @public
     * @param {string} key - Item key to remove
     * @throws Error
     * @returns {QueryHash} this, chainable
     */
    remove(key) {
        if (arguments.length !== 1)
            throw new Error(`QueryHash.remove expects one parameter, ${arguments.length} given.`);
        // do we really need to throw an error here? Or just skip the delete statement?
        if (!this.has(key))
            throw new Error(`Item "${key}" does not exist in instance of QueryHash`);

        this._items = this._items.filter(item => item.key !== key);

        return this;
    }

    /**
     * Find items with a given key
     * @public
     * @param {string} key - Item key to find
     * @throws Error
     * @returns {QueryHashItem[]}
     */
    find(key) {
        if (arguments.length !== 1)
            throw new Error(`QueryHash.find expects one parameter, ${arguments.length} given.`);
        if (!this.has(key))
            throw new Error(`Item "${key}" does not exist in instance of QueryHash`);

        return this._items.filter(item => item.key === key);
    }

    /**
     * Return the first value found matching the given key. Useful when keys are unique.
     * @param {string} key - Return the item value with this key
     * @returns {string|null}
     */
    first(key) {
        if (arguments.length !== 1)
            throw new Error(`QueryHash.first expects one parameter, ${arguments.length} given.`);

        if (!this.has(key))
            return null;

        let item = this.find(key)[0];

        return item ? item.value : null;
    }

    /**
     * Return an array of unique item keys
     * @public
     * @returns {Array}
     */
    keys() {
        let seen = {};
        return this._items
            .map(item => item.key)
            .filter(item => seen.hasOwnProperty(item) ? false : (seen[item] = true));
    }

    /**
     * Test whether or not an item exists by its key
     * @public
     * @param {string} key - Item key to test
     * @returns {boolean}
     */
    has(key) {
        return this.keys().some(k => k === key);
    }

    /**
     * Return a base64 encoded query string
     * @public
     * @returns {string}
     */
    toUrlToken() {
        return Buffer.from(this.toQueryString()).toString('base64');
    }

    /**
     * Add items to instance from base64 query string
     * @public
     * @param {string} urlToken - a base64 encoded query string
     * @throws Error
     * @returns {QueryHash} this, chainable
     */
    fromUrlToken(urlToken) {
        if (arguments.length !== 1)
            throw new Error(`QueryHash.fromUrlToken expects 1 parameter. ${arguments.length} given.`);
        if (typeof urlToken !== 'string')
            throw new Error(`QueryHash.fromUrlToken expects input to be of type string. Type ${Object.prototype.toString.call(urlToken)} provided`);

        this._fromString(urlToken, true);

        return this;
    }

    /**
     * Return a valid query string composed of instance items
     * @public
     * @returns {string}
     */
    toQueryString() {
        return this._items.map(item => item.toString()).join('&');
    }

    /**
     * Add items to instance from a query string
     * @public
     * @param {string} qs - a valid query string
     * @throws Error
     * @returns {QueryHash} this, chainable
     */
    fromQueryString(qs) {
        if (arguments.length !== 1)
            throw new Error(`QueryHash.fromQueryString expects 1 parameter. ${arguments.length} given.`);
        if (typeof qs !== 'string')
            throw new Error(`QueryHash.fromQueryString expects input to be of type string. Type ${Object.prototype.toString.call(qs)} provided`);

        this._fromString(qs, false);

        return this;
    }

    /**
     * Add items to instance, only keys with primitive values will be added
     * @public
     * @param {object} obj - A plain object with no nested/reference values
     * @throws Error
     * @returns {QueryHash} this, chainable
     */
    fromObject(obj) {
        if (arguments.length !== 1)
            throw new Error(`QueryHash.fromObject expects one parameter, ${arguments.length} given.`);
        if (Object.prototype.toString.call(obj) !== '[object Object]')
            throw new Error('QueryHash.fromObject expects an object');

        Object.keys(obj)
            .filter(key => typeof obj[key] !== 'object')
            .forEach(key => this.add(key, obj[key]));

        return this;
    }

    /**
     * Store string input internally in instance._items
     * @private
     * @param {string} input - Query string (can be base64 encoded)
     * @param {boolean} [isBase64] - Indicates whether or not the string is base64 encoded
     * @returns {QueryHashItem[]}
     */
    _fromString(input, isBase64) {
        if (isBase64 === void 0) {
            isBase64 = Utils.isBase64(input);
        }

        let qs = isBase64 ? Buffer.from(input, 'base64').toString() : input;
        let clean = Utils.trimStringEntry(qs);

        clean.split('&')
            .map(kv => kv.split('='))
            .forEach(p => this.add(p[0], p[1]));
    }
}

export default QueryHash;