Skip to content

Commit

Permalink
IBX-8418: Remove draft when trashing or deleting its parent or ancest…
Browse files Browse the repository at this point in the history
…or location
  • Loading branch information
barw4 committed Oct 17, 2024
1 parent c338dc5 commit be1c176
Show file tree
Hide file tree
Showing 15 changed files with 274 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/contracts/Persistence/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ public function create(CreateStruct $location);
*/
public function removeSubtree($locationId);

/**
* Removes all draft contents that have no location assigned to them under the given parent location.
*/
public function deleteChildrenDrafts(int $locationId): void;

/**
* Set section on all content objects in the subtree.
* Only main locations will be updated.
Expand Down
6 changes: 6 additions & 0 deletions src/contracts/Test/IbexaKernelTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Ibexa\Contracts\Core\Repository\RoleService;
use Ibexa\Contracts\Core\Repository\SearchService;
use Ibexa\Contracts\Core\Repository\SectionService;
use Ibexa\Contracts\Core\Repository\TrashService;
use Ibexa\Contracts\Core\Repository\URLAliasService;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Core\Test\Persistence\Fixture\FixtureImporter;
Expand Down Expand Up @@ -169,6 +170,11 @@ protected static function getUrlAliasService(): URLAliasService
return self::getServiceByClassName(URLAliasService::class);
}

protected static function getTrashService(): TrashService
{
return self::getServiceByClassName(TrashService::class);
}

protected static function setAnonymousUser(): void
{
$anonymousUserId = 10;
Expand Down
1 change: 1 addition & 0 deletions src/contracts/Test/IbexaTestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class IbexaTestKernel extends Kernel implements IbexaTestKernelInterface
Repository\TokenService::class,
Repository\URLAliasService::class,
Repository\BookmarkService::class,
Repository\TrashService::class,
Handler::class,
];

Expand Down
14 changes: 14 additions & 0 deletions src/lib/Persistence/Cache/LocationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,20 @@ public function removeSubtree($locationId)
return $return;
}

public function deleteChildrenDrafts(int $locationId): void
{
$this->logger->logCall(__METHOD__, ['location' => $locationId]);

$this->persistenceHandler->locationHandler()->deleteChildrenDrafts($locationId);

$this->cache->invalidateTags([
$this->cacheIdentifierGenerator->generateTag(
self::LOCATION_PATH_IDENTIFIER,
[$locationId],
),
]);
}

/**
* {@inheritdoc}
*/
Expand Down
1 change: 1 addition & 0 deletions src/lib/Persistence/Legacy/Content/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,7 @@ public function deleteContent($contentId)
$this->removeRawContent($contentId);
} else {
foreach ($contentLocations as $locationId) {
$this->treeHandler->deleteChildrenDrafts($locationId);
$this->treeHandler->removeSubtree($locationId);
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/Persistence/Legacy/Content/Location/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ abstract public function loadParentLocationsDataForDraftContent(int $contentId):
*/
abstract public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array;

/**
* Finds draft contents created under the given parent location.
*
* @return array<int>
*/
abstract public function getSubtreeChildrenDraftContentIds(int $sourceId): array;

abstract public function getSubtreeSize(string $path): int;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Doctrine\DBAL\FetchMode;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Result;
use Ibexa\Contracts\Core\Persistence\Content\ContentInfo;
use Ibexa\Contracts\Core\Persistence\Content\Location;
use Ibexa\Contracts\Core\Persistence\Content\Location\CreateStruct;
Expand Down Expand Up @@ -237,6 +238,29 @@ public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array
: $results;
}

/**
* @return array<int>
*
* @throws \Doctrine\DBAL\Exception
* @throws \Doctrine\DBAL\Driver\Exception
*/
public function getSubtreeChildrenDraftContentIds(int $sourceId): array
{
$query = $this->connection->createQueryBuilder();
$query
->select('contentobject_id')
->from('eznode_assignment', 'n')
->innerJoin('n', 'ezcontentobject', 'c', 'n.contentobject_id = c.id')
->andWhere('n.parent_node = :parentNode')
->andWhere('c.status = :status')
->setParameter(':parentNode', $sourceId, ParameterType::INTEGER)
->setParameter(':status', ContentInfo::STATUS_DRAFT, ParameterType::INTEGER);

$statement = $query->execute();

return $statement->fetchFirstColumn();
}

public function getSubtreeSize(string $path): int
{
$query = $this->createNodeQueryBuilder([$this->dbPlatform->getCountExpression('node_id')]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array
}
}

/**
* @return array<int>
*/
public function getSubtreeChildrenDraftContentIds(int $sourceId): array
{
try {
return $this->innerGateway->getSubtreeChildrenDraftContentIds($sourceId);
} catch (PDOException $e) {
throw DatabaseException::wrap($e);
}
}

public function getSubtreeSize(string $path): int
{
try {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/Persistence/Legacy/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,11 @@ public function removeSubtree($locationId)
$this->treeHandler->removeSubtree($locationId);
}

public function deleteChildrenDrafts(int $locationId): void
{
$this->treeHandler->deleteChildrenDrafts($locationId);
}

/**
* Set section on all content objects in the subtree.
*
Expand Down
20 changes: 20 additions & 0 deletions src/lib/Persistence/Legacy/Content/TreeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,26 @@ public function removeSubtree($locationId)
$this->locationGateway->deleteNodeAssignment($contentId);
}

/**
* Removes draft contents assigned to the given parent location and its descendant locations.
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
public function deleteChildrenDrafts(int $locationId): void
{
$subLocations = $this->locationGateway->getChildren($locationId);
foreach ($subLocations as $subLocation) {
$this->deleteChildrenDrafts($subLocation['node_id']);
}

// Fetch child draft content ids
$subtreeChildrenDraftIds = $this->locationGateway->getSubtreeChildrenDraftContentIds($locationId);

foreach ($subtreeChildrenDraftIds as $contentId) {
$this->removeRawContent($contentId);
}
}

/**
* Set section on all content objects in the subtree.
*
Expand Down
1 change: 1 addition & 0 deletions src/lib/Repository/TrashService.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public function trash(Location $location): ?APITrashItem

$this->repository->beginTransaction();
try {
$this->persistenceHandler->locationHandler()->deleteChildrenDrafts($location->id);
$spiTrashItem = $this->persistenceHandler->trashHandler()->trashSubtree($location->id);
$this->persistenceHandler->urlAliasHandler()->locationDeleted($location->id);
$this->repository->commit();
Expand Down
110 changes: 110 additions & 0 deletions tests/integration/Core/Repository/ContentService/DeleteContentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Integration\Core\Repository\ContentService;

use Ibexa\Tests\Integration\Core\RepositoryTestCase;
use PHPUnit\Framework\Assert;

/**
* @covers \Ibexa\Contracts\Core\Repository\ContentService
*/
final class DeleteContentTest extends RepositoryTestCase
{
/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception
*/
public function testDeleteContentDeletesChildrenDrafts(): void
{
$contentService = self::getContentService();

[$folder, $draft1, $draft2, $draft3, $draftSecondDepth] = $this->prepareContentStructure();

$contentService->deleteContent($folder->getContentInfo());

Check failure on line 28 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getContentInfo() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 28 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getContentInfo() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 28 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getContentInfo() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

$contentInfos = $contentService->loadContentInfoList([
$draft1->getId(),

Check failure on line 31 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 31 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 31 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
$draft2->getId(),

Check failure on line 32 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 32 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 32 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
$draft3->getId(),

Check failure on line 33 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 33 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 33 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
$draftSecondDepth->getId(),

Check failure on line 34 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 34 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 34 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
]);

self::assertEmpty($contentInfos);
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception
*/
public function testTrashLocationDeletesChildrenDrafts(): void
{
$trashService = self::getTrashService();
$contentService = self::getContentService();

[$folder, $draft1, $draft2, $draft3, $draftSecondDepth] = $this->prepareContentStructure();

$folderMainLocationId = $folder->getVersionInfo()->getContentInfo()->getMainLocationId();

Check failure on line 50 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getVersionInfo() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 50 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getVersionInfo() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 50 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getVersionInfo() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
Assert::assertIsNumeric($folderMainLocationId);

$locationToTrash = self::getLocationService()->loadLocation($folderMainLocationId);

Check failure on line 53 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Parameter #1 $locationId of method Ibexa\Contracts\Core\Repository\LocationService::loadLocation() expects int, float|int|string given.

Check failure on line 53 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Parameter #1 $locationId of method Ibexa\Contracts\Core\Repository\LocationService::loadLocation() expects int, float|int|string given.

Check failure on line 53 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Parameter #1 $locationId of method Ibexa\Contracts\Core\Repository\LocationService::loadLocation() expects int, float|int|string given.

$trashService->trash($locationToTrash);

$contentInfos = $contentService->loadContentInfoList([
$draft1->getId(),

Check failure on line 58 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 58 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 58 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
$draft2->getId(),

Check failure on line 59 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 59 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 59 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
$draft3->getId(),

Check failure on line 60 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.0)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 60 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (7.4)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.

Check failure on line 60 in tests/integration/Core/Repository/ContentService/DeleteContentTest.php

View workflow job for this annotation

GitHub Actions / Unit tests & SQLite integration tests (8.1)

Call to method getId() on an unknown class Ibexa\Tests\Integration\Core\Repository\ContentService\Content.
$draftSecondDepth->getId(),
]);

self::assertEmpty($contentInfos);
}

/**
* @return array<Content>
*/
private function prepareContentStructure(): array
{
$folder = $this->createFolder(['eng-GB' => 'Folder'], 2);
$folderMainLocationId = $folder->getVersionInfo()->getContentInfo()->getMainLocationId();
Assert::assertIsNumeric($folderMainLocationId);

$childFolder = $this->createFolder(
['eng-GB' => 'Child folder'],
$folderMainLocationId,
);
$childFolderMainLocationId = $childFolder->getVersionInfo()->getContentInfo()->getMainLocationId();
Assert::assertIsNumeric($childFolderMainLocationId);

$secondDepthChildFolder = $this->createFolder(
['eng-GB' => 'Second depth folder'],
$childFolderMainLocationId,
);
$secondDepthChildFolderLocationId = $secondDepthChildFolder
->getVersionInfo()
->getContentInfo()
->getMainLocationId()
;
Assert::assertIsNumeric($secondDepthChildFolderLocationId);

$draft1 = $this->createFolderDraft(['eng-GB' => 'Folder draft 1'], $folderMainLocationId);
$draft2 = $this->createFolderDraft(['eng-GB' => 'Folder draft 2'], $childFolderMainLocationId);
$draft3 = $this->createFolderDraft(['eng-GB' => 'Folder draft 3'], $childFolderMainLocationId);
$draftSecondDepth = $this->createFolderDraft(
['eng-GB' => 'Folder draft 4'],
$secondDepthChildFolderLocationId,
);

return [
$folder,
$draft1,
$draft2,
$draft3,
$draftSecondDepth,
];
}
}
1 change: 1 addition & 0 deletions tests/lib/Persistence/Cache/LocationHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function providerForUnCachedMethods(): array
['c-4', 'ragl-4'],
],
['removeSubtree', [12], [['location_path', [12], false]], null, ['lp-12']],
['deleteChildrenDrafts', [12], [['location_path', [12], false]], null, ['lp-12']],
['setSectionForSubtree', [12, 2], [['location_path', [12], false]], null, ['lp-12']],
['changeMainLocation', [4, 12], [['content', [4], false]], null, ['c-4']],
['countLocationsByContent', [4]],
Expand Down
12 changes: 12 additions & 0 deletions tests/lib/Persistence/Legacy/Content/LocationHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,18 @@ public function testRemoveSubtree()
$handler->removeSubtree(42);
}

public function testDeleteChildrenDrafts(): void
{
$handler = $this->getLocationHandler();

$this->treeHandler
->expects(self::once())
->method('deleteChildrenDrafts')
->with(42);

$handler->deleteChildrenDrafts(42);
}

/**
* Test for the copySubtree() method.
*/
Expand Down
55 changes: 55 additions & 0 deletions tests/lib/Persistence/Legacy/Content/TreeHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,61 @@ public function testLoadLocation()
$this->assertTrue($location instanceof Location);
}

public function testDeleteChildrenDraftsRecursive(): void
{
$locationGatewayMock = $this->getLocationGatewayMock();
$contentGatewayMock = $this->getContentGatewayMock();
$contentMapperMock = $this->getContentMapperMock();

$locationGatewayMock
->expects(self::exactly(3))
->method('getChildren')
->willReturnMap([
[42, [
['node_id' => 201],
['node_id' => 202],
]],
[201, []],
[202, []],
]);

$locationGatewayMock
->expects(self::exactly(3))
->method('getSubtreeChildrenDraftContentIds')
->willReturnMap([
[201, [101]],
[202, [102]],
[42, [99]],
]);

$contentGatewayMock
->expects(self::exactly(3))
->method('loadContentInfo')
->willReturnMap([
[101, ['main_node_id' => 201]],
[102, ['main_node_id' => 202]],
[99, ['main_node_id' => 42]],
]);

$contentMapperMock
->expects(self::exactly(3))
->method('extractContentInfoFromRow')
->willReturnCallback(static function (array $row): ContentInfo {
return new ContentInfo(['mainLocationId' => $row['main_node_id']]);
});

$contentGatewayMock
->expects(self::exactly(3))
->method('deleteContent')
->willReturnCallback(static function (int $contentId): void {
self::assertContains($contentId, [99, 101, 102]);
});

$treeHandler = $this->getTreeHandler();

$treeHandler->deleteChildrenDrafts(42);
}

/** @var \PHPUnit\Framework\MockObject\MockObject|\Ibexa\Core\Persistence\Legacy\Content\Location\Gateway */
protected $locationGatewayMock;

Expand Down

0 comments on commit be1c176

Please sign in to comment.