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.
|
* 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),
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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' }
|
||||||
|
|||||||
Reference in New Issue
Block a user