mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Add restoration functionality for stored object versions
Introduce a service to restore stored object versions along with relevant tests and an API endpoint. This includes database migrations for version relationships, enhancing stored object version tracking.
This commit is contained in:
parent
b0e2e65885
commit
5906171041
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class StoredObjectRestoreVersionApiController
|
||||
{
|
||||
public function __construct(private Security $security, private StoredObjectRestoreInterface $storedObjectRestore, private EntityManagerInterface $entityManager, private SerializerInterface $serializer) {}
|
||||
|
||||
#[Route('/api/1.0/doc-store/stored-object/restore-from-version/{id}', methods: ['POST'])]
|
||||
public function restoreStoredObjectVersion(StoredObjectVersion $storedObjectVersion): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObjectVersion->getStoredObject())) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit the stored object');
|
||||
}
|
||||
|
||||
$newVersion = $this->storedObjectRestore->restore($storedObjectVersion);
|
||||
|
||||
$this->entityManager->persist($newVersion);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($newVersion, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT]]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@ -48,6 +48,25 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
#[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection&Selectable $pointInTimes;
|
||||
|
||||
/**
|
||||
* Previous storedObjectVersion, from which the current stored object version is created.
|
||||
*
|
||||
* If null, the current stored object version is generated by other means.
|
||||
*
|
||||
* Those version may be associated with the same storedObject, or not. In this last case, that means that
|
||||
* the stored object's current version is created from another stored object version.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class)]
|
||||
private ?StoredObjectVersion $createdFrom = null;
|
||||
|
||||
/**
|
||||
* List of stored object versions created from the current version.
|
||||
*
|
||||
* @var Collection<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'createdFrom', targetEntity: StoredObjectVersion::class)]
|
||||
private Collection $children;
|
||||
|
||||
public function __construct(
|
||||
/**
|
||||
* The stored object associated with this version.
|
||||
@ -87,6 +106,7 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
) {
|
||||
$this->filename = $filename ?? self::generateFilename($this);
|
||||
$this->pointInTimes = new ArrayCollection();
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
|
||||
@ -149,8 +169,6 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @internal use @see{StoredObjectPointInTime} constructor instead
|
||||
*/
|
||||
public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
|
||||
@ -170,4 +188,42 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedFrom(): ?StoredObjectVersion
|
||||
{
|
||||
return $this->createdFrom;
|
||||
}
|
||||
|
||||
public function setCreatedFrom(?StoredObjectVersion $createdFrom): StoredObjectVersion
|
||||
{
|
||||
if (null === $createdFrom && null !== $this->createdFrom) {
|
||||
$this->createdFrom->removeChild($this);
|
||||
}
|
||||
|
||||
$createdFrom?->addChild($this);
|
||||
|
||||
$this->createdFrom = $createdFrom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addChild(StoredObjectVersion $child): self
|
||||
{
|
||||
if (!$this->children->contains($child)) {
|
||||
$this->children->add($child);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeChild(StoredObjectVersion $child): self
|
||||
{
|
||||
$result = $this->children->removeElement($child);
|
||||
|
||||
if (false === $result) {
|
||||
throw new \UnexpectedValueException('the child is not associated with the current stored object version');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final readonly class StoredObjectRestore implements StoredObjectRestoreInterface
|
||||
{
|
||||
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}
|
||||
|
||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion
|
||||
{
|
||||
$oldContent = $this->storedObjectManager->read($storedObjectVersion);
|
||||
|
||||
$newVersion = $this->storedObjectManager->write($storedObjectVersion->getStoredObject(), $oldContent, $storedObjectVersion->getType());
|
||||
|
||||
$newVersion->setCreatedFrom($storedObjectVersion);
|
||||
|
||||
$this->logger->info('[StoredObjectRestore] Restore stored object version', [
|
||||
'stored_object_uuid' => $storedObjectVersion->getStoredObject()->getUuid(),
|
||||
'old_version_id' => $storedObjectVersion->getId(),
|
||||
'old_version_version' => $storedObjectVersion->getVersion(),
|
||||
'new_version_id' => $newVersion->getVersion(),
|
||||
]);
|
||||
|
||||
return $newVersion;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
|
||||
/**
|
||||
* Restore an old version of the stored object as the current one.
|
||||
*/
|
||||
interface StoredObjectRestoreInterface
|
||||
{
|
||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion;
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectRestoreVersionApiController;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectRestoreVersionApiControllerTest extends TestCase
|
||||
{
|
||||
public function testRestoreStoredObjectVersion(): void
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
||||
|
||||
$security->expects($this->once())
|
||||
->method('isGranted')
|
||||
->willReturn(true);
|
||||
$storedObjectRestore->expects($this->once())
|
||||
->method('restore')
|
||||
->willReturn($storedObjectVersion);
|
||||
$entityManager->expects($this->once())
|
||||
->method('persist');
|
||||
$entityManager->expects($this->once())
|
||||
->method('flush');
|
||||
|
||||
$serializer->expects($this->once())
|
||||
->method('serialize')
|
||||
->willReturn('test');
|
||||
|
||||
$response = $controller->restoreStoredObjectVersion($storedObjectVersion);
|
||||
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
self::assertEquals('test', $response->getContent());
|
||||
}
|
||||
|
||||
public function testRestoreStoredObjectVersionAccessDenied(): void
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
||||
|
||||
self::expectException(\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException::class);
|
||||
$security->expects($this->once())
|
||||
->method('isGranted')
|
||||
->willReturn(false);
|
||||
$controller->restoreStoredObjectVersion($storedObjectVersion);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestore;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectRestoreTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testRestore(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->registerVersion(type: 'application/test');
|
||||
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->read($version)->willReturn('1234')->shouldBeCalledOnce();
|
||||
$storedObjectManager->write($storedObject, '1234', 'application/test')->shouldBeCalledOnce()
|
||||
->will(function ($args) {
|
||||
/** @var StoredObject $object */
|
||||
$object = $args[0];
|
||||
|
||||
return $object->registerVersion();
|
||||
})
|
||||
;
|
||||
|
||||
$restore = new StoredObjectRestore($storedObjectManager->reveal(), new NullLogger());
|
||||
|
||||
$newVersion = $restore->restore($version);
|
||||
|
||||
self::assertNotSame($version, $newVersion);
|
||||
self::assertSame($version, $newVersion->getCreatedFrom());
|
||||
}
|
||||
}
|
@ -130,3 +130,25 @@ paths:
|
||||
description: "Unauthorized"
|
||||
404:
|
||||
description: "Not found"
|
||||
|
||||
/1.0/doc-store/stored-object/restore-from-version/{id}:
|
||||
post:
|
||||
tags:
|
||||
- storedobject
|
||||
summary: Restore an old version of a stored object
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
allowEmptyValue: false
|
||||
description: The id of the stored object version
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\Migrations\DocStore;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20240918073234 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a relation between stored object version when a version is restored';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD createdFrom_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD CONSTRAINT FK_C1D553024DEC38BB FOREIGN KEY (createdFrom_id) REFERENCES chill_doc.stored_object_version (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_C1D553024DEC38BB ON chill_doc.stored_object_version (createdFrom_id)');
|
||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d55302232d562b RENAME TO IDX_C1D553024B136083');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version DROP createdFrom_id');
|
||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d553024b136083 RENAME TO idx_c1d55302232d562b');
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user