converter.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. // ██████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗██████╗ ████████╗███████╗██████╗
  2. // ██╔════╝██╔═══██╗████╗ ██║██║ ██║██╔════╝██╔══██╗╚══██╔══╝██╔════╝██╔══██╗
  3. // ██║ ██║ ██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝ ██║ █████╗ ██████╔╝
  4. // ██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══╝ ██╔══██╗ ██║ ██╔══╝ ██╔══██╗
  5. // ╚██████╗╚██████╔╝██║ ╚████║ ╚████╔╝ ███████╗██║ ██║ ██║ ███████╗██║ ██║
  6. // ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝
  7. //
  8. // The Converter takes a Waterline query and converts it into a Waterline
  9. // statement. The difference may sound tiny but it's important. The way
  10. // Waterline currently works is that it takes up to four seperate pieces to
  11. // build a query: modelName, method, criteria, and possibly values.
  12. //
  13. // A Waterline statement is an object that encompasses the entire query. It can
  14. // easily be transformed into a native query such as a SQL string or a Mongo
  15. // object. It more closely represents a native query and is much easier to
  16. // validate. Going forward Waterline will move more and more to having end users
  17. // work with statements.
  18. var _ = require('@sailshq/lodash');
  19. module.exports = function convert(options) {
  20. var model = options.model;
  21. var method = options.method;
  22. var criteria = options.criteria;
  23. var values = options.values;
  24. var joins = options.joins;
  25. var opts = options.opts || undefined;
  26. // Hold the final query value
  27. var query = {};
  28. // Validate options
  29. if (!model) {
  30. throw new Error('Convert must contain a model to use to build the query.');
  31. }
  32. if (!method) {
  33. throw new Error('Convert must contain a method to use to build the query.');
  34. }
  35. // Validate Criteria Input is a dictionary
  36. if (criteria && !_.isPlainObject(criteria)) {
  37. throw new Error('Criteria must be a dictionary.');
  38. }
  39. // Validate Criteria Input contains a WHERE clause
  40. if (criteria && _.keys(criteria).length && !_.has(criteria, 'where')) {
  41. throw new Error('Criteria must contain a WHERE clause.');
  42. }
  43. // ╔╦╗╔═╗╔╦╗╦╔═╗╦╔═╗╦═╗╔═╗
  44. // ║║║║ ║ ║║║╠╣ ║║╣ ╠╦╝╚═╗
  45. // ╩ ╩╚═╝═╩╝╩╚ ╩╚═╝╩╚═╚═╝
  46. if (criteria && _.keys(criteria).length) {
  47. if (_.has(criteria, 'skip')) {
  48. query.skip = criteria.skip;
  49. }
  50. // Sort should be pre-normalized coming from Waterline
  51. if (_.has(criteria, 'sort')) {
  52. query.orderBy = criteria.sort;
  53. }
  54. if (_.has(criteria, 'limit')) {
  55. query.limit = criteria.limit;
  56. }
  57. }
  58. // ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗ ╔═╗╔═╗╔═╗╦ ╦
  59. // ║ ╠╦╝║╣ ╠═╣ ║ ║╣ ║╣ ╠═╣║ ╠═╣
  60. // ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝ ╚═╝╩ ╩╚═╝╩ ╩
  61. //
  62. // Process a CREATE EACH query and build a WQL insert query
  63. var processCreateEach = function processCreateEach() {
  64. query.into = model;
  65. query.insert = values || [];
  66. // Add the opts
  67. if (opts) {
  68. query.opts = opts;
  69. }
  70. };
  71. // ╔═╗╦═╗╔═╗╔═╗╔╦╗╔═╗
  72. // ║ ╠╦╝║╣ ╠═╣ ║ ║╣
  73. // ╚═╝╩╚═╚═╝╩ ╩ ╩ ╚═╝
  74. //
  75. // Process a CREATE query and build a WQL insert query
  76. var processCreate = function processCreate() {
  77. query.into = model;
  78. query.insert = values || {};
  79. // Add the opts
  80. if (opts) {
  81. query.opts = opts;
  82. }
  83. };
  84. // ╔═╗╦╔╗╔╔╦╗
  85. // ╠╣ ║║║║ ║║
  86. // ╚ ╩╝╚╝═╩╝
  87. //
  88. // Process a FIND or FINDONE query and build a WQL select query.
  89. var processFind = function processFind(criteria) {
  90. query.select = criteria.select || [];
  91. query.from = model;
  92. query.where = criteria.where || {};
  93. // If there are any joins add them as well
  94. if (joins && joins.length) {
  95. // First be sure to update the select so there are no ambiguous columns
  96. query.select = _.map(query.select, function remapSelect(key) {
  97. return model + '.' + key;
  98. });
  99. // Ensure values only exist once
  100. query.select = _.uniq(query.select);
  101. // Process Joins
  102. processJoins(joins);
  103. }
  104. // Add the opts
  105. if (opts) {
  106. query.opts = opts;
  107. }
  108. };
  109. // ╔╦╗╔═╗╔═╗╔╦╗╦═╗╔═╗╦ ╦
  110. // ║║║╣ ╚═╗ ║ ╠╦╝║ ║╚╦╝
  111. // ═╩╝╚═╝╚═╝ ╩ ╩╚═╚═╝ ╩
  112. //
  113. // Process a DESTROY query and a build a WQL destroy query.
  114. var processDestroy = function processDestroy(criteria) {
  115. query.del = true;
  116. query.from = model;
  117. query.where = criteria.where || {};
  118. // Add the opts
  119. if (opts) {
  120. query.opts = opts;
  121. }
  122. };
  123. // ╦ ╦╔═╗╔╦╗╔═╗╔╦╗╔═╗
  124. // ║ ║╠═╝ ║║╠═╣ ║ ║╣
  125. // ╚═╝╩ ═╩╝╩ ╩ ╩ ╚═╝
  126. //
  127. // Process an UPDATE query and a build a WQL update query.
  128. var processUpdate = function processUpdate(criteria) {
  129. query.update = values || {};
  130. query.using = model;
  131. query.where = criteria.where || {};
  132. // Add the opts
  133. if (opts) {
  134. query.opts = opts;
  135. }
  136. };
  137. // ╔═╗╦ ╦╔═╗╦═╗╔═╗╔═╗╔═╗
  138. // ╠═╣╚╗╔╝║╣ ╠╦╝╠═╣║ ╦║╣
  139. // ╩ ╩ ╚╝ ╚═╝╩╚═╩ ╩╚═╝╚═╝
  140. //
  141. // Process an AVERAGE aggregation. In WQL you can only average by one field
  142. // at a time so if the array contains more than one item, throw an error.
  143. //
  144. // If any of `skip`, `sort`, or `limit` is used, a table subquery will be
  145. // created to prevent any weird `groupBy` logic and stick with only returning
  146. // a single value.
  147. var processAverage = function processAverage(criteria) {
  148. query.avg = values || '';
  149. // Check is a subquery is needed
  150. (function determineSubQueryUsage() {
  151. var subQuery = false;
  152. if (_.has(query, 'skip') || _.has(query, 'sort') || _.has(query, 'limit')) {
  153. subQuery = true;
  154. }
  155. // If no subquery is needed, a simple query statement can be generated
  156. if (!subQuery) {
  157. query.from = model;
  158. // Add a where clause
  159. if (_.has(criteria, 'where')) {
  160. query.where = criteria.where || {};
  161. }
  162. // Add the opts
  163. if (opts) {
  164. query.opts = opts;
  165. }
  166. return;
  167. }
  168. // Otherwise a subquery must be used
  169. query.from = {
  170. select: [values],
  171. from: model
  172. };
  173. // Add the top-level criteria pieces to the sub-query and remove them
  174. // from the top-level.
  175. if (_.has(query, 'skip')) {
  176. query.from.skip = query.skip;
  177. delete query.skip;
  178. }
  179. if (_.has(query, 'limit')) {
  180. query.from.limit = query.limit;
  181. delete query.limit;
  182. }
  183. if (_.has(query, 'orderBy')) {
  184. query.from.orderBy = query.orderBy;
  185. delete query.orderBy;
  186. }
  187. // Add a where clause
  188. if (_.has(criteria, 'where')) {
  189. query.from.where = criteria.where || {};
  190. }
  191. // Add the opts
  192. if (opts) {
  193. query.from.opts = opts;
  194. }
  195. // Set the "AS" clause so subquery will be run correctly
  196. query.from.as = 'avg';
  197. })();
  198. };
  199. // ╔═╗╦ ╦╔╦╗
  200. // ╚═╗║ ║║║║
  201. // ╚═╝╚═╝╩ ╩
  202. //
  203. // Process a SUM aggregation. In WQL you can only sum by one field
  204. // at a time so if the array contains more than one item, throw an error.
  205. //
  206. // If any of `skip`, `sort`, or `limit` is used, a table subquery will be
  207. // created to prevent any weird `groupBy` logic and stick with only returning
  208. // a single value.
  209. var processSum = function processSum(criteria) {
  210. query.sum = values || '';
  211. // Check is a subquery is needed
  212. (function determineSubQueryUsage() {
  213. var subQuery = false;
  214. if (_.has(query, 'skip') || _.has(query, 'sort') || _.has(query, 'limit')) {
  215. subQuery = true;
  216. }
  217. // If no subquery is needed, a simple query statement can be generated
  218. if (!subQuery) {
  219. query.from = model;
  220. // Add a where clause
  221. if (_.has(criteria, 'where')) {
  222. query.where = criteria.where || {};
  223. }
  224. // Add the opts
  225. if (opts) {
  226. query.opts = opts;
  227. }
  228. return;
  229. }
  230. // Otherwise a subquery must be used
  231. query.from = {
  232. select: [values],
  233. from: model
  234. };
  235. // Add the top-level criteria pieces to the sub-query and remove them
  236. // from the top-level.
  237. if (_.has(query, 'skip')) {
  238. query.from.skip = query.skip;
  239. delete query.skip;
  240. }
  241. if (_.has(query, 'limit')) {
  242. query.from.limit = query.limit;
  243. delete query.limit;
  244. }
  245. if (_.has(query, 'orderBy')) {
  246. query.from.orderBy = query.orderBy;
  247. delete query.orderBy;
  248. }
  249. // Add a where clause
  250. if (_.has(criteria, 'where')) {
  251. query.from.where = criteria.where || {};
  252. }
  253. // Add the opts
  254. if (opts) {
  255. query.from.opts = opts;
  256. }
  257. // Set the "AS" clause so subquery will be run correctly
  258. query.from.as = 'sum';
  259. })();
  260. };
  261. // ╔═╗╔═╗╦ ╦╔╗╔╔╦╗
  262. // ║ ║ ║║ ║║║║ ║
  263. // ╚═╝╚═╝╚═╝╝╚╝ ╩
  264. //
  265. // Process a COUNT query and a build a WQL count query.
  266. var processCount = function processCount(criteria) {
  267. query.count = true;
  268. query.from = model;
  269. query.where = criteria.where || {};
  270. // Add the opts
  271. if (opts) {
  272. query.opts = opts;
  273. }
  274. };
  275. // ╔═╗╦═╗╔═╗╔═╗╔═╗╔═╗╔═╗ ┬┌─┐┬┌┐┌┌─┐
  276. // ╠═╝╠╦╝║ ║║ ║╣ ╚═╗╚═╗ ││ │││││└─┐
  277. // ╩ ╩╚═╚═╝╚═╝╚═╝╚═╝╚═╝ └┘└─┘┴┘└┘└─┘
  278. //
  279. // When a find query contains an instruction set, build up a set of joins for
  280. // the query to use.
  281. function processJoins(instructions) {
  282. // Build an array to hold all the normalized join instructions
  283. var joins = [];
  284. _.each(instructions, function processJoinSet(join) {
  285. var strategy = join.strategy && join.strategy.strategy;
  286. if (!strategy) {
  287. throw new Error('Join instructions are missing a valid strategy.');
  288. }
  289. _.each(join.instructions, function buildJoin(instructions, idx) {
  290. var obj = {};
  291. obj.from = instructions.child + ' as ' + instructions.childAlias;
  292. obj.on = {};
  293. // Check the idx and determine which parent to use (in a m:m the parent)
  294. // will use the alias
  295. if (idx > 0 || instructions.forceAlias) {
  296. obj.on[instructions.parentAlias] = instructions.parentKey;
  297. } else {
  298. obj.on[instructions.parent] = instructions.parentKey;
  299. }
  300. obj.on[instructions.childAlias] = instructions.childKey;
  301. // If there is a select on the instructions, move the select to the
  302. // top level and append each item with the child name.
  303. if (instructions.criteria && instructions.criteria.select && instructions.criteria.select.length) {
  304. var _select = _.map(instructions.criteria.select, function mapSelect(col) {
  305. return instructions.childAlias + '.' + col + ' as ' + instructions.alias + '__' + col;
  306. });
  307. // Concat the select on the main criteria
  308. query.select = _.uniq(query.select.concat(_select));
  309. }
  310. joins.push(obj);
  311. });
  312. });
  313. query.leftOuterJoin = joins;
  314. }
  315. // ╔╗ ╦ ╦╦╦ ╔╦╗ ╔═╗ ╦ ╦╔═╗╦═╗╦ ╦
  316. // ╠╩╗║ ║║║ ║║ ║═╬╗║ ║║╣ ╠╦╝╚╦╝
  317. // ╚═╝╚═╝╩╩═╝═╩╝ ╚═╝╚╚═╝╚═╝╩╚═ ╩
  318. //
  319. var buildQuery = function buildQuery() {
  320. // If there was any criteria, process it
  321. var _criteria = criteria || {};
  322. switch (method) {
  323. case 'create':
  324. processCreate();
  325. break;
  326. case 'createEach':
  327. processCreateEach();
  328. break;
  329. case 'find':
  330. case 'findOne':
  331. processFind(_criteria);
  332. break;
  333. case 'destroy':
  334. processDestroy(_criteria);
  335. break;
  336. case 'update':
  337. processUpdate(_criteria);
  338. break;
  339. case 'avg':
  340. processAverage(_criteria);
  341. break;
  342. case 'sum':
  343. processSum(_criteria);
  344. break;
  345. case 'count':
  346. processCount(_criteria);
  347. break;
  348. }
  349. };
  350. // Build the query
  351. buildQuery();
  352. // Delete any SKIP 0 clauses
  353. if (_.has(query.where, 'skip') && query.where.skip === 0) {
  354. delete query.where.skip;
  355. }
  356. // Return the result
  357. return query;
  358. };