depresto
Version:
Why am I doing this to myself
213 lines (185 loc) • 6.04 kB
JavaScript
const operations = [
'$in',
'$notIn',
'$like',
'$notLike',
'$gt',
'$gte',
'$lt',
'$lte',
'$or',
'$is',
'$isNot',
]
function depresto(ast, isSubquery = false) {
const statements = [];
if (!isQuery(ast)) {
throw Error('Query must contain `from` attribute at a minimum')
}
const { select, from, limit, groupBy, ...conditions } = ast;
statements.push(createSelectStatement(select, from));
if (conditions.hasOwnProperty('where')) {
statements.push('WHERE');
statements.push(compileConditionals(conditions.where));
}
if (groupBy) statements.push(`GROUP BY ${groupBy}`);
if (limit) statements.push(`LIMIT ${limit}`);
// If we're in a subquery we don't want to append a semi-colon on the end
if (isSubquery) {
return statements.join(' ');
} else {
return `${statements.join(' ')};`
}
}
function createSelectStatement(select, from) {
if (select) {
if (Array.isArray(select)) {
return `SELECT ${select.join(', ')} FROM ${from}`;
} else {
return `SELECT ${select} FROM ${from}`;
}
} else {
return `SELECT * FROM ${from}`;
}
}
function compileConditionals(conditionals) {
const andStatements = []
for ([key, val] of Object.entries(conditionals)) {
if (operations.includes(key)) { // Need to use an operation
andStatements.push(parseOperation(key, val));
} else { // Not using an operation (usually a standard comparison)
andStatements.push(parseSimpleCondition(key, val));
}
}
return andStatements.join(' AND ');
}
function parseSimpleCondition(key, val) {
return `${key} = ${surroundStrWithQuotes(val)}`;
}
function parseOperation(operation, expression) {
switch (operation) {
case "$or":
return _or(expression);
case "$in":
return _in(expression);
case "$notIn":
return _notIn(expression);
case "$gt":
return _gt(expression);
case "$gte":
return _gte(expression);
case "$lt":
return _lt(expression);
case "$lte":
return _lte(expression);
case "$is":
return _is(expression);
case "$isNot":
return _isNot(expression);
case "$like":
return _like(expression);
case "$notLike":
return _notLike(expression);
default:
throw new Error(`Unknown operator ${operation}`);
}
}
function _like(expression) {
const [key, val] = Object.entries(expression)[0];
if (typeof val !== 'string') throw Error("Only strings are valid in $like operations");
return `${key} LIKE '${val}'`;
}
function _notLike(expression) {
const [key, val] = Object.entries(expression)[0];
if (typeof val !== 'string') throw Error("Only strings are valid in $notLike operations");
return `${key} NOT LIKE '${val}'`;
}
function _is(expression) {
const [key, val] = Object.entries(expression)[0];
if (val !== null) throw Error("Only NULL is valid for $is operations");
return `${key} IS NULL`;
}
function _isNot(expression) {
const [key, val] = Object.entries(expression)[0];
if (val !== null) throw Error("Only NULL is valid for $isNot operations");
return `${key} IS NOT NULL`;
}
function _lte(expression) {
const [key, val] = Object.entries(expression)[0];
return `${key} <= ${surroundStrWithQuotes(val)}`
}
function _lt(expression) {
const [key, val] = Object.entries(expression)[0];
return `${key} < ${surroundStrWithQuotes(val)}`;
}
function _gt(expression) {
const [key, val] = Object.entries(expression)[0];
return `${key} > ${surroundStrWithQuotes(val)}`;
}
function _gte(expression) {
const [key, val] = Object.entries(expression)[0];
return `${key} >= ${surroundStrWithQuotes(val)}`
}
function _or(expression) {
if (!Array.isArray(expression)) throw Error('Invalid $or clause. Must of of type `Array`');
const orConditions = expression.map(condition => {
return Object.entries(condition).length > 1
? surroundWithParens(compileConditionals(condition))
: compileConditionals(condition);
});
return surroundWithParens(orConditions.join(' OR '));
}
function _notIn(expression) {
const [key, val] = Object.entries(expression)[0]
if (isQuery(val)) {
return `${key} NOT IN (${depresto(val, true)})`;
} else {
const formattedVals = val.map(v => surroundStrWithQuotes(v)).join(', ');
return `${key} NOT IN (${formattedVals})`;
}
}
function _in(expression) {
const [key, val] = Object.entries(expression)[0];
if (isQuery(val)) {
return `${key} IN (${depresto(val, true)})`;
} else {
const formattedVals = val.map(v => surroundStrWithQuotes(v)).join(', ');
return `${key} IN (${formattedVals})`;
}
}
function isQuery(obj) {
return obj.hasOwnProperty('from');
}
function surroundWithParens(str) {
return `(${str})`
}
function surroundStrWithQuotes(value) {
return typeof value === 'string'
? `'${value.replace('\'', '\'\'')}'`
: value;
}
function fn(fnName, ...args) {
/*
* Here we have to parse the arguments to work out if they should
* be surrounded with quotations or not before they are inserted
* in the function call
*/
const argsString = args.map((arg) => {
switch (typeof arg) {
case 'string':
return surroundStrWithQuotes(arg);
case 'object':
if (!arg.hasOwnProperty('col')) {
throw Error('Error function arguments')
}
return arg.col;
default:
return arg;
}
}).join(', ');
return `${fnName}(${argsString})`;
}
function col(columnName) {
return { col: columnName };
}
module.exports = { depresto, fn, col };