OfflineQuery.js 18 KB

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