From 01f7d5ab9220144f2266c57f4a813ce3b7b0dd50 Mon Sep 17 00:00:00 2001 From: peaklabs-dev <122374094+peaklabs-dev@users.noreply.github.com> Date: Thu, 24 Sep 2026 18:11:13 +0200 Subject: [PATCH] feat(database): add StandaloneSqlite model and migration --- app/Models/StandaloneSqlite.php | 318 ++++++++++++++++++ ...145710_create_standalone_sqlites_table.php | 58 ++++ 2 files changed, 376 insertions(+) create mode 100644 app/Models/StandaloneSqlite.php create mode 100644 database/migrations/2026_09_18_145710_create_standalone_sqlites_table.php diff --git a/app/Models/StandaloneSqlite.php b/app/Models/StandaloneSqlite.php new file mode 100644 index 0000000000..797254bc8c --- /dev/null +++ b/app/Models/StandaloneSqlite.php @@ -0,0 +1,318 @@ + 'boolean', + 'health_check_interval' => 'integer', + 'health_check_timeout' => 'integer', + 'health_check_retries' => 'integer', + 'health_check_start_period' => 'integer', + 'restart_count' => 'integer', + 'last_restart_at' => 'datetime', + 'last_restart_type' => 'string', + ]; + + protected static function booted() + { + static::created(function ($database) { + LocalPersistentVolume::create([ + 'name' => 'sqlite-data-'.$database->uuid, + 'mount_path' => self::DATA_DIRECTORY, + 'host_path' => null, + 'resource_id' => $database->id, + 'resource_type' => $database->getMorphClass(), + ]); + }); + static::forceDeleting(function ($database) { + $database->persistentStorages()->delete(); + $database->scheduledBackups()->delete(); + $database->environment_variables()->delete(); + $database->tags()->detach(); + }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->last_online_at = now(); + } + }); + } + + /** + * Get query builder for SQLite databases owned by current team. + * If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead. + */ + public static function ownedByCurrentTeam() + { + return StandaloneSqlite::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + + /** + * Get all SQLite databases owned by current team (cached for request duration). + */ + public static function ownedByCurrentTeamCached() + { + return once(function () { + return StandaloneSqlite::ownedByCurrentTeam()->get(); + }); + } + + protected function serverStatus(): Attribute + { + return Attribute::make( + get: function () { + return $this->destination->server->isFunctional(); + } + ); + } + + public function isConfigurationChanged(bool $save = false) + { + $newConfigHash = $this->image.$this->sqlite_databases; + $newConfigHash .= $this->healthCheckConfigurationHash(); + $newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('value')->sort()); + $newConfigHash = md5($newConfigHash); + $oldConfigHash = data_get($this, 'config_hash'); + if ($oldConfigHash === null) { + if ($save) { + $this->config_hash = $newConfigHash; + $this->save(); + } + + return true; + } + if ($oldConfigHash === $newConfigHash) { + return false; + } else { + if ($save) { + $this->config_hash = $newConfigHash; + $this->save(); + } + + return true; + } + } + + public function isRunning() + { + return (bool) str($this->status)->contains('running'); + } + + public function isExited() + { + return (bool) str($this->status)->startsWith('exited'); + } + + public function workdir() + { + return database_configuration_dir()."/{$this->uuid}"; + } + + public function deleteConfigurations() + { + $server = data_get($this, 'destination.server'); + $workdir = $this->workdir(); + if (str($workdir)->endsWith($this->uuid)) { + instant_remote_process(['rm -rf '.$this->workdir()], $server, false); + } + } + + public function deleteVolumes() + { + $persistentStorages = $this->persistentStorages()->get() ?? collect(); + if ($persistentStorages->count() === 0) { + return; + } + $server = data_get($this, 'destination.server'); + foreach ($persistentStorages as $storage) { + instant_remote_process(['docker volume rm -f '.escapeshellarg($storage->name)], $server, false); + } + } + + public function realStatus() + { + return $this->getRawOriginal('status'); + } + + public function status(): Attribute + { + return Attribute::make( + set: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } elseif (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + + return "$status:$health"; + }, + get: function ($value) { + if (str($value)->contains('(')) { + $status = str($value)->before('(')->trim()->value(); + $health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy'; + } elseif (str($value)->contains(':')) { + $status = str($value)->before(':')->trim()->value(); + $health = str($value)->after(':')->trim()->value() ?? 'unhealthy'; + } else { + $status = $value; + $health = 'unhealthy'; + } + + return "$status:$health"; + }, + ); + } + + public function tags() + { + return $this->morphToMany(Tag::class, 'taggable'); + } + + public function project() + { + return data_get($this, 'environment.project'); + } + + public function sslCertificates() + { + return $this->morphMany(SslCertificate::class, 'resource'); + } + + public function link() + { + if (data_get($this, 'environment.project.uuid')) { + return route('project.database.configuration', [ + 'project_uuid' => data_get($this, 'environment.project.uuid'), + 'environment_uuid' => data_get($this, 'environment.uuid'), + 'database_uuid' => data_get($this, 'uuid'), + ]); + } + + return null; + } + + public function isLogDrainEnabled() + { + return data_get($this, 'is_log_drain_enabled', false); + } + + public function team() + { + return data_get($this, 'environment.project.team'); + } + + public function databaseType(): Attribute + { + return new Attribute( + get: fn () => $this->type(), + ); + } + + public function type(): string + { + return 'standalone-sqlite'; + } + + public function databaseFilePath(): string + { + return self::DATA_DIRECTORY.'/'.str($this->sqlite_databases)->before(',')->trim(); + } + + public function environment() + { + return $this->belongsTo(Environment::class); + } + + public function fileStorages() + { + return $this->morphMany(LocalFileVolume::class, 'resource'); + } + + public function destination() + { + return $this->morphTo(); + } + + public function environment_variables() + { + return $this->morphMany(EnvironmentVariable::class, 'resourceable'); + } + + public function runtime_environment_variables() + { + return $this->morphMany(EnvironmentVariable::class, 'resourceable'); + } + + public function persistentStorages() + { + return $this->morphMany(LocalPersistentVolume::class, 'resource'); + } + + public function scheduledBackups() + { + return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); + } + + public function isBackupSolutionAvailable() + { + return true; + } +} diff --git a/database/migrations/2026_09_18_145710_create_standalone_sqlites_table.php b/database/migrations/2026_09_18_145710_create_standalone_sqlites_table.php new file mode 100644 index 0000000000..48d016ec56 --- /dev/null +++ b/database/migrations/2026_09_18_145710_create_standalone_sqlites_table.php @@ -0,0 +1,58 @@ +id(); + $table->string('uuid')->unique(); + $table->string('name'); + $table->string('description')->nullable(); + + $table->string('sqlite_databases')->default('database.sqlite'); + + $table->boolean('is_log_drain_enabled')->default(false); + $table->boolean('is_include_timestamps')->default(false); + $table->softDeletes(); + + $table->string('status')->default('exited'); + $table->integer('restart_count')->default(0); + $table->timestamp('last_restart_at')->nullable(); + $table->string('last_restart_type', 10)->nullable(); + + $table->string('image')->default('peakimages/sqlite:3.53.4-v0.1.0'); + + $table->boolean('is_public')->default(false); + + $table->string('limits_memory')->default('0'); + $table->string('limits_memory_swap')->default('0'); + $table->integer('limits_memory_swappiness')->default(60); + $table->string('limits_memory_reservation')->default('0'); + + $table->string('limits_cpus')->default('0'); + $table->string('limits_cpuset')->nullable()->default(null); + $table->integer('limits_cpu_shares')->default(1024); + + $table->timestamp('started_at')->nullable(); + $table->morphs('destination'); + $table->foreignId('environment_id')->nullable(); + + $table->string('config_hash')->nullable(); + $table->text('custom_docker_run_options')->nullable(); + + $table->boolean('health_check_enabled')->default(true); + $table->integer('health_check_interval')->default(15); + $table->integer('health_check_timeout')->default(5); + $table->integer('health_check_retries')->default(5); + $table->integer('health_check_start_period')->default(5); + + $table->timestamps(); + $table->timestamp('last_online_at')->default(now()); + }); + } +};