Add master seed data export and auto-apply during setup install - idempotent REPLACE mode prevents duplicates
This commit is contained in:
parent
67fbc68c1c
commit
ee3193ce43
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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<string, mixed>
|
||||
*/
|
||||
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<int, string>
|
||||
*/
|
||||
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<string, mixed>
|
||||
*/
|
||||
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);
|
||||
|
|
|
|||
5
app/Database/master_seed_data.json
Normal file
5
app/Database/master_seed_data.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"generated_at": "",
|
||||
"source": "manual",
|
||||
"tables": {}
|
||||
}
|
||||
|
|
@ -62,6 +62,21 @@
|
|||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<form method="post" action="<?= base_url('setup/db-tools/export-master-seed') ?>" style="margin-bottom:12px;">
|
||||
<?= csrf_field() ?>
|
||||
<?php if (!empty($setup_key ?? '')) : ?>
|
||||
<input type="hidden" name="key" value="<?= esc($setup_key) ?>">
|
||||
<?php endif; ?>
|
||||
<button type="submit" class="btn btn-success">Export Master Seed Data (from current DB)</button>
|
||||
<span class="text-muted" style="margin-left:10px;">File: <?= esc($master_seed_file ?? '') ?></span>
|
||||
<?php if (!empty($master_seed_exists ?? false)) : ?>
|
||||
<span class="label label-success" style="margin-left:6px;">Found</span>
|
||||
<?php else : ?>
|
||||
<span class="label label-warning" style="margin-left:6px;">Not Found</span>
|
||||
<?php endif; ?>
|
||||
<div class="text-muted" style="margin-top:4px;">Uses .env key <code>setup.seed.export_tables</code> (comma-separated table names).</div>
|
||||
</form>
|
||||
|
||||
<form method="post" action="<?= base_url('setup/db-tools/schema-sync') ?>">
|
||||
<?= csrf_field() ?>
|
||||
<?php if (!empty($setup_key ?? '')) : ?>
|
||||
|
|
@ -145,6 +160,24 @@
|
|||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (array_key_exists('seed', (array) $sync_result)) : ?>
|
||||
<?php $seedResult = is_array($sync_result['seed'] ?? null) ? $sync_result['seed'] : []; ?>
|
||||
<?php if (!empty($seedResult['errors'] ?? [])) : ?>
|
||||
<div class="alert alert-danger">
|
||||
<strong>Seed Apply Errors:</strong>
|
||||
<?php foreach (($seedResult['errors'] ?? []) as $seedErr) : ?>
|
||||
<div><?= esc($seedErr) ?></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="alert alert-success">
|
||||
Seed data applied successfully.
|
||||
Rows upserted: <?= esc((string) ($seedResult['applied'] ?? 0)) ?>,
|
||||
tables skipped: <?= esc((string) ($seedResult['skipped'] ?? 0)) ?>.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Generated SQL</label>
|
||||
<textarea class="form-control" rows="14" readonly><?php
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user