waterline-utils
Version:
Various utilities for working with Waterline queries and adapters.
1,319 lines (1,105 loc) • 38.9 kB
JavaScript
/* eslint-disable no-use-before-define */
// ████████╗ ██████╗ ██╗ ██╗███████╗███╗ ██╗██╗███████╗███████╗██████╗
// ╚══██╔══╝██╔═══██╗██║ ██╔╝██╔════╝████╗ ██║██║╚══███╔╝██╔════╝██╔══██╗
// ██║ ██║ ██║█████╔╝ █████╗ ██╔██╗ ██║██║ ███╔╝ █████╗ ██████╔╝
// ██║ ██║ ██║██╔═██╗ ██╔══╝ ██║╚██╗██║██║ ███╔╝ ██╔══╝ ██╔══██╗
// ██║ ╚██████╔╝██║ ██╗███████╗██║ ╚████║██║███████╗███████╗██║ ██║
// ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚═╝╚══════╝╚══════╝╚═╝ ╚═╝
//
// The tokenizer is responsible for taking a nested Waterline statement and
// turning it into a flat set of keys. This allows the query to more easily be
// parsed and prevents further recusion as the query progresses to eventually
// end up as a native query.
//
// In most cases this will not be implemented by adapter authors but will be used
// inside a database driver's `compileStatement` machine.
var _ = require('@sailshq/lodash');
// ╔═╗╦═╗╔═╗╔═╗╔═╗╔═╗╔═╗╔═╗╦═╗ ╔═╗╦ ╦╔╗╔╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ╠═╝╠╦╝║ ║║ ║╣ ╚═╗╚═╗║ ║╠╦╝ ╠╣ ║ ║║║║║ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╚═╝╚═╝╚═╝╚═╝╚═╝╚═╝╩╚═ ╚ ╚═╝╝╚╝╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
// These are the identifiers used in RQL for various keys
var identifiers = {
'select': 'SELECT',
'from': 'FROM',
'or': 'OR',
'and': 'AND',
'not': 'NOT',
'nin': 'NOTIN',
'in': 'IN',
'distinct': 'DISTINCT',
'count': 'COUNT',
'min': 'MIN',
'max': 'MAX',
'sum': 'SUM',
'avg': 'AVG',
'limit': 'LIMIT',
'skip': 'SKIP',
'groupBy': 'GROUPBY',
'orderBy': 'ORDERBY',
'where': 'WHERE',
'insert': 'INSERT',
'into': 'INTO',
'update': 'UPDATE',
'using': 'USING',
'del': 'DELETE',
'join': 'JOIN',
'innerJoin': 'JOIN',
'outerJoin': 'JOIN',
'crossJoin': 'JOIN',
'leftJoin': 'JOIN',
'leftOuterJoin': 'JOIN',
'rightJoin': 'JOIN',
'rightOuterJoin': 'JOIN',
'fullOuterJoin': 'JOIN',
'union': 'UNION',
'unionAll': 'UNIONALL',
'as': 'AS',
'>': 'OPERATOR',
'<': 'OPERATOR',
'<>': 'OPERATOR',
'<=': 'OPERATOR',
'>=': 'OPERATOR',
'!=': 'OPERATOR',
'like': 'OPERATOR',
'opts': 'OPTS',
'returning': 'RETURNING'
};
// If these identifiers are found within a WHERE clause, treat them as regular keys.
var WHERE_EXEMPT = [
'from',
'distinct',
'count',
'min',
'max',
'sum',
'avg',
'insert',
'union',
'as',
'returning',
'join'
];
// These are the Data Manipulation Identifiers that denote a subquery
var DML_IDENTIFIERS = [
'select',
'insert',
'update',
'del'
];
// ╔╦╗╔═╗╦╔═╔═╗╔╗╔╦╔═╗╔═╗ ┌─┐┌┐ ┬┌─┐┌─┐┌┬┐
// ║ ║ ║╠╩╗║╣ ║║║║╔═╝║╣ │ │├┴┐ │├┤ │ │
// ╩ ╚═╝╩ ╩╚═╝╝╚╝╩╚═╝╚═╝ └─┘└─┘└┘└─┘└─┘ ┴
// @obj {Object} - the token obj being processed
// @processor {Object} - a value to insert between each key in the array
var tokenizeObject = function tokenizeObject(obj, processor, parent, isSubQuery, results) {
// If this obj represent a sub-query, add a sub query token
if (isSubQuery) {
results.push({
type: 'SUBQUERY',
value: null
});
}
// Determine whether we're currently processing a WHERE clause.
// We use a stack of counters to do this, adding and removing from the stack
// when subqueries are started / ended, and incrementing/decrementing the
// first counter in the stack when WHERE clauses are started and ended.
var inWhere = !!_.reduce(results, function(memo, item) {
if (item.type === 'IDENTIFIER' || item.value === 'WHERE') {
memo[0]++;
}
else if (item.type === 'ENDIDENTIFIER' || item.value === 'WHERE') {
memo[0]--;
}
else if (item.type === 'SUBQUERY') {
memo.unshift(0);
}
else if (item.type === 'ENDSUBQUERY') {
memo.unshift();
}
return memo;
}, [0])[0];
_.each(_.keys(obj), function tokenizeKey(key, idx) {
// Check if the key is a known identifier
var isIdentitifier = identifiers[key];
// If so, look ahead at it's value to determine what to do next.
if (isIdentitifier) {
// ╦ ╦╦ ╦╔═╗╦═╗╔═╗ ╔═╗═╗ ╦╔═╗╔╦╗╔═╗╔╦╗
// ║║║╠═╣║╣ ╠╦╝║╣───║╣ ╔╩╦╝║╣ ║║║╠═╝ ║
// ╚╩╝╩ ╩╚═╝╩╚═╚═╝ ╚═╝╩ ╚═╚═╝╩ ╩╩ ╩
// If we're currently inside a WHERE clause and we encounter a key that is normally an identifier
// but is in the WHERE_EXEMPT list, process it as a regular key.
if (inWhere && _.contains(WHERE_EXEMPT, key)) {
results.push({
type: 'KEY',
value: key
});
if (_.isObject(obj[key])) {
tokenizeObject(obj[key], undefined, undefined, undefined, results);
return;
}
results.push({
type: 'VALUE',
value: obj[key]
});
return;
}
// ╔═╗╔═╗╔═╗╦═╗╔═╗╔╦╗╔═╗╦═╗ ╔═╗╦═╗╔═╗╔╦╗╦╔═╗╔═╗╔╦╗╔═╗╔═╗
// ║ ║╠═╝║╣ ╠╦╝╠═╣ ║ ║ ║╠╦╝ ╠═╝╠╦╝║╣ ║║║║ ╠═╣ ║ ║╣ ╚═╗
// ╚═╝╩ ╚═╝╩╚═╩ ╩ ╩ ╚═╝╩╚═ ╩ ╩╚═╚═╝═╩╝╩╚═╝╩ ╩ ╩ ╚═╝╚═╝
// If the identifier is an OPERATOR, add it's tokens
if (identifiers[key] === 'OPERATOR') {
// If there is a parent and the previous key in the results isn't
// a KEY add it's key first. This is used when a key has multiple
// criteria. EX: { values: { '>': 100, '<': 200 }}
if (parent && _.last(results).type !== 'KEY') {
results.push({
type: 'KEY',
value: parent
});
}
processOperator(key, obj[key], results);
return;
}
// If the identifier is an IN
if (identifiers[key] === 'IN') {
processIn(obj[key], undefined, results);
return;
}
// If the identifier is an OR, start a group and add each token.
if (identifiers[key] === 'OR') {
processOr(obj[key], results);
return;
}
// If the identifier is an AND, start a group and add each token.
if (identifiers[key] === 'AND') {
processAnd(obj[key], results);
return;
}
// If the identifier is a NOT
if (identifiers[key] === 'NOT') {
processNot(obj[key], results);
return;
}
// If the identifier is a NOTIN
if (identifiers[key] === 'NOTIN') {
processIn(obj[key], true, results);
return;
}
// ╔═╗ ╦ ╦╔═╗╦═╗╦╔═╗╔═╗
// ║═╬╗║ ║║╣ ╠╦╝║║╣ ╚═╗
// ╚═╝╚╚═╝╚═╝╩╚═╩╚═╝╚═╝
// If the identifier is a FROM, add it's token
if (identifiers[key] === 'FROM') {
processFrom(obj[key], results);
return;
}
// If the identifier is a WHERE, add it's token and process it's values
if (identifiers[key] === 'WHERE') {
processWhere(obj[key], results);
return;
}
// If the identifier is a GROUP BY aggregation
if (identifiers[key] === 'GROUPBY') {
processGroupBy(obj[key], results);
return;
}
// If the identifier is an ORDER BY, add the sort options
if (identifiers[key] === 'ORDERBY') {
processOrderBy(obj[key], results);
return;
}
// ╔╦╗╔╦╗╦ ╔═╗╔═╗╔╦╗╔╦╗╔═╗╔╗╔╔╦╗╔═╗
// ║║║║║║ ║ ║ ║║║║║║║╠═╣║║║ ║║╚═╗
// ═╩╝╩ ╩╩═╝ ╚═╝╚═╝╩ ╩╩ ╩╩ ╩╝╚╝═╩╝╚═╝
// If the identifier is a SELECT, add it's token
if (identifiers[key] === 'SELECT') {
processSelect(obj[key], results);
return;
}
// If the identifier is an INSERT, add it's token
if (identifiers[key] === 'INSERT') {
processInsert(obj[key], results);
return;
}
// If the identifier is an UPDATE, add it's token
if (identifiers[key] === 'UPDATE') {
processUpdate(obj[key], results);
return;
}
// If the identifier is a DELETE, add it's token
if (identifiers[key] === 'DELETE') {
processDelete(results);
return;
}
// If the identifier is an INTO, add it's token
if (identifiers[key] === 'INTO') {
processInto(obj[key], results);
return;
}
// If the identifier is an USING, add it's token
if (identifiers[key] === 'USING') {
processUsing(obj[key], results);
return;
}
// ╔═╗╔═╗╔═╗╦═╗╔═╗╔═╗╔═╗╔╦╗╔═╗╔═╗
// ╠═╣║ ╦║ ╦╠╦╝║╣ ║ ╦╠═╣ ║ ║╣ ╚═╗
// ╩ ╩╚═╝╚═╝╩╚═╚═╝╚═╝╩ ╩ ╩ ╚═╝╚═╝
// If the identifier is a AVG
if (identifiers[key] === 'AVG') {
processAggregations(obj[key], 'AVG', results);
return;
}
// If the identifier is a SUM
if (identifiers[key] === 'SUM') {
processAggregations(obj[key], 'SUM', results);
return;
}
// If the identifier is a MIN
if (identifiers[key] === 'MIN') {
processAggregations(obj[key], 'MIN', results);
return;
}
// If the identifier is a MAX
if (identifiers[key] === 'MAX') {
processAggregations(obj[key], 'MAX', results);
return;
}
// If the identifier is a COUNT
if (identifiers[key] === 'COUNT') {
processAggregations(obj[key], 'COUNT', results);
return;
}
// ╔═╗╔╦╗╦ ╦╔═╗╦═╗
// ║ ║ ║ ╠═╣║╣ ╠╦╝
// ╚═╝ ╩ ╩ ╩╚═╝╩╚═
// If the identifier is a LIMIT
if (identifiers[key] === 'LIMIT') {
processPagination(obj[key], 'LIMIT', results);
return;
}
// If the indetifier is an SKIP
if (identifiers[key] === 'SKIP') {
processPagination(obj[key], 'SKIP', results);
return;
}
// AS is only available on sub queries
if (identifiers[key] === 'AS') {
if (!isSubQuery) {
return;
}
processAs(obj[key], results);
return;
}
// If the indetifier is an RETURNING
if (identifiers[key] === 'RETURNING') {
processReturning(obj[key], results);
return;
}
// ╦╔═╗╦╔╗╔╔═╗
// ║║ ║║║║║╚═╗
// ╚╝╚═╝╩╝╚╝╚═╝
// If the identifier is a JOIN, add it's token and process the joins
if (identifiers[key] === 'JOIN') {
processJoin(obj[key], key, results);
return;
}
// ╦ ╦╔╗╔╦╔═╗╔╗╔╔═╗
// ║ ║║║║║║ ║║║║╚═╗
// ╚═╝╝╚╝╩╚═╝╝╚╝╚═╝
// If the identifier is a UNION
if (identifiers[key] === 'UNION') {
processUnion(obj[key], 'UNION', results);
return;
}
// If the identifier is a UNIONALL
if (identifiers[key] === 'UNIONALL') {
processUnion(obj[key], 'UNIONALL', results);
return;
}
// ╔═╗╔═╗╔╦╗╔═╗
// ║ ║╠═╝ ║ ╚═╗
// ╚═╝╩ ╩ ╚═╝
// Handle any known values in the opts. Opts must be a dictionary.
if (identifiers[key] === 'OPTS') {
if (!_.isPlainObject(obj[key])) {
return;
}
_.each(obj[key], function processOpt(val, key) {
// Handle PG schema values
if (key === 'schema') {
return processSchema(val, results);
}
});
return;
}
// Add the identifier
results.push({
type: identifiers[key],
value: key
});
// If the identifier is an array, loop through each item and tokenize
if (_.isArray(obj[key])) {
_.each(obj[key], function tokenizeJoinPiece(expr) {
tokenizeObject(expr, undefined, undefined, undefined, results);
});
return;
}
// If the identifier is an object, continue tokenizing it
if (_.isPlainObject(obj[key])) {
tokenizeObject(obj[key], undefined, key, undefined, results);
return;
}
// Otherwise WTF?
return;
}
// Otherwise add the token for the key
results.push({
type: 'KEY',
value: key
});
// If the value is an object, recursively parse it unless it matches as
// a sub query
if (_.isPlainObject(obj[key])) {
// Check if the value is a subquery first
var subQuery = checkForSubquery(obj[key], results);
if (subQuery) {
return;
}
// Otherwise parse the object
tokenizeObject(obj[key], undefined, key, undefined, results);
return;
}
// If the value is a primitive add it's token
results.push({
type: 'VALUE',
value: obj[key]
});
// If there is a processor and we are not on the last key, add it as well.
// This is used for things like:
// {
// not: {
// firstName: 'foo',
// lastName: 'bar'
// }
// }
// Where we need to insert a NOT statement between each key
if (processor && (_.keys(obj).length > idx + 1)) {
results.push(processor);
}
});
// If this obj represent a sub-query, close the sub query token
if (isSubQuery) {
results.push({
type: 'ENDSUBQUERY',
value: null
});
}
};
// ╔═╗╦ ╦╔═╗╔═╗╦╔═ ╔═╗╔═╗╦═╗ ╔═╗╦ ╦╔╗ ╔═╗ ╦ ╦╔═╗╦═╗╦ ╦
// ║ ╠═╣║╣ ║ ╠╩╗ ╠╣ ║ ║╠╦╝ ╚═╗║ ║╠╩╗║═╬╗║ ║║╣ ╠╦╝╚╦╝
// ╚═╝╩ ╩╚═╝╚═╝╩ ╩ ╚ ╚═╝╩╚═ ╚═╝╚═╝╚═╝╚═╝╚╚═╝╚═╝╩╚═ ╩
var checkForSubquery = function checkForSubquery(value, results) {
var isSubquery = false;
// Check if the object has any top level DML identifiers
_.each(value, function checkForIdentifier(val, key) {
if (_.indexOf(DML_IDENTIFIERS, key) < 0) {
return;
}
isSubquery = true;
});
// If this is a sub query, tokenize it as such
if (isSubquery) {
tokenizeObject(value, undefined, undefined, isSubquery, results);
return isSubquery;
}
return isSubquery;
};
// ╔═╗╔═╗╔═╗╦═╗╔═╗╔╦╗╔═╗╦═╗╔═╗
// ║ ║╠═╝║╣ ╠╦╝╠═╣ ║ ║ ║╠╦╝╚═╗
// ╚═╝╩ ╚═╝╩╚═╩ ╩ ╩ ╚═╝╩╚═╚═╝
var processOperator = function processOperator(operator, value, results) {
// Add the operator to the results
results.push({
type: 'OPERATOR',
value: operator
});
results.push({
type: 'VALUE',
value: value
});
// Add the operator to the results
results.push({
type: 'ENDOPERATOR',
value: operator
});
};
// ╔═╗╔═╗╦ ╔═╗╔═╗╔╦╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ╚═╗║╣ ║ ║╣ ║ ║ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ╚═╝╚═╝╩═╝╚═╝╚═╝ ╩ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processSelect = function processSelect(value, results) {
// Check if a distinct or other key is being used
if (_.isPlainObject(value) && !_.isArray(value)) {
if (value.distinct) {
// Add the distinct to the results
results.push({
type: 'IDENTIFIER',
value: 'DISTINCT'
});
// Add the value to the results
results.push({
type: 'VALUE',
value: value.distinct
});
// Add the enddistinct to the results
results.push({
type: 'ENDIDENTIFIER',
value: 'DISTINCT'
});
return;
}
}
// If the value is not an array or object, add the value
if (!_.isPlainObject(value) && !_.isArray(value)) {
// Add the SELECT to the results
results.push({
type: 'IDENTIFIER',
value: 'SELECT'
});
// Add the value to the results
results.push({
type: 'VALUE',
value: value
});
// Add the ENDSELECT to the results
results.push({
type: 'ENDIDENTIFIER',
value: 'SELECT'
});
return;
}
// If the value is not an array, make it one so that we can process each
// element.
if (!_.isArray(value)) {
value = [value];
}
// Process each item in there SELECT statement and process subqueries as
// needed.
_.each(value, function processSelectKey(val) {
// Add the SELECT to the results
results.push({
type: 'IDENTIFIER',
value: 'SELECT'
});
// If the value isn't an object, no need to process it further
if (!_.isPlainObject(val)) {
results.push({
type: 'VALUE',
value: val
});
}
// Check if the object is a sub-query
if (_.isPlainObject(val)) {
var isSubquery = checkForSubquery(val, results);
// If it's not, add it's value
if (!isSubquery) {
results.push({
type: 'VALUE',
value: val
});
}
}
// Add the ENDSELECT to the results
results.push({
type: 'ENDIDENTIFIER',
value: 'SELECT'
});
});
};
// ╔═╗╦═╗╔═╗╔╦╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ╠╣ ╠╦╝║ ║║║║ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ╚ ╩╚═╚═╝╩ ╩ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processFrom = function processFrom(value, results) {
// Check if a schema is being used
if (_.isObject(value) && !_.isFunction(value) && !_.isArray(value)) {
// Add the FROM identifier
results.push({
type: 'IDENTIFIER',
value: 'FROM'
});
// Check if a subquery is being used
var isSubQuery = checkForSubquery(value, results);
if (!isSubQuery && value.table) {
results.push({
type: 'VALUE',
value: value.table
});
}
results.push({
type: 'ENDIDENTIFIER',
value: 'FROM'
});
return;
}
// Otherwise just add the FROM identifier and value
results.push({
type: 'IDENTIFIER',
value: 'FROM'
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: 'FROM'
});
};
// ╔═╗╔═╗╦ ╦╔═╗╔╦╗╔═╗ ╔═╗╔═╗╔╦╗
// ╚═╗║ ╠═╣║╣ ║║║╠═╣ ║ ║╠═╝ ║
// ╚═╝╚═╝╩ ╩╚═╝╩ ╩╩ ╩ ╚═╝╩ ╩
var processSchema = function processSchema(value, results) {
results.push({
type: 'IDENTIFIER',
value: 'SCHEMA'
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: 'SCHEMA'
});
};
// ╦╔╗╔╔═╗╔═╗╦═╗╔╦╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ║║║║╚═╗║╣ ╠╦╝ ║ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ╩╝╚╝╚═╝╚═╝╩╚═ ╩ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processInsert = function processInsert(value, results) {
// Add the insert statment
results.push({
type: 'IDENTIFIER',
value: 'INSERT'
});
// Check if an array is being used
if (_.isArray(value)) {
_.each(value, function appendInsertEach(record, idx) {
// Add a group clause
results.push({
type: 'GROUP',
value: idx
});
// If the value is a plain object, proccess it
if (_.isObject(record) && !_.isFunction(record) && !_.isArray(record)) {
_.each(_.keys(record), function appendInsertValue(key) {
results.push({
type: 'KEY',
value: key
});
results.push({
type: 'VALUE',
value: record[key]
});
});
}
// Close the group clause
results.push({
type: 'ENDGROUP',
value: idx
});
});
}
// Check if a plain object value is being used
if (_.isObject(value) && !_.isFunction(value) && !_.isArray(value)) {
_.each(_.keys(value), function appendInsertValue(key) {
results.push({
type: 'KEY',
value: key
});
results.push({
type: 'VALUE',
value: value[key]
});
});
}
results.push({
type: 'ENDIDENTIFIER',
value: 'INSERT'
});
};
// ╦╔╗╔╔╦╗╔═╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ║║║║ ║ ║ ║ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ╩╝╚╝ ╩ ╚═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processInto = function processInto(value, results) {
results.push({
type: 'IDENTIFIER',
value: 'INTO'
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: 'INTO'
});
};
// ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ║ ║╠═╝ ║║╠═╣ ║ ║╣ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ╚═╝╩ ═╩╝╩ ╩ ╩ ╚═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processUpdate = function processUpdate(value, results) {
// Add the update statment
results.push({
type: 'IDENTIFIER',
value: 'UPDATE'
});
// Check if a value is being used
if (_.isObject(value)) {
_.each(_.keys(value), function appendUpdateValue(key) {
results.push({
type: 'KEY',
value: key
});
results.push({
type: 'VALUE',
value: value[key]
});
});
}
results.push({
type: 'ENDIDENTIFIER',
value: 'UPDATE'
});
};
// ╦ ╦╔═╗╦╔╗╔╔═╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ║ ║╚═╗║║║║║ ╦ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ╚═╝╚═╝╩╝╚╝╚═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processUsing = function processUsing(value, results) {
results.push({
type: 'IDENTIFIER',
value: 'USING'
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: 'USING'
});
};
// ╔╦╗╔═╗╦ ╔═╗╔╦╗╔═╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ║║║╣ ║ ║╣ ║ ║╣ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ═╩╝╚═╝╩═╝╚═╝ ╩ ╚═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processDelete = function processDelete(results) {
results.push({
type: 'IDENTIFIER',
value: 'DELETE'
});
results.push({
type: 'ENDIDENTIFIER',
value: 'DELETE'
});
};
// ╔╗╔╔═╗╔╦╗ ╔═╗╔═╗╔╗╔╔╦╗╦╔╦╗╦╔═╗╔╗╔
// ║║║║ ║ ║ ║ ║ ║║║║ ║║║ ║ ║║ ║║║║
// ╝╚╝╚═╝ ╩ ╚═╝╚═╝╝╚╝═╩╝╩ ╩ ╩╚═╝╝╚╝
var processNot = function processNot(value, results) {
// Add a condition
var condition = {
type: 'CONDITION',
value: 'NOT'
};
results.push(condition);
// Tokenize the values within the condition
if (_.isObject(value) && !_.isFunction(value) && !_.isArray(value)) {
tokenizeObject(value, condition, undefined, undefined, results);
return;
}
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDCONDITION',
value: 'NOT'
});
};
// ╦╔╗╔ ╔═╗╔═╗╔╗╔╔╦╗╦╔╦╗╦╔═╗╔╗╔
// ║║║║ ║ ║ ║║║║ ║║║ ║ ║║ ║║║║
// ╩╝╚╝ ╚═╝╚═╝╝╚╝═╩╝╩ ╩ ╩╚═╝╝╚╝
var processIn = function processIn(value, negate, results) {
// Add a condition
var startCondition;
var endCondition;
if (negate) {
startCondition = {
type: 'CONDITION',
value: 'NOTIN'
};
endCondition = {
type: 'ENDCONDITION',
value: 'NOTIN'
};
} else {
startCondition = {
type: 'CONDITION',
value: 'IN'
};
endCondition = {
type: 'ENDCONDITION',
value: 'IN'
};
}
results.push(startCondition);
// If the value isn't an object, no need to process it further
if (!_.isPlainObject(value)) {
results.push({
type: 'VALUE',
value: value
});
}
// Check if the object is a sub-query
if (_.isObject(value) && !_.isFunction(value) && !_.isArray(value)) {
var isSubquery = checkForSubquery(value, results);
// If it's not, add it's value
if (!isSubquery) {
results.push({
type: 'VALUE',
value: value
});
}
}
results.push(endCondition);
};
// ╦ ╦╦ ╦╔═╗╦═╗╔═╗ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗
// ║║║╠═╣║╣ ╠╦╝║╣ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║
// ╚╩╝╩ ╩╚═╝╩╚═╚═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩
var processWhere = function processWhere(value, results) {
// Tokenize the where and then call the tokenizer on the where values
results.push({
type: 'IDENTIFIER',
value: 'WHERE'
});
tokenizeObject(value, undefined, undefined, undefined, results);
results.push({
type: 'ENDIDENTIFIER',
value: 'WHERE'
});
};
// ╔═╗╦═╗ ╔═╗╦═╗╔═╗╦ ╦╔═╗╦╔╗╔╔═╗
// ║ ║╠╦╝ ║ ╦╠╦╝║ ║║ ║╠═╝║║║║║ ╦
// ╚═╝╩╚═ ╚═╝╩╚═╚═╝╚═╝╩ ╩╝╚╝╚═╝
var processOr = function processOr(value, results) {
// Add the Or token
results.push({
type: 'CONDITION',
value: 'OR'
});
// For each condition in the OR, add a group token and process the criteria.
_.forEach(value, function appendOrCrieria(criteria, idx) {
// Start a group
results.push({
type: 'GROUP',
value: idx
});
tokenizeObject(criteria, undefined, undefined, undefined, results);
// End a group
results.push({
type: 'ENDGROUP',
value: idx
});
});
// Close the condition
results.push({
type: 'ENDCONDITION',
value: 'OR'
});
};
// ╔═╗╔╗╔╔╦╗ ╔═╗╦═╗╔═╗╦ ╦╔═╗╦╔╗╔╔═╗
// ╠═╣║║║ ║║ ║ ╦╠╦╝║ ║║ ║╠═╝║║║║║ ╦
// ╩ ╩╝╚╝═╩╝ ╚═╝╩╚═╚═╝╚═╝╩ ╩╝╚╝╚═╝
var processAnd = function processAnd(value, results) {
// Only process grouped AND's if the value is an array
if (!_.isArray(value)) {
return;
}
// Add the AND token
results.push({
type: 'CONDITION',
value: 'AND'
});
// For each condition in the OR, add a group token and process the criteria.
_.each(value, function appendAndCrieria(criteria, idx) {
// Start a group
results.push({
type: 'GROUP',
value: idx
});
tokenizeObject(criteria, undefined, undefined, undefined, results);
// End a group
results.push({
type: 'ENDGROUP',
value: idx
});
});
// Close the condition
results.push({
type: 'ENDCONDITION',
value: 'AND'
});
};
// ╦╔═╗╦╔╗╔ ╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╦╗╔═╗╔╗╔╔╦╗╔═╗
// ║║ ║║║║║ ╚═╗ ║ ╠═╣ ║ ║╣ ║║║║╣ ║║║ ║ ╚═╗
// ╚╝╚═╝╩╝╚╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝╩ ╩╚═╝╝╚╝ ╩ ╚═╝
var processJoin = function processJoin(value, joinType, results) {
// Ensure we have an array value
if (!_.isArray(value)) {
value = [value];
}
_.each(value, function processJoinInstructions(joinInstructions) {
// Add a JOIN token
results.push({
type: 'IDENTIFIER',
value: joinType.toUpperCase()
});
// Ensure the instructions include a FROM and an ON and that the ON
// is made up of two table keys.
if (!_.has(joinInstructions, 'from') || !_.has(joinInstructions, 'on')) {
throw new Error('Invalid join instructions');
}
// Check if this is an AND or an OR join statement. An AND statement will
// just be an array of conditions and an OR statement will have a single
// OR key as the value.
// Process AND
if (_.isArray(joinInstructions.on)) {
(function andInstructions() {
var JOIN_TABLE = joinInstructions.from;
results.push({ type: 'KEY', value: 'TABLE' });
results.push({ type: 'VALUE', value: JOIN_TABLE });
_.each(joinInstructions.on, function onSet(set) {
var PARENT_TABLE = _.first(_.keys(set));
var CHILD_TABLE = _.keys(set)[1];
var PARENT_COLUMN = set[_.first(_.keys(set))];
var CHILD_COLUMN = set[_.keys(set)[1]];
var setKeys = [
{ type: 'COMBINATOR', value: 'AND' },
{ type: 'KEY', value: 'TABLE_KEY' },
{ type: 'VALUE', value: PARENT_TABLE },
{ type: 'KEY', value: 'COLUMN_KEY' },
{ type: 'VALUE', value: PARENT_COLUMN },
{ type: 'KEY', value: 'TABLE_KEY' },
{ type: 'VALUE', value: CHILD_TABLE },
{ type: 'KEY', value: 'COLUMN_KEY' },
{ type: 'VALUE', value: CHILD_COLUMN }
];
_.each(setKeys, function appendSet(set) {
results.push(set);
});
});
})();
// Process OR
} else if (_.isArray(joinInstructions.on.or)) {
(function orInstructions() {
var JOIN_TABLE = joinInstructions.from;
results.push({ type: 'KEY', value: 'TABLE' });
results.push({ type: 'VALUE', value: JOIN_TABLE });
_.each(joinInstructions.on.or, function orSet(set) {
var PARENT_TABLE = _.first(_.keys(set));
var CHILD_TABLE = _.keys(set)[1];
var PARENT_COLUMN = set[_.first(_.keys(set))];
var CHILD_COLUMN = set[_.keys(set)[1]];
var setKeys = [
{ type: 'COMBINATOR', value: 'OR' },
{ type: 'KEY', value: 'TABLE_KEY' },
{ type: 'VALUE', value: PARENT_TABLE },
{ type: 'KEY', value: 'COLUMN_KEY' },
{ type: 'VALUE', value: PARENT_COLUMN },
{ type: 'KEY', value: 'TABLE_KEY' },
{ type: 'VALUE', value: CHILD_TABLE },
{ type: 'KEY', value: 'COLUMN_KEY' },
{ type: 'VALUE', value: CHILD_COLUMN }
];
_.each(setKeys, function appendSet(set) {
results.push(set);
});
});
})();
// Otherwise ensure that the ON key has two keys
} else if (!_.isPlainObject(joinInstructions.on) || _.keys(joinInstructions.on).length !== 2) {
throw new Error('Invalid join instructions');
// Handle normal, single level joins
} else {
(function buildJoinResults() {
var JOIN_TABLE = joinInstructions.from;
var PARENT_TABLE = _.first(_.keys(joinInstructions.on));
var CHILD_TABLE = _.keys(joinInstructions.on)[1];
var PARENT_COLUMN = joinInstructions.on[_.first(_.keys(joinInstructions.on))];
var CHILD_COLUMN = joinInstructions.on[_.keys(joinInstructions.on)[1]];
var joinResults = [
{ type: 'KEY', value: 'TABLE' },
{ type: 'VALUE', value: JOIN_TABLE },
{ type: 'KEY', value: 'TABLE_KEY' },
{ type: 'VALUE', value: PARENT_TABLE },
{ type: 'KEY', value: 'COLUMN_KEY' },
{ type: 'VALUE', value: PARENT_COLUMN },
{ type: 'KEY', value: 'TABLE_KEY' },
{ type: 'VALUE', value: CHILD_TABLE },
{ type: 'KEY', value: 'COLUMN_KEY' },
{ type: 'VALUE', value: CHILD_COLUMN }
];
_.each(joinResults, function appendSet(set) {
results.push(set);
});
})();
}
results.push({
type: 'ENDIDENTIFIER',
value: joinType.toUpperCase()
});
});
};
// ╔═╗╦═╗╔═╗╦ ╦╔═╗ ╔╗ ╦ ╦
// ║ ╦╠╦╝║ ║║ ║╠═╝ ╠╩╗╚╦╝
// ╚═╝╩╚═╚═╝╚═╝╩ ╚═╝ ╩
var processGroupBy = function processGroupBy(value, results) {
results.push({
type: 'IDENTIFIER',
value: 'GROUPBY'
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: 'GROUPBY'
});
};
// ╔═╗╦═╗╔╦╗╔═╗╦═╗ ╔╗ ╦ ╦
// ║ ║╠╦╝ ║║║╣ ╠╦╝ ╠╩╗╚╦╝
// ╚═╝╩╚══╩╝╚═╝╩╚═ ╚═╝ ╩
var processOrderBy = function processOrderBy(values, results) {
// Tokenize the order by and then call the tokenizer on the values
results.push({
type: 'IDENTIFIER',
value: 'ORDERBY'
});
if (!_.isArray(values)) {
values = [values];
}
_.each(values, function tokenizeSet(tokenSet) {
tokenizeObject(tokenSet, undefined, undefined, undefined, results);
});
results.push({
type: 'ENDIDENTIFIER',
value: 'ORDERBY'
});
};
// ╔═╗╔═╗╔═╗╦═╗╔═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ╠═╣║ ╦║ ╦╠╦╝║╣ ║ ╦╠═╣ ║ ║║ ║║║║╚═╗
// ╩ ╩╚═╝╚═╝╩╚═╚═╝╚═╝╩ ╩ ╩ ╩╚═╝╝╚╝╚═╝
var processAggregations = function processAggregations(value, aggregation, results) {
results.push({
type: 'IDENTIFIER',
value: aggregation
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: aggregation
});
};
// ╔═╗╔═╗╔═╗╦╔╗╔╔═╗╔╦╗╦╔═╗╔╗╔
// ╠═╝╠═╣║ ╦║║║║╠═╣ ║ ║║ ║║║║
// ╩ ╩ ╩╚═╝╩╝╚╝╩ ╩ ╩ ╩╚═╝╝╚╝
var processPagination = function processPagination(value, operator, results) {
results.push({
type: 'IDENTIFIER',
value: operator
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: operator
});
};
// ╔═╗╦═╗╔═╗╔═╗╔═╗╔═╗╔═╗ ╦ ╦╔╗╔╦╔═╗╔╗╔
// ╠═╝╠╦╝║ ║║ ║╣ ╚═╗╚═╗ ║ ║║║║║║ ║║║║
// ╩ ╩╚═╚═╝╚═╝╚═╝╚═╝╚═╝ ╚═╝╝╚╝╩╚═╝╝╚╝
var processUnion = function processUnion(values, type, results) {
results.push({
type: 'UNION',
value: type
});
_.each(values, function processUnionValue(value, idx) {
// Start each union subquery with an ENDGROUP
results.push({
type: 'GROUP',
value: idx
});
// Build the subquery
checkForSubquery(value, results);
// Close each subquery with an ENDGROUP token
results.push({
type: 'ENDGROUP',
value: idx
});
});
results.push({
type: 'ENDUNION',
value: type
});
};
// ╔═╗╦═╗╔═╗╔═╗╔═╗╔═╗╔═╗ ╔═╗╔═╗
// ╠═╝╠╦╝║ ║║ ║╣ ╚═╗╚═╗ ╠═╣╚═╗
// ╩ ╩╚═╚═╝╚═╝╚═╝╚═╝╚═╝ ╩ ╩╚═╝
var processAs = function processAs(value, results) {
results.push({
type: 'IDENTIFIER',
value: 'AS'
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: 'AS'
});
};
// ╦═╗╔═╗╔╦╗╦ ╦╦═╗╔╗╔╦╔╗╔╔═╗
// ╠╦╝║╣ ║ ║ ║╠╦╝║║║║║║║║ ╦
// ╩╚═╚═╝ ╩ ╚═╝╩╚═╝╚╝╩╝╚╝╚═╝
var processReturning = function processReturning(value, results) {
// Add the RETURNING to the results
results.push({
type: 'IDENTIFIER',
value: 'RETURNING'
});
results.push({
type: 'VALUE',
value: value
});
results.push({
type: 'ENDIDENTIFIER',
value: 'RETURNING'
});
};
module.exports = function tokenizer(expression) {
if (!expression) {
throw new Error('Missing expression');
}
// Hold the built up results
var results = [];
// Kick off recursive parsing of the RQL object
tokenizeObject(expression, undefined, undefined, undefined, results);
// Return the tokenenized result set
return results;
};
/* eslint-enable no-use-before-define */