sql-filtrex
Version:
SQL-like expression parser to generate Where Expression for end-users
118 lines (103 loc) • 3.85 kB
JavaScript
// lib/parser is auto generated on build by jison
const parser = require('../lib/parser.js');
function parseExpression(input, options = {}) {
const columns = Array.isArray(options.columns) && options.columns.length > 0 ? options.columns : null;
const functions = Array.isArray(options.functions) ? options.functions : [];
const constants = typeof options.constants === 'object' && options.constants !== null ? options.constants : {};
return parser.parse(input, columns, functions, constants);
}
function astToSqlBricks(sql, node) {
// its already raw value
switch (node.type) {
case 'AND':
case 'OR':
// Logical combinations
return sql[node.type.toLowerCase()](
astToSqlBricks(sql, node.left),
astToSqlBricks(sql, node.right)
);
case 'COMPARE': {
// Simple comparisons (=, !=, >, <, >=, <=)
const { column, op, value } = node;
// console.log(node)
const opMap = {
'=': 'eq',
'!=': 'notEq',
'>': 'gt',
'<': 'lt',
'>=': 'gte',
'<=': 'lte'
};
const fn = opMap[op];
if (!fn || !sql[fn]) throw new Error(`Unsupported comparison operator: ${op}`);
// alias for 'is null'
if (value?.type === 'NULL') {
if (fn === 'eq') {
return sql.isNull(astToSqlBricks(sql, column));
} else if (fn === 'notEq') {
return sql.isNotNull(astToSqlBricks(sql, column));
}
}
return sql[fn](astToSqlBricks(sql, column), astToSqlBricks(sql, value));
}
case 'LIKE': {
// LIKE with ~= in the DSL
return sql.like(astToSqlBricks(sql, node.column), astToSqlBricks(sql, node.pattern));
}
case 'BETWEEN': {
// BETWEEN low AND high
return sql.between(astToSqlBricks(sql, node.column), astToSqlBricks(sql, node.low), astToSqlBricks(sql, node.high));
}
case 'IS_NULL':
// IS NULL
return sql.isNull(astToSqlBricks(sql, node.column));
case 'IS_NOT_NULL':
// IS NOT NULL
return sql.isNotNull(astToSqlBricks(sql, node.column));
case 'IN': {
// IN [v1, v2, v3, …]
// sql.in takes (column, valuesArray)
const processedValues = node.values.map(e => astToSqlBricks(sql, e));
return sql.in(astToSqlBricks(sql, node.column), processedValues);
};
case 'NOT_IN': {
// NOT IN [v1, v2, …]
const processedValues = node.values.map(e => astToSqlBricks(sql, e));
return sql.not(sql.in(astToSqlBricks(sql, node.column), processedValues));
};
case 'FUNCTION': {
// e.g. { type:'FUNCTION', name:'upper', args:[{type:'COLUMN',name:'col'}] }
const fn = node.name;
const args = node.args.map(e => e.type === 'STRING' ? JSON.stringify(astToSqlBricks(sql, e)) : astToSqlBricks(sql, e)).join(',');
// SQL Bricks: sql.func('upper', columnExpr, ...)
return sql(`${fn}(${args})`);
}
case 'CONST':
case 'STRING':
case 'NUMBER':
// e.g. { type:'CONST', value: 34 }
// We inline constants as literal values
return node.value;
case 'NULL':
return null;
case 'SQL_PARAM':
// raw value
return sql(node.value);
case 'COLUMN':
// fallback for identifiers that were not constants
return sql(node.name);
default:
throw new Error(`Unknown AST node type: ${node.type}`);
}
}
function filterToQuery(sql, filter, options = {}) {
if (!sql) throw new Error('You must provide a SqlBricks instance');
if (typeof filter !== 'string') throw new Error('Filter expression must be a string');
if (typeof options !== 'object' || options === null) throw new Error('Filter expression must be a string');
const ast = parseExpression(filter, options);
return astToSqlBricks(sql, ast);
}
module.exports = {
filterToQuery,
parseExpression,
};