cards.js 19 KB

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