Source: query.js

'use strict';

let transit = require('transit-js');

/**
 * A builder for datomic queries. This is meant to help build datomic
 * queries in a more fluent manner. Without this, you would need to
 * specify transit.Keyword and transit.Symbol values in your query,
 * which is verbose and cumbersome.
 *
 * @deprecated Use EDN template strings instead.
 *
 * @constructor QueryBuilder
 */
function QueryBuilder() {
    this._find = null;
    this._with = null;
    this._in = null;
    this._where = null;
    this._keys = null;
    this._syms = null;
    this._strs = null;
}

/**
 * Add a :find expression to the query.
 *
 * If there are already find elements added, these are added to the end.
 *
 * @param args Elements to find. Can either be strings (to find attributes)
 *   or composite query elements (pull, count, etc).
 * @returns This builder.
 */
QueryBuilder.prototype.find = function(...args) {
    let find = [];
    for (let i = 0; i < args.length; i++) {
        let arg = args[i];
        if (typeof arg === 'string') {
            find.push(transit.symbol(arg));
        } else {
            find.push(arg);
        }
    }
    this._find = (this._find || []).concat(find);
    return this;
};

/**
 * Add an :in expression to the query.
 *
 * @param args The input sources. Can be strings, arrays of strings,
 *   or an array of a string and '...'.
 * @returns This builder.
 */
QueryBuilder.prototype.in = function(...args) {
    let convertInput = function(input) {
        if (typeof input === 'string') {
            return transit.symbol(input);
        } else if (Array.isArray(input)) {
            return input.map(convertInput);
        } else {
            return input;
        }
    };
    this._in = (this._in || []).concat(args.map(convertInput));
    return this;
};

/**
 * Add a :with expression to the query.
 *
 * @param args Symbol strings that should be added to the :with clause.
 * @returns {QueryBuilder} This instance.
 */
QueryBuilder.prototype.with = function(...args) {
    this._with = (this._with || []).concat(args.map(transit.symbol));
    return this;
};

/**
 * Add a :keys expression to the query.
 *
 * @param args Symbol strings to add to the :keys clause.
 * @returns {QueryBuilder} This instance.
 */
QueryBuilder.prototype.keys = function(...args) {
    this._keys = (this._keys || []).concat(args.map(transit.symbol));
    return this;
};

/**
 * Add a :syms expression to the query.
 *
 * @param args Symbol strings to add to the :syms clause.
 * @returns {QueryBuilder} This instance.
 */
QueryBuilder.prototype.syms = function(...args) {
    this._syms = (this._syms || []).concat(args.map(transit.symbol));
    return this;
};

/**
 * Add a :strs expression to the query.
 *
 * @param args Symbol strings to add to the :strs clause.
 * @returns {QueryBuilder} This instance.
 */
QueryBuilder.prototype.strs = function(...args) {
    this._strs = (this._strs || []).concat(args.map(transit.symbol));
    return this;
};

/**
 * Adds :where clauses to the query.
 *
 * Each argument may be one of the following:
 *
 * <ul>
 *     <li><pre>[e, attr]</pre></li>
 *     <li><pre>[e, attr, (symbol|value)]</pre></li>
 *     <li><pre>[e, attr, (symbol|value), t]</pre></li>
 *     <li><pre>[e, attr, (symbol|value), t, added]</pre></li>
 *     <li><pre>[predicate-expression]</pre></li>
 *     <li>TODO there should be more.</li>
 * </ul>
 *
 * @param args Where clauses to add.
 * @returns {QueryBuilder} This instance.
 */
QueryBuilder.prototype.where = function(...args) {
    let convertWhere = function (where) {
        if (Array.isArray(where)) {
            if (typeof where[0] === 'string' && typeof where[1] === 'string') {
                return [transit.symbol(where[0]), transit.keyword(where[1])].concat(where.slice(2).map((e) => {
                    if (typeof e == 'string') {
                        return transit.symbol(e);
                    } else {
                        return e;
                    }
                }));
            } else {
                throw new Error('todo');
            }
        } else {
            throw new Error('each where expression should be an array');
        }
    };
    this._where = (this._where || []).concat(args.map(convertWhere));
    return this;
};

/**
 * Build the query.
 *
 * @returns {*} A value that can be sent via {@link Connection.q}.
 */
QueryBuilder.prototype.build = function () {
    let res = [];
    if (this._find != null) {
        res.push(transit.keyword('find'));
        res = res.concat(this._find);
    } else {
        throw new Error('no find expression');
    }
    if (this._in != null) {
        res.push(transit.keyword('in'));
        res = res.concat(this._in);
    }
    if (this._with != null) {
        res.push(transit.keyword('with'));
        res = res.concat(this._with);
    }
    if (this._where != null) {
        res.push(transit.keyword('where'));
        res = res.concat(this._where);
    } else {
        throw new Error('no where expressions');
    }
    if (this._keys != null) {
        res.push(transit.keyword('keys'));
        res = res.concat(this._keys);
    }
    if (this._syms != null) {
        res.push(transit.keyword('syms'));
        res = res.concat(this._syms);
    }
    if (this._strs != null) {
        res.push(transit.keyword('strs'));
        res = res.concat(this._strs);
    }
    return res;
};

function convertAttributeSpec(spec) {
    if (spec === '*') {
        return transit.symbol('*');
    } else if (typeof spec === 'string') {
        return transit.keyword(spec);
    } else if (Array.isArray(spec)) {
        if (spec[0] === 'limit') { // limit expression
            return transit.list([transit.symbol('limit'), transit.keyword(spec[1]), spec[2]]);
        } else if (spec[0] === 'default') { // default expression
            return transit.list([transit.symbol('default'), transit.keyword(spec[1]), spec[2]]);
        } else { // attr-with-options
            let expr = [transit.keyword(spec[0])];
            for (let i = 1; i + 1 < spec.length; i += 2) {
                expr.push(transit.keyword(spec[i]));
                expr.push(spec[i + 1]);
            }
            return expr;
        }
    } else if (typeof spec === 'object') {
        let res = transit.map();
        for (let k in spec) {
            if (spec.hasOwnProperty(k)) {
                let v = spec[k];
                if (typeof v === 'number') {
                    res.set(convertAttributeSpec(k), v);
                } else if (v === '...') {
                    res.set(convertAttributeSpec(k), transit.symbol('...'));
                } else {
                    res.set(convertAttributeSpec(k), convertSelector(v));
                }
            }
        }
        return res;
    }
}

function convertSelector(selector) {
    if (Array.isArray(selector)) {
        return selector.map(convertAttributeSpec);
    } else if (typeof selector === 'object') {
        throw new Error('pull selector must be an array');
    }
}

/**
 * Build a pull expression for use in a datomic query. The result
 * is typically passed to {@link #find}.
 *
 * @param eid A string describing the symbol name to use in the pull.
 * @param selector A selector that conforms to the datomic selector syntax,
 *   that is, nested arrays and objects, with the refinement that keywords
 *   may be represented just as strings.
 */
function pull(eid, selector) {
    let res = [];
    res.push(transit.symbol('pull'));
    res.push(transit.symbol('eid'));
    res.push(convertSelector(selector));
    return transit.list(res);
};

/**
 * Return a count expression, for use in {@link #find).
 *
 * @param symbol The symbol name being queried.
 * @returns The constructed count expression.
 */
function count(symbol) {
    return transit.list([transit.symbol('count'), transit.symbol(symbol)]);
}

exports.QueryBuilder = QueryBuilder;
exports.convertSelector = convertSelector;