cards.js 20 KB

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