Skip to content

Commit

Permalink
APIv4 - Default select clause to exclude "Extra" fields
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
colemanw committed Sep 13, 2021
1 parent 741ebf1 commit 60a6221
Show file tree
Hide file tree
Showing 11 changed files with 106 additions and 12 deletions.
24 changes: 19 additions & 5 deletions Civi/Api4/Generic/AbstractGetAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,32 @@ protected function setDefaultWhereClause() {
}

/**
* 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);
}
Expand Down
4 changes: 2 additions & 2 deletions Civi/Api4/Generic/DAOGetAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion ext/afform/core/Civi/Api4/Action/Afform/Get.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];

Expand Down
4 changes: 4 additions & 0 deletions ext/afform/core/Civi/Api4/Afform.php
Original file line number Diff line number Diff line change
Expand Up @@ -201,19 +201,23 @@ public static function getFields($checkPermissions = TRUE) {
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,
];
Expand Down
57 changes: 57 additions & 0 deletions ext/afform/core/tests/phpunit/Civi/Afform/AfformGetTest.php
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public function testMultiRecordCustomBlock(): void {
// 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();
Expand Down
12 changes: 9 additions & 3 deletions ext/afform/mock/tests/phpunit/api/v4/AfformTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ public function testGetUpdateRevert($formName, $originalMetadata): void {
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);
Expand All @@ -75,7 +77,9 @@ public function testGetUpdateRevert($formName, $originalMetadata): void {
$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);
Expand All @@ -88,7 +92,9 @@ public function testGetUpdateRevert($formName, $originalMetadata): void {

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);
Expand Down
3 changes: 3 additions & 0 deletions ext/oauth-client/Civi/Api4/OAuthProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public static function getFields($checkPermissions = TRUE) {
[
'name' => 'options',
],
[
'name' => 'contactTemplate',
],
];
});
return $action->setCheckPermissions($checkPermissions);
Expand Down
2 changes: 1 addition & 1 deletion tests/phpunit/api/v4/Action/BasicActionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public function testBatchFrobnicate() {
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']);
Expand Down
6 changes: 6 additions & 0 deletions tests/phpunit/api/v4/Action/CurrentFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ public function testCurrentRelationship() {
$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);
}

}
3 changes: 3 additions & 0 deletions tests/phpunit/api/v4/Mock/Api4/MockBasicEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public static function getFields($checkPermissions = TRUE) {
[
'name' => 'size',
],
[
'name' => 'foo',
],
[
'name' => 'weight',
'data_type' => 'Integer',
Expand Down

0 comments on commit 60a6221

Please sign in to comment.