query-classes.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. import { TAPi18n } from '/imports/i18n';
  2. import {
  3. formatDateTime,
  4. formatDate,
  5. formatTime,
  6. getISOWeek,
  7. isValidDate,
  8. isBefore,
  9. isAfter,
  10. isSame,
  11. add,
  12. subtract,
  13. startOf,
  14. endOf,
  15. format,
  16. parseDate,
  17. now,
  18. createDate,
  19. fromNow,
  20. calendar
  21. } from '/imports/lib/dateUtils';
  22. import {
  23. OPERATOR_ASSIGNEE,
  24. OPERATOR_BOARD,
  25. OPERATOR_COMMENT,
  26. OPERATOR_CREATED_AT,
  27. OPERATOR_CREATOR,
  28. OPERATOR_DEBUG,
  29. OPERATOR_DUE,
  30. OPERATOR_HAS,
  31. OPERATOR_LABEL,
  32. OPERATOR_LIMIT,
  33. OPERATOR_LIST,
  34. OPERATOR_MEMBER,
  35. OPERATOR_MODIFIED_AT,
  36. OPERATOR_ORG,
  37. OPERATOR_SORT,
  38. OPERATOR_STATUS,
  39. OPERATOR_SWIMLANE,
  40. OPERATOR_TEAM,
  41. OPERATOR_UNKNOWN,
  42. OPERATOR_USER,
  43. ORDER_ASCENDING,
  44. ORDER_DESCENDING,
  45. PREDICATE_ALL,
  46. PREDICATE_ARCHIVED,
  47. PREDICATE_ASSIGNEES,
  48. PREDICATE_ATTACHMENT,
  49. PREDICATE_CHECKLIST,
  50. PREDICATE_CREATED_AT,
  51. PREDICATE_DESCRIPTION,
  52. PREDICATE_DUE_AT,
  53. PREDICATE_END_AT,
  54. PREDICATE_ENDED,
  55. PREDICATE_MEMBERS,
  56. PREDICATE_MODIFIED_AT,
  57. PREDICATE_MONTH,
  58. PREDICATE_OPEN,
  59. PREDICATE_OVERDUE,
  60. PREDICATE_PRIVATE,
  61. PREDICATE_PROJECTION,
  62. PREDICATE_PUBLIC,
  63. PREDICATE_QUARTER,
  64. PREDICATE_SELECTOR,
  65. PREDICATE_START_AT,
  66. PREDICATE_WEEK,
  67. PREDICATE_YEAR,
  68. } from './search-const';
  69. import Boards from '../models/boards';
  70. export class QueryDebug {
  71. predicate = null;
  72. constructor(predicate) {
  73. if (predicate) {
  74. this.set(predicate)
  75. }
  76. }
  77. get() {
  78. return this.predicate;
  79. }
  80. set(predicate) {
  81. if ([PREDICATE_ALL, PREDICATE_SELECTOR, PREDICATE_PROJECTION].includes(
  82. predicate
  83. )) {
  84. this.predicate = predicate;
  85. } else {
  86. this.predicate = null;
  87. }
  88. }
  89. show() {
  90. return (this.predicate !== null);
  91. }
  92. showAll() {
  93. return (this.predicate === PREDICATE_ALL);
  94. }
  95. showSelector() {
  96. return (this.predicate === PREDICATE_ALL || this.predicate === PREDICATE_SELECTOR);
  97. }
  98. showProjection() {
  99. return (this.predicate === PREDICATE_ALL || this.predicate === PREDICATE_PROJECTION);
  100. }
  101. }
  102. export class QueryParams {
  103. text = '';
  104. constructor(params = {}, text = '') {
  105. this.params = params;
  106. this.text = text;
  107. }
  108. hasOperator(operator) {
  109. return (
  110. this.params[operator] !== undefined &&
  111. (this.params[operator].length === undefined ||
  112. this.params[operator].length > 0)
  113. );
  114. }
  115. addPredicate(operator, predicate) {
  116. if (!this.hasOperator(operator)) {
  117. this.params[operator] = [];
  118. }
  119. this.params[operator].push(predicate);
  120. }
  121. setPredicate(operator, predicate) {
  122. this.params[operator] = predicate;
  123. }
  124. getPredicate(operator) {
  125. if (this.hasOperator(operator)){
  126. if (typeof this.params[operator] === 'object') {
  127. return this.params[operator][0];
  128. } else {
  129. return this.params[operator];
  130. }
  131. }
  132. return null;
  133. }
  134. getPredicates(operator) {
  135. return this.params[operator];
  136. }
  137. getParams() {
  138. return this.params;
  139. }
  140. }
  141. export class QueryErrors {
  142. operatorTagMap = [
  143. [OPERATOR_BOARD, 'board-title-not-found'],
  144. [OPERATOR_SWIMLANE, 'swimlane-title-not-found'],
  145. [
  146. OPERATOR_LABEL,
  147. label => {
  148. if (Boards.labelColors().includes(label)) {
  149. return {
  150. tag: 'label-color-not-found',
  151. value: label,
  152. color: true,
  153. };
  154. } else {
  155. return {
  156. tag: 'label-not-found',
  157. value: label,
  158. color: false,
  159. };
  160. }
  161. },
  162. ],
  163. [OPERATOR_LIST, 'list-title-not-found'],
  164. [OPERATOR_COMMENT, 'comment-not-found'],
  165. [OPERATOR_USER, 'user-username-not-found'],
  166. [OPERATOR_ASSIGNEE, 'user-username-not-found'],
  167. [OPERATOR_MEMBER, 'user-username-not-found'],
  168. [OPERATOR_CREATOR, 'user-username-not-found'],
  169. [OPERATOR_ORG, 'org-name-not-found'],
  170. [OPERATOR_TEAM, 'team-name-not-found'],
  171. ];
  172. constructor() {
  173. this._errors = {};
  174. this.operatorTags = {};
  175. this.operatorTagMap.forEach(([operator, tag]) => {
  176. this.operatorTags[operator] = tag;
  177. });
  178. this.colorMap = Boards.colorMap();
  179. }
  180. addError(operator, error) {
  181. if (!this._errors[operator]) {
  182. this._errors[operator] = [];
  183. }
  184. this._errors[operator].push(error);
  185. }
  186. addNotFound(operator, value) {
  187. if (typeof this.operatorTags[operator] === 'function') {
  188. this.addError(operator, this.operatorTags[operator](value));
  189. } else {
  190. this.addError(operator, { tag: this.operatorTags[operator], value });
  191. }
  192. }
  193. hasErrors() {
  194. return Object.entries(this._errors).length > 0;
  195. }
  196. errors() {
  197. const errs = [];
  198. // eslint-disable-next-line no-unused-vars
  199. Object.entries(this._errors).forEach(([, errors]) => {
  200. errors.forEach(err => {
  201. errs.push(err);
  202. });
  203. });
  204. return errs;
  205. }
  206. errorMessages() {
  207. const messages = [];
  208. // eslint-disable-next-line no-unused-vars
  209. Object.entries(this._errors).forEach(([, errors]) => {
  210. errors.forEach(err => {
  211. messages.push(TAPi18n.__(err.tag, err.value));
  212. });
  213. });
  214. return messages;
  215. }
  216. }
  217. export class Query {
  218. selector = {};
  219. projection = {};
  220. constructor(selector, projection) {
  221. this._errors = new QueryErrors();
  222. this.queryParams = new QueryParams();
  223. this.colorMap = Boards.colorMap();
  224. if (selector) {
  225. this.selector = selector;
  226. }
  227. if (projection) {
  228. this.projection = projection;
  229. }
  230. }
  231. hasErrors() {
  232. return this._errors.hasErrors();
  233. }
  234. errors() {
  235. return this._errors.errors();
  236. }
  237. addError(operator, error) {
  238. this._errors.addError(operator, error)
  239. }
  240. errorMessages() {
  241. return this._errors.errorMessages();
  242. }
  243. getQueryParams() {
  244. return this.queryParams;
  245. }
  246. setQueryParams(queryParams) {
  247. this.queryParams = queryParams;
  248. }
  249. addPredicate(operator, predicate) {
  250. this.queryParams.addPredicate(operator, predicate);
  251. }
  252. buildParams(queryText) {
  253. this.queryParams = new QueryParams();
  254. queryText = queryText.trim();
  255. // eslint-disable-next-line no-console
  256. //console.log('query:', query);
  257. if (!queryText) {
  258. return;
  259. }
  260. const reOperator1 = new RegExp(
  261. '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<value>[\\p{Letter}\\p{Mark}]+)(\\s+|$)',
  262. 'iu',
  263. );
  264. const reOperator2 = new RegExp(
  265. '^((?<operator>[\\p{Letter}\\p{Mark}]+):|(?<abbrev>[#@]))(?<quote>["\']*)(?<value>.*?)\\k<quote>(\\s+|$)',
  266. 'iu',
  267. );
  268. const reText = new RegExp('^(?<text>\\S+)(\\s+|$)', 'u');
  269. const reQuotedText = new RegExp(
  270. '^(?<quote>["\'])(?<text>.*?)\\k<quote>(\\s+|$)',
  271. 'u',
  272. );
  273. const reNegatedOperator = new RegExp('^-(?<operator>.*)$');
  274. const operators = {
  275. 'operator-board': OPERATOR_BOARD,
  276. 'operator-board-abbrev': OPERATOR_BOARD,
  277. 'operator-swimlane': OPERATOR_SWIMLANE,
  278. 'operator-swimlane-abbrev': OPERATOR_SWIMLANE,
  279. 'operator-list': OPERATOR_LIST,
  280. 'operator-list-abbrev': OPERATOR_LIST,
  281. 'operator-label': OPERATOR_LABEL,
  282. 'operator-label-abbrev': OPERATOR_LABEL,
  283. 'operator-user': OPERATOR_USER,
  284. 'operator-user-abbrev': OPERATOR_USER,
  285. 'operator-member': OPERATOR_MEMBER,
  286. 'operator-member-abbrev': OPERATOR_MEMBER,
  287. 'operator-assignee': OPERATOR_ASSIGNEE,
  288. 'operator-creator': OPERATOR_CREATOR,
  289. 'operator-assignee-abbrev': OPERATOR_ASSIGNEE,
  290. 'operator-status': OPERATOR_STATUS,
  291. 'operator-due': OPERATOR_DUE,
  292. 'operator-created': OPERATOR_CREATED_AT,
  293. 'operator-modified': OPERATOR_MODIFIED_AT,
  294. 'operator-comment': OPERATOR_COMMENT,
  295. 'operator-has': OPERATOR_HAS,
  296. 'operator-sort': OPERATOR_SORT,
  297. 'operator-limit': OPERATOR_LIMIT,
  298. 'operator-debug': OPERATOR_DEBUG,
  299. 'operator-org': OPERATOR_ORG,
  300. 'operator-team': OPERATOR_TEAM,
  301. };
  302. const predicates = {
  303. durations: {
  304. 'predicate-week': PREDICATE_WEEK,
  305. 'predicate-month': PREDICATE_MONTH,
  306. 'predicate-quarter': PREDICATE_QUARTER,
  307. 'predicate-year': PREDICATE_YEAR,
  308. },
  309. };
  310. predicates[OPERATOR_DUE] = {
  311. 'predicate-overdue': PREDICATE_OVERDUE,
  312. };
  313. predicates[OPERATOR_STATUS] = {
  314. 'predicate-archived': PREDICATE_ARCHIVED,
  315. 'predicate-all': PREDICATE_ALL,
  316. 'predicate-open': PREDICATE_OPEN,
  317. 'predicate-ended': PREDICATE_ENDED,
  318. 'predicate-public': PREDICATE_PUBLIC,
  319. 'predicate-private': PREDICATE_PRIVATE,
  320. };
  321. predicates[OPERATOR_SORT] = {
  322. 'predicate-due': PREDICATE_DUE_AT,
  323. 'predicate-created': PREDICATE_CREATED_AT,
  324. 'predicate-modified': PREDICATE_MODIFIED_AT,
  325. };
  326. predicates[OPERATOR_HAS] = {
  327. 'predicate-description': PREDICATE_DESCRIPTION,
  328. 'predicate-checklist': PREDICATE_CHECKLIST,
  329. 'predicate-attachment': PREDICATE_ATTACHMENT,
  330. 'predicate-start': PREDICATE_START_AT,
  331. 'predicate-end': PREDICATE_END_AT,
  332. 'predicate-due': PREDICATE_DUE_AT,
  333. 'predicate-assignee': PREDICATE_ASSIGNEES,
  334. 'predicate-member': PREDICATE_MEMBERS,
  335. };
  336. predicates[OPERATOR_DEBUG] = {
  337. 'predicate-all': PREDICATE_ALL,
  338. 'predicate-selector': PREDICATE_SELECTOR,
  339. 'predicate-projection': PREDICATE_PROJECTION,
  340. };
  341. const predicateTranslations = {};
  342. Object.entries(predicates).forEach(([category, catPreds]) => {
  343. predicateTranslations[category] = {};
  344. Object.entries(catPreds).forEach(([tag, value]) => {
  345. predicateTranslations[category][TAPi18n.__(tag)] = value;
  346. });
  347. });
  348. // eslint-disable-next-line no-console
  349. // console.log('predicateTranslations:', predicateTranslations);
  350. const operatorMap = {};
  351. Object.entries(operators).forEach(([key, value]) => {
  352. operatorMap[TAPi18n.__(key).toLowerCase()] = value;
  353. });
  354. // eslint-disable-next-line no-console
  355. // console.log('operatorMap:', operatorMap);
  356. let text = '';
  357. while (queryText) {
  358. let m = queryText.match(reOperator1);
  359. if (!m) {
  360. m = queryText.match(reOperator2);
  361. if (m) {
  362. queryText = queryText.replace(reOperator2, '');
  363. }
  364. } else {
  365. queryText = queryText.replace(reOperator1, '');
  366. }
  367. if (m) {
  368. let op;
  369. if (m.groups.operator) {
  370. op = m.groups.operator.toLowerCase();
  371. } else {
  372. op = m.groups.abbrev.toLowerCase();
  373. }
  374. // eslint-disable-next-line no-prototype-builtins
  375. if (operatorMap.hasOwnProperty(op)) {
  376. const operator = operatorMap[op];
  377. let value = m.groups.value;
  378. if (operator === OPERATOR_LABEL) {
  379. if (value in this.colorMap) {
  380. value = this.colorMap[value];
  381. // console.log('found color:', value);
  382. }
  383. } else if (
  384. [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].includes(
  385. operator,
  386. )
  387. ) {
  388. const days = parseInt(value, 10);
  389. let duration = null;
  390. if (isNaN(days)) {
  391. // duration was specified as text
  392. if (predicateTranslations.durations[value]) {
  393. duration = predicateTranslations.durations[value];
  394. let date = null;
  395. switch (duration) {
  396. case PREDICATE_WEEK:
  397. // eslint-disable-next-line no-case-declarations
  398. const week = getISOWeek(now());
  399. if (week === 52) {
  400. date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
  401. } else {
  402. // Calculate the date for the next week
  403. const currentDate = now();
  404. const daysToAdd = (week + 1) * 7 - (currentDate.getDay() + 6) % 7;
  405. date = add(currentDate, daysToAdd, 'days');
  406. }
  407. break;
  408. case PREDICATE_MONTH:
  409. // eslint-disable-next-line no-case-declarations
  410. const month = now().getMonth();
  411. // .getMonth() is zero indexed
  412. if (month === 11) {
  413. date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
  414. } else {
  415. date = new Date(now().getFullYear(), month + 1, 1); // First day of next month
  416. }
  417. break;
  418. case PREDICATE_QUARTER:
  419. // eslint-disable-next-line no-case-declarations
  420. const quarter = Math.floor(now().getMonth() / 3) + 1;
  421. if (quarter === 4) {
  422. date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
  423. } else {
  424. const nextQuarterMonth = quarter * 3; // 3, 6, 9 for quarters 2, 3, 4
  425. date = new Date(now().getFullYear(), nextQuarterMonth, 1); // First day of next quarter
  426. }
  427. break;
  428. case PREDICATE_YEAR:
  429. date = new Date(now().getFullYear() + 1, 0, 1); // January 1st of next year
  430. break;
  431. }
  432. if (date) {
  433. value = {
  434. operator: '$lt',
  435. value: formatDate(date),
  436. };
  437. }
  438. } else if (
  439. operator === OPERATOR_DUE &&
  440. value === PREDICATE_OVERDUE
  441. ) {
  442. value = {
  443. operator: '$lt',
  444. value: formatDate(now()),
  445. };
  446. } else {
  447. this.addError(OPERATOR_DUE, {
  448. tag: 'operator-number-expected',
  449. value: { operator: op, value },
  450. });
  451. continue;
  452. }
  453. } else if (operator === OPERATOR_DUE) {
  454. value = {
  455. operator: '$lt',
  456. value: formatDate(add(add(now(), 1, 'days'), days + 1, duration ? duration : 'days')),
  457. };
  458. } else {
  459. value = {
  460. operator: '$gte',
  461. value: formatDate(subtract(now(), days, duration ? duration : 'days')),
  462. };
  463. }
  464. } else if (operator === OPERATOR_SORT) {
  465. let negated = false;
  466. const m = value.match(reNegatedOperator);
  467. if (m) {
  468. value = m.groups.operator;
  469. negated = true;
  470. }
  471. if (!predicateTranslations[OPERATOR_SORT][value]) {
  472. this.addError(OPERATOR_SORT, {
  473. tag: 'operator-sort-invalid',
  474. value,
  475. });
  476. continue;
  477. } else {
  478. value = {
  479. name: predicateTranslations[OPERATOR_SORT][value],
  480. order: negated ? ORDER_DESCENDING : ORDER_ASCENDING,
  481. };
  482. }
  483. } else if (operator === OPERATOR_STATUS) {
  484. if (!predicateTranslations[OPERATOR_STATUS][value]) {
  485. this.addError(OPERATOR_STATUS, {
  486. tag: 'operator-status-invalid',
  487. value,
  488. });
  489. continue;
  490. } else {
  491. value = predicateTranslations[OPERATOR_STATUS][value];
  492. }
  493. } else if (operator === OPERATOR_HAS) {
  494. let negated = false;
  495. const m = value.match(reNegatedOperator);
  496. if (m) {
  497. value = m.groups.operator;
  498. negated = true;
  499. }
  500. if (!predicateTranslations[OPERATOR_HAS][value]) {
  501. this.addError(OPERATOR_HAS, {
  502. tag: 'operator-has-invalid',
  503. value,
  504. });
  505. continue;
  506. } else {
  507. value = {
  508. field: predicateTranslations[OPERATOR_HAS][value],
  509. exists: !negated,
  510. };
  511. }
  512. } else if (operator === OPERATOR_LIMIT) {
  513. const limit = parseInt(value, 10);
  514. if (isNaN(limit) || limit < 0) {
  515. this.addError(OPERATOR_LIMIT, {
  516. tag: 'operator-limit-invalid',
  517. value,
  518. });
  519. continue;
  520. } else if (limit == 0) {
  521. // no limit
  522. continue;
  523. } else {
  524. value = limit;
  525. }
  526. } else if (operator === OPERATOR_DEBUG) {
  527. if (!predicateTranslations[OPERATOR_DEBUG][value]) {
  528. this.addError(OPERATOR_DEBUG, {
  529. tag: 'operator-debug-invalid',
  530. value,
  531. });
  532. continue;
  533. } else {
  534. value = predicateTranslations[OPERATOR_DEBUG][value];
  535. }
  536. }
  537. this.queryParams.addPredicate(operator, value);
  538. } else {
  539. this.addError(OPERATOR_UNKNOWN, {
  540. tag: 'operator-unknown-error',
  541. value: op,
  542. });
  543. }
  544. continue;
  545. }
  546. m = queryText.match(reQuotedText);
  547. if (!m) {
  548. m = queryText.match(reText);
  549. if (m) {
  550. queryText = queryText.replace(reText, '');
  551. }
  552. } else {
  553. queryText = queryText.replace(reQuotedText, '');
  554. }
  555. if (m) {
  556. text += (text ? ' ' : '') + m.groups.text;
  557. }
  558. }
  559. this.queryParams.text = text;
  560. // eslint-disable-next-line no-console
  561. if (this.queryParams.hasOperator(OPERATOR_DEBUG)) {
  562. // eslint-disable-next-line no-console
  563. console.log('text:', this.queryParams.text);
  564. console.log('queryParams:', this.queryParams);
  565. }
  566. }
  567. }