APIv4 - Default select clause to exclude "Extra" fields
authorColeman Watts <coleman@civicrm.org>
Sat, 11 Sep 2021 19:16:18 +0000 (15:16 -0400)
committerColeman Watts <coleman@civicrm.org>
Mon, 13 Sep 2021 13:45:12 +0000 (09:45 -0400)
Recently a "type" property was added to getFieldSpec to allow "Extra"
calculated fields to be declared. This updates api Get to *not*
select those fields by default, as their calculation can be expensive.

Civi/Api4/Generic/AbstractGetAction.php
Civi/Api4/Generic/DAOGetAction.php
ext/afform/core/Civi/Api4/Action/Afform/Get.php
ext/afform/core/Civi/Api4/Afform.php
ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php [new file with mode: 0644]
ext/afform/mock/tests/phpunit/api/v4/AfformCustomFieldUsageTest.php
ext/afform/mock/tests/phpunit/api/v4/AfformTest.php
ext/oauth-client/Civi/Api4/OAuthProvider.php
tests/phpunit/api/v4/Action/BasicActionsTest.php
tests/phpunit/api/v4/Action/CurrentFilterTest.php
tests/phpunit/api/v4/Mock/Api4/MockBasicEntity.php

index 0d5b27739b996d127460e54d49f56057d9ba9229..5fd927468302cec43c826dce4293cf47bbccdfe3 100644 (file)
@@ -66,18 +66,32 @@ abstract class AbstractGetAction extends AbstractQueryAction {
   }
 
   /**
-   * Adds all fields matched by the * wildcard
+   * Adds all standard fields matched by the * wildcard
+   *
+   * Note: this function only deals with simple wildcard expressions.
+   * It ignores those containing special characters like dots or parentheses,
+   * they are handled separately in Api4SelectQuery.
    *
    * @throws \API_Exception
    */
   protected function expandSelectClauseWildcards() {
+    if (!$this->select) {
+      $this->select = ['*'];
+    }
+    // Get expressions containing wildcards but no dots or parentheses
     $wildFields = array_filter($this->select, function($item) {
       return strpos($item, '*') !== FALSE && strpos($item, '.') === FALSE && strpos($item, '(') === FALSE && strpos($item, ' ') === FALSE;
     });
-    foreach ($wildFields as $item) {
-      $pos = array_search($item, array_values($this->select));
-      $matches = SelectUtil::getMatchingFields($item, array_column($this->entityFields(), 'name'));
-      array_splice($this->select, $pos, 1, $matches);
+    if ($wildFields) {
+      // Wildcards should not match "Extra" fields
+      $standardFields = array_filter(array_map(function($field) {
+        return $field['type'] === 'Extra' ? NULL : $field['name'];
+      }, $this->entityFields()));
+      foreach ($wildFields as $item) {
+        $pos = array_search($item, array_values($this->select));
+        $matches = SelectUtil::getMatchingFields($item, $standardFields);
+        array_splice($this->select, $pos, 1, $matches);
+      }
     }
     $this->select = array_unique($this->select);
   }
index aedc6362ca4ac5e99607970a09cd0b9785a7c67f..e8a46ce5b54dc7e006e0fa1cb58b0be22a489969 100644 (file)
@@ -29,9 +29,9 @@ class DAOGetAction extends AbstractGetAction {
   use Traits\DAOActionTrait;
 
   /**
-   * Fields to return. Defaults to all non-custom fields `['*']`.
+   * Fields to return. Defaults to all standard (non-custom, non-extra) fields `['*']`.
    *
-   * The keyword `"custom.*"` selects all custom fields. So to select all core + custom fields, select `['*', 'custom.*']`.
+   * The keyword `"custom.*"` selects all custom fields. So to select all standard + custom fields, select `['*', 'custom.*']`.
    *
    * Use the dot notation to perform joins in the select clause, e.g. selecting `['*', 'contact.*']` from `Email::get()`
    * will select all fields for the email + all fields for the related contact.
index 6a9ae6e68183edefdbc73a241f9392fd3b1eeabb..f02b12410cdbf176ef13df5906caa11d775ff2de 100644 (file)
@@ -17,7 +17,7 @@ class Get extends \Civi\Api4\Generic\BasicGetAction {
   public function getRecords() {
     /** @var \CRM_Afform_AfformScanner $scanner */
     $scanner = \Civi::service('afform_scanner');
-    $getComputed = $this->_isFieldSelected('has_local') || $this->_isFieldSelected('has_base');
+    $getComputed = $this->_isFieldSelected('has_local''has_base');
     $getLayout = $this->_isFieldSelected('layout');
     $values = [];
 
index ec76fd3df2f29c2059483c2e9e844b32a879b163..8ccf9eb54d3339a145bd7d7a798054f548dab130 100644 (file)
@@ -201,19 +201,23 @@ class Afform extends Generic\AbstractEntity {
       if ($self->getAction() === 'get') {
         $fields[] = [
           'name' => 'module_name',
+          'type' => 'Extra',
           'readonly' => TRUE,
         ];
         $fields[] = [
           'name' => 'directive_name',
+          'type' => 'Extra',
           'readonly' => TRUE,
         ];
         $fields[] = [
           'name' => 'has_local',
+          'type' => 'Extra',
           'data_type' => 'Boolean',
           'readonly' => TRUE,
         ];
         $fields[] = [
           'name' => 'has_base',
+          'type' => 'Extra',
           'data_type' => 'Boolean',
           'readonly' => TRUE,
         ];
diff --git a/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php b/ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php
new file mode 100644 (file)
index 0000000..2268cb1
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+namespace Civi\Afform;
+
+use Civi\Api4\Afform;
+use Civi\Test\HeadlessInterface;
+use Civi\Test\TransactionalInterface;
+
+/**
+ * @group headless
+ */
+class AfformGetTest extends \PHPUnit\Framework\TestCase implements HeadlessInterface, TransactionalInterface {
+
+  private $formName = 'abc_123_test';
+
+  public function setUpHeadless() {
+    return \Civi\Test::headless()->installMe(__DIR__)->apply();
+  }
+
+  public function tearDown(): void {
+    Afform::revert(FALSE)->addWhere('name', '=', $this->formName)->execute();
+    parent::tearDown();
+  }
+
+  public function testGetReturnFields() {
+    Afform::create()
+      ->addValue('name', $this->formName)
+      ->addValue('title', 'Test Form')
+      ->execute();
+
+    // Omitting select should return regular fields but not extra fields
+    $result = Afform::get()
+      ->addWhere('name', '=', $this->formName)
+      ->execute()->single();
+    $this->assertEquals($this->formName, $result['name']);
+    $this->assertArrayNotHasKey('directive_name', $result);
+    $this->assertArrayNotHasKey('has_base', $result);
+
+    // Select * should also return regular fields only
+    $result = Afform::get()
+      ->addSelect('*')
+      ->addWhere('name', '=', $this->formName)
+      ->execute()->single();
+    $this->assertEquals($this->formName, $result['name']);
+    $this->assertArrayNotHasKey('module_name', $result);
+    $this->assertArrayNotHasKey('has_local', $result);
+
+    // Selecting * and has_base should return core and that one extra field
+    $result = Afform::get()
+      ->addSelect('*', 'has_base')
+      ->addWhere('name', '=', $this->formName)
+      ->execute()->single();
+    $this->assertEquals($this->formName, $result['name']);
+    $this->assertFalse($result['has_base']);
+    $this->assertArrayNotHasKey('has_local', $result);
+  }
+
+}
index 3ece0fb88deed85ffec8ec21ca5a9c9b179069c0..e1e848a208d8d8f9f93f352bd17d2f97a8bb1573 100644 (file)
@@ -50,6 +50,7 @@ EOHTML;
     // Creating a custom group should automatically create an afform block
     $block = \Civi\Api4\Afform::get()
       ->addWhere('name', '=', 'afblockCustom_MyThings')
+      ->addSelect('layout', 'directive_name')
       ->setLayoutFormat('shallow')
       ->setFormatWhitespace(TRUE)
       ->execute()->single();
index 3d57c90bb0c1c6e51bce222fcad32182f5ea077d..ffb62766f81c29d801d5b2399e500813372b7d32 100644 (file)
@@ -53,7 +53,9 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
     Civi\Api4\Afform::revert()->addWhere('name', '=', $formName)->execute();
 
     $message = 'The initial Afform.get should return default data';
-    $result = Civi\Api4\Afform::get()->addWhere('name', '=', $formName)->execute();
+    $result = Civi\Api4\Afform::get()
+      ->addSelect('*', 'has_base', 'has_local')
+      ->addWhere('name', '=', $formName)->execute();
     $this->assertEquals($formName, $result[0]['name'], $message);
     $this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
     $this->assertEquals($get($originalMetadata, 'description'), $get($result[0], 'description'), $message);
@@ -75,7 +77,9 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
     $this->assertEquals('The temporary description', $result[0]['description'], $message);
 
     $message = 'After updating, the Afform.get API should return blended data';
-    $result = Civi\Api4\Afform::get()->addWhere('name', '=', $formName)->execute();
+    $result = Civi\Api4\Afform::get()
+      ->addSelect('*', 'has_base', 'has_local')
+      ->addWhere('name', '=', $formName)->execute();
     $this->assertEquals($formName, $result[0]['name'], $message);
     $this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
     $this->assertEquals('The temporary description', $get($result[0], 'description'), $message);
@@ -88,7 +92,9 @@ class api_v4_AfformTest extends api_v4_AfformTestCase {
 
     Civi\Api4\Afform::revert()->addWhere('name', '=', $formName)->execute();
     $message = 'After reverting, the final Afform.get should return default data';
-    $result = Civi\Api4\Afform::get()->addWhere('name', '=', $formName)->execute();
+    $result = Civi\Api4\Afform::get()
+      ->addSelect('*', 'has_base', 'has_local')
+      ->addWhere('name', '=', $formName)->execute();
     $this->assertEquals($formName, $result[0]['name'], $message);
     $this->assertEquals($get($originalMetadata, 'title'), $get($result[0], 'title'), $message);
     $this->assertEquals($get($originalMetadata, 'description'), $get($result[0], 'description'), $message);
index fc556d51923cee391cf0ccd21857fa5e971b8e45..726248815f59284dff6e9dd5edc5c69f5ed6c6aa 100644 (file)
@@ -59,6 +59,9 @@ class OAuthProvider extends Generic\AbstractEntity {
         [
           'name' => 'options',
         ],
+        [
+          'name' => 'contactTemplate',
+        ],
       ];
     });
     return $action->setCheckPermissions($checkPermissions);
index 4dae4f5f9d73a6972e8f3ea0e9a9a8793e38cadf..d8d5e9d1c16fc72701200d537f4ae224bda07084 100644 (file)
@@ -158,7 +158,7 @@ class BasicActionsTest extends UnitTestCase {
   public function testGetFields() {
     $getFields = MockBasicEntity::getFields()->execute()->indexBy('name');
 
-    $this->assertCount(7, $getFields);
+    $this->assertCount(8, $getFields);
     $this->assertEquals('Identifier', $getFields['identifier']['title']);
     // Ensure default data type is "String" when not specified
     $this->assertEquals('String', $getFields['color']['data_type']);
index febac7383b2062f0e3871befa8ca266abc7e85d5..aa214029a058bbcf59a83dd19a6396b49bd4724e 100644 (file)
@@ -83,6 +83,12 @@ class CurrentFilterTest extends UnitTestCase {
     $this->assertArrayNotHasKey($expiring['id'], $notCurrent);
     $this->assertArrayHasKey($past['id'], $notCurrent);
     $this->assertArrayHasKey($inactive['id'], $notCurrent);
+
+    // Assert that "Extra" fields like is_current are not returned with select *
+    $defaultGet = Relationship::get()->setLimit(1)->execute()->single();
+    $this->assertArrayNotHasKey('is_current', $defaultGet);
+    $starGet = Relationship::get()->addSelect('*')->setLimit(1)->execute()->single();
+    $this->assertArrayNotHasKey('is_current', $starGet);
   }
 
 }
index b991947cd73993041f6beaec25e2fe7bfa4c5229..a9c832e0507f8c113ad372de374e4127ccaeeb54 100644 (file)
@@ -60,6 +60,9 @@ class MockBasicEntity extends Generic\BasicEntity {
         [
           'name' => 'size',
         ],
+        [
+          'name' => 'foo',
+        ],
         [
           'name' => 'weight',
           'data_type' => 'Integer',