Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SearchKit - enable search by case role #22121

Merged
merged 2 commits into from
Nov 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Civi/Api4/CaseContact.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,24 @@ protected static function getEntityTitle($plural = FALSE) {
return $plural ? ts('Case Clients') : ts('Case Client');
}

/**
* @return array
*/
public static function getInfo() {
$info = parent::getInfo();
$info['bridge_title'] = ts('Clients');
$info['bridge'] = [
'case_id' => [
'to' => 'contact_id',
'description' => ts('Cases with this contact as a client'),
],
'contact_id' => [
'label' => ts('Clients'),
'to' => 'case_id',
'description' => ts('Clients for this case'),
],
];
return $info;
}

}
4 changes: 2 additions & 2 deletions Civi/Api4/EntityFinancialAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class EntityFinancialAccount extends Generic\DAOEntity {
public static function getInfo() {
$info = parent::getInfo();
$info['bridge'] = [
'entity_id' => [],
'financial_account_id' => [],
'entity_id' => ['to' => 'financial_account_id'],
'financial_account_id' => ['to' => 'entity_id'],
];
return $info;
}
Expand Down
4 changes: 2 additions & 2 deletions Civi/Api4/EntityFinancialTrxn.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class EntityFinancialTrxn extends Generic\DAOEntity {
public static function getInfo() {
$info = parent::getInfo();
$info['bridge'] = [
'entity_id' => [],
'financial_trxn_id' => [],
'entity_id' => ['to' => 'financial_trxn_id'],
'financial_trxn_id' => ['to' => 'entity_id'],
];
return $info;
}
Expand Down
11 changes: 8 additions & 3 deletions Civi/Api4/Generic/Traits/EntityBridge.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
* A bridge is a small table that provides an intermediary link between two other tables.
*
* The API can automatically incorporate a Bridge into a join expression.
*
* Note: at time of writing this trait does nothing except affect the "type" shown in Entity::get() metadata.
*/
trait EntityBridge {

Expand All @@ -29,13 +27,20 @@ trait EntityBridge {
*/
public static function getInfo() {
$info = parent::getInfo();
$bridgeFields = [];
if (!empty($info['dao'])) {
foreach (($info['dao'])::fields() as $field) {
if (!empty($field['FKClassName']) || $field['name'] === 'entity_id') {
$info['bridge'][$field['name']] = [];
$bridgeFields[] = $field['name'];
}
}
}
if (count($bridgeFields) === 2) {
$info['bridge'] = [
$bridgeFields[0] => ['to' => $bridgeFields[1]],
$bridgeFields[1] => ['to' => $bridgeFields[0]],
];
}
return $info;
}

Expand Down
10 changes: 8 additions & 2 deletions Civi/Api4/GroupContact.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,14 @@ public static function update($checkPermissions = TRUE) {
public static function getInfo() {
$info = parent::getInfo();
$info['bridge'] = [
'group_id' => ['description' => ts('Static (non-smart) group contacts')],
'contact_id' => ['description' => ts('Static (non-smart) group contacts')],
'group_id' => [
'to' => 'contact_id',
'description' => ts('Static (non-smart) group contacts'),
],
'contact_id' => [
'to' => 'group_id',
'description' => ts('Static (non-smart) group contacts'),
],
];
return $info;
}
Expand Down
29 changes: 13 additions & 16 deletions Civi/Api4/Query/Api4SelectQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -872,31 +872,28 @@ protected function addBridgeJoin($joinTree, $joinEntity, $alias, $side) {
* @throws \API_Exception
*/
private function getBridgeRefs(string $bridgeEntity, string $joinEntity): array {
$bridgeFields = CoreUtil::getInfoItem($bridgeEntity, 'bridge') ?? [];
// Sanity check - bridge entity should declare exactly 2 FK fields
if (count($bridgeFields) !== 2) {
throw new \API_Exception("Illegal bridge entity specified: $bridgeEntity. Expected 2 bridge fields, found " . count($bridgeFields));
}
$bridges = CoreUtil::getInfoItem($bridgeEntity, 'bridge') ?? [];
/* @var \CRM_Core_DAO $bridgeDAO */
$bridgeDAO = CoreUtil::getInfoItem($bridgeEntity, 'dao');
$bridgeEntityFields = \Civi\API\Request::create($bridgeEntity, 'get', ['version' => 4, 'checkPermissions' => $this->getCheckPermissions()])->entityFields();
$bridgeTable = $bridgeDAO::getTableName();

// Get the 2 bridge reference columns as CRM_Core_Reference_* objects
$joinRef = $baseRef = NULL;
foreach ($bridgeDAO::getReferenceColumns() as $ref) {
if (array_key_exists($ref->getReferenceKey(), $bridgeFields)) {
if (!$joinRef && in_array($joinEntity, $ref->getTargetEntities())) {
$joinRef = $ref;
$referenceColumns = $bridgeDAO::getReferenceColumns();
foreach ($referenceColumns as $joinRef) {
$refKey = $joinRef->getReferenceKey();
if (array_key_exists($refKey, $bridges) && in_array($joinEntity, $joinRef->getTargetEntities())) {
if (!empty($bridgeEntityFields[$refKey]['fk_entity']) && $joinEntity !== $bridgeEntityFields[$refKey]['fk_entity']) {
continue;
}
else {
$baseRef = $ref;
foreach ($bridgeDAO::getReferenceColumns() as $baseRef) {
if ($baseRef->getReferenceKey() === $bridges[$refKey]['to']) {
return [$bridgeTable, $baseRef, $joinRef];
}
}
}
}
if (!$joinRef || !$baseRef) {
throw new \API_Exception("Unable to join $bridgeEntity to $joinEntity");
}
return [$bridgeTable, $baseRef, $joinRef];
throw new \API_Exception("Unable to join $bridgeEntity to $joinEntity");
}

/**
Expand Down
13 changes: 11 additions & 2 deletions Civi/Api4/RelationshipCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,18 @@ public static function getInfo() {
$info = parent::getInfo();
$info['bridge_title'] = ts('Relationship');
$info['bridge'] = [
'near_contact_id' => ['description' => ts('One or more contacts with a relationship to this contact')],
'far_contact_id' => ['description' => ts('One or more contacts with a relationship to this contact')],
'near_contact_id' => [
'to' => 'far_contact_id',
'description' => ts('One or more related contacts'),
],
];
if (in_array('CiviCase', \Civi::settings()->get('enable_components'), TRUE)) {
$info['bridge']['case_id'] = [
'to' => 'far_contact_id',
'label' => ts('Case Roles'),
'description' => ts('Cases in which this contact has a role'),
];
}
return $info;
}

Expand Down
102 changes: 56 additions & 46 deletions ext/search_kit/Civi/Search/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,21 +180,22 @@ public static function getJoins(array $allowedEntities) {
foreach ($allowedEntities as $entity) {
// Multi-record custom field groups (to-date only the contact entity supports these)
if (in_array('CustomValue', $entity['type'])) {
// TODO: Lookup target entity from custom group if someday other entities support multi-record custom data
$targetEntity = $allowedEntities['Contact'];
// Join from Custom group to Contact (n-1)
$alias = $entity['name'] . '_Contact_entity_id';
$alias = "{$entity['name']}_{$targetEntity['name']}_entity_id";
$joins[$entity['name']][] = [
'label' => $entity['title'] . ' ' . $targetEntity['title'],
'description' => '',
'entity' => 'Contact',
'entity' => $targetEntity['name'],
'conditions' => self::getJoinConditions('entity_id', $alias . '.id'),
'defaults' => self::getJoinDefaults($alias, $targetEntity),
'alias' => $alias,
'multi' => FALSE,
];
// Join from Contact to Custom group (n-n)
$alias = 'Contact_' . $entity['name'] . '_entity_id';
$joins['Contact'][] = [
$alias = "{$targetEntity['name']}_{$entity['name']}_entity_id";
$joins[$targetEntity['name']][] = [
'label' => $entity['title_plural'],
'description' => '',
'entity' => $entity['name'],
Expand All @@ -209,43 +210,32 @@ public static function getJoins(array $allowedEntities) {
/* @var \CRM_Core_DAO $daoClass */
$daoClass = $entity['dao'];
$references = $daoClass::getReferenceColumns();
// Only the first bridge reference gets processed, so if it's dynamic we want to be sure it's first in the list
usort($references, function($first, $second) {
foreach ([-1 => $first, 1 => $second] as $weight => $reference) {
if (is_a($reference, 'CRM_Core_Reference_Dynamic')) {
return $weight;
}
}
return 0;
});
$fields = array_column($entity['fields'], NULL, 'name');
$bridge = in_array('EntityBridge', $entity['type']) ? $entity['name'] : NULL;
$bridgeFields = array_keys($entity['bridge'] ?? []);
foreach ($references as $reference) {
$keyField = $fields[$reference->getReferenceKey()] ?? NULL;
if (
// Sanity check - keyField must exist
!$keyField ||
// Exclude any joins that are better represented by pseudoconstants
is_a($reference, 'CRM_Core_Reference_OptionValue') || (!$bridge && !empty($keyField['options'])) ||
// Limit bridge joins to just the first
($bridge && array_search($keyField['name'], $bridgeFields) !== 0) ||
// Sanity check - table should match
$daoClass::getTableName() !== $reference->getReferenceTable()
) {
continue;
}
// Dynamic references use a column like "entity_table" (for normal joins this value will be null)
$dynamicCol = $reference->getTypeColumn();

// For dynamic references getTargetEntities will return multiple targets; for normal joins this loop will only run once
foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) {
if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) {
// Non-bridge joins directly between 2 entities
if (!$bridge) {
foreach ($references as $reference) {
$keyField = $fields[$reference->getReferenceKey()] ?? NULL;
if (
// Sanity check - keyField must exist
!$keyField ||
// Exclude any joins that are better represented by pseudoconstants
is_a($reference, 'CRM_Core_Reference_OptionValue') || !empty($keyField['options']) ||
// Sanity check - table should match
$daoClass::getTableName() !== $reference->getReferenceTable()
) {
continue;
}
$targetEntity = $allowedEntities[$targetEntityName];
// Non-bridge joins directly between 2 entities
if (!$bridge) {
// Dynamic references use a column like "entity_table" (for normal joins this value will be null)
$dynamicCol = $reference->getTypeColumn();

// For dynamic references getTargetEntities will return multiple targets; for normal joins this loop will only run once
foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) {
if (!isset($allowedEntities[$targetEntityName]) || $targetEntityName === $entity['name']) {
continue;
}
$targetEntity = $allowedEntities[$targetEntityName];
// Add the straight 1-1 join
$alias = $entity['name'] . '_' . $targetEntityName . '_' . $keyField['name'];
$joins[$entity['name']][] = [
Expand All @@ -269,21 +259,27 @@ public static function getJoins(array $allowedEntities) {
'multi' => TRUE,
];
}
// Bridge joins (sanity check - bridge must specify exactly 2 FK fields)
elseif (count($entity['bridge']) === 2) {
// Get the other entity being linked through this bridge
$baseKey = array_search($reference->getReferenceKey(), $bridgeFields) ? $bridgeFields[0] : $bridgeFields[1];
}
}
// Bridge joins go through an intermediary table
elseif (!empty($entity['bridge'])) {
foreach ($entity['bridge'] as $targetKey => $bridgeInfo) {
$baseKey = $bridgeInfo['to'];
$reference = self::getReference($targetKey, $references);
$dynamicCol = $reference->getTypeColumn();
$keyField = $fields[$reference->getReferenceKey()] ?? NULL;
foreach ($reference->getTargetEntities() as $targetTable => $targetEntityName) {
$targetEntity = $allowedEntities[$targetEntityName] ?? NULL;
$baseEntity = $allowedEntities[$fields[$baseKey]['fk_entity']] ?? NULL;
if (!$baseEntity) {
if (!$targetEntity || !$baseEntity) {
continue;
}
// Add joins for the two entities that connect through this bridge (n-n)
$symmetric = $baseEntity['name'] === $targetEntityName;
$targetsTitle = $symmetric ? $allowedEntities[$bridge]['title_plural'] : $targetEntity['title_plural'];
$targetsTitle = $bridgeInfo['label'] ?? $targetEntity['title_plural'];
$alias = $baseEntity['name'] . "_{$bridge}_" . $targetEntityName;
$joins[$baseEntity['name']][] = [
'label' => $baseEntity['title'] . ' ' . $targetsTitle,
'description' => $entity['bridge'][$baseKey]['description'] ?? E::ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
'description' => $bridgeInfo['description'] ?? E::ts('Multiple %1 per %2', [1 => $targetsTitle, 2 => $baseEntity['title']]),
'entity' => $targetEntityName,
'conditions' => array_merge(
[$bridge],
Expand All @@ -294,10 +290,11 @@ public static function getJoins(array $allowedEntities) {
'alias' => $alias,
'multi' => TRUE,
];
if (!$symmetric) {
// Back-fill the reverse join if declared
if ($dynamicCol && $keyField && !empty($entity['bridge'][$baseKey])) {
$alias = $targetEntityName . "_{$bridge}_" . $baseEntity['name'];
$joins[$targetEntityName][] = [
'label' => $targetEntity['title'] . ' ' . $baseEntity['title_plural'],
'label' => $targetEntity['title'] . ' ' . ($entity['bridge'][$baseKey]['label'] ?? $baseEntity['title_plural']),
'description' => $entity['bridge'][$reference->getReferenceKey()]['description'] ?? E::ts('Multiple %1 per %2', [1 => $baseEntity['title_plural'], 2 => $targetEntity['title']]),
'entity' => $baseEntity['name'],
'conditions' => array_merge(
Expand All @@ -318,6 +315,19 @@ public static function getJoins(array $allowedEntities) {
return $joins;
}

/**
* @param string $fieldName
* @param \CRM_Core_Reference_Basic[] $references
* @return \CRM_Core_Reference_Basic
*/
private static function getReference(string $fieldName, array $references) {
foreach ($references as $reference) {
if ($reference->getReferenceKey() === $fieldName) {
return $reference;
}
}
}

/**
* Boilerplate join clause
*
Expand Down
Loading