Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/app/Console/Commands/MakeSchemaCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace App\Console\Commands;

use App\Models\Crew;
use App\Models\Schema;
use Illuminate\Console\Command;
use Illuminate\Database\Schema\Builder;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class MakeSchemaCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'make:schema {team_id}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Generates a schema for a team in the database and creates the associated migration folder if the schema doesn\'t already exist';

protected Builder $builder;

public function __construct(
Builder $builder,
)
{
parent::__construct();
$this->builder = DB::getSchemaBuilder();
}

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
$crew_id = $this->argument('team_id');
$crew = Crew::find($crew_id);

if(!$crew) {
$this->warn("Team " . $this->argument('team_id') . " doesn't exist yet.");
$this->info("Create the team before creating its schema");
return Command::FAILURE;
}

$migrationFolderPath = Config::get('database.migration_path');
File::ensureDirectoryExists($migrationFolderPath . DIRECTORY_SEPARATOR . $crew_id);

$dbName = Schema::getSchemaName($crew_id);
$schema = Schema::where('name', $dbName)->first();

if(!$schema) {
$schema['name'] = $dbName;
$schema['crew_id'] = $crew_id;
Schema::Create($schema);
Comment on lines +63 to +65
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $schema is being used as an array but was previously assigned a model instance from the query. This will cause a runtime error. Should initialize as an empty array or use Schema::create() with lowercase 'c'.

Suggested change
$schema['name'] = $dbName;
$schema['crew_id'] = $crew_id;
Schema::Create($schema);
$newSchemaData = [
'name' => $dbName,
'crew_id' => $crew_id,
];
Schema::create($newSchemaData);

Copilot uses AI. Check for mistakes.
$this->builder->createDatabase($dbName);
$this->info("Created database: " . $dbName);
} else {
$this->info("Database already exists for this team");
}

return Command::SUCCESS;

} catch(\Exception $e) {
$this->error($e->getMessage());
return Command::FAILURE;
}
}
}
61 changes: 61 additions & 0 deletions src/app/Console/Commands/MigrateCustomSchemaCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace App\Console\Commands;

use App\Models\Crew;
use App\Models\Schema;
use Illuminate\Console\Command;

class MigrateCustomSchemaCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/

protected $signature = 'migrate:custom {team_id} {--path=}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Executes the migration for the database linked to the team id';

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$crew_id = $this->argument('team_id');
$crew = Crew::find($crew_id);

if(!$crew) {
$this->warn("Team " . $this->argument('team_id') . " doesn't exist yet.");
$this->info("Create the team before migrating to its schema");
return Command::FAILURE;
}

$dbName = Schema::getSchemaName($crew_id);
$schema = Schema::where("name", $dbName)->first();
if(!$schema) {
$this->warn("No database found for " . $this->argument('team_id'));
$this->info("You can initiate a new schema with artisan make:schema {team_id}");
return Command::FAILURE;
}

$schema->openDatabaseConnection();
$path = $this->option('path') ?? config('database.migration_path');

$migrationResult = $this->call('migrate', ['--database' => $dbName, '--path' => $path]);

if ($migrationResult !== Command::SUCCESS) {
$this->warn("Something went wrong during the migration. Aborting.");
return Command::FAILURE;
}
return Command::SUCCESS;
}
}
55 changes: 55 additions & 0 deletions src/app/Models/Schema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace App\Models;

use App\Traits\Uuids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
Use Illuminate\Database\Eloquent\SoftDeletes;
Copy link

Copilot AI Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected 'Use' to 'use' for proper PHP namespace import syntax.

Suggested change
Use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\SoftDeletes;

Copilot uses AI. Check for mistakes.
use Illuminate\Support\Facades\Config;


class Schema extends Model
{
use HasFactory, Uuids, SoftDeletes;
/**
* The data type of the auto-incrementing ID.
*
* @var string
*/
protected $keyType = 'string';

/**
* Indicates if the model's ID is auto-incrementing.
*
* @var bool
*/

public $incrementing = false;

/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];

public function crew(): BelongsTo
{
return $this->belongsTo(Crew::class);
}

public static function getSchemaName(string $crew_id): string
{
return 'db_scan_' . $crew_id;
}

public function openDatabaseConnection(): void {
$dbName = $this->name;
$dbConfig = Config::get('database.connections.mysql');
$dbConfig['database'] = $dbName;
Config::set('database.connections.' . $dbName, $dbConfig);
}

}
34 changes: 34 additions & 0 deletions src/database/migrations/2025_01_15_143802_create_schemas_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('schemas', function (Blueprint $table) {
$table->uuid("id")->primary();
$table->string("name")->unique();
$table->foreignUuid("crew_id");
$table->timestamps();
$table->softDeletes();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('schemas');
}
};
96 changes: 96 additions & 0 deletions src/tests/Feature/CustomSchemaTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

namespace Tests\Feature;

use App\Models\Crew;
use App\Models\Schema;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Tests\TestCase;

class CustomSchemaTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
}

public function test_create_schema_for_existing_team()
{
$test_crew = Crew::factory()->create();
$crew_id = $test_crew->id;
$db_name = Schema::getSchemaName($crew_id);

$this->artisan('make:schema ' . $crew_id)
->assertSuccessful()
->expectsOutput("Created database: " . $db_name);

$this->assertDatabaseHas('schemas', [
'crew_id' => $crew_id
]);

$migrationFolderPath = Config::get('database.migration_path');
$this->assertTrue(File::exists($migrationFolderPath . DIRECTORY_SEPARATOR . $crew_id));

$dbConfig = Config::get('database.connections.mysql');
$dbConfig['database'] = $db_name;
Config::set('database.connections.' . $db_name, $dbConfig);

$this->assertEquals($db_name, DB::connection($db_name)->getDatabaseName());

}

public function test_recreate_schema_for_existing_team()
{
$test_crew = Crew::factory()->create();
$this->artisan('make:schema ' . $test_crew->id)
->assertSuccessful();

$this->artisan('make:schema ' . $test_crew->id)
->assertSuccessful()
->expectsOutput('Database already exists for this team');
}

public function test_create_schema_for_non_existing_team()
{
$this->artisan('make:schema ' . 'test')
->assertFailed()
->expectsOutput("Team test doesn't exist yet.");
}

public function test_migrate_to_non_existing_team()
{
$this->artisan('migrate:custom test')
->assertFailed()
->expectsOutput("Team test doesn't exist yet.");
}

public function test_migrate_to_existing_team_without_schema()
{
$crew = Crew::factory()->create();

$this->artisan('migrate:custom ' . $crew->id)
->assertFailed()
->expectsOutput("No database found for " . $crew->id);
}

public function test_migrate_to_existing_team_with_schema()
{
$crew = Crew::factory()->create();
$db_name = Schema::getSchemaName($crew->id);

$this->artisan('make:schema ' . $crew->id)
->assertSuccessful();

$this->artisan('migrate:custom ' . $crew->id)
->assertSuccessful();

$dbConfig = Config::get('database.connections.mysql');
$dbConfig['database'] = $db_name;
Config::set('database.connections.' . $db_name, $dbConfig);

$schemaBuilder = \Illuminate\Support\Facades\Schema::connection($db_name);
$this->assertTrue($schemaBuilder->hasTable("migrations"));
}
}