feat: implement database management service layer and frontend SQL import/export utility

This commit is contained in:
Ümit Tunç
2026-04-28 21:12:20 +03:00
parent 01ddb81aa9
commit ab5a12f8f2
6 changed files with 48 additions and 24 deletions
@@ -37,7 +37,7 @@ interface DatabaseDriverInterface
/** /**
* Export the database. * Export the database.
*/ */
public function export(array $config, array $filters = []): string; public function export(array $config, array $filters = [], array $options = []): string;
/** /**
* Import the database. * Import the database.
@@ -112,8 +112,11 @@ class SchemaController extends Controller
$this->initializeDriver($request); $this->initializeDriver($request);
$config = $request->only(['host', 'username', 'password', 'database', 'port', 'table']); $config = $request->only(['host', 'username', 'password', 'database', 'port', 'table']);
$filters = json_decode($request->get('filters', '[]'), true); $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); return Response::download($filePath)->deleteFileAfterSend(true);
} catch (\Exception $e) { } catch (\Exception $e) {
@@ -147,7 +147,7 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
return $this->query($sql, [$table, $dbName]); 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'] ?? ''; $database = $config['database'] ?? '';
$table = $config['table'] ?? ''; $table = $config['table'] ?? '';
@@ -157,9 +157,11 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
return $this->exportFilteredTable($database, $table, $filters); return $this->exportFilteredTable($database, $table, $filters);
} }
$isStructureOnly = $options['structureOnly'] ?? false;
$filename = !empty($table) $filename = !empty($table)
? "{$table}-" . date('Y-m-d') . ".sql" ? "{$table}-" . ($isStructureOnly ? 'schema-' : '') . date('Y-m-d') . ".sql"
: "backup-" . ($database ?: 'all') . "-" . date('Y-m-d') . ".sql"; : "backup-" . ($database ?: 'all') . "-" . ($isStructureOnly ? 'schema-' : '') . date('Y-m-d') . ".sql";
$directory = storage_path('app/backups'); $directory = storage_path('app/backups');
if (!is_dir($directory)) { if (!is_dir($directory)) {
@@ -191,6 +193,10 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
// --quick: useful for large tables // --quick: useful for large tables
$flags = "--single-transaction --skip-lock-tables --routines --triggers --events --quick"; $flags = "--single-transaction --skip-lock-tables --routines --triggers --events --quick";
if ($isStructureOnly) {
$flags .= " --no-data";
}
$command = sprintf( $command = sprintf(
'%s -u %s %s -h %s -P %s %s %s > %s 2> %s', '%s -u %s %s -h %s -P %s %s %s > %s 2> %s',
$mysqldumpPath === 'mysqldump' ? 'mysqldump' : escapeshellarg($mysqldumpPath), $mysqldumpPath === 'mysqldump' ? 'mysqldump' : escapeshellarg($mysqldumpPath),
+2 -2
View File
@@ -102,9 +102,9 @@ class DatabaseService
/** /**
* Export the database. * 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);
} }
/** /**
+23 -9
View File
@@ -38,22 +38,23 @@ const TransferContent: React.FC<TransferContentProps> = ({ mode = 'both' }) => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
const handleExport = async () => { const handleExport = async (onlyStructure: boolean = false) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
setSuccess(null); setSuccess(null);
try { 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 url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
const suffix = onlyStructure ? '-schema' : '';
const filename = activeTable const filename = activeTable
? `${activeTable}-${new Date().toISOString().split('T')[0]}.sql` ? `${activeTable}${suffix}-${new Date().toISOString().split('T')[0]}.sql`
: `backup-${activeDatabase || 'all'}-${new Date().toISOString().split('T')[0]}.sql`; : `backup-${activeDatabase || 'all'}${suffix}-${new Date().toISOString().split('T')[0]}.sql`;
link.setAttribute('download', filename); link.setAttribute('download', filename);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
setSuccess(`${activeTable ? 'Table' : 'Database'} exported successfully!`); setSuccess(`${activeTable ? 'Table' : 'Database'} ${onlyStructure ? 'structure' : ''} exported successfully!`);
} catch (err: any) { } catch (err: any) {
setError('Export failed: ' + (err.response?.data?.error || err.message)); setError('Export failed: ' + (err.response?.data?.error || err.message));
} finally { } finally {
@@ -138,22 +139,35 @@ const TransferContent: React.FC<TransferContentProps> = ({ mode = 'both' }) => {
<TableCell sx={{ py: 3 }}> <TableCell sx={{ py: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>Export {activeTable ? 'Table' : 'Database'}</Typography> <Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>Export {activeTable ? 'Table' : 'Database'}</Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Create a full backup of the current {activeTable ? 'table' : 'database'}: <br/> Create a backup of the current {activeTable ? 'table' : 'database'}: <br/>
<Box component="span" sx={{ color: 'primary.main', fontWeight: 600 }}> <Box component="span" sx={{ color: 'primary.main', fontWeight: 600 }}>
{activeTable ? `${activeDatabase}.${activeTable}` : (activeDatabase || 'All Databases')} {activeTable ? `${activeDatabase}.${activeTable}` : (activeDatabase || 'All Databases')}
</Box> </Box>
</Typography> </Typography>
</TableCell> </TableCell>
<TableCell align="right" sx={{ py: 3 }}> <TableCell align="right" sx={{ py: 3 }}>
<Stack direction="row" spacing={1} justifyContent="flex-end">
<Button
variant="outlined"
color="primary"
startIcon={<CloudDownload />}
onClick={() => handleExport(true)}
disabled={loading}
sx={{ borderRadius: 2, px: 2, py: 1, fontWeight: 700 }}
>
Export Structure
</Button>
<Button <Button
variant="contained" variant="contained"
color="primary"
startIcon={<CloudDownload />} startIcon={<CloudDownload />}
onClick={handleExport} onClick={() => handleExport(false)}
disabled={loading} disabled={loading}
sx={{ borderRadius: 2, px: 3, py: 1, fontWeight: 700, minWidth: 160 }} sx={{ borderRadius: 2, px: 2, py: 1, fontWeight: 700 }}
> >
Start Export Full Export
</Button> </Button>
</Stack>
</TableCell> </TableCell>
</TableRow> </TableRow>
)} )}
+3 -2
View File
@@ -39,10 +39,11 @@ export const SchemaService = {
bulkAction: (data: { tables: string[], action: string, database: string }) => api.post('/schema/bulk-action', data), 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 }), batchUpdate: (table: string, changes: any[]) => api.post(`/schema/${table}/batch-update`, { changes }),
executeQuery: (query: string) => api.post('/schema/execute', { query }), 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, database,
table, table,
filters: filters ? JSON.stringify(filters) : undefined filters: filters ? JSON.stringify(filters) : undefined,
structureOnly
}, { responseType: 'blob' }), }, { responseType: 'blob' }),
importDatabase: (formData: FormData) => api.post('/schema/import', formData, { importDatabase: (formData: FormData) => api.post('/schema/import', formData, {
headers: { 'Content-Type': 'multipart/form-data' } headers: { 'Content-Type': 'multipart/form-data' }