From 61a1227f87f749dc1afa246bbe8200c91cde9e84 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Mon, 15 Jan 2024 11:18:06 +0200 Subject: [PATCH] user stream ref, add index --- .../Resources/metadata/entityDefs/Note.json | 6 + .../Espo/Tools/Stream/GlobalRecordService.php | 8 +- .../Espo/Tools/Stream/RecordService.php | 6 +- .../Tools/Stream/RecordService/Helper.php | 68 ----- .../Stream/RecordService/QueryHelper.php | 235 ++++++++++++++++++ .../Espo/Tools/Stream/UserRecordService.php | 122 +++------ 6 files changed, 277 insertions(+), 168 deletions(-) create mode 100644 application/Espo/Tools/Stream/RecordService/QueryHelper.php diff --git a/application/Espo/Resources/metadata/entityDefs/Note.json b/application/Espo/Resources/metadata/entityDefs/Note.json index 9a1a939318..249ecaaa87 100644 --- a/application/Espo/Resources/metadata/entityDefs/Note.json +++ b/application/Espo/Resources/metadata/entityDefs/Note.json @@ -35,6 +35,7 @@ "EmailReceived", "EmailSent" ], + "maxLength": 24, "customizationDisabled": true, "default": "Post" }, @@ -47,6 +48,7 @@ "users", "portals" ], + "maxLength": 7, "customizationDisabled": true }, "parent": { @@ -209,6 +211,10 @@ "type": "index", "columns": ["type"] }, + "targetType": { + "type": "index", + "columns": ["targetType"] + }, "parentId": { "type": "index", "columns": ["parentId"] diff --git a/application/Espo/Tools/Stream/GlobalRecordService.php b/application/Espo/Tools/Stream/GlobalRecordService.php index d47eee0ee0..4de3f8752c 100644 --- a/application/Espo/Tools/Stream/GlobalRecordService.php +++ b/application/Espo/Tools/Stream/GlobalRecordService.php @@ -40,7 +40,7 @@ use Espo\Entities\User; use Espo\ORM\Collection; use Espo\ORM\EntityManager; use Espo\ORM\Query\Part\Order; -use Espo\Tools\Stream\RecordService\Helper; +use Espo\Tools\Stream\RecordService\QueryHelper; class GlobalRecordService { @@ -52,7 +52,7 @@ class GlobalRecordService private User $user, private Metadata $metadata, private EntityManager $entityManager, - private Helper $helper, + private QueryHelper $queryHelper, private NoteAccessControl $noteAccessControl ) {} @@ -67,8 +67,8 @@ class GlobalRecordService $maxSize = $searchParams->getMaxSize() ?? 0; - $baseBuilder = $this->helper->buildBaseQueryBuilder($searchParams) - ->select($this->helper->getUserQuerySelect()) + $baseBuilder = $this->queryHelper->buildBaseQueryBuilder($searchParams) + ->select($this->queryHelper->getUserQuerySelect()) ->order('number', Order::DESC) ->where([ 'OR' => [ diff --git a/application/Espo/Tools/Stream/RecordService.php b/application/Espo/Tools/Stream/RecordService.php index 28352c6f88..dd7ad1fcca 100644 --- a/application/Espo/Tools/Stream/RecordService.php +++ b/application/Espo/Tools/Stream/RecordService.php @@ -42,6 +42,7 @@ use Espo\Core\Acl\Table; use Espo\Core\Record\Collection as RecordCollection; use Espo\ORM\Query\SelectBuilder; use Espo\Tools\Stream\RecordService\Helper; +use Espo\Tools\Stream\RecordService\QueryHelper; class RecordService { @@ -50,7 +51,8 @@ class RecordService private User $user, private Acl $acl, private NoteAccessControl $noteAccessControl, - private Helper $helper + private Helper $helper, + private QueryHelper $queryHelper ) {} /** @@ -77,7 +79,7 @@ class RecordService throw new Forbidden(); } - $builder = $this->helper->buildBaseQueryBuilder($searchParams); + $builder = $this->queryHelper->buildBaseQueryBuilder($searchParams); $where = $this->user->isPortal() ? [ diff --git a/application/Espo/Tools/Stream/RecordService/Helper.php b/application/Espo/Tools/Stream/RecordService/Helper.php index b913b4373e..324932bac9 100644 --- a/application/Espo/Tools/Stream/RecordService/Helper.php +++ b/application/Espo/Tools/Stream/RecordService/Helper.php @@ -33,86 +33,18 @@ use Espo\Core\Acl\Exceptions\NotAvailable; use Espo\Core\Acl\Exceptions\NotImplemented as AclNotImplemented; use Espo\Core\Acl\Table; use Espo\Core\AclManager; -use Espo\Core\Exceptions\BadRequest; -use Espo\Core\Exceptions\Forbidden; -use Espo\Core\Select\SearchParams; -use Espo\Core\Select\SelectBuilderFactory; use Espo\Core\Utils\Acl\UserAclManagerProvider; use Espo\Core\Utils\Metadata; -use Espo\Entities\Note; use Espo\Entities\User; -use Espo\ORM\EntityManager; -use Espo\ORM\Query\SelectBuilder as SelectQueryBuilder; class Helper { public function __construct( - private EntityManager $entityManager, - private SelectBuilderFactory $selectBuilderFactory, private Metadata $metadata, private AclManager $aclManager, private UserAclManagerProvider $userAclManagerProvider ) {} - /** - * @return string[] - */ - public function getUserQuerySelect(): array - { - return [ - 'id', - 'number', - 'type', - 'post', - 'data', - 'parentType', - 'parentId', - 'relatedType', - 'relatedId', - 'targetType', - 'createdAt', - 'createdById', - 'createdByName', - 'isGlobal', - 'isInternal', - 'createdByGender', - ]; - } - - /** - * @throws BadRequest - * @throws Forbidden - */ - public function buildBaseQueryBuilder(SearchParams $searchParams): SelectQueryBuilder - { - $builder = $this->entityManager - ->getQueryBuilder() - ->select() - ->from(Note::ENTITY_TYPE); - - if ( - $searchParams->getWhere() || - $searchParams->getTextFilter() || - $searchParams->getPrimaryFilter() || - $searchParams->getBoolFilterList() !== [] - ) { - $builder = $this->selectBuilderFactory - ->create() - ->from(Note::ENTITY_TYPE) - ->withComplexExpressionsForbidden() - ->withWherePermissionCheck() - ->withSearchParams( - $searchParams - ->withOffset(null) - ->withMaxSize(null) - ) - ->buildQueryBuilder() - ->order([]); - } - - return $builder; - } - /** * @return string[] */ diff --git a/application/Espo/Tools/Stream/RecordService/QueryHelper.php b/application/Espo/Tools/Stream/RecordService/QueryHelper.php new file mode 100644 index 0000000000..0c7d64aa47 --- /dev/null +++ b/application/Espo/Tools/Stream/RecordService/QueryHelper.php @@ -0,0 +1,235 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Tools\Stream\RecordService; + +use Espo\Core\Acl\Table; +use Espo\Core\AclManager; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Forbidden; +use Espo\Core\Select\SearchParams; +use Espo\Core\Select\SelectBuilderFactory; +use Espo\Entities\Note; +use Espo\Entities\User; +use Espo\ORM\EntityManager; +use Espo\ORM\Query\Part\Condition as Cond; +use Espo\ORM\Query\Part\Expression as Expr; +use Espo\ORM\Query\Select; +use Espo\ORM\Query\SelectBuilder; + +class QueryHelper +{ + public function __construct( + private EntityManager $entityManager, + private SelectBuilderFactory $selectBuilderFactory, + private AclManager $aclManager + ) {} + + /** + * @throws BadRequest + * @throws Forbidden + */ + public function buildBaseQueryBuilder(SearchParams $searchParams): SelectBuilder + { + $builder = $this->entityManager + ->getQueryBuilder() + ->select() + ->from(Note::ENTITY_TYPE); + + if ( + $searchParams->getWhere() || + $searchParams->getTextFilter() || + $searchParams->getPrimaryFilter() || + $searchParams->getBoolFilterList() !== [] + ) { + $builder = $this->selectBuilderFactory + ->create() + ->from(Note::ENTITY_TYPE) + ->withComplexExpressionsForbidden() + ->withWherePermissionCheck() + ->withSearchParams( + $searchParams + ->withOffset(null) + ->withMaxSize(null) + ) + ->buildQueryBuilder() + ->order([]); + } + + return $builder; + } + + /** + * @return string[] + */ + public function getUserQuerySelect(): array + { + return [ + 'id', + 'number', + 'type', + 'post', + 'data', + 'parentType', + 'parentId', + 'relatedType', + 'relatedId', + 'targetType', + 'createdAt', + 'createdById', + 'createdByName', + 'isGlobal', + 'isInternal', + 'createdByGender', + ]; + } + + public function buildPostedToUserQuery(User $user, SelectBuilder $baseBuilder): Select + { + return (clone $baseBuilder) + ->where([ + 'type' => Note::TYPE_POST, + 'targetType' => Note::TARGET_USERS, + 'parentId' => null, + 'createdById!=' => $user->getId(), + 'isGlobal' => false, + ]) + ->where( + Cond::in( + Expr::column('id'), + SelectBuilder::create() + ->select('noteId') + ->from('NoteUser') + ->where(['userId' => $user->getId()]) + ->build() + ) + ) + ->build(); + } + + public function buildPostedToPortalQuery(User $user, SelectBuilder $baseBuilder): ?Select + { + if (!$user->isPortal()) { + if ($this->aclManager->getPermissionLevel($user, 'portal') !== Table::LEVEL_YES) { + return null; + } + + return (clone $baseBuilder) + ->where([ + 'parentId' => null, + 'type' => Note::TYPE_POST, + 'targetType' => Note::TARGET_PORTALS, + 'createdById!=' => $user->getId(), + 'isGlobal' => false, + ]) + ->build(); + } + + $portalIdList = $user->getPortals()->getIdList(); + + if ($portalIdList === []) { + return null; + } + + return (clone $baseBuilder) + ->where([ + 'parentId' => null, + 'type' => Note::TYPE_POST, + 'targetType' => Note::TARGET_PORTALS, + 'createdById!=' => $user->getId(), + 'isGlobal' => false, + ]) + ->where( + Cond::in( + Expr::column('id'), + SelectBuilder::create() + ->select('noteId') + ->from('NotePortal') + ->where(['portalId' => $portalIdList]) + ->build() + ) + ) + ->build(); + } + + public function buildPostedToTeamsQuery(User $user, SelectBuilder $baseBuilder): ?Select + { + if ($user->getTeamIdList() === []) { + return null; + } + + return (clone $baseBuilder) + ->where([ + 'parentId' => null, + 'type' => Note::TYPE_POST, + 'targetType' => Note::TARGET_TEAMS, + 'createdById!=' => $user->getId(), + 'isGlobal' => false, + ]) + ->where( + Cond::in( + Expr::column('id'), + SelectBuilder::create() + ->select('noteId') + ->from('NoteTeam') + ->where(['teamId' => $user->getTeamIdList()]) + ->build() + ) + ) + ->build(); + } + + public function buildPostedByUserQuery(User $user, SelectBuilder $baseBuilder): Select + { + return (clone $baseBuilder) + ->where([ + 'parentId' => null, + 'type' => Note::TYPE_POST, + 'createdById' => $user->getId(), + ]) + ->build(); + } + + public function buildPostedToGlobalQuery(User $user, SelectBuilder $baseBuilder): ?Select + { + if ($user->isPortal() || $user->isApi()) { + return null; + } + + return (clone $baseBuilder) + ->where([ + 'type' => Note::TYPE_POST, + 'targetType' => Note::TARGET_ALL, + 'parentId' => null, + 'createdBy!=' => $user->getId(), + 'isGlobal' => true, + ]) + ->build(); + } +} diff --git a/application/Espo/Tools/Stream/UserRecordService.php b/application/Espo/Tools/Stream/UserRecordService.php index f4145d865c..28afd9aae9 100644 --- a/application/Espo/Tools/Stream/UserRecordService.php +++ b/application/Espo/Tools/Stream/UserRecordService.php @@ -51,6 +51,7 @@ use Espo\ORM\Query\Part\Where\OrGroup; use Espo\ORM\Query\Select; use Espo\ORM\Query\SelectBuilder; use Espo\Tools\Stream\RecordService\Helper; +use Espo\Tools\Stream\RecordService\QueryHelper; class UserRecordService { @@ -63,7 +64,8 @@ class UserRecordService private Acl $acl, private UserAclManagerProvider $userAclManagerProvider, private NoteAccessControl $noteAccessControl, - private Helper $helper + private Helper $helper, + private QueryHelper $queryHelper ) {} /** @@ -81,8 +83,6 @@ class UserRecordService $offset = $searchParams->getOffset() ?? 0; $maxSize = $searchParams->getMaxSize(); - $sqLimit = $offset + $maxSize + 1; - $user = $userId === $this->user->getId() ? $this->user : $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId); @@ -98,10 +98,11 @@ class UserRecordService $queryList = []; - $baseBuilder = $this->helper->buildBaseQueryBuilder($searchParams) - ->select($this->helper->getUserQuerySelect()) + $baseBuilder = $this->queryHelper->buildBaseQueryBuilder($searchParams) + ->select($this->queryHelper->getUserQuerySelect()) + ->leftJoin('createdBy') ->order('number', Order::DESC) - ->limit(0, $sqLimit); + ->limit(0, $offset + $maxSize + 1); $this->buildSubscriptionQueries($user, $baseBuilder, $queryList, $searchParams); $this->buildSubscriptionSuperQuery($user, $baseBuilder, $queryList, $searchParams); @@ -305,7 +306,6 @@ class UserRecordService $builder = clone $baseBuilder; $builder - ->leftJoin('createdBy') ->join( Subscription::ENTITY_TYPE, 'subscription', @@ -394,125 +394,59 @@ class UserRecordService /** * @param Select[] $queryList */ - private function buildPostedToUserQuery( - User $user, - SelectBuilder $baseBuilder, - array &$queryList - ): void { - - $queryList[] = (clone $baseBuilder) - ->leftJoin('users') - ->leftJoin('createdBy') - ->where([ - 'createdById!=' => $user->getId(), - 'usersMiddle.userId' => $user->getId(), - 'parentId' => null, - 'type' => Note::TYPE_POST, - 'isGlobal' => false, - ]) - ->build(); + private function buildPostedToUserQuery(User $user, SelectBuilder $baseBuilder, array &$queryList): void + { + $queryList[] = $this->queryHelper->buildPostedToUserQuery($user, $baseBuilder); } /** * @param Select[] $queryList */ - private function buildPostedToPortalQuery( - User $user, - SelectBuilder $baseBuilder, - array &$queryList - ): void { + private function buildPostedToPortalQuery(User $user, SelectBuilder $baseBuilder, array &$queryList): void + { + $query = $this->queryHelper->buildPostedToPortalQuery($user, $baseBuilder); - if (!$user->isPortal()) { + if (!$query) { return; } - $portalIdList = $user->getLinkMultipleIdList('portals'); - - if ($portalIdList === []) { - return; - } - - $queryList[] = (clone $baseBuilder) - ->leftJoin('portals') - ->leftJoin('createdBy') - ->where([ - 'parentId' => null, - 'portalsMiddle.portalId' => $portalIdList, - 'type' => Note::TYPE_POST, - 'isGlobal' => false, - ]) - ->build(); + $queryList[] = $query; } /** * @param Select[] $queryList */ - private function buildPostedToTeamsQuery( - User $user, - SelectBuilder $baseBuilder, - array &$queryList - ): void { + private function buildPostedToTeamsQuery(User $user, SelectBuilder $baseBuilder, array &$queryList): void + { + $query = $this->queryHelper->buildPostedToTeamsQuery($user, $baseBuilder); - if ($user->getTeamIdList() === []) { + if (!$query) { return; } - $queryList[] = (clone $baseBuilder) - ->leftJoin('teams') - ->leftJoin('createdBy') - ->where([ - 'parentId' => null, - 'teamsMiddle.teamId' => $user->getTeamIdList(), - 'type' => Note::TYPE_POST, - 'isGlobal' => false, - ]) - ->build(); + $queryList[] = $query; } /** * @param Select[] $queryList */ - private function buildPostedByUserQuery( - User $user, - SelectBuilder $baseBuilder, - array &$queryList - ): void { - - $queryList[] = (clone $baseBuilder) - ->leftJoin('createdBy') - ->where([ - 'createdById' => $user->getId(), - 'parentId' => null, - 'type' => Note::TYPE_POST, - 'isGlobal' => false, - ]) - ->build(); + private function buildPostedByUserQuery(User $user, SelectBuilder $baseBuilder, array &$queryList): void + { + $queryList[] = $this->queryHelper->buildPostedByUserQuery($user, $baseBuilder); } /** * @param Select[] $queryList */ - private function buildPostedToGlobalQuery( - User $user, - SelectBuilder $baseBuilder, - array &$queryList - ): void { + private function buildPostedToGlobalQuery(User $user, SelectBuilder $baseBuilder, array &$queryList): void + { + $query = $this->queryHelper->buildPostedToGlobalQuery($user, $baseBuilder); - if ( - $user->isPortal() && - !$user->isAdmin() || $user->isApi() - ) { + if (!$query) { return; } - $queryList[] = (clone $baseBuilder) - ->leftJoin('createdBy') - ->where([ - 'parentId' => null, - 'type' => Note::TYPE_POST, - 'isGlobal' => true, - ]) - ->build(); + $queryList[] = $query; } /**