cards.js 20 KB

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