123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- // ██████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗██████╗ ████████╗███████╗██████╗
- // ██╔════╝██╔═══██╗████╗ ██║██║ ██║██╔════╝██╔══██╗╚══██╔══╝██╔════╝██╔══██╗
- // ██║ ██║ ██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝ ██║ █████╗ ██████╔╝
- // ██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██╗
- // ╚██████╗╚██████╔╝██║ ╚████║ ╚████╔╝ ███████╗██║ ██║ ██║ ███████╗██║ ██║
- // ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
- //
- // 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;
- };
|