feat: implement database management service layer and frontend SQL import/export utility
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -38,22 +38,23 @@ const TransferContent: React.FC<TransferContentProps> = ({ mode = 'both' }) => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [file, setFile] = useState<File | null>(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<TransferContentProps> = ({ mode = 'both' }) => {
|
||||
<TableCell sx={{ py: 3 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, mb: 0.5 }}>Export {activeTable ? 'Table' : 'Database'}</Typography>
|
||||
<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 }}>
|
||||
{activeTable ? `${activeDatabase}.${activeTable}` : (activeDatabase || 'All Databases')}
|
||||
</Box>
|
||||
</Typography>
|
||||
</TableCell>
|
||||
<TableCell align="right" sx={{ py: 3 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<CloudDownload />}
|
||||
onClick={handleExport}
|
||||
disabled={loading}
|
||||
sx={{ borderRadius: 2, px: 3, py: 1, fontWeight: 700, minWidth: 160 }}
|
||||
>
|
||||
Start Export
|
||||
</Button>
|
||||
<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
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<CloudDownload />}
|
||||
onClick={() => handleExport(false)}
|
||||
disabled={loading}
|
||||
sx={{ borderRadius: 2, px: 2, py: 1, fontWeight: 700 }}
|
||||
>
|
||||
Full Export
|
||||
</Button>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@@ -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' }
|
||||
|
||||
Reference in New Issue
Block a user