Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Tapp\FilamentLms\Helpers\TenantHelper;

return new class extends Migration
{
/**
* @var list<string>
*/
private array $columns = ['name', 'slug', 'external_id'];

public function up(): void
{
if (! $this->shouldScopeUniqueIndexes()) {
return;
}

$tenantColumn = TenantHelper::getTenantColumnName();
$drop = [];
$add = [];

foreach ($this->columns as $column) {
if (Schema::hasIndex('lms_courses', [$column], 'unique')) {
$drop[] = $column;
}

if (! Schema::hasIndex('lms_courses', [$tenantColumn, $column], 'unique')) {
$add[] = $column;
}
}

if ($drop !== []) {
Schema::table('lms_courses', function (Blueprint $table) use ($drop): void {
foreach ($drop as $column) {
$table->dropUnique([$column]);
}
});
}

if ($add !== []) {
Schema::table('lms_courses', function (Blueprint $table) use ($add, $tenantColumn): void {
foreach ($add as $column) {
$table->unique([$tenantColumn, $column]);
}
});
}
}

public function down(): void
{
// Do not restore global unique indexes. After this migration runs,
// two tenants may share a name, slug, or external_id; rolling back
// would drop tenant-scoped constraints and can fail to recreate globals.
}

private function shouldScopeUniqueIndexes(): bool
{
if (! config('filament-lms.tenancy.enabled')) {
return false;
}

$tenantColumn = TenantHelper::getTenantColumnName();

return Schema::hasTable('lms_courses')
&& Schema::hasColumn('lms_courses', $tenantColumn);
}
};
1 change: 1 addition & 0 deletions src/FilamentLmsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public function configurePackage(Package $package): void
'create_lms_course_user_group_table',
'create_lms_user_group_memberships_table',
'add_is_explicitly_assigned_to_lms_course_user_table',
'scope_lms_courses_unique_indexes_to_tenant',
])
->hasCommand(BackfillCourseCompletedAt::class)
->hasCommand(BackfillEmbeddedPlayerCourses::class)
Expand Down
6 changes: 3 additions & 3 deletions src/Resources/CourseResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ public static function form(Schema $schema): Schema
$set('external_id', Str::slug($state ?? '', '_'));
}
})
->unique(ignoreRecord: true)
->scopedUnique(ignoreRecord: true)
Comment thread
andreia marked this conversation as resolved.
->required(),
TextInput::make('external_id')
->label('External ID')
->helperText('Used for external integrations like HubSpot. Updating this will cause a new property to be added to the integration.')
->required()
->unique(ignoreRecord: true)
->scopedUnique(ignoreRecord: true)
->rules([
'regex:/^[a-z][a-z0-9_]*$/',
'max:100',
Expand All @@ -100,7 +100,7 @@ public static function form(Schema $schema): Schema
]),
TextInput::make('slug')
->helperText('Used for urls.')
->unique(ignoreRecord: true)
->scopedUnique(ignoreRecord: true)
->required(),
SpatieMediaLibraryFileUpload::make('image')
->helperText('Upload a course image.')
Expand Down
102 changes: 102 additions & 0 deletions tests/Feature/ScopeLmsCoursesUniqueIndexesToTenantTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace Tapp\FilamentLms\Tests\Feature;

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

test('scopes course unique indexes to the tenant column', function () {
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->timestamps();
});

Schema::table('lms_courses', function (Blueprint $table) {
$table->foreignId('company_id')->nullable()->constrained('companies');
});

config([
'filament-lms.tenancy.enabled' => true,
'filament-lms.tenancy.column' => 'company_id',
]);

$migration = require dirname(__DIR__, 2).'/database/migrations/scope_lms_courses_unique_indexes_to_tenant.php.stub';

$migration->up();
$migration->up();

expect(Schema::hasIndex('lms_courses', ['name'], 'unique'))->toBeFalse()
->and(Schema::hasIndex('lms_courses', ['slug'], 'unique'))->toBeFalse()
->and(Schema::hasIndex('lms_courses', ['external_id'], 'unique'))->toBeFalse()
->and(Schema::hasIndex('lms_courses', ['company_id', 'name'], 'unique'))->toBeTrue()
->and(Schema::hasIndex('lms_courses', ['company_id', 'slug'], 'unique'))->toBeTrue()
->and(Schema::hasIndex('lms_courses', ['company_id', 'external_id'], 'unique'))->toBeTrue();
});

test('does not change course unique indexes when tenancy is disabled', function () {
$migration = require dirname(__DIR__, 2).'/database/migrations/scope_lms_courses_unique_indexes_to_tenant.php.stub';

$migration->up();

expect(Schema::hasIndex('lms_courses', ['name'], 'unique'))->toBeTrue()
->and(Schema::hasIndex('lms_courses', ['slug'], 'unique'))->toBeTrue()
->and(Schema::hasIndex('lms_courses', ['external_id'], 'unique'))->toBeTrue();
});

test('does not replace tenant unique indexes with global uniques on rollback', function () {
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->timestamps();
});

Schema::table('lms_courses', function (Blueprint $table) {
$table->foreignId('company_id')->nullable()->constrained('companies');
});

config([
'filament-lms.tenancy.enabled' => true,
'filament-lms.tenancy.column' => 'company_id',
]);

$migration = require dirname(__DIR__, 2).'/database/migrations/scope_lms_courses_unique_indexes_to_tenant.php.stub';

$migration->up();

$alphaId = DB::table('companies')->insertGetId([
'created_at' => now(),
'updated_at' => now(),
]);
$betaId = DB::table('companies')->insertGetId([
'created_at' => now(),
'updated_at' => now(),
]);

DB::table('lms_courses')->insert([
[
'company_id' => $alphaId,
'name' => 'Shared Course Name',
'slug' => 'shared-course-slug',
'external_id' => 'shared-external-id',
'created_at' => now(),
'updated_at' => now(),
],
[
'company_id' => $betaId,
'name' => 'Shared Course Name',
'slug' => 'shared-course-slug',
'external_id' => 'shared-external-id',
'created_at' => now(),
'updated_at' => now(),
],
]);

$migration->down();

expect(Schema::hasIndex('lms_courses', ['company_id', 'name'], 'unique'))->toBeTrue()
->and(Schema::hasIndex('lms_courses', ['company_id', 'slug'], 'unique'))->toBeTrue()
->and(Schema::hasIndex('lms_courses', ['company_id', 'external_id'], 'unique'))->toBeTrue()
->and(Schema::hasIndex('lms_courses', ['name'], 'unique'))->toBeFalse()
->and(Schema::hasIndex('lms_courses', ['slug'], 'unique'))->toBeFalse()
->and(Schema::hasIndex('lms_courses', ['external_id'], 'unique'))->toBeFalse();
});
Loading