diff --git a/application/Espo/Core/Select/Where/Converter.php b/application/Espo/Core/Select/Where/Converter.php index 8abc40485c..73c411dfef 100644 --- a/application/Espo/Core/Select/Where/Converter.php +++ b/application/Espo/Core/Select/Where/Converter.php @@ -30,12 +30,7 @@ namespace Espo\Core\Select\Where; use Espo\Core\Exceptions\BadRequest; -use Espo\Core\Select\Helpers\RandomStringGenerator; use Espo\Core\Select\Where\Item\Type; -use Espo\Entities\Team; -use Espo\Entities\User; -use Espo\ORM\Defs as ORMDefs; -use Espo\ORM\Entity; use Espo\ORM\Query\Part\WhereClause; use Espo\ORM\Query\Part\WhereItem; use Espo\ORM\Query\SelectBuilder as QueryBuilder; @@ -46,15 +41,9 @@ use InvalidArgumentException; */ class Converter { - private const TYPE_IN_CATEGORY = 'inCategory'; - private const TYPE_IS_USER_FROM_TEAMS = 'isUserFromTeams'; - public function __construct( - private string $entityType, private ItemConverter $itemConverter, - private Scanner $scanner, - private RandomStringGenerator $randomStringGenerator, - private ORMDefs $ormDefs + private Scanner $scanner ) {} /** @@ -64,9 +53,7 @@ class Converter { $whereClause = []; - $itemList = $this->itemToList($item); - - foreach ($itemList as $subItemRaw) { + foreach ($this->itemToList($item) as $subItemRaw) { try { $subItem = Item::fromRaw($subItemRaw); } @@ -76,7 +63,7 @@ class Converter $part = $this->processItem($queryBuilder, $subItem); - if (empty($part)) { + if ($part === []) { continue; } @@ -110,156 +97,11 @@ class Converter } /** - * @return ?array - * @throws BadRequest - */ - private function processItem(QueryBuilder $queryBuilder, Item $item): ?array - { - $type = $item->getType(); - $attribute = $item->getAttribute(); - $value = $item->getValue(); - - if ( - $type === self::TYPE_IN_CATEGORY || - $type === self::TYPE_IS_USER_FROM_TEAMS - ) { - // Processing special filters. Only at the top level of the tree. - - if (!$attribute) { - throw new BadRequest("Bad where definition. Missing attribute."); - } - - if (!$value) { - return null; - } - - if ($type === self::TYPE_IN_CATEGORY) { - return $this->applyInCategory($queryBuilder, $attribute, $value); - } - - return $this->applyIsUserFromTeams($queryBuilder, $attribute, $value); - } - - return $this->itemConverter - ->convert($queryBuilder, $item) - ->getRaw(); - } - - /** - * @param mixed $value * @return array * @throws BadRequest */ - private function applyInCategory(QueryBuilder $queryBuilder, string $attribute, $value): array + private function processItem(QueryBuilder $queryBuilder, Item $item): array { - $link = $attribute; - - $entityDefs = $this->ormDefs->getEntity($this->entityType); - - if (!$entityDefs->hasRelation($link)) { - throw new BadRequest("Not existing '$link' in where item."); - } - - $defs = $entityDefs->getRelation($link); - - $foreignEntity = $defs->getForeignEntityType(); - - $pathName = lcfirst($foreignEntity) . 'Path'; - - $relationType = $defs->getType(); - - if ($relationType === Entity::MANY_MANY) { - $queryBuilder->distinct(); - - $alias = $link . 'InCategoryFilter'; - - $queryBuilder->join($link, $alias); - - $key = $defs->getForeignMidKey(); - - $middleName = $alias . 'Middle'; - - $queryBuilder->join( - ucfirst($pathName), - $pathName, - [ - "$pathName.descendorId:" => "$middleName.$key", - ] - ); - - return [ - $pathName . '.ascendorId' => $value, - ]; - } - - if ($relationType === Entity::BELONGS_TO) { - $key = $defs->getKey(); - - $queryBuilder->join( - ucfirst($pathName), - $pathName, - [ - "$pathName.descendorId:" => "$key", - ] - ); - - return [ - $pathName . '.ascendorId' => $value, - ]; - } - - throw new BadRequest("Not supported link '$link' in where item."); - } - - /** - * @param mixed $value - * @return array - * @throws BadRequest - */ - private function applyIsUserFromTeams(QueryBuilder $queryBuilder, string $attribute, $value): array - { - $link = $attribute; - - if (is_array($value) && count($value) == 1) { - $value = $value[0]; - } - - $entityDefs = $this->ormDefs->getEntity($this->entityType); - - if (!$entityDefs->hasRelation($link)) { - throw new BadRequest("Not existing '$link' in where item."); - } - - $defs = $entityDefs->getRelation($link); - - $relationType = $defs->getType(); - $entityType = $defs->getForeignEntityType(); - - if ($entityType !== User::ENTITY_TYPE) { - throw new BadRequest("Not supported link '$link' in where item."); - } - - if ($relationType === Entity::BELONGS_TO) { - $key = $defs->getKey(); - - $aliasName = $link . 'IsUserFromTeamsFilter' . $this->randomStringGenerator->generate(); - - $queryBuilder->leftJoin( - Team::RELATIONSHIP_TEAM_USER, - $aliasName . 'Middle', - [ - $aliasName . 'Middle.userId:' => $key, - $aliasName . 'Middle.deleted' => false, - ] - ); - - $queryBuilder->distinct(); - - return [ - $aliasName . 'Middle.teamId' => $value, - ]; - } - - throw new BadRequest("Not supported link '$link' in where item."); + return $this->itemConverter->convert($queryBuilder, $item)->getRaw(); } } diff --git a/application/Espo/Core/Select/Where/ItemConverter.php b/application/Espo/Core/Select/Where/ItemConverter.php index 4a6ba168ca..aecb6c349b 100644 --- a/application/Espo/Core/Select/Where/ItemConverter.php +++ b/application/Espo/Core/Select/Where/ItemConverter.php @@ -29,6 +29,7 @@ namespace Espo\Core\Select\Where; +use Espo\Core\Exceptions\BadRequest; use Espo\ORM\Query\Part\WhereItem as WhereClauseItem; use Espo\ORM\Query\SelectBuilder as QueryBuilder; @@ -37,5 +38,8 @@ use Espo\ORM\Query\SelectBuilder as QueryBuilder; */ interface ItemConverter { + /** + * @throws BadRequest + */ public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem; } diff --git a/application/Espo/Core/Select/Where/ItemConverters/InCategory.php b/application/Espo/Core/Select/Where/ItemConverters/InCategory.php new file mode 100644 index 0000000000..eddaad0398 --- /dev/null +++ b/application/Espo/Core/Select/Where/ItemConverters/InCategory.php @@ -0,0 +1,106 @@ +. + * + * 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\Core\Select\Where\ItemConverters; + +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Select\Where\Item; +use Espo\Core\Select\Where\ItemConverter; +use Espo\ORM\Defs; +use Espo\ORM\Entity; +use Espo\ORM\Query\Part\WhereClause; +use Espo\ORM\Query\Part\WhereItem as WhereClauseItem; +use Espo\ORM\Query\SelectBuilder as QueryBuilder; + +/** + * @noinspection PhpUnused + */ +class InCategory implements ItemConverter +{ + public function __construct( + private string $entityType, + private Defs $ormDefs + ) {} + + public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem + { + $link = $item->getAttribute(); + $value = $item->getValue(); + + if (!$link) { + throw new BadRequest("No attribute."); + } + + if ($value === null) { + return WhereClause::create(); + } + + $entityDefs = $this->ormDefs->getEntity($this->entityType); + + if (!$entityDefs->hasRelation($link)) { + throw new BadRequest("Not existing '$link' in where item."); + } + + $defs = $entityDefs->getRelation($link); + + $foreignEntity = $defs->getForeignEntityType(); + $pathName = lcfirst($foreignEntity) . 'Path'; + $relationType = $defs->getType(); + + if ($relationType === Entity::MANY_MANY) { + $alias = $link . 'InCategoryFilter'; + $key = $defs->getForeignMidKey(); + $middleName = $alias . 'Middle'; + + $queryBuilder->distinct(); + $queryBuilder->join($link, $alias); + $queryBuilder->join( + ucfirst($pathName), + $pathName, + ["$pathName.descendorId:" => "$middleName.$key"] + ); + + return WhereClause::fromRaw([$pathName . '.ascendorId' => $value]); + } + + if ($relationType === Entity::BELONGS_TO) { + $key = $defs->getKey(); + + $queryBuilder->join( + ucfirst($pathName), + $pathName, + ["$pathName.descendorId:" => "$key"] + ); + + return WhereClause::fromRaw([$pathName . '.ascendorId' => $value]); + } + + throw new BadRequest("Not supported link '$link' in where item."); + } +} diff --git a/application/Espo/Core/Select/Where/ItemConverters/IsUserFromTeams.php b/application/Espo/Core/Select/Where/ItemConverters/IsUserFromTeams.php new file mode 100644 index 0000000000..c2b573bc93 --- /dev/null +++ b/application/Espo/Core/Select/Where/ItemConverters/IsUserFromTeams.php @@ -0,0 +1,106 @@ +. + * + * 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\Core\Select\Where\ItemConverters; + +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Select\Helpers\RandomStringGenerator; +use Espo\Core\Select\Where\Item; +use Espo\Core\Select\Where\ItemConverter; +use Espo\Entities\Team; +use Espo\Entities\User; +use Espo\ORM\Defs; +use Espo\ORM\Entity; +use Espo\ORM\Query\Part\WhereClause; +use Espo\ORM\Query\Part\WhereItem as WhereClauseItem; +use Espo\ORM\Query\SelectBuilder as QueryBuilder; + +/** + * @noinspection PhpUnused + */ +class IsUserFromTeams implements ItemConverter +{ + public function __construct( + private string $entityType, + private Defs $ormDefs, + private RandomStringGenerator $randomStringGenerator + ) {} + + public function convert(QueryBuilder $queryBuilder, Item $item): WhereClauseItem + { + $link = $item->getAttribute(); + $value = $item->getValue(); + + if (!$link) { + throw new BadRequest("No attribute."); + } + + if ($value === null) { + return WhereClause::create(); + } + + if (is_array($value) && count($value) == 1) { + $value = $value[0]; + } + + $entityDefs = $this->ormDefs->getEntity($this->entityType); + + if (!$entityDefs->hasRelation($link)) { + throw new BadRequest("Not existing '$link' in where item."); + } + + $defs = $entityDefs->getRelation($link); + + $relationType = $defs->getType(); + $entityType = $defs->getForeignEntityType(); + + if ($entityType !== User::ENTITY_TYPE) { + throw new BadRequest("Not supported link '$link' in where item."); + } + + if ($relationType === Entity::BELONGS_TO) { + $key = $defs->getKey(); + $aliasName = $link . 'IsUserFromTeamsFilter' . $this->randomStringGenerator->generate(); + + $queryBuilder->distinct(); + $queryBuilder->leftJoin( + Team::RELATIONSHIP_TEAM_USER, + $aliasName . 'Middle', + [ + "{$aliasName}Middle.userId:" => $key, + "{$aliasName}Middle.deleted" => false, + ] + ); + + return WhereClause::fromRaw(["{$aliasName}Middle.teamId" => $value]); + } + + throw new BadRequest("Not supported link '$link' in where item."); + } +} diff --git a/application/Espo/Resources/metadata/app/select.json b/application/Espo/Resources/metadata/app/select.json new file mode 100644 index 0000000000..fb611775a5 --- /dev/null +++ b/application/Espo/Resources/metadata/app/select.json @@ -0,0 +1,6 @@ +{ + "whereItemConverterClassNameMap": { + "inCategory": "Espo\\Core\\Select\\Where\\ItemConverters\\InCategory", + "isUserFromTeams": "Espo\\Core\\Select\\Where\\ItemConverters\\IsUserFromTeams" + } +} diff --git a/tests/unit/Espo/Core/Select/Where/ConverterTest.php b/tests/unit/Espo/Core/Select/Where/ConverterTest.php index 9d5d46a4aa..e2dd59dd31 100644 --- a/tests/unit/Espo/Core/Select/Where/ConverterTest.php +++ b/tests/unit/Espo/Core/Select/Where/ConverterTest.php @@ -50,8 +50,9 @@ use Espo\ORM\Query\Part\WhereClause; use Espo\ORM\Query\Select; use Espo\ORM\Query\SelectBuilder as QueryBuilder; use Espo\ORM\QueryBuilder as BaseQueryBuilder; +use PHPUnit\Framework\TestCase; -class ConverterTest extends \PHPUnit\Framework\TestCase +class ConverterTest extends TestCase { protected function setUp() : void { @@ -105,11 +106,8 @@ class ConverterTest extends \PHPUnit\Framework\TestCase ); $this->converter = new Converter( - $this->entityType, $this->itemConverter, - $this->scanner, - $this->randomStringGenerator, - $this->ormDefs + $this->scanner ); } @@ -215,184 +213,6 @@ class ConverterTest extends \PHPUnit\Framework\TestCase $this->assertEquals($expected, $whereClause->getRaw()); } - public function testConvertInCategoryManyMany() - { - $this->ormDefs - ->expects($this->any()) - ->method('getEntity') - ->with($this->entityType) - ->willReturn( - EntityDefs::fromRaw( - [ - 'relations' => [ - 'test' => [ - 'type' => Entity::MANY_MANY, - 'entity' => 'Foreign', - 'midKeys' => ['localId', 'foreignId'], - ], - ], - ], - $this->entityType - ) - ); - - $item = Item::fromRaw([ - 'type' => 'and', - 'value' => [ - [ - 'type' => 'inCategory', - 'attribute' => 'test', - 'value' => 'value', - ], - ], - ]); - - $this->queryBuilder - ->expects($this->once()) - ->method('distinct'); - - $this->queryBuilder - ->method('join') - ->withConsecutive( - [ - 'test', - 'testInCategoryFilter', - ], - [ - 'ForeignPath', - 'foreignPath', - [ - "foreignPath.descendorId:" => "testInCategoryFilterMiddle.foreignId", - ] - ] - ); - - $whereClause = $this->converter->convert($this->queryBuilder, $item); - - $expected = [ - 'foreignPath.ascendorId' => 'value', - ]; - - $this->assertEquals($expected, $whereClause->getRaw()); - } - - public function testConvertInCategoryBelongsTo() - { - $this->ormDefs - ->expects($this->any()) - ->method('getEntity') - ->with($this->entityType) - ->willReturn( - EntityDefs::fromRaw( - [ - 'relations' => [ - 'test' => [ - 'type' => Entity::BELONGS_TO, - 'entity' => 'Foreign', - 'key' => 'foreignId', - ], - ], - ], - $this->entityType - ) - ); - - $item = Item::fromRaw([ - 'type' => 'and', - 'value' => [ - [ - 'type' => 'inCategory', - 'attribute' => 'test', - 'value' => 'value', - ], - ], - ]); - - $this->queryBuilder - ->expects($this->never()) - ->method('distinct'); - - $this->queryBuilder - ->method('join') - ->withConsecutive( - [ - 'ForeignPath', - 'foreignPath', - [ - "foreignPath.descendorId:" => "foreignId", - ] - ] - ); - - $whereClause = $this->converter->convert($this->queryBuilder, $item); - - $expected = [ - 'foreignPath.ascendorId' => 'value', - ]; - - $this->assertEquals($expected, $whereClause->getRaw()); - } - - public function testConvertIsUserFromTeams() - { - $this->ormDefs - ->expects($this->any()) - ->method('getEntity') - ->with($this->entityType) - ->willReturn( - EntityDefs::fromRaw( - [ - 'relations' => [ - 'user' => [ - 'type' => Entity::BELONGS_TO, - 'entity' => 'User', - 'key' => 'userId', - ], - ], - ], - $this->entityType - ) - ); - - $item = Item::fromRaw([ - 'type' => 'and', - 'value' => [ - [ - 'type' => 'isUserFromTeams', - 'attribute' => 'user', - 'value' => 'valueTeamId', - ], - ], - ]); - - $this->queryBuilder - ->expects($this->once()) - ->method('distinct'); - - $aliasName = 'userIsUserFromTeamsFilterRandom'; - - $this->queryBuilder - ->method('join') - ->withConsecutive( - [ - 'TeamUser', - $aliasName . 'Middle', - [ - $aliasName . 'Middle.userId:' => 'userId', - $aliasName . 'Middle.deleted' => false, - ] - ] - ); - - $whereClause = $this->converter->convert($this->queryBuilder, $item); - - $expected = [ - $aliasName . 'Middle.teamId' => 'valueTeamId', - ]; - - $this->assertEquals($expected, $whereClause->getRaw()); - } - public function testConvertDateTimeOn1() { $item = Item::fromRaw([