optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,30 @@
<?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('user_activities', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('type', 32);
$table->string('entity_type', 48);
$table->unsignedBigInteger('entity_id');
$table->json('meta')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['user_id', 'created_at'], 'user_activities_user_created_idx');
$table->index(['user_id', 'type', 'created_at'], 'user_activities_user_type_created_idx');
$table->index(['entity_type', 'entity_id'], 'user_activities_entity_idx');
});
}
public function down(): void
{
Schema::dropIfExists('user_activities');
}
};

View File

@@ -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
{
public function up(): void
{
Schema::table('user_activities', function (Blueprint $table): void {
$table->timestamp('hidden_at')->nullable()->after('created_at');
$table->foreignId('hidden_by')->nullable()->after('hidden_at')->constrained('users')->nullOnDelete();
$table->string('hidden_reason', 500)->nullable()->after('hidden_by');
$table->timestamp('flagged_at')->nullable()->after('hidden_reason');
$table->foreignId('flagged_by')->nullable()->after('flagged_at')->constrained('users')->nullOnDelete();
$table->string('flag_reason', 500)->nullable()->after('flagged_by');
$table->index(['hidden_at'], 'user_activities_hidden_at_idx');
$table->index(['flagged_at'], 'user_activities_flagged_at_idx');
});
}
public function down(): void
{
Schema::table('user_activities', function (Blueprint $table): void {
$table->dropIndex('user_activities_hidden_at_idx');
$table->dropIndex('user_activities_flagged_at_idx');
$table->dropConstrainedForeignId('hidden_by');
$table->dropConstrainedForeignId('flagged_by');
$table->dropColumn(['hidden_at', 'hidden_reason', 'flagged_at', 'flag_reason']);
});
}
};

View File

@@ -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
{
public function up(): void
{
if (Schema::hasTable('user_follow_analytics')) {
return;
}
Schema::create('user_follow_analytics', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->date('date');
$table->unsignedInteger('followers_gained')->default(0);
$table->unsignedInteger('followers_lost')->default(0);
$table->unsignedInteger('follows_made')->default(0);
$table->unsignedInteger('unfollows_made')->default(0);
$table->timestamps();
$table->unique(['user_id', 'date']);
$table->index(['date', 'user_id']);
});
}
public function down(): void
{
Schema::dropIfExists('user_follow_analytics');
}
};

View File

@@ -0,0 +1,41 @@
<?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('discovery_feedback_daily_metrics', function (Blueprint $table): void {
$table->id();
$table->date('metric_date');
$table->string('algo_version', 64);
$table->string('surface', 64);
$table->unsignedInteger('views')->default(0);
$table->unsignedInteger('clicks')->default(0);
$table->unsignedInteger('favorites')->default(0);
$table->unsignedInteger('downloads')->default(0);
$table->unsignedInteger('feedback_actions')->default(0);
$table->unsignedInteger('unique_users')->default(0);
$table->unsignedInteger('unique_artworks')->default(0);
$table->decimal('ctr', 8, 6)->default(0);
$table->decimal('favorite_rate_per_click', 8, 6)->default(0);
$table->decimal('download_rate_per_click', 8, 6)->default(0);
$table->decimal('feedback_rate_per_click', 8, 6)->default(0);
$table->timestamps();
$table->unique(['metric_date', 'algo_version', 'surface'], 'discovery_feedback_daily_unique_idx');
$table->index(['metric_date', 'algo_version', 'surface'], 'discovery_feedback_daily_lookup_idx');
});
}
public function down(): void
{
Schema::dropIfExists('discovery_feedback_daily_metrics');
}
};

View File

@@ -0,0 +1,47 @@
<?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::table('artworks', function (Blueprint $table): void {
if (! Schema::hasColumn('artworks', 'trending_score_1h')) {
$table->float('trending_score_1h', 10, 4)->default(0)->after('is_approved')->index();
}
});
if (! Schema::hasTable('user_negative_signals')) {
Schema::create('user_negative_signals', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('signal_type', 24)->index();
$table->foreignId('artwork_id')->nullable()->constrained('artworks')->cascadeOnDelete();
$table->foreignId('tag_id')->nullable()->constrained('tags')->cascadeOnDelete();
$table->string('source', 64)->nullable();
$table->string('algo_version', 64)->nullable()->index();
$table->json('meta')->nullable();
$table->timestamps();
$table->unique(['user_id', 'signal_type', 'artwork_id'], 'user_negative_signals_user_artwork_unique');
$table->unique(['user_id', 'signal_type', 'tag_id'], 'user_negative_signals_user_tag_unique');
$table->index(['user_id', 'signal_type', 'created_at'], 'user_negative_signals_lookup_idx');
});
}
}
public function down(): void
{
Schema::dropIfExists('user_negative_signals');
Schema::table('artworks', function (Blueprint $table): void {
if (Schema::hasColumn('artworks', 'trending_score_1h')) {
$table->dropIndex(['trending_score_1h']);
$table->dropColumn('trending_score_1h');
}
});
}
};

View File

@@ -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
{
public function up(): void
{
Schema::table('discovery_feedback_daily_metrics', function (Blueprint $table): void {
$table->unsignedInteger('hidden_artworks')->default(0)->after('downloads');
$table->unsignedInteger('disliked_tags')->default(0)->after('hidden_artworks');
$table->unsignedInteger('undo_hidden_artworks')->default(0)->after('disliked_tags');
$table->unsignedInteger('undo_disliked_tags')->default(0)->after('undo_hidden_artworks');
$table->unsignedInteger('negative_feedback_actions')->default(0)->after('feedback_actions');
$table->unsignedInteger('undo_actions')->default(0)->after('negative_feedback_actions');
});
}
public function down(): void
{
Schema::table('discovery_feedback_daily_metrics', function (Blueprint $table): void {
$table->dropColumn([
'hidden_artworks',
'disliked_tags',
'undo_hidden_artworks',
'undo_disliked_tags',
'negative_feedback_actions',
'undo_actions',
]);
});
}
};

View File

@@ -0,0 +1,46 @@
<?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::table('artworks', function (Blueprint $table): void {
if (! Schema::hasColumn('artworks', 'clip_tags_json')) {
$table->json('clip_tags_json')->nullable()->after('thumb_ext');
}
if (! Schema::hasColumn('artworks', 'blip_caption')) {
$table->text('blip_caption')->nullable()->after('clip_tags_json');
}
if (! Schema::hasColumn('artworks', 'yolo_objects_json')) {
$table->json('yolo_objects_json')->nullable()->after('blip_caption');
}
if (! Schema::hasColumn('artworks', 'vision_metadata_updated_at')) {
$table->timestamp('vision_metadata_updated_at')->nullable()->after('yolo_objects_json');
}
});
}
public function down(): void
{
Schema::table('artworks', function (Blueprint $table): void {
$columns = [];
foreach (['clip_tags_json', 'blip_caption', 'yolo_objects_json', 'vision_metadata_updated_at'] as $column) {
if (Schema::hasColumn('artworks', $column)) {
$columns[] = $column;
}
}
if ($columns !== []) {
$table->dropColumn($columns);
}
});
}
};

View File

@@ -0,0 +1,23 @@
<?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::table('artworks', function (Blueprint $table): void {
$table->timestamp('last_vector_indexed_at')->nullable()->after('vision_metadata_updated_at')->index();
});
}
public function down(): void
{
Schema::table('artworks', function (Blueprint $table): void {
$table->dropIndex(['last_vector_indexed_at']);
$table->dropColumn('last_vector_indexed_at');
});
}
};

View File

@@ -0,0 +1,81 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const LEGACY_INDEX = 'messages_conversation_client_temp_idx';
private const UNIQUE_INDEX = 'messages_sender_client_temp_unique';
public function up(): void
{
if (! Schema::hasTable('messages') || ! Schema::hasColumn('messages', 'client_temp_id')) {
return;
}
DB::table('messages')
->where('client_temp_id', '')
->update(['client_temp_id' => null]);
$duplicates = DB::table('messages')
->select(['conversation_id', 'sender_id', 'client_temp_id'])
->whereNotNull('client_temp_id')
->groupBy('conversation_id', 'sender_id', 'client_temp_id')
->havingRaw('count(*) > 1')
->get();
foreach ($duplicates as $duplicate) {
$idsToClear = DB::table('messages')
->where('conversation_id', $duplicate->conversation_id)
->where('sender_id', $duplicate->sender_id)
->where('client_temp_id', $duplicate->client_temp_id)
->orderBy('id')
->pluck('id')
->slice(1)
->all();
if ($idsToClear === []) {
continue;
}
DB::table('messages')
->whereIn('id', $idsToClear)
->update(['client_temp_id' => null]);
}
Schema::table('messages', function (Blueprint $table): void {
$schema = Schema::getConnection()->getSchemaBuilder();
if ($schema->hasIndex('messages', self::LEGACY_INDEX)) {
$table->dropIndex(self::LEGACY_INDEX);
}
if (! $schema->hasIndex('messages', self::UNIQUE_INDEX)) {
$table->unique(['conversation_id', 'sender_id', 'client_temp_id'], self::UNIQUE_INDEX);
}
});
}
public function down(): void
{
if (! Schema::hasTable('messages') || ! Schema::hasColumn('messages', 'client_temp_id')) {
return;
}
Schema::table('messages', function (Blueprint $table): void {
$schema = Schema::getConnection()->getSchemaBuilder();
if ($schema->hasIndex('messages', self::UNIQUE_INDEX)) {
$table->dropUnique(self::UNIQUE_INDEX);
}
if (! $schema->hasIndex('messages', self::LEGACY_INDEX)) {
$table->index(['conversation_id', 'client_temp_id'], self::LEGACY_INDEX);
}
});
}
};

View File

@@ -0,0 +1,58 @@
<?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('collections', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')
->constrained('users')
->cascadeOnDelete();
$table->string('title', 120);
$table->string('slug', 140);
$table->text('description')->nullable();
$table->foreignId('cover_artwork_id')
->nullable()
->constrained('artworks')
->nullOnDelete();
$table->enum('visibility', ['public', 'unlisted', 'private'])->default('public');
$table->enum('sort_mode', ['manual', 'newest', 'oldest', 'popular'])->default('manual');
$table->unsignedInteger('artworks_count')->default(0);
$table->boolean('is_featured')->default(false);
$table->timestamps();
$table->softDeletes();
$table->unique(['user_id', 'slug'], 'collections_user_slug_unique');
$table->index('user_id');
$table->index('visibility');
$table->index('created_at');
});
Schema::create('collection_artwork', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')
->constrained('collections')
->cascadeOnDelete();
$table->foreignId('artwork_id')
->constrained('artworks')
->cascadeOnDelete();
$table->unsignedInteger('order_num')->default(0);
$table->timestamps();
$table->unique(['collection_id', 'artwork_id'], 'collection_artwork_unique');
$table->index(['collection_id', 'order_num'], 'collection_artwork_order_idx');
$table->index('artwork_id');
});
}
public function down(): void
{
Schema::dropIfExists('collection_artwork');
Schema::dropIfExists('collections');
}
};

View File

@@ -0,0 +1,74 @@
<?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::table('collections', function (Blueprint $table): void {
$table->enum('mode', ['manual', 'smart'])->default('manual')->after('visibility');
$table->unsignedInteger('profile_order')->nullable()->after('is_featured');
$table->unsignedInteger('views_count')->default(0)->after('profile_order');
$table->unsignedInteger('likes_count')->default(0)->after('views_count');
$table->unsignedInteger('followers_count')->default(0)->after('likes_count');
$table->unsignedInteger('shares_count')->default(0)->after('followers_count');
$table->string('subtitle', 160)->nullable()->after('description');
$table->text('summary')->nullable()->after('subtitle');
$table->json('smart_rules_json')->nullable()->after('summary');
$table->timestamp('last_activity_at')->nullable()->after('smart_rules_json');
$table->timestamp('featured_at')->nullable()->after('last_activity_at');
$table->index(['user_id', 'is_featured', 'featured_at'], 'collections_user_featured_idx');
$table->index(['user_id', 'profile_order'], 'collections_user_profile_order_idx');
$table->index(['mode', 'visibility'], 'collections_mode_visibility_idx');
});
Schema::create('collection_follows', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('created_at')->useCurrent();
$table->unique(['collection_id', 'user_id'], 'collection_follows_unique');
$table->index('user_id');
});
Schema::create('collection_likes', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('created_at')->useCurrent();
$table->unique(['collection_id', 'user_id'], 'collection_likes_unique');
$table->index('user_id');
});
}
public function down(): void
{
Schema::dropIfExists('collection_likes');
Schema::dropIfExists('collection_follows');
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex('collections_user_featured_idx');
$table->dropIndex('collections_user_profile_order_idx');
$table->dropIndex('collections_mode_visibility_idx');
$table->dropColumn([
'mode',
'profile_order',
'views_count',
'likes_count',
'followers_count',
'shares_count',
'subtitle',
'summary',
'smart_rules_json',
'last_activity_at',
'featured_at',
]);
});
}
};

View File

@@ -0,0 +1,27 @@
<?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::table('artworks', function (Blueprint $table): void {
if (! Schema::hasColumn('artworks', 'is_mature')) {
$table->boolean('is_mature')->default(false)->after('is_approved')->index();
}
});
}
public function down(): void
{
Schema::table('artworks', function (Blueprint $table): void {
if (Schema::hasColumn('artworks', 'is_mature')) {
$table->dropIndex(['is_mature']);
$table->dropColumn('is_mature');
}
});
}
};

View File

@@ -0,0 +1,98 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const CURRENT_TARGET_TYPES = [
'message',
'conversation',
'user',
'story',
'collection',
'collection_comment',
'collection_submission',
];
private const PREVIOUS_TARGET_TYPES = [
'message',
'conversation',
'user',
'story',
];
public function up(): void
{
$this->syncTargetTypes(self::CURRENT_TARGET_TYPES);
}
public function down(): void
{
$this->syncTargetTypes(self::PREVIOUS_TARGET_TYPES);
}
/**
* @param array<int, string> $targetTypes
*/
private function syncTargetTypes(array $targetTypes): void
{
if (! Schema::hasTable('reports') || ! Schema::hasColumn('reports', 'target_type')) {
return;
}
$driver = DB::getDriverName();
if ($driver === 'mysql') {
$allowed = implode("','", $targetTypes);
DB::statement("ALTER TABLE reports MODIFY target_type ENUM('{$allowed}') NOT NULL");
return;
}
if ($driver === 'sqlite') {
$this->rebuildSqliteReportsTable($targetTypes);
}
}
/**
* @param array<int, string> $targetTypes
*/
private function rebuildSqliteReportsTable(array $targetTypes): void
{
$quotedTargetTypes = collect($targetTypes)
->map(fn (string $type): string => "'{$type}'")
->implode(', ');
DB::statement('DROP TABLE IF EXISTS reports_temp');
DB::statement(
<<<SQL
CREATE TABLE reports_temp (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
reporter_id INTEGER NOT NULL,
target_type VARCHAR NOT NULL CHECK (target_type IN ({$quotedTargetTypes})),
target_id INTEGER NOT NULL,
reason VARCHAR(120) NOT NULL,
details TEXT NULL,
status VARCHAR NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'reviewing', 'closed')),
created_at DATETIME NULL,
updated_at DATETIME NULL,
FOREIGN KEY (reporter_id) REFERENCES users(id) ON DELETE CASCADE
)
SQL
);
DB::statement(
'INSERT INTO reports_temp (id, reporter_id, target_type, target_id, reason, details, status, created_at, updated_at) '
.'SELECT id, reporter_id, target_type, target_id, reason, details, status, created_at, updated_at FROM reports'
);
DB::statement('DROP TABLE reports');
DB::statement('ALTER TABLE reports_temp RENAME TO reports');
DB::statement('CREATE INDEX reports_target_type_target_id_index ON reports (target_type, target_id)');
DB::statement('CREATE INDEX reports_status_index ON reports (status)');
DB::statement('CREATE INDEX reports_reporter_id_index ON reports (reporter_id)');
}
};

View File

@@ -0,0 +1,118 @@
<?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::table('collections', function (Blueprint $table): void {
$table->enum('type', ['personal', 'community', 'editorial'])->default('personal')->after('slug');
$table->enum('collaboration_mode', ['closed', 'invite_only', 'open'])->default('closed')->after('summary');
$table->boolean('allow_submissions')->default(false)->after('collaboration_mode');
$table->boolean('allow_comments')->default(true)->after('allow_submissions');
$table->boolean('allow_saves')->default(true)->after('allow_comments');
$table->enum('moderation_status', ['active', 'under_review', 'restricted', 'hidden'])->default('active')->after('allow_saves');
$table->unsignedInteger('comments_count')->default(0)->after('artworks_count');
$table->unsignedInteger('saves_count')->default(0)->after('shares_count');
$table->unsignedInteger('collaborators_count')->default(1)->after('saves_count');
$table->timestamp('published_at')->nullable()->after('featured_at');
$table->timestamp('unpublished_at')->nullable()->after('published_at');
$table->string('event_key', 80)->nullable()->after('unpublished_at');
$table->string('event_label', 120)->nullable()->after('event_key');
$table->string('season_key', 80)->nullable()->after('event_label');
$table->string('badge_label', 80)->nullable()->after('season_key');
$table->index(['type', 'visibility', 'moderation_status'], 'collections_type_visibility_mod_idx');
$table->index(['collaboration_mode', 'allow_submissions'], 'collections_collab_submission_idx');
});
Schema::create('collection_members', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('invited_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->enum('role', ['owner', 'editor', 'contributor', 'viewer'])->default('contributor');
$table->enum('status', ['pending', 'active', 'revoked'])->default('pending');
$table->string('note', 320)->nullable();
$table->timestamp('invited_at')->nullable();
$table->timestamp('accepted_at')->nullable();
$table->timestamp('revoked_at')->nullable();
$table->timestamps();
$table->unique(['collection_id', 'user_id'], 'collection_members_unique');
$table->index(['collection_id', 'status'], 'collection_members_status_idx');
});
Schema::create('collection_submissions', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->text('message')->nullable();
$table->enum('status', ['pending', 'approved', 'rejected', 'withdrawn'])->default('pending');
$table->foreignId('reviewed_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('reviewed_at')->nullable();
$table->timestamps();
$table->index(['collection_id', 'status'], 'collection_submissions_status_idx');
$table->unique(['collection_id', 'artwork_id', 'user_id'], 'collection_submissions_unique');
});
Schema::create('collection_saves', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamp('created_at')->useCurrent();
$table->unique(['collection_id', 'user_id'], 'collection_saves_unique');
$table->index('user_id');
});
Schema::create('collection_comments', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('parent_id')->nullable()->constrained('collection_comments')->cascadeOnDelete();
$table->text('body');
$table->text('rendered_body');
$table->enum('status', ['visible', 'hidden', 'flagged'])->default('visible');
$table->timestamps();
$table->softDeletes();
$table->index(['collection_id', 'status'], 'collection_comments_status_idx');
});
}
public function down(): void
{
Schema::dropIfExists('collection_comments');
Schema::dropIfExists('collection_saves');
Schema::dropIfExists('collection_submissions');
Schema::dropIfExists('collection_members');
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex('collections_type_visibility_mod_idx');
$table->dropIndex('collections_collab_submission_idx');
$table->dropColumn([
'type',
'collaboration_mode',
'allow_submissions',
'allow_comments',
'allow_saves',
'moderation_status',
'comments_count',
'saves_count',
'collaborators_count',
'published_at',
'unpublished_at',
'event_key',
'event_label',
'season_key',
'badge_label',
]);
});
}
};

View File

@@ -0,0 +1,31 @@
<?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('collections') || ! Schema::hasColumn('collections', 'moderation_status')) {
return;
}
if (DB::getDriverName() === 'mysql') {
DB::statement("ALTER TABLE collections MODIFY moderation_status ENUM('active','under_review','restricted','hidden') NOT NULL DEFAULT 'active'");
}
}
public function down(): void
{
if (! Schema::hasTable('collections') || ! Schema::hasColumn('collections', 'moderation_status')) {
return;
}
if (DB::getDriverName() === 'mysql') {
DB::statement("UPDATE collections SET moderation_status = 'active' WHERE moderation_status IN ('under_review', 'restricted')");
DB::statement("ALTER TABLE collections MODIFY moderation_status ENUM('active','review','hidden') NOT NULL DEFAULT 'active'");
}
}
};

View File

@@ -0,0 +1,47 @@
<?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::table('collections', function (Blueprint $table): void {
$table->foreignId('managed_by_user_id')
->nullable()
->after('user_id')
->constrained('users')
->nullOnDelete();
$table->enum('editorial_owner_mode', ['creator', 'staff_account', 'system'])
->default('creator')
->after('type');
$table->foreignId('editorial_owner_user_id')
->nullable()
->after('managed_by_user_id')
->constrained('users')
->nullOnDelete();
$table->string('editorial_owner_label', 120)
->nullable()
->after('editorial_owner_user_id');
$table->json('layout_modules_json')
->nullable()
->after('smart_rules_json');
$table->index('managed_by_user_id', 'collections_managed_by_user_idx');
$table->index(['type', 'editorial_owner_mode'], 'collections_editorial_owner_mode_idx');
});
}
public function down(): void
{
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex('collections_managed_by_user_idx');
$table->dropIndex('collections_editorial_owner_mode_idx');
$table->dropConstrainedForeignId('managed_by_user_id');
$table->dropConstrainedForeignId('editorial_owner_user_id');
$table->dropColumn(['editorial_owner_mode', 'editorial_owner_label', 'layout_modules_json']);
});
}
};

View File

@@ -0,0 +1,24 @@
<?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::table('collection_members', function (Blueprint $table): void {
$table->timestamp('expires_at')->nullable()->after('invited_at');
$table->index('expires_at', 'collection_members_expires_at_idx');
});
}
public function down(): void
{
Schema::table('collection_members', function (Blueprint $table): void {
$table->dropIndex('collection_members_expires_at_idx');
$table->dropColumn('expires_at');
});
}
};

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::table('collections', function (Blueprint $table): void {
$table->string('banner_text', 200)->nullable()->after('season_key');
$table->string('spotlight_style', 40)->default('default')->after('badge_label');
});
}
public function down(): void
{
Schema::table('collections', function (Blueprint $table): void {
$table->dropColumn([
'banner_text',
'spotlight_style',
]);
});
}
};

View File

@@ -0,0 +1,162 @@
<?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::table('collections', function (Blueprint $table): void {
$table->enum('lifecycle_state', ['draft', 'scheduled', 'published', 'featured', 'archived', 'hidden', 'restricted', 'under_review', 'expired'])
->default('draft')
->after('slug');
$table->decimal('quality_score', 6, 2)->nullable()->after('spotlight_style');
$table->decimal('ranking_score', 6, 2)->nullable()->after('quality_score');
$table->boolean('analytics_enabled')->default(true)->after('ranking_score');
$table->enum('presentation_style', ['standard', 'editorial_grid', 'hero_grid', 'masonry'])->default('standard')->after('analytics_enabled');
$table->enum('emphasis_mode', ['cover_heavy', 'balanced', 'artwork_first'])->default('balanced')->after('presentation_style');
$table->string('theme_token', 40)->nullable()->after('emphasis_mode');
$table->string('series_key', 80)->nullable()->after('theme_token');
$table->unsignedInteger('series_order')->nullable()->after('series_key');
$table->string('campaign_key', 80)->nullable()->after('series_order');
$table->string('campaign_label', 120)->nullable()->after('campaign_key');
$table->boolean('commercial_eligibility')->default(false)->after('campaign_label');
$table->string('promotion_tier', 40)->nullable()->after('commercial_eligibility');
$table->string('sponsorship_label', 120)->nullable()->after('promotion_tier');
$table->string('partner_label', 120)->nullable()->after('sponsorship_label');
$table->string('monetization_ready_status', 40)->nullable()->after('partner_label');
$table->string('brand_safe_status', 40)->nullable()->after('monetization_ready_status');
$table->timestamp('archived_at')->nullable()->after('unpublished_at');
$table->timestamp('expired_at')->nullable()->after('archived_at');
$table->unsignedInteger('history_count')->default(0)->after('expired_at');
$table->index(['lifecycle_state', 'moderation_status', 'visibility'], 'collections_lifecycle_visibility_idx');
$table->index(['series_key', 'series_order'], 'collections_series_idx');
$table->index(['campaign_key', 'lifecycle_state'], 'collections_campaign_lifecycle_idx');
});
Schema::create('collection_history', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('actor_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('action_type', 80);
$table->string('summary', 255)->nullable();
$table->json('before_json')->nullable();
$table->json('after_json')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index('collection_id');
$table->index('action_type');
$table->index('actor_user_id');
});
Schema::create('collection_surface_definitions', function (Blueprint $table): void {
$table->id();
$table->string('surface_key', 120)->unique();
$table->string('title', 120);
$table->string('description', 255)->nullable();
$table->enum('mode', ['manual', 'automatic', 'hybrid'])->default('manual');
$table->json('rules_json')->nullable();
$table->string('ranking_mode', 60)->nullable();
$table->unsignedInteger('max_items')->default(12);
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('collection_surface_placements', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->string('surface_key', 120);
$table->string('placement_type', 60);
$table->unsignedInteger('priority')->default(0);
$table->timestamp('starts_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->boolean('is_active')->default(true);
$table->string('campaign_key', 80)->nullable();
$table->text('notes')->nullable();
$table->foreignId('created_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->index('surface_key');
$table->index('starts_at');
$table->index('ends_at');
$table->index('is_active');
});
Schema::create('collection_daily_stats', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->date('stat_date');
$table->unsignedInteger('views_count')->default(0);
$table->unsignedInteger('likes_count')->default(0);
$table->unsignedInteger('follows_count')->default(0);
$table->unsignedInteger('saves_count')->default(0);
$table->unsignedInteger('comments_count')->default(0);
$table->unsignedInteger('shares_count')->default(0);
$table->unsignedInteger('submissions_count')->default(0);
$table->timestamps();
$table->unique(['collection_id', 'stat_date'], 'collection_daily_stats_unique');
});
Schema::create('collection_saved_lists', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('title', 120);
$table->string('slug', 140);
$table->timestamps();
$table->unique(['user_id', 'slug'], 'collection_saved_lists_user_slug_unique');
});
Schema::create('collection_saved_list_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('saved_list_id')->constrained('collection_saved_lists')->cascadeOnDelete();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->unsignedInteger('order_num')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->unique(['saved_list_id', 'collection_id'], 'collection_saved_list_items_unique');
});
}
public function down(): void
{
Schema::dropIfExists('collection_saved_list_items');
Schema::dropIfExists('collection_saved_lists');
Schema::dropIfExists('collection_daily_stats');
Schema::dropIfExists('collection_surface_placements');
Schema::dropIfExists('collection_surface_definitions');
Schema::dropIfExists('collection_history');
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex('collections_lifecycle_visibility_idx');
$table->dropIndex('collections_series_idx');
$table->dropIndex('collections_campaign_lifecycle_idx');
$table->dropColumn([
'lifecycle_state',
'quality_score',
'ranking_score',
'analytics_enabled',
'presentation_style',
'emphasis_mode',
'theme_token',
'series_key',
'series_order',
'campaign_key',
'campaign_label',
'commercial_eligibility',
'promotion_tier',
'sponsorship_label',
'partner_label',
'monetization_ready_status',
'brand_safe_status',
'archived_at',
'expired_at',
'history_count',
]);
});
}
};

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
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::table('collection_surface_definitions', function (Blueprint $table): void {
$table->timestamp('starts_at')->nullable()->after('is_active');
$table->timestamp('ends_at')->nullable()->after('starts_at');
$table->string('fallback_surface_key', 120)->nullable()->after('ends_at');
$table->index('starts_at');
$table->index('ends_at');
$table->index('fallback_surface_key');
});
}
public function down(): void
{
Schema::table('collection_surface_definitions', function (Blueprint $table): void {
$table->dropIndex(['starts_at']);
$table->dropIndex(['ends_at']);
$table->dropIndex(['fallback_surface_key']);
$table->dropColumn(['starts_at', 'ends_at', 'fallback_surface_key']);
});
}
};

View File

@@ -0,0 +1,28 @@
<?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('collection_related_links', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('related_collection_id')->constrained('collections')->cascadeOnDelete();
$table->unsignedInteger('sort_order')->default(0);
$table->foreignId('created_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->unique(['collection_id', 'related_collection_id'], 'collection_related_links_unique');
$table->index(['collection_id', 'sort_order'], 'collection_related_links_sort_idx');
});
}
public function down(): void
{
Schema::dropIfExists('collection_related_links');
}
};

View File

@@ -0,0 +1,23 @@
<?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::table('collections', function (Blueprint $table): void {
$table->string('series_title', 160)->nullable()->after('series_key');
$table->text('series_description')->nullable()->after('series_title');
});
}
public function down(): void
{
Schema::table('collections', function (Blueprint $table): void {
$table->dropColumn(['series_title', 'series_description']);
});
}
};

View File

@@ -0,0 +1,23 @@
<?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::table('collections', function (Blueprint $table): void {
$table->text('editorial_notes')->nullable()->after('brand_safe_status');
$table->text('staff_commercial_notes')->nullable()->after('editorial_notes');
});
}
public function down(): void
{
Schema::table('collections', function (Blueprint $table): void {
$table->dropColumn(['editorial_notes', 'staff_commercial_notes']);
});
}
};

View File

@@ -0,0 +1,162 @@
<?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::table('collections', function (Blueprint $table): void {
$table->string('workflow_state', 40)->nullable()->after('lifecycle_state');
$table->string('readiness_state', 40)->nullable()->after('workflow_state');
$table->string('health_state', 40)->nullable()->after('readiness_state');
$table->json('health_flags_json')->nullable()->after('health_state');
$table->foreignId('canonical_collection_id')->nullable()->after('health_flags_json')->constrained('collections')->nullOnDelete();
$table->string('duplicate_cluster_key', 120)->nullable()->after('canonical_collection_id');
$table->string('program_key', 80)->nullable()->after('duplicate_cluster_key');
$table->string('partner_key', 80)->nullable()->after('program_key');
$table->string('trust_tier', 40)->nullable()->after('partner_key');
$table->string('experiment_key', 80)->nullable()->after('trust_tier');
$table->string('recommendation_tier', 40)->nullable()->after('experiment_key');
$table->string('ranking_bucket', 40)->nullable()->after('recommendation_tier');
$table->string('search_boost_tier', 40)->nullable()->after('ranking_bucket');
$table->decimal('metadata_completeness_score', 6, 2)->nullable()->after('search_boost_tier');
$table->decimal('editorial_readiness_score', 6, 2)->nullable()->after('metadata_completeness_score');
$table->decimal('freshness_score', 6, 2)->nullable()->after('editorial_readiness_score');
$table->decimal('engagement_score', 6, 2)->nullable()->after('freshness_score');
$table->decimal('health_score', 6, 2)->nullable()->after('engagement_score');
$table->timestamp('last_health_check_at')->nullable()->after('health_score');
$table->timestamp('last_recommendation_refresh_at')->nullable()->after('last_health_check_at');
$table->boolean('placement_eligibility')->default(true)->after('last_recommendation_refresh_at');
$table->index(['workflow_state', 'readiness_state'], 'collections_workflow_readiness_idx');
$table->index(['health_state', 'placement_eligibility'], 'collections_health_eligibility_idx');
$table->index(['program_key', 'placement_eligibility'], 'collections_program_eligibility_idx');
$table->index(['partner_key', 'trust_tier'], 'collections_partner_trust_idx');
$table->index(['canonical_collection_id', 'duplicate_cluster_key'], 'collections_canonical_duplicate_idx');
});
Schema::create('collection_program_assignments', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->string('program_key', 80);
$table->string('campaign_key', 80)->nullable();
$table->string('placement_scope', 80)->nullable();
$table->timestamp('starts_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->unsignedInteger('priority')->default(0);
$table->text('notes')->nullable();
$table->foreignId('created_by_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
$table->index(['program_key', 'priority'], 'collection_program_assignments_program_priority_idx');
$table->index(['campaign_key', 'placement_scope'], 'collection_program_assignments_campaign_scope_idx');
});
Schema::create('collection_quality_snapshots', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->date('snapshot_date');
$table->decimal('quality_score', 6, 2)->nullable();
$table->decimal('health_score', 6, 2)->nullable();
$table->decimal('metadata_completeness_score', 6, 2)->nullable();
$table->decimal('freshness_score', 6, 2)->nullable();
$table->decimal('engagement_score', 6, 2)->nullable();
$table->decimal('readiness_score', 6, 2)->nullable();
$table->json('flags_json')->nullable();
$table->timestamps();
$table->unique(['collection_id', 'snapshot_date'], 'collection_quality_snapshots_unique');
});
Schema::create('collection_recommendation_snapshots', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->string('context_key', 80);
$table->decimal('recommendation_score', 8, 2)->nullable();
$table->json('rationale_json')->nullable();
$table->date('snapshot_date');
$table->timestamps();
$table->index(['context_key', 'snapshot_date'], 'collection_recommendation_snapshots_context_date_idx');
});
Schema::create('collection_merge_actions', function (Blueprint $table): void {
$table->id();
$table->foreignId('source_collection_id')->constrained('collections')->cascadeOnDelete();
$table->foreignId('target_collection_id')->constrained('collections')->cascadeOnDelete();
$table->enum('action_type', ['suggested', 'approved', 'completed', 'rejected', 'reverted']);
$table->foreignId('actor_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('summary', 255)->nullable();
$table->timestamps();
$table->index(['source_collection_id', 'target_collection_id'], 'collection_merge_actions_pair_idx');
});
Schema::create('collection_entity_links', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->string('linked_type', 80);
$table->unsignedBigInteger('linked_id');
$table->string('relationship_type', 80);
$table->json('metadata_json')->nullable();
$table->timestamps();
$table->index(['linked_type', 'linked_id'], 'collection_entity_links_target_idx');
$table->index(['collection_id', 'relationship_type'], 'collection_entity_links_relationship_idx');
});
Schema::create('collection_saved_notes', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('collection_id')->constrained('collections')->cascadeOnDelete();
$table->text('note')->nullable();
$table->timestamps();
$table->unique(['user_id', 'collection_id'], 'collection_saved_notes_user_collection_unique');
});
}
public function down(): void
{
Schema::dropIfExists('collection_saved_notes');
Schema::dropIfExists('collection_entity_links');
Schema::dropIfExists('collection_merge_actions');
Schema::dropIfExists('collection_recommendation_snapshots');
Schema::dropIfExists('collection_quality_snapshots');
Schema::dropIfExists('collection_program_assignments');
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex('collections_workflow_readiness_idx');
$table->dropIndex('collections_health_eligibility_idx');
$table->dropIndex('collections_program_eligibility_idx');
$table->dropIndex('collections_partner_trust_idx');
$table->dropIndex('collections_canonical_duplicate_idx');
$table->dropConstrainedForeignId('canonical_collection_id');
$table->dropColumn([
'workflow_state',
'readiness_state',
'health_state',
'health_flags_json',
'duplicate_cluster_key',
'program_key',
'partner_key',
'trust_tier',
'experiment_key',
'recommendation_tier',
'ranking_bucket',
'search_boost_tier',
'metadata_completeness_score',
'editorial_readiness_score',
'freshness_score',
'engagement_score',
'health_score',
'last_health_check_at',
'last_recommendation_refresh_at',
'placement_eligibility',
]);
});
}
};

View File

@@ -0,0 +1,37 @@
<?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::table('collections', function (Blueprint $table): void {
$table->string('experiment_treatment', 80)->nullable()->after('experiment_key');
$table->string('placement_variant', 80)->nullable()->after('experiment_treatment');
$table->string('ranking_mode_variant', 80)->nullable()->after('placement_variant');
$table->string('collection_pool_version', 80)->nullable()->after('ranking_mode_variant');
$table->string('test_label', 120)->nullable()->after('collection_pool_version');
$table->index(['experiment_key', 'experiment_treatment'], 'collections_experiment_treatment_idx');
$table->index(['placement_variant', 'ranking_mode_variant'], 'collections_experiment_variants_idx');
});
}
public function down(): void
{
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex('collections_experiment_treatment_idx');
$table->dropIndex('collections_experiment_variants_idx');
$table->dropColumn([
'experiment_treatment',
'placement_variant',
'ranking_mode_variant',
'collection_pool_version',
'test_label',
]);
});
}
};

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
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::table('collections', function (Blueprint $table): void {
$table->string('sponsorship_state', 40)->nullable()->after('sponsorship_label');
$table->string('ownership_domain', 80)->nullable()->after('partner_label');
$table->string('commercial_review_state', 40)->nullable()->after('ownership_domain');
$table->string('legal_review_state', 40)->nullable()->after('commercial_review_state');
$table->index('sponsorship_state');
$table->index('ownership_domain');
});
}
public function down(): void
{
Schema::table('collections', function (Blueprint $table): void {
$table->dropIndex(['sponsorship_state']);
$table->dropIndex(['ownership_domain']);
$table->dropColumn([
'sponsorship_state',
'ownership_domain',
'commercial_review_state',
'legal_review_state',
]);
});
}
};

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
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::table('collection_saves', function (Blueprint $table): void {
$table->timestamp('last_viewed_at')->nullable()->after('created_at');
$table->string('save_context', 80)->nullable()->after('last_viewed_at');
$table->json('save_context_meta_json')->nullable()->after('save_context');
$table->index(['user_id', 'last_viewed_at'], 'collection_saves_user_last_viewed_idx');
});
}
public function down(): void
{
Schema::table('collection_saves', function (Blueprint $table): void {
$table->dropIndex('collection_saves_user_last_viewed_idx');
$table->dropColumn([
'last_viewed_at',
'save_context',
'save_context_meta_json',
]);
});
}
};

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
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('nova_card_categories', function (Blueprint $table): void {
$table->id();
$table->string('slug', 120)->unique();
$table->string('name', 120);
$table->text('description')->nullable();
$table->boolean('active')->default(true);
$table->integer('order_num')->default(0);
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('nova_card_categories');
}
};

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
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('nova_card_templates', function (Blueprint $table): void {
$table->id();
$table->string('slug', 120)->unique();
$table->string('name', 120);
$table->text('description')->nullable();
$table->string('preview_image')->nullable();
$table->json('config_json');
$table->json('supported_formats');
$table->boolean('active')->default(true);
$table->boolean('official')->default(true);
$table->integer('order_num')->default(0);
$table->timestamps();
$table->softDeletes();
});
}
public function down(): void
{
Schema::dropIfExists('nova_card_templates');
}
};

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
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('nova_card_tags', function (Blueprint $table): void {
$table->id();
$table->string('slug', 120)->unique();
$table->string('name', 120);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('nova_card_tags');
}
};

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
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('nova_card_backgrounds', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('original_path');
$table->string('processed_path')->nullable();
$table->unsignedInteger('width')->nullable();
$table->unsignedInteger('height')->nullable();
$table->string('mime_type', 120);
$table->unsignedBigInteger('file_size');
$table->string('sha256', 64)->nullable()->index();
$table->enum('visibility', ['private', 'card-only', 'public'])->default('card-only');
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'visibility']);
});
}
public function down(): void
{
Schema::dropIfExists('nova_card_backgrounds');
}
};

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
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('nova_cards', function (Blueprint $table): void {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('category_id')->nullable()->constrained('nova_card_categories')->nullOnDelete();
$table->string('title', 120);
$table->string('slug', 160);
$table->text('quote_text');
$table->string('quote_author', 160)->nullable();
$table->string('quote_source', 180)->nullable();
$table->text('description')->nullable();
$table->enum('format', ['square', 'portrait', 'story', 'landscape'])->default('square');
$table->json('project_json');
$table->unsignedInteger('render_version')->default(1);
$table->string('preview_path')->nullable();
$table->unsignedInteger('preview_width')->nullable();
$table->unsignedInteger('preview_height')->nullable();
$table->enum('background_type', ['gradient', 'upload', 'template', 'solid'])->default('gradient');
$table->foreignId('background_image_id')->nullable()->constrained('nova_card_backgrounds')->nullOnDelete();
$table->foreignId('template_id')->nullable()->constrained('nova_card_templates')->nullOnDelete();
$table->enum('visibility', ['public', 'unlisted', 'private'])->default('private');
$table->enum('status', ['draft', 'processing', 'published', 'hidden', 'rejected'])->default('draft');
$table->enum('moderation_status', ['pending', 'approved', 'flagged', 'rejected'])->default('pending');
$table->boolean('featured')->default(false);
$table->boolean('allow_download')->default(true);
$table->unsignedInteger('views_count')->default(0);
$table->unsignedInteger('shares_count')->default(0);
$table->unsignedInteger('downloads_count')->default(0);
$table->timestamp('published_at')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index('user_id');
$table->index('status');
$table->index('moderation_status');
$table->index('category_id');
$table->index('published_at');
$table->index('featured');
$table->index(['visibility', 'status', 'published_at']);
});
Schema::create('nova_card_tag_relation', function (Blueprint $table): void {
$table->id();
$table->foreignId('card_id')->constrained('nova_cards')->cascadeOnDelete();
$table->foreignId('tag_id')->constrained('nova_card_tags')->cascadeOnDelete();
$table->timestamps();
$table->unique(['card_id', 'tag_id']);
});
}
public function down(): void
{
Schema::dropIfExists('nova_card_tag_relation');
Schema::dropIfExists('nova_cards');
}
};

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
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::table('nova_cards', function (Blueprint $table): void {
$table->unsignedSmallInteger('schema_version')->default(1)->after('project_json');
$table->foreignId('original_card_id')->nullable()->after('background_image_id')->constrained('nova_cards')->nullOnDelete();
$table->foreignId('root_card_id')->nullable()->after('original_card_id')->constrained('nova_cards')->nullOnDelete();
$table->boolean('allow_remix')->default(true)->after('allow_download');
$table->unsignedInteger('likes_count')->default(0)->after('downloads_count');
$table->unsignedInteger('favorites_count')->default(0)->after('likes_count');
$table->unsignedInteger('saves_count')->default(0)->after('favorites_count');
$table->unsignedInteger('remixes_count')->default(0)->after('saves_count');
$table->unsignedInteger('challenge_entries_count')->default(0)->after('remixes_count');
$table->timestamp('last_engaged_at')->nullable()->after('published_at');
$table->index('schema_version');
$table->index('original_card_id');
$table->index('root_card_id');
$table->index('likes_count');
$table->index('favorites_count');
$table->index('saves_count');
$table->index('remixes_count');
});
Schema::create('nova_card_reactions', function (Blueprint $table): void {
$table->id();
$table->foreignId('card_id')->constrained('nova_cards')->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('type', 24);
$table->timestamps();
$table->unique(['card_id', 'user_id', 'type']);
$table->index(['type', 'created_at']);
});
Schema::create('nova_card_collections', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('slug', 140);
$table->string('name', 120);
$table->text('description')->nullable();
$table->enum('visibility', ['private', 'public'])->default('private');
$table->boolean('official')->default(false);
$table->unsignedInteger('cards_count')->default(0);
$table->timestamps();
$table->unique(['user_id', 'slug']);
$table->index(['visibility', 'updated_at']);
});
Schema::create('nova_card_collection_items', function (Blueprint $table): void {
$table->id();
$table->foreignId('collection_id')->constrained('nova_card_collections')->cascadeOnDelete();
$table->foreignId('card_id')->constrained('nova_cards')->cascadeOnDelete();
$table->text('note')->nullable();
$table->unsignedInteger('sort_order')->default(0);
$table->timestamps();
$table->unique(['collection_id', 'card_id']);
$table->index(['card_id', 'created_at']);
});
Schema::create('nova_card_versions', function (Blueprint $table): void {
$table->id();
$table->foreignId('card_id')->constrained('nova_cards')->cascadeOnDelete();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->unsignedInteger('version_number');
$table->string('label', 120)->nullable();
$table->string('snapshot_hash', 64);
$table->json('snapshot_json');
$table->timestamps();
$table->unique(['card_id', 'version_number']);
$table->index(['card_id', 'created_at']);
$table->index('snapshot_hash');
});
Schema::create('nova_card_challenges', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('slug', 140)->unique();
$table->string('title', 140);
$table->text('description')->nullable();
$table->text('prompt')->nullable();
$table->json('rules_json')->nullable();
$table->enum('status', ['draft', 'active', 'completed', 'archived'])->default('draft');
$table->boolean('official')->default(false);
$table->boolean('featured')->default(false);
$table->foreignId('winner_card_id')->nullable()->constrained('nova_cards')->nullOnDelete();
$table->unsignedInteger('entries_count')->default(0);
$table->timestamp('starts_at')->nullable();
$table->timestamp('ends_at')->nullable();
$table->timestamps();
$table->index(['status', 'featured', 'starts_at']);
});
Schema::create('nova_card_challenge_entries', function (Blueprint $table): void {
$table->id();
$table->foreignId('challenge_id')->constrained('nova_card_challenges')->cascadeOnDelete();
$table->foreignId('card_id')->constrained('nova_cards')->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('status', ['active', 'hidden', 'rejected', 'featured', 'winner', 'submitted'])->default('active');
$table->text('note')->nullable();
$table->timestamps();
$table->unique(['challenge_id', 'card_id']);
$table->index(['user_id', 'created_at']);
});
Schema::create('nova_card_asset_packs', function (Blueprint $table): void {
$table->id();
$table->string('slug', 140)->unique();
$table->string('name', 120);
$table->text('description')->nullable();
$table->enum('type', ['asset', 'template'])->default('asset');
$table->string('preview_image')->nullable();
$table->json('manifest_json')->nullable();
$table->boolean('official')->default(false);
$table->boolean('active')->default(true);
$table->unsignedInteger('order_num')->default(0);
$table->timestamps();
$table->index(['type', 'active', 'official', 'order_num']);
});
Schema::create('nova_card_assets', function (Blueprint $table): void {
$table->id();
$table->foreignId('asset_pack_id')->nullable()->constrained('nova_card_asset_packs')->nullOnDelete();
$table->string('asset_key', 80);
$table->string('label', 120);
$table->string('type', 32)->default('glyph');
$table->string('preview_image')->nullable();
$table->json('data_json')->nullable();
$table->boolean('official')->default(false);
$table->boolean('active')->default(true);
$table->unsignedInteger('order_num')->default(0);
$table->timestamps();
$table->unique(['asset_pack_id', 'asset_key']);
$table->index(['type', 'active', 'order_num']);
});
}
public function down(): void
{
Schema::dropIfExists('nova_card_assets');
Schema::dropIfExists('nova_card_asset_packs');
Schema::dropIfExists('nova_card_challenge_entries');
Schema::dropIfExists('nova_card_challenges');
Schema::dropIfExists('nova_card_versions');
Schema::dropIfExists('nova_card_collection_items');
Schema::dropIfExists('nova_card_collections');
Schema::dropIfExists('nova_card_reactions');
Schema::table('nova_cards', function (Blueprint $table): void {
$table->dropConstrainedForeignId('original_card_id');
$table->dropConstrainedForeignId('root_card_id');
$table->dropColumn([
'schema_version',
'allow_remix',
'likes_count',
'favorites_count',
'saves_count',
'remixes_count',
'challenge_entries_count',
'last_engaged_at',
]);
});
}
};

View File

@@ -0,0 +1,104 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const CURRENT_TARGET_TYPES = [
'message',
'conversation',
'user',
'story',
'collection',
'collection_comment',
'collection_submission',
'nova_card',
'nova_card_challenge',
'nova_card_challenge_entry',
];
private const PREVIOUS_TARGET_TYPES = [
'message',
'conversation',
'user',
'story',
'collection',
'collection_comment',
'collection_submission',
];
public function up(): void
{
$this->syncTargetTypes(self::CURRENT_TARGET_TYPES);
}
public function down(): void
{
$this->syncTargetTypes(self::PREVIOUS_TARGET_TYPES);
}
/**
* @param array<int, string> $targetTypes
*/
private function syncTargetTypes(array $targetTypes): void
{
if (! Schema::hasTable('reports') || ! Schema::hasColumn('reports', 'target_type')) {
return;
}
$driver = DB::getDriverName();
if ($driver === 'mysql') {
$allowed = implode("','", $targetTypes);
DB::statement("ALTER TABLE reports MODIFY target_type ENUM('{$allowed}') NOT NULL");
return;
}
if ($driver === 'sqlite') {
$this->rebuildSqliteReportsTable($targetTypes);
}
}
/**
* @param array<int, string> $targetTypes
*/
private function rebuildSqliteReportsTable(array $targetTypes): void
{
$quotedTargetTypes = collect($targetTypes)
->map(fn (string $type): string => "'{$type}'")
->implode(', ');
DB::statement('DROP TABLE IF EXISTS reports_temp');
DB::statement(
<<<SQL
CREATE TABLE reports_temp (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
reporter_id INTEGER NOT NULL,
target_type VARCHAR NOT NULL CHECK (target_type IN ({$quotedTargetTypes})),
target_id INTEGER NOT NULL,
reason VARCHAR(120) NOT NULL,
details TEXT NULL,
status VARCHAR NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'reviewing', 'closed')),
created_at DATETIME NULL,
updated_at DATETIME NULL,
FOREIGN KEY (reporter_id) REFERENCES users(id) ON DELETE CASCADE
)
SQL
);
DB::statement(
'INSERT INTO reports_temp (id, reporter_id, target_type, target_id, reason, details, status, created_at, updated_at) '
.'SELECT id, reporter_id, target_type, target_id, reason, details, status, created_at, updated_at FROM reports'
);
DB::statement('DROP TABLE reports');
DB::statement('ALTER TABLE reports_temp RENAME TO reports');
DB::statement('CREATE INDEX reports_target_type_target_id_index ON reports (target_type, target_id)');
DB::statement('CREATE INDEX reports_status_index ON reports (status)');
DB::statement('CREATE INDEX reports_reporter_id_index ON reports (reporter_id)');
}
};

View File

@@ -0,0 +1,70 @@
<?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
{
if (Schema::hasTable('reports')) {
Schema::table('reports', function (Blueprint $table): void {
if (! Schema::hasColumn('reports', 'moderator_note')) {
$table->text('moderator_note')->nullable()->after('details');
}
if (! Schema::hasColumn('reports', 'last_moderated_by_id')) {
$table->foreignId('last_moderated_by_id')->nullable()->after('status')->constrained('users')->nullOnDelete();
}
if (! Schema::hasColumn('reports', 'last_moderated_at')) {
$table->timestamp('last_moderated_at')->nullable()->after('last_moderated_by_id');
}
});
}
if (Schema::hasTable('report_history')) {
return;
}
Schema::create('report_history', function (Blueprint $table): void {
$table->id();
$table->foreignId('report_id')->constrained('reports')->cascadeOnDelete();
$table->foreignId('actor_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->string('action_type', 80);
$table->string('summary', 255)->nullable();
$table->text('note')->nullable();
$table->json('before_json')->nullable();
$table->json('after_json')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index('report_id');
$table->index('action_type');
$table->index('actor_user_id');
});
}
public function down(): void
{
Schema::dropIfExists('report_history');
if (! Schema::hasTable('reports')) {
return;
}
Schema::table('reports', function (Blueprint $table): void {
if (Schema::hasColumn('reports', 'last_moderated_by_id')) {
$table->dropConstrainedForeignId('last_moderated_by_id');
}
if (Schema::hasColumn('reports', 'last_moderated_at')) {
$table->dropColumn('last_moderated_at');
}
if (Schema::hasColumn('reports', 'moderator_note')) {
$table->dropColumn('moderator_note');
}
});
}
};

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
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::table('nova_cards', function (Blueprint $table): void {
$table->decimal('trending_score', 12, 4)->default(0)->after('challenge_entries_count');
$table->index(['trending_score', 'published_at']);
});
}
public function down(): void
{
Schema::table('nova_cards', function (Blueprint $table): void {
$table->dropIndex(['trending_score', 'published_at']);
$table->dropColumn('trending_score');
});
}
};

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
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('nova_card_comments', function (Blueprint $table): void {
$table->id();
$table->foreignId('card_id')->constrained('nova_cards')->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('parent_id')->nullable()->constrained('nova_card_comments')->nullOnDelete();
$table->text('body');
$table->text('rendered_body')->nullable();
$table->string('status', 24)->default('visible');
$table->timestamps();
$table->softDeletes();
$table->index(['card_id', 'status', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('nova_card_comments');
}
};

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('nova_cards', function (Blueprint $table): void {
$table->unsignedInteger('comments_count')->default(0)->after('remixes_count');
$table->index('comments_count');
});
Schema::table('nova_card_collections', function (Blueprint $table): void {
$table->boolean('featured')->default(false)->after('official');
$table->index(['featured', 'updated_at']);
});
DB::table('nova_card_challenge_entries')
->where('status', 'submitted')
->update(['status' => 'active']);
$driver = Schema::getConnection()->getDriverName();
if (in_array($driver, ['mysql', 'mariadb'], true)) {
DB::statement("ALTER TABLE nova_card_challenge_entries MODIFY status ENUM('active','hidden','rejected','featured','winner','submitted') NOT NULL DEFAULT 'active'");
}
}
public function down(): void
{
$driver = Schema::getConnection()->getDriverName();
if (in_array($driver, ['mysql', 'mariadb'], true)) {
DB::statement("ALTER TABLE nova_card_challenge_entries MODIFY status ENUM('submitted','featured','winner') NOT NULL DEFAULT 'submitted'");
}
DB::table('nova_card_challenge_entries')
->where('status', 'active')
->update(['status' => 'submitted']);
Schema::table('nova_card_collections', function (Blueprint $table): void {
$table->dropIndex(['featured', 'updated_at']);
$table->dropColumn('featured');
});
Schema::table('nova_cards', function (Blueprint $table): void {
$table->dropIndex(['comments_count']);
$table->dropColumn('comments_count');
});
}
};

View File

@@ -0,0 +1,190 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// v3 additions to nova_cards — guarded so a partially-applied migration can re-run safely
Schema::table('nova_cards', function (Blueprint $table): void {
if (! Schema::hasColumn('nova_cards', 'allow_background_reuse')) {
$table->boolean('allow_background_reuse')->default(true)->after('allow_remix');
}
if (! Schema::hasColumn('nova_cards', 'allow_export')) {
$table->boolean('allow_export')->default(true)->after('allow_background_reuse');
}
if (! Schema::hasColumn('nova_cards', 'style_family')) {
$table->string('style_family', 64)->nullable()->after('allow_export');
}
if (! Schema::hasColumn('nova_cards', 'palette_family')) {
$table->string('palette_family', 64)->nullable()->after('style_family');
}
if (! Schema::hasColumn('nova_cards', 'featured_score')) {
$table->decimal('featured_score', 12, 4)->nullable()->after('trending_score');
}
if (! Schema::hasColumn('nova_cards', 'density_score')) {
$table->tinyInteger('density_score')->unsigned()->nullable()->after('featured_score');
}
if (! Schema::hasColumn('nova_cards', 'editor_mode_last_used')) {
$table->string('editor_mode_last_used', 24)->nullable()->after('density_score');
}
if (! Schema::hasColumn('nova_cards', 'original_creator_id')) {
$table->foreignId('original_creator_id')->nullable()->after('root_card_id')->constrained('users')->nullOnDelete();
}
if (! Schema::hasColumn('nova_cards', 'last_ranked_at')) {
$table->timestamp('last_ranked_at')->nullable()->after('last_engaged_at');
}
if (! Schema::hasColumn('nova_cards', 'last_rendered_at')) {
$table->timestamp('last_rendered_at')->nullable()->after('last_ranked_at');
}
// Indexes — ignore errors if they already exist (handled below via DB::statement)
});
// Add indexes only if absent (guard against duplicate-index errors)
$hasIndex = static function (string $table, string $index): bool {
$driver = DB::getDriverName();
if ($driver === 'sqlite') {
return collect(DB::select("PRAGMA index_list('{$table}')"))
->contains(static fn (object $row): bool => ($row->name ?? null) === $index);
}
return collect(DB::select("SHOW INDEX FROM `{$table}` WHERE Key_name = ?", [$index]))->isNotEmpty();
};
if (! $hasIndex('nova_cards', 'nova_cards_style_family_index')) {
Schema::table('nova_cards', fn (Blueprint $t) => $t->index('style_family'));
}
if (! $hasIndex('nova_cards', 'nova_cards_palette_family_index')) {
Schema::table('nova_cards', fn (Blueprint $t) => $t->index('palette_family'));
}
if (! $hasIndex('nova_cards', 'nova_cards_featured_score_index')) {
Schema::table('nova_cards', fn (Blueprint $t) => $t->index('featured_score'));
}
if (! $hasIndex('nova_cards', 'nova_cards_original_creator_id_index')) {
Schema::table('nova_cards', fn (Blueprint $t) => $t->index('original_creator_id'));
}
// Creator presets table
if (! Schema::hasTable('nova_card_creator_presets')) {
Schema::create('nova_card_creator_presets', function (Blueprint $table): void {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('name', 120);
$table->enum('preset_type', ['style', 'layout', 'background', 'typography', 'starter'])->default('style');
$table->json('config_json');
$table->boolean('is_default')->default(false);
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'preset_type']);
$table->index(['user_id', 'is_default']);
});
}
// v3 enhancements to nova_card_collections
Schema::table('nova_card_collections', function (Blueprint $table): void {
if (! Schema::hasColumn('nova_card_collections', 'cover_card_id')) {
$table->unsignedBigInteger('cover_card_id')->nullable()->after('featured');
}
if (! Schema::hasColumn('nova_card_collections', 'cover_image_path')) {
$table->string('cover_image_path', 500)->nullable()->after('cover_card_id');
}
if (! Schema::hasColumn('nova_card_collections', 'intro_heading')) {
$table->string('intro_heading', 160)->nullable()->after('cover_image_path');
}
if (! Schema::hasColumn('nova_card_collections', 'sort_mode')) {
$table->string('sort_mode', 32)->default('manual')->after('intro_heading');
}
});
// v3 enhancements to nova_card_challenges
Schema::table('nova_card_challenges', function (Blueprint $table): void {
if (! Schema::hasColumn('nova_card_challenges', 'category')) {
$table->string('category', 80)->nullable()->after('featured');
}
if (! Schema::hasColumn('nova_card_challenges', 'series_slug')) {
$table->string('series_slug', 120)->nullable()->after('category');
}
if (! Schema::hasColumn('nova_card_challenges', 'edition_number')) {
$table->unsignedInteger('edition_number')->nullable()->after('series_slug');
}
if (! Schema::hasColumn('nova_card_challenges', 'cover_image_path')) {
$table->string('cover_image_path', 500)->nullable()->after('edition_number');
}
});
if (! $hasIndex('nova_card_challenges', 'nova_card_challenges_series_slug_index')) {
Schema::table('nova_card_challenges', fn (Blueprint $t) => $t->index('series_slug'));
}
if (! $hasIndex('nova_card_challenges', 'nova_card_challenges_category_index')) {
Schema::table('nova_card_challenges', fn (Blueprint $t) => $t->index('category'));
}
// Nova card export requests (for queued export generation)
if (! Schema::hasTable('nova_card_exports')) {
Schema::create('nova_card_exports', function (Blueprint $table): void {
$table->id();
$table->foreignId('card_id')->constrained('nova_cards')->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('export_type', ['preview', 'hires', 'square', 'story', 'wallpaper', 'og'])->default('preview');
$table->enum('status', ['pending', 'processing', 'ready', 'failed'])->default('pending');
$table->string('output_path', 500)->nullable();
$table->unsignedInteger('width')->nullable();
$table->unsignedInteger('height')->nullable();
$table->string('format', 12)->default('webp');
$table->json('options_json')->nullable();
$table->timestamp('ready_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->index(['card_id', 'status']);
$table->index(['user_id', 'created_at']);
});
}
}
public function down(): void
{
Schema::dropIfExists('nova_card_exports');
Schema::table('nova_card_challenges', function (Blueprint $table): void {
$table->dropIndex(['series_slug']);
$table->dropIndex(['category']);
$table->dropColumn(['category', 'series_slug', 'edition_number', 'cover_image_path']);
});
Schema::table('nova_card_collections', function (Blueprint $table): void {
$table->dropColumn(['cover_card_id', 'cover_image_path', 'intro_heading', 'sort_mode']);
});
Schema::dropIfExists('nova_card_creator_presets');
Schema::table('nova_cards', function (Blueprint $table): void {
$table->dropIndex(['style_family']);
$table->dropIndex(['palette_family']);
$table->dropIndex(['featured_score']);
$table->dropIndex(['original_creator_id']);
$table->dropForeign(['original_creator_id']);
$table->dropColumn([
'allow_background_reuse',
'allow_export',
'style_family',
'palette_family',
'featured_score',
'density_score',
'editor_mode_last_used',
'original_creator_id',
'last_ranked_at',
'last_rendered_at',
]);
});
}
};

View File

@@ -0,0 +1,22 @@
<?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::table('users', function (Blueprint $table): void {
$table->boolean('nova_featured_creator')->default(false)->after('role');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table): void {
$table->dropColumn('nova_featured_creator');
});
}
};

View File

@@ -0,0 +1,69 @@
<?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('artwork_ai_assists', function (Blueprint $table): void {
$table->id();
$table->foreignId('artwork_id')->unique()->constrained('artworks')->cascadeOnDelete();
$table->string('status', 24)->default('pending')->index();
$table->string('mode', 24)->nullable();
$table->json('title_suggestions_json')->nullable();
$table->json('description_suggestions_json')->nullable();
$table->json('tag_suggestions_json')->nullable();
$table->json('category_suggestions_json')->nullable();
$table->json('similar_candidates_json')->nullable();
$table->json('raw_response_json')->nullable();
$table->json('action_log_json')->nullable();
$table->text('error_message')->nullable();
$table->timestamp('processed_at')->nullable()->index();
$table->timestamps();
});
Schema::table('artworks', function (Blueprint $table): void {
if (! Schema::hasColumn('artworks', 'ai_status')) {
$table->string('ai_status', 24)->nullable()->after('last_vector_indexed_at');
}
if (! Schema::hasColumn('artworks', 'title_source')) {
$table->string('title_source', 24)->nullable()->after('ai_status');
}
if (! Schema::hasColumn('artworks', 'description_source')) {
$table->string('description_source', 24)->nullable()->after('title_source');
}
if (! Schema::hasColumn('artworks', 'tags_source')) {
$table->string('tags_source', 24)->nullable()->after('description_source');
}
if (! Schema::hasColumn('artworks', 'category_source')) {
$table->string('category_source', 24)->nullable()->after('tags_source');
}
});
}
public function down(): void
{
Schema::table('artworks', function (Blueprint $table): void {
$columns = [];
foreach (['ai_status', 'title_source', 'description_source', 'tags_source', 'category_source'] as $column) {
if (Schema::hasColumn('artworks', $column)) {
$columns[] = $column;
}
}
if ($columns !== []) {
$table->dropColumn($columns);
}
});
Schema::dropIfExists('artwork_ai_assists');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::table('artworks', function (Blueprint $table): void {
$table->string('visibility', 16)->default('public')->after('is_public');
$table->index('visibility');
});
DB::table('artworks')
->where('is_public', false)
->update(['visibility' => 'private']);
}
public function down(): void
{
Schema::table('artworks', function (Blueprint $table): void {
$table->dropIndex(['visibility']);
$table->dropColumn('visibility');
});
}
};

View File

@@ -0,0 +1,29 @@
<?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('artwork_ai_assist_events', function (Blueprint $table): void {
$table->id();
$table->foreignId('artwork_ai_assist_id')->nullable()->constrained('artwork_ai_assists')->nullOnDelete();
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->string('event_type', 64)->index();
$table->json('meta')->nullable();
$table->timestamps();
$table->index(['artwork_id', 'created_at'], 'artwork_ai_assist_events_artwork_created_idx');
$table->index(['user_id', 'created_at'], 'artwork_ai_assist_events_user_created_idx');
});
}
public function down(): void
{
Schema::dropIfExists('artwork_ai_assist_events');
}
};