feat(db): add macro_assignments, service macro override tables, and guarded legacy data wipe

This commit is contained in:
Thorsten Bus 2026-05-03 22:16:46 +02:00
parent 09ab4821fc
commit a65bf3d595
6 changed files with 174 additions and 0 deletions

View file

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
public function up(): void
{
Schema::create('macro_assignments', function (Blueprint $table) {
$table->id();
$table->enum('part_type', ['information', 'moderation', 'sermon', 'song', 'agenda_item']);
$table->foreignId('macro_id')->constrained('macros')->restrictOnDelete();
$table->enum('position', ['all_slides', 'first_slide', 'last_slide', 'by_label']);
$table->foreignId('label_id')->nullable()->constrained('labels')->restrictOnDelete();
$table->unsignedInteger('order')->default(0);
$table->timestamps();
$table->index(['part_type', 'order']);
});
}
public function down(): void
{
Schema::dropIfExists('macro_assignments');
}
};

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
public function up(): void
{
Schema::create('service_macro_overrides', function (Blueprint $table) {
$table->id();
$table->foreignId('service_id')->constrained('services')->cascadeOnDelete();
$table->enum('part_type', ['information', 'moderation', 'sermon', 'song', 'agenda_item']);
$table->timestamps();
$table->unique(['service_id', 'part_type']);
});
Schema::create('service_macro_assignments', function (Blueprint $table) {
$table->id();
$table->foreignId('service_id')->constrained('services')->cascadeOnDelete();
$table->enum('part_type', ['information', 'moderation', 'sermon', 'song', 'agenda_item']);
$table->foreignId('macro_id')->constrained('macros')->restrictOnDelete();
$table->enum('position', ['all_slides', 'first_slide', 'last_slide', 'by_label']);
$table->foreignId('label_id')->nullable()->constrained('labels')->restrictOnDelete();
$table->unsignedInteger('order')->default(0);
$table->timestamps();
$table->index(['service_id', 'part_type', 'order']);
});
}
public function down(): void
{
Schema::dropIfExists('service_macro_assignments');
Schema::dropIfExists('service_macro_overrides');
}
};

View file

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
public function up(): void
{
if (! Schema::hasTable('song_groups')) {
return;
}
if (DB::table('song_groups')->count() === 0) {
return;
}
DB::statement('PRAGMA foreign_keys = OFF');
try {
DB::table('song_arrangement_groups')->delete();
DB::table('song_arrangements')->delete();
DB::table('song_slides')->delete();
DB::table('song_groups')->delete();
DB::table('service_songs')->delete();
DB::table('songs')->delete();
} finally {
DB::statement('PRAGMA foreign_keys = ON');
}
}
public function down(): void
{
// Data is unrecoverable — intentional no-op
}
};

View file

@ -0,0 +1,25 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
test('macro_assignments table has expected columns', function () {
expect(Schema::hasTable('macro_assignments'))->toBeTrue();
expect(Schema::hasColumns('macro_assignments', ['id', 'part_type', 'macro_id', 'position', 'label_id', 'order', 'created_at', 'updated_at']))->toBeTrue();
});
test('macro_assignments restrictOnDelete prevents deleting referenced macro', function () {
$macroId = DB::table('macros')->insertGetId(['uuid' => 'AABB-1111-2222-3333-FFFFFFFFFFFF', 'name' => 'Test', 'created_at' => now(), 'updated_at' => now()]);
$labelId = DB::table('labels')->insertGetId(['name' => 'Copyright', 'created_at' => now(), 'updated_at' => now()]);
DB::table('macro_assignments')->insert(['part_type' => 'song', 'macro_id' => $macroId, 'position' => 'by_label', 'label_id' => $labelId, 'order' => 0, 'created_at' => now(), 'updated_at' => now()]);
expect(fn () => DB::table('macros')->where('id', $macroId)->delete())
->toThrow(\Exception::class);
});
test('macro_assignments allows nullable label_id', function () {
$macroId = DB::table('macros')->insertGetId(['uuid' => 'CCBB-1111-2222-3333-FFFFFFFFFFFF', 'name' => 'Test2', 'created_at' => now(), 'updated_at' => now()]);
DB::table('macro_assignments')->insert(['part_type' => 'sermon', 'macro_id' => $macroId, 'position' => 'all_slides', 'label_id' => null, 'order' => 0, 'created_at' => now(), 'updated_at' => now()]);
expect(DB::table('macro_assignments')->where('macro_id', $macroId)->value('label_id'))->toBeNull();
});

View file

@ -0,0 +1,35 @@
<?php
use App\Models\Service;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
test('service_macro_overrides table has expected columns', function () {
expect(Schema::hasTable('service_macro_overrides'))->toBeTrue();
expect(Schema::hasColumns('service_macro_overrides', ['id', 'service_id', 'part_type', 'created_at', 'updated_at']))->toBeTrue();
});
test('service_macro_assignments table has expected columns', function () {
expect(Schema::hasTable('service_macro_assignments'))->toBeTrue();
expect(Schema::hasColumns('service_macro_assignments', ['id', 'service_id', 'part_type', 'macro_id', 'position', 'label_id', 'order', 'created_at', 'updated_at']))->toBeTrue();
});
test('service_macro_overrides unique constraint on service_id and part_type', function () {
$service = Service::factory()->create();
DB::table('service_macro_overrides')->insert(['service_id' => $service->id, 'part_type' => 'song', 'created_at' => now(), 'updated_at' => now()]);
expect(fn () => DB::table('service_macro_overrides')->insert(['service_id' => $service->id, 'part_type' => 'song', 'created_at' => now(), 'updated_at' => now()]))
->toThrow(\Exception::class);
});
test('service delete cascades to overrides and assignments', function () {
$service = Service::factory()->create();
$macroId = DB::table('macros')->insertGetId(['uuid' => 'CCDD-1111-2222-3333-FFFFFFFFFFFF', 'name' => 'Test', 'created_at' => now(), 'updated_at' => now()]);
DB::table('service_macro_overrides')->insert(['service_id' => $service->id, 'part_type' => 'sermon', 'created_at' => now(), 'updated_at' => now()]);
DB::table('service_macro_assignments')->insert(['service_id' => $service->id, 'part_type' => 'sermon', 'macro_id' => $macroId, 'position' => 'all_slides', 'order' => 0, 'created_at' => now(), 'updated_at' => now()]);
$service->delete();
expect(DB::table('service_macro_overrides')->where('service_id', $service->id)->count())->toBe(0);
expect(DB::table('service_macro_assignments')->where('service_id', $service->id)->count())->toBe(0);
});

View file

@ -0,0 +1,16 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
test('wipe migration is no-op on fresh database', function () {
expect(DB::table('songs')->count())->toBe(0);
expect(Schema::hasTable('songs'))->toBeTrue();
expect(Schema::hasTable('song_groups'))->toBeTrue();
expect(DB::table('song_groups')->count())->toBe(0);
});
test('wipe migration guard: song_groups exists but is empty after fresh install', function () {
expect(Schema::hasTable('song_groups'))->toBeTrue();
expect(DB::table('song_groups')->count())->toBe(0);
});