SearchKit - Expose relationship description, created/modified_date and permission...
authorColeman Watts <coleman@civicrm.org>
Sun, 23 Jan 2022 04:29:58 +0000 (23:29 -0500)
committerColeman Watts <coleman@civicrm.org>
Sun, 23 Jan 2022 04:34:09 +0000 (23:34 -0500)
This uses in-sql calculated fields instead of adding 5 new columns to the civicrm_relationship_cache table
Fixes dev/core#3019

Civi/Api4/Service/Spec/Provider/RelationshipCacheSpecProvider.php [new file with mode: 0644]
tests/phpunit/api/v4/Entity/RelationshipTest.php

diff --git a/Civi/Api4/Service/Spec/Provider/RelationshipCacheSpecProvider.php b/Civi/Api4/Service/Spec/Provider/RelationshipCacheSpecProvider.php
new file mode 100644 (file)
index 0000000..cb65e5a
--- /dev/null
@@ -0,0 +1,99 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Service\Spec\Provider;
+
+use Civi\Api4\Service\Spec\FieldSpec;
+use Civi\Api4\Service\Spec\RequestSpec;
+
+class RelationshipCacheSpecProvider implements Generic\SpecProviderInterface {
+
+  /**
+   * @param \Civi\Api4\Service\Spec\RequestSpec $spec
+   */
+  public function modifySpec(RequestSpec $spec) {
+    $mirrorFields = [
+      'description' => 'description',
+      // Alias these two to avoid name conflict with fields in civicrm_contact table during bridge joins
+      'created_date' => 'relationship_created_date',
+      'modified_date' => 'relationship_modified_date',
+    ];
+    $relationshipFields = \CRM_Contact_DAO_Relationship::getSupportedFields();
+    foreach (array_intersect_key($relationshipFields, $mirrorFields) as $origName => $origField) {
+      $field = new FieldSpec($mirrorFields[$origName], $spec->getEntity(), \CRM_Utils_Type::typeToString($origField['type']));
+      $field
+        ->setTitle($origField['title'])
+        ->setLabel($origField['html']['label'] ?? NULL)
+        // Fetches the value from the relationship
+        ->setColumnName('relationship_id')
+        ->setDescription($origField['description'])
+        ->setSqlRenderer([__CLASS__, 'mirrorRelationshipField']);
+      $spec->addFieldSpec($field);
+    }
+
+    $directionalFields = [
+      'permission_near_to_far' => [
+        'title' => ts("Permission to access related contact"),
+        'description' => ts('Whether contact has permission to view or update update the related contact'),
+      ],
+      'permission_far_to_near' => [
+        'title' => ts("Permission to be accessed by related contact"),
+        'description' => ts('Whether related contact has permission to view or update this contact'),
+      ],
+    ];
+    foreach ($directionalFields as $name => $fieldInfo) {
+      $field = new FieldSpec($name, $spec->getEntity(), 'Integer');
+      $field
+        ->setTitle($fieldInfo['title'])
+        // Fetches the value from the relationship
+        ->setColumnName('relationship_id')
+        ->setDescription($fieldInfo['description'])
+        ->setOptionsCallback(['CRM_Core_SelectValues', 'getPermissionedRelationshipOptions'])
+        ->setSqlRenderer([__CLASS__, 'directionalRelationshipField']);
+      $spec->addFieldSpec($field);
+    }
+
+  }
+
+  /**
+   * @inheritDoc
+   */
+  public function applies($entity, $action) {
+    return $entity === 'RelationshipCache' && $action === 'get';
+  }
+
+  /**
+   * Generates sql for `description`, `relationship_created_date` and `relationship_modified_date` pseudo fields
+   *
+   * Note: the latter two have `relationship_` prefixing the field names to avoid naming conflicts during bridge joins.
+   *
+   * @param array $field
+   * return string
+   */
+  public static function mirrorRelationshipField(array $field): string {
+    $fieldName = str_replace('relationship_', '', $field['name']);
+    return "(SELECT r.`$fieldName` FROM `civicrm_relationship` r WHERE r.`id` = {$field['sql_name']})";
+  }
+
+  /**
+   * Generates sql for `permission_near_to_far` and `permission_far_to_near` pseudo fields
+   *
+   * @param array $field
+   * return string
+   */
+  public static function directionalRelationshipField(array $field): string {
+    $direction = $field['name'] === 'permission_near_to_far' ? 'a_b' : 'b_a';
+    $orientation = str_replace('.`relationship_id`', '.`orientation`', $field['sql_name']);
+    return "(SELECT IF($orientation = '$direction', r.is_permission_a_b, r.is_permission_b_a) FROM `civicrm_relationship` r WHERE r.`id` = {$field['sql_name']})";
+  }
+
+}
index 81ab67d9b1bfb1f224f4259842f48c2759c62449..ef42074f665e825a31892660657b6b53cc55694c 100644 (file)
@@ -32,7 +32,7 @@ use Civi\Test\TransactionalInterface;
  */
 class RelationshipTest extends UnitTestCase implements TransactionalInterface {
 
-  public function testRelCache() {
+  public function testRelCacheCount() {
     $c1 = Contact::create(FALSE)->addValue('first_name', '1')->execute()->first()['id'];
     $c2 = Contact::create(FALSE)->addValue('first_name', '2')->execute()->first()['id'];
     Relationship::create(FALSE)
@@ -47,4 +47,34 @@ class RelationshipTest extends UnitTestCase implements TransactionalInterface {
     $this->assertCount(2, $cacheRecords);
   }
 
+  public function testRelCacheCalcFields() {
+    $c1 = Contact::create(FALSE)->addValue('first_name', '1')->execute()->first()['id'];
+    $c2 = Contact::create(FALSE)->addValue('first_name', '2')->execute()->first()['id'];
+    $relationship = Relationship::create(FALSE)
+      ->setValues([
+        'contact_id_a' => $c1,
+        'contact_id_b' => $c2,
+        'relationship_type_id' => 1,
+        'description' => "Wow, we're related!",
+        'is_permission_a_b' => 1,
+        'is_permission_b_a' => 2,
+      ])->execute()->first();
+    $relationship = Relationship::get(FALSE)
+      ->addWhere('id', '=', $relationship['id'])
+      ->execute()->first();
+    $cacheRecords = RelationshipCache::get(FALSE)
+      ->addWhere('near_contact_id', 'IN', [$c1, $c2])
+      ->addSelect('near_contact_id', 'orientation', 'description', 'relationship_created_date', 'relationship_modified_date', 'permission_near_to_far', 'permission_far_to_near')
+      ->execute()->indexBy('near_contact_id');
+    $this->assertCount(2, $cacheRecords);
+    $this->assertEquals("Wow, we're related!", $cacheRecords[$c1]['description']);
+    $this->assertEquals("Wow, we're related!", $cacheRecords[$c2]['description']);
+    $this->assertEquals(1, $cacheRecords[$c1]['permission_near_to_far']);
+    $this->assertEquals(2, $cacheRecords[$c2]['permission_near_to_far']);
+    $this->assertEquals(2, $cacheRecords[$c1]['permission_far_to_near']);
+    $this->assertEquals(1, $cacheRecords[$c2]['permission_far_to_near']);
+    $this->assertEquals($relationship['created_date'], $cacheRecords[$c1]['relationship_created_date']);
+    $this->assertEquals($relationship['modified_date'], $cacheRecords[$c2]['relationship_modified_date']);
+  }
+
 }