OfflineQuery.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. "use strict";
  2. const equalObjects = require('./equals').default;
  3. const decode = require('./decode').default;
  4. const ParseError = require('./ParseError').default;
  5. const ParsePolygon = require('./ParsePolygon').default;
  6. const ParseGeoPoint = require('./ParseGeoPoint').default;
  7. /**
  8. * contains -- Determines if an object is contained in a list with special handling for Parse pointers.
  9. *
  10. * @param haystack
  11. * @param needle
  12. * @private
  13. * @returns {boolean}
  14. */
  15. function contains(haystack, needle) {
  16. if (needle && needle.__type && (needle.__type === 'Pointer' || needle.__type === 'Object')) {
  17. for (const i in haystack) {
  18. const ptr = haystack[i];
  19. if (typeof ptr === 'string' && ptr === needle.objectId) {
  20. return true;
  21. }
  22. if (ptr.className === needle.className && ptr.objectId === needle.objectId) {
  23. return true;
  24. }
  25. }
  26. return false;
  27. }
  28. return haystack.indexOf(needle) > -1;
  29. }
  30. function transformObject(object) {
  31. if (object._toFullJSON) {
  32. return object._toFullJSON();
  33. }
  34. return object;
  35. }
  36. /**
  37. * matchesQuery -- Determines if an object would be returned by a Parse Query
  38. * It's a lightweight, where-clause only implementation of a full query engine.
  39. * Since we find queries that match objects, rather than objects that match
  40. * queries, we can avoid building a full-blown query tool.
  41. *
  42. * @param className
  43. * @param object
  44. * @param objects
  45. * @param query
  46. * @private
  47. * @returns {boolean}
  48. */
  49. function matchesQuery(className, object, objects, query) {
  50. if (object.className !== className) {
  51. return false;
  52. }
  53. let obj = object;
  54. let q = query;
  55. if (object.toJSON) {
  56. obj = object.toJSON();
  57. }
  58. if (query.toJSON) {
  59. q = query.toJSON().where;
  60. }
  61. obj.className = className;
  62. for (const field in q) {
  63. if (!matchesKeyConstraints(className, obj, objects, field, q[field])) {
  64. return false;
  65. }
  66. }
  67. return true;
  68. }
  69. function equalObjectsGeneric(obj, compareTo, eqlFn) {
  70. if (Array.isArray(obj)) {
  71. for (let i = 0; i < obj.length; i++) {
  72. if (eqlFn(obj[i], compareTo)) {
  73. return true;
  74. }
  75. }
  76. return false;
  77. }
  78. return eqlFn(obj, compareTo);
  79. }
  80. /**
  81. * @typedef RelativeTimeToDateResult
  82. * @property {string} status The conversion status, `error` if conversion failed or
  83. * `success` if conversion succeeded.
  84. * @property {string} info The error message if conversion failed, or the relative
  85. * time indication (`past`, `present`, `future`) if conversion succeeded.
  86. * @property {Date|undefined} result The converted date, or `undefined` if conversion
  87. * failed.
  88. */
  89. /**
  90. * Converts human readable relative date string, for example, 'in 10 days' to a date
  91. * relative to now.
  92. *
  93. * @param {string} text The text to convert.
  94. * @param {Date} [now=new Date()] The date from which add or subtract. Default is now.
  95. * @returns {RelativeTimeToDateResult}
  96. */
  97. function relativeTimeToDate(text, now = new Date()) {
  98. text = text.toLowerCase();
  99. let parts = text.split(' '); // Filter out whitespace
  100. parts = parts.filter(part => part !== '');
  101. const future = parts[0] === 'in';
  102. const past = parts[parts.length - 1] === 'ago';
  103. if (!future && !past && text !== 'now') {
  104. return {
  105. status: 'error',
  106. info: "Time should either start with 'in' or end with 'ago'"
  107. };
  108. }
  109. if (future && past) {
  110. return {
  111. status: 'error',
  112. info: "Time cannot have both 'in' and 'ago'"
  113. };
  114. } // strip the 'ago' or 'in'
  115. if (future) {
  116. parts = parts.slice(1);
  117. } else {
  118. // past
  119. parts = parts.slice(0, parts.length - 1);
  120. }
  121. if (parts.length % 2 !== 0 && text !== 'now') {
  122. return {
  123. status: 'error',
  124. info: 'Invalid time string. Dangling unit or number.'
  125. };
  126. }
  127. const pairs = [];
  128. while (parts.length) {
  129. pairs.push([parts.shift(), parts.shift()]);
  130. }
  131. let seconds = 0;
  132. for (const [num, interval] of pairs) {
  133. const val = Number(num);
  134. if (!Number.isInteger(val)) {
  135. return {
  136. status: 'error',
  137. info: `'${num}' is not an integer.`
  138. };
  139. }
  140. switch (interval) {
  141. case 'yr':
  142. case 'yrs':
  143. case 'year':
  144. case 'years':
  145. seconds += val * 31536000; // 365 * 24 * 60 * 60
  146. break;
  147. case 'wk':
  148. case 'wks':
  149. case 'week':
  150. case 'weeks':
  151. seconds += val * 604800; // 7 * 24 * 60 * 60
  152. break;
  153. case 'd':
  154. case 'day':
  155. case 'days':
  156. seconds += val * 86400; // 24 * 60 * 60
  157. break;
  158. case 'hr':
  159. case 'hrs':
  160. case 'hour':
  161. case 'hours':
  162. seconds += val * 3600; // 60 * 60
  163. break;
  164. case 'min':
  165. case 'mins':
  166. case 'minute':
  167. case 'minutes':
  168. seconds += val * 60;
  169. break;
  170. case 'sec':
  171. case 'secs':
  172. case 'second':
  173. case 'seconds':
  174. seconds += val;
  175. break;
  176. default:
  177. return {
  178. status: 'error',
  179. info: `Invalid interval: '${interval}'`
  180. };
  181. }
  182. }
  183. const milliseconds = seconds * 1000;
  184. if (future) {
  185. return {
  186. status: 'success',
  187. info: 'future',
  188. result: new Date(now.valueOf() + milliseconds)
  189. };
  190. } else if (past) {
  191. return {
  192. status: 'success',
  193. info: 'past',
  194. result: new Date(now.valueOf() - milliseconds)
  195. };
  196. } else {
  197. return {
  198. status: 'success',
  199. info: 'present',
  200. result: new Date(now.valueOf())
  201. };
  202. }
  203. }
  204. /**
  205. * Determines whether an object matches a single key's constraints
  206. *
  207. * @param className
  208. * @param object
  209. * @param objects
  210. * @param key
  211. * @param constraints
  212. * @private
  213. * @returns {boolean}
  214. */
  215. function matchesKeyConstraints(className, object, objects, key, constraints) {
  216. if (constraints === null) {
  217. return false;
  218. }
  219. if (key.indexOf('.') >= 0) {
  220. // Key references a subobject
  221. const keyComponents = key.split('.');
  222. const subObjectKey = keyComponents[0];
  223. const keyRemainder = keyComponents.slice(1).join('.');
  224. return matchesKeyConstraints(className, object[subObjectKey] || {}, objects, keyRemainder, constraints);
  225. }
  226. let i;
  227. if (key === '$or') {
  228. for (i = 0; i < constraints.length; i++) {
  229. if (matchesQuery(className, object, objects, constraints[i])) {
  230. return true;
  231. }
  232. }
  233. return false;
  234. }
  235. if (key === '$and') {
  236. for (i = 0; i < constraints.length; i++) {
  237. if (!matchesQuery(className, object, objects, constraints[i])) {
  238. return false;
  239. }
  240. }
  241. return true;
  242. }
  243. if (key === '$nor') {
  244. for (i = 0; i < constraints.length; i++) {
  245. if (matchesQuery(className, object, objects, constraints[i])) {
  246. return false;
  247. }
  248. }
  249. return true;
  250. }
  251. if (key === '$relatedTo') {
  252. // Bail! We can't handle relational queries locally
  253. return false;
  254. }
  255. if (!/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) {
  256. throw new ParseError(ParseError.INVALID_KEY_NAME, `Invalid Key: ${key}`);
  257. } // Equality (or Array contains) cases
  258. if (typeof constraints !== 'object') {
  259. if (Array.isArray(object[key])) {
  260. return object[key].indexOf(constraints) > -1;
  261. }
  262. return object[key] === constraints;
  263. }
  264. let compareTo;
  265. if (constraints.__type) {
  266. if (constraints.__type === 'Pointer') {
  267. return equalObjectsGeneric(object[key], constraints, function (obj, ptr) {
  268. return typeof obj !== 'undefined' && ptr.className === obj.className && ptr.objectId === obj.objectId;
  269. });
  270. }
  271. return equalObjectsGeneric(decode(object[key]), decode(constraints), equalObjects);
  272. } // More complex cases
  273. for (const condition in constraints) {
  274. compareTo = constraints[condition];
  275. if (compareTo.__type) {
  276. compareTo = decode(compareTo);
  277. } // is it a $relativeTime? convert to date
  278. if (compareTo['$relativeTime']) {
  279. const parserResult = relativeTimeToDate(compareTo['$relativeTime']);
  280. if (parserResult.status !== 'success') {
  281. throw new ParseError(ParseError.INVALID_JSON, `bad $relativeTime (${key}) value. ${parserResult.info}`);
  282. }
  283. compareTo = parserResult.result;
  284. } // Compare Date Object or Date String
  285. if (toString.call(compareTo) === '[object Date]' || typeof compareTo === 'string' && new Date(compareTo) !== 'Invalid Date' && !isNaN(new Date(compareTo))) {
  286. object[key] = new Date(object[key].iso ? object[key].iso : object[key]);
  287. }
  288. switch (condition) {
  289. case '$lt':
  290. if (object[key] >= compareTo) {
  291. return false;
  292. }
  293. break;
  294. case '$lte':
  295. if (object[key] > compareTo) {
  296. return false;
  297. }
  298. break;
  299. case '$gt':
  300. if (object[key] <= compareTo) {
  301. return false;
  302. }
  303. break;
  304. case '$gte':
  305. if (object[key] < compareTo) {
  306. return false;
  307. }
  308. break;
  309. case '$ne':
  310. if (equalObjects(object[key], compareTo)) {
  311. return false;
  312. }
  313. break;
  314. case '$in':
  315. if (!contains(compareTo, object[key])) {
  316. return false;
  317. }
  318. break;
  319. case '$nin':
  320. if (contains(compareTo, object[key])) {
  321. return false;
  322. }
  323. break;
  324. case '$all':
  325. for (i = 0; i < compareTo.length; i++) {
  326. if (object[key].indexOf(compareTo[i]) < 0) {
  327. return false;
  328. }
  329. }
  330. break;
  331. case '$exists':
  332. {
  333. const propertyExists = typeof object[key] !== 'undefined';
  334. const existenceIsRequired = constraints['$exists'];
  335. if (typeof constraints['$exists'] !== 'boolean') {
  336. // The SDK will never submit a non-boolean for $exists, but if someone
  337. // tries to submit a non-boolean for $exits outside the SDKs, just ignore it.
  338. break;
  339. }
  340. if (!propertyExists && existenceIsRequired || propertyExists && !existenceIsRequired) {
  341. return false;
  342. }
  343. break;
  344. }
  345. case '$regex':
  346. {
  347. if (typeof compareTo === 'object') {
  348. return compareTo.test(object[key]);
  349. } // JS doesn't support perl-style escaping
  350. let expString = '';
  351. let escapeEnd = -2;
  352. let escapeStart = compareTo.indexOf('\\Q');
  353. while (escapeStart > -1) {
  354. // Add the unescaped portion
  355. expString += compareTo.substring(escapeEnd + 2, escapeStart);
  356. escapeEnd = compareTo.indexOf('\\E', escapeStart);
  357. if (escapeEnd > -1) {
  358. expString += compareTo.substring(escapeStart + 2, escapeEnd).replace(/\\\\\\\\E/g, '\\E').replace(/\W/g, '\\$&');
  359. }
  360. escapeStart = compareTo.indexOf('\\Q', escapeEnd);
  361. }
  362. expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2));
  363. let modifiers = constraints.$options || '';
  364. modifiers = modifiers.replace('x', '').replace('s', ''); // Parse Server / Mongo support x and s modifiers but JS RegExp doesn't
  365. const exp = new RegExp(expString, modifiers);
  366. if (!exp.test(object[key])) {
  367. return false;
  368. }
  369. break;
  370. }
  371. case '$nearSphere':
  372. {
  373. if (!compareTo || !object[key]) {
  374. return false;
  375. }
  376. const distance = compareTo.radiansTo(object[key]);
  377. const max = constraints.$maxDistance || Infinity;
  378. return distance <= max;
  379. }
  380. case '$within':
  381. {
  382. if (!compareTo || !object[key]) {
  383. return false;
  384. }
  385. const southWest = compareTo.$box[0];
  386. const northEast = compareTo.$box[1];
  387. if (southWest.latitude > northEast.latitude || southWest.longitude > northEast.longitude) {
  388. // Invalid box, crosses the date line
  389. return false;
  390. }
  391. return object[key].latitude > southWest.latitude && object[key].latitude < northEast.latitude && object[key].longitude > southWest.longitude && object[key].longitude < northEast.longitude;
  392. }
  393. case '$options':
  394. // Not a query type, but a way to add options to $regex. Ignore and
  395. // avoid the default
  396. break;
  397. case '$maxDistance':
  398. // Not a query type, but a way to add a cap to $nearSphere. Ignore and
  399. // avoid the default
  400. break;
  401. case '$select':
  402. {
  403. const subQueryObjects = objects.filter((obj, index, arr) => {
  404. return matchesQuery(compareTo.query.className, obj, arr, compareTo.query.where);
  405. });
  406. for (let i = 0; i < subQueryObjects.length; i += 1) {
  407. const subObject = transformObject(subQueryObjects[i]);
  408. return equalObjects(object[key], subObject[compareTo.key]);
  409. }
  410. return false;
  411. }
  412. case '$dontSelect':
  413. {
  414. const subQueryObjects = objects.filter((obj, index, arr) => {
  415. return matchesQuery(compareTo.query.className, obj, arr, compareTo.query.where);
  416. });
  417. for (let i = 0; i < subQueryObjects.length; i += 1) {
  418. const subObject = transformObject(subQueryObjects[i]);
  419. return !equalObjects(object[key], subObject[compareTo.key]);
  420. }
  421. return false;
  422. }
  423. case '$inQuery':
  424. {
  425. const subQueryObjects = objects.filter((obj, index, arr) => {
  426. return matchesQuery(compareTo.className, obj, arr, compareTo.where);
  427. });
  428. for (let i = 0; i < subQueryObjects.length; i += 1) {
  429. const subObject = transformObject(subQueryObjects[i]);
  430. if (object[key].className === subObject.className && object[key].objectId === subObject.objectId) {
  431. return true;
  432. }
  433. }
  434. return false;
  435. }
  436. case '$notInQuery':
  437. {
  438. const subQueryObjects = objects.filter((obj, index, arr) => {
  439. return matchesQuery(compareTo.className, obj, arr, compareTo.where);
  440. });
  441. for (let i = 0; i < subQueryObjects.length; i += 1) {
  442. const subObject = transformObject(subQueryObjects[i]);
  443. if (object[key].className === subObject.className && object[key].objectId === subObject.objectId) {
  444. return false;
  445. }
  446. }
  447. return true;
  448. }
  449. case '$containedBy':
  450. {
  451. for (const value of object[key]) {
  452. if (!contains(compareTo, value)) {
  453. return false;
  454. }
  455. }
  456. return true;
  457. }
  458. case '$geoWithin':
  459. {
  460. if (compareTo.$polygon) {
  461. const points = compareTo.$polygon.map(geoPoint => [geoPoint.latitude, geoPoint.longitude]);
  462. const polygon = new ParsePolygon(points);
  463. return polygon.containsPoint(object[key]);
  464. }
  465. if (compareTo.$centerSphere) {
  466. const [WGS84Point, maxDistance] = compareTo.$centerSphere;
  467. const centerPoint = new ParseGeoPoint({
  468. latitude: WGS84Point[1],
  469. longitude: WGS84Point[0]
  470. });
  471. const point = new ParseGeoPoint(object[key]);
  472. const distance = point.radiansTo(centerPoint);
  473. return distance <= maxDistance;
  474. }
  475. break;
  476. }
  477. case '$geoIntersects':
  478. {
  479. const polygon = new ParsePolygon(object[key].coordinates);
  480. const point = new ParseGeoPoint(compareTo.$point);
  481. return polygon.containsPoint(point);
  482. }
  483. default:
  484. return false;
  485. }
  486. }
  487. return true;
  488. }
  489. function validateQuery(query
  490. /*: any*/
  491. ) {
  492. let q = query;
  493. if (query.toJSON) {
  494. q = query.toJSON().where;
  495. }
  496. const specialQuerykeys = ['$and', '$or', '$nor', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count'];
  497. Object.keys(q).forEach(key => {
  498. if (q && q[key] && q[key].$regex) {
  499. if (typeof q[key].$options === 'string') {
  500. if (!q[key].$options.match(/^[imxs]+$/)) {
  501. throw new ParseError(ParseError.INVALID_QUERY, `Bad $options value for query: ${q[key].$options}`);
  502. }
  503. }
  504. }
  505. if (specialQuerykeys.indexOf(key) < 0 && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) {
  506. throw new ParseError(ParseError.INVALID_KEY_NAME, `Invalid key name: ${key}`);
  507. }
  508. });
  509. }
  510. const OfflineQuery = {
  511. matchesQuery: matchesQuery,
  512. validateQuery: validateQuery
  513. };
  514. module.exports = OfflineQuery;