From 49208bdbf6caad7894475bad0e601ee410a1c6b1 Mon Sep 17 00:00:00 2001 From: Coleman Watts Date: Fri, 11 Feb 2022 11:24:02 -0500 Subject: [PATCH] SearchKit - Fix support for non-DAO entities Before: SearchKit would crash if running a search on a non-DAO entity (Extension, Entity, Afform, etc) After: Works, test added --- .../SearchDisplay/AbstractRunAction.php | 5 ++- .../SavedSearchInspectorTrait.php | 39 ++++++++++++++++--- .../api/v4/SearchDisplay/SearchRunTest.php | 38 ++++++++++++++++++ 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php index d702f76335..4d95db8520 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/AbstractRunAction.php @@ -688,12 +688,13 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { $field = $this->getField($fieldName); // If field is not found it must be an aggregated column & belongs in the HAVING clause. if (!$field) { + $this->_apiParams += ['having' => []]; $clause =& $this->_apiParams['having']; } // If field belongs to an EXCLUDE join, it should be added as a join condition else { $prefix = strpos($fieldName, '.') ? explode('.', $fieldName)[0] : NULL; - foreach ($this->_apiParams['join'] as $idx => $join) { + foreach ($this->_apiParams['join'] ?? [] as $idx => $join) { if (($join[1] ?? 'LEFT') === 'EXCLUDE' && (explode(' AS ', $join[0])[1] ?? '') === $prefix) { $clause =& $this->_apiParams['join'][$idx]; } @@ -872,7 +873,7 @@ abstract class AbstractRunAction extends \Civi\Api4\Generic\AbstractAction { */ protected function getJoinFromAlias(string $alias) { $result = ''; - foreach ($this->_apiParams['join'] as $join) { + foreach ($this->_apiParams['join'] ?? [] as $join) { $joinName = explode(' AS ', $join[0])[1]; if (strpos($alias, $joinName) === 0) { $parsed = $joinName . '.' . substr($alias, strlen($joinName) + 1); diff --git a/ext/search_kit/Civi/Api4/Action/SearchDisplay/SavedSearchInspectorTrait.php b/ext/search_kit/Civi/Api4/Action/SearchDisplay/SavedSearchInspectorTrait.php index 84a45f78aa..a1157b3c07 100644 --- a/ext/search_kit/Civi/Api4/Action/SearchDisplay/SavedSearchInspectorTrait.php +++ b/ext/search_kit/Civi/Api4/Action/SearchDisplay/SavedSearchInspectorTrait.php @@ -2,6 +2,7 @@ namespace Civi\Api4\Action\SearchDisplay; +use Civi\API\Request; use Civi\Api4\Query\SqlExpression; use Civi\Api4\SavedSearch; use Civi\Api4\Utils\CoreUtil; @@ -37,6 +38,11 @@ trait SavedSearchInspectorTrait { */ private $_selectClause; + /** + * @var array + */ + private $_searchEntityFields; + /** * If SavedSearch is supplied as a string, this will load it as an array * @throws \API_Exception @@ -48,7 +54,7 @@ trait SavedSearchInspectorTrait { ->addWhere('name', '=', $this->savedSearch) ->execute()->single(); } - $this->_apiParams = ($this->savedSearch['api_params'] ?? []) + ['select' => [], 'where' => [], 'join' => [], 'having' => []]; + $this->_apiParams = ($this->savedSearch['api_params'] ?? []) + ['select' => [], 'where' => []]; } /** @@ -57,7 +63,10 @@ trait SavedSearchInspectorTrait { * @return array|null */ protected function getField($fieldName) { - return $this->getQuery() ? $this->getQuery()->getField($fieldName, FALSE) : NULL; + [$fieldName] = explode(':', $fieldName); + return $this->getQuery() ? + $this->getQuery()->getField($fieldName, FALSE) : + ($this->getEntityFields()[$fieldName] ?? NULL); } /** @@ -65,7 +74,7 @@ trait SavedSearchInspectorTrait { * @return array{entity: string, alias: string, table: string, bridge: string|NULL}|NULL */ protected function getJoin($joinAlias) { - return $this->getQuery()->getExplicitJoin($joinAlias); + return $this->getQuery() ? $this->getQuery()->getExplicitJoin($joinAlias) : NULL; } /** @@ -76,16 +85,34 @@ trait SavedSearchInspectorTrait { } /** - * @return \Civi\Api4\Query\Api4SelectQuery + * Returns a Query object for the search entity, or FALSE if it doesn't have a DAO + * + * @return \Civi\Api4\Query\Api4SelectQuery|bool */ private function getQuery() { - if (!$this->_selectQuery && !empty($this->savedSearch['api_entity'])) { - $api = \Civi\API\Request::create($this->savedSearch['api_entity'], 'get', $this->savedSearch['api_params']); + if (!isset($this->_selectQuery) && !empty($this->savedSearch['api_entity'])) { + if (!in_array('DAOEntity', CoreUtil::getInfoItem($this->savedSearch['api_entity'], 'type'), TRUE)) { + return $this->_selectQuery = FALSE; + } + $api = Request::create($this->savedSearch['api_entity'], 'get', $this->savedSearch['api_params']); $this->_selectQuery = new \Civi\Api4\Query\Api4SelectQuery($api); } return $this->_selectQuery; } + /** + * Used as a fallback for non-DAO entities which don't use the Query object + * + * @return array + */ + private function getEntityFields() { + if (!isset($this->_searchEntityFields)) { + $this->_searchEntityFields = Request::create($this->savedSearch['api_entity'], 'get', $this->savedSearch['api_params']) + ->entityFields(); + } + return $this->_searchEntityFields; + } + /** * Returns the select clause enhanced with metadata * diff --git a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php index 2712da75be..6f5fecd957 100644 --- a/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php +++ b/ext/search_kit/tests/phpunit/api/v4/SearchDisplay/SearchRunTest.php @@ -875,4 +875,42 @@ class SearchRunTest extends \PHPUnit\Framework\TestCase implements HeadlessInter $this->assertNotEmpty($result[1]['columns'][1]['links'][0]['url']); } + /** + * Ensure SearchKit can cope with a non-DAO-based entity + */ + public function testRunWithNonDaoEntity() { + $search = [ + 'api_entity' => 'Entity', + 'api_params' => [ + 'version' => 4, + 'select' => ['name'], + 'where' => [['name', '=', 'Contact']], + ], + ]; + + $display = [ + 'type' => 'table', + 'settings' => [ + 'actions' => TRUE, + 'columns' => [ + [ + 'type' => 'field', + 'key' => 'name', + 'label' => 'Name', + 'sortable' => TRUE, + ], + ], + ], + ]; + + $result = SearchDisplay::Run(FALSE) + ->setSavedSearch($search) + ->setDisplay($display) + ->setReturn('page:1') + ->execute(); + + $this->assertCount(1, $result); + $this->assertEquals('Contact', $result[0]['columns'][0]['val']); + } + } -- 2.25.1