Skip to content

Commit

Permalink
SearchKit - Add API filter for contacts in groups and smart groups
Browse files Browse the repository at this point in the history
  • Loading branch information
colemanw committed Jun 5, 2021
1 parent f96e498 commit 2841601
Show file tree
Hide file tree
Showing 22 changed files with 400 additions and 95 deletions.
14 changes: 1 addition & 13 deletions Civi/Api4/Action/CustomValue/GetFields.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,8 @@ protected function getRecords() {
$fields = $this->_itemsToGet('name');
/** @var \Civi\Api4\Service\Spec\SpecGatherer $gatherer */
$gatherer = \Civi::container()->get('spec_gatherer');
$spec = $gatherer->getSpec('Custom_' . $this->getCustomGroup(), $this->getAction(), $this->includeCustom, $this->values);
$spec = $gatherer->getSpec('Custom_' . $this->getCustomGroup(), $this->getAction(), TRUE, $this->values);
return $this->specToArray($spec->getFields($fields));
}

/**
* @inheritDoc
*/
public function getParamInfo($param = NULL) {
$info = parent::getParamInfo($param);
if (!$param) {
// This param is meaningless here.
unset($info['includeCustom']);
}
return $info;
}

}
6 changes: 5 additions & 1 deletion Civi/Api4/Generic/AbstractAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,15 @@ public function getPermissions() {
*/
public function entityFields() {
if (!$this->_entityFields) {
$allowedTypes = ['Field', 'Filter'];
if (method_exists($this, 'getCustomGroup')) {
$allowedTypes[] = 'Custom';
}
$getFields = \Civi\API\Request::create($this->getEntityName(), 'getFields', [
'version' => 4,
'checkPermissions' => $this->checkPermissions,
'action' => $this->getActionName(),
'includeCustom' => FALSE,
'where' => [['type', 'IN', $allowedTypes]],
]);
$result = new Result();
// Pass TRUE for the private $isInternal param
Expand Down
34 changes: 22 additions & 12 deletions Civi/Api4/Generic/BasicGetFieldsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ class BasicGetFieldsAction extends BasicGetAction {
*/
protected $values = [];

/**
* @var bool
* @deprecated
*/
protected $includeCustom;

/**
* To implement getFields for your own entity:
*
Expand Down Expand Up @@ -207,18 +213,6 @@ public function addValue(string $fieldName, $value) {
return $this;
}

/**
* @param bool $includeCustom
* @return $this
*/
public function setIncludeCustom(bool $includeCustom) {
// Be forgiving if the param doesn't exist and don't throw an exception
if (property_exists($this, 'includeCustom')) {
$this->includeCustom = $includeCustom;
}
return $this;
}

/**
* Helper function to retrieve options from an option group (for non-DAO entities).
*
Expand Down Expand Up @@ -259,6 +253,17 @@ public function fields() {
'data_type' => 'String',
'description' => ts('Explanation of the purpose of the field'),
],
[
'name' => 'type',
'data_type' => 'String',
'default_value' => 'Field',
'options' => [
'Field' => ts('Primary Field'),
'Custom' => ts('Custom Field'),
'Filter' => ts('Search Filter'),
'Extra' => ts('Extra API Field'),
],
],
[
'name' => 'default_value',
'data_type' => 'String',
Expand All @@ -277,6 +282,11 @@ public function fields() {
'data_type' => 'Array',
'default_value' => FALSE,
],
[
'name' => 'operators',
'data_type' => 'Array',
'description' => 'If set, limits the operators that can be used on this field for "get" actions.',
],
[
'name' => 'data_type',
'default_value' => 'String',
Expand Down
25 changes: 14 additions & 11 deletions Civi/Api4/Generic/DAOGetFieldsAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,25 @@
*/
class DAOGetFieldsAction extends BasicGetFieldsAction {

/**
* Include custom fields for this entity, or only core fields?
*
* @var bool
*/
protected $includeCustom = TRUE;

/**
* Get fields for a DAO-based entity.
*
* @return array
*/
protected function getRecords() {
$fieldsToGet = $this->_itemsToGet('name');
$typesToGet = $this->_itemsToGet('type');
/** @var \Civi\Api4\Service\Spec\SpecGatherer $gatherer */
$gatherer = \Civi::container()->get('spec_gatherer');
// Any fields name with a dot in it is either custom or an implicit join
if ($fieldsToGet) {
$this->includeCustom = strpos(implode('', $fieldsToGet), '.') !== FALSE;
$includeCustom = TRUE;
if ($typesToGet) {
$includeCustom = in_array('Custom', $typesToGet, TRUE);
}
$spec = $gatherer->getSpec($this->getEntityName(), $this->getAction(), $this->includeCustom, $this->values);
elseif ($fieldsToGet) {
// Any fields name with a dot in it is either custom or an implicit join
$includeCustom = strpos(implode('', $fieldsToGet), '.') !== FALSE;
}
$spec = $gatherer->getSpec($this->getEntityName(), $this->getAction(), $includeCustom, $this->values);
$fields = $this->specToArray($spec->getFields($fieldsToGet));
foreach ($fieldsToGet ?? [] as $fieldName) {
if (empty($fields[$fieldName]) && strpos($fieldName, '.') !== FALSE) {
Expand Down Expand Up @@ -122,6 +120,11 @@ public function fields() {
'name' => 'custom_group_id',
'data_type' => 'Integer',
];
$fields[] = [
'name' => 'sql_filters',
'data_type' => 'Array',
'@internal' => TRUE,
];
return $fields;
}

Expand Down
62 changes: 44 additions & 18 deletions Civi/Api4/Query/Api4SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ class Api4SelectQuery {
*/
protected $apiFieldSpec;

/**
* @var array
*/
protected $entityFieldNames = [];

/**
* @var array
*/
Expand Down Expand Up @@ -97,7 +92,6 @@ public function __construct($apiGet) {

// Build field lists
foreach ($this->api->entityFields() as $field) {
$this->entityFieldNames[] = $field['name'];
$field['sql_name'] = '`' . self::MAIN_TABLE_ALIAS . '`.`' . $field['column_name'] . '`';
$this->addSpecField($field['name'], $field);
}
Expand Down Expand Up @@ -205,7 +199,7 @@ protected function buildSelectClause($select = NULL) {
$select = array_diff($select ?? $this->getSelect(), ['row_count']);
// An empty select is the same as *
if (empty($select)) {
$select = $this->entityFieldNames;
$select = $this->selectMatchingFields('*');
}
else {
if ($this->forceSelectId) {
Expand Down Expand Up @@ -236,7 +230,7 @@ protected function buildSelectClause($select = NULL) {
// If the joined_entity.id isn't in the fieldspec already, autoJoinFK will attempt to add the entity.
$idField = substr($wildField, 0, strrpos($wildField, '.')) . '.id';
$this->autoJoinFK($idField);
$matches = SelectUtil::getMatchingFields($wildField, array_keys($this->apiFieldSpec));
$matches = $this->selectMatchingFields($wildField);
array_splice($select, $pos, 1, $matches);
}
$select = array_unique($select);
Expand All @@ -247,7 +241,7 @@ protected function buildSelectClause($select = NULL) {
foreach ($expr->getFields() as $fieldName) {
$field = $this->getField($fieldName);
// Remove expressions with unknown fields without raising an error
if (!$field) {
if (!$field || !in_array($field['type'], ['Field', 'Custom'], TRUE)) {
$select = array_diff($select, [$item]);
$this->debug('undefined_fields', $fieldName);
$valid = FALSE;
Expand All @@ -264,6 +258,20 @@ protected function buildSelectClause($select = NULL) {
}
}

/**
* Get all fields for SELECT clause matching a wildcard pattern
*
* @param $pattern
* @return array
*/
private function selectMatchingFields($pattern) {
// Only core & custom fields can be selected
$availableFields = array_filter($this->apiFieldSpec, function($field) {
return in_array($field['type'], ['Field', 'Custom'], TRUE);
});
return SelectUtil::getMatchingFields($pattern, array_keys($availableFields));
}

/**
* Add WHERE clause to query
*/
Expand Down Expand Up @@ -352,12 +360,13 @@ protected function buildGroupBy() {
* @param array $clause
* @param string $type
* WHERE|HAVING|ON
* @param int $depth
* @return string SQL where clause
*
* @throws \API_Exception
* @uses composeClause() to generate the SQL etc.
*/
protected function treeWalkClauses($clause, $type) {
protected function treeWalkClauses($clause, $type, $depth = 0) {
// Skip empty leaf.
if (in_array($clause[0], ['AND', 'OR', 'NOT']) && empty($clause[1])) {
return '';
Expand All @@ -368,12 +377,12 @@ protected function treeWalkClauses($clause, $type) {
// handle branches
if (count($clause[1]) === 1) {
// a single set so AND|OR is immaterial
return $this->treeWalkClauses($clause[1][0], $type);
return $this->treeWalkClauses($clause[1][0], $type, $depth + 1);
}
else {
$sql_subclauses = [];
foreach ($clause[1] as $subclause) {
$sql_subclauses[] = $this->treeWalkClauses($subclause, $type);
$sql_subclauses[] = $this->treeWalkClauses($subclause, $type, $depth + 1);
}
return '(' . implode("\n" . $clause[0], $sql_subclauses) . ')';
}
Expand All @@ -383,10 +392,10 @@ protected function treeWalkClauses($clause, $type) {
if (!is_string($clause[1][0])) {
$clause[1] = ['AND', $clause[1]];
}
return 'NOT (' . $this->treeWalkClauses($clause[1], $type) . ')';
return 'NOT (' . $this->treeWalkClauses($clause[1], $type, $depth + 1) . ')';

default:
return $this->composeClause($clause, $type);
return $this->composeClause($clause, $type, $depth);
}
}

Expand All @@ -395,11 +404,13 @@ protected function treeWalkClauses($clause, $type) {
* @param array $clause [$fieldName, $operator, $criteria]
* @param string $type
* WHERE|HAVING|ON
* @param int $depth
* @return string SQL
* @throws \API_Exception
* @throws \Exception
*/
protected function composeClause(array $clause, string $type) {
protected function composeClause(array $clause, string $type, int $depth) {
$field = NULL;
// Pad array for unary operators
[$expr, $operator, $value] = array_pad($clause, 3, NULL);
if (!in_array($operator, CoreUtil::getOperators(), TRUE)) {
Expand Down Expand Up @@ -454,7 +465,7 @@ protected function composeClause(array $clause, string $type) {
if ($fieldName && $valExpr->getType() === 'SqlString') {
$value = $valExpr->getExpr();
FormattingUtil::formatInputValue($value, $fieldName, $this->apiFieldSpec[$fieldName], $operator);
return $this->createSQLClause($fieldAlias, $operator, $value, $this->apiFieldSpec[$fieldName]);
return $this->createSQLClause($fieldAlias, $operator, $value, $this->apiFieldSpec[$fieldName], $depth);
}
else {
$value = $valExpr->render($this->apiFieldSpec);
Expand All @@ -467,7 +478,7 @@ protected function composeClause(array $clause, string $type) {
}
}

$sqlClause = $this->createSQLClause($fieldAlias, $operator, $value, $field ?? NULL);
$sqlClause = $this->createSQLClause($fieldAlias, $operator, $value, $field, $depth);
if ($sqlClause === NULL) {
throw new \API_Exception("Invalid value in $type clause for '$expr'");
}
Expand All @@ -479,10 +490,25 @@ protected function composeClause(array $clause, string $type) {
* @param string $operator
* @param mixed $value
* @param array|null $field
* @param int $depth
* @return array|string|NULL
* @throws \Exception
*/
protected function createSQLClause($fieldAlias, $operator, $value, $field) {
protected function createSQLClause($fieldAlias, $operator, $value, $field, int $depth) {
if (!empty($field['operators']) && !in_array($operator, $field['operators'], TRUE)) {
throw new \API_Exception('Illegal operator for ' . $field['name']);
}
// Some fields use a callback to generate their sql
if (!empty($field['sql_filters'])) {
$sql = [];
foreach ($field['sql_filters'] as $filter) {
$clause = is_callable($filter) ? $filter($fieldAlias, $operator, $value, $this, $depth) : NULL;
if ($clause) {
$sql[] = $clause;
}
}
return $sql ? implode(' AND ', $sql) : NULL;
}
if ($operator === 'CONTAINS') {
switch ($field['serialize'] ?? NULL) {
case \CRM_Core_DAO::SERIALIZE_JSON:
Expand Down
Loading

0 comments on commit 2841601

Please sign in to comment.