OfflineQuery.js 17 KB

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