cards.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  1. import moment from 'moment';
  2. import Users from '../../models/users';
  3. import Boards from '../../models/boards';
  4. import Lists from '../../models/lists';
  5. import Swimlanes from '../../models/swimlanes';
  6. import Cards from '../../models/cards';
  7. import CardComments from '../../models/cardComments';
  8. import Attachments from '../../models/attachments';
  9. import Checklists from '../../models/checklists';
  10. import ChecklistItems from '../../models/checklistItems';
  11. import SessionData from '../../models/usersessiondata';
  12. import CustomFields from '../../models/customFields';
  13. import {
  14. DEFAULT_LIMIT,
  15. OPERATOR_ASSIGNEE,
  16. OPERATOR_BOARD,
  17. OPERATOR_COMMENT,
  18. OPERATOR_CREATED_AT,
  19. OPERATOR_CREATOR,
  20. OPERATOR_DUE,
  21. OPERATOR_HAS,
  22. OPERATOR_LABEL,
  23. OPERATOR_LIMIT,
  24. OPERATOR_LIST,
  25. OPERATOR_MEMBER,
  26. OPERATOR_MODIFIED_AT,
  27. OPERATOR_SORT,
  28. OPERATOR_STATUS,
  29. OPERATOR_SWIMLANE,
  30. OPERATOR_USER,
  31. ORDER_ASCENDING,
  32. PREDICATE_ALL,
  33. PREDICATE_ARCHIVED,
  34. PREDICATE_ASSIGNEES,
  35. PREDICATE_ATTACHMENT,
  36. PREDICATE_CHECKLIST,
  37. PREDICATE_CREATED_AT,
  38. PREDICATE_DESCRIPTION,
  39. PREDICATE_DUE_AT,
  40. PREDICATE_END_AT,
  41. PREDICATE_ENDED,
  42. PREDICATE_MEMBERS,
  43. PREDICATE_MODIFIED_AT,
  44. PREDICATE_PRIVATE,
  45. PREDICATE_PUBLIC,
  46. PREDICATE_START_AT,
  47. PREDICATE_SYSTEM,
  48. } from '/config/search-const';
  49. import { QueryErrors, QueryParams, Query } from '/config/query-classes';
  50. const escapeForRegex = require('escape-string-regexp');
  51. Meteor.publish('card', cardId => {
  52. check(cardId, String);
  53. return Cards.find({ _id: cardId });
  54. });
  55. Meteor.publish('myCards', function(sessionId) {
  56. const queryParams = new QueryParams();
  57. queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
  58. return findCards(sessionId, buildQuery(queryParams));
  59. });
  60. // Meteor.publish('dueCards', function(sessionId, allUsers = false) {
  61. // check(sessionId, String);
  62. // check(allUsers, Boolean);
  63. //
  64. // // eslint-disable-next-line no-console
  65. // // console.log('all users:', allUsers);
  66. //
  67. // const queryParams = {
  68. // has: [{ field: 'dueAt', exists: true }],
  69. // limit: 25,
  70. // skip: 0,
  71. // sort: { name: 'dueAt', order: 'des' },
  72. // };
  73. //
  74. // if (!allUsers) {
  75. // queryParams.users = [Meteor.user().username];
  76. // }
  77. //
  78. // return buildQuery(sessionId, queryParams);
  79. // });
  80. Meteor.publish('globalSearch', function(sessionId, params, text) {
  81. check(sessionId, String);
  82. check(params, Object);
  83. check(text, String);
  84. // eslint-disable-next-line no-console
  85. // console.log('queryParams:', params);
  86. return findCards(sessionId, buildQuery(new QueryParams(params, text)));
  87. });
  88. function buildSelector(queryParams) {
  89. const userId = Meteor.userId();
  90. const errors = new QueryErrors();
  91. let selector = {};
  92. // eslint-disable-next-line no-console
  93. console.log('queryParams:', queryParams);
  94. if (queryParams.selector) {
  95. selector = queryParams.selector;
  96. } else {
  97. const boardsSelector = {};
  98. let archived = false;
  99. let endAt = null;
  100. if (queryParams.hasOperator(OPERATOR_STATUS)) {
  101. queryParams.getPredicates(OPERATOR_STATUS).forEach(status => {
  102. if (status === PREDICATE_ARCHIVED) {
  103. archived = true;
  104. } else if (status === PREDICATE_ALL) {
  105. archived = null;
  106. } else if (status === PREDICATE_ENDED) {
  107. endAt = { $nin: [null, ''] };
  108. } else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) {
  109. boardsSelector.permission = status;
  110. }
  111. });
  112. }
  113. selector = {
  114. type: 'cardType-card',
  115. // boardId: { $in: Boards.userBoardIds(userId) },
  116. $and: [],
  117. };
  118. if (archived !== null) {
  119. if (archived) {
  120. selector.boardId = {
  121. $in: Boards.userBoardIds(userId, null, boardsSelector),
  122. };
  123. selector.$and.push({
  124. $or: [
  125. {
  126. boardId: {
  127. $in: Boards.userBoardIds(userId, archived, boardsSelector),
  128. },
  129. },
  130. { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
  131. { listId: { $in: Lists.archivedListIds() } },
  132. { archived: true },
  133. ],
  134. });
  135. } else {
  136. selector.boardId = {
  137. $in: Boards.userBoardIds(userId, false, boardsSelector),
  138. };
  139. selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
  140. selector.listId = { $nin: Lists.archivedListIds() };
  141. selector.archived = false;
  142. }
  143. } else {
  144. selector.boardId = {
  145. $in: Boards.userBoardIds(userId, null, boardsSelector),
  146. };
  147. }
  148. if (endAt !== null) {
  149. selector.endAt = endAt;
  150. }
  151. if (queryParams.hasOperator(OPERATOR_BOARD)) {
  152. const queryBoards = [];
  153. queryParams.getPredicates(OPERATOR_BOARD).forEach(query => {
  154. const boards = Boards.userSearch(userId, {
  155. title: new RegExp(escapeForRegex(query), 'i'),
  156. });
  157. if (boards.count()) {
  158. boards.forEach(board => {
  159. queryBoards.push(board._id);
  160. });
  161. } else {
  162. errors.addNotFound(OPERATOR_BOARD, query);
  163. }
  164. });
  165. selector.boardId.$in = queryBoards;
  166. }
  167. if (queryParams.hasOperator(OPERATOR_SWIMLANE)) {
  168. const querySwimlanes = [];
  169. queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => {
  170. const swimlanes = Swimlanes.find({
  171. title: new RegExp(escapeForRegex(query), 'i'),
  172. });
  173. if (swimlanes.count()) {
  174. swimlanes.forEach(swim => {
  175. querySwimlanes.push(swim._id);
  176. });
  177. } else {
  178. errors.addNotFound(OPERATOR_SWIMLANE, query);
  179. }
  180. });
  181. // eslint-disable-next-line no-prototype-builtins
  182. if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
  183. selector.swimlaneId = { $in: [] };
  184. }
  185. selector.swimlaneId.$in = querySwimlanes;
  186. }
  187. if (queryParams.hasOperator(OPERATOR_LIST)) {
  188. const queryLists = [];
  189. queryParams.getPredicates(OPERATOR_LIST).forEach(query => {
  190. const lists = Lists.find({
  191. title: new RegExp(escapeForRegex(query), 'i'),
  192. });
  193. if (lists.count()) {
  194. lists.forEach(list => {
  195. queryLists.push(list._id);
  196. });
  197. } else {
  198. errors.addNotFound(OPERATOR_LIST, query);
  199. }
  200. });
  201. // eslint-disable-next-line no-prototype-builtins
  202. if (!selector.hasOwnProperty('listId')) {
  203. selector.listId = { $in: [] };
  204. }
  205. selector.listId.$in = queryLists;
  206. }
  207. if (queryParams.hasOperator(OPERATOR_COMMENT)) {
  208. const cardIds = CardComments.textSearch(
  209. userId,
  210. queryParams.getPredicates(OPERATOR_COMMENT),
  211. com => {
  212. return com.cardId;
  213. },
  214. );
  215. if (cardIds.length) {
  216. selector._id = { $in: cardIds };
  217. } else {
  218. queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => {
  219. errors.addNotFound(OPERATOR_COMMENT, comment);
  220. });
  221. }
  222. }
  223. [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].forEach(field => {
  224. if (queryParams.hasOperator(field)) {
  225. selector[field] = {};
  226. const predicate = queryParams.getPredicate(field);
  227. selector[field][predicate.operator] = new Date(predicate.value);
  228. }
  229. });
  230. const queryUsers = {};
  231. queryUsers[OPERATOR_ASSIGNEE] = [];
  232. queryUsers[OPERATOR_MEMBER] = [];
  233. queryUsers[OPERATOR_CREATOR] = [];
  234. if (queryParams.hasOperator(OPERATOR_USER)) {
  235. const users = [];
  236. queryParams.getPredicates(OPERATOR_USER).forEach(username => {
  237. const user = Users.findOne({ username });
  238. if (user) {
  239. users.push(user._id);
  240. } else {
  241. errors.addNotFound(OPERATOR_USER, username);
  242. }
  243. });
  244. if (users.length) {
  245. selector.$and.push({
  246. $or: [{ members: { $in: users } }, { assignees: { $in: users } }],
  247. });
  248. }
  249. }
  250. [OPERATOR_MEMBER, OPERATOR_ASSIGNEE, OPERATOR_CREATOR].forEach(key => {
  251. if (queryParams.hasOperator(key)) {
  252. const users = [];
  253. queryParams.getPredicates(key).forEach(username => {
  254. const user = Users.findOne({ username });
  255. if (user) {
  256. users.push(user._id);
  257. } else {
  258. errors.addNotFound(key, username);
  259. }
  260. });
  261. if (users.length) {
  262. selector[key] = { $in: users };
  263. }
  264. }
  265. });
  266. if (queryParams.hasOperator(OPERATOR_LABEL)) {
  267. const queryLabels = [];
  268. queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
  269. let boards = Boards.userBoards(userId, null, {
  270. labels: { $elemMatch: { color: label.toLowerCase() } },
  271. });
  272. if (boards.count()) {
  273. boards.forEach(board => {
  274. // eslint-disable-next-line no-console
  275. // console.log('board:', board);
  276. // eslint-disable-next-line no-console
  277. // console.log('board.labels:', board.labels);
  278. board.labels
  279. .filter(boardLabel => {
  280. return boardLabel.color === label.toLowerCase();
  281. })
  282. .forEach(boardLabel => {
  283. queryLabels.push(boardLabel._id);
  284. });
  285. });
  286. } else {
  287. // eslint-disable-next-line no-console
  288. // console.log('label:', label);
  289. const reLabel = new RegExp(escapeForRegex(label), 'i');
  290. // eslint-disable-next-line no-console
  291. // console.log('reLabel:', reLabel);
  292. boards = Boards.userBoards(userId, null, {
  293. labels: { $elemMatch: { name: reLabel } },
  294. });
  295. if (boards.count()) {
  296. boards.forEach(board => {
  297. board.labels
  298. .filter(boardLabel => {
  299. if (!boardLabel.name) {
  300. return false;
  301. }
  302. return boardLabel.name.match(reLabel);
  303. })
  304. .forEach(boardLabel => {
  305. queryLabels.push(boardLabel._id);
  306. });
  307. });
  308. } else {
  309. errors.addNotFound(OPERATOR_LABEL, label);
  310. }
  311. }
  312. });
  313. if (queryLabels.length) {
  314. // eslint-disable-next-line no-console
  315. // console.log('queryLabels:', queryLabels);
  316. selector.labelIds = { $in: _.uniq(queryLabels) };
  317. }
  318. }
  319. if (queryParams.hasOperator(OPERATOR_HAS)) {
  320. queryParams.getPredicates(OPERATOR_HAS).forEach(has => {
  321. switch (has.field) {
  322. case PREDICATE_ATTACHMENT:
  323. selector.$and.push({
  324. _id: {
  325. $in: Attachments.find({}, { fields: { cardId: 1 } }).map(
  326. a => a.cardId,
  327. ),
  328. },
  329. });
  330. break;
  331. case PREDICATE_CHECKLIST:
  332. selector.$and.push({
  333. _id: {
  334. $in: Checklists.find({}, { fields: { cardId: 1 } }).map(
  335. a => a.cardId,
  336. ),
  337. },
  338. });
  339. break;
  340. case PREDICATE_DESCRIPTION:
  341. case PREDICATE_START_AT:
  342. case PREDICATE_DUE_AT:
  343. case PREDICATE_END_AT:
  344. if (has.exists) {
  345. selector[has.field] = { $exists: true, $nin: [null, ''] };
  346. } else {
  347. selector[has.field] = { $in: [null, ''] };
  348. }
  349. break;
  350. case PREDICATE_ASSIGNEES:
  351. case PREDICATE_MEMBERS:
  352. if (has.exists) {
  353. selector[has.field] = { $exists: true, $nin: [null, []] };
  354. } else {
  355. selector[has.field] = { $in: [null, []] };
  356. }
  357. break;
  358. }
  359. });
  360. }
  361. if (queryParams.text) {
  362. const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
  363. const items = ChecklistItems.find(
  364. { title: regex },
  365. { fields: { cardId: 1 } },
  366. );
  367. const checklists = Checklists.find(
  368. {
  369. $or: [
  370. { title: regex },
  371. { _id: { $in: items.map(item => item.checklistId) } },
  372. ],
  373. },
  374. { fields: { cardId: 1 } },
  375. );
  376. const attachments = Attachments.find({ 'original.name': regex });
  377. const comments = CardComments.find(
  378. { text: regex },
  379. { fields: { cardId: 1 } },
  380. );
  381. selector.$and.push({
  382. $or: [
  383. { title: regex },
  384. { description: regex },
  385. { customFields: { $elemMatch: { value: regex } } },
  386. // {
  387. // _id: {
  388. // $in: CardComments.textSearch(userId, [queryParams.text]).map(
  389. // com => com.cardId,
  390. // ),
  391. // },
  392. // },
  393. { _id: { $in: checklists.map(list => list.cardId) } },
  394. { _id: { $in: attachments.map(attach => attach.cardId) } },
  395. { _id: { $in: comments.map(com => com.cardId) } },
  396. ],
  397. });
  398. }
  399. if (selector.$and.length === 0) {
  400. delete selector.$and;
  401. }
  402. }
  403. // eslint-disable-next-line no-console
  404. console.log('selector:', selector);
  405. // eslint-disable-next-line no-console
  406. console.log('selector.$and:', selector.$and);
  407. const query = new Query();
  408. query.selector = selector;
  409. query.params = queryParams;
  410. query._errors = errors;
  411. return query;
  412. }
  413. function buildProjection(query) {
  414. let skip = 0;
  415. if (query.params.skip) {
  416. skip = query.params.skip;
  417. }
  418. let limit = DEFAULT_LIMIT;
  419. const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10);
  420. if (!isNaN(configLimit) && configLimit > 0) {
  421. limit = configLimit;
  422. }
  423. if (query.params.hasOperator(OPERATOR_LIMIT)) {
  424. limit = query.params.getPredicate(OPERATOR_LIMIT);
  425. }
  426. const projection = {
  427. fields: {
  428. _id: 1,
  429. archived: 1,
  430. boardId: 1,
  431. swimlaneId: 1,
  432. listId: 1,
  433. title: 1,
  434. type: 1,
  435. sort: 1,
  436. members: 1,
  437. assignees: 1,
  438. colors: 1,
  439. dueAt: 1,
  440. createdAt: 1,
  441. modifiedAt: 1,
  442. labelIds: 1,
  443. customFields: 1,
  444. userId: 1,
  445. },
  446. sort: {
  447. boardId: 1,
  448. swimlaneId: 1,
  449. listId: 1,
  450. sort: 1,
  451. },
  452. skip,
  453. limit,
  454. };
  455. if (query.params.hasOperator(OPERATOR_SORT)) {
  456. const order =
  457. query.params.getPredicate(OPERATOR_SORT).order === ORDER_ASCENDING
  458. ? 1
  459. : -1;
  460. switch (query.params.getPredicate(OPERATOR_SORT).name) {
  461. case PREDICATE_DUE_AT:
  462. projection.sort = {
  463. dueAt: order,
  464. boardId: 1,
  465. swimlaneId: 1,
  466. listId: 1,
  467. sort: 1,
  468. };
  469. break;
  470. case PREDICATE_MODIFIED_AT:
  471. projection.sort = {
  472. modifiedAt: order,
  473. boardId: 1,
  474. swimlaneId: 1,
  475. listId: 1,
  476. sort: 1,
  477. };
  478. break;
  479. case PREDICATE_CREATED_AT:
  480. projection.sort = {
  481. createdAt: order,
  482. boardId: 1,
  483. swimlaneId: 1,
  484. listId: 1,
  485. sort: 1,
  486. };
  487. break;
  488. case PREDICATE_SYSTEM:
  489. projection.sort = {
  490. boardId: order,
  491. swimlaneId: order,
  492. listId: order,
  493. modifiedAt: order,
  494. sort: order,
  495. };
  496. break;
  497. }
  498. }
  499. // eslint-disable-next-line no-console
  500. // console.log('projection:', projection);
  501. query.projection = projection;
  502. return query;
  503. }
  504. function buildQuery(queryParams) {
  505. const query = buildSelector(queryParams);
  506. return buildProjection(query);
  507. }
  508. Meteor.publish('brokenCards', function(sessionId) {
  509. check(sessionId, String);
  510. const params = new QueryParams();
  511. params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL);
  512. const query = buildQuery(params);
  513. query.selector.$or = [
  514. { boardId: { $in: [null, ''] } },
  515. { swimlaneId: { $in: [null, ''] } },
  516. { listId: { $in: [null, ''] } },
  517. ];
  518. // console.log('brokenCards selector:', query.selector);
  519. return findCards(sessionId, query);
  520. });
  521. Meteor.publish('nextPage', function(sessionId) {
  522. check(sessionId, String);
  523. const session = SessionData.findOne({ sessionId });
  524. const projection = session.getProjection();
  525. projection.skip = session.lastHit;
  526. return findCards(sessionId, new Query(session.getSelector(), projection));
  527. });
  528. Meteor.publish('previousPage', function(sessionId) {
  529. check(sessionId, String);
  530. const session = SessionData.findOne({ sessionId });
  531. const projection = session.getProjection();
  532. projection.skip = session.lastHit - session.resultsCount - projection.limit;
  533. return findCards(sessionId, new Query(session.getSelector(), projection));
  534. });
  535. function findCards(sessionId, query) {
  536. const userId = Meteor.userId();
  537. // eslint-disable-next-line no-console
  538. // console.log('selector:', query.selector);
  539. // console.log('selector.$and:', query.selector.$and);
  540. // eslint-disable-next-line no-console
  541. // console.log('projection:', projection);
  542. const cards = Cards.find(query.selector, query.projection);
  543. // eslint-disable-next-line no-console
  544. // console.log('count:', cards.count());
  545. const update = {
  546. $set: {
  547. totalHits: 0,
  548. lastHit: 0,
  549. resultsCount: 0,
  550. cards: [],
  551. selector: SessionData.pickle(query.selector),
  552. projection: SessionData.pickle(query.projection),
  553. errors: query.errors(),
  554. },
  555. };
  556. if (cards) {
  557. update.$set.totalHits = cards.count();
  558. update.$set.lastHit =
  559. query.projection.skip + query.projection.limit < cards.count()
  560. ? query.projection.skip + query.projection.limit
  561. : cards.count();
  562. update.$set.cards = cards.map(card => {
  563. return card._id;
  564. });
  565. update.$set.resultsCount = update.$set.cards.length;
  566. }
  567. SessionData.upsert({ userId, sessionId }, update);
  568. // remove old session data
  569. SessionData.remove({
  570. userId,
  571. modifiedAt: {
  572. $lt: new Date(
  573. moment()
  574. .subtract(1, 'day')
  575. .format(),
  576. ),
  577. },
  578. });
  579. if (cards) {
  580. const boards = [];
  581. const swimlanes = [];
  582. const lists = [];
  583. const customFieldIds = [];
  584. const users = [this.userId];
  585. cards.forEach(card => {
  586. if (card.boardId) boards.push(card.boardId);
  587. if (card.swimlaneId) swimlanes.push(card.swimlaneId);
  588. if (card.listId) lists.push(card.listId);
  589. if (card.userId) {
  590. users.push(card.userId);
  591. }
  592. if (card.members) {
  593. card.members.forEach(userId => {
  594. users.push(userId);
  595. });
  596. }
  597. if (card.assignees) {
  598. card.assignees.forEach(userId => {
  599. users.push(userId);
  600. });
  601. }
  602. if (card.customFields) {
  603. card.customFields.forEach(field => {
  604. customFieldIds.push(field._id);
  605. });
  606. }
  607. });
  608. const fields = {
  609. _id: 1,
  610. title: 1,
  611. archived: 1,
  612. sort: 1,
  613. type: 1,
  614. };
  615. return [
  616. cards,
  617. Boards.find(
  618. { _id: { $in: boards } },
  619. { fields: { ...fields, labels: 1, color: 1 } },
  620. ),
  621. Swimlanes.find(
  622. { _id: { $in: swimlanes } },
  623. { fields: { ...fields, color: 1 } },
  624. ),
  625. Lists.find({ _id: { $in: lists } }, { fields }),
  626. CustomFields.find({ _id: { $in: customFieldIds } }),
  627. Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
  628. Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
  629. Attachments.find({ cardId: { $in: cards.map(c => c._id) } }),
  630. CardComments.find({ cardId: { $in: cards.map(c => c._id) } }),
  631. SessionData.find({ userId, sessionId }),
  632. ];
  633. }
  634. return [SessionData.find({ userId, sessionId })];
  635. }