M3 Integration fixes
This commit is contained in:
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Executable
+291
@@ -0,0 +1,291 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Exceptions\DatabaseStageException;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
|
||||
class DatabaseStage extends Command
|
||||
{
|
||||
protected $signature = 'db:stage
|
||||
{--force : Skip the confirmation prompt}
|
||||
{--keep : Keep the local dump after completion}';
|
||||
|
||||
protected $description = 'Clone the complete local PostgreSQL database to the stage server';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$local = $this->localConfiguration();
|
||||
$stage = $this->stageConfiguration();
|
||||
$exitCode = self::SUCCESS;
|
||||
|
||||
if (! $this->validateStageConfiguration($stage)) {
|
||||
$exitCode = self::FAILURE;
|
||||
} else {
|
||||
[$localDump, $remoteDump] = $this->dumpPaths();
|
||||
$this->displayCloneSummary($local, $stage);
|
||||
|
||||
if (! $this->confirmed($stage)) {
|
||||
$this->warn('Cancelled.');
|
||||
} else {
|
||||
try {
|
||||
$this->runClone($local, $stage, $localDump, $remoteDump);
|
||||
$this->reportSuccess($local, $stage);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->reportFailure($exception, $localDump, $remoteDump);
|
||||
$exitCode = self::FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $exitCode;
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function localConfiguration(): array
|
||||
{
|
||||
return [
|
||||
'host' => config('database.connections.pgsql.host'),
|
||||
'port' => config('database.connections.pgsql.port'),
|
||||
'database' => config('database.connections.pgsql.database'),
|
||||
'username' => config('database.connections.pgsql.username'),
|
||||
'password' => config('database.connections.pgsql.password'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
private function stageConfiguration(): array
|
||||
{
|
||||
return [
|
||||
'ssh_host' => env('STAGE_SSH_HOST'),
|
||||
'ssh_user' => env('STAGE_SSH_USER'),
|
||||
'ssh_port' => env('STAGE_SSH_PORT', 22),
|
||||
'db_host' => env('STAGE_DB_HOST', '127.0.0.1'),
|
||||
'db_port' => env('STAGE_DB_PORT', 5432),
|
||||
'database' => env('STAGE_DB_DATABASE'),
|
||||
'username' => env('STAGE_DB_USERNAME'),
|
||||
'password' => env('STAGE_DB_PASSWORD'),
|
||||
'admin_username' => env('STAGE_DB_ADMIN_USERNAME', 'postgres'),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function validateStageConfiguration(array $stage): bool
|
||||
{
|
||||
$required = [
|
||||
'STAGE_SSH_HOST' => $stage['ssh_host'],
|
||||
'STAGE_SSH_USER' => $stage['ssh_user'],
|
||||
'STAGE_DB_DATABASE' => $stage['database'],
|
||||
'STAGE_DB_USERNAME' => $stage['username'],
|
||||
];
|
||||
|
||||
foreach ($required as $name => $value) {
|
||||
if (! $value) {
|
||||
$this->error("Missing {$name}.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @return array{string, string} */
|
||||
private function dumpPaths(): array
|
||||
{
|
||||
$filename = 'wms-' . now()->format('Ymd-His') . '.dump';
|
||||
|
||||
return [storage_path("app/{$filename}"), "/tmp/{$filename}"];
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local @param array<string, mixed> $stage */
|
||||
private function displayCloneSummary(array $local, array $stage): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->components->info('WMS PostgreSQL FULL CLONE → Stage');
|
||||
$this->newLine();
|
||||
$this->table(['Setting', 'Value'], [
|
||||
['Local database', $local['database']],
|
||||
['Stage server', "{$stage['ssh_user']}@{$stage['ssh_host']}"],
|
||||
['Stage database', $stage['database']],
|
||||
['Stage owner', $stage['username']],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function confirmed(array $stage): bool
|
||||
{
|
||||
return $this->option('force') || $this->confirm(
|
||||
"This will DROP and completely RECREATE '{$stage['database']}'. Continue?",
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local @param array<string, mixed> $stage */
|
||||
private function runClone(array $local, array $stage, string $localDump, string $remoteDump): void
|
||||
{
|
||||
$this->ensureDumpDirectory($localDump);
|
||||
$this->createDump($local, $localDump);
|
||||
$this->line(' Dump size: ' . number_format((int) filesize($localDump) / 1048576, 2) . ' MB');
|
||||
$this->verifyDump($localDump);
|
||||
$this->uploadDump($stage, $localDump, $remoteDump);
|
||||
$this->recreateStageDatabase($stage);
|
||||
$this->restoreStageDatabase($stage, $remoteDump);
|
||||
$this->verifyStageDatabase($stage);
|
||||
$this->cleanupDumps($stage, $localDump, $remoteDump);
|
||||
}
|
||||
|
||||
private function ensureDumpDirectory(string $localDump): void
|
||||
{
|
||||
$directory = dirname($localDump);
|
||||
|
||||
if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) {
|
||||
throw new DatabaseStageException("Unable to create dump directory: {$directory}");
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local */
|
||||
private function createDump(array $local, string $localDump): void
|
||||
{
|
||||
$this->components->task('Backing up complete local PostgreSQL database', function () use ($local, $localDump): bool {
|
||||
$result = Process::env(['PGPASSWORD' => $local['password']])->timeout(3600)->run([
|
||||
'pg_dump', '--host=' . $local['host'], '--port=' . $local['port'],
|
||||
'--username=' . $local['username'], '--format=custom', '--compress=9',
|
||||
'--no-owner', '--no-acl', '--file=' . $localDump, $local['database'],
|
||||
]);
|
||||
$this->throwWhenFailed($result->failed(), "pg_dump failed:\n" . $result->errorOutput());
|
||||
|
||||
if (! file_exists($localDump) || filesize($localDump) === 0) {
|
||||
throw new DatabaseStageException('Database dump was not created or is empty.');
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private function verifyDump(string $localDump): void
|
||||
{
|
||||
$this->components->task('Verifying PostgreSQL archive', function () use ($localDump): bool {
|
||||
$result = Process::run(['pg_restore', '--list', $localDump]);
|
||||
$this->throwWhenFailed($result->failed(), "Invalid PostgreSQL archive:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function uploadDump(array $stage, string $localDump, string $remoteDump): void
|
||||
{
|
||||
$this->components->task('Uploading database backup', function () use ($stage, $localDump, $remoteDump): bool {
|
||||
$result = Process::timeout(3600)->run([
|
||||
'scp', '-P', (string) $stage['ssh_port'], $localDump,
|
||||
"{$stage['ssh_user']}@{$stage['ssh_host']}:{$remoteDump}",
|
||||
]);
|
||||
$this->throwWhenFailed($result->failed(), "Upload failed:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function recreateStageDatabase(array $stage): void
|
||||
{
|
||||
$this->components->task('Recreating stage database', function () use ($stage): bool {
|
||||
$database = '"' . str_replace('"', '""', (string) $stage['database']) . '"';
|
||||
$owner = '"' . str_replace('"', '""', (string) $stage['username']) . '"';
|
||||
$terminateDatabase = str_replace("'", "''", (string) $stage['database']);
|
||||
$sql = "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{$terminateDatabase}' AND pid <> pg_backend_pid();\n"
|
||||
. "DROP DATABASE IF EXISTS {$database};\nCREATE DATABASE {$database} WITH OWNER {$owner} ENCODING 'UTF8' TEMPLATE template0;";
|
||||
$remoteCommand = sprintf(
|
||||
'sudo -u %s psql --set ON_ERROR_STOP=on --dbname=postgres',
|
||||
escapeshellarg((string) $stage['admin_username']),
|
||||
);
|
||||
$result = Process::input($sql)->timeout(300)->run($this->sshCommand($stage, $remoteCommand));
|
||||
$this->throwWhenFailed($result->failed(), "Unable to recreate stage database:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function restoreStageDatabase(array $stage, string $remoteDump): void
|
||||
{
|
||||
$this->components->task('Restoring complete stage database', function () use ($stage, $remoteDump): bool {
|
||||
$restore = sprintf(
|
||||
'PGPASSWORD=%s pg_restore --host=%s --port=%s --username=%s --dbname=%s --no-owner --no-acl --exit-on-error --verbose %s',
|
||||
escapeshellarg((string) $stage['password']),
|
||||
escapeshellarg((string) $stage['db_host']),
|
||||
escapeshellarg((string) $stage['db_port']),
|
||||
escapeshellarg((string) $stage['username']),
|
||||
escapeshellarg((string) $stage['database']),
|
||||
escapeshellarg($remoteDump),
|
||||
);
|
||||
$result = Process::timeout(3600)->run($this->sshCommand($stage, $restore));
|
||||
$this->throwWhenFailed($result->failed(), "Database restore failed:\n" . $result->errorOutput());
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function verifyStageDatabase(array $stage): void
|
||||
{
|
||||
$this->components->task('Verifying restored database', function () use ($stage): bool {
|
||||
$command = sprintf(
|
||||
'PGPASSWORD=%s psql --host=%s --port=%s --username=%s --dbname=%s --tuples-only --command=%s',
|
||||
escapeshellarg((string) $stage['password']),
|
||||
escapeshellarg((string) $stage['db_host']),
|
||||
escapeshellarg((string) $stage['db_port']),
|
||||
escapeshellarg((string) $stage['username']),
|
||||
escapeshellarg((string) $stage['database']),
|
||||
escapeshellarg("SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'public';"),
|
||||
);
|
||||
$result = Process::run($this->sshCommand($stage, $command));
|
||||
$this->throwWhenFailed($result->failed(), "Database verification failed:\n" . $result->errorOutput());
|
||||
$this->line(' Stage public tables: ' . trim($result->output()));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage */
|
||||
private function cleanupDumps(array $stage, string $localDump, string $remoteDump): void
|
||||
{
|
||||
Process::run($this->sshCommand($stage, 'rm -f ' . escapeshellarg($remoteDump)));
|
||||
|
||||
if (! $this->option('keep')) {
|
||||
@unlink($localDump);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $stage @return list<string> */
|
||||
private function sshCommand(array $stage, string $command): array
|
||||
{
|
||||
return ['ssh', '-p', (string) $stage['ssh_port'], "{$stage['ssh_user']}@{$stage['ssh_host']}", $command];
|
||||
}
|
||||
|
||||
private function throwWhenFailed(bool $failed, string $message): void
|
||||
{
|
||||
if ($failed) {
|
||||
throw new DatabaseStageException($message);
|
||||
}
|
||||
}
|
||||
|
||||
private function reportFailure(\Throwable $exception, string $localDump, string $remoteDump): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->components->error('Database clone failed.');
|
||||
$this->error($exception->getMessage());
|
||||
$this->newLine();
|
||||
$this->warn("The dump may still exist at {$localDump} and/or {$remoteDump}.");
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $local @param array<string, mixed> $stage */
|
||||
private function reportSuccess(array $local, array $stage): void
|
||||
{
|
||||
$this->newLine();
|
||||
$this->components->info('Stage PostgreSQL database cloned successfully.');
|
||||
$this->line("Local {$local['database']} → Stage {$stage['database']}");
|
||||
}
|
||||
}
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use ZipArchive;
|
||||
|
||||
class ExportSource extends Command
|
||||
{
|
||||
protected $signature = 'source:export';
|
||||
|
||||
protected $description = 'Export project source to ~/Downloads';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$project = basename(base_path());
|
||||
$timestamp = now()->format('Ymd-His');
|
||||
$downloads = rtrim((string) getenv('HOME'), '/') . '/Downloads';
|
||||
$archive = "{$downloads}/{$project}-source-{$timestamp}.zip";
|
||||
|
||||
$this->info("Exporting {$project}...");
|
||||
|
||||
/*
|
||||
* Fast export of all Git-tracked source.
|
||||
*/
|
||||
$result = Process::path(base_path())
|
||||
->timeout(60)
|
||||
->run([
|
||||
'git',
|
||||
'archive',
|
||||
'--format=zip',
|
||||
"--output={$archive}",
|
||||
'HEAD',
|
||||
]);
|
||||
|
||||
if ($result->failed()) {
|
||||
$this->error($result->errorOutput());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$zip = new ZipArchive;
|
||||
|
||||
if ($zip->open($archive) !== true) {
|
||||
$this->error('Could not open ZIP.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove ONLY:
|
||||
*
|
||||
* node_modules
|
||||
* vendor
|
||||
* Provision
|
||||
* .scannerwork
|
||||
* contents of storage/framework
|
||||
* contents of storage/logs
|
||||
*/
|
||||
$removeDirectories = [
|
||||
'node_modules/',
|
||||
'vendor/',
|
||||
'Provision/',
|
||||
'provision/',
|
||||
'.scannerwork/',
|
||||
'storage/framework/',
|
||||
'storage/logs/',
|
||||
];
|
||||
|
||||
for ($i = $zip->numFiles - 1; $i >= 0; $i--) {
|
||||
$name = $zip->getNameIndex($i);
|
||||
|
||||
foreach ($removeDirectories as $directory) {
|
||||
if (str_starts_with($name, $directory)) {
|
||||
$zip->deleteIndex($i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Restore the storage directory structure EMPTY.
|
||||
*/
|
||||
$zip->addEmptyDir('storage/framework');
|
||||
$zip->addEmptyDir('storage/framework/cache');
|
||||
$zip->addEmptyDir('storage/framework/cache/data');
|
||||
$zip->addEmptyDir('storage/framework/sessions');
|
||||
$zip->addEmptyDir('storage/framework/testing');
|
||||
$zip->addEmptyDir('storage/framework/views');
|
||||
$zip->addEmptyDir('storage/logs');
|
||||
|
||||
$zip->close();
|
||||
|
||||
$this->info('Done.');
|
||||
$this->line($archive);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class DatabaseStageException extends RuntimeException {}
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
-1
@@ -44,7 +44,6 @@ class User extends Authenticatable implements PasskeyUser
|
||||
return $this->belongsTo(Company::class);
|
||||
}
|
||||
|
||||
|
||||
public function divisions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(CompanyDivision::class, 'company_division_user')
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
-13
@@ -3,23 +3,10 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\Horizon;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Horizon gate.
|
||||
*
|
||||
|
||||
Regular → Executable
+1
-1
@@ -95,7 +95,7 @@ class TenancyServiceProvider extends ServiceProvider
|
||||
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
// No container bindings are required; tenancy is configured through event listeners in boot().
|
||||
}
|
||||
|
||||
public function boot()
|
||||
|
||||
Regular → Executable
Regular → Executable
Reference in New Issue
Block a user