lodash-query.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. /*
  2. Underscore Query - A lightweight query API for JavaScript collections
  3. (c)2015 - Dave Tonge
  4. May be freely distributed according to MIT license.
  5. This is small library that provides a query api for JavaScript arrays similar to *mongo db*.
  6. The aim of the project is to provide a simple, well tested, way of filtering data in JavaScript.
  7. */
  8. var QueryBuilder, addToQuery, buildQuery, createUtils, expose, findOne, i, key, len, makeTest, multipleConditions, parseGetter, parseParamType, parseQuery, parseSubQuery, performQuery, performQuerySingle, ref, root, runQuery, score, single, testModelAttribute, testQueryValue, underscoreReplacement, utils,
  9. slice = [].slice,
  10. indexOf = function(item) {
  11. for (var i = 0, l = this.length; i < l; i++) {
  12. if (this[i].constructor.name === 'RegExp') {
  13. if (i in this && this[i].test(item)) return i;
  14. }
  15. else {
  16. if (i in this && this[i] === item) return i;
  17. }
  18. }
  19. return -1;
  20. },
  21. hasProp = {}.hasOwnProperty;
  22. root = this;
  23. /* UTILS */
  24. utils = {};
  25. underscoreReplacement = function() {
  26. var out;
  27. out = {};
  28. ["every", "some", "filter", "reduce", "map"].forEach(function(key) {
  29. return out[key] = function() {
  30. var args, array;
  31. array = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : [];
  32. return array[key].apply(array, args);
  33. };
  34. });
  35. out.keys = Object.keys;
  36. out.isArray = Array.isArray;
  37. out.result = function(obj, key) {
  38. if (obj == null) {
  39. obj = {};
  40. }
  41. if (utils.getType(obj[key]) === "Function") {
  42. return obj[key]();
  43. } else {
  44. return obj[key];
  45. }
  46. };
  47. out.detect = function(array, fn) {
  48. var i, item, len;
  49. for (i = 0, len = array.length; i < len; i++) {
  50. item = array[i];
  51. if (fn(item)) {
  52. return item;
  53. }
  54. }
  55. };
  56. out.reject = function(array, fn) {
  57. var i, item, len, results;
  58. results = [];
  59. for (i = 0, len = array.length; i < len; i++) {
  60. item = array[i];
  61. if (!fn(item)) {
  62. results.push(item);
  63. }
  64. }
  65. return results;
  66. };
  67. out.intersection = function(array1, array2) {
  68. var i, item, len, results;
  69. results = [];
  70. for (i = 0, len = array1.length; i < len; i++) {
  71. item = array1[i];
  72. if (array2.indexOf(item) !== -1) {
  73. results.push(item);
  74. }
  75. }
  76. return results;
  77. };
  78. out.isEqual = function(a, b) {
  79. return JSON.stringify(a) === JSON.stringify(b);
  80. };
  81. return out;
  82. };
  83. createUtils = function(_) {
  84. var i, key, len, ref;
  85. ref = ["every", "some", "filter", "find", "reject", "reduce", "intersection", "isEqual", "keys", "isArray", "result", "map"];
  86. for (i = 0, len = ref.length; i < len; i++) {
  87. key = ref[i];
  88. utils[key] = _[key];
  89. if (!utils[key]) {
  90. throw new Error(key + " missing. Please ensure that you first initialize underscore-query with either lodash or underscore");
  91. }
  92. }
  93. };
  94. utils.getType = function(obj) {
  95. var type;
  96. type = Object.prototype.toString.call(obj).substr(8);
  97. return type.substr(0, type.length - 1);
  98. };
  99. utils.makeObj = function(key, val) {
  100. var o;
  101. (o = {})[key] = val;
  102. return o;
  103. };
  104. utils.reverseString = function(str) {
  105. return str.toLowerCase().split("").reverse().join("");
  106. };
  107. utils.compoundKeys = ["$and", "$not", "$or", "$nor"];
  108. utils.makeGetter = function(keys) {
  109. keys = keys.split(".");
  110. return function(obj) {
  111. var i, key, len, out;
  112. out = obj;
  113. for (i = 0, len = keys.length; i < len; i++) {
  114. key = keys[i];
  115. if (out) {
  116. out = utils.result(out, key);
  117. }
  118. }
  119. return out;
  120. };
  121. };
  122. multipleConditions = function(key, queries) {
  123. var results, type, val;
  124. results = [];
  125. for (type in queries) {
  126. val = queries[type];
  127. results.push(utils.makeObj(key, utils.makeObj(type, val)));
  128. }
  129. return results;
  130. };
  131. parseParamType = function(query) {
  132. var key, o, paramType, queryParam, type, value;
  133. key = utils.keys(query)[0];
  134. queryParam = query[key];
  135. o = {
  136. key: key
  137. };
  138. if (queryParam != null ? queryParam.$boost : void 0) {
  139. o.boost = queryParam.$boost;
  140. delete queryParam.$boost;
  141. }
  142. if (key.indexOf(".") !== -1) {
  143. o.getter = utils.makeGetter(key);
  144. }
  145. paramType = utils.getType(queryParam);
  146. switch (paramType) {
  147. case "RegExp":
  148. case "Date":
  149. o.type = "$" + (paramType.toLowerCase());
  150. o.value = queryParam;
  151. break;
  152. case "Object":
  153. if (indexOf.call(utils.compoundKeys, key) >= 0) {
  154. o.type = key;
  155. o.value = parseSubQuery(queryParam);
  156. o.key = null;
  157. } else if (utils.keys(queryParam).length > 1) {
  158. o.type = "$and";
  159. o.value = parseSubQuery(multipleConditions(key, queryParam));
  160. o.key = null;
  161. } else {
  162. for (type in queryParam) {
  163. if (!hasProp.call(queryParam, type)) continue;
  164. value = queryParam[type];
  165. if (testQueryValue(type, value)) {
  166. o.type = type;
  167. switch (type) {
  168. case "$elemMatch":
  169. o.value = single(parseQuery(value));
  170. break;
  171. case "$endsWith":
  172. o.value = utils.reverseString(value);
  173. break;
  174. case "$likeI":
  175. case "$startsWith":
  176. o.value = value.toLowerCase();
  177. break;
  178. case "$not":
  179. case "$nor":
  180. case "$or":
  181. case "$and":
  182. o.value = parseSubQuery(utils.makeObj(o.key, value));
  183. o.key = null;
  184. break;
  185. case "$computed":
  186. o = parseParamType(utils.makeObj(key, value));
  187. o.getter = utils.makeGetter(key);
  188. break;
  189. default:
  190. o.value = value;
  191. }
  192. } else {
  193. throw new Error("Query value (" + value + ") doesn't match query type: (" + type + ")");
  194. }
  195. }
  196. }
  197. break;
  198. default:
  199. o.type = "$equal";
  200. o.value = queryParam;
  201. }
  202. if ((o.type === "$equal") && (paramType === "Object" || paramType === "Array")) {
  203. o.type = "$deepEqual";
  204. }
  205. return o;
  206. };
  207. parseSubQuery = function(rawQuery) {
  208. var i, key, len, query, queryArray, results, val;
  209. if (utils.isArray(rawQuery)) {
  210. queryArray = rawQuery;
  211. } else {
  212. queryArray = (function() {
  213. var results;
  214. results = [];
  215. for (key in rawQuery) {
  216. if (!hasProp.call(rawQuery, key)) continue;
  217. val = rawQuery[key];
  218. results.push(utils.makeObj(key, val));
  219. }
  220. return results;
  221. })();
  222. }
  223. results = [];
  224. for (i = 0, len = queryArray.length; i < len; i++) {
  225. query = queryArray[i];
  226. results.push(parseParamType(query));
  227. }
  228. return results;
  229. };
  230. testQueryValue = function(queryType, value) {
  231. var valueType;
  232. valueType = utils.getType(value);
  233. switch (queryType) {
  234. case "$in":
  235. case "$nin":
  236. case "$all":
  237. case "$any":
  238. return valueType === "Array";
  239. case "$size":
  240. return valueType === "Number";
  241. case "$regex":
  242. case "$regexp":
  243. return valueType === "RegExp";
  244. case "$like":
  245. case "$likeI":
  246. return valueType === "String";
  247. case "$between":
  248. case "$mod":
  249. return (valueType === "Array") && (value.length === 2);
  250. case "$cb":
  251. return valueType === "Function";
  252. default:
  253. return true;
  254. }
  255. };
  256. testModelAttribute = function(queryType, value) {
  257. var valueType;
  258. valueType = utils.getType(value);
  259. switch (queryType) {
  260. case "$like":
  261. case "$likeI":
  262. case "$regex":
  263. case "$startsWith":
  264. case "$endsWith":
  265. return valueType === "String";
  266. case "$contains":
  267. case "$all":
  268. case "$any":
  269. case "$elemMatch":
  270. return valueType === "Array";
  271. case "$size":
  272. return valueType === "String" || valueType === "Array";
  273. case "$in":
  274. case "$nin":
  275. return value != null;
  276. default:
  277. return true;
  278. }
  279. };
  280. performQuery = function(type, value, attr, model, getter) {
  281. switch (type) {
  282. case "$equal":
  283. if (utils.isArray(attr)) {
  284. return indexOf.call(attr, value) >= 0;
  285. } else {
  286. return attr === value;
  287. }
  288. break;
  289. case "$deepEqual":
  290. return utils.isEqual(attr, value);
  291. case "$contains":
  292. return indexOf.call(attr, value) >= 0;
  293. case "$ne":
  294. return attr !== value;
  295. case "$lt":
  296. return attr < value;
  297. case "$gt":
  298. return attr > value;
  299. case "$lte":
  300. return attr <= value;
  301. case "$gte":
  302. return attr >= value;
  303. case "$between":
  304. return (value[0] < attr && attr < value[1]);
  305. case "$betweene":
  306. return (value[0] <= attr && attr <= value[1]);
  307. case "$in":
  308. return indexOf.call(value, attr) >= 0;
  309. case "$nin":
  310. return indexOf.call(value, attr) < 0;
  311. case "$all":
  312. return utils.every(value, function(item) {
  313. return indexOf.call(attr, item) >= 0;
  314. });
  315. case "$any":
  316. return utils.some(attr, function(item) {
  317. return indexOf.call(value, item) >= 0;
  318. });
  319. case "$size":
  320. return attr.length === value;
  321. case "$exists":
  322. case "$has":
  323. return (attr != null) === value;
  324. case "$like":
  325. return attr.indexOf(value) !== -1;
  326. case "$likeI":
  327. return attr.toLowerCase().indexOf(value) !== -1;
  328. case "$startsWith":
  329. return attr.toLowerCase().indexOf(value) === 0;
  330. case "$endsWith":
  331. return utils.reverseString(attr).indexOf(value) === 0;
  332. case "$type":
  333. return typeof attr === value;
  334. case "$regex":
  335. case "$regexp":
  336. return value.test(attr);
  337. case "$cb":
  338. return value.call(model, attr);
  339. case "$mod":
  340. return (attr % value[0]) === value[1];
  341. case "$elemMatch":
  342. return runQuery(attr, value, null, true);
  343. case "$and":
  344. case "$or":
  345. case "$nor":
  346. case "$not":
  347. return performQuerySingle(type, value, getter, model);
  348. default:
  349. return false;
  350. }
  351. };
  352. single = function(queries, getter, isScore) {
  353. var method, queryObj;
  354. if (utils.getType(getter) === "String") {
  355. method = getter;
  356. getter = function(obj, key) {
  357. return obj[method](key);
  358. };
  359. }
  360. if (isScore) {
  361. if (queries.length !== 1) {
  362. throw new Error("score operations currently don't work on compound queries");
  363. }
  364. queryObj = queries[0];
  365. if (queryObj.type !== "$and") {
  366. throw new Error("score operations only work on $and queries (not " + queryObj.type);
  367. }
  368. return function(model) {
  369. model._score = performQuerySingle(queryObj.type, queryObj.parsedQuery, getter, model, true);
  370. return model;
  371. };
  372. } else {
  373. return function(model) {
  374. var i, len;
  375. for (i = 0, len = queries.length; i < len; i++) {
  376. queryObj = queries[i];
  377. if (!performQuerySingle(queryObj.type, queryObj.parsedQuery, getter, model, isScore)) {
  378. return false;
  379. }
  380. }
  381. return true;
  382. };
  383. }
  384. };
  385. performQuerySingle = function(type, query, getter, model, isScore) {
  386. var attr, boost, i, len, passes, q, ref, score, scoreInc, test;
  387. passes = 0;
  388. score = 0;
  389. scoreInc = 1 / query.length;
  390. for (i = 0, len = query.length; i < len; i++) {
  391. q = query[i];
  392. if (q.getter) {
  393. attr = q.getter(model, q.key);
  394. } else if (getter) {
  395. attr = getter(model, q.key);
  396. } else {
  397. attr = model[q.key];
  398. }
  399. test = testModelAttribute(q.type, attr);
  400. if (test) {
  401. test = performQuery(q.type, q.value, attr, model, getter);
  402. }
  403. if (test) {
  404. passes++;
  405. if (isScore) {
  406. boost = (ref = q.boost) != null ? ref : 1;
  407. score += scoreInc * boost;
  408. }
  409. }
  410. switch (type) {
  411. case "$and":
  412. if (!isScore) {
  413. if (!test) {
  414. return false;
  415. }
  416. }
  417. break;
  418. case "$not":
  419. if (test) {
  420. return false;
  421. }
  422. break;
  423. case "$or":
  424. if (test) {
  425. return true;
  426. }
  427. break;
  428. case "$nor":
  429. if (test) {
  430. return false;
  431. }
  432. break;
  433. default:
  434. throw new Error("Invalid compound method");
  435. }
  436. }
  437. if (isScore) {
  438. return score;
  439. } else if (type === "$not") {
  440. return passes === 0;
  441. } else {
  442. return type !== "$or";
  443. }
  444. };
  445. parseQuery = function(query) {
  446. var compoundQuery, key, queryKeys, type, val;
  447. queryKeys = utils.keys(query);
  448. if (!queryKeys.length) {
  449. return [];
  450. }
  451. compoundQuery = utils.intersection(utils.compoundKeys, queryKeys);
  452. if (compoundQuery.length === 0) {
  453. return [{
  454. type: "$and",
  455. parsedQuery: parseSubQuery(query)
  456. }];
  457. } else {
  458. if (compoundQuery.length !== queryKeys.length) {
  459. if (indexOf.call(compoundQuery, "$and") < 0) {
  460. query.$and = {};
  461. compoundQuery.unshift("$and");
  462. }
  463. for (key in query) {
  464. if (!hasProp.call(query, key)) continue;
  465. val = query[key];
  466. if (!(indexOf.call(utils.compoundKeys, key) < 0)) {
  467. continue;
  468. }
  469. query.$and[key] = val;
  470. delete query[key];
  471. }
  472. }
  473. return (function() {
  474. var i, len, results;
  475. results = [];
  476. for (i = 0, len = compoundQuery.length; i < len; i++) {
  477. type = compoundQuery[i];
  478. results.push({
  479. type: type,
  480. parsedQuery: parseSubQuery(query[type])
  481. });
  482. }
  483. return results;
  484. })();
  485. }
  486. };
  487. parseGetter = function(getter) {
  488. var method;
  489. if (utils.getType(getter) === "String") {
  490. method = getter;
  491. getter = function(obj, key) {
  492. return obj[method](key);
  493. };
  494. }
  495. return getter;
  496. };
  497. QueryBuilder = (function() {
  498. function QueryBuilder(items1, _getter) {
  499. this.items = items1;
  500. this._getter = _getter;
  501. this.theQuery = {};
  502. }
  503. QueryBuilder.prototype.all = function(items, first) {
  504. if (items) {
  505. this.items = items;
  506. }
  507. if (this.indexes) {
  508. items = this.getIndexedItems(this.items);
  509. } else {
  510. items = this.items;
  511. }
  512. return runQuery(items, this.theQuery, this._getter, first);
  513. };
  514. QueryBuilder.prototype.chain = function() {
  515. return _.chain(this.all.apply(this, arguments));
  516. };
  517. QueryBuilder.prototype.tester = function() {
  518. return makeTest(this.theQuery, this._getter);
  519. };
  520. QueryBuilder.prototype.first = function(items) {
  521. return this.all(items, true);
  522. };
  523. QueryBuilder.prototype.getter = function(_getter) {
  524. this._getter = _getter;
  525. return this;
  526. };
  527. return QueryBuilder;
  528. })();
  529. addToQuery = function(type) {
  530. return function(params, qVal) {
  531. var base;
  532. if (qVal) {
  533. params = utils.makeObj(params, qVal);
  534. }
  535. if ((base = this.theQuery)[type] == null) {
  536. base[type] = [];
  537. }
  538. this.theQuery[type].push(params);
  539. return this;
  540. };
  541. };
  542. ref = utils.compoundKeys;
  543. for (i = 0, len = ref.length; i < len; i++) {
  544. key = ref[i];
  545. QueryBuilder.prototype[key.substr(1)] = addToQuery(key);
  546. }
  547. QueryBuilder.prototype.find = QueryBuilder.prototype.query = QueryBuilder.prototype.run = QueryBuilder.prototype.all;
  548. buildQuery = function(items, getter) {
  549. return new QueryBuilder(items, getter);
  550. };
  551. makeTest = function(query, getter) {
  552. return single(parseQuery(query), parseGetter(getter));
  553. };
  554. findOne = function(items, query, getter) {
  555. return runQuery(items, query, getter, true);
  556. };
  557. runQuery = function(items, query, getter, first, isScore) {
  558. var fn;
  559. if (arguments.length < 2) {
  560. return buildQuery.apply(this, arguments);
  561. }
  562. if (getter) {
  563. getter = parseGetter(getter);
  564. }
  565. if (!(utils.getType(query) === "Function")) {
  566. query = single(parseQuery(query), getter, isScore);
  567. }
  568. if (isScore) {
  569. fn = utils.map;
  570. } else if (first) {
  571. fn = utils.find;
  572. } else {
  573. fn = utils.filter;
  574. }
  575. return fn(items, query);
  576. };
  577. score = function(items, query, getter) {
  578. return runQuery(items, query, getter, false, true);
  579. };
  580. runQuery.build = buildQuery;
  581. runQuery.parse = parseQuery;
  582. runQuery.findOne = runQuery.first = findOne;
  583. runQuery.score = score;
  584. runQuery.tester = runQuery.testWith = makeTest;
  585. runQuery.getter = runQuery.pluckWith = utils.makeGetter;
  586. expose = function(_, mixin) {
  587. if (mixin == null) {
  588. mixin = true;
  589. }
  590. if (!_) {
  591. _ = underscoreReplacement();
  592. mixin = false;
  593. }
  594. createUtils(_);
  595. if (mixin) {
  596. _.mixin({
  597. query: runQuery,
  598. q: runQuery
  599. });
  600. }
  601. return runQuery;
  602. };
  603. if (root._) {
  604. return expose(root._);
  605. }
  606. module.exports = expose;