OfflineQuery.js 16 KB

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