query-classes.js 17 KB

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