wekan/server/publications/cards.js

703 lines
19 KiB
JavaScript
Raw Normal View History

2021-03-10 12:39:39 +02:00
import moment from 'moment';
2021-03-07 02:12:31 +02:00
import Users from '../../models/users';
import Boards from '../../models/boards';
import Lists from '../../models/lists';
import Swimlanes from '../../models/swimlanes';
2021-03-10 12:39:39 +02:00
import Cards from '../../models/cards';
2021-03-07 02:12:31 +02:00
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';
2021-03-07 02:12:31 +02:00
const escapeForRegex = require('escape-string-regexp');
Meteor.publish('card', cardId => {
check(cardId, String);
return Cards.find({ _id: cardId });
});
2020-12-31 19:14:55 +02:00
Meteor.publish('myCards', function(sessionId) {
const queryParams = new QueryParams();
queryParams.addPredicate(OPERATOR_USER, Meteor.user().username);
return findCards(sessionId, buildQuery(queryParams));
2020-12-31 19:14:55 +02:00
});
2021-01-07 22:36:10 +02:00
// 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) {
2021-01-24 12:28:36 +02:00
check(sessionId, String);
check(params, Object);
// eslint-disable-next-line no-console
2021-03-10 12:40:20 +02:00
// console.log('queryParams:', params);
return findCards(sessionId, buildQuery(new QueryParams(params)));
});
function buildSelector(queryParams) {
const userId = Meteor.userId();
2021-03-07 02:12:31 +02:00
const errors = new QueryErrors();
let selector = {};
2021-01-27 02:21:12 +02:00
if (queryParams.selector) {
selector = queryParams.selector;
} else {
const boardsSelector = {};
2021-01-27 02:21:12 +02:00
let archived = false;
let endAt = null;
if (queryParams.hasOperator(OPERATOR_STATUS)) {
queryParams.getPredicates(OPERATOR_STATUS).forEach(status => {
if (status === PREDICATE_ARCHIVED) {
2021-01-27 02:21:12 +02:00
archived = true;
} else if (status === PREDICATE_ALL) {
2021-01-27 02:21:12 +02:00
archived = null;
} else if (status === PREDICATE_ENDED) {
2021-01-27 02:21:12 +02:00
endAt = { $nin: [null, ''] };
} else if ([PREDICATE_PRIVATE, PREDICATE_PUBLIC].includes(status)) {
boardsSelector.permission = status;
2021-01-27 02:21:12 +02:00
}
});
}
2021-01-27 02:21:12 +02:00
selector = {
type: 'cardType-card',
// boardId: { $in: Boards.userBoardIds(userId) },
$and: [],
};
2021-01-27 02:21:12 +02:00
if (archived !== null) {
if (archived) {
selector.boardId = {
$in: Boards.userBoardIds(userId, null, boardsSelector),
};
2021-01-27 02:21:12 +02:00
selector.$and.push({
$or: [
{
boardId: {
$in: Boards.userBoardIds(userId, archived, boardsSelector),
},
},
2021-01-27 02:21:12 +02:00
{ swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } },
{ listId: { $in: Lists.archivedListIds() } },
{ archived: true },
],
});
} else {
selector.boardId = {
$in: Boards.userBoardIds(userId, false, boardsSelector),
};
2021-01-27 02:21:12 +02:00
selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() };
selector.listId = { $nin: Lists.archivedListIds() };
selector.archived = false;
}
2021-01-27 02:21:12 +02:00
} else {
selector.boardId = {
$in: Boards.userBoardIds(userId, null, boardsSelector),
};
2021-01-27 02:21:12 +02:00
}
if (endAt !== null) {
selector.endAt = endAt;
}
if (queryParams.hasOperator(OPERATOR_BOARD)) {
2021-01-27 02:21:12 +02:00
const queryBoards = [];
queryParams.hasOperator(OPERATOR_BOARD).forEach(query => {
2021-01-27 02:21:12 +02:00
const boards = Boards.userSearch(userId, {
title: new RegExp(escapeForRegex(query), 'i'),
});
2021-01-27 02:21:12 +02:00
if (boards.count()) {
boards.forEach(board => {
queryBoards.push(board._id);
});
} else {
errors.addNotFound(OPERATOR_BOARD, query);
2021-01-27 02:21:12 +02:00
}
});
2021-01-27 02:21:12 +02:00
selector.boardId.$in = queryBoards;
}
if (queryParams.hasOperator(OPERATOR_SWIMLANE)) {
2021-01-27 02:21:12 +02:00
const querySwimlanes = [];
queryParams.getPredicates(OPERATOR_SWIMLANE).forEach(query => {
2021-01-27 02:21:12 +02:00
const swimlanes = Swimlanes.find({
title: new RegExp(escapeForRegex(query), 'i'),
});
if (swimlanes.count()) {
swimlanes.forEach(swim => {
querySwimlanes.push(swim._id);
});
} else {
errors.addNotFound(OPERATOR_SWIMLANE, query);
2021-01-27 02:21:12 +02:00
}
});
2021-01-27 02:21:12 +02:00
2021-02-21 01:39:34 +02:00
// eslint-disable-next-line no-prototype-builtins
if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) {
selector.swimlaneId = { $in: [] };
}
2021-01-27 02:21:12 +02:00
selector.swimlaneId.$in = querySwimlanes;
}
if (queryParams.hasOperator(OPERATOR_LIST)) {
2021-01-27 02:21:12 +02:00
const queryLists = [];
queryParams.getPredicates(OPERATOR_LIST).forEach(query => {
2021-01-27 02:21:12 +02:00
const lists = Lists.find({
title: new RegExp(escapeForRegex(query), 'i'),
});
2021-01-27 02:21:12 +02:00
if (lists.count()) {
lists.forEach(list => {
queryLists.push(list._id);
});
} else {
errors.addNotFound(OPERATOR_LIST, query);
2021-01-27 02:21:12 +02:00
}
});
2021-02-21 01:39:34 +02:00
// eslint-disable-next-line no-prototype-builtins
if (!selector.hasOwnProperty('listId')) {
selector.listId = { $in: [] };
}
2021-01-27 02:21:12 +02:00
selector.listId.$in = queryLists;
}
if (queryParams.hasOperator(OPERATOR_COMMENT)) {
const cardIds = CardComments.textSearch(
userId,
queryParams.getPredicates(OPERATOR_COMMENT),
2021-01-27 02:21:12 +02:00
com => {
return com.cardId;
},
);
if (cardIds.length) {
selector._id = { $in: cardIds };
} else {
queryParams.getPredicates(OPERATOR_COMMENT).forEach(comment => {
errors.addNotFound(OPERATOR_COMMENT, comment);
});
2021-01-27 02:21:12 +02:00
}
}
[OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => {
if (queryParams.hasOperator(field)) {
2021-02-26 01:13:45 +02:00
selector[field] = {};
const predicate = queryParams.getPredicate(field);
selector[field][predicate.operator] = new Date(predicate.value);
2021-02-26 01:13:45 +02:00
}
});
const queryUsers = {};
queryUsers[OPERATOR_ASSIGNEE] = [];
queryUsers[OPERATOR_MEMBER] = [];
if (queryParams.hasOperator(OPERATOR_USER)) {
queryParams.getPredicates(OPERATOR_USER).forEach(query => {
2021-01-27 02:21:12 +02:00
const users = Users.find({
username: query,
});
2021-01-27 02:21:12 +02:00
if (users.count()) {
users.forEach(user => {
queryUsers[OPERATOR_MEMBER].push(user._id);
queryUsers[OPERATOR_ASSIGNEE].push(user._id);
2021-01-27 02:21:12 +02:00
});
} else {
errors.addNotFound(OPERATOR_USER, query);
2021-01-27 02:21:12 +02:00
}
});
2021-01-27 02:21:12 +02:00
}
[OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => {
if (queryParams.hasOperator(key)) {
queryParams.getPredicates(key).forEach(query => {
2021-03-07 02:12:31 +02:00
const users = Users.find({
username: query,
2021-01-27 02:21:12 +02:00
});
2021-03-07 02:12:31 +02:00
if (users.count()) {
users.forEach(user => {
queryUsers[key].push(user._id);
});
} else {
errors.addNotFound(key, query);
2021-03-07 02:12:31 +02:00
}
2021-01-27 02:21:12 +02:00
});
2021-03-07 02:12:31 +02:00
}
});
if (
queryUsers[OPERATOR_MEMBER].length &&
queryUsers[OPERATOR_ASSIGNEE].length
) {
2021-01-27 02:21:12 +02:00
selector.$and.push({
$or: [
{ members: { $in: queryUsers[OPERATOR_MEMBER] } },
{ assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } },
2021-01-27 02:21:12 +02:00
],
});
} else if (queryUsers[OPERATOR_MEMBER].length) {
selector.members = { $in: queryUsers[OPERATOR_MEMBER] };
} else if (queryUsers[OPERATOR_ASSIGNEE].length) {
selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] };
2021-01-27 02:21:12 +02:00
}
if (queryParams.hasOperator(OPERATOR_LABEL)) {
queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
2021-01-27 02:21:12 +02:00
const queryLabels = [];
let boards = Boards.userSearch(userId, {
labels: { $elemMatch: { color: label.toLowerCase() } },
});
if (boards.count()) {
boards.forEach(board => {
2021-01-27 02:21:12 +02:00
// eslint-disable-next-line no-console
// console.log('board:', board);
// eslint-disable-next-line no-console
// console.log('board.labels:', board.labels);
board.labels
.filter(boardLabel => {
2021-01-27 02:21:12 +02:00
return boardLabel.color === label.toLowerCase();
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
2021-01-27 02:21:12 +02:00
// eslint-disable-next-line no-console
// console.log('label:', label);
const reLabel = new RegExp(escapeForRegex(label), 'i');
// eslint-disable-next-line no-console
// console.log('reLabel:', reLabel);
boards = Boards.userSearch(userId, {
labels: { $elemMatch: { name: reLabel } },
});
2021-01-27 02:21:12 +02:00
if (boards.count()) {
boards.forEach(board => {
board.labels
.filter(boardLabel => {
2021-03-04 18:35:02 +02:00
if (!boardLabel.name) {
return false;
}
2021-01-27 02:21:12 +02:00
return boardLabel.name.match(reLabel);
})
.forEach(boardLabel => {
queryLabels.push(boardLabel._id);
});
});
} else {
errors.addNotFound(OPERATOR_LABEL, label);
2021-01-27 02:21:12 +02:00
}
}
2021-03-07 02:12:31 +02:00
selector.labelIds = { $in: _.uniq(queryLabels) };
2021-01-27 02:21:12 +02:00
});
}
if (queryParams.hasOperator(OPERATOR_HAS)) {
queryParams.getPredicates(OPERATOR_HAS).forEach(has => {
2021-02-27 02:26:58 +02:00
switch (has.field) {
case PREDICATE_ATTACHMENT:
selector.$and.push({
_id: {
$in: Attachments.find({}, { fields: { cardId: 1 } }).map(
a => a.cardId,
),
},
});
2021-02-27 02:26:58 +02:00
break;
case PREDICATE_CHECKLIST:
selector.$and.push({
_id: {
$in: Checklists.find({}, { fields: { cardId: 1 } }).map(
a => a.cardId,
),
},
});
2021-02-27 02:26:58 +02:00
break;
case PREDICATE_DESCRIPTION:
case PREDICATE_START_AT:
case PREDICATE_DUE_AT:
case PREDICATE_END_AT:
2021-02-27 02:26:58 +02:00
if (has.exists) {
selector[has.field] = { $exists: true, $nin: [null, ''] };
} else {
selector[has.field] = { $in: [null, ''] };
}
break;
case PREDICATE_ASSIGNEES:
case PREDICATE_MEMBERS:
2021-02-27 02:26:58 +02:00
if (has.exists) {
selector[has.field] = { $exists: true, $nin: [null, []] };
} else {
selector[has.field] = { $in: [null, []] };
}
break;
2021-02-21 01:41:58 +02:00
}
});
}
if (queryParams.text) {
const regex = new RegExp(escapeForRegex(queryParams.text), 'i');
2021-02-21 01:39:34 +02:00
const items = ChecklistItems.find(
{ title: regex },
{ fields: { cardId: 1 } },
);
const checklists = Checklists.find(
{
$or: [
{ title: regex },
{ _id: { $in: items.map(item => item.checklistId) } },
],
},
{ fields: { cardId: 1 } },
);
const attachments = Attachments.find({ 'original.name': regex });
2021-03-10 12:39:39 +02:00
const comments = CardComments.find(
{ text: regex },
{ fields: { cardId: 1 } },
);
selector.$and.push({
$or: [
{ title: regex },
{ description: regex },
{ customFields: { $elemMatch: { value: regex } } },
2021-03-10 12:39:39 +02:00
// {
// _id: {
// $in: CardComments.textSearch(userId, [queryParams.text]).map(
// com => com.cardId,
// ),
// },
// },
2021-02-20 18:35:44 +02:00
{ _id: { $in: checklists.map(list => list.cardId) } },
2021-02-21 01:39:34 +02:00
{ _id: { $in: attachments.map(attach => attach.cardId) } },
2021-03-10 12:39:39 +02:00
{ _id: { $in: comments.map(com => com.cardId) } },
],
});
}
if (selector.$and.length === 0) {
delete selector.$and;
}
2021-01-27 02:21:12 +02:00
}
2021-01-27 02:21:12 +02:00
// eslint-disable-next-line no-console
// console.log('selector:', selector);
2021-01-27 02:21:12 +02:00
// 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;
if (query.params.hasOperator(OPERATOR_LIMIT)) {
limit = query.params.getPredicate(OPERATOR_LIMIT);
}
const projection = {
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,
createdAt: 1,
modifiedAt: 1,
labelIds: 1,
customFields: 1,
},
sort: {
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
},
skip,
limit,
};
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,
swimlaneId: 1,
listId: 1,
sort: 1,
};
break;
case PREDICATE_MODIFIED_AT:
projection.sort = {
modifiedAt: order,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
break;
case PREDICATE_CREATED_AT:
projection.sort = {
createdAt: order,
boardId: 1,
swimlaneId: 1,
listId: 1,
sort: 1,
};
break;
case PREDICATE_SYSTEM:
projection.sort = {
boardId: order,
swimlaneId: order,
listId: order,
modifiedAt: order,
sort: order,
};
break;
}
}
// eslint-disable-next-line no-console
// console.log('projection:', projection);
query.projection = projection;
return query;
}
function buildQuery(queryParams) {
const query = buildSelector(queryParams);
return buildProjection(query);
}
Meteor.publish('brokenCards', function(sessionId) {
check(sessionId, String);
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, ''] } },
];
2021-03-10 12:40:20 +02:00
// console.log('brokenCards selector:', query.selector);
return findCards(sessionId, query);
});
Meteor.publish('nextPage', function(sessionId) {
check(sessionId, String);
const session = SessionData.findOne({ sessionId });
const projection = session.getProjection();
projection.skip = session.lastHit;
return findCards(sessionId, new Query(session.getSelector(), projection));
});
Meteor.publish('previousPage', function(sessionId) {
check(sessionId, String);
const session = SessionData.findOne({ sessionId });
const projection = session.getProjection();
projection.skip = session.lastHit - session.resultsCount - projection.limit;
return findCards(sessionId, new Query(session.getSelector(), projection));
});
function findCards(sessionId, query) {
const userId = Meteor.userId();
2021-02-27 00:56:12 +02:00
// eslint-disable-next-line no-console
2021-03-10 12:40:20 +02:00
// console.log('selector:', query.selector);
// console.log('selector.$and:', query.selector.$and);
2021-02-27 00:56:12 +02:00
// eslint-disable-next-line no-console
// console.log('projection:', projection);
let cards;
if (!query.hasErrors()) {
cards = Cards.find(query.selector, query.projection);
}
2021-02-27 00:56:12 +02:00
// eslint-disable-next-line no-console
// console.log('count:', cards.count());
2021-01-24 02:32:37 +02:00
const update = {
$set: {
totalHits: 0,
lastHit: 0,
2021-01-27 02:21:12 +02:00
resultsCount: 0,
2021-01-24 02:32:37 +02:00
cards: [],
selector: SessionData.pickle(query.selector),
projection: SessionData.pickle(query.projection),
errors: query.errors(),
2021-01-24 02:32:37 +02:00
},
};
2021-01-24 02:32:37 +02:00
if (cards) {
update.$set.totalHits = cards.count();
2021-01-27 02:21:12 +02:00
update.$set.lastHit =
query.projection.skip + query.projection.limit < cards.count()
? query.projection.skip + query.projection.limit
: cards.count();
2021-01-24 02:32:37 +02:00
update.$set.cards = cards.map(card => {
return card._id;
});
2021-01-27 02:21:12 +02:00
update.$set.resultsCount = update.$set.cards.length;
}
2021-01-25 16:12:33 +02:00
SessionData.upsert({ userId, sessionId }, update);
// remove old session data
SessionData.remove({
userId,
modifiedAt: {
$lt: new Date(
moment()
.subtract(1, 'day')
.format(),
),
},
});
2021-01-24 02:32:37 +02:00
if (cards) {
const boards = [];
const swimlanes = [];
const lists = [];
const customFieldIds = [];
const users = [this.userId];
2021-01-24 02:32:37 +02:00
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);
});
}
if (card.customFields) {
card.customFields.forEach(field => {
customFieldIds.push(field._id);
});
}
2021-01-24 02:32:37 +02:00
});
const fields = {
_id: 1,
title: 1,
archived: 1,
2021-01-25 15:55:35 +02:00
sort: 1,
type: 1,
};
return [
cards,
2021-01-25 15:55:35 +02:00
Boards.find(
{ _id: { $in: boards } },
{ fields: { ...fields, labels: 1, color: 1 } },
),
Swimlanes.find(
{ _id: { $in: swimlanes } },
{ fields: { ...fields, color: 1 } },
),
Lists.find({ _id: { $in: lists } }, { fields }),
CustomFields.find({ _id: { $in: customFieldIds } }),
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
Checklists.find({ cardId: { $in: cards.map(c => c._id) } }),
Attachments.find({ cardId: { $in: cards.map(c => c._id) } }),
CardComments.find({ cardId: { $in: cards.map(c => c._id) } }),
SessionData.find({ userId, sessionId }),
];
2021-01-24 02:32:37 +02:00
}
return [SessionData.find({ userId, sessionId })];
}