Skip to content
Snippets Groups Projects
reqQueryFields.js 14.5 KiB
Newer Older
const libs = `${process.cwd()}/libs`;

const log = require(`${libs}/log`)(module);

const _ = require('lodash');

function parseWhereValue(type, value) {
    if(type === 'integer') return parseInt(value, 10);
    if(type === 'double') return parseFloat(value);
    if(type === 'string') return '%'+value+'%';
    if(type === 'boolean') {
Vytor Calixto's avatar
Vytor Calixto committed
        if(value === null || typeof value === 'boolean') {
            return value;
Vytor Calixto's avatar
Vytor Calixto committed
        if(typeof value === 'string') {
            if(value.toLowerCase() === 'true' || value.toLowerCase() === '1') {
                return true;
            }
            if(value.toLowerCase() === 'null') {
                return null;
            }
        }
        return false;
class ReqQueryFields {
    constructor(fields = {}, fieldValues = {}) {
        // Exemplo de requisição: `/api/v1/enrollments?dims=state,region,location`

        // Parâmetros no campo query da requisição.
        // Exemplo de field:
        // {
        //    name: 'dims',
        //    field: true,
        //    where: false
        // }
        // ```
        // Array de valores aceitos pelo campo.
        // Exemplo de valor:
        // ```
        // {
        //     name: 'location',
        //     table: 'localizacao',
        //     tableField: 'descricao'
        //     resultField: 'location_name',
        //     where: {
        //         relation: '=',
        //         type: 'integer',
        //         field: 'id_localizacao',
        //         table: 'turma'
        //     },
        //     join: {
        //         primary: 'pk_localizacao_id',
        //         foreign: 'id_localizacao',
        //         foreignTable: 'turma'
        //     }
        // }
        // ```
        this.fields = fields;
        this.fieldValues = fieldValues;
    }

    addField(field) {
        // Parâmetro no campo query da requisição.
        // Exemplo de field:
        // {
        //    name: 'dims',
        //    field: true,
        //    where: false,
        //    fieldValues: {}
        // }
        if(typeof this.fields[field.name] === 'undefined') {
            this.fields[field.name] = field;
        }
        return this;
    }

    addValue(fieldValue) {
        // Valor aceito por **todos** os campos.
        // Exemplo de valor:
        // {
        //     name: 'location',
        //     table: 'localizacao',
        //     tableField: 'descricao'
        //     resultField: 'location_name',
        //     where: {
        //         relation: '=',
        //         type: 'integer',
        //         field: 'id_localizacao',
        //         table: 'turma'
        //     },
        //     join: {
        //         primary: 'pk_localizacao_id',
        //         foreign: 'id_localizacao',
        //         foreignTable: 'turma'
        //     }
        // }
        if(typeof this.fieldValues[fieldValue.name] === 'undefined') {
            this.fieldValues[fieldValue.name] = fieldValue;
        }
        return this;
    }

    addValueToField(fieldValue, field) {
        // Valor aceito **apenas** pelo campo `field`.
        if(typeof this.fields[field] === 'undefined') {
            // Se o campo não existe, lança uma exception
            throw 'No field with name ' +field+ ' defined';
        }
        if(typeof this.fields[field].values === 'undefined') {
            this.fields[field].values = {};
        }
        if(typeof this.fields[field].values[fieldValue.name] === 'undefined') {
            this.fields[field].values[fieldValue.name] = fieldValue;
        // Faz o parse dos valores que vem na requisição para objetos.
        return (req, res, next) => {
            // "Foreach" nos campos aceitos
            Object.keys(this.fields).map((key, index) => {
                let params = [];
                let f = this.fields[key];
                // Unimos os valores parametros globalmente com os aceitos apenas pelo campo
                let values = _.merge(this.fieldValues, f.values);
                // Fazemos um foreach nos parametros aceitos
                Object.keys(values).map((k, i) => {
                    let value = values[k];
                    // Pushamos o parametro
                    params.push(value.name);
                });
                let queryField = f.name;
                let arrayOfParams = params;
                // Criamos o atributo com o nome do **campo** no objeto `req` (nome do campo é 'filter', 'dims', 'search', etc)
                req[queryField] = {};
                if (req.query[queryField]) {
                    // Se há mais de um parametro no campo, eles estão separados por vírgula.
                    // Fazemos o split então para separar os valores
                    const regex = /,(?=[a-z])/; // Pega as vírgulas que não estão nos atributos multivalorados
                    const params = req.query[queryField].split(regex);
                    // Objeto temporário para guardar os parametros e seus valores.
                    const obj = {};
                    for (const param of params) {
                        // O parametro *pode* ter um valor (por exemplo: `state:41`).
                        // Fazemos o split e temos um array `['state', 41]`
                        const kv = param.split(':');
                        // Checa se há um valor. Se não tem, definimos como true.
Vytor Calixto's avatar
Vytor Calixto committed
                        if ( (typeof kv[1] === 'undefined')) {
                            obj[kv[0]] = true;
                        } else {
                            try {
                                obj[kv[0]] = JSON.parse(kv[1]);
                            } catch(err) {
                                obj[kv[0]] = kv[1];
                            }
                        }
                        // `obj` é agora `{kv[0]: kv[1]}` ou `{kv[0]: true}`.
                        // No exemplo `{'state': 41}`
                    // Se o array existe e não está vazio fazemos a interseção
                    if (typeof arrayOfParams !== 'undefined' && arrayOfParams.length > 0) {
                        // Fazemos a interseção das chaves de `obj` com o array `arrayOfParams`.
                        // O array resultante são as chaves aceitas pelo campo.
                        const intersection = _.intersection(arrayOfParams, Object.keys(obj));
                        // Isso é um pouco complicado...
                        // Para cada chave na interseção pegamos seu valor em `obj`
                        // e atribuímos para o atributo que definimos no objeto `req`.
                        // Por exemplo: intersection = `['state']` então
                        // `obj[intersection[i]]` (com i=0) é `obj['state']`, cujo valor é 41.
                        // Então fazemos `req[queryField]['state'] = 41`
                        for (let i = 0; i < intersection.length; ++i) {
                            req[queryField][intersection[i]] = obj[intersection[i]];
                        }
                        req[queryField].size = intersection.length;
                    } else {
                        req[queryField] = obj;
                        req[queryField].size = Object.keys(obj).length;
                    }
                }
            });
            next();
        };
    }

Vytor Calixto's avatar
Vytor Calixto committed
    buildQuery(req, sql) {
Vytor Calixto's avatar
Vytor Calixto committed
        let hasJoined = {};
        let thisTable = sql.tableFrom;
        // Foreach no campos
        Object.keys(this.fields).forEach((key) => {
            // Campo
            let field = this.fields[key];
            log.debug(field);
            // `param` aqui é o atributo no objeto `req` (dims, filter, search, ...)
            let param = req[field.name];
            // Fazemos um foreach nos parametros dentro do atributo
            Object.keys(param).forEach((k) => {
                let values = _.merge(this.fieldValues, field.values);
                if(typeof values[k] !== 'undefined') {
                    // Clonamos para não alterar o original
                    let value = _.clone(values[k]);
                    if(value.parseOnly) return;
                    // Checa se não fizemos o join para este valor e se é necessário fazer
                    if(!hasJoined[value.table] && typeof value.join !== 'undefined') {
                        let foreignTable = '';
                        if(value.join.foreignTable) foreignTable = value.join.foreignTable+'.';
                        if(value.join.foreignTable === '@') foreignTable = thisTable+'.';
                        // Fazemos o join
                        let onClause = '';
                        if(Array.isArray(value.join.primary)) {
                            // Se é um array, montamos a cláusula ON com mais de uma coluna
                            value.join.primary.forEach((column, index, arr) => {
                                onClause += foreignTable+value.join.foreign[index]+'='+value.table+'.'+column;
                                if(index < arr.length-1) {
                                    onClause+=' AND ';
                                }
                            });
                        } else {
                            onClause = foreignTable+value.join.foreign+'='+value.table+'.'+value.join.primary;
Vytor Calixto's avatar
Vytor Calixto committed
                        sql.join(value.table, null, onClause);
                        // Marcamos o join como feito para não ter problemas
                        hasJoined[value.table] = true;
                    }
                    // Se o valor é um campo a ser incluído no SELECT
                    if(typeof field.field !== 'undefined' && field.field) {
                        log.debug('SELECT');
                        let table = value.table;
                        if(table === '@') table = thisTable;
                        if (Array.isArray(value.resultField)) {
                            value.tableField.forEach((f, i) => {
                                sql.field(table+'.'+f, value.resultField[i] || f)
                                    .group(table+'.'+f)
                                    .order(table+'.'+f);
                            })
                        }else{
                            sql.field(table+'.'+value.tableField, value.resultField || value.tableField)
                                .order(table+'.'+value.tableField)
                                .group(table+'.'+value.tableField);
Vytor Calixto's avatar
Vytor Calixto committed
                    }
                    // Se o valor é um campo para ser usado no WHERE
                    if(typeof field.where !== 'undefined' && field.where) {
                        log.debug('WHERE');
                        // Valor do where
Vytor Calixto's avatar
Vytor Calixto committed
                        let whereValue = param[k];
                        log.debug('whereValue');
                        log.debug(whereValue);
                        log.debug(`Where value é array? ${Array.isArray(whereValue)}`);
Vytor Calixto's avatar
Vytor Calixto committed
                        let tbl = value.where.table || value.table;
                        if (tbl === '@') tbl = thisTable;
                        // multiple where
                        if (Array.isArray(value.where.field)) {
                            let lower = (value.where.type === 'string') ? ' LOWER(?) ' : ' ? ';
                            let whereField = '';
                            let whereValues = [];
                            value.where.field.forEach((f, i, arr) => {
                                whereField += (value.where.type === 'string') ? 'LOWER(' + tbl + '.' + value.where.field[i] + ')' : tbl + '.' + value.where.field[i];
                                whereField += ' ' + value.where.relation + ' ?';
                                if (i < arr.length - 1) {
                                    whereField += ' ' + value.where.condition + ' ';
                                }
Vytor Calixto's avatar
Vytor Calixto committed
                                if (Array.isArray(whereValue)) {
                                    let whereString = '(';
                                    for(let i = 0; i < whereValue.length; ++i) {
Vytor Calixto's avatar
Vytor Calixto committed
                                        whereString += whereField;
                                        whereValues.push(parseWhereValue(value.where.type, whereValue[i]));
                                        if(i < whereValue.length-1) {
                                            whereString += ' OR ';
                                        }
                                    }
                                    whereString += ')';
                                } else {
Vytor Calixto's avatar
Vytor Calixto committed
                                    whereValues.push(parseWhereValue(value.where.type, whereValue));
Vytor Calixto's avatar
Vytor Calixto committed
                            });

                            sql.where(whereField, ...whereValues);
                        } else {
                            let whereField = (value.where.type === 'string') ? 'LOWER(' + tbl + '.' + value.where.field + ')' : tbl + '.' + value.where.field;
                            let lower = (value.where.type === 'string') ? ' LOWER(?) ' : ' ? ';
                            if(Array.isArray(whereValue)) {
                                let whereString = '(';
                                let arrayWhereValues = [];
                                for(let i = 0; i < whereValue.length; ++i) {
                                    whereString += whereField + ' ' + value.where.relation + lower;
                                    arrayWhereValues.push(parseWhereValue(value.where.type, whereValue[i]));
                                    if(i < whereValue.length-1) {
                                        whereString += ' OR ';
                                    }
                                }
                                whereString += ')';
                                sql.where(whereString, ...arrayWhereValues);
                            } else {
                                sql.where(whereField + ' ' + value.where.relation + lower, parseWhereValue(value.where.type, whereValue));
Vytor Calixto's avatar
Vytor Calixto committed
                }
            });
        });
        return sql;
    }

    build() {
        return (req, res, next) => {
            req.sql = this.buildQuery(req, req.sql);
            next();
        };
    }

    multibuild() {
        return (req, res, next) => {
            req.querySet.forEach((query) => {
                query = this.buildQuery(req, query);
            });
            next();
        };
    }
}

module.exports = ReqQueryFields;