From bca325cc07dbe3229128aee817aa38db948e2566 Mon Sep 17 00:00:00 2001 From: thibaud-leclere Date: Tue, 5 May 2026 09:49:16 +0200 Subject: [PATCH] feat: add installer mapping persistence --- migrations/Version20260505073300.php | 30 +++ src/Entity/ScriptMapping.php | 212 +++++++++++++++++++++ src/Entity/User.php | 86 +++++++++ src/Repository/ScriptMappingRepository.php | 23 +++ src/Repository/UserRepository.php | 21 ++ tests/Entity/ScriptMappingTest.php | 31 +++ 6 files changed, 403 insertions(+) create mode 100644 migrations/Version20260505073300.php create mode 100644 src/Entity/ScriptMapping.php create mode 100644 src/Entity/User.php create mode 100644 src/Repository/ScriptMappingRepository.php create mode 100644 src/Repository/UserRepository.php create mode 100644 tests/Entity/ScriptMappingTest.php diff --git a/migrations/Version20260505073300.php b/migrations/Version20260505073300.php new file mode 100644 index 0000000..a6f30ee --- /dev/null +++ b/migrations/Version20260505073300.php @@ -0,0 +1,30 @@ +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'); + } +} diff --git a/src/Entity/ScriptMapping.php b/src/Entity/ScriptMapping.php new file mode 100644 index 0000000..8824b8d --- /dev/null +++ b/src/Entity/ScriptMapping.php @@ -0,0 +1,212 @@ +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(); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..bec4d74 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,86 @@ + */ + #[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 */ + public function getRoles(): array + { + return array_values(array_unique([...$this->roles, 'ROLE_ADMIN'])); + } + + /** @param list $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 + { + } +} diff --git a/src/Repository/ScriptMappingRepository.php b/src/Repository/ScriptMappingRepository.php new file mode 100644 index 0000000..4f41cad --- /dev/null +++ b/src/Repository/ScriptMappingRepository.php @@ -0,0 +1,23 @@ +findOneBy([ + 'publicPath' => $publicPath, + 'active' => true, + ]); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..c2c4255 --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,21 @@ +findOneBy(['username' => $identifier]); + } +} diff --git a/tests/Entity/ScriptMappingTest.php b/tests/Entity/ScriptMappingTest.php new file mode 100644 index 0000000..d6b793f --- /dev/null +++ b/tests/Entity/ScriptMappingTest.php @@ -0,0 +1,31 @@ +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()); + } +}