diff --git a/app/Http/Controllers/MagicController.php b/app/Http/Controllers/MagicController.php deleted file mode 100644 index 59c9b8b94..000000000 --- a/app/Http/Controllers/MagicController.php +++ /dev/null @@ -1,84 +0,0 @@ -json([ - 'servers' => Server::isUsable()->get(), - ]); - } - - public function destinations() - { - return response()->json([ - 'destinations' => Server::destinationsByServer(request()->query('server_id'))->sortBy('name'), - ]); - } - - public function projects() - { - return response()->json([ - 'projects' => Project::ownedByCurrentTeam()->get(), - ]); - } - - public function environments() - { - $project = Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->first(); - if (! $project) { - return response()->json([ - 'environments' => [], - ]); - } - - return response()->json([ - 'environments' => $project->environments, - ]); - } - - public function newProject() - { - $project = Project::firstOrCreate( - ['name' => request()->query('name') ?? generate_random_name()], - ['team_id' => currentTeam()->id] - ); - - return response()->json([ - 'project_uuid' => $project->uuid, - ]); - } - - public function newEnvironment() - { - $environment = Environment::firstOrCreate( - ['name' => request()->query('name') ?? generate_random_name()], - ['project_id' => Project::ownedByCurrentTeam()->whereUuid(request()->query('project_uuid'))->firstOrFail()->id] - ); - - return response()->json([ - 'environment_name' => $environment->name, - ]); - } - - public function newTeam() - { - $team = Team::create( - [ - 'name' => request()->query('name') ?? generate_random_name(), - 'personal_team' => false, - ], - ); - auth()->user()->teams()->attach($team, ['role' => 'admin']); - refreshSession(); - - return redirect(request()->header('Referer')); - } -} diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index 57ecaa8a2..8c2be9ab6 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -18,9 +18,9 @@ class Dashboard extends Component public function mount() { - $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get(); - $this->servers = Server::ownedByCurrentTeam()->get(); - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->privateKeys = PrivateKey::ownedByCurrentTeamCached(); + $this->servers = Server::ownedByCurrentTeamCached(); + $this->projects = Project::ownedByCurrentTeam()->with('environments')->get(); } public function render() diff --git a/app/Livewire/DeploymentsIndicator.php b/app/Livewire/DeploymentsIndicator.php index ac9cfd1c2..268aed152 100644 --- a/app/Livewire/DeploymentsIndicator.php +++ b/app/Livewire/DeploymentsIndicator.php @@ -14,7 +14,7 @@ class DeploymentsIndicator extends Component #[Computed] public function deployments() { - $servers = Server::ownedByCurrentTeam()->get(); + $servers = Server::ownedByCurrentTeamCached(); return ApplicationDeploymentQueue::with(['application.environment.project']) ->whereIn('status', ['in_progress', 'queued']) diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index 0e4f15a5c..7aa8dfc49 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -17,9 +17,9 @@ class Index extends Component public function mount() { - $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); - $this->projects = Project::ownedByCurrentTeam()->get(); - $this->servers = Server::ownedByCurrentTeam()->count(); + $this->private_keys = PrivateKey::ownedByCurrentTeamCached(); + $this->projects = Project::ownedByCurrentTeamCached(); + $this->servers = Server::ownedByCurrentTeamCached(); } public function render() diff --git a/app/Livewire/Project/Shared/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index 47b3534a2..4ba961dfd 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -36,7 +36,7 @@ public function mount() $parameters = get_route_parameters(); $this->projectUuid = data_get($parameters, 'project_uuid'); $this->environmentUuid = data_get($parameters, 'environment_uuid'); - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->projects = Project::ownedByCurrentTeamCached(); $this->servers = currentTeam()->servers->filter(fn ($server) => ! $server->isBuildServer()); } diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index f7947951b..b1b34dd71 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -72,7 +72,7 @@ public function mount(string $task_uuid, string $project_uuid, string $environme } elseif ($service_uuid) { $this->type = 'service'; $this->service_uuid = $service_uuid; - $this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail(); + $this->resource = Service::ownedByCurrentTeamCached()->where('uuid', $service_uuid)->firstOrFail(); } $this->parameters = [ 'environment_uuid' => $environment_uuid, diff --git a/app/Livewire/Server/Create.php b/app/Livewire/Server/Create.php index cf77664fe..5fd2ea4f7 100644 --- a/app/Livewire/Server/Create.php +++ b/app/Livewire/Server/Create.php @@ -17,7 +17,7 @@ class Create extends Component public function mount() { - $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); + $this->private_keys = PrivateKey::ownedByCurrentTeamCached(); if (! isCloud()) { $this->limit_reached = false; diff --git a/app/Livewire/Server/Index.php b/app/Livewire/Server/Index.php index 74764960a..eb832d72f 100644 --- a/app/Livewire/Server/Index.php +++ b/app/Livewire/Server/Index.php @@ -12,7 +12,7 @@ class Index extends Component public function mount() { - $this->servers = Server::ownedByCurrentTeam()->get(); + $this->servers = Server::ownedByCurrentTeamCached(); } public function render() diff --git a/app/Livewire/SharedVariables/Environment/Index.php b/app/Livewire/SharedVariables/Environment/Index.php index 3673a3882..6685c5c99 100644 --- a/app/Livewire/SharedVariables/Environment/Index.php +++ b/app/Livewire/SharedVariables/Environment/Index.php @@ -12,7 +12,7 @@ class Index extends Component public function mount() { - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->projects = Project::ownedByCurrentTeamCached(); } public function render() diff --git a/app/Livewire/SharedVariables/Project/Index.php b/app/Livewire/SharedVariables/Project/Index.php index 570da74d3..58929bade 100644 --- a/app/Livewire/SharedVariables/Project/Index.php +++ b/app/Livewire/SharedVariables/Project/Index.php @@ -12,7 +12,7 @@ class Index extends Component public function mount() { - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->projects = Project::ownedByCurrentTeamCached(); } public function render() diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 4bd0b798a..0a38e6088 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -196,7 +196,7 @@ public function mount() $github_app_uuid = request()->github_app_uuid; $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); $this->github_app->makeVisible(['client_secret', 'webhook_secret']); - $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get(); + $this->privateKeys = PrivateKey::ownedByCurrentTeamCached(); $this->applications = $this->github_app->applications; $settings = instanceSettings(); diff --git a/app/Models/Application.php b/app/Models/Application.php index 118245546..5006d0ff8 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -338,11 +338,25 @@ public static function ownedByCurrentTeamAPI(int $teamId) return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); } + /** + * Get query builder for applications owned by current team. + * If you need all applications without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all applications owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return Application::ownedByCurrentTeam()->get(); + }); + } + public function getContainersToStop(Server $server, bool $previewDeployments = false): array { $containers = $previewDeployments diff --git a/app/Models/PrivateKey.php b/app/Models/PrivateKey.php index 46531ed34..bb76d5ed6 100644 --- a/app/Models/PrivateKey.php +++ b/app/Models/PrivateKey.php @@ -80,6 +80,10 @@ public function getPublicKey() return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key'; } + /** + * Get query builder for private keys owned by current team. + * If you need all private keys without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam(array $select = ['*']) { $teamId = currentTeam()->id; @@ -88,6 +92,16 @@ public static function ownedByCurrentTeam(array $select = ['*']) return self::whereTeamId($teamId)->select($selectArray->all()); } + /** + * Get all private keys owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return PrivateKey::ownedByCurrentTeam()->get(); + }); + } + public static function ownedAndOnlySShKeys(array $select = ['*']) { $teamId = currentTeam()->id; diff --git a/app/Models/Project.php b/app/Models/Project.php index a9bf76803..8b26672f0 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -30,11 +30,25 @@ class Project extends BaseModel protected $guarded = []; + /** + * Get query builder for projects owned by current team. + * If you need all projects without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)'); } + /** + * Get all projects owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return Project::ownedByCurrentTeam()->get(); + }); + } + protected static function booted() { static::created(function ($project) { diff --git a/app/Models/Server.php b/app/Models/Server.php index 8b153c8ac..82ee6721d 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -242,6 +242,10 @@ public static function isReachable() return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true); } + /** + * Get query builder for servers owned by current team. + * If you need all servers without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam(array $select = ['*']) { $teamId = currentTeam()->id; @@ -250,6 +254,16 @@ public static function ownedByCurrentTeam(array $select = ['*']) return Server::whereTeamId($teamId)->with('settings', 'swarmDockers', 'standaloneDockers')->select($selectArray->all())->orderBy('name'); } + /** + * Get all servers owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return Server::ownedByCurrentTeam()->get(); + }); + } + public static function isUsable() { return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false); diff --git a/app/Models/Service.php b/app/Models/Service.php index 2cea4c805..2daf9c39d 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -153,11 +153,25 @@ public function tags() return $this->morphToMany(Tag::class, 'taggable'); } + /** + * Get query builder for services owned by current team. + * If you need all services without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all services owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return Service::ownedByCurrentTeam()->get(); + }); + } + public function deleteConfigurations() { $server = data_get($this, 'destination.server'); diff --git a/app/Models/ServiceApplication.php b/app/Models/ServiceApplication.php index aef74b402..7b8b46812 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -37,11 +37,25 @@ public static function ownedByCurrentTeamAPI(int $teamId) return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); } + /** + * Get query builder for service applications owned by current team. + * If you need all service applications without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all service applications owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return ServiceApplication::ownedByCurrentTeam()->get(); + }); + } + public function isRunning() { return str($this->status)->contains('running'); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 3a249059c..f6a39cfe4 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -30,11 +30,25 @@ public static function ownedByCurrentTeamAPI(int $teamId) return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); } + /** + * Get query builder for service databases owned by current team. + * If you need all service databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all service databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return ServiceDatabase::ownedByCurrentTeam()->get(); + }); + } + public function restart() { $container_id = $this->name.'-'.$this->service->uuid; diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 6ac685618..f598ef2ea 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -44,11 +44,25 @@ protected static function booted() }); } + /** + * Get query builder for ClickHouse databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandaloneClickhouse::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all ClickHouse databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneClickhouse::ownedByCurrentTeam()->get(); + }); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 2d004246c..47170056f 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -44,11 +44,25 @@ protected static function booted() }); } + /** + * Get query builder for Dragonfly databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandaloneDragonfly::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all Dragonfly databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneDragonfly::ownedByCurrentTeam()->get(); + }); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 131e5bb3f..266110d0a 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -44,11 +44,25 @@ protected static function booted() }); } + /** + * Get query builder for KeyDB databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandaloneKeydb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all KeyDB databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneKeydb::ownedByCurrentTeam()->get(); + }); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index 675c7987f..aa7f2d31a 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -45,11 +45,25 @@ protected static function booted() }); } + /** + * Get query builder for MariaDB databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandaloneMariadb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all MariaDB databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneMariadb::ownedByCurrentTeam()->get(); + }); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 7b70988f6..9046ab013 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -47,11 +47,25 @@ protected static function booted() }); } + /** + * Get query builder for MongoDB databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandaloneMongodb::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all MongoDB databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneMongodb::ownedByCurrentTeam()->get(); + }); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 6f79241af..719387b36 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -45,11 +45,25 @@ protected static function booted() }); } + /** + * Get query builder for MySQL databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandaloneMysql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all MySQL databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneMysql::ownedByCurrentTeam()->get(); + }); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 2dc5616a2..03080fd3d 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -45,11 +45,25 @@ protected static function booted() }); } + /** + * Get query builder for PostgreSQL databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandalonePostgresql::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all PostgreSQL databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandalonePostgresql::ownedByCurrentTeam()->get(); + }); + } + public function workdir() { return database_configuration_dir()."/{$this->uuid}"; diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index c0223304a..6aca8af9a 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -46,11 +46,25 @@ protected static function booted() }); } + /** + * Get query builder for Redis databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ public static function ownedByCurrentTeam() { return StandaloneRedis::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } + /** + * Get all Redis databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneRedis::ownedByCurrentTeam()->get(); + }); + } + protected function serverStatus(): Attribute { return Attribute::make( diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index 48c3a62c3..1a0ae0fbd 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -5,39 +5,44 @@ function isSubscriptionActive() { - if (! isCloud()) { - return false; - } - $team = currentTeam(); - if (! $team) { - return false; - } - $subscription = $team?->subscription; + return once(function () { + if (! isCloud()) { + return false; + } + $team = currentTeam(); + if (! $team) { + return false; + } + $subscription = $team?->subscription; - if (is_null($subscription)) { - return false; - } - if (isStripe()) { - return $subscription->stripe_invoice_paid === true; - } + if (is_null($subscription)) { + return false; + } + if (isStripe()) { + return $subscription->stripe_invoice_paid === true; + } - return false; + return false; + }); } + function isSubscriptionOnGracePeriod() { - $team = currentTeam(); - if (! $team) { - return false; - } - $subscription = $team?->subscription; - if (! $subscription) { - return false; - } - if (isStripe()) { - return $subscription->stripe_cancel_at_period_end; - } + return once(function () { + $team = currentTeam(); + if (! $team) { + return false; + } + $subscription = $team?->subscription; + if (! $subscription) { + return false; + } + if (isStripe()) { + return $subscription->stripe_cancel_at_period_end; + } - return false; + return false; + }); } function subscriptionProvider() { diff --git a/database/migrations/2025_12_08_000000_add_performance_indexes.php b/database/migrations/2025_12_08_000000_add_performance_indexes.php new file mode 100644 index 000000000..680c4b4f7 --- /dev/null +++ b/database/migrations/2025_12_08_000000_add_performance_indexes.php @@ -0,0 +1,49 @@ +indexes as [$table, $columns, $indexName]) { + if (! $this->indexExists($indexName)) { + $columnList = implode(', ', array_map(fn ($col) => "\"$col\"", $columns)); + DB::statement("CREATE INDEX \"{$indexName}\" ON \"{$table}\" ({$columnList})"); + } + } + } + + public function down(): void + { + foreach ($this->indexes as [, , $indexName]) { + DB::statement("DROP INDEX IF EXISTS \"{$indexName}\""); + } + } + + private function indexExists(string $indexName): bool + { + $result = DB::selectOne( + 'SELECT 1 FROM pg_indexes WHERE indexname = ?', + [$indexName] + ); + + return $result !== null; + } +};