Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dev/core#4286 APIv4 - Improve export action handling of $match param #26237

Merged
merged 2 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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