Implement advanced search queries

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2021-04-01 11:45:11 +02:00
parent 88a5e420b9
commit 840c143b92
23 changed files with 942 additions and 99 deletions

View File

@@ -24,10 +24,14 @@
namespace OCA\Deck\Db;
use Exception;
use OCA\Deck\AppInfo\Application;
use OCA\Deck\Search\Query\SearchQuery;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IGroupManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
@@ -37,6 +41,8 @@ class CardMapper extends QBMapper implements IPermissionMapper {
private $labelMapper;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var IManager */
private $notificationManager;
private $databaseType;
@@ -46,6 +52,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
IDBConnection $db,
LabelMapper $labelMapper,
IUserManager $userManager,
IGroupManager $groupManager,
IManager $notificationManager,
$databaseType = 'sqlite',
$database4ByteSupport = true
@@ -53,6 +60,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
parent::__construct($db, 'deck_cards', Card::class);
$this->labelMapper = $labelMapper;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->notificationManager = $notificationManager;
$this->databaseType = $databaseType;
$this->database4ByteSupport = $database4ByteSupport;
@@ -117,7 +125,7 @@ class CardMapper extends QBMapper implements IPermissionMapper {
->addOrderBy('id');
/** @var Card $card */
$card = $this->findEntity($qb);
$labels = $this->labelMapper->findAssignedLabelsForCard($card->id);
$labels = $this->labelMapper->findAssignedLabelsForCard($card->getId());
$card->setLabels($labels);
$this->mapOwner($card);
return $card;
@@ -260,25 +268,202 @@ class CardMapper extends QBMapper implements IPermissionMapper {
return $this->findEntities($qb);
}
public function search($boardIds, $term, $limit = null, $offset = null) {
public function search(array $boardIds, SearchQuery $query, int $limit = null, int $offset = null): array {
$qb = $this->queryCardsByBoards($boardIds);
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%')),
$qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%'))
)
);
$this->extendQueryByFilter($qb, $query);
if (count($query->getTextTokens()) > 0) {
$tokenMatching = $qb->expr()->andX(
...array_map(function (string $token) use ($qb) {
return $qb->expr()->orX(
$qb->expr()->iLike(
'c.title',
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
IQueryBuilder::PARAM_STR
),
$qb->expr()->iLike(
'c.description',
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
IQueryBuilder::PARAM_STR
)
);
}, $query->getTextTokens())
);
$qb->andWhere(
$tokenMatching
);
}
$qb->groupBy('c.id');
$qb->orderBy('c.last_modified', 'DESC');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
$qb->andWhere($qb->expr()->lt('c.last_modified', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
}
return $this->findEntities($qb);
$result = $qb->execute();
$entities = [];
while ($row = $result->fetch()) {
$entities[] = Card::fromRow($row);
}
$result->closeCursor();
return $entities;
}
public function searchComments(array $boardIds, SearchQuery $query, int $limit = null, int $offset = null): array {
if (count($query->getTextTokens()) === 0) {
return [];
}
$qb = $this->queryCardsByBoards($boardIds);
$this->extendQueryByFilter($qb, $query);
$qb->innerJoin('c', 'comments', 'comments', $qb->expr()->andX(
$qb->expr()->eq('comments.object_id', 'c.id', IQueryBuilder::PARAM_STR),
$qb->expr()->eq('comments.object_type', $qb->createNamedParameter(Application::COMMENT_ENTITY_TYPE, IQueryBuilder::PARAM_STR))
));
$qb->selectAlias('comments.id', 'comment_id');
$tokenMatching = $qb->expr()->andX(
...array_map(function (string $token) use ($qb) {
return $qb->expr()->iLike(
'comments.message',
$qb->createNamedParameter('%' . $this->db->escapeLikeParameter($token) . '%', IQueryBuilder::PARAM_STR),
IQueryBuilder::PARAM_STR
);
}, $query->getTextTokens())
);
$qb->andWhere(
$tokenMatching
);
$qb->groupBy('comments.id');
$qb->orderBy('comments.id', 'DESC');
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->andWhere($qb->expr()->lt('comments.id', $qb->createNamedParameter($offset, IQueryBuilder::PARAM_INT)));
}
$result = $qb->execute();
$entities = $result->fetchAll();
$result->closeCursor();
return $entities;
}
private function extendQueryByFilter(IQueryBuilder $qb, SearchQuery $query) {
$qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere($qb->expr()->eq('s.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
$qb->innerJoin('s', 'deck_boards', 'b', $qb->expr()->eq('b.id', 's.board_id'));
$qb->andWhere($qb->expr()->eq('b.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
foreach ($query->getTitle() as $title) {
$qb->andWhere($qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($title->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
foreach ($query->getDescription() as $description) {
$qb->andWhere($qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($description->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
foreach ($query->getStack() as $stack) {
$qb->andWhere($qb->expr()->iLike('s.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($stack->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
if (count($query->getTag())) {
foreach ($query->getTag() as $index => $tag) {
$qb->innerJoin('c', 'deck_assigned_labels', 'al' . $index, $qb->expr()->eq('c.id', 'al' . $index . '.card_id'));
$qb->innerJoin('al'. $index, 'deck_labels', 'l' . $index, $qb->expr()->eq('al' . $index . '.label_id', 'l' . $index . '.id'));
$qb->andWhere($qb->expr()->iLike('l' . $index . '.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($tag->getValue()) . '%', IQueryBuilder::PARAM_STR)));
}
}
foreach ($query->getDuedate() as $duedate) {
$date = $duedate->getValue();
$supportedFilters = ['overdue', 'today', 'week', 'month', 'none'];
if (in_array($date, $supportedFilters, true)) {
$currentDate = new \DateTime();
$rangeDate = new \DateTime();
if ($date === 'overdue') {
$qb->andWhere($qb->expr()->lt('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
} elseif ($date === 'today') {
$rangeDate->add(new \DateInterval('P1D'));
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($rangeDate, IQueryBuilder::PARAM_DATE)));
} elseif ($date === 'week') {
$rangeDate->add(new \DateInterval('P7D'));
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($rangeDate, IQueryBuilder::PARAM_DATE)));
} elseif ($date === 'month') {
$rangeDate->add(new \DateInterval('P1M'));
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($currentDate, IQueryBuilder::PARAM_DATE)));
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($rangeDate, IQueryBuilder::PARAM_DATE)));
} else {
$qb->andWhere($qb->expr()->isNull('c.duedate'));
}
}
try {
$date = new \DateTime($date);
if ($duedate->getComparator() === SearchQuery::COMPARATOR_LESS) {
$qb->andWhere($qb->expr()->lt('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_LESS_EQUAL) {
// take the end of the day to include due dates at the same day (as datetime does't allow just setting the day)
$date->setTime(23, 59, 59);
$qb->andWhere($qb->expr()->lte('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_MORE) {
// take the end of the day to exclude due dates at the same day (as datetime does't allow just setting the day)
$date->setTime(23, 59, 59);
$qb->andWhere($qb->expr()->gt('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
} elseif ($duedate->getComparator() === SearchQuery::COMPARATOR_MORE_EQUAL) {
$qb->andWhere($qb->expr()->gte('c.duedate', $qb->createNamedParameter($date, IQueryBuilder::PARAM_DATE)));
}
} catch (Exception $e) {
// Invalid date, ignoring
continue;
}
}
if (count($query->getAssigned()) > 0) {
foreach ($query->getAssigned() as $index => $assignment) {
$qb->innerJoin('c', 'deck_assigned_users', 'au' . $index, $qb->expr()->eq('c.id', 'au' . $index . '.card_id'));
$assignedQueryValue = $assignment->getValue();
$searchUsers = $this->userManager->searchDisplayName($assignment->getValue());
$users = array_filter($searchUsers, function (IUser $user) use ($assignedQueryValue) {
return (mb_strtolower($user->getDisplayName()) === mb_strtolower($assignedQueryValue) || $user->getUID() === $assignedQueryValue);
});
$groups = $this->groupManager->search($assignment->getValue());
foreach ($searchUsers as $user) {
$groups = array_merge($groups, $this->groupManager->getUserIdGroups($user->getUID()));
}
$assignmentSearches = [];
$hasAssignedMatches = false;
foreach ($users as $user) {
$hasAssignedMatches = true;
$assignmentSearches[] = $qb->expr()->andX(
$qb->expr()->eq('au' . $index . '.participant', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('au' . $index . '.type', $qb->createNamedParameter(Assignment::TYPE_USER, IQueryBuilder::PARAM_INT))
);
}
foreach ($groups as $group) {
$hasAssignedMatches = true;
$assignmentSearches[] = $qb->expr()->andX(
$qb->expr()->eq('au' . $index . '.participant', $qb->createNamedParameter($group->getGID(), IQueryBuilder::PARAM_STR)),
$qb->expr()->eq('au' . $index . '.type', $qb->createNamedParameter(Assignment::TYPE_GROUP, IQueryBuilder::PARAM_INT))
);
}
if (!$hasAssignedMatches) {
return [];
}
$qb->andWhere($qb->expr()->orX(...$assignmentSearches));
}
}
}
public function searchRaw($boardIds, $term, $limit = null, $offset = null) {
$qb = $this->queryCardsByBoards($boardIds)
->select('s.board_id', 'board_id')