cascade removal

This commit is contained in:
Yurii
2026-03-05 09:46:30 +02:00
parent 9e2029dc0a
commit 8561b1af65
16 changed files with 602 additions and 27 deletions
@@ -33,6 +33,7 @@ use Espo\Core\Exceptions\Conflict;
use Espo\Core\Exceptions\Error;
use Espo\Core\InjectableFactory;
use Espo\Entities\User;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\Tools\EntityManager\EntityManager as EntityManagerTool;
use Espo\Core\Api\Request;
use Espo\Core\Exceptions\BadRequest;
@@ -443,11 +444,15 @@ class EntityManager
throw new BadRequest();
}
/** @var array{readOnly?: bool} $params */
/** @var array{readOnly?: bool, cascadeRemocal?: bool} $params */
$params = [];
if (property_exists($rawParams, 'readOnly')) {
$params['readOnly'] = (bool) $rawParams->readOnly;
if (property_exists($rawParams, RelationParam::READ_ONLY)) {
$params[RelationParam::READ_ONLY] = (bool) $rawParams->{RelationParam::READ_ONLY};
}
if (property_exists($rawParams, RelationParam::CASCADE_REMOVAL)) {
$params[RelationParam::CASCADE_REMOVAL] = (bool) $rawParams->{RelationParam::CASCADE_REMOVAL};
}
$this->linkManager->updateParams($entityType, $link, $params);
@@ -29,12 +29,18 @@
namespace Espo\Core\Record\Deleted;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\Conflict;
use Espo\Core\Field\DateTime;
use Espo\Core\Name\Field;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\Utils\Metadata;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Defs\RelationDefs;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\SelectBuilder;
use Espo\ORM\Repository\Util;
/**
* @implements Restorer<Entity>
@@ -44,12 +50,13 @@ class DefaultRestorer implements Restorer
public function __construct(
private EntityManager $entityManager,
private Metadata $metadata,
private RestorerFactory $restorerFactory,
) {}
public function restore(Entity $entity): void
{
if (!$entity->get(Attribute::DELETED)) {
throw new Forbidden("No 'deleted' attribute.");
throw new Conflict("Entity is not soft-deleted.");
}
$this->entityManager
@@ -57,8 +64,13 @@ class DefaultRestorer implements Restorer
->run(fn () => $this->restoreInTransaction($entity));
}
/**
* @throws Conflict
*/
private function restoreInTransaction(Entity $entity): void
{
$modifiedAt = $this->getModifiedAt($entity);
$repository = $this->entityManager->getRDBRepository($entity->getEntityType());
$repository->restoreDeleted($entity->getId());
@@ -72,5 +84,100 @@ class DefaultRestorer implements Restorer
$entity->set('deleteId', '0');
$repository->save($entity, [SaveOption::SILENT => true]);
}
if ($modifiedAt) {
$this->restoreRelatedRecords($entity, $modifiedAt);
}
}
/**
* @throws Conflict
*/
private function restoreRelatedRecords(Entity $entity, DateTime $modifiedAt): void
{
$relations = $this->entityManager
->getDefs()
->getEntity($entity->getEntityType())
->getRelationList();
foreach ($relations as $relation) {
if (!$relation->getParam(RelationParam::CASCADE_REMOVAL)) {
continue;
}
$this->restoreRelatedLink($entity, $relation, $modifiedAt);
}
}
/**
* @throws Conflict
*/
private function restoreRelatedLink(Entity $entity, RelationDefs $relation, DateTime $modifiedAt): void
{
$foreignEntityType = $relation->tryGetForeignEntityType();
$foreign = $relation->tryGetForeignRelationName();
if (!$foreignEntityType || !$foreign) {
return;
}
$foreignType = $this->entityManager
->getDefs()
->tryGetEntity($foreignEntityType)
?->tryGetRelation($foreign)
?->getType();
if (!$foreignType) {
return;
}
if (!Util::isRelationshipEligibleForCascadeRemoval($relation->getType(), $foreignType)) {
return;
}
$link = $relation->getName();
$builder = SelectBuilder::create()
->from($foreignEntityType)
->withDeleted();
$collection = $this->entityManager
->getRelation($entity, $link)
->clone($builder->build())
->sth()
->where([
Attribute::DELETED => true,
Field::MODIFIED_AT . '>=' => $modifiedAt->toString(),
])
->find();
foreach ($collection as $relatedEntity) {
$this->restoreRelated($relatedEntity);
}
}
/**
* @throws Conflict
*/
private function restoreRelated(Entity $relatedEntity): void
{
if (
!$relatedEntity->hasAttribute(Attribute::DELETED) ||
!$relatedEntity->get(Attribute::DELETED)
) {
return;
}
$restorer = $this->restorerFactory->create($relatedEntity->getEntityType());
$restorer->restore($relatedEntity);
}
private function getModifiedAt(Entity $entity): ?DateTime
{
$modifiedAtString = $entity->get(Field::MODIFIED_AT);
return $modifiedAtString ? DateTime::fromString($modifiedAtString) : null;
}
}
@@ -30,17 +30,15 @@
namespace Espo\Core\Record\Deleted;
use Espo\Core\Exceptions\Conflict;
use Espo\Core\Exceptions\Forbidden;
use Espo\ORM\Entity;
/**
* @template TEntity of Entity
* @template TEntity of Entity = Entity
*/
interface Restorer
{
/**
* @param TEntity $entity A deleted entity.
* @throws Forbidden
* @throws Conflict
*/
public function restore(Entity $entity): void;
@@ -0,0 +1,63 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Record\Deleted;
use Espo\Core\Binding\BindingContainer;
use Espo\Core\Binding\BindingContainerBuilder;
use Espo\Core\InjectableFactory;
use Espo\Core\Utils\Metadata;
use Espo\Entities\User;
use Espo\ORM\Entity;
class RestorerFactory
{
public function __construct(
private Metadata $metadata,
private InjectableFactory $injectableFactory,
private User $user,
) {}
public function create(string $entityType): Restorer
{
/** @var class-string<Restorer<Entity>> $restorerClassName */
$restorerClassName = $this->metadata->get("recordDefs.$entityType.deletedRestorerClassName") ??
DefaultRestorer::class;
/** @var Restorer */
return $this->injectableFactory->createWithBinding($restorerClassName, $this->createBinding());
}
private function createBinding(): BindingContainer
{
return BindingContainerBuilder::create()
->bindInstance(User::class, $this->user)
->build();
}
}
+10 -5
View File
@@ -42,6 +42,7 @@ use Espo\Core\FieldSanitize\SanitizeManager;
use Espo\Core\ORM\Defs\AttributeParam;
use Espo\Core\ORM\Entity as CoreEntity;
use Espo\Core\ORM\Repository\Option\RemoveOption;
use Espo\Core\Record\Deleted\RestorerFactory;
use Espo\ORM\Repository\Option\SaveContext;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\ORM\Type\FieldType;
@@ -976,12 +977,16 @@ class Service implements Crud,
throw new NotFound();
}
/** @var class-string<Restorer<Entity>> $restorerClassName */
$restorerClassName = $this->metadata->get("recordDefs.$this->entityType.deletedRestorerClassName") ??
DefaultRestorer::class;
if (!$entity->hasAttribute(Attribute::DELETED)) {
throw new Forbidden("Record type is not restorable.");
}
/** @var Restorer<Entity> $restorer */
$restorer = $this->injectableFactory->createWithBinding($restorerClassName, $this->createBinding());
if ($entity->get(Attribute::DELETED) !== true) {
throw new Conflict("Record type is not soft-deleted.");
}
$factory = $this->injectableFactory->createWithBinding(RestorerFactory::class, $this->createBinding());
$restorer = $factory->create($this->entityType);
$restorer->restore($entity);
}
@@ -59,6 +59,7 @@ class RelationConverter
RelationParam::ADDITIONAL_COLUMNS,
'noJoin',
RelationParam::INDEXES,
RelationParam::CASCADE_REMOVAL,
];
/** @var string[] */
@@ -119,4 +119,11 @@ class RelationParam
* @since 9.4.0
*/
public const DISABLED = 'disabled';
/**
* Cascade removal Only for one-to-many, one-to-one, and parent-to-children.
*
* @since 9.4.0
*/
public const string CASCADE_REMOVAL = 'cascadeRemoval';
}
@@ -29,6 +29,7 @@
namespace Espo\ORM\Repository;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Repository\Option\SaveContext;
use Espo\ORM\Defs\RelationDefs;
use Espo\ORM\EntityCollection;
@@ -285,6 +286,7 @@ class RDBRepository implements Repository
$this->processCheckEntity($entity);
$this->beforeRemove($entity, $options);
$this->getMapper()->delete($entity);
$this->cascadeRemoveRelated($entity, $options);
$this->afterRemove($entity, $options);
}
@@ -847,4 +849,63 @@ class RDBRepository implements Repository
$mapper->deleteFromDb($this->entityType, $id, $onlyDeleted);
}
/**
* @param array<string, mixed> $options
*/
private function cascadeRemoveRelated(Entity $entity, array $options): void
{
$relations = $this->entityManager
->getDefs()
->getEntity($this->entityType)
->getRelationList();
foreach ($relations as $relation) {
if (!$relation->getParam(RelationParam::CASCADE_REMOVAL)) {
continue;
}
$this->cascadeRemoveRelation($entity, $relation, $options);
}
}
/**
* @param array<string, mixed> $options
*/
private function cascadeRemoveRelation(Entity $entity, RelationDefs $relation, array $options): void
{
$foreignEntityType = $relation->tryGetForeignEntityType();
$foreign = $relation->tryGetForeignRelationName();
if (!$foreignEntityType || !$foreign) {
return;
}
$foreignType = $this->entityManager
->getDefs()
->tryGetEntity($foreignEntityType)
?->tryGetRelation($foreign)
?->getType();
if (!$foreignType) {
return;
}
if (!Util::isRelationshipEligibleForCascadeRemoval($relation->getType(), $foreignType)) {
return;
}
$link = $relation->getName();
$collection = $this->entityManager
->getRelation($entity, $link)
->sth()
->find();
unset($options[SaveContext::NAME]);
foreach ($collection as $relatedEntity) {
$this->entityManager->removeEntity($relatedEntity, $options);
}
}
}
+11
View File
@@ -31,6 +31,7 @@ namespace Espo\ORM\Repository;
use Espo\ORM\Entity;
use Espo\ORM\Type\RelationType;
use ReflectionClass;
use InvalidArgumentException;
@@ -54,4 +55,14 @@ class Util
return $class->getShortName();
}
/**
* @internal
*/
public static function isRelationshipEligibleForCascadeRemoval(string $type, string $foreignType): bool
{
return in_array($type, [RelationType::HAS_CHILDREN, RelationType::HAS_ONE]) ||
$type === RelationType::BELONGS_TO && $foreignType === RelationType::HAS_ONE ||
$type === RelationType::HAS_MANY && $foreignType === RelationType::BELONGS_TO;
}
}
@@ -246,7 +246,8 @@
"itemsEditable": "Items Editable",
"openInNewTab": "Open in new tab",
"notLockable": "Not Lockable",
"protocolRequired": "Protocol Required"
"protocolRequired": "Protocol Required",
"cascadeRemoval": "Cascade Removal"
},
"strings" : {
"rebuildRequired": "Rebuild is required"
@@ -123,7 +123,8 @@
"entityType": "Base Plus - has Activities, History and Tasks panels.\n\nEvent - available in Calendar and Activities panel.",
"countDisabled": "Total number won't be displayed on the list view. Can decrease loading time when the DB table is big.",
"fullTextSearch": "Running rebuild is required.",
"linkParamReadOnly": "A read-only link cannot be edited via the *link* and *unlink* API requests. It won't be possible to relate and unrelate records via the relationship panel. It still possible to edit read-only links via link and link-multiple fields."
"linkParamReadOnly": "A read-only link cannot be edited via the *link* and *unlink* API requests. It won't be possible to relate and unrelate records via the relationship panel. It still possible to edit read-only links via link and link-multiple fields.",
"linkParamCascadeRemoval": "If enabled, when a parent record is removed, associated related records will be removed as well."
},
"entityNameParts": {
"Category": "Category"
@@ -38,10 +38,13 @@ use Espo\Core\Utils\Language;
use Espo\Core\Utils\Metadata;
use Espo\Core\Utils\Route;
use Espo\Core\Utils\Util;
use Espo\Entities\Team;
use Espo\Entities\User;
use Espo\ORM\Defs\Params\EntityParam;
use Espo\ORM\Defs\Params\FieldParam;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Entity;
use Espo\ORM\Repository\Util as RepositoryUtil;
use Espo\ORM\Type\RelationType;
use Espo\Tools\LinkManager\Hook\HookProcessor as LinkHookProcessor;
use Espo\Tools\LinkManager\Params as LinkParams;
@@ -1152,20 +1155,47 @@ class LinkManager
}
/**
* @param array{readOnly?: bool} $params
* @param array{readOnly?: bool, cascadeRemoval?: bool} $params
* @throws Error
*/
public function updateParams(string $entityType, string $link, array $params): void
{
$type = $this->metadata->get("entityDefs.$entityType.links.$link.type");
$foreignEntityType = $this->metadata->get("entityDefs.$entityType.links.$link.entity");
$foreign = $this->metadata->get("entityDefs.$entityType.links.$link.foreign");
$foreignType = null;
$isObject = false;
$isSystem = false;
if ($foreignEntityType && $foreign) {
$foreignType = $this->metadata->get("entityDefs.$foreignEntityType.links.$foreign.type");
$isObject = $this->metadata->get("scopes.$foreignEntityType.object");
$isSystem = in_array($foreignEntityType, [
User::ENTITY_TYPE,
Team::ENTITY_TYPE,
]);
}
$defs = [];
if (
in_array($type, [RelationType::HAS_MANY, RelationType::HAS_CHILDREN]) &&
array_key_exists('readOnly', $params)
array_key_exists(RelationParam::READ_ONLY, $params)
) {
$defs['readOnly'] = $params['readOnly'];
$defs[RelationParam::READ_ONLY] = $params[RelationParam::READ_ONLY];
}
if (
$foreignEntityType &&
$foreignType &&
RepositoryUtil::isRelationshipEligibleForCascadeRemoval($type, $foreignType) &&
array_key_exists(RelationParam::CASCADE_REMOVAL, $params) &&
$isObject &&
!$isSystem
) {
// @todo Check non-system and object.
$defs[RelationParam::CASCADE_REMOVAL] = $params[RelationParam::CASCADE_REMOVAL];
}
$this->metadata->set('entityDefs', $entityType, [
@@ -1184,7 +1214,8 @@ class LinkManager
public function resetToDefault(string $entityType, string $link): void
{
$this->metadata->delete('entityDefs', $entityType, [
"links.$link.readOnly",
"links.$link." . RelationParam::READ_ONLY,
"links.$link." . RelationParam::CASCADE_REMOVAL,
]);
$this->metadata->save();
+2 -1
View File
@@ -180,7 +180,8 @@ class LinkManagerIndexView extends View {
const isRemovable = defs.isCustom;
const hasEditParams = defs.type === 'hasMany' || defs.type === 'hasChildren';
const hasEditParams = ['hasChildren', 'hasMany'].includes(defs.type) ||
['oneToOneLeft', 'oneToOneRight', 'oneToMany'].includes(type);
this.linkDataList.push({
link: link,
@@ -43,6 +43,18 @@ export default class LinkManagerEditParamsModalView extends ModalView {
*/
type
/**
* @private
* @type {string|null}
*/
foreignType = null
/**
* @private
* @type {string|null}
*/
foreignEntityType
/**
* @param {{
* entityType: string,
@@ -60,10 +72,21 @@ export default class LinkManagerEditParamsModalView extends ModalView {
this.translate(this.props.entityType, 'scopeNames') + ' · ' +
this.translate(this.props.link, 'links', this.props.entityType);
/** @type {{type: string, isCustom: boolean}} */
this.systemEntityTypeList = ['User', 'Team'];
/** @type {{type: string, isCustom?: boolean, entity?: string, foreign?: string|null}} */
const defs = this.getMetadata().get(`entityDefs.${this.props.entityType}.links.${this.props.link}`) || {};
this.type = defs.type;
const foreignEntityType = defs.entity ?? null;
const foreign = defs.foreign;
this.foreignEntityType = foreignEntityType;
if (foreignEntityType && foreign) {
this.foreignType = this.getMetadata().get(`entityDefs.${foreignEntityType}.links.${foreign}.type`);
}
this.buttonList = [
{
name: 'save',
@@ -104,18 +127,35 @@ export default class LinkManagerEditParamsModalView extends ModalView {
}),
},
false
],
[
{
view: new BoolFieldView({
name: 'cascadeRemoval',
labelText: this.translate('cascadeRemoval', 'fields', 'Admin'),
params: {
tooltip: 'EntityManager.linkParamCascadeRemoval',
},
}),
},
false
]
]
}
]
});
this.assignView('record', this.recordView).then(() => {
if (!this.hasReadOnly()) {
this.recordView.hideField('readOnly');
this.recordView.setFieldReadOnly('readOnly');
}
this.assignView('record', this.recordView, '.record');
if (!this.hasCascadeRemoval()) {
this.recordView.hideField('cascadeRemoval');
this.recordView.setFieldReadOnly('cascadeRemoval');
}
});
}
/**
@@ -126,6 +166,23 @@ export default class LinkManagerEditParamsModalView extends ModalView {
return ['hasMany', 'hasChildren'].includes(this.type);
}
/**
* @private
* @return {boolean}
*/
hasCascadeRemoval() {
if (!this.foreignEntityType) {
return false;
}
const isObject = this.getMetadata().get(`scopes.${this.foreignEntityType}.object`) &&
!this.systemEntityTypeList.includes(this.foreignEntityType);
return ['hasChildren', 'hasOne'].includes(this.type) ||
(this.type === 'belongsTo' && this.foreignType === 'hasOne') ||
(this.type === 'hasMany' && this.foreignType === 'belongsTo');
}
/**
* @private
* @return {Record}
@@ -135,7 +192,8 @@ export default class LinkManagerEditParamsModalView extends ModalView {
const defs = this.getMetadata().get(`entityDefs.${this.props.entityType}.links.${this.props.link}`) || {};
return {
readOnly: defs.readOnly || false,
readOnly: defs.readOnly ?? false,
cascadeRemoval: defs.cascadeRemoval ?? false,
};
}
@@ -172,6 +230,10 @@ export default class LinkManagerEditParamsModalView extends ModalView {
params.readOnly = this.formModel.attributes.readOnly;
}
if (this.hasCascadeRemoval()) {
params.cascadeRemoval = this.formModel.attributes.cascadeRemoval;
}
try {
await Espo.Ajax.postRequest('EntityManager/action/updateLinkParams', {
entityType: this.props.entityType,
+22
View File
@@ -1633,6 +1633,28 @@
}
}
}
},
{
"if": {
"properties": {
"type": {
"anyOf": [
{"const": "hasMany"},
{"const": "hasChildren"},
{"const": "hasOne"},
{"const": "belongsTo"}
]
}
}
},
"then": {
"properties": {
"cascadeRemoval": {
"type": "boolean",
"description": "Cascade removal. Related records will be removed along with the parent record removal. Not allowed for many-to-many and many-to-one. As of v9.4."
}
}
}
}
]
},
@@ -0,0 +1,199 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace tests\integration\Espo\Record;
use Espo\Core\Record\ServiceContainer;
use Espo\Modules\Crm\Entities\Account;
use Espo\Modules\Crm\Entities\Contact;
use Espo\Modules\Crm\Entities\Opportunity;
use Espo\Modules\Crm\Entities\Task;
use Espo\ORM\Defs\Params\RelationParam;
use Espo\ORM\Name\Attribute;
use Espo\ORM\Query\SelectBuilder;
use tests\integration\Core\BaseTestCase;
class CascadeRemovalTest extends BaseTestCase
{
/**
* @noinspection PhpUnhandledExceptionInspection
*/
public function testRemovalAndRestoral(): void
{
$metadata = $this->getMetadata();
$metadata->set('entityDefs', 'Account', [
'links' => [
'opportunities' => [
RelationParam::CASCADE_REMOVAL => true,
]
]
]);
$metadata->set('entityDefs', 'Opportunity', [
'links' => [
'tasks' => [
RelationParam::CASCADE_REMOVAL => true,
]
]
]);
$metadata->save();
$this->getDataManager()->clearCache();
$this->reCreateApplication();
$em = $this->getEntityManager();
$account = $em->createEntity('Account', ['name' => 'a1']);
$contact = $em->createEntity('Contact', ['lastName' => 'c1', 'accountId' => $account->getId()]);
$opp1 = $em->createEntity('Opportunity', ['name' => 'o1', 'accountId' => $account->getId()]);
$opp2 = $em->createEntity('Opportunity', ['name' => 'o2', 'accountId' => $account->getId()]);
$opp3 = $em->createEntity('Opportunity', ['name' => 'o3']);
$task1 = $em->createEntity('Task', [
'name' => 't1',
'parentType' => 'Opportunity',
'parentId' => $opp1->getId(),
]);
$em->removeEntity($account);
$this->assertNotNull(
$em->getRDBRepository(Account::ENTITY_TYPE)
->clone(
SelectBuilder::create()
->from(Account::ENTITY_TYPE)
->withDeleted()
->build()
)
->where([
Attribute::ID => $account->getId(),
Attribute::DELETED => true,
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Contact::ENTITY_TYPE)
->where([
Attribute::ID => $contact->getId(),
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Opportunity::ENTITY_TYPE)
->clone(
SelectBuilder::create()
->from(Opportunity::ENTITY_TYPE)
->withDeleted()
->build()
)
->where([
Attribute::ID => $opp1->getId(),
Attribute::DELETED => true,
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Opportunity::ENTITY_TYPE)
->clone(
SelectBuilder::create()
->from(Opportunity::ENTITY_TYPE)
->withDeleted()
->build()
)
->where([
Attribute::ID => $opp2->getId(),
Attribute::DELETED => true,
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Task::ENTITY_TYPE)
->clone(
SelectBuilder::create()
->from(Task::ENTITY_TYPE)
->withDeleted()
->build()
)
->where([
Attribute::ID => $task1->getId(),
Attribute::DELETED => true,
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Opportunity::ENTITY_TYPE)
->where([
Attribute::ID => $opp3->getId(),
])
->findOne()
);
//
$service = $this->getContainer()->getByClass(ServiceContainer::class)->get(Account::ENTITY_TYPE);
$service->restoreDeleted($account->getId());
$this->assertNotNull(
$em->getRDBRepository(Account::ENTITY_TYPE)
->where([
Attribute::ID => $account->getId(),
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Opportunity::ENTITY_TYPE)
->where([
Attribute::ID => $opp1->getId(),
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Opportunity::ENTITY_TYPE)
->where([
Attribute::ID => $opp2->getId(),
])
->findOne()
);
$this->assertNotNull(
$em->getRDBRepository(Task::ENTITY_TYPE)
->where([
Attribute::ID => $task1->getId(),
])
->findOne()
);
}
}