|  | @@ -1,3 +1,50 @@
 | 
	
		
			
				|  |  | +import moment from 'moment';
 | 
	
		
			
				|  |  | +import Users from '../../models/users';
 | 
	
		
			
				|  |  | +import Boards from '../../models/boards';
 | 
	
		
			
				|  |  | +import Lists from '../../models/lists';
 | 
	
		
			
				|  |  | +import Swimlanes from '../../models/swimlanes';
 | 
	
		
			
				|  |  | +import Cards from '../../models/cards';
 | 
	
		
			
				|  |  | +import CardComments from '../../models/cardComments';
 | 
	
		
			
				|  |  | +import Attachments from '../../models/attachments';
 | 
	
		
			
				|  |  | +import Checklists from '../../models/checklists';
 | 
	
		
			
				|  |  | +import ChecklistItems from '../../models/checklistItems';
 | 
	
		
			
				|  |  | +import SessionData from '../../models/usersessiondata';
 | 
	
		
			
				|  |  | +import CustomFields from '../../models/customFields';
 | 
	
		
			
				|  |  | +import {
 | 
	
		
			
				|  |  | +  DEFAULT_LIMIT,
 | 
	
		
			
				|  |  | +  OPERATOR_ASSIGNEE,
 | 
	
		
			
				|  |  | +  OPERATOR_BOARD,
 | 
	
		
			
				|  |  | +  OPERATOR_COMMENT,
 | 
	
		
			
				|  |  | +  OPERATOR_DUE,
 | 
	
		
			
				|  |  | +  OPERATOR_HAS,
 | 
	
		
			
				|  |  | +  OPERATOR_LABEL,
 | 
	
		
			
				|  |  | +  OPERATOR_LIMIT,
 | 
	
		
			
				|  |  | +  OPERATOR_LIST,
 | 
	
		
			
				|  |  | +  OPERATOR_MEMBER,
 | 
	
		
			
				|  |  | +  OPERATOR_SORT,
 | 
	
		
			
				|  |  | +  OPERATOR_STATUS,
 | 
	
		
			
				|  |  | +  OPERATOR_SWIMLANE,
 | 
	
		
			
				|  |  | +  OPERATOR_USER,
 | 
	
		
			
				|  |  | +  ORDER_ASCENDING,
 | 
	
		
			
				|  |  | +  PREDICATE_ALL,
 | 
	
		
			
				|  |  | +  PREDICATE_ARCHIVED,
 | 
	
		
			
				|  |  | +  PREDICATE_ASSIGNEES,
 | 
	
		
			
				|  |  | +  PREDICATE_ATTACHMENT,
 | 
	
		
			
				|  |  | +  PREDICATE_CHECKLIST,
 | 
	
		
			
				|  |  | +  PREDICATE_CREATED_AT,
 | 
	
		
			
				|  |  | +  PREDICATE_DESCRIPTION,
 | 
	
		
			
				|  |  | +  PREDICATE_DUE_AT,
 | 
	
		
			
				|  |  | +  PREDICATE_END_AT,
 | 
	
		
			
				|  |  | +  PREDICATE_ENDED,
 | 
	
		
			
				|  |  | +  PREDICATE_MEMBERS,
 | 
	
		
			
				|  |  | +  PREDICATE_MODIFIED_AT,
 | 
	
		
			
				|  |  | +  PREDICATE_PRIVATE,
 | 
	
		
			
				|  |  | +  PREDICATE_PUBLIC,
 | 
	
		
			
				|  |  | +  PREDICATE_START_AT,
 | 
	
		
			
				|  |  | +  PREDICATE_SYSTEM,
 | 
	
		
			
				|  |  | +} from '../../config/search-const';
 | 
	
		
			
				|  |  | +import { QueryErrors, QueryParams, Query } from '../../config/query-classes';
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  const escapeForRegex = require('escape-string-regexp');
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  Meteor.publish('card', cardId => {
 | 
	
	
		
			
				|  | @@ -5,260 +52,50 @@ Meteor.publish('card', cardId => {
 | 
	
		
			
				|  |  |    return Cards.find({ _id: cardId });
 | 
	
		
			
				|  |  |  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -Meteor.publish('myCards', function() {
 | 
	
		
			
				|  |  | -  const userId = Meteor.userId();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const archivedBoards = [];
 | 
	
		
			
				|  |  | -  Boards.find({ archived: true }).forEach(board => {
 | 
	
		
			
				|  |  | -    archivedBoards.push(board._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const archivedSwimlanes = [];
 | 
	
		
			
				|  |  | -  Swimlanes.find({ archived: true }).forEach(swimlane => {
 | 
	
		
			
				|  |  | -    archivedSwimlanes.push(swimlane._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const archivedLists = [];
 | 
	
		
			
				|  |  | -  Lists.find({ archived: true }).forEach(list => {
 | 
	
		
			
				|  |  | -    archivedLists.push(list._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  selector = {
 | 
	
		
			
				|  |  | -    archived: false,
 | 
	
		
			
				|  |  | -    boardId: { $nin: archivedBoards },
 | 
	
		
			
				|  |  | -    swimlaneId: { $nin: archivedSwimlanes },
 | 
	
		
			
				|  |  | -    listId: { $nin: archivedLists },
 | 
	
		
			
				|  |  | -    $or: [{ members: userId }, { assignees: userId }],
 | 
	
		
			
				|  |  | -  };
 | 
	
		
			
				|  |  | +Meteor.publish('myCards', function(sessionId) {
 | 
	
		
			
				|  |  | +  const queryParams = new QueryParams();
 | 
	
		
			
				|  |  | +  queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  const cards = Cards.find(selector, {
 | 
	
		
			
				|  |  | -    fields: {
 | 
	
		
			
				|  |  | -      _id: 1,
 | 
	
		
			
				|  |  | -      archived: 1,
 | 
	
		
			
				|  |  | -      boardId: 1,
 | 
	
		
			
				|  |  | -      swimlaneId: 1,
 | 
	
		
			
				|  |  | -      listId: 1,
 | 
	
		
			
				|  |  | -      title: 1,
 | 
	
		
			
				|  |  | -      type: 1,
 | 
	
		
			
				|  |  | -      sort: 1,
 | 
	
		
			
				|  |  | -      members: 1,
 | 
	
		
			
				|  |  | -      assignees: 1,
 | 
	
		
			
				|  |  | -      colors: 1,
 | 
	
		
			
				|  |  | -      dueAt: 1,
 | 
	
		
			
				|  |  | -    },
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const boards = [];
 | 
	
		
			
				|  |  | -  const swimlanes = [];
 | 
	
		
			
				|  |  | -  const lists = [];
 | 
	
		
			
				|  |  | -  const users = [];
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  cards.forEach(card => {
 | 
	
		
			
				|  |  | -    if (card.boardId) boards.push(card.boardId);
 | 
	
		
			
				|  |  | -    if (card.swimlaneId) swimlanes.push(card.swimlaneId);
 | 
	
		
			
				|  |  | -    if (card.listId) lists.push(card.listId);
 | 
	
		
			
				|  |  | -    if (card.members) {
 | 
	
		
			
				|  |  | -      card.members.forEach(userId => {
 | 
	
		
			
				|  |  | -        users.push(userId);
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    if (card.assignees) {
 | 
	
		
			
				|  |  | -      card.assignees.forEach(userId => {
 | 
	
		
			
				|  |  | -        users.push(userId);
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  return [
 | 
	
		
			
				|  |  | -    cards,
 | 
	
		
			
				|  |  | -    Boards.find({ _id: { $in: boards } }),
 | 
	
		
			
				|  |  | -    Swimlanes.find({ _id: { $in: swimlanes } }),
 | 
	
		
			
				|  |  | -    Lists.find({ _id: { $in: lists } }),
 | 
	
		
			
				|  |  | -    Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
 | 
	
		
			
				|  |  | -  ];
 | 
	
		
			
				|  |  | +  return findCards(sessionId, buildQuery(queryParams));
 | 
	
		
			
				|  |  |  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -Meteor.publish('dueCards', function(allUsers = false) {
 | 
	
		
			
				|  |  | -  check(allUsers, Boolean);
 | 
	
		
			
				|  |  | +// Meteor.publish('dueCards', function(sessionId, allUsers = false) {
 | 
	
		
			
				|  |  | +//   check(sessionId, String);
 | 
	
		
			
				|  |  | +//   check(allUsers, Boolean);
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +//   // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  | +//   // console.log('all users:', allUsers);
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +//   const queryParams = {
 | 
	
		
			
				|  |  | +//     has: [{ field: 'dueAt', exists: true }],
 | 
	
		
			
				|  |  | +//     limit: 25,
 | 
	
		
			
				|  |  | +//     skip: 0,
 | 
	
		
			
				|  |  | +//     sort: { name: 'dueAt', order: 'des' },
 | 
	
		
			
				|  |  | +//   };
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +//   if (!allUsers) {
 | 
	
		
			
				|  |  | +//     queryParams.users = [Meteor.user().username];
 | 
	
		
			
				|  |  | +//   }
 | 
	
		
			
				|  |  | +//
 | 
	
		
			
				|  |  | +//   return buildQuery(sessionId, queryParams);
 | 
	
		
			
				|  |  | +// });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +Meteor.publish('globalSearch', function(sessionId, params) {
 | 
	
		
			
				|  |  | +  check(sessionId, String);
 | 
	
		
			
				|  |  | +  check(params, Object);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  | -  // console.log('all users:', allUsers);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const user = Users.findOne({ _id: this.userId });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const archivedBoards = [];
 | 
	
		
			
				|  |  | -  Boards.find({ archived: true }).forEach(board => {
 | 
	
		
			
				|  |  | -    archivedBoards.push(board._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const permiitedBoards = [];
 | 
	
		
			
				|  |  | -  let selector = {
 | 
	
		
			
				|  |  | -    archived: false,
 | 
	
		
			
				|  |  | -  };
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  selector.$or = [
 | 
	
		
			
				|  |  | -    { permission: 'public' },
 | 
	
		
			
				|  |  | -    { members: { $elemMatch: { userId: user._id, isActive: true } } },
 | 
	
		
			
				|  |  | -  ];
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  Boards.find(selector).forEach(board => {
 | 
	
		
			
				|  |  | -    permiitedBoards.push(board._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const archivedSwimlanes = [];
 | 
	
		
			
				|  |  | -  Swimlanes.find({ archived: true }).forEach(swimlane => {
 | 
	
		
			
				|  |  | -    archivedSwimlanes.push(swimlane._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | +  // console.log('queryParams:', params);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  const archivedLists = [];
 | 
	
		
			
				|  |  | -  Lists.find({ archived: true }).forEach(list => {
 | 
	
		
			
				|  |  | -    archivedLists.push(list._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  selector = {
 | 
	
		
			
				|  |  | -    archived: false,
 | 
	
		
			
				|  |  | -    boardId: { $nin: archivedBoards, $in: permiitedBoards },
 | 
	
		
			
				|  |  | -    swimlaneId: { $nin: archivedSwimlanes },
 | 
	
		
			
				|  |  | -    listId: { $nin: archivedLists },
 | 
	
		
			
				|  |  | -    dueAt: { $ne: null },
 | 
	
		
			
				|  |  | -    endAt: null,
 | 
	
		
			
				|  |  | -  };
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  if (!allUsers) {
 | 
	
		
			
				|  |  | -    selector.$or = [{ members: user._id }, { assignees: user._id }];
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const cards = Cards.find(selector, {
 | 
	
		
			
				|  |  | -    fields: {
 | 
	
		
			
				|  |  | -      _id: 1,
 | 
	
		
			
				|  |  | -      archived: 1,
 | 
	
		
			
				|  |  | -      boardId: 1,
 | 
	
		
			
				|  |  | -      swimlaneId: 1,
 | 
	
		
			
				|  |  | -      listId: 1,
 | 
	
		
			
				|  |  | -      title: 1,
 | 
	
		
			
				|  |  | -      type: 1,
 | 
	
		
			
				|  |  | -      sort: 1,
 | 
	
		
			
				|  |  | -      members: 1,
 | 
	
		
			
				|  |  | -      assignees: 1,
 | 
	
		
			
				|  |  | -      colors: 1,
 | 
	
		
			
				|  |  | -      dueAt: 1,
 | 
	
		
			
				|  |  | -    },
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const boards = [];
 | 
	
		
			
				|  |  | -  const swimlanes = [];
 | 
	
		
			
				|  |  | -  const lists = [];
 | 
	
		
			
				|  |  | -  const users = [];
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  cards.forEach(card => {
 | 
	
		
			
				|  |  | -    if (card.boardId) boards.push(card.boardId);
 | 
	
		
			
				|  |  | -    if (card.swimlaneId) swimlanes.push(card.swimlaneId);
 | 
	
		
			
				|  |  | -    if (card.listId) lists.push(card.listId);
 | 
	
		
			
				|  |  | -    if (card.members) {
 | 
	
		
			
				|  |  | -      card.members.forEach(userId => {
 | 
	
		
			
				|  |  | -        users.push(userId);
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    if (card.assignees) {
 | 
	
		
			
				|  |  | -      card.assignees.forEach(userId => {
 | 
	
		
			
				|  |  | -        users.push(userId);
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  return [
 | 
	
		
			
				|  |  | -    cards,
 | 
	
		
			
				|  |  | -    Boards.find({ _id: { $in: boards } }),
 | 
	
		
			
				|  |  | -    Swimlanes.find({ _id: { $in: swimlanes } }),
 | 
	
		
			
				|  |  | -    Lists.find({ _id: { $in: lists } }),
 | 
	
		
			
				|  |  | -    Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
 | 
	
		
			
				|  |  | -  ];
 | 
	
		
			
				|  |  | +  return findCards(sessionId, buildQuery(new QueryParams(params)));
 | 
	
		
			
				|  |  |  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  | -  check(sessionId, String);
 | 
	
		
			
				|  |  | -  check(queryParams, Object);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  | -  // console.log('queryParams:', queryParams);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +function buildSelector(queryParams) {
 | 
	
		
			
				|  |  |    const userId = Meteor.userId();
 | 
	
		
			
				|  |  | -  // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  | -  // console.log('userId:', userId);
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const errors = new (class {
 | 
	
		
			
				|  |  | -    constructor() {
 | 
	
		
			
				|  |  | -      this.notFound = {
 | 
	
		
			
				|  |  | -        boards: [],
 | 
	
		
			
				|  |  | -        swimlanes: [],
 | 
	
		
			
				|  |  | -        lists: [],
 | 
	
		
			
				|  |  | -        labels: [],
 | 
	
		
			
				|  |  | -        users: [],
 | 
	
		
			
				|  |  | -        members: [],
 | 
	
		
			
				|  |  | -        assignees: [],
 | 
	
		
			
				|  |  | -        status: [],
 | 
	
		
			
				|  |  | -        comments: [],
 | 
	
		
			
				|  |  | -      };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -      this.colorMap = Boards.colorMap();
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    hasErrors() {
 | 
	
		
			
				|  |  | -      for (const value of Object.values(this.notFound)) {
 | 
	
		
			
				|  |  | -        if (value.length) {
 | 
	
		
			
				|  |  | -          return true;
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -      }
 | 
	
		
			
				|  |  | -      return false;
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -    errorMessages() {
 | 
	
		
			
				|  |  | -      const messages = [];
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      this.notFound.boards.forEach(board => {
 | 
	
		
			
				|  |  | -        messages.push({ tag: 'board-title-not-found', value: board });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -      this.notFound.swimlanes.forEach(swim => {
 | 
	
		
			
				|  |  | -        messages.push({ tag: 'swimlane-title-not-found', value: swim });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -      this.notFound.lists.forEach(list => {
 | 
	
		
			
				|  |  | -        messages.push({ tag: 'list-title-not-found', value: list });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -      this.notFound.comments.forEach(comments => {
 | 
	
		
			
				|  |  | -        comments.forEach(text => {
 | 
	
		
			
				|  |  | -          messages.push({ tag: 'comment-not-found', value: text });
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -      this.notFound.labels.forEach(label => {
 | 
	
		
			
				|  |  | -        messages.push({
 | 
	
		
			
				|  |  | -          tag: 'label-not-found',
 | 
	
		
			
				|  |  | -          value: label,
 | 
	
		
			
				|  |  | -          color: Boards.labelColors().includes(label),
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -      this.notFound.users.forEach(user => {
 | 
	
		
			
				|  |  | -        messages.push({ tag: 'user-username-not-found', value: user });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -      this.notFound.members.forEach(user => {
 | 
	
		
			
				|  |  | -        messages.push({ tag: 'user-username-not-found', value: user });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -      this.notFound.assignees.forEach(user => {
 | 
	
		
			
				|  |  | -        messages.push({ tag: 'user-username-not-found', value: user });
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -      return messages;
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  })();
 | 
	
		
			
				|  |  | +  const errors = new QueryErrors();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    let selector = {};
 | 
	
		
			
				|  |  | -  let skip = 0;
 | 
	
		
			
				|  |  | -  if (queryParams.skip) {
 | 
	
		
			
				|  |  | -    skip = queryParams.skip;
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  | -  let limit = 25;
 | 
	
		
			
				|  |  | -  if (queryParams.limit) {
 | 
	
		
			
				|  |  | -    limit = queryParams.limit;
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    if (queryParams.selector) {
 | 
	
		
			
				|  |  |      selector = queryParams.selector;
 | 
	
	
		
			
				|  | @@ -267,15 +104,15 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      let archived = false;
 | 
	
		
			
				|  |  |      let endAt = null;
 | 
	
		
			
				|  |  | -    if (queryParams.status.length) {
 | 
	
		
			
				|  |  | -      queryParams.status.forEach(status => {
 | 
	
		
			
				|  |  | -        if (status === 'archived') {
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_STATUS)) {
 | 
	
		
			
				|  |  | +      queryParams.getPredicates(OPERATOR_STATUS).forEach(status => {
 | 
	
		
			
				|  |  | +        if (status === PREDICATE_ARCHIVED) {
 | 
	
		
			
				|  |  |            archived = true;
 | 
	
		
			
				|  |  | -        } else if (status === 'all') {
 | 
	
		
			
				|  |  | +        } else if (status === PREDICATE_ALL) {
 | 
	
		
			
				|  |  |            archived = null;
 | 
	
		
			
				|  |  | -        } else if (status === 'ended') {
 | 
	
		
			
				|  |  | +        } else if (status === PREDICATE_ENDED) {
 | 
	
		
			
				|  |  |            endAt = { $nin: [null, ''] };
 | 
	
		
			
				|  |  | -        } else if (['private', 'public'].includes(status)) {
 | 
	
		
			
				|  |  | +        } else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) {
 | 
	
		
			
				|  |  |            boardsSelector.permission = status;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        });
 | 
	
	
		
			
				|  | @@ -320,9 +157,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |        selector.endAt = endAt;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.boards.length) {
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_BOARD)) {
 | 
	
		
			
				|  |  |        const queryBoards = [];
 | 
	
		
			
				|  |  | -      queryParams.boards.forEach(query => {
 | 
	
		
			
				|  |  | +      queryParams.hasOperator(OPERATOR_BOARD).forEach(query => {
 | 
	
		
			
				|  |  |          const boards = Boards.userSearch(userId, {
 | 
	
		
			
				|  |  |            title: new RegExp(escapeForRegex(query), 'i'),
 | 
	
		
			
				|  |  |          });
 | 
	
	
		
			
				|  | @@ -331,16 +168,16 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |              queryBoards.push(board._id);
 | 
	
		
			
				|  |  |            });
 | 
	
		
			
				|  |  |          } else {
 | 
	
		
			
				|  |  | -          errors.notFound.boards.push(query);
 | 
	
		
			
				|  |  | +          errors.addNotFound(OPERATOR_BOARD, query);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        selector.boardId.$in = queryBoards;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.swimlanes.length) {
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_SWIMLANE)) {
 | 
	
		
			
				|  |  |        const querySwimlanes = [];
 | 
	
		
			
				|  |  | -      queryParams.swimlanes.forEach(query => {
 | 
	
		
			
				|  |  | +      queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => {
 | 
	
		
			
				|  |  |          const swimlanes = Swimlanes.find({
 | 
	
		
			
				|  |  |            title: new RegExp(escapeForRegex(query), 'i'),
 | 
	
		
			
				|  |  |          });
 | 
	
	
		
			
				|  | @@ -349,7 +186,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |              querySwimlanes.push(swim._id);
 | 
	
		
			
				|  |  |            });
 | 
	
		
			
				|  |  |          } else {
 | 
	
		
			
				|  |  | -          errors.notFound.swimlanes.push(query);
 | 
	
		
			
				|  |  | +          errors.addNotFound(OPERATOR_SWIMLANE, query);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -360,9 +197,9 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |        selector.swimlaneId.$in = querySwimlanes;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.lists.length) {
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_LIST)) {
 | 
	
		
			
				|  |  |        const queryLists = [];
 | 
	
		
			
				|  |  | -      queryParams.lists.forEach(query => {
 | 
	
		
			
				|  |  | +      queryParams.getPredicates(OPERATOR_LIST).forEach(query => {
 | 
	
		
			
				|  |  |          const lists = Lists.find({
 | 
	
		
			
				|  |  |            title: new RegExp(escapeForRegex(query), 'i'),
 | 
	
		
			
				|  |  |          });
 | 
	
	
		
			
				|  | @@ -371,7 +208,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |              queryLists.push(list._id);
 | 
	
		
			
				|  |  |            });
 | 
	
		
			
				|  |  |          } else {
 | 
	
		
			
				|  |  | -          errors.notFound.lists.push(query);
 | 
	
		
			
				|  |  | +          errors.addNotFound(OPERATOR_LIST, query);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -382,8 +219,10 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |        selector.listId.$in = queryLists;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.comments.length) {
 | 
	
		
			
				|  |  | -      const cardIds = CardComments.textSearch(userId, queryParams.comments).map(
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_COMMENT)) {
 | 
	
		
			
				|  |  | +      const cardIds = CardComments.textSearch(
 | 
	
		
			
				|  |  | +        userId,
 | 
	
		
			
				|  |  | +        queryParams.getPredicates(OPERATOR_COMMENT),
 | 
	
		
			
				|  |  |          com => {
 | 
	
		
			
				|  |  |            return com.cardId;
 | 
	
		
			
				|  |  |          },
 | 
	
	
		
			
				|  | @@ -391,82 +230,75 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |        if (cardIds.length) {
 | 
	
		
			
				|  |  |          selector._id = { $in: cardIds };
 | 
	
		
			
				|  |  |        } else {
 | 
	
		
			
				|  |  | -        errors.notFound.comments.push(queryParams.comments);
 | 
	
		
			
				|  |  | +        queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => {
 | 
	
		
			
				|  |  | +          errors.addNotFound(OPERATOR_COMMENT, comment);
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    ['dueAt', 'createdAt', 'modifiedAt'].forEach(field => {
 | 
	
		
			
				|  |  | -      if (queryParams[field]) {
 | 
	
		
			
				|  |  | +    [OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => {
 | 
	
		
			
				|  |  | +      if (queryParams.hasOperator(field)) {
 | 
	
		
			
				|  |  |          selector[field] = {};
 | 
	
		
			
				|  |  | -        selector[field][queryParams[field]['operator']] = new Date(
 | 
	
		
			
				|  |  | -          queryParams[field]['value'],
 | 
	
		
			
				|  |  | -        );
 | 
	
		
			
				|  |  | +        const predicate = queryParams.getPredicate(field);
 | 
	
		
			
				|  |  | +        selector[field][predicate.operator] = new Date(predicate.value);
 | 
	
		
			
				|  |  |        }
 | 
	
		
			
				|  |  |      });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    const queryMembers = [];
 | 
	
		
			
				|  |  | -    const queryAssignees = [];
 | 
	
		
			
				|  |  | -    if (queryParams.users.length) {
 | 
	
		
			
				|  |  | -      queryParams.users.forEach(query => {
 | 
	
		
			
				|  |  | -        const users = Users.find({
 | 
	
		
			
				|  |  | -          username: query,
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -        if (users.count()) {
 | 
	
		
			
				|  |  | -          users.forEach(user => {
 | 
	
		
			
				|  |  | -            queryMembers.push(user._id);
 | 
	
		
			
				|  |  | -            queryAssignees.push(user._id);
 | 
	
		
			
				|  |  | -          });
 | 
	
		
			
				|  |  | -        } else {
 | 
	
		
			
				|  |  | -          errors.notFound.users.push(query);
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | +    const queryUsers = {};
 | 
	
		
			
				|  |  | +    queryUsers[OPERATOR_ASSIGNEE] = [];
 | 
	
		
			
				|  |  | +    queryUsers[OPERATOR_MEMBER] = [];
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.members.length) {
 | 
	
		
			
				|  |  | -      queryParams.members.forEach(query => {
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_USER)) {
 | 
	
		
			
				|  |  | +      queryParams.getPredicates(OPERATOR_USER).forEach(query => {
 | 
	
		
			
				|  |  |          const users = Users.find({
 | 
	
		
			
				|  |  |            username: query,
 | 
	
		
			
				|  |  |          });
 | 
	
		
			
				|  |  |          if (users.count()) {
 | 
	
		
			
				|  |  |            users.forEach(user => {
 | 
	
		
			
				|  |  | -            queryMembers.push(user._id);
 | 
	
		
			
				|  |  | +            queryUsers[OPERATOR_MEMBER].push(user._id);
 | 
	
		
			
				|  |  | +            queryUsers[OPERATOR_ASSIGNEE].push(user._id);
 | 
	
		
			
				|  |  |            });
 | 
	
		
			
				|  |  |          } else {
 | 
	
		
			
				|  |  | -          errors.notFound.members.push(query);
 | 
	
		
			
				|  |  | +          errors.addNotFound(OPERATOR_USER, query);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.assignees.length) {
 | 
	
		
			
				|  |  | -      queryParams.assignees.forEach(query => {
 | 
	
		
			
				|  |  | -        const users = Users.find({
 | 
	
		
			
				|  |  | -          username: query,
 | 
	
		
			
				|  |  | -        });
 | 
	
		
			
				|  |  | -        if (users.count()) {
 | 
	
		
			
				|  |  | -          users.forEach(user => {
 | 
	
		
			
				|  |  | -            queryAssignees.push(user._id);
 | 
	
		
			
				|  |  | +    [OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => {
 | 
	
		
			
				|  |  | +      if (queryParams.hasOperator(key)) {
 | 
	
		
			
				|  |  | +        queryParams.getPredicates(key).forEach(query => {
 | 
	
		
			
				|  |  | +          const users = Users.find({
 | 
	
		
			
				|  |  | +            username: query,
 | 
	
		
			
				|  |  |            });
 | 
	
		
			
				|  |  | -        } else {
 | 
	
		
			
				|  |  | -          errors.notFound.assignees.push(query);
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | +          if (users.count()) {
 | 
	
		
			
				|  |  | +            users.forEach(user => {
 | 
	
		
			
				|  |  | +              queryUsers[key].push(user._id);
 | 
	
		
			
				|  |  | +            });
 | 
	
		
			
				|  |  | +          } else {
 | 
	
		
			
				|  |  | +            errors.addNotFound(key, query);
 | 
	
		
			
				|  |  | +          }
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +    });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryMembers.length && queryAssignees.length) {
 | 
	
		
			
				|  |  | +    if (
 | 
	
		
			
				|  |  | +      queryUsers[OPERATOR_MEMBER].length &&
 | 
	
		
			
				|  |  | +      queryUsers[OPERATOR_ASSIGNEE].length
 | 
	
		
			
				|  |  | +    ) {
 | 
	
		
			
				|  |  |        selector.$and.push({
 | 
	
		
			
				|  |  |          $or: [
 | 
	
		
			
				|  |  | -          { members: { $in: queryMembers } },
 | 
	
		
			
				|  |  | -          { assignees: { $in: queryAssignees } },
 | 
	
		
			
				|  |  | +          { members: { $in: queryUsers[OPERATOR_MEMBER] } },
 | 
	
		
			
				|  |  | +          { assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } },
 | 
	
		
			
				|  |  |          ],
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  | -    } else if (queryMembers.length) {
 | 
	
		
			
				|  |  | -      selector.members = { $in: queryMembers };
 | 
	
		
			
				|  |  | -    } else if (queryAssignees.length) {
 | 
	
		
			
				|  |  | -      selector.assignees = { $in: queryAssignees };
 | 
	
		
			
				|  |  | +    } else if (queryUsers[OPERATOR_MEMBER].length) {
 | 
	
		
			
				|  |  | +      selector.members = { $in: queryUsers[OPERATOR_MEMBER] };
 | 
	
		
			
				|  |  | +    } else if (queryUsers[OPERATOR_ASSIGNEE].length) {
 | 
	
		
			
				|  |  | +      selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] };
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.labels.length) {
 | 
	
		
			
				|  |  | -      queryParams.labels.forEach(label => {
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_LABEL)) {
 | 
	
		
			
				|  |  | +      queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
 | 
	
		
			
				|  |  |          const queryLabels = [];
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          let boards = Boards.userSearch(userId, {
 | 
	
	
		
			
				|  | @@ -511,39 +343,47 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |                  });
 | 
	
		
			
				|  |  |              });
 | 
	
		
			
				|  |  |            } else {
 | 
	
		
			
				|  |  | -            errors.notFound.labels.push(label);
 | 
	
		
			
				|  |  | +            errors.addNotFound(OPERATOR_LABEL, label);
 | 
	
		
			
				|  |  |            }
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        selector.labelIds = { $in: queryLabels };
 | 
	
		
			
				|  |  | +        selector.labelIds = { $in: _.uniq(queryLabels) };
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    if (queryParams.has.length) {
 | 
	
		
			
				|  |  | -      queryParams.has.forEach(has => {
 | 
	
		
			
				|  |  | +    if (queryParams.hasOperator(OPERATOR_HAS)) {
 | 
	
		
			
				|  |  | +      queryParams.getPredicates(OPERATOR_HAS).forEach(has => {
 | 
	
		
			
				|  |  |          switch (has.field) {
 | 
	
		
			
				|  |  | -          case 'attachment':
 | 
	
		
			
				|  |  | -            const attachments = Attachments.find({}, { fields: { cardId: 1 } });
 | 
	
		
			
				|  |  | +          case PREDICATE_ATTACHMENT:
 | 
	
		
			
				|  |  |              selector.$and.push({
 | 
	
		
			
				|  |  | -              _id: { $in: attachments.map(a => a.cardId) },
 | 
	
		
			
				|  |  | +              _id: {
 | 
	
		
			
				|  |  | +                $in: Attachments.find({}, { fields: { cardId: 1 } }).map(
 | 
	
		
			
				|  |  | +                  a => a.cardId,
 | 
	
		
			
				|  |  | +                ),
 | 
	
		
			
				|  |  | +              },
 | 
	
		
			
				|  |  |              });
 | 
	
		
			
				|  |  |              break;
 | 
	
		
			
				|  |  | -          case 'checklist':
 | 
	
		
			
				|  |  | -            const checklists = Checklists.find({}, { fields: { cardId: 1 } });
 | 
	
		
			
				|  |  | -            selector.$and.push({ _id: { $in: checklists.map(a => a.cardId) } });
 | 
	
		
			
				|  |  | +          case PREDICATE_CHECKLIST:
 | 
	
		
			
				|  |  | +            selector.$and.push({
 | 
	
		
			
				|  |  | +              _id: {
 | 
	
		
			
				|  |  | +                $in: Checklists.find({}, { fields: { cardId: 1 } }).map(
 | 
	
		
			
				|  |  | +                  a => a.cardId,
 | 
	
		
			
				|  |  | +                ),
 | 
	
		
			
				|  |  | +              },
 | 
	
		
			
				|  |  | +            });
 | 
	
		
			
				|  |  |              break;
 | 
	
		
			
				|  |  | -          case 'description':
 | 
	
		
			
				|  |  | -          case 'startAt':
 | 
	
		
			
				|  |  | -          case 'dueAt':
 | 
	
		
			
				|  |  | -          case 'endAt':
 | 
	
		
			
				|  |  | +          case PREDICATE_DESCRIPTION:
 | 
	
		
			
				|  |  | +          case PREDICATE_START_AT:
 | 
	
		
			
				|  |  | +          case PREDICATE_DUE_AT:
 | 
	
		
			
				|  |  | +          case PREDICATE_END_AT:
 | 
	
		
			
				|  |  |              if (has.exists) {
 | 
	
		
			
				|  |  |                selector[has.field] = { $exists: true, $nin: [null, ''] };
 | 
	
		
			
				|  |  |              } else {
 | 
	
		
			
				|  |  |                selector[has.field] = { $in: [null, ''] };
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  |              break;
 | 
	
		
			
				|  |  | -          case 'assignees':
 | 
	
		
			
				|  |  | -          case 'members':
 | 
	
		
			
				|  |  | +          case PREDICATE_ASSIGNEES:
 | 
	
		
			
				|  |  | +          case PREDICATE_MEMBERS:
 | 
	
		
			
				|  |  |              if (has.exists) {
 | 
	
		
			
				|  |  |                selector[has.field] = { $exists: true, $nin: [null, []] };
 | 
	
		
			
				|  |  |              } else {
 | 
	
	
		
			
				|  | @@ -573,26 +413,26 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        const attachments = Attachments.find({ 'original.name': regex });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -      // const comments = CardComments.find(
 | 
	
		
			
				|  |  | -      //   { text: regex },
 | 
	
		
			
				|  |  | -      //   { fields: { cardId: 1 } },
 | 
	
		
			
				|  |  | -      // );
 | 
	
		
			
				|  |  | +      const comments = CardComments.find(
 | 
	
		
			
				|  |  | +        { text: regex },
 | 
	
		
			
				|  |  | +        { fields: { cardId: 1 } },
 | 
	
		
			
				|  |  | +      );
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |        selector.$and.push({
 | 
	
		
			
				|  |  |          $or: [
 | 
	
		
			
				|  |  |            { title: regex },
 | 
	
		
			
				|  |  |            { description: regex },
 | 
	
		
			
				|  |  |            { customFields: { $elemMatch: { value: regex } } },
 | 
	
		
			
				|  |  | -          {
 | 
	
		
			
				|  |  | -            _id: {
 | 
	
		
			
				|  |  | -              $in: CardComments.textSearch(userId, [queryParams.text]).map(
 | 
	
		
			
				|  |  | -                com => com.cardId,
 | 
	
		
			
				|  |  | -              ),
 | 
	
		
			
				|  |  | -            },
 | 
	
		
			
				|  |  | -          },
 | 
	
		
			
				|  |  | +          // {
 | 
	
		
			
				|  |  | +          //   _id: {
 | 
	
		
			
				|  |  | +          //     $in: CardComments.textSearch(userId, [queryParams.text]).map(
 | 
	
		
			
				|  |  | +          //       com => com.cardId,
 | 
	
		
			
				|  |  | +          //     ),
 | 
	
		
			
				|  |  | +          //   },
 | 
	
		
			
				|  |  | +          // },
 | 
	
		
			
				|  |  |            { _id: { $in: checklists.map(list => list.cardId) } },
 | 
	
		
			
				|  |  |            { _id: { $in: attachments.map(attach => attach.cardId) } },
 | 
	
		
			
				|  |  | -          // { _id: { $in: comments.map(com => com.cardId) } },
 | 
	
		
			
				|  |  | +          { _id: { $in: comments.map(com => com.cardId) } },
 | 
	
		
			
				|  |  |          ],
 | 
	
		
			
				|  |  |        });
 | 
	
		
			
				|  |  |      }
 | 
	
	
		
			
				|  | @@ -607,6 +447,29 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |    // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  |    // console.log('selector.$and:', selector.$and);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +  const query = new Query();
 | 
	
		
			
				|  |  | +  query.selector = selector;
 | 
	
		
			
				|  |  | +  query.params = queryParams;
 | 
	
		
			
				|  |  | +  query._errors = errors;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  return query;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +function buildProjection(query) {
 | 
	
		
			
				|  |  | +  let skip = 0;
 | 
	
		
			
				|  |  | +  if (query.params.skip) {
 | 
	
		
			
				|  |  | +    skip = query.params.skip;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +  let limit = DEFAULT_LIMIT;
 | 
	
		
			
				|  |  | +  const configLimit = parseInt(process.env.RESULTS_PER_PAGE, 10);
 | 
	
		
			
				|  |  | +  if (!isNaN(configLimit) && configLimit > 0) {
 | 
	
		
			
				|  |  | +    limit = configLimit;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  if (query.params.hasOperator(OPERATOR_LIMIT)) {
 | 
	
		
			
				|  |  | +    limit = query.params.getPredicate(OPERATOR_LIMIT);
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |    const projection = {
 | 
	
		
			
				|  |  |      fields: {
 | 
	
		
			
				|  |  |        _id: 1,
 | 
	
	
		
			
				|  | @@ -636,10 +499,13 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |      limit,
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  if (queryParams.sort) {
 | 
	
		
			
				|  |  | -    const order = queryParams.sort.order === 'asc' ? 1 : -1;
 | 
	
		
			
				|  |  | -    switch (queryParams.sort.name) {
 | 
	
		
			
				|  |  | -      case 'dueAt':
 | 
	
		
			
				|  |  | +  if (query.params.hasOperator(OPERATOR_SORT)) {
 | 
	
		
			
				|  |  | +    const order =
 | 
	
		
			
				|  |  | +      query.params.getPredicate(OPERATOR_SORT).order === ORDER_ASCENDING
 | 
	
		
			
				|  |  | +        ? 1
 | 
	
		
			
				|  |  | +        : -1;
 | 
	
		
			
				|  |  | +    switch (query.params.getPredicate(OPERATOR_SORT).name) {
 | 
	
		
			
				|  |  | +      case PREDICATE_DUE_AT:
 | 
	
		
			
				|  |  |          projection.sort = {
 | 
	
		
			
				|  |  |            dueAt: order,
 | 
	
		
			
				|  |  |            boardId: 1,
 | 
	
	
		
			
				|  | @@ -648,7 +514,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |            sort: 1,
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  |          break;
 | 
	
		
			
				|  |  | -      case 'modifiedAt':
 | 
	
		
			
				|  |  | +      case PREDICATE_MODIFIED_AT:
 | 
	
		
			
				|  |  |          projection.sort = {
 | 
	
		
			
				|  |  |            modifiedAt: order,
 | 
	
		
			
				|  |  |            boardId: 1,
 | 
	
	
		
			
				|  | @@ -657,7 +523,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |            sort: 1,
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  |          break;
 | 
	
		
			
				|  |  | -      case 'createdAt':
 | 
	
		
			
				|  |  | +      case PREDICATE_CREATED_AT:
 | 
	
		
			
				|  |  |          projection.sort = {
 | 
	
		
			
				|  |  |            createdAt: order,
 | 
	
		
			
				|  |  |            boardId: 1,
 | 
	
	
		
			
				|  | @@ -666,7 +532,7 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |            sort: 1,
 | 
	
		
			
				|  |  |          };
 | 
	
		
			
				|  |  |          break;
 | 
	
		
			
				|  |  | -      case 'system':
 | 
	
		
			
				|  |  | +      case PREDICATE_SYSTEM:
 | 
	
		
			
				|  |  |          projection.sort = {
 | 
	
		
			
				|  |  |            boardId: order,
 | 
	
		
			
				|  |  |            swimlaneId: order,
 | 
	
	
		
			
				|  | @@ -681,77 +547,31 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
 | 
	
		
			
				|  |  |    // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  |    // console.log('projection:', projection);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  return findCards(sessionId, selector, projection, errors);
 | 
	
		
			
				|  |  | -});
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -Meteor.publish('brokenCards', function() {
 | 
	
		
			
				|  |  | -  const user = Users.findOne({ _id: this.userId });
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  const permiitedBoards = [null];
 | 
	
		
			
				|  |  | -  let selector = {};
 | 
	
		
			
				|  |  | -  selector.$or = [
 | 
	
		
			
				|  |  | -    { permission: 'public' },
 | 
	
		
			
				|  |  | -    { members: { $elemMatch: { userId: user._id, isActive: true } } },
 | 
	
		
			
				|  |  | -  ];
 | 
	
		
			
				|  |  | +  query.projection = projection;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  Boards.find(selector).forEach(board => {
 | 
	
		
			
				|  |  | -    permiitedBoards.push(board._id);
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | +  return query;
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  selector = {
 | 
	
		
			
				|  |  | -    boardId: { $in: permiitedBoards },
 | 
	
		
			
				|  |  | -    $or: [
 | 
	
		
			
				|  |  | -      { boardId: { $in: [null, ''] } },
 | 
	
		
			
				|  |  | -      { swimlaneId: { $in: [null, ''] } },
 | 
	
		
			
				|  |  | -      { listId: { $in: [null, ''] } },
 | 
	
		
			
				|  |  | -    ],
 | 
	
		
			
				|  |  | -  };
 | 
	
		
			
				|  |  | +function buildQuery(queryParams) {
 | 
	
		
			
				|  |  | +  const query = buildSelector(queryParams);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  const cards = Cards.find(selector, {
 | 
	
		
			
				|  |  | -    fields: {
 | 
	
		
			
				|  |  | -      _id: 1,
 | 
	
		
			
				|  |  | -      archived: 1,
 | 
	
		
			
				|  |  | -      boardId: 1,
 | 
	
		
			
				|  |  | -      swimlaneId: 1,
 | 
	
		
			
				|  |  | -      listId: 1,
 | 
	
		
			
				|  |  | -      title: 1,
 | 
	
		
			
				|  |  | -      type: 1,
 | 
	
		
			
				|  |  | -      sort: 1,
 | 
	
		
			
				|  |  | -      members: 1,
 | 
	
		
			
				|  |  | -      assignees: 1,
 | 
	
		
			
				|  |  | -      colors: 1,
 | 
	
		
			
				|  |  | -      dueAt: 1,
 | 
	
		
			
				|  |  | -    },
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | +  return buildProjection(query);
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  const boards = [];
 | 
	
		
			
				|  |  | -  const swimlanes = [];
 | 
	
		
			
				|  |  | -  const lists = [];
 | 
	
		
			
				|  |  | -  const users = [];
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -  cards.forEach(card => {
 | 
	
		
			
				|  |  | -    if (card.boardId) boards.push(card.boardId);
 | 
	
		
			
				|  |  | -    if (card.swimlaneId) swimlanes.push(card.swimlaneId);
 | 
	
		
			
				|  |  | -    if (card.listId) lists.push(card.listId);
 | 
	
		
			
				|  |  | -    if (card.members) {
 | 
	
		
			
				|  |  | -      card.members.forEach(userId => {
 | 
	
		
			
				|  |  | -        users.push(userId);
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -    if (card.assignees) {
 | 
	
		
			
				|  |  | -      card.assignees.forEach(userId => {
 | 
	
		
			
				|  |  | -        users.push(userId);
 | 
	
		
			
				|  |  | -      });
 | 
	
		
			
				|  |  | -    }
 | 
	
		
			
				|  |  | -  });
 | 
	
		
			
				|  |  | +Meteor.publish('brokenCards', function(sessionId) {
 | 
	
		
			
				|  |  | +  check(sessionId, String);
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  return [
 | 
	
		
			
				|  |  | -    cards,
 | 
	
		
			
				|  |  | -    Boards.find({ _id: { $in: boards } }),
 | 
	
		
			
				|  |  | -    Swimlanes.find({ _id: { $in: swimlanes } }),
 | 
	
		
			
				|  |  | -    Lists.find({ _id: { $in: lists } }),
 | 
	
		
			
				|  |  | -    Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
 | 
	
		
			
				|  |  | +  const params = new QueryParams();
 | 
	
		
			
				|  |  | +  params.addPredicate(OPERATOR_STATUS, PREDICATE_ALL);
 | 
	
		
			
				|  |  | +  const query = buildQuery(params);
 | 
	
		
			
				|  |  | +  query.selector.$or = [
 | 
	
		
			
				|  |  | +    { boardId: { $in: [null, ''] } },
 | 
	
		
			
				|  |  | +    { swimlaneId: { $in: [null, ''] } },
 | 
	
		
			
				|  |  | +    { listId: { $in: [null, ''] } },
 | 
	
		
			
				|  |  |    ];
 | 
	
		
			
				|  |  | +  // console.log('brokenCards selector:', query.selector);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  return findCards(sessionId, query);
 | 
	
		
			
				|  |  |  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  Meteor.publish('nextPage', function(sessionId) {
 | 
	
	
		
			
				|  | @@ -761,7 +581,7 @@ Meteor.publish('nextPage', function(sessionId) {
 | 
	
		
			
				|  |  |    const projection = session.getProjection();
 | 
	
		
			
				|  |  |    projection.skip = session.lastHit;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  return findCards(sessionId, session.getSelector(), projection);
 | 
	
		
			
				|  |  | +  return findCards(sessionId, new Query(session.getSelector(), projection));
 | 
	
		
			
				|  |  |  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  Meteor.publish('previousPage', function(sessionId) {
 | 
	
	
		
			
				|  | @@ -771,19 +591,20 @@ Meteor.publish('previousPage', function(sessionId) {
 | 
	
		
			
				|  |  |    const projection = session.getProjection();
 | 
	
		
			
				|  |  |    projection.skip = session.lastHit - session.resultsCount - projection.limit;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  return findCards(sessionId, session.getSelector(), projection);
 | 
	
		
			
				|  |  | +  return findCards(sessionId, new Query(session.getSelector(), projection));
 | 
	
		
			
				|  |  |  });
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -function findCards(sessionId, selector, projection, errors = null) {
 | 
	
		
			
				|  |  | +function findCards(sessionId, query) {
 | 
	
		
			
				|  |  |    const userId = Meteor.userId();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  | -  // console.log('selector:', selector);
 | 
	
		
			
				|  |  | +  // console.log('selector:', query.selector);
 | 
	
		
			
				|  |  | +  // console.log('selector.$and:', query.selector.$and);
 | 
	
		
			
				|  |  |    // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  |    // console.log('projection:', projection);
 | 
	
		
			
				|  |  |    let cards;
 | 
	
		
			
				|  |  | -  if (!errors || !errors.hasErrors()) {
 | 
	
		
			
				|  |  | -    cards = Cards.find(selector, projection);
 | 
	
		
			
				|  |  | +  if (!query.hasErrors()) {
 | 
	
		
			
				|  |  | +    cards = Cards.find(query.selector, query.projection);
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |    // eslint-disable-next-line no-console
 | 
	
		
			
				|  |  |    // console.log('count:', cards.count());
 | 
	
	
		
			
				|  | @@ -794,19 +615,17 @@ function findCards(sessionId, selector, projection, errors = null) {
 | 
	
		
			
				|  |  |        lastHit: 0,
 | 
	
		
			
				|  |  |        resultsCount: 0,
 | 
	
		
			
				|  |  |        cards: [],
 | 
	
		
			
				|  |  | -      selector: SessionData.pickle(selector),
 | 
	
		
			
				|  |  | -      projection: SessionData.pickle(projection),
 | 
	
		
			
				|  |  | +      selector: SessionData.pickle(query.selector),
 | 
	
		
			
				|  |  | +      projection: SessionData.pickle(query.projection),
 | 
	
		
			
				|  |  | +      errors: query.errors(),
 | 
	
		
			
				|  |  |      },
 | 
	
		
			
				|  |  |    };
 | 
	
		
			
				|  |  | -  if (errors) {
 | 
	
		
			
				|  |  | -    update.$set.errors = errors.errorMessages();
 | 
	
		
			
				|  |  | -  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |    if (cards) {
 | 
	
		
			
				|  |  |      update.$set.totalHits = cards.count();
 | 
	
		
			
				|  |  |      update.$set.lastHit =
 | 
	
		
			
				|  |  | -      projection.skip + projection.limit < cards.count()
 | 
	
		
			
				|  |  | -        ? projection.skip + projection.limit
 | 
	
		
			
				|  |  | +      query.projection.skip + query.projection.limit < cards.count()
 | 
	
		
			
				|  |  | +        ? query.projection.skip + query.projection.limit
 | 
	
		
			
				|  |  |          : cards.count();
 | 
	
		
			
				|  |  |      update.$set.cards = cards.map(card => {
 | 
	
		
			
				|  |  |        return card._id;
 | 
	
	
		
			
				|  | @@ -884,5 +703,5 @@ function findCards(sessionId, selector, projection, errors = null) {
 | 
	
		
			
				|  |  |      ];
 | 
	
		
			
				|  |  |    }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -  return [SessionData.find({ userId: userId, sessionId })];
 | 
	
		
			
				|  |  | +  return [SessionData.find({ userId, sessionId })];
 | 
	
		
			
				|  |  |  }
 |