first commit

This commit is contained in:
brusnitsyn
2026-03-23 00:51:38 +09:00
commit 07854e0a9d
110 changed files with 19528 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers;
use App\Models\SourceDatabase;
use App\Models\TargetDatabase;
use App\Models\Table;
use App\Models\MigrationSchedule;
use App\Models\MigrationRun;
use Inertia\Inertia;
class DashboardController extends Controller
{
public function index()
{
$stats = [
'source_databases' => SourceDatabase::count(),
'target_databases' => TargetDatabase::count(),
'tables' => Table::count(),
'schedules' => MigrationSchedule::where('is_active', true)->count(),
];
$recentMigrations = MigrationRun::with('schedule')
->orderBy('created_at', 'desc')
->limit(10)
->get()
->map(fn($run) => [
'id' => $run->id,
'schedule_name' => $run->schedule->name ?? 'N/A',
'status' => $run->status,
'started_at' => $run->started_at?->format('Y-m-d H:i:s'),
'completed_at' => $run->completed_at?->format('Y-m-d H:i:s'),
]);
return Inertia::render('Dashboard', [
'stats' => $stats,
'recentMigrations' => $recentMigrations,
]);
}
}

View File

@@ -0,0 +1,231 @@
<?php
namespace App\Http\Controllers;
use App\Models\MigrationSchedule;
use App\Models\SourceDatabase;
use App\Models\TargetDatabase;
use App\Models\Table;
use App\Models\MigrationRun;
use App\Jobs\RunMigrationJob;
use Illuminate\Http\Request;
use Inertia\Inertia;
class MigrationScheduleController extends Controller
{
public function index()
{
$schedules = MigrationSchedule::with(['sourceDatabase', 'targetDatabase'])
->withCount('migrationRuns')
->get();
return Inertia::render('Migrations/Index', [
'schedules' => $schedules,
]);
}
public function create()
{
$sourceDatabases = SourceDatabase::where('is_active', true)->get();
$targetDatabases = TargetDatabase::where('is_active', true)->get();
$tables = Table::with('sourceDatabase')->get()->groupBy('source_database_id');
return Inertia::render('Migrations/ScheduleForm', [
'schedule' => null,
'sourceDatabases' => $sourceDatabases,
'targetDatabases' => $targetDatabases,
'tables' => $tables,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'source_database_id' => 'required|exists:source_databases,id',
'target_database_id' => 'required|exists:target_databases,id',
'tables' => 'required|array|min:1',
'tables.*' => 'exists:tables,id',
'cron_expression' => 'required|string|max:100',
'timezone' => 'nullable|string|max:50',
'is_active' => 'boolean',
'run_in_parallel' => 'boolean',
'batch_size' => 'integer|min:1',
'truncate_before_migration' => 'boolean',
'create_indexes_after' => 'boolean',
'python_script_path' => 'nullable|string',
'description' => 'nullable|string',
]);
$validated['timezone'] = $validated['timezone'] ?? 'UTC';
$validated['is_active'] = $validated['is_active'] ?? true;
$validated['is_incremental'] = $validated['is_incremental'] ?? false;
$validated['incremental_column'] = $validated['is_incremental'] ? ($validated['incremental_column'] ?? 'updated_at') : null;
$validated['use_life_table'] = $validated['use_life_table'] ?? false;
if ($validated['use_life_table']) {
$validated['life_table_name'] = $validated['life_table_name'] ?? null;
$validated['life_id_column'] = $validated['life_id_column'] ?? null;
$validated['base_id_column'] = $validated['base_id_column'] ?? null;
$validated['operation_column'] = $validated['operation_column'] ?? 'x_Operation';
$validated['datetime_column'] = $validated['datetime_column'] ?? 'x_DateTime';
}
$validated['run_in_parallel'] = $validated['run_in_parallel'] ?? false;
$validated['batch_size'] = $validated['batch_size'] ?? 1000;
$validated['truncate_before_migration'] = $validated['truncate_before_migration'] ?? false;
$validated['create_indexes_after'] = $validated['create_indexes_after'] ?? true;
$validated['python_script_args'] = [
'incremental' => $validated['is_incremental'],
'incremental_column' => $validated['incremental_column'],
'use_life_table' => $validated['use_life_table'],
'life_table_name' => $validated['life_table_name'],
'life_id_column' => $validated['life_id_column'],
'base_id_column' => $validated['base_id_column'],
'operation_column' => $validated['operation_column'],
'datetime_column' => $validated['datetime_column'],
];
$schedule = MigrationSchedule::create($validated);
// Attach tables to schedule
$schedule->scheduledTables()->sync($validated['tables']);
return redirect()->route('migrations.index')
->with('success', 'Migration schedule created successfully.');
}
public function edit(MigrationSchedule $schedule)
{
$schedule->load('scheduledTables');
$sourceDatabases = SourceDatabase::where('is_active', true)->get();
$targetDatabases = TargetDatabase::where('is_active', true)->get();
$tables = Table::with('sourceDatabase')
->where('source_database_id', $schedule->source_database_id)
->get();
return Inertia::render('Migrations/ScheduleForm', [
'schedule' => $schedule,
'sourceDatabases' => $sourceDatabases,
'targetDatabases' => $targetDatabases,
'tables' => $tables,
]);
}
public function update(Request $request, MigrationSchedule $schedule)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'source_database_id' => 'required|exists:source_databases,id',
'target_database_id' => 'required|exists:target_databases,id',
'tables' => 'required|array|min:1',
'tables.*' => 'exists:tables,id',
'cron_expression' => 'required|string|max:100',
'timezone' => 'nullable|string|max:50',
'is_active' => 'boolean',
'is_incremental' => 'boolean',
'incremental_column' => 'nullable|string|max:100',
'use_life_table' => 'boolean',
'life_table_name' => 'nullable|string|max:255',
'life_id_column' => 'nullable|string|max:100',
'base_id_column' => 'nullable|string|max:100',
'operation_column' => 'nullable|string|max:100',
'datetime_column' => 'nullable|string|max:100',
'run_in_parallel' => 'boolean',
'batch_size' => 'integer|min:1',
'truncate_before_migration' => 'boolean',
'create_indexes_after' => 'boolean',
'python_script_path' => 'nullable|string',
'description' => 'nullable|string',
]);
$validated['is_incremental'] = $validated['is_incremental'] ?? false;
$validated['incremental_column'] = $validated['is_incremental'] ? ($validated['incremental_column'] ?? 'updated_at') : null;
$validated['use_life_table'] = $validated['use_life_table'] ?? false;
if ($validated['use_life_table']) {
$validated['life_table_name'] = $validated['life_table_name'] ?? null;
$validated['life_id_column'] = $validated['life_id_column'] ?? null;
$validated['base_id_column'] = $validated['base_id_column'] ?? null;
$validated['operation_column'] = $validated['operation_column'] ?? 'x_Operation';
$validated['datetime_column'] = $validated['datetime_column'] ?? 'x_DateTime';
}
$validated['python_script_args'] = [
'incremental' => $validated['is_incremental'],
'incremental_column' => $validated['incremental_column'],
'use_life_table' => $validated['use_life_table'],
'life_table_name' => $validated['life_table_name'],
'life_id_column' => $validated['life_id_column'],
'base_id_column' => $validated['base_id_column'],
'operation_column' => $validated['operation_column'],
'datetime_column' => $validated['datetime_column'],
];
$schedule->update($validated);
// Sync tables
$schedule->scheduledTables()->sync($validated['tables']);
return redirect()->route('migrations.index')
->with('success', 'Migration schedule updated successfully.');
}
public function destroy(MigrationSchedule $schedule)
{
$schedule->delete();
return redirect()->route('migrations.index')
->with('success', 'Migration schedule deleted successfully.');
}
public function runNow(MigrationSchedule $schedule)
{
// Check if there's already a running or pending migration for this schedule
$existingRun = MigrationRun::where('schedule_id', $schedule->id)
->whereIn('status', ['pending', 'running'])
->latest()
->first();
if ($existingRun) {
return redirect()->back()
->with('warning', 'Миграция уже выполняется или ожидает в очереди.');
}
$migrationRun = MigrationRun::create([
'schedule_id' => $schedule->id,
'status' => 'pending',
'total_tables' => count($schedule->tables),
]);
RunMigrationJob::dispatch($migrationRun)->onQueue('default');
return redirect()->back()
->with('success', 'Миграция запущена.');
}
public function toggle(MigrationSchedule $schedule)
{
$schedule->update(['is_active' => !$schedule->is_active]);
return redirect()->back()
->with('success', 'Schedule ' . ($schedule->is_active ? 'activated' : 'deactivated') . '.');
}
public function show(MigrationSchedule $schedule)
{
$schedule->load(['sourceDatabase', 'targetDatabase', 'scheduledTables']);
$runs = MigrationRun::where('schedule_id', $schedule->id)
->orderBy('created_at', 'desc')
->limit(50)
->get();
return Inertia::render('Migrations/Show', [
'schedule' => $schedule,
'runs' => $runs,
]);
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Http\Controllers;
use App\Models\SourceDatabase;
use App\Models\Table;
use App\Models\Column;
use App\Services\SchemaExtractionService;
use App\Services\SchemaComparisonService;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SchemaController extends Controller
{
public function __construct(
private SchemaExtractionService $extractionService,
private SchemaComparisonService $comparisonService
) {}
public function index()
{
$databases = SourceDatabase::with('tables')->get();
return Inertia::render('Schemas/Index', [
'databases' => $databases,
]);
}
public function show(SourceDatabase $database)
{
$database->load(['tables.columns', 'tables.indexes', 'tables.foreignKeys']);
return Inertia::render('Schemas/Show', [
'database' => $database,
]);
}
public function sync(SourceDatabase $database)
{
try {
$this->extractionService->saveSchema($database);
return redirect()->back()
->with('success', 'Schema synchronized successfully.');
} catch (\Exception $e) {
return redirect()->back()
->with('error', 'Failed to sync schema: ' . $e->getMessage());
}
}
public function checkChanges(SourceDatabase $database)
{
$changes = $this->comparisonService->checkForChanges($database);
return response()->json([
'success' => true,
'changes' => $changes,
]);
}
public function tableDetail(Table $table)
{
$table->load(['columns', 'indexes', 'foreignKeys', 'sourceDatabase']);
return Inertia::render('Schemas/TableDetail', [
'table' => $table,
]);
}
public function updateColumn(Request $request, Column $column)
{
$validated = $request->validate([
'target_column_name' => 'nullable|string|max:255',
'target_data_type' => 'nullable|string|max:100',
'exclude_from_migration' => 'boolean',
]);
$column->update($validated);
return redirect()->back()
->with('success', 'Column settings updated successfully.');
}
public function pendingChanges()
{
$changes = $this->comparisonService->getPendingChanges();
return Inertia::render('Schemas/PendingChanges', [
'changes' => $changes,
]);
}
public function applyChange(int $changeId)
{
$this->comparisonService->markAsApplied($changeId);
return redirect()->back()
->with('success', 'Schema change marked as applied.');
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers;
use App\Models\SourceDatabase;
use App\Services\DatabaseConnectionService;
use App\Services\SchemaExtractionService;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SourceDatabaseController extends Controller
{
public function __construct(
private DatabaseConnectionService $connectionService,
private SchemaExtractionService $extractionService
) {}
public function index()
{
$databases = SourceDatabase::withCount('tables')->get();
return Inertia::render('Databases/SourceDatabases', [
'databases' => $databases,
]);
}
public function create()
{
return Inertia::render('Databases/SourceDatabaseForm', [
'database' => null,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'host' => 'required|string|max:255',
'port' => 'required|integer|between:1,65535',
'database' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'required|string',
'driver' => 'nullable|string|max:50',
'description' => 'nullable|string',
]);
$validated['driver'] = $validated['driver'] ?? 'pgsql';
$database = SourceDatabase::create($validated);
return redirect()->route('databases.source.index')
->with('success', 'Source database created successfully.');
}
public function edit(SourceDatabase $database)
{
return Inertia::render('Databases/SourceDatabaseForm', [
'database' => $database,
]);
}
public function update(Request $request, SourceDatabase $database)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'host' => 'required|string|max:255',
'port' => 'required|integer|between:1,65535',
'database' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'nullable|string',
'driver' => 'nullable|string|max:50',
'description' => 'nullable|string',
]);
if (empty($validated['password'])) {
unset($validated['password']);
}
$database->update($validated);
return redirect()->route('databases.source.index')
->with('success', 'Source database updated successfully.');
}
public function destroy(SourceDatabase $database)
{
$database->delete();
return redirect()->route('databases.source.index')
->with('success', 'Source database deleted successfully.');
}
public function testConnection(SourceDatabase $database)
{
$result = $this->connectionService->testSourceConnection($database);
return response()->json($result);
}
public function sync(SourceDatabase $database)
{
try {
$this->extractionService->saveSchema($database);
return redirect()->back()
->with('success', 'Schema synchronized successfully.');
} catch (\Exception $e) {
return redirect()->back()
->with('error', 'Failed to sync schema: ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers;
use App\Models\TargetDatabase;
use App\Services\DatabaseConnectionService;
use Illuminate\Http\Request;
use Inertia\Inertia;
class TargetDatabaseController extends Controller
{
public function __construct(
private DatabaseConnectionService $connectionService
) {}
public function index()
{
$databases = TargetDatabase::withCount('migrationSchedules')->get();
return Inertia::render('Databases/TargetDatabases', [
'databases' => $databases,
]);
}
public function create()
{
return Inertia::render('Databases/TargetDatabaseForm', [
'database' => null,
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'host' => 'required|string|max:255',
'port' => 'required|integer|between:1,65535',
'database' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'required|string',
'driver' => 'nullable|string|max:50',
'description' => 'nullable|string',
]);
$validated['driver'] = $validated['driver'] ?? 'pgsql';
$database = TargetDatabase::create($validated);
return redirect()->route('databases.target.index')
->with('success', 'Target database created successfully.');
}
public function edit(TargetDatabase $database)
{
return Inertia::render('Databases/TargetDatabaseForm', [
'database' => $database,
]);
}
public function update(Request $request, TargetDatabase $database)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'host' => 'required|string|max:255',
'port' => 'required|integer|between:1,65535',
'database' => 'required|string|max:255',
'username' => 'required|string|max:255',
'password' => 'nullable|string',
'driver' => 'nullable|string|max:50',
'description' => 'nullable|string',
]);
if (empty($validated['password'])) {
unset($validated['password']);
}
$database->update($validated);
return redirect()->route('databases.target.index')
->with('success', 'Target database updated successfully.');
}
public function destroy(TargetDatabase $database)
{
$database->delete();
return redirect()->route('databases.target.index')
->with('success', 'Target database deleted successfully.');
}
public function testConnection(TargetDatabase $database)
{
$result = $this->connectionService->testTargetConnection($database);
return response()->json($result);
}
}