Skip to content

Commit

Permalink
Merge pull request #26237 from colemanw/exportMatchFields
Browse files Browse the repository at this point in the history
dev/core#4286 APIv4 - Improve export action handling of $match param
  • Loading branch information
eileenmcnaughton authored May 25, 2023
2 parents e2b7cfd + 088c2d4 commit e371a09
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 12 deletions.
52 changes: 46 additions & 6 deletions Civi/Api4/Generic/ExportAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@
*
* @method $this setId(int $id)
* @method int getId()
* @method $this setMatch(array $match) Specify fields to match for update.
* @method bool getMatch()
* @method $this setCleanup(string $cleanup)
* @method string getCleanup()
* @method $this setUpdate(string $update)
* @method string getUpdate()
*/
class ExportAction extends AbstractAction {
use Traits\MatchParamTrait;

/**
* Id of $ENTITY to export
Expand All @@ -38,6 +39,19 @@ class ExportAction extends AbstractAction {
*/
protected $id;

/**
* Specify fields to match when managed records are being reconciled.
*
* To prevent "DB Error: Already Exists" errors, it's generally a good idea to set this
* value to whatever unique fields this entity has (for most entities it's "name").
* The managed system will then check if a record with that name already exists before
* trying to create a new one.
*
* @var array
* @optionsCallback getMatchFields
*/
protected $match = ['name'];

/**
* Specify rule for auto-updating managed entity
* @var string
Expand All @@ -62,17 +76,18 @@ class ExportAction extends AbstractAction {
* @param \Civi\Api4\Generic\Result $result
*/
public function _run(Result $result) {
$this->exportRecord($this->getEntityName(), $this->id, $result);
$this->exportRecord($this->getEntityName(), $this->id, $result, $this->match);
}

/**
* @param string $entityType
* @param int $entityId
* @param \Civi\Api4\Generic\Result $result
* @param array $matchFields
* @param string $parentName
* @param array $excludeFields
*/
private function exportRecord(string $entityType, int $entityId, Result $result, $parentName = NULL, $excludeFields = []) {
private function exportRecord(string $entityType, int $entityId, Result $result, array $matchFields, $parentName = NULL, $excludeFields = []) {
if (isset($this->exportedEntities[$entityType][$entityId])) {
throw new \CRM_Core_Exception("Circular reference detected: attempted to export $entityType id $entityId multiple times.");
}
Expand Down Expand Up @@ -134,7 +149,7 @@ private function exportRecord(string $entityType, int $entityId, Result $result,
// Sometimes fields share an option group; only export it once.
empty($this->exportedEntities['OptionGroup'][$record['option_group_id']])
) {
$this->exportRecord('OptionGroup', $record['option_group_id'], $result);
$this->exportRecord('OptionGroup', $record['option_group_id'], $result, $matchFields);
}
}
// Don't use joins/pseudoconstants if null or if it has the same value as the original
Expand All @@ -156,7 +171,7 @@ private function exportRecord(string $entityType, int $entityId, Result $result,
'values' => $record,
],
];
foreach (array_intersect($this->match, array_keys($allFields)) as $match) {
foreach (array_unique(array_intersect($matchFields, array_keys($allFields))) as $match) {
$export['params']['match'][] = $match;
}
$result[] = $export;
Expand Down Expand Up @@ -204,8 +219,18 @@ private function exportRecord(string $entityType, int $entityId, Result $result,
return $a->$weightCol < $b->$weightCol ? -1 : 1;
});
}
$referenceMatchFields = $matchFields;
// Add back-reference to "match" fields to enforce uniqueness
// See https://lab.civicrm.org/dev/core/-/issues/4286
if ($referenceMatchFields) {
foreach ($reference::fields() as $field) {
if (($field['FKClassName'] ?? '') === $daoName) {
$referenceMatchFields[] = $field['name'];
}
}
}
foreach ($records as $record) {
$this->exportRecord($refEntity, $record->id, $result, $name . '_', $exclude);
$this->exportRecord($refEntity, $record->id, $result, $referenceMatchFields, $name . '_', $exclude);
}
}
}
Expand Down Expand Up @@ -266,4 +291,19 @@ private function getFieldsForExport($entityType, $loadOptions = FALSE, $excludeF
}
}

/**
* Options callback for $this->match
* @return array
*/
protected function getMatchFields() {
return (array) civicrm_api4($this->getEntityName(), 'getFields', [
'checkPermissions' => FALSE,
'action' => 'get',
'where' => [
['type', 'IN', ['Field']],
['readonly', '!=', TRUE],
],
], ['name']);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
crmApi4(apiCalls)
.then(function(result) {
_.each(ctrl.types, function (type) {
type.values = _.pluck(_.pluck(_.where(result[0], {entity: type.entity}), 'params'), 'values');
type.enabled = !!type.values.length;
var params = _.pluck(_.where(result[0], {entity: type.entity}), 'params');
type.values = _.pluck(params, 'values');
type.match = params[0] && params[0].match;
type.enabled = !!params.length;
});
// Afforms are not included in the export and are fetched separately
if (ctrl.afformEnabled) {
Expand All @@ -50,10 +52,8 @@
_.each(ctrl.types, function(type) {
if (type.enabled) {
var params = {records: type.values};
// Afform always matches on 'name', no need to add it to the API 'save' params
if (type.entity !== 'Afform') {
// Group and SavedSearch match by 'name', SearchDisplay also matches by 'saved_search_id'.
params.match = type.entity === 'SearchDisplay' ? ['name', 'saved_search_id'] : ['name'];
if (type.match && type.match.length) {
params.match = type.match;
}
data.push([type.entity, 'save', params]);
}
Expand Down

0 comments on commit e371a09

Please sign in to comment.