diff --git a/src/Commands/Migrate.php b/src/Commands/Migrate.php index 5348b509..6ecd6e14 100644 --- a/src/Commands/Migrate.php +++ b/src/Commands/Migrate.php @@ -51,13 +51,24 @@ class Migrate extends MigrateCommand return 1; } - if ($this->getProcesses() > 1) { - return $this->runConcurrently($this->getTenantChunks()->map(function ($chunk) { - return $this->getTenants($chunk); - })); + $originalTemplateConnection = config('tenancy.database.template_tenant_connection'); + + if ($database = $this->input->getOption('database')) { + config(['tenancy.database.template_tenant_connection' => $database]); } - return $this->migrateTenants($this->getTenants()) ? 0 : 1; + if ($this->getProcesses() > 1) { + $code = $this->runConcurrently($this->getTenantChunks()->map(function ($chunk) { + return $this->getTenants($chunk); + })); + } else { + $code = $this->migrateTenants($this->getTenants()) ? 0 : 1; + } + + // Reset the template tenant connection to the original one + config(['tenancy.database.template_tenant_connection' => $originalTemplateConnection]); + + return $code; } protected function childHandle(mixed ...$args): bool diff --git a/src/Commands/MigrateFresh.php b/src/Commands/MigrateFresh.php index 4e89cefd..d4733552 100644 --- a/src/Commands/MigrateFresh.php +++ b/src/Commands/MigrateFresh.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Stancl\Tenancy\Commands; +use Illuminate\Console\ConfirmableTrait; use Illuminate\Database\Console\Migrations\BaseCommand; use Illuminate\Database\QueryException; use Illuminate\Support\LazyCollection; @@ -17,7 +18,7 @@ use Symfony\Component\Console\Output\OutputInterface as OI; class MigrateFresh extends BaseCommand { - use HasTenantOptions, DealsWithMigrations, ParallelCommand; + use HasTenantOptions, DealsWithMigrations, ParallelCommand, ConfirmableTrait; protected $description = 'Drop all tables and re-run all migrations for tenant(s)'; @@ -27,6 +28,7 @@ class MigrateFresh extends BaseCommand $this->addOption('drop-views', null, InputOption::VALUE_NONE, 'Drop views along with tenant tables.', null); $this->addOption('step', null, InputOption::VALUE_NONE, 'Force the migrations to be run so they can be rolled back individually.'); + $this->addOption('force', null, InputOption::VALUE_NONE, 'Force the command to run when in production.', null); $this->addProcessesOption(); $this->setName('tenants:migrate-fresh'); @@ -34,6 +36,10 @@ class MigrateFresh extends BaseCommand public function handle(): int { + if (! $this->confirmToProceed()) { + return 1; + } + $success = true; if ($this->getProcesses() > 1) { diff --git a/src/Features/ViteBundler.php b/src/Features/ViteBundler.php index 96f379b7..987187c7 100644 --- a/src/Features/ViteBundler.php +++ b/src/Features/ViteBundler.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Stancl\Tenancy\Features; use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\Vite; use Stancl\Tenancy\Contracts\Feature; -use Stancl\Tenancy\Overrides\Vite; use Stancl\Tenancy\Tenancy; class ViteBundler implements Feature @@ -21,6 +21,8 @@ class ViteBundler implements Feature public function bootstrap(Tenancy $tenancy): void { - $this->app->singleton(\Illuminate\Foundation\Vite::class, Vite::class); + Vite::createAssetPathsUsing(function ($path, $secure = null) { + return global_asset($path); + }); } } diff --git a/src/Overrides/Vite.php b/src/Overrides/Vite.php deleted file mode 100644 index 66bc9268..00000000 --- a/src/Overrides/Vite.php +++ /dev/null @@ -1,22 +0,0 @@ -toBeTrue(); }); +test('migrate command uses the passed database option as the template tenant connection', function () { + $originalTemplateConnection = config('tenancy.database.template_tenant_connection'); + + // Add a custom connection that will be used as the template for the tenant connection + // Identical to the default (mysql), just with different charset and collation + config(['database.connections.custom_connection' => [ + "driver" => "mysql", + "url" => "", + "host" => "mysql", + "port" => "3306", + "database" => "main", + "username" => "root", + "password" => "password", + "unix_socket" => "", + "charset" => "latin1", // Different from the default (utf8mb4) + "collation" => "latin1_swedish_ci", // Different from the default (utf8mb4_unicode_ci) + "prefix" => "", + "prefix_indexes" => true, + "strict" => true, + "engine" => null, + "options" => [] + ]]); + + $templateConnectionDuringMigration = null; + $tenantConnectionDuringMigration = null; + + Event::listen(MigratingDatabase::class, function() use (&$templateConnectionDuringMigration, &$tenantConnectionDuringMigration) { + $templateConnectionDuringMigration = config('tenancy.database.template_tenant_connection'); + $tenantConnectionDuringMigration = DB::connection('tenant')->getConfig(); + }); + + // The original tenant template connection config remains default + expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection); + + Tenant::create(); + + // The original template connection is used when the --database option is not passed + pest()->artisan('tenants:migrate'); + expect($templateConnectionDuringMigration)->toBe($originalTemplateConnection); + + Tenant::create(); + + // The migrate command temporarily uses the connection passed in the --database option + pest()->artisan('tenants:migrate', ['--database' => 'custom_connection']); + expect($templateConnectionDuringMigration)->toBe('custom_connection'); + + // The tenant connection during migration actually used custom_connection's config + expect($tenantConnectionDuringMigration['charset'])->toBe('latin1'); + expect($tenantConnectionDuringMigration['collation'])->toBe('latin1_swedish_ci'); + + // The tenant template connection config is restored to the original after migrating + expect(config('tenancy.database.template_tenant_connection'))->toBe($originalTemplateConnection); +}); + test('migrate command only throws exceptions if skip-failing is not passed', function() { Tenant::create(); @@ -311,6 +366,21 @@ test('migrate fresh command works', function () { expect(DB::table('users')->exists())->toBeFalse(); }); +test('migrate fresh command respects force option in production', function () { + // Set environment to production + app()->detectEnvironment(fn() => 'production'); + + Tenant::create(); + + // Without --force in production, command should prompt for confirmation + pest()->artisan('tenants:migrate-fresh') + ->expectsConfirmation('Are you sure you want to run this command?'); + + // With --force, command should succeed without prompting + pest()->artisan('tenants:migrate-fresh', ['--force' => true]) + ->assertSuccessful(); +}); + test('run command with array of tenants works', function () { $tenantId1 = Tenant::create()->getTenantKey(); $tenantId2 = Tenant::create()->getTenantKey(); diff --git a/tests/Features/ViteBundlerTest.php b/tests/Features/ViteBundlerTest.php index 8254e8cc..3934698f 100644 --- a/tests/Features/ViteBundlerTest.php +++ b/tests/Features/ViteBundlerTest.php @@ -3,27 +3,41 @@ declare(strict_types=1); use Illuminate\Foundation\Vite; -use Stancl\Tenancy\Tests\Etc\Tenant; -use Stancl\Tenancy\Overrides\Vite as StanclVite; +use Illuminate\Support\Facades\File; +use Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper; use Stancl\Tenancy\Features\ViteBundler; +use Stancl\Tenancy\Tests\Etc\Tenant; -test('vite helper uses our custom class', function() { - $vite = app(Vite::class); - - expect($vite)->toBeInstanceOf(Vite::class); - expect($vite)->not()->toBeInstanceOf(StanclVite::class); +use function Stancl\Tenancy\Tests\withBootstrapping; +beforeEach(function () { config([ - 'tenancy.features' => [ViteBundler::class], + 'tenancy.filesystem.asset_helper_override' => true, + 'tenancy.bootstrappers' => [FilesystemTenancyBootstrapper::class], ]); - $tenant = Tenant::create(); - - tenancy()->initialize($tenant); - - app()->forgetInstance(Vite::class); - - $vite = app(Vite::class); - - expect($vite)->toBeInstanceOf(StanclVite::class); + File::ensureDirectoryExists(dirname($manifestPath = public_path('build/manifest.json'))); + File::put($manifestPath, json_encode([ + 'foo' => [ + 'file' => 'assets/foo-AbC123.js', + 'src' => 'js/foo.js', + ], + ])); +}); + +test('vite bundler ensures vite assets use global_asset when asset_helper_override is enabled', function () { + config(['tenancy.features' => [ViteBundler::class]]); + + withBootstrapping(); + + tenancy()->initialize(Tenant::create()); + + // Not what we want + expect(asset('foo'))->toBe(route('stancl.tenancy.asset', ['path' => 'foo'])); + + $viteAssetUrl = app(Vite::class)->asset('foo'); + $expectedGlobalUrl = global_asset('build/assets/foo-AbC123.js'); + + expect($viteAssetUrl)->toBe($expectedGlobalUrl); + expect($viteAssetUrl)->toBe('http://localhost/build/assets/foo-AbC123.js'); });