waterline-utils
Version:
Various utilities for working with Waterline queries and adapters.
444 lines (358 loc) • 13.7 kB
JavaScript
// ██████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗██████╗ ████████╗███████╗██████╗
// ██╔════╝██╔═══██╗████╗ ██║██║ ██║██╔════╝██╔══██╗╚══██╔══╝██╔════╝██╔══██╗
// ██║ ██║ ██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝ ██║ █████╗ ██████╔╝
// ██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██╗
// ╚██████╗╚██████╔╝██║ ╚████║ ╚████╔╝ ███████╗██║ ██║ ██║ ███████╗██║ ██║
// ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
//
// The Converter takes a Waterline query and converts it into a Waterline
// statement. The difference may sound tiny but it's important. The way
// Waterline currently works is that it takes up to four seperate pieces to
// build a query: modelName, method, criteria, and possibly values.
//
// A Waterline statement is an object that encompasses the entire query. It can
// easily be transformed into a native query such as a SQL string or a Mongo
// object. It more closely represents a native query and is much easier to
// validate. Going forward Waterline will move more and more to having end users
// work with statements.
var _ = require('@sailshq/lodash');
module.exports = function convert(options) {
var model = options.model;
var method = options.method;
var criteria = options.criteria;
var values = options.values;
var joins = options.joins;
var opts = options.opts || undefined;
// Hold the final query value
var query = {};
// Validate options
if (!model) {
throw new Error('Convert must contain a model to use to build the query.');
}
if (!method) {
throw new Error('Convert must contain a method to use to build the query.');
}
// Validate Criteria Input is a dictionary
if (criteria && !_.isPlainObject(criteria)) {
throw new Error('Criteria must be a dictionary.');
}
// Validate Criteria Input contains a WHERE clause
if (criteria && _.keys(criteria).length && !_.has(criteria, 'where')) {
throw new Error('Criteria must contain a WHERE clause.');
}
// ╔╦╗╔═╗╔╦╗╦╔═╗╦╔═╗╦═╗╔═╗
// ║║║║ ║ ║║║╠╣ ║║╣ ╠╦╝╚═╗
// ╩ ╩╚═╝═╩╝╩╚ ╩╚═╝╩╚═╚═╝
if (criteria && _.keys(criteria).length) {
if (_.has(criteria, 'skip')) {
query.skip = criteria.skip;
}
// Sort should be pre-normalized coming from Waterline
if (_.has(criteria, 'sort')) {
query.orderBy = criteria.sort;
}
if (_.has(criteria, 'limit')) {
query.limit = criteria.limit;
}
}
// ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ ╔═╗╔═╗╔═╗╦ ╦
// ║ ╠╦╝║╣ ╠═╣ ║ ║╣ ║╣ ╠═╣║ ╠═╣
// ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ ╚═╝╩ ╩╚═╝╩ ╩
//
// Process a CREATE EACH query and build a WQL insert query
var processCreateEach = function processCreateEach() {
query.into = model;
query.insert = values || [];
// Add the opts
if (opts) {
query.opts = opts;
}
};
// ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗
// ║ ╠╦╝║╣ ╠═╣ ║ ║╣
// ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝
//
// Process a CREATE query and build a WQL insert query
var processCreate = function processCreate() {
query.into = model;
query.insert = values || {};
// Add the opts
if (opts) {
query.opts = opts;
}
};
// ╔═╗╦╔╗╔╔╦╗
// ╠╣ ║║║║ ║║
// ╚ ╩╝╚╝═╩╝
//
// Process a FIND or FINDONE query and build a WQL select query.
var processFind = function processFind(criteria) {
query.select = criteria.select || [];
query.from = model;
query.where = criteria.where || {};
// If there are any joins add them as well
if (joins && joins.length) {
// First be sure to update the select so there are no ambiguous columns
query.select = _.map(query.select, function remapSelect(key) {
return model + '.' + key;
});
// Ensure values only exist once
query.select = _.uniq(query.select);
// Process Joins
processJoins(joins);
}
// Add the opts
if (opts) {
query.opts = opts;
}
};
// ╔╦╗╔═╗╔═╗╔╦╗╦═╗╔═╗╦ ╦
// ║║║╣ ╚═╗ ║ ╠╦╝║ ║╚╦╝
// ═╩╝╚═╝╚═╝ ╩ ╩╚═╚═╝ ╩
//
// Process a DESTROY query and a build a WQL destroy query.
var processDestroy = function processDestroy(criteria) {
query.del = true;
query.from = model;
query.where = criteria.where || {};
// Add the opts
if (opts) {
query.opts = opts;
}
};
// ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║ ║╠═╝ ║║╠═╣ ║ ║╣
// ╚═╝╩ ═╩╝╩ ╩ ╩ ╚═╝
//
// Process an UPDATE query and a build a WQL update query.
var processUpdate = function processUpdate(criteria) {
query.update = values || {};
query.using = model;
query.where = criteria.where || {};
// Add the opts
if (opts) {
query.opts = opts;
}
};
// ╔═╗╦ ╦╔═╗╦═╗╔═╗╔═╗╔═╗
// ╠═╣╚╗╔╝║╣ ╠╦╝╠═╣║ ╦║╣
// ╩ ╩ ╚╝ ╚═╝╩╚═╩ ╩╚═╝╚═╝
//
// Process an AVERAGE aggregation. In WQL you can only average by one field
// at a time so if the array contains more than one item, throw an error.
//
// If any of `skip`, `sort`, or `limit` is used, a table subquery will be
// created to prevent any weird `groupBy` logic and stick with only returning
// a single value.
var processAverage = function processAverage(criteria) {
query.avg = values || '';
// Check is a subquery is needed
(function determineSubQueryUsage() {
var subQuery = false;
if (_.has(query, 'skip') || _.has(query, 'sort') || _.has(query, 'limit')) {
subQuery = true;
}
// If no subquery is needed, a simple query statement can be generated
if (!subQuery) {
query.from = model;
// Add a where clause
if (_.has(criteria, 'where')) {
query.where = criteria.where || {};
}
// Add the opts
if (opts) {
query.opts = opts;
}
return;
}
// Otherwise a subquery must be used
query.from = {
select: [values],
from: model
};
// Add the top-level criteria pieces to the sub-query and remove them
// from the top-level.
if (_.has(query, 'skip')) {
query.from.skip = query.skip;
delete query.skip;
}
if (_.has(query, 'limit')) {
query.from.limit = query.limit;
delete query.limit;
}
if (_.has(query, 'orderBy')) {
query.from.orderBy = query.orderBy;
delete query.orderBy;
}
// Add a where clause
if (_.has(criteria, 'where')) {
query.from.where = criteria.where || {};
}
// Add the opts
if (opts) {
query.from.opts = opts;
}
// Set the "AS" clause so subquery will be run correctly
query.from.as = 'avg';
})();
};
// ╔═╗╦ ╦╔╦╗
// ╚═╗║ ║║║║
// ╚═╝╚═╝╩ ╩
//
// Process a SUM aggregation. In WQL you can only sum by one field
// at a time so if the array contains more than one item, throw an error.
//
// If any of `skip`, `sort`, or `limit` is used, a table subquery will be
// created to prevent any weird `groupBy` logic and stick with only returning
// a single value.
var processSum = function processSum(criteria) {
query.sum = values || '';
// Check is a subquery is needed
(function determineSubQueryUsage() {
var subQuery = false;
if (_.has(query, 'skip') || _.has(query, 'sort') || _.has(query, 'limit')) {
subQuery = true;
}
// If no subquery is needed, a simple query statement can be generated
if (!subQuery) {
query.from = model;
// Add a where clause
if (_.has(criteria, 'where')) {
query.where = criteria.where || {};
}
// Add the opts
if (opts) {
query.opts = opts;
}
return;
}
// Otherwise a subquery must be used
query.from = {
select: [values],
from: model
};
// Add the top-level criteria pieces to the sub-query and remove them
// from the top-level.
if (_.has(query, 'skip')) {
query.from.skip = query.skip;
delete query.skip;
}
if (_.has(query, 'limit')) {
query.from.limit = query.limit;
delete query.limit;
}
if (_.has(query, 'orderBy')) {
query.from.orderBy = query.orderBy;
delete query.orderBy;
}
// Add a where clause
if (_.has(criteria, 'where')) {
query.from.where = criteria.where || {};
}
// Add the opts
if (opts) {
query.from.opts = opts;
}
// Set the "AS" clause so subquery will be run correctly
query.from.as = 'sum';
})();
};
// ╔═╗╔═╗╦ ╦╔╗╔╔╦╗
// ║ ║ ║║ ║║║║ ║
// ╚═╝╚═╝╚═╝╝╚╝ ╩
//
// Process a COUNT query and a build a WQL count query.
var processCount = function processCount(criteria) {
query.count = true;
query.from = model;
query.where = criteria.where || {};
// Add the opts
if (opts) {
query.opts = opts;
}
};
// ╔═╗╦═╗╔═╗╔═╗╔═╗╔═╗╔═╗ ┬┌─┐┬┌┐┌┌─┐
// ╠═╝╠╦╝║ ║║ ║╣ ╚═╗╚═╗ ││ │││││└─┐
// ╩ ╩╚═╚═╝╚═╝╚═╝╚═╝╚═╝ └┘└─┘┴┘└┘└─┘
//
// When a find query contains an instruction set, build up a set of joins for
// the query to use.
function processJoins(instructions) {
// Build an array to hold all the normalized join instructions
var joins = [];
_.each(instructions, function processJoinSet(join) {
var strategy = join.strategy && join.strategy.strategy;
if (!strategy) {
throw new Error('Join instructions are missing a valid strategy.');
}
_.each(join.instructions, function buildJoin(instructions, idx) {
var obj = {};
obj.from = instructions.child + ' as ' + instructions.childAlias;
obj.on = {};
// Check the idx and determine which parent to use (in a m:m the parent)
// will use the alias
if (idx > 0 || instructions.forceAlias) {
obj.on[instructions.parentAlias] = instructions.parentKey;
} else {
obj.on[instructions.parent] = instructions.parentKey;
}
obj.on[instructions.childAlias] = instructions.childKey;
// If there is a select on the instructions, move the select to the
// top level and append each item with the child name.
if (instructions.criteria && instructions.criteria.select && instructions.criteria.select.length) {
var _select = _.map(instructions.criteria.select, function mapSelect(col) {
return instructions.childAlias + '.' + col + ' as ' + instructions.alias + '__' + col;
});
// Concat the select on the main criteria
query.select = _.uniq(query.select.concat(_select));
}
joins.push(obj);
});
});
query.leftOuterJoin = joins;
}
// ╔╗ ╦ ╦╦╦ ╔╦╗ ╔═╗ ╦ ╦╔═╗╦═╗╦ ╦
// ╠╩╗║ ║║║ ║║ ║═╬╗║ ║║╣ ╠╦╝╚╦╝
// ╚═╝╚═╝╩╩═╝═╩╝ ╚═╝╚╚═╝╚═╝╩╚═ ╩
//
var buildQuery = function buildQuery() {
// If there was any criteria, process it
var _criteria = criteria || {};
switch (method) {
case 'create':
processCreate();
break;
case 'createEach':
processCreateEach();
break;
case 'find':
case 'findOne':
processFind(_criteria);
break;
case 'destroy':
processDestroy(_criteria);
break;
case 'update':
processUpdate(_criteria);
break;
case 'avg':
processAverage(_criteria);
break;
case 'sum':
processSum(_criteria);
break;
case 'count':
processCount(_criteria);
break;
}
};
// Build the query
buildQuery();
// Delete any SKIP 0 clauses
if (_.has(query.where, 'skip') && query.where.skip === 0) {
delete query.where.skip;
}
// Return the result
return query;
};