Skip to content

Commit

Permalink
Merge pull request #19585 from colemanw/searchKitApi
Browse files Browse the repository at this point in the history
Search Kit - Use wrapper api to run searches
  • Loading branch information
eileenmcnaughton authored Feb 17, 2021
2 parents 4049f51 + a3caaf9 commit 2b7821f
Show file tree
Hide file tree
Showing 24 changed files with 523 additions and 211 deletions.
39 changes: 27 additions & 12 deletions Civi/Api4/Query/Api4SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -275,19 +275,32 @@ protected function buildOrderBy() {
if ($dir !== 'ASC' && $dir !== 'DESC') {
throw new \API_Exception("Invalid sort direction. Cannot order by $item $dir");
}
$expr = $this->getExpression($item);
$column = $expr->render($this->apiFieldSpec);

// Use FIELD() function to sort on pseudoconstant values
$suffix = strstr($item, ':');
if ($suffix && $expr->getType() === 'SqlField') {
$field = $this->getField($item);
$options = FormattingUtil::getPseudoconstantList($field, substr($suffix, 1));
if ($options) {
asort($options);
$column = "FIELD($column,'" . implode("','", array_keys($options)) . "')";

try {
$expr = $this->getExpression($item);
$column = $expr->render($this->apiFieldSpec);

// Use FIELD() function to sort on pseudoconstant values
$suffix = strstr($item, ':');
if ($suffix && $expr->getType() === 'SqlField') {
$field = $this->getField($item);
$options = FormattingUtil::getPseudoconstantList($field, substr($suffix, 1));
if ($options) {
asort($options);
$column = "FIELD($column,'" . implode("','", array_keys($options)) . "')";
}
}
}
// If the expression could not be rendered, it might be a field alias
catch (\API_Exception $e) {
if (!empty($this->selectAliases[$item])) {
$column = '`' . $item . '`';
}
else {
throw new \API_Exception("Invalid field '{$item}'");
}
}

$this->query->orderBy("$column $dir");
}
}
Expand Down Expand Up @@ -524,7 +537,9 @@ public function getField($expr, $strict = FALSE) {
if ($strict && !$field) {
throw new \API_Exception("Invalid field '$fieldName'");
}
$this->apiFieldSpec[$expr] = $field;
if ($field) {
$this->apiFieldSpec[$expr] = $field;
}
return $field;
}

Expand Down
26 changes: 15 additions & 11 deletions ext/afform/core/Civi/Afform/AfformMetadataInjector.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,36 +74,38 @@ public static function preprocess($e) {
/**
* Merge field definition metadata into an afform field's definition
*
* @param string $entityType
* @param string $entityName
* @param string $action
* @param \DOMElement $afField
* @throws \API_Exception
*/
private static function fillFieldMetadata($entityType, $action, \DOMElement $afField) {
private static function fillFieldMetadata($entityName, $action, \DOMElement $afField) {
$fieldName = $afField->getAttribute('name');
if (strpos($entityType, ' AS ')) {
[$entityType, $alias] = explode(' AS ', $entityType);
// For explicit joins, strip the alias off the field name
if (strpos($entityName, ' AS ')) {
[$entityName, $alias] = explode(' AS ', $entityName);
$fieldName = preg_replace('/^' . preg_quote($alias . '.', '/') . '/', '', $fieldName);
}
$params = [
'action' => $action,
'where' => [['name', '=', $fieldName]],
'select' => ['label', 'input_type', 'input_attrs', 'options'],
'loadOptions' => ['id', 'label'],
// If the admin included this field on the form, then it's OK to get metadata about the field regardless of user permissions.
'checkPermissions' => FALSE,
];
if (in_array($entityType, \CRM_Contact_BAO_ContactType::basicTypes(TRUE))) {
$params['values'] = ['contact_type' => $entityType];
$entityType = 'Contact';
if (in_array($entityName, \CRM_Contact_BAO_ContactType::basicTypes(TRUE))) {
$params['values'] = ['contact_type' => $entityName];
$entityName = 'Contact';
}
$fieldInfo = civicrm_api4($entityName, 'getFields', $params)->first();
// Merge field definition data with whatever's already in the markup.
// If the admin has chosen to include this field on the form, then it's OK for us to get metadata about the field - regardless of user's other permissions.
$getFields = civicrm_api4($entityType, 'getFields', $params + ['checkPermissions' => FALSE]);
$deep = ['input_attrs'];
foreach ($getFields as $fieldInfo) {
if ($fieldInfo) {
$existingFieldDefn = trim(pq($afField)->attr('defn') ?: '');
if ($existingFieldDefn && $existingFieldDefn[0] != '{') {
// If it's not an object, don't mess with it.
continue;
return;
}
// Default placeholder for select inputs
if ($fieldInfo['input_type'] === 'Select') {
Expand All @@ -125,6 +127,8 @@ private static function fillFieldMetadata($entityType, $action, \DOMElement $afF
}

/**
* Determines name of the api entity based on the field name prefix
*
* @param string $fieldName
* @param string[] $entityList
* @return string
Expand Down
42 changes: 42 additions & 0 deletions ext/search/CRM/Search/Upgrader.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public function disable() {
->execute();
}

/**
* Upgrade 1000 - install schema
* @return bool
*/
public function upgrade_1000() {
$this->ctx->log->info('Applying update 1000 - install schema.');
// For early, early adopters who installed the extension pre-beta
Expand All @@ -41,4 +45,42 @@ public function upgrade_1000() {
return TRUE;
}

/**
* Upgrade 1001 - normalize search display column keys
* @return bool
*/
public function upgrade_1001() {
$this->ctx->log->info('Applying update 1001 - normalize search display columns.');
$savedSearches = \Civi\Api4\SavedSearch::get(FALSE)
->addWhere('api_params', 'IS NOT NULL')
->addChain('displays', \Civi\Api4\SearchDisplay::get()->addWhere('saved_search_id', '=', '$id'))
->execute();
foreach ($savedSearches as $savedSearch) {
$newAliases = [];
foreach ($savedSearch['api_params']['select'] ?? [] as $i => $select) {
if (strstr($select, '(') && !strstr($select, ' AS ')) {
$alias = CRM_Utils_String::munge(str_replace(')', '', $select), '_', 256);
$newAliases[$select] = $alias;
$savedSearch['api_params']['select'][$i] = $select . ' AS ' . $alias;
}
}
if ($newAliases) {
\Civi\Api4\SavedSearch::update(FALSE)
->setValues(array_diff_key($savedSearch, ['displays' => 0]))
->execute();
}
foreach ($savedSearch['displays'] ?? [] as $display) {
foreach ($display['settings']['columns'] ?? [] as $c => $column) {
$key = $newAliases[$column['expr']] ?? $column['expr'];
unset($display['settings']['columns'][$c]['expr']);
$display['settings']['columns'][$c]['key'] = explode(' AS ', $key)[1] ?? $key;
}
\Civi\Api4\SearchDisplay::update(FALSE)
->setValues($display)
->execute();
}
}
return TRUE;
}

}
211 changes: 211 additions & 0 deletions ext/search/Civi/Api4/Action/SearchDisplay/Run.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php

namespace Civi\Api4\Action\SearchDisplay;

use Civi\API\Exception\UnauthorizedException;
use Civi\Api4\SavedSearch;
use Civi\Api4\SearchDisplay;

/**
* Load the results for rendering a SearchDisplay.
*
* @package Civi\Api4\Action\SearchDisplay
*/
class Run extends \Civi\Api4\Generic\AbstractAction {

/**
* Either the name of the savedSearch or an array containing the savedSearch definition (for preview mode)
* @var string|array
* @required
*/
protected $savedSearch;

/**
* Either the name of the display or an array containing the display definition (for preview mode)
* @var string|array
* @required
*/
protected $display;

/**
* Array of fields to use for ordering the results
* @var array
*/
protected $sort;

/**
* Should this api call return a page of results or the row_count or the ids
* E.g. "page:1" or "row_count" or "id"
* @var string
*/
protected $return;

private $_selectQuery;

/**
* Search conditions that will be automatically added to the WHERE or HAVING clauses
* @var array
*/
protected $filters = [];

/**
* @param \Civi\Api4\Generic\Result $result
* @throws UnauthorizedException
* @throws \API_Exception
*/
public function _run(\Civi\Api4\Generic\Result $result) {
// Only administrators can use this in unsecured "preview mode"
if (!(is_string($this->savedSearch) && is_string($this->display)) && $this->checkPermissions && !\CRM_Core_Permission::check('administer CiviCRM')) {
throw new UnauthorizedException('Access denied');
}
if (is_string($this->savedSearch)) {
$this->savedSearch = SavedSearch::get(FALSE)
->addWhere('name', '=', $this->savedSearch)
->execute()->first();
}
if (is_string($this->display)) {
$this->display = SearchDisplay::get(FALSE)
->addWhere('name', '=', $this->display)
->addWhere('saved_search_id', '=', $this->savedSearch['id'])
->execute()->first();
}
$entityName = $this->savedSearch['api_entity'];
$apiParams =& $this->savedSearch['api_params'];
$settings = $this->display['settings'];
$page = NULL;

switch ($this->return) {
case 'row_count':
case 'id':
if (empty($apiParams['having'])) {
$apiParams['select'] = [];
}
if (!in_array($this->return, $apiParams)) {
$apiParams['select'][] = $this->return;
}
unset($apiParams['orderBy'], $apiParams['limit']);
break;

default:
if (!empty($settings['pager']) && preg_match('/^page:\d+$/', $this->return)) {
$page = explode(':', $this->return)[1];
}
$apiParams['limit'] = $settings['limit'] ?? NULL;
$apiParams['offset'] = $page ? $apiParams['limit'] * ($page - 1) : 0;
$apiParams['orderBy'] = $this->getOrderByFromSort();

// Select the ids of joined entities (helps with displaying links)
foreach ($apiParams['join'] ?? [] as $join) {
$joinEntity = explode(' AS ', $join[0])[1];
$idField = $joinEntity . '.id';
if (!in_array($idField, $apiParams['select']) && !$this->canAggregate('id', $joinEntity . '.')) {
$apiParams['select'][] = $idField;
}
}
}

$this->applyFilters();

$apiResult = civicrm_api4($entityName, 'get', $apiParams);

$result->rowCount = $apiResult->rowCount;
$result->exchangeArray($apiResult->getArrayCopy());
}

/**
* Applies supplied filters to the where clause
*/
private function applyFilters() {
// Global setting determines if % wildcard should be added to both sides (default) or only the end of the search term
$prefixWithWildcard = \Civi::settings()->get('includeWildCardInName');

foreach ($this->filters as $fieldName => $value) {
if ($value) {
$field = $this->getField($fieldName) ?? [];
$dataType = $field['data_type'] ?? NULL;

if (!empty($field['serialize'])) {
$this->savedSearch['api_params']['where'][] = [$fieldName, 'CONTAINS', $value];
}
elseif (!empty($field['options']) || in_array($dataType, ['Integer', 'Boolean', 'Date', 'Timestamp'])) {
$this->savedSearch['api_params']['where'][] = [$fieldName, '=', $value];
}
elseif ($prefixWithWildcard) {
$this->savedSearch['api_params']['where'][] = [$fieldName, 'CONTAINS', $value];
}
else {
$this->savedSearch['api_params']['where'][] = [$fieldName, 'LIKE', $value . '%'];
}
}
}
}

/**
* Transforms the SORT param (which is expected to be an array of arrays)
* to the ORDER BY clause (which is an associative array of [field => DIR]
*
* @return array
*/
private function getOrderByFromSort() {
$defaultSort = $this->display['settings']['sort'] ?? [];
$currentSort = $this->sort;

// Validate that requested sort fields are part of the SELECT
foreach ($this->sort as $item) {
if (!in_array($item[0], $this->getSelectAliases())) {
$currentSort = NULL;
}
}

$orderBy = [];
foreach ($currentSort ?: $defaultSort as $item) {
$orderBy[$item[0]] = $item[1];
}
return $orderBy;
}

/**
* Returns an array of field names or aliases from the SELECT clause
* @return string[]
*/
private function getSelectAliases() {
return array_map(function($select) {
return array_slice(explode(' AS ', $select), -1)[0];
}, $this->savedSearch['api_params']['select']);
}

/**
* Determines if a column is eligible to use an aggregate function
* @param $fieldName
* @param $prefix
* @return bool
*/
private function canAggregate($fieldName, $prefix) {
$apiParams = $this->savedSearch['api_params'];

// If the query does not use grouping, never
if (empty($apiParams['groupBy'])) {
return FALSE;
}
// If the column is used for a groupBy, no
if (in_array($prefix . $fieldName, $apiParams['groupBy'])) {
return FALSE;
}
// If the entity this column belongs to is being grouped by id, then also no
return !in_array($prefix . 'id', $apiParams['groupBy']);
}

/**
* Returns field definition for a given field or NULL if not found
* @param $fieldName
* @return array|null
*/
private function getField($fieldName) {
if (!$this->_selectQuery) {
$api = \Civi\API\Request::create($this->savedSearch['api_entity'], 'get', $this->savedSearch['api_params']);
$this->_selectQuery = new \Civi\Api4\Query\Api4SelectQuery($api);
}
return $this->_selectQuery->getField($fieldName, FALSE);
}

}
9 changes: 9 additions & 0 deletions ext/search/Civi/Api4/SearchDisplay.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@
*/
class SearchDisplay extends Generic\DAOEntity {

/**
* @param bool $checkPermissions
* @return Action\SearchDisplay\Run
*/
public static function run($checkPermissions = TRUE) {
return (new Action\SearchDisplay\Run(__CLASS__, __FUNCTION__))
->setCheckPermissions($checkPermissions);
}

}
Loading

0 comments on commit 2b7821f

Please sign in to comment.