feat: implement MySQL database driver and API service for schema management and data operations
This commit is contained in:
@@ -22,12 +22,12 @@ interface DatabaseDriverInterface
|
||||
/**
|
||||
* Get table data.
|
||||
*/
|
||||
public function getTableData(string $table, int $limit = 100, int $offset = 0): array;
|
||||
public function getTableData(string $table, int $limit = 100, int $offset = 0, array $filters = []): array;
|
||||
|
||||
/**
|
||||
* Get the count of rows in a table.
|
||||
*/
|
||||
public function getTableCount(string $table): int;
|
||||
public function getTableCount(string $table, array $filters = []): int;
|
||||
|
||||
/**
|
||||
* Get the underlying connection instance.
|
||||
|
||||
@@ -68,15 +68,16 @@ class SchemaController extends Controller
|
||||
|
||||
$skip = $request->get('skip', 0);
|
||||
$take = $request->get('take', 100);
|
||||
$filters = json_decode($request->get('filters', '[]'), true);
|
||||
|
||||
$data = $this->databaseService->getTableData($table, $take, $skip);
|
||||
$data = $this->databaseService->getTableData($table, $take, $skip, $filters);
|
||||
|
||||
$response = [
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
if ($request->get('requireTotalCount') === 'true') {
|
||||
$response['totalCount'] = $this->databaseService->getTableCount($table);
|
||||
$response['totalCount'] = $this->databaseService->getTableCount($table, $filters);
|
||||
}
|
||||
|
||||
return Response::json($response);
|
||||
|
||||
@@ -64,15 +64,68 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
|
||||
return $this->query("DESCRIBE `{$table}`");
|
||||
}
|
||||
|
||||
public function getTableData(string $table, int $limit = 100, int $offset = 0): array
|
||||
public function getTableData(string $table, int $limit = 100, int $offset = 0, array $filters = []): array
|
||||
{
|
||||
return $this->query("SELECT * FROM `{$table}` LIMIT ? OFFSET ?", [$limit, $offset]);
|
||||
$query = DB::connection($this->connectionName)->table($table);
|
||||
$this->applyFilters($query, $filters);
|
||||
return $query->limit($limit)->offset($offset)->get()->all();
|
||||
}
|
||||
|
||||
public function getTableCount(string $table): int
|
||||
public function getTableCount(string $table, array $filters = []): int
|
||||
{
|
||||
$result = $this->query("SELECT COUNT(*) as count FROM `{$table}`");
|
||||
return (int) ($result[0]->count ?? 0);
|
||||
$query = DB::connection($this->connectionName)->table($table);
|
||||
$this->applyFilters($query, $filters);
|
||||
return $query->count();
|
||||
}
|
||||
|
||||
protected function applyFilters($query, array $filters)
|
||||
{
|
||||
if (empty($filters)) return $query;
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
$field = $filter['field'] ?? ($filter['columnField'] ?? null);
|
||||
$operator = $filter['operator'] ?? ($filter['operatorValue'] ?? null);
|
||||
$value = $filter['value'] ?? null;
|
||||
|
||||
if (!$field) continue;
|
||||
|
||||
switch ($operator) {
|
||||
case 'contains':
|
||||
$query->where($field, 'LIKE', "%{$value}%");
|
||||
break;
|
||||
case 'equals':
|
||||
case '=':
|
||||
$query->where($field, '=', $value);
|
||||
break;
|
||||
case 'startsWith':
|
||||
$query->where($field, 'LIKE', "{$value}%");
|
||||
break;
|
||||
case 'endsWith':
|
||||
$query->where($field, 'LIKE', "%{$value}");
|
||||
break;
|
||||
case 'isEmpty':
|
||||
$query->where(function($q) use ($field) {
|
||||
$q->whereNull($field)->orWhere($field, '');
|
||||
});
|
||||
break;
|
||||
case 'isNotEmpty':
|
||||
$query->whereNotNull($field)->where($field, '!=', '');
|
||||
break;
|
||||
case 'isAnyOf':
|
||||
if (is_array($value)) {
|
||||
$query->whereIn($field, $value);
|
||||
}
|
||||
break;
|
||||
case '>':
|
||||
case '<':
|
||||
case '>=':
|
||||
case '<=':
|
||||
case '!=':
|
||||
$query->where($field, $operator, $value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function getForeignKeys(string $table): array
|
||||
|
||||
@@ -78,17 +78,17 @@ class DatabaseService
|
||||
/**
|
||||
* Get table data.
|
||||
*/
|
||||
public function getTableData(string $table, int $limit = 100, int $offset = 0): array
|
||||
public function getTableData(string $table, int $limit = 100, int $offset = 0, array $filters = []): array
|
||||
{
|
||||
return $this->getDriver()->getTableData($table, $limit, $offset);
|
||||
return $this->getDriver()->getTableData($table, $limit, $offset, $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get table row count.
|
||||
*/
|
||||
public function getTableCount(string $table): int
|
||||
public function getTableCount(string $table, array $filters = []): int
|
||||
{
|
||||
return $this->getDriver()->getTableCount($table);
|
||||
return $this->getDriver()->getTableCount($table, $filters);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,13 @@ import {
|
||||
Save,
|
||||
Undo
|
||||
} from '@mui/icons-material';
|
||||
import { DataGrid, useGridApiRef, type GridPaginationModel, type GridRowModel } from '@mui/x-data-grid';
|
||||
import {
|
||||
DataGrid,
|
||||
useGridApiRef,
|
||||
type GridPaginationModel,
|
||||
type GridRowModel,
|
||||
type GridFilterModel
|
||||
} from '@mui/x-data-grid';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import { SchemaService } from '../services/api';
|
||||
import TransferContent from './TransferContent';
|
||||
@@ -39,6 +45,7 @@ const MainContent: React.FC = () => {
|
||||
page: 0,
|
||||
pageSize: 100,
|
||||
});
|
||||
const [filterModel, setFilterModel] = useState<GridFilterModel>({ items: [] });
|
||||
|
||||
const [notification, setNotification] = useState<MainNotification>({
|
||||
open: false,
|
||||
@@ -58,7 +65,7 @@ const MainContent: React.FC = () => {
|
||||
const [selectionEnd, setSelectionEnd] = useState<{ id: string | number, field: string } | null>(null);
|
||||
|
||||
const { columns, loading: loadingSchema, primaryKey } = useTableSchema(activeTable, activeDatabase);
|
||||
const { rows, rowCount, loading: loadingData, refetch } = useTableData(activeTable, activeDatabase, paginationModel);
|
||||
const { rows, rowCount, loading: loadingData, refetch } = useTableData(activeTable, activeDatabase, paginationModel, filterModel);
|
||||
|
||||
const handleRowUpdate = (newRow: GridRowModel, oldRow: GridRowModel) => {
|
||||
const hasChanged = Object.keys(newRow).some(key => newRow[key] !== oldRow[key]);
|
||||
@@ -101,6 +108,51 @@ const MainContent: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = async (format: 'sql' | 'csv') => {
|
||||
if (!activeTable || !activeDatabase) return;
|
||||
try {
|
||||
setNotification({ open: true, title: 'Exporting', message: 'Generating export file...', severity: 'success' });
|
||||
|
||||
// For now, we'll use the existing SQL export for 'sql'
|
||||
// and implement a client-side CSV export for 'csv' since we have the rows
|
||||
if (format === 'sql') {
|
||||
const response = await SchemaService.exportDatabase(activeDatabase, activeTable);
|
||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${activeTable}_${new Date().toISOString().split('T')[0]}.sql`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
} else {
|
||||
// Simple CSV Export
|
||||
const headers = columns.map(c => c.headerName || c.field).join(',');
|
||||
const csvRows = rows.map(row =>
|
||||
columns.map(c => {
|
||||
const val = row[c.field];
|
||||
return typeof val === 'string' ? `"${val.replace(/"/g, '""')}"` : val;
|
||||
}).join(',')
|
||||
);
|
||||
const csvContent = [headers, ...csvRows].join('\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.setAttribute('download', `${activeTable}_export.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
} catch (error: any) {
|
||||
setNotification({
|
||||
open: true,
|
||||
title: 'Export Failed',
|
||||
message: error.message,
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handlePaste = useCallback((event: React.ClipboardEvent) => {
|
||||
if (!cellFocus || !activeTable) return;
|
||||
|
||||
@@ -385,6 +437,10 @@ const MainContent: React.FC = () => {
|
||||
<Box sx={{ px: 1.5, py: 0.5, borderRadius: 10, bgcolor: 'rgba(0, 97, 255, 0.1)', color: 'primary.main' }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 700 }}>{rowCount} rows found</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button size="small" variant="outlined" startIcon={<CloudDownload />} onClick={() => handleExport('sql')} sx={{ borderRadius: 2, fontSize: '0.7rem' }}>SQL Export</Button>
|
||||
<Button size="small" variant="outlined" startIcon={<TableChart />} onClick={() => handleExport('csv')} sx={{ borderRadius: 2, fontSize: '0.7rem' }}>Excel/CSV</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{Object.keys(pendingChanges).length > 0 && (
|
||||
@@ -444,6 +500,9 @@ const MainContent: React.FC = () => {
|
||||
paginationMode="server"
|
||||
paginationModel={paginationModel}
|
||||
onPaginationModelChange={setPaginationModel}
|
||||
filterMode="server"
|
||||
filterModel={filterModel}
|
||||
onFilterModelChange={setFilterModel}
|
||||
pageSizeOptions={[25, 50, 100]}
|
||||
processRowUpdate={handleRowUpdate}
|
||||
onProcessRowUpdateError={(error) => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import type { GridPaginationModel } from '@mui/x-data-grid';
|
||||
import type { GridPaginationModel, GridFilterModel } from '@mui/x-data-grid';
|
||||
import { SchemaService } from '../services/api';
|
||||
|
||||
export const useTableData = (
|
||||
activeTable: string | null,
|
||||
activeDatabase: string | null,
|
||||
paginationModel: GridPaginationModel
|
||||
paginationModel: GridPaginationModel,
|
||||
filterModel?: GridFilterModel
|
||||
) => {
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [rowCount, setRowCount] = useState(0);
|
||||
@@ -16,13 +17,17 @@ export const useTableData = (
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = {
|
||||
const params: any = {
|
||||
skip: paginationModel.page * paginationModel.pageSize,
|
||||
take: paginationModel.pageSize,
|
||||
requireTotalCount: true,
|
||||
database: activeDatabase,
|
||||
};
|
||||
|
||||
if (filterModel && filterModel.items.length > 0) {
|
||||
params.filters = JSON.stringify(filterModel.items);
|
||||
}
|
||||
|
||||
const response = await SchemaService.getTableData(activeTable, params);
|
||||
|
||||
const dataWithIds = response.data.data.map((row: any, index: number) => ({
|
||||
@@ -37,7 +42,7 @@ export const useTableData = (
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [activeTable, activeDatabase, paginationModel]);
|
||||
}, [activeTable, activeDatabase, paginationModel, filterModel]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
|
||||
Reference in New Issue
Block a user