query-classes.js 17 KB

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