feat(api): support sqlite databases in the databases API

This commit is contained in:
peaklabs-dev
2026-09-24 22:15:08 +02:00
parent 46e53e1fb8
commit 032ea6498c
5 changed files with 338 additions and 2 deletions
@@ -21,6 +21,7 @@ use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneSqlite;
use App\Models\SwarmDocker;
use App\Support\ValidationPatterns;
use Illuminate\Database\Eloquent\Model;
@@ -482,6 +483,7 @@ class DatabasesController extends Controller
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
'sqlite_databases' => ['type' => 'string', 'description' => 'Comma-separated SQLite database file names'],
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Enable the database healthcheck probe.', 'default' => true],
'health_check_interval' => ['type' => 'integer', 'description' => 'Healthcheck interval in seconds.', 'minimum' => 1, 'default' => 15],
'health_check_timeout' => ['type' => 'integer', 'description' => 'Healthcheck timeout in seconds.', 'minimum' => 1, 'default' => 5],
@@ -516,7 +518,7 @@ class DatabasesController extends Controller
)]
public function update_by_uuid(Request $request)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'sqlite_databases'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
@@ -752,6 +754,12 @@ class DatabasesController extends Controller
$request->offsetSet('mysql_conf', $mysqlConf);
}
break;
case 'standalone-sqlite':
$allowedFields = ['name', 'description', 'image', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'sqlite_databases'];
$validator = customApiValidator($request->all(), [
'sqlite_databases' => ['string', 'max:255', 'regex:'.StandaloneSqlite::DATABASES_PATTERN],
]);
break;
}
$allowedFields = array_merge($allowedFields, ['health_check_enabled', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period']);
$healthCheckValidator = customApiValidator($request->all(), [
@@ -1008,6 +1016,8 @@ class DatabasesController extends Controller
$backupData['databases_to_backup'] = $database->mariadb_database;
} elseif ($database->type() === 'standalone-clickhouse') {
$backupData['databases_to_backup'] = $database->clickhouse_db;
} elseif ($database->type() === 'standalone-sqlite') {
$backupData['databases_to_backup'] = $database->sqlite_databases;
}
}
@@ -1844,9 +1854,74 @@ class DatabasesController extends Controller
return $this->create_database($request, NewDatabaseTypes::MONGODB);
}
#[OA\Post(
summary: 'Create (SQLite)',
description: 'Create a new SQLite database.',
path: '/databases/sqlite',
operationId: 'create-database-sqlite',
security: [
['bearerAuth' => []],
],
tags: ['Databases'],
requestBody: new OA\RequestBody(
description: 'Database data',
required: true,
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'],
properties: [
'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'],
'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'],
'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'],
'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'],
'sqlite_databases' => ['type' => 'string', 'description' => 'Comma-separated SQLite database file names'],
'name' => ['type' => 'string', 'description' => 'Name of the database'],
'description' => ['type' => 'string', 'description' => 'Description of the database'],
'image' => ['type' => 'string', 'description' => 'Docker Image of the database'],
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'],
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'],
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'],
'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'],
'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'],
'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'],
'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'],
'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'],
'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'],
],
),
)
),
responses: [
new OA\Response(
response: 200,
description: 'Database updated',
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 422,
ref: '#/components/responses/422',
),
]
)]
public function create_database_sqlite(Request $request)
{
return $this->create_database($request, NewDatabaseTypes::SQLITE);
}
public function create_database(Request $request, NewDatabaseTypes $type)
{
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'tags'];
$allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'sqlite_databases', 'tags'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
@@ -2488,6 +2563,47 @@ class DatabasesController extends Controller
]);
return response()->json(serializeApiResponse($payload))->setStatusCode(201);
} elseif ($type === NewDatabaseTypes::SQLITE) {
$allowedFields = ['name', 'description', 'image', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'sqlite_databases', 'tags'];
$validator = customApiValidator($request->all(), [
'sqlite_databases' => ['string', 'max:255', 'regex:'.StandaloneSqlite::DATABASES_PATTERN],
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
removeUnnecessaryFieldsFromRequest($request);
$database = create_standalone_sqlite($environment->id, $destination, $request->only($allowedFields));
if ($instantDeploy) {
StartDatabase::dispatch($database);
}
if ($tagNames !== []) {
$this->attachTagsToResource($database, $tagNames, $teamId);
}
auditLog('api.database.created', [
'team_id' => $teamId,
'database_uuid' => $database->uuid,
'database_name' => $database->name,
'database_type' => $type->value,
'server_uuid' => $serverUuid,
'is_public' => (bool) $database->is_public,
'instant_deploy' => (bool) $instantDeploy,
]);
return response()->json(serializeApiResponse([
'uuid' => $database->uuid,
]))->setStatusCode(201);
}
return response()->json(['message' => 'Invalid database type requested.'], 400);
@@ -4900,6 +5016,7 @@ class DatabasesController extends Controller
str_starts_with($originalName, 'mongodb-data-') => 'mongodb-data-'.$newDatabase->uuid,
str_starts_with($originalName, 'keydb-data-') => 'keydb-data-'.$newDatabase->uuid,
str_starts_with($originalName, 'dragonfly-data-') => 'dragonfly-data-'.$newDatabase->uuid,
str_starts_with($originalName, 'sqlite-data-') => 'sqlite-data-'.$newDatabase->uuid,
str_starts_with($volume->name, $database->uuid) => str($volume->name)->replace($database->uuid, $newDatabase->uuid)->toString(),
default => $newDatabase->uuid.'-'.$volume->name,
};
@@ -405,6 +405,7 @@ class DestinationsController extends Controller
|| (method_exists($destination, 'keydbs') && $destination->keydbs()->exists())
|| (method_exists($destination, 'dragonflies') && $destination->dragonflies()->exists())
|| (method_exists($destination, 'clickhouses') && $destination->clickhouses()->exists())
|| (method_exists($destination, 'sqlites') && $destination->sqlites()->exists())
|| (method_exists($destination, 'services') && $destination->services()->exists());
if ($hasAttached) {
return response()->json(['message' => 'Destination has attached resources, detach first.'], 409);
+127
View File
@@ -6706,6 +6706,10 @@
"type": "string",
"description": "MySQL conf"
},
"sqlite_databases": {
"type": "string",
"description": "Comma-separated SQLite database file names"
},
"health_check_enabled": {
"type": "boolean",
"description": "Enable the database healthcheck probe.",
@@ -8118,6 +8122,129 @@
]
}
},
"\/databases\/sqlite": {
"post": {
"tags": [
"Databases"
],
"summary": "Create (SQLite)",
"description": "Create a new SQLite database.",
"operationId": "create-database-sqlite",
"requestBody": {
"description": "Database data",
"required": true,
"content": {
"application\/json": {
"schema": {
"required": [
"server_uuid",
"project_uuid",
"environment_name",
"environment_uuid"
],
"properties": {
"server_uuid": {
"type": "string",
"description": "UUID of the server"
},
"project_uuid": {
"type": "string",
"description": "UUID of the project"
},
"environment_name": {
"type": "string",
"description": "Name of the environment. You need to provide at least one of environment_name or environment_uuid."
},
"environment_uuid": {
"type": "string",
"description": "UUID of the environment. You need to provide at least one of environment_name or environment_uuid."
},
"destination_uuid": {
"type": "string",
"description": "UUID of the destination if the server has multiple destinations"
},
"sqlite_databases": {
"type": "string",
"description": "Comma-separated SQLite database file names"
},
"name": {
"type": "string",
"description": "Name of the database"
},
"description": {
"type": "string",
"description": "Description of the database"
},
"image": {
"type": "string",
"description": "Docker Image of the database"
},
"limits_memory": {
"type": "string",
"description": "Memory limit of the database"
},
"limits_memory_swap": {
"type": "string",
"description": "Memory swap limit of the database"
},
"limits_memory_swappiness": {
"type": "integer",
"description": "Memory swappiness of the database"
},
"limits_memory_reservation": {
"type": "string",
"description": "Memory reservation of the database"
},
"limits_cpus": {
"type": "string",
"description": "CPU limit of the database"
},
"limits_cpuset": {
"type": "string",
"description": "CPU set of the database"
},
"limits_cpu_shares": {
"type": "integer",
"description": "CPU shares of the database"
},
"instant_deploy": {
"type": "boolean",
"description": "Instant deploy the database"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags to assign to the database."
}
},
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Database updated"
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"422": {
"$ref": "#\/components\/responses\/422"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/databases\/{uuid}\/logs": {
"get": {
"tags": [
+90
View File
@@ -4343,6 +4343,9 @@ paths:
mysql_conf:
type: string
description: 'MySQL conf'
sqlite_databases:
type: string
description: 'Comma-separated SQLite database file names'
health_check_enabled:
type: boolean
description: 'Enable the database healthcheck probe.'
@@ -5342,6 +5345,93 @@ paths:
security:
-
bearerAuth: []
/databases/sqlite:
post:
tags:
- Databases
summary: 'Create (SQLite)'
description: 'Create a new SQLite database.'
operationId: create-database-sqlite
requestBody:
description: 'Database data'
required: true
content:
application/json:
schema:
required:
- server_uuid
- project_uuid
- environment_name
- environment_uuid
properties:
server_uuid:
type: string
description: 'UUID of the server'
project_uuid:
type: string
description: 'UUID of the project'
environment_name:
type: string
description: 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'
environment_uuid:
type: string
description: 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'
destination_uuid:
type: string
description: 'UUID of the destination if the server has multiple destinations'
sqlite_databases:
type: string
description: 'Comma-separated SQLite database file names'
name:
type: string
description: 'Name of the database'
description:
type: string
description: 'Description of the database'
image:
type: string
description: 'Docker Image of the database'
limits_memory:
type: string
description: 'Memory limit of the database'
limits_memory_swap:
type: string
description: 'Memory swap limit of the database'
limits_memory_swappiness:
type: integer
description: 'Memory swappiness of the database'
limits_memory_reservation:
type: string
description: 'Memory reservation of the database'
limits_cpus:
type: string
description: 'CPU limit of the database'
limits_cpuset:
type: string
description: 'CPU set of the database'
limits_cpu_shares:
type: integer
description: 'CPU shares of the database'
instant_deploy:
type: boolean
description: 'Instant deploy the database'
tags:
type: array
items: { type: string }
description: 'Tags to assign to the database.'
type: object
responses:
'200':
description: 'Database updated'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'422':
$ref: '#/components/responses/422'
security:
-
bearerAuth: []
'/databases/{uuid}/logs':
get:
tags:
+1
View File
@@ -309,6 +309,7 @@ Route::group([
Route::post('/databases/clickhouse', [DatabasesController::class, 'create_database_clickhouse'])->middleware(['api.ability:write']);
Route::post('/databases/dragonfly', [DatabasesController::class, 'create_database_dragonfly'])->middleware(['api.ability:write']);
Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['api.ability:write']);
Route::post('/databases/sqlite', [DatabasesController::class, 'create_database_sqlite'])->middleware(['api.ability:write']);
Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']);
Route::post('/databases/{uuid}/imports/uploads', [DatabasesController::class, 'upload_import'])->middleware(['api.ability:deploy'])->name('api.databases.imports.upload');