query-classes.js 16 KB

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