From ab5a12f8f2878fe2aaad93c5c94fa7b32878a2e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Cmit=20Tun=C3=A7?= Date: Tue, 28 Apr 2026 21:12:20 +0300 Subject: [PATCH] feat: implement database management service layer and frontend SQL import/export utility --- .../app/Contracts/DatabaseDriverInterface.php | 2 +- .../Http/Controllers/Api/SchemaController.php | 5 ++- backend/app/Services/Database/MySqlDriver.php | 12 +++-- backend/app/Services/DatabaseService.php | 4 +- frontend/src/components/TransferContent.tsx | 44 ++++++++++++------- frontend/src/services/api.ts | 5 ++- 6 files changed, 48 insertions(+), 24 deletions(-) diff --git a/backend/app/Contracts/DatabaseDriverInterface.php b/backend/app/Contracts/DatabaseDriverInterface.php index 9027253..5a37478 100644 --- a/backend/app/Contracts/DatabaseDriverInterface.php +++ b/backend/app/Contracts/DatabaseDriverInterface.php @@ -37,7 +37,7 @@ interface DatabaseDriverInterface /** * Export the database. */ - public function export(array $config, array $filters = []): string; + public function export(array $config, array $filters = [], array $options = []): string; /** * Import the database. diff --git a/backend/app/Http/Controllers/Api/SchemaController.php b/backend/app/Http/Controllers/Api/SchemaController.php index 03692ea..56e6c39 100644 --- a/backend/app/Http/Controllers/Api/SchemaController.php +++ b/backend/app/Http/Controllers/Api/SchemaController.php @@ -112,8 +112,11 @@ class SchemaController extends Controller $this->initializeDriver($request); $config = $request->only(['host', 'username', 'password', 'database', 'port', 'table']); $filters = json_decode($request->get('filters', '[]'), true); + $options = [ + 'structureOnly' => filter_var($request->get('structureOnly'), FILTER_VALIDATE_BOOLEAN) + ]; - $filePath = $this->databaseService->export($config, $filters); + $filePath = $this->databaseService->export($config, $filters, $options); return Response::download($filePath)->deleteFileAfterSend(true); } catch (\Exception $e) { diff --git a/backend/app/Services/Database/MySqlDriver.php b/backend/app/Services/Database/MySqlDriver.php index 3d83d78..8c4f114 100644 --- a/backend/app/Services/Database/MySqlDriver.php +++ b/backend/app/Services/Database/MySqlDriver.php @@ -147,7 +147,7 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface return $this->query($sql, [$table, $dbName]); } - public function export(array $config, array $filters = []): string + public function export(array $config, array $filters = [], array $options = []): string { $database = $config['database'] ?? ''; $table = $config['table'] ?? ''; @@ -157,9 +157,11 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface return $this->exportFilteredTable($database, $table, $filters); } + $isStructureOnly = $options['structureOnly'] ?? false; + $filename = !empty($table) - ? "{$table}-" . date('Y-m-d') . ".sql" - : "backup-" . ($database ?: 'all') . "-" . date('Y-m-d') . ".sql"; + ? "{$table}-" . ($isStructureOnly ? 'schema-' : '') . date('Y-m-d') . ".sql" + : "backup-" . ($database ?: 'all') . "-" . ($isStructureOnly ? 'schema-' : '') . date('Y-m-d') . ".sql"; $directory = storage_path('app/backups'); if (!is_dir($directory)) { @@ -191,6 +193,10 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface // --quick: useful for large tables $flags = "--single-transaction --skip-lock-tables --routines --triggers --events --quick"; + if ($isStructureOnly) { + $flags .= " --no-data"; + } + $command = sprintf( '%s -u %s %s -h %s -P %s %s %s > %s 2> %s', $mysqldumpPath === 'mysqldump' ? 'mysqldump' : escapeshellarg($mysqldumpPath), diff --git a/backend/app/Services/DatabaseService.php b/backend/app/Services/DatabaseService.php index c227a61..b07634d 100644 --- a/backend/app/Services/DatabaseService.php +++ b/backend/app/Services/DatabaseService.php @@ -102,9 +102,9 @@ class DatabaseService /** * Export the database. */ - public function export(array $config, array $filters = []): string + public function export(array $config, array $filters = [], array $options = []): string { - return $this->getDriver()->export($config, $filters); + return $this->getDriver()->export($config, $filters, $options); } /** diff --git a/frontend/src/components/TransferContent.tsx b/frontend/src/components/TransferContent.tsx index 8d51578..3338317 100644 --- a/frontend/src/components/TransferContent.tsx +++ b/frontend/src/components/TransferContent.tsx @@ -38,22 +38,23 @@ const TransferContent: React.FC = ({ mode = 'both' }) => { const [error, setError] = useState(null); const [file, setFile] = useState(null); - const handleExport = async () => { + const handleExport = async (onlyStructure: boolean = false) => { setLoading(true); setError(null); setSuccess(null); try { - const response = await SchemaService.exportDatabase(activeDatabase || undefined, activeTable || undefined); + const response = await SchemaService.exportDatabase(activeDatabase || undefined, activeTable || undefined, undefined, onlyStructure); const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement('a'); link.href = url; + const suffix = onlyStructure ? '-schema' : ''; const filename = activeTable - ? `${activeTable}-${new Date().toISOString().split('T')[0]}.sql` - : `backup-${activeDatabase || 'all'}-${new Date().toISOString().split('T')[0]}.sql`; + ? `${activeTable}${suffix}-${new Date().toISOString().split('T')[0]}.sql` + : `backup-${activeDatabase || 'all'}${suffix}-${new Date().toISOString().split('T')[0]}.sql`; link.setAttribute('download', filename); document.body.appendChild(link); link.click(); - setSuccess(`${activeTable ? 'Table' : 'Database'} exported successfully!`); + setSuccess(`${activeTable ? 'Table' : 'Database'} ${onlyStructure ? 'structure' : ''} exported successfully!`); } catch (err: any) { setError('Export failed: ' + (err.response?.data?.error || err.message)); } finally { @@ -138,22 +139,35 @@ const TransferContent: React.FC = ({ mode = 'both' }) => { Export {activeTable ? 'Table' : 'Database'} - Create a full backup of the current {activeTable ? 'table' : 'database'}:
+ Create a backup of the current {activeTable ? 'table' : 'database'}:
{activeTable ? `${activeDatabase}.${activeTable}` : (activeDatabase || 'All Databases')}
- + + + + )} diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index e8a5acc..8feec77 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -39,10 +39,11 @@ export const SchemaService = { bulkAction: (data: { tables: string[], action: string, database: string }) => api.post('/schema/bulk-action', data), batchUpdate: (table: string, changes: any[]) => api.post(`/schema/${table}/batch-update`, { changes }), executeQuery: (query: string) => api.post('/schema/execute', { query }), - exportDatabase: (database?: string, table?: string, filters?: any) => api.post('/schema/export', { + exportDatabase: (database?: string, table?: string, filters?: any, structureOnly: boolean = false) => api.post('/schema/export', { database, table, - filters: filters ? JSON.stringify(filters) : undefined + filters: filters ? JSON.stringify(filters) : undefined, + structureOnly }, { responseType: 'blob' }), importDatabase: (formData: FormData) => api.post('/schema/import', formData, { headers: { 'Content-Type': 'multipart/form-data' }