From 475cfd78cd19d1beecb33bcd80e16c76f544087e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 11 Dec 2025 21:23:46 +0100 Subject: [PATCH] fix: Prevent N+1 query in LocalPersistentVolume.isDockerComposeResource() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use relationLoaded() check before accessing the application relationship to avoid triggering individual queries for each volume when rendering storage lists. Update Storage.php to eager load the relationship. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/Livewire/Project/Service/Storage.php | 2 +- app/Models/LocalPersistentVolume.php | 53 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/Livewire/Project/Service/Storage.php b/app/Livewire/Project/Service/Storage.php index 644b100b8..12d8bcbc3 100644 --- a/app/Livewire/Project/Service/Storage.php +++ b/app/Livewire/Project/Service/Storage.php @@ -67,7 +67,7 @@ public function refreshStoragesFromEvent() public function refreshStorages() { $this->fileStorage = $this->resource->fileStorages()->get(); - $this->resource->refresh(); + $this->resource->load('persistentStorages.resource'); } public function getFilesProperty() diff --git a/app/Models/LocalPersistentVolume.php b/app/Models/LocalPersistentVolume.php index 26e9b3e85..7126253ea 100644 --- a/app/Models/LocalPersistentVolume.php +++ b/app/Models/LocalPersistentVolume.php @@ -10,6 +10,11 @@ class LocalPersistentVolume extends Model { protected $guarded = []; + public function resource() + { + return $this->morphTo('resource'); + } + public function application() { return $this->morphTo('resource'); @@ -50,6 +55,54 @@ protected function hostPath(): Attribute ); } + // Check if this volume belongs to a service resource + public function isServiceResource(): bool + { + return in_array($this->resource_type, [ + 'App\Models\ServiceApplication', + 'App\Models\ServiceDatabase', + ]); + } + + // Check if this volume belongs to a dockercompose application + public function isDockerComposeResource(): bool + { + if ($this->resource_type !== 'App\Models\Application') { + return false; + } + + // Only access relationship if already eager loaded to avoid N+1 + if (! $this->relationLoaded('resource')) { + return false; + } + + $application = $this->resource; + if (! $application) { + return false; + } + + return data_get($application, 'build_pack') === 'dockercompose'; + } + + // Determine if this volume should be read-only in the UI + // Service volumes and dockercompose application volumes are read-only + // (users should edit compose file directly) + public function shouldBeReadOnlyInUI(): bool + { + // All service volumes should be read-only in UI + if ($this->isServiceResource()) { + return true; + } + + // All dockercompose application volumes should be read-only in UI + if ($this->isDockerComposeResource()) { + return true; + } + + // Check for explicit :ro flag in compose (existing logic) + return $this->isReadOnlyVolume(); + } + // Check if this volume is read-only by parsing the docker-compose content public function isReadOnlyVolume(): bool {