feat: add installer mapping persistence
This commit is contained in:
parent
c90bac1740
commit
bca325cc07
6 changed files with 403 additions and 0 deletions
30
migrations/Version20260505073300.php
Normal file
30
migrations/Version20260505073300.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20260505073300 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create admin users and script mappings.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE admin_user (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, username VARCHAR(180) NOT NULL, password_hash VARCHAR(255) NOT NULL, roles CLOB NOT NULL)');
|
||||
$this->addSql('CREATE UNIQUE INDEX uniq_admin_user_username ON admin_user (username)');
|
||||
$this->addSql('CREATE TABLE script_mapping (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, public_path VARCHAR(255) NOT NULL, repository_url VARCHAR(2048) NOT NULL, git_ref VARCHAR(255) NOT NULL, repository_file_path VARCHAR(1024) NOT NULL, access_token CLOB DEFAULT NULL, active BOOLEAN NOT NULL, last_sync_status VARCHAR(32) NOT NULL, last_successful_sync_at DATETIME DEFAULT NULL, last_sync_error CLOB DEFAULT NULL, cache_key VARCHAR(1024) DEFAULT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL)');
|
||||
$this->addSql('CREATE UNIQUE INDEX uniq_script_mapping_public_path ON script_mapping (public_path)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE script_mapping');
|
||||
$this->addSql('DROP TABLE admin_user');
|
||||
}
|
||||
}
|
||||
212
src/Entity/ScriptMapping.php
Normal file
212
src/Entity/ScriptMapping.php
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ScriptMappingRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ScriptMappingRepository::class)]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_script_mapping_public_path', columns: ['public_path'])]
|
||||
class ScriptMapping
|
||||
{
|
||||
public const SYNC_STATUS_NEVER_SYNCED = 'never_synced';
|
||||
public const SYNC_STATUS_SYNCED = 'synced';
|
||||
public const SYNC_STATUS_FAILED = 'failed';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private string $publicPath = '';
|
||||
|
||||
#[ORM\Column(length: 2048)]
|
||||
private string $repositoryUrl = '';
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private string $gitRef = 'main';
|
||||
|
||||
#[ORM\Column(length: 1024)]
|
||||
private string $repositoryFilePath = '';
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $accessToken = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $active = true;
|
||||
|
||||
#[ORM\Column(length: 32)]
|
||||
private string $lastSyncStatus = self::SYNC_STATUS_NEVER_SYNCED;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?DateTimeImmutable $lastSuccessfulSyncAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $lastSyncError = null;
|
||||
|
||||
#[ORM\Column(length: 1024, nullable: true)]
|
||||
private ?string $cacheKey = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
private ?DateTimeImmutable $updatedAt = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getPublicPath(): string
|
||||
{
|
||||
return $this->publicPath;
|
||||
}
|
||||
|
||||
public function setPublicPath(string $publicPath): self
|
||||
{
|
||||
$this->publicPath = trim($publicPath);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRepositoryUrl(): string
|
||||
{
|
||||
return $this->repositoryUrl;
|
||||
}
|
||||
|
||||
public function setRepositoryUrl(string $repositoryUrl): self
|
||||
{
|
||||
$this->repositoryUrl = trim($repositoryUrl);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGitRef(): string
|
||||
{
|
||||
return $this->gitRef;
|
||||
}
|
||||
|
||||
public function setGitRef(string $gitRef): self
|
||||
{
|
||||
$this->gitRef = trim($gitRef) ?: 'main';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRepositoryFilePath(): string
|
||||
{
|
||||
return $this->repositoryFilePath;
|
||||
}
|
||||
|
||||
public function setRepositoryFilePath(string $repositoryFilePath): self
|
||||
{
|
||||
$this->repositoryFilePath = trim($repositoryFilePath);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAccessToken(): ?string
|
||||
{
|
||||
return $this->accessToken;
|
||||
}
|
||||
|
||||
public function setAccessToken(?string $accessToken): self
|
||||
{
|
||||
$accessToken = $accessToken === null ? null : trim($accessToken);
|
||||
$this->accessToken = $accessToken === '' ? null : $accessToken;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasAccessToken(): bool
|
||||
{
|
||||
return $this->accessToken !== null && $this->accessToken !== '';
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
public function setActive(bool $active): self
|
||||
{
|
||||
$this->active = $active;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastSyncStatus(): string
|
||||
{
|
||||
return $this->lastSyncStatus;
|
||||
}
|
||||
|
||||
public function getLastSuccessfulSyncAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->lastSuccessfulSyncAt;
|
||||
}
|
||||
|
||||
public function getLastSyncError(): ?string
|
||||
{
|
||||
return $this->lastSyncError;
|
||||
}
|
||||
|
||||
public function getCacheKey(): ?string
|
||||
{
|
||||
return $this->cacheKey;
|
||||
}
|
||||
|
||||
public function setCacheKey(?string $cacheKey): self
|
||||
{
|
||||
$cacheKey = $cacheKey === null ? null : trim($cacheKey);
|
||||
$this->cacheKey = $cacheKey === '' ? null : $cacheKey;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markSyncSucceeded(string $cacheKey): self
|
||||
{
|
||||
$this->cacheKey = $cacheKey;
|
||||
$this->lastSyncStatus = self::SYNC_STATUS_SYNCED;
|
||||
$this->lastSuccessfulSyncAt = new DateTimeImmutable();
|
||||
$this->lastSyncError = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markSyncFailed(string $error): self
|
||||
{
|
||||
$this->lastSyncStatus = self::SYNC_STATUS_FAILED;
|
||||
$this->lastSyncError = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function initializeTimestamps(): void
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$this->createdAt ??= $now;
|
||||
$this->updatedAt ??= $now;
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function refreshUpdatedAt(): void
|
||||
{
|
||||
$this->updatedAt = new DateTimeImmutable();
|
||||
}
|
||||
}
|
||||
86
src/Entity/User.php
Normal file
86
src/Entity/User.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
#[ORM\Table(name: 'admin_user')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_admin_user_username', columns: ['username'])]
|
||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
private string $username = '';
|
||||
|
||||
#[ORM\Column]
|
||||
private string $passwordHash = '';
|
||||
|
||||
/** @var list<string> */
|
||||
#[ORM\Column(type: 'json')]
|
||||
private array $roles = ['ROLE_ADMIN'];
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function setUsername(string $username): self
|
||||
{
|
||||
$this->username = trim($username);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function getRoles(): array
|
||||
{
|
||||
return array_values(array_unique([...$this->roles, 'ROLE_ADMIN']));
|
||||
}
|
||||
|
||||
/** @param list<string> $roles */
|
||||
public function setRoles(array $roles): self
|
||||
{
|
||||
$this->roles = $roles;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->passwordHash;
|
||||
}
|
||||
|
||||
public function getPasswordHash(): string
|
||||
{
|
||||
return $this->passwordHash;
|
||||
}
|
||||
|
||||
public function setPasswordHash(string $passwordHash): self
|
||||
{
|
||||
$this->passwordHash = $passwordHash;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
23
src/Repository/ScriptMappingRepository.php
Normal file
23
src/Repository/ScriptMappingRepository.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ScriptMapping;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
final class ScriptMappingRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ScriptMapping::class);
|
||||
}
|
||||
|
||||
public function findActiveByPublicPath(string $publicPath): ?ScriptMapping
|
||||
{
|
||||
return $this->findOneBy([
|
||||
'publicPath' => $publicPath,
|
||||
'active' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
21
src/Repository/UserRepository.php
Normal file
21
src/Repository/UserRepository.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
|
||||
|
||||
final class UserRepository extends ServiceEntityRepository implements UserLoaderInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
|
||||
public function loadUserByIdentifier(string $identifier): ?User
|
||||
{
|
||||
return $this->findOneBy(['username' => $identifier]);
|
||||
}
|
||||
}
|
||||
31
tests/Entity/ScriptMappingTest.php
Normal file
31
tests/Entity/ScriptMappingTest.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\ScriptMapping;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ScriptMappingTest extends TestCase
|
||||
{
|
||||
public function testDefaultsAreUsableForNewMapping(): void
|
||||
{
|
||||
$mapping = new ScriptMapping();
|
||||
|
||||
self::assertSame('main', $mapping->getGitRef());
|
||||
self::assertTrue($mapping->isActive());
|
||||
self::assertSame('never_synced', $mapping->getLastSyncStatus());
|
||||
self::assertNull($mapping->getLastSuccessfulSyncAt());
|
||||
self::assertNull($mapping->getLastSyncError());
|
||||
}
|
||||
|
||||
public function testSyncFailureDoesNotClearPreviousCacheKey(): void
|
||||
{
|
||||
$mapping = new ScriptMapping();
|
||||
$mapping->setCacheKey('scripts/1.sh');
|
||||
$mapping->markSyncFailed('git fetch failed');
|
||||
|
||||
self::assertSame('scripts/1.sh', $mapping->getCacheKey());
|
||||
self::assertSame('failed', $mapping->getLastSyncStatus());
|
||||
self::assertSame('git fetch failed', $mapping->getLastSyncError());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue