optimizations
This commit is contained in:
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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)');
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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'");
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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)');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user