From ee3193ce4329da4b450a9dbe353a7b568a3363aa Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Wed, 18 Mar 2026 04:41:51 +0530 Subject: [PATCH] Add master seed data export and auto-apply during setup install - idempotent REPLACE mode prevents duplicates --- app/Config/Routes.php | 1 + app/Controllers/Setup.php | 228 +++++++++++++++++++++++++++++ app/Database/master_seed_data.json | 5 + app/Views/setup/db_setup.php | 33 +++++ 4 files changed, 267 insertions(+) create mode 100644 app/Database/master_seed_data.json diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 0db3d1b..505ab4a 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -313,6 +313,7 @@ $routes->get('Ipd_discharge/show_file3/(:num)', 'Ipd_discharge::show_file3/$1'); $routes->get('setup/db-tools', 'Setup::dbTools'); $routes->post('setup/db-tools/generate', 'Setup::generateMigrations'); $routes->post('setup/db-tools/export-master-schema', 'Setup::exportMasterSchema'); +$routes->post('setup/db-tools/export-master-seed', 'Setup::exportMasterSeedData'); $routes->post('setup/db-tools/schema-sync', 'Setup::schemaSync'); $routes->post('setup/db-tools/schema-sync-step', 'Setup::schemaSyncStep'); $routes->post('setup/db-tools/repair-auth-schema', 'Setup::repairAuthSchema'); diff --git a/app/Controllers/Setup.php b/app/Controllers/Setup.php index 59fc0ac..73d2579 100644 --- a/app/Controllers/Setup.php +++ b/app/Controllers/Setup.php @@ -30,6 +30,20 @@ class Setup extends BaseController return APPPATH . 'Database/master_schema.json'; } + private function masterSeedFilePath(): string + { + $fromEnv = trim((string) env('setup.seed.file', '')); + if ($fromEnv !== '') { + if (str_starts_with($fromEnv, '/') || preg_match('/^[A-Za-z]:[\\\/]/', $fromEnv) === 1) { + return $fromEnv; + } + + return ROOTPATH . ltrim(str_replace(['\\'], '/', $fromEnv), '/'); + } + + return APPPATH . 'Database/master_seed_data.json'; + } + private function isSetupLocked(): bool { return is_file($this->lockFilePath()); @@ -181,6 +195,16 @@ class Setup extends BaseController $syncResult['apply_truncated'] = $applyResult['truncated']; } + if ($action === 'apply') { + $canRunSeed = empty($syncResult['errors']) + && empty($syncResult['apply_errors'] ?? []) + && empty($syncResult['apply_truncated'] ?? false); + + if ($canRunSeed) { + $syncResult['seed'] = $this->applySeedDataToClientDb(); + } + } + return $syncResult; } catch (\Throwable $e) { log_message('error', 'Setup schemaSync failed: {message}', ['message' => $e->getMessage()]); @@ -223,6 +247,56 @@ class Setup extends BaseController return redirect()->to($this->dbToolsUrlWithKey()); } + public function exportMasterSeedData() + { + if ($deny = $this->ensureSetupAccess()) { + return $deny; + } + + $tables = $this->parseSeedTableList(); + if (empty($tables)) { + session()->setFlashdata('setup_msg', 'No seed tables configured. Set setup.seed.export_tables in .env (comma-separated table names).'); + return redirect()->to($this->dbToolsUrlWithKey()); + } + + $seedPayload = [ + 'generated_at' => date('Y-m-d H:i:s'), + 'source' => 'local-current-db', + 'tables' => [], + ]; + + foreach ($tables as $tableName) { + if (!$this->tableExists($tableName)) { + $seedPayload['tables'][$tableName] = []; + continue; + } + + try { + $rows = $this->db->table($tableName)->get()->getResultArray(); + $seedPayload['tables'][$tableName] = is_array($rows) ? $rows : []; + } catch (\Throwable $e) { + log_message('error', 'Seed export table read failed for {table}: {message}', ['table' => $tableName, 'message' => $e->getMessage()]); + $seedPayload['tables'][$tableName] = []; + } + } + + $file = $this->masterSeedFilePath(); + $dir = dirname($file); + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + session()->setFlashdata('setup_msg', 'Unable to create seed file directory.'); + return redirect()->to($this->dbToolsUrlWithKey()); + } + + $json = json_encode($seedPayload, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + if (!is_string($json) || @file_put_contents($file, $json) === false) { + session()->setFlashdata('setup_msg', 'Unable to write seed data file.'); + return redirect()->to($this->dbToolsUrlWithKey()); + } + + session()->setFlashdata('setup_msg', 'Master seed data exported: ' . $file . ' (' . count($tables) . ' table(s)).'); + return redirect()->to($this->dbToolsUrlWithKey()); + } + public function generateMigrations() { if ($deny = $this->ensureSetupAccess()) { @@ -515,6 +589,8 @@ class Setup extends BaseController 'sync_client_database' => (string) env('setup.sync.client.database', ''), 'master_schema_file' => $this->masterSchemaFilePath(), 'master_schema_exists' => is_file($this->masterSchemaFilePath()), + 'master_seed_file' => $this->masterSeedFilePath(), + 'master_seed_exists' => is_file($this->masterSeedFilePath()), 'diagnostics' => $this->collectSetupDiagnostics($dbError), ]; } @@ -950,6 +1026,143 @@ class Setup extends BaseController ]; } + /** + * @return array + */ + private function loadMasterSeedDefinition(): array + { + $file = $this->masterSeedFilePath(); + if (!is_file($file)) { + return [ + 'errors' => ['Master seed data file not found: ' . $file], + 'tables' => [], + 'source' => 'master-seed-file', + ]; + } + + $json = @file_get_contents($file); + $decoded = is_string($json) ? json_decode($json, true) : null; + if (!is_array($decoded) || !is_array($decoded['tables'] ?? null)) { + return [ + 'errors' => ['Invalid master seed data file format: ' . $file], + 'tables' => [], + 'source' => 'master-seed-file', + ]; + } + + return [ + 'errors' => [], + 'tables' => $decoded['tables'], + 'source' => 'master-seed-file', + ]; + } + + /** + * @return array + */ + private function parseSeedTableList(): array + { + $raw = trim((string) env('setup.seed.export_tables', '')); + if ($raw === '') { + return []; + } + + $parts = preg_split('/[\s,]+/', $raw) ?: []; + $out = []; + foreach ($parts as $part) { + $name = strtolower(trim((string) $part)); + $name = preg_replace('/[^a-z0-9_]/', '', $name) ?? ''; + if ($name !== '') { + $out[$name] = true; + } + } + + return array_keys($out); + } + + /** + * @return array + */ + private function applySeedDataToClientDb(): array + { + $definition = $this->loadMasterSeedDefinition(); + if (!empty($definition['errors'])) { + return [ + 'applied' => 0, + 'skipped' => 0, + 'errors' => (array) $definition['errors'], + 'tables' => [], + ]; + } + + $tables = is_array($definition['tables'] ?? null) ? $definition['tables'] : []; + if (empty($tables)) { + return [ + 'applied' => 0, + 'skipped' => 0, + 'errors' => [], + 'tables' => [], + ]; + } + + $client = $this->resolveClientConnection(); + $applied = 0; + $skipped = 0; + $errors = []; + $tableStats = []; + + foreach ($tables as $tableName => $rows) { + $table = strtolower(trim((string) $tableName)); + if ($table === '') { + continue; + } + + $safeTable = preg_replace('/[^a-z0-9_]/', '', $table) ?? ''; + if ($safeTable === '') { + continue; + } + + if (!$this->tableExistsInConnection($client, $safeTable)) { + $skipped++; + $tableStats[] = $safeTable . ' (skipped: table missing)'; + continue; + } + + if (!is_array($rows)) { + $skipped++; + $tableStats[] = $safeTable . ' (skipped: invalid row list)'; + continue; + } + + $tableApplied = 0; + foreach ($rows as $row) { + if (!is_array($row) || empty($row)) { + continue; + } + + try { + // REPLACE provides idempotent seed behavior: on re-run, rows with existing + // PRIMARY or UNIQUE keys are updated instead of duplicated. + // This ensures seed data can be safely applied multiple times. + $client->table($safeTable)->replace($row); + $applied++; + $tableApplied++; + } catch (\Throwable $e) { + $errors[] = $safeTable . ': ' . $e->getMessage(); + } + } + + $tableStats[] = $safeTable . ' (rows: ' . $tableApplied . ')'; + } + + return [ + 'applied' => $applied, + 'skipped' => $skipped, + 'errors' => $errors, + 'tables' => $tableStats, + ]; + } + private function hasSyncRoleConfig(string $role): bool { $role = strtolower(trim($role)); @@ -2269,6 +2482,21 @@ class Setup extends BaseController } } + private function tableExistsInConnection($db, string $tableName): bool + { + $tableName = trim($tableName); + if ($tableName === '') { + return false; + } + + try { + $tables = $db->listTables(); + return in_array($tableName, $tables, true); + } catch (\Throwable $e) { + return false; + } + } + private function columnExists(string $tableName, string $columnName): bool { $tableName = trim($tableName); diff --git a/app/Database/master_seed_data.json b/app/Database/master_seed_data.json new file mode 100644 index 0000000..0dd05ab --- /dev/null +++ b/app/Database/master_seed_data.json @@ -0,0 +1,5 @@ +{ + "generated_at": "", + "source": "manual", + "tables": {} +} diff --git a/app/Views/setup/db_setup.php b/app/Views/setup/db_setup.php index d9af15a..1d29d3f 100644 --- a/app/Views/setup/db_setup.php +++ b/app/Views/setup/db_setup.php @@ -62,6 +62,21 @@ +
+ + + + + + File: + + Found + + Not Found + +
Uses .env key setup.seed.export_tables (comma-separated table names).
+
+
@@ -145,6 +160,24 @@ + + + +
+ Seed Apply Errors: + +
+ +
+ +
+ Seed data applied successfully. + Rows upserted: , + tables skipped: . +
+ + +