Skip to content

Commit

Permalink
ManagedEntity - Add update mode 'auto' and cleanup mode 'unmodified'
Browse files Browse the repository at this point in the history
These new settings work only for entities opted-in to the APIv4 ManagedEntity trait
Update mode 'auto' will only update a record if it has not been manually modified
Cleanup mode 'unmodified' will only delete a record if it has not been manually modified
  • Loading branch information
colemanw committed Nov 7, 2021
1 parent b8109f0 commit 638735c
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 15 deletions.
11 changes: 11 additions & 0 deletions CRM/Core/ManagedEntities.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static function getCleanupOptions() {
'always' => ts('Always'),
'never' => ts('Never'),
'unused' => ts('If Unused'),
'unmodified' => ts('If Not Modified'),
];
}

Expand Down Expand Up @@ -185,6 +186,7 @@ protected function reconcileEnabledModule(string $module): void {
$dao->name = $todo['name'];
$dao->entity_type = $todo['entity_type'];
$dao->entity_id = $todo['entity_id'];
$dao->entity_modified_date = $todo['entity_modified_date'];
$dao->id = $todo['id'];
$this->updateExistingEntity($dao, $todo);
}
Expand All @@ -197,6 +199,7 @@ protected function reconcileEnabledModule(string $module): void {
$dao->id = $todo['id'];
$dao->cleanup = $todo['cleanup'];
$dao->entity_id = $todo['entity_id'];
$dao->entity_modified_date = $todo['entity_modified_date'];
$this->removeStaleEntity($dao);
}
foreach ($this->getManagedEntitiesToCreate(['module' => $module]) as $todo) {
Expand Down Expand Up @@ -339,6 +342,10 @@ protected function updateExistingEntity($dao, $todo) {
$policy = $todo['update'] ?? 'always';
$doUpdate = ($policy === 'always');

if ($policy === 'auto') {
$doUpdate = empty($dao->entity_modified_date);
}

if ($doUpdate && $todo['params']['version'] == 3) {
$defaults = ['id' => $dao->entity_id];
if ($this->isActivationSupported($dao->entity_type)) {
Expand Down Expand Up @@ -426,6 +433,10 @@ protected function removeStaleEntity($dao) {
$doDelete = FALSE;
break;

case 'unmodified':
$doDelete = empty($dao->entity_modified_date);
break;

case 'unused':
$getRefCount = civicrm_api3($dao->entity_type, 'getrefcount', [
'debug' => 1,
Expand Down
205 changes: 190 additions & 15 deletions tests/phpunit/api/v4/Entity/ManagedEntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
namespace api\v4\Entity;

use api\v4\UnitTestCase;
use Civi\Api4\Managed;
use Civi\Api4\SavedSearch;
use Civi\Test\HookInterface;
use Civi\Test\TransactionalInterface;
Expand All @@ -27,9 +28,33 @@
* @group headless
*/
class ManagedEntityTest extends UnitTestCase implements TransactionalInterface, HookInterface {
/**
* @var array[]
*/
private $_managedEntities = [];

public function setUp() {
$this->_managedEntities = [];
parent::setUp();
}

public function hook_civicrm_managed(array &$entities): void {
$entities[] = [
$entities = array_merge($entities, $this->_managedEntities);
}

public function testGetFields() {
$fields = SavedSearch::getFields(FALSE)
->addWhere('type', '=', 'Extra')
->setLoadOptions(TRUE)
->execute()->indexBy('name');

$this->assertEquals('Boolean', $fields['has_base']['data_type']);
// If this core extension ever goes away or gets renamed, just pick a different one here
$this->assertArrayHasKey('org.civicrm.flexmailer', $fields['base_module']['options']);
}

public function testRevertSavedSearch() {
$this->_managedEntities[] = [
// Setting module to 'civicrm' works for the test but not sure we should actually support that
// as it's probably better to package stuff in a core extension instead of core itself.
'module' => 'civicrm',
Expand All @@ -52,20 +77,7 @@ public function hook_civicrm_managed(array &$entities): void {
],
],
];
}

public function testGetFields() {
$fields = SavedSearch::getFields(FALSE)
->addWhere('type', '=', 'Extra')
->setLoadOptions(TRUE)
->execute()->indexBy('name');

$this->assertEquals('Boolean', $fields['has_base']['data_type']);
// If this core extension ever goes away or gets renamed, just pick a different one here
$this->assertArrayHasKey('org.civicrm.flexmailer', $fields['base_module']['options']);
}

public function testRevertSavedSearch() {
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

$search = SavedSearch::get(FALSE)
Expand Down Expand Up @@ -101,7 +113,6 @@ public function testRevertSavedSearch() {
$result = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestManagedSavedSearch')
->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
->setDebug(TRUE)
->execute();
$search = $result->single();
$this->assertEquals('Original state', $search['description']);
Expand All @@ -128,6 +139,170 @@ public function testRevertSavedSearch() {
$this->assertNull($search['local_modified_date']);
}

public function testAutoUpdateSearch() {
$autoUpdateSearch = [
'module' => 'civicrm',
'name' => 'testAutoUpdate',
'entity' => 'SavedSearch',
'cleanup' => 'unmodified',
'update' => 'auto',
'params' => [
'version' => 4,
'values' => [
'name' => 'TestAutoUpdateSavedSearch',
'label' => 'Test AutoUpdate Search',
'description' => 'Original state',
'api_entity' => 'Email',
'api_params' => [
'version' => 4,
'select' => ['id'],
'orderBy' => ['id', 'ASC'],
],
],
],
];
// Add managed search
$this->_managedEntities[] = $autoUpdateSearch;
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

$search = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->addSelect('description', 'local_modified_date')
->execute()->single();
$this->assertEquals('Original state', $search['description']);
$this->assertNull($search['local_modified_date']);

// Remove managed search
$this->_managedEntities = [];
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

// Because the search was not modified, it will be deleted (cleanup = unmodified)
$search = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->execute();
$this->assertCount(0, $search);

// Add managed search
$this->_managedEntities[] = $autoUpdateSearch;
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

$search = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->addSelect('description', 'local_modified_date')
->execute()->single();
$this->assertEquals('Original state', $search['description']);
$this->assertNull($search['local_modified_date']);

// Update packaged version
$autoUpdateSearch['params']['values']['description'] = 'New packaged state';
$this->_managedEntities = [];
$this->_managedEntities[] = $autoUpdateSearch;
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

// Because the entity was not modified, it will be updated to match the new packaged version
$search = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->addSelect('description', 'local_modified_date')
->execute()->single();
$this->assertEquals('New packaged state', $search['description']);
$this->assertNull($search['local_modified_date']);

// Update local
SavedSearch::update(FALSE)
->addValue('id', $search['id'])
->addValue('description', 'Altered state')
->execute();

// Update packaged version
$autoUpdateSearch['params']['values']['description'] = 'Newer packaged state';
$this->_managedEntities = [];
$this->_managedEntities[] = $autoUpdateSearch;
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

// Because the entity was modified, it will not be updated
$search = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->addSelect('description', 'local_modified_date')
->execute()->single();
$this->assertEquals('Altered state', $search['description']);
$this->assertNotNull($search['local_modified_date']);

SavedSearch::revert(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->execute();

// Entity should be revered to newer packaged state
$result = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
->execute();
$search = $result->single();
$this->assertEquals('Newer packaged state', $search['description']);
// Check calculated fields
$this->assertTrue($search['has_base']);
$this->assertEquals('civicrm', $search['base_module']);
// local_modified_date should be reset by the revert action
$this->assertNull($search['local_modified_date']);

// Update local
$time = $this->getCurrentTimestamp();
SavedSearch::update(FALSE)
->addValue('id', $search['id'])
->addValue('description', 'Altered state 2')
->execute();

// Remove managed search
$this->_managedEntities = [];
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

// Because it's been locally modified, the search will not be auto-deleted
$search = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
->execute()->single();
$this->assertEquals('Altered state 2', $search['description']);
// Check calculated fields
$this->assertTrue($search['has_base']);
$this->assertEquals('civicrm', $search['base_module']);
// local_modified_date should reflect the manual update
$this->assertGreaterThanOrEqual($time, $search['local_modified_date']);
$this->assertLessThanOrEqual($this->getCurrentTimestamp(), $search['local_modified_date']);

// The managed record persists because it hasn't been cleaned up
$result = Managed::get(FALSE)
->addWhere('entity_type', '=', 'SavedSearch')
->addWhere('entity_id', '=', $search['id'])
->execute();
$this->assertCount(1, $result);

// If manually deleted, the managed record gets deleted to
SavedSearch::delete(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->execute();
$result = Managed::get(FALSE)
->addWhere('entity_type', '=', 'SavedSearch')
->addWhere('entity_id', '=', $search['id'])
->execute();
$this->assertCount(0, $result);

// Restore managed entity
$this->_managedEntities = [];
$this->_managedEntities[] = $autoUpdateSearch;
\CRM_Core_ManagedEntities::singleton(TRUE)->reconcile();

// Entity should be restored
$result = SavedSearch::get(FALSE)
->addWhere('name', '=', 'TestAutoUpdateSavedSearch')
->addSelect('description', 'has_base', 'base_module', 'local_modified_date')
->execute();
$search = $result->single();
$this->assertEquals('Newer packaged state', $search['description']);
// Check calculated fields
$this->assertTrue($search['has_base']);
$this->assertEquals('civicrm', $search['base_module']);
$this->assertNull($search['local_modified_date']);
}

/**
* @dataProvider sampleEntityTypes
* @param string $entityName
Expand Down

0 comments on commit 638735c

Please sign in to comment.