Commit workspace changes
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
<?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('groups', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('owner_user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->string('name', 80);
|
||||
$table->string('slug', 90)->unique();
|
||||
$table->string('headline', 160)->nullable();
|
||||
$table->text('bio')->nullable();
|
||||
$table->enum('visibility', ['public', 'private'])->default('public');
|
||||
$table->string('website_url', 2048)->nullable();
|
||||
$table->json('links_json')->nullable();
|
||||
$table->string('avatar_path')->nullable();
|
||||
$table->string('banner_path')->nullable();
|
||||
$table->unsignedInteger('artworks_count')->default(0);
|
||||
$table->unsignedInteger('collections_count')->default(0);
|
||||
$table->unsignedInteger('followers_count')->default(0);
|
||||
$table->timestamp('last_activity_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['visibility', 'followers_count']);
|
||||
$table->index(['owner_user_id', 'created_at']);
|
||||
});
|
||||
|
||||
Schema::create('group_members', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')
|
||||
->constrained('groups')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('invited_by_user_id')
|
||||
->nullable()
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
$table->enum('role', ['owner', 'admin', 'editor', 'member'])->default('member');
|
||||
$table->enum('status', ['pending', 'active', 'revoked'])->default('pending');
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamp('invited_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('accepted_at')->nullable();
|
||||
$table->timestamp('revoked_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_id', 'user_id'], 'group_members_group_user_unique');
|
||||
$table->index(['group_id', 'status', 'role'], 'group_members_status_role_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_follows', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')
|
||||
->constrained('groups')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_id', 'user_id'], 'group_follows_group_user_unique');
|
||||
});
|
||||
|
||||
Schema::create('artwork_contributors', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('artwork_id')
|
||||
->constrained('artworks')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['artwork_id', 'user_id'], 'artwork_contributors_artwork_user_unique');
|
||||
$table->index(['artwork_id', 'sort_order'], 'artwork_contributors_sort_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('artwork_contributors');
|
||||
Schema::dropIfExists('group_follows');
|
||||
Schema::dropIfExists('group_members');
|
||||
Schema::dropIfExists('groups');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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->foreignId('group_id')
|
||||
->nullable()
|
||||
->after('user_id')
|
||||
->constrained('groups')
|
||||
->nullOnDelete();
|
||||
$table->foreignId('uploaded_by_user_id')
|
||||
->nullable()
|
||||
->after('group_id')
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
$table->foreignId('primary_author_user_id')
|
||||
->nullable()
|
||||
->after('uploaded_by_user_id')
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index(['group_id', 'published_at'], 'artworks_group_published_idx');
|
||||
});
|
||||
|
||||
Schema::table('collections', function (Blueprint $table): void {
|
||||
$table->foreignId('group_id')
|
||||
->nullable()
|
||||
->after('user_id')
|
||||
->constrained('groups')
|
||||
->nullOnDelete();
|
||||
|
||||
$table->index(['group_id', 'visibility'], 'collections_group_visibility_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('collections', function (Blueprint $table): void {
|
||||
$table->dropIndex('collections_group_visibility_idx');
|
||||
$table->dropConstrainedForeignId('group_id');
|
||||
});
|
||||
|
||||
Schema::table('artworks', function (Blueprint $table): void {
|
||||
$table->dropIndex('artworks_group_published_idx');
|
||||
$table->dropConstrainedForeignId('primary_author_user_id');
|
||||
$table->dropConstrainedForeignId('uploaded_by_user_id');
|
||||
$table->dropConstrainedForeignId('group_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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('groups', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('groups', 'status')) {
|
||||
$table->string('status', 24)->default('active')->after('visibility');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('groups', 'membership_policy')) {
|
||||
$table->string('membership_policy', 32)->default('invite_only')->after('status');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('groups', 'type')) {
|
||||
$table->string('type', 80)->nullable()->after('bio');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('groups', 'founded_at')) {
|
||||
$table->timestamp('founded_at')->nullable()->after('owner_user_id');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('groups', 'featured_artwork_id')) {
|
||||
$table->foreignId('featured_artwork_id')->nullable()->after('owner_user_id')->constrained('artworks')->nullOnDelete();
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('groups', 'is_verified')) {
|
||||
$table->boolean('is_verified')->default(false)->after('featured_artwork_id');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('groups', 'deleted_at')) {
|
||||
$table->softDeletes();
|
||||
}
|
||||
});
|
||||
|
||||
if (DB::connection()->getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE `groups` MODIFY `visibility` ENUM('public','private','unlisted') NOT NULL DEFAULT 'public'");
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::connection()->getDriverName() === 'mysql') {
|
||||
DB::statement("ALTER TABLE `groups` MODIFY `visibility` ENUM('public','private') NOT NULL DEFAULT 'public'");
|
||||
}
|
||||
|
||||
Schema::table('groups', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('groups', 'deleted_at')) {
|
||||
$table->dropSoftDeletes();
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('groups', 'is_verified')) {
|
||||
$table->dropColumn('is_verified');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('groups', 'featured_artwork_id')) {
|
||||
$table->dropConstrainedForeignId('featured_artwork_id');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('groups', 'founded_at')) {
|
||||
$table->dropColumn('founded_at');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('groups', 'type')) {
|
||||
$table->dropColumn('type');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('groups', 'membership_policy')) {
|
||||
$table->dropColumn('membership_policy');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('groups', 'status')) {
|
||||
$table->dropColumn('status');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
use App\Models\GroupInvitation;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('group_invitations', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')
|
||||
->constrained('groups')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('invited_user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('invited_by_user_id')
|
||||
->nullable()
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
$table->foreignId('source_group_member_id')
|
||||
->nullable()
|
||||
->constrained('group_members')
|
||||
->nullOnDelete();
|
||||
$table->enum('role', ['admin', 'editor', 'member'])->default('member');
|
||||
$table->enum('status', [
|
||||
GroupInvitation::STATUS_PENDING,
|
||||
GroupInvitation::STATUS_ACCEPTED,
|
||||
GroupInvitation::STATUS_DECLINED,
|
||||
GroupInvitation::STATUS_REVOKED,
|
||||
GroupInvitation::STATUS_EXPIRED,
|
||||
])->default(GroupInvitation::STATUS_PENDING);
|
||||
$table->string('token', 80)->unique();
|
||||
$table->text('note')->nullable();
|
||||
$table->timestamp('invited_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('responded_at')->nullable();
|
||||
$table->timestamp('accepted_at')->nullable();
|
||||
$table->timestamp('revoked_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'status', 'role'], 'group_invitations_status_role_idx');
|
||||
$table->index(['invited_user_id', 'status'], 'group_invitations_user_status_idx');
|
||||
});
|
||||
|
||||
if (! Schema::hasTable('group_members')) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('group_members')
|
||||
->whereIn('status', ['pending', 'revoked'])
|
||||
->orderBy('id')
|
||||
->get()
|
||||
->each(function (object $member): void {
|
||||
$status = match ((string) $member->status) {
|
||||
'pending' => GroupInvitation::STATUS_PENDING,
|
||||
default => GroupInvitation::STATUS_REVOKED,
|
||||
};
|
||||
|
||||
DB::table('group_invitations')->insert([
|
||||
'group_id' => (int) $member->group_id,
|
||||
'invited_user_id' => (int) $member->user_id,
|
||||
'invited_by_user_id' => $member->invited_by_user_id ? (int) $member->invited_by_user_id : null,
|
||||
'source_group_member_id' => (int) $member->id,
|
||||
'role' => (string) $member->role,
|
||||
'status' => $status,
|
||||
'token' => Str::random(64),
|
||||
'note' => $member->note,
|
||||
'invited_at' => $member->invited_at,
|
||||
'expires_at' => $member->expires_at,
|
||||
'responded_at' => $member->accepted_at ?? $member->revoked_at,
|
||||
'accepted_at' => null,
|
||||
'revoked_at' => $member->revoked_at,
|
||||
'created_at' => $member->created_at ?? now(),
|
||||
'updated_at' => $member->updated_at ?? now(),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('group_invitations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?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('published_as_type', 16)
|
||||
->nullable()
|
||||
->after('primary_author_user_id');
|
||||
$table->unsignedBigInteger('published_as_id')
|
||||
->nullable()
|
||||
->after('published_as_type');
|
||||
|
||||
$table->index(['published_as_type', 'published_as_id'], 'artworks_published_as_idx');
|
||||
});
|
||||
|
||||
DB::table('artworks')
|
||||
->whereNotNull('group_id')
|
||||
->update([
|
||||
'published_as_type' => 'group',
|
||||
'published_as_id' => DB::raw('group_id'),
|
||||
]);
|
||||
|
||||
DB::table('artworks')
|
||||
->whereNull('published_as_id')
|
||||
->update([
|
||||
'published_as_type' => 'user',
|
||||
'published_as_id' => DB::raw('user_id'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table): void {
|
||||
$table->dropIndex('artworks_published_as_idx');
|
||||
$table->dropColumn(['published_as_type', 'published_as_id']);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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('artwork_contributors', function (Blueprint $table): void {
|
||||
$table->string('credit_role', 80)->nullable()->after('user_id');
|
||||
$table->boolean('is_primary')->default(false)->after('credit_role');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artwork_contributors', function (Blueprint $table): void {
|
||||
$table->dropColumn(['credit_role', 'is_primary']);
|
||||
});
|
||||
}
|
||||
};
|
||||
163
database/migrations/2026_04_04_000007_add_groups_v2_schema.php
Normal file
163
database/migrations/2026_04_04_000007_add_groups_v2_schema.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?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('group_join_requests', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')
|
||||
->constrained('groups')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->text('message')->nullable();
|
||||
$table->string('portfolio_url', 2048)->nullable();
|
||||
$table->string('desired_role', 32)->nullable();
|
||||
$table->json('skills_json')->nullable();
|
||||
$table->string('status', 24)->default('pending');
|
||||
$table->foreignId('reviewed_by_user_id')
|
||||
->nullable()
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
$table->text('review_notes')->nullable();
|
||||
$table->timestamp('reviewed_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'status', 'created_at'], 'group_join_requests_group_status_idx');
|
||||
$table->index(['group_id', 'user_id', 'status'], 'group_join_requests_group_user_status_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_posts', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')
|
||||
->constrained('groups')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('author_user_id')
|
||||
->constrained('users')
|
||||
->cascadeOnDelete();
|
||||
$table->string('type', 32)->default('announcement');
|
||||
$table->string('title', 180);
|
||||
$table->string('slug', 190)->unique();
|
||||
$table->string('excerpt', 320)->nullable();
|
||||
$table->longText('content')->nullable();
|
||||
$table->string('cover_path', 2048)->nullable();
|
||||
$table->string('status', 24)->default('draft');
|
||||
$table->boolean('is_pinned')->default(false);
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'status', 'published_at'], 'group_posts_group_status_published_idx');
|
||||
$table->index(['group_id', 'is_pinned', 'published_at'], 'group_posts_group_pinned_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_recruitment_profiles', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')
|
||||
->constrained('groups')
|
||||
->cascadeOnDelete();
|
||||
$table->boolean('is_recruiting')->default(false);
|
||||
$table->string('headline', 180)->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->json('roles_json')->nullable();
|
||||
$table->json('skills_json')->nullable();
|
||||
$table->string('contact_mode', 32)->nullable();
|
||||
$table->string('visibility', 24)->default('public');
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('group_id', 'group_recruitment_profiles_group_unique');
|
||||
});
|
||||
|
||||
Schema::create('group_histories', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')
|
||||
->constrained('groups')
|
||||
->cascadeOnDelete();
|
||||
$table->foreignId('actor_user_id')
|
||||
->nullable()
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
$table->string('action_type', 64);
|
||||
$table->string('target_type', 64)->nullable();
|
||||
$table->unsignedBigInteger('target_id')->nullable();
|
||||
$table->string('summary', 255)->nullable();
|
||||
$table->json('before_json')->nullable();
|
||||
$table->json('after_json')->nullable();
|
||||
$table->timestamp('created_at')->nullable();
|
||||
|
||||
$table->index(['group_id', 'action_type', 'created_at'], 'group_histories_group_action_idx');
|
||||
$table->index(['group_id', 'target_type', 'target_id'], 'group_histories_target_idx');
|
||||
});
|
||||
|
||||
Schema::table('group_members', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('group_members', 'permission_overrides_json')) {
|
||||
$table->json('permission_overrides_json')->nullable()->after('note');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('artworks', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('artworks', 'group_review_status')) {
|
||||
$table->string('group_review_status', 24)->default('none')->after('artwork_status');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('artworks', 'group_review_submitted_at')) {
|
||||
$table->timestamp('group_review_submitted_at')->nullable()->after('group_review_status');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('artworks', 'group_reviewed_by_user_id')) {
|
||||
$table->foreignId('group_reviewed_by_user_id')
|
||||
->nullable()
|
||||
->after('group_review_submitted_at')
|
||||
->constrained('users')
|
||||
->nullOnDelete();
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('artworks', 'group_reviewed_at')) {
|
||||
$table->timestamp('group_reviewed_at')->nullable()->after('group_reviewed_by_user_id');
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('artworks', 'group_review_notes')) {
|
||||
$table->text('group_review_notes')->nullable()->after('group_reviewed_at');
|
||||
}
|
||||
|
||||
$table->index(['group_id', 'group_review_status', 'group_review_submitted_at'], 'artworks_group_review_queue_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('artworks', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('artworks', 'group_reviewed_by_user_id')) {
|
||||
$table->dropConstrainedForeignId('group_reviewed_by_user_id');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('artworks', 'group_review_status')) {
|
||||
$table->dropIndex('artworks_group_review_queue_idx');
|
||||
$table->dropColumn([
|
||||
'group_review_status',
|
||||
'group_review_submitted_at',
|
||||
'group_reviewed_at',
|
||||
'group_review_notes',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
Schema::table('group_members', function (Blueprint $table): void {
|
||||
if (Schema::hasColumn('group_members', 'permission_overrides_json')) {
|
||||
$table->dropColumn('permission_overrides_json');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::dropIfExists('group_histories');
|
||||
Schema::dropIfExists('group_recruitment_profiles');
|
||||
Schema::dropIfExists('group_posts');
|
||||
Schema::dropIfExists('group_join_requests');
|
||||
}
|
||||
};
|
||||
180
database/migrations/2026_04_04_000008_add_groups_v3_schema.php
Normal file
180
database/migrations/2026_04_04_000008_add_groups_v3_schema.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?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('group_projects', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->string('title', 180);
|
||||
$table->string('slug', 190)->unique();
|
||||
$table->string('summary', 320)->nullable();
|
||||
$table->longText('description')->nullable();
|
||||
$table->string('cover_path', 2048)->nullable();
|
||||
$table->string('status', 24)->default('planned');
|
||||
$table->string('visibility', 24)->default('public');
|
||||
$table->date('start_date')->nullable();
|
||||
$table->date('target_date')->nullable();
|
||||
$table->timestamp('released_at')->nullable();
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('lead_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
|
||||
$table->foreignId('linked_featured_artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
|
||||
$table->foreignId('pinned_post_id')->nullable()->constrained('group_posts')->nullOnDelete();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'status', 'visibility'], 'group_projects_group_status_visibility_idx');
|
||||
$table->index(['group_id', 'released_at'], 'group_projects_group_released_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_project_artworks', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_project_id')->constrained('group_projects')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_project_id', 'artwork_id'], 'group_project_artworks_unique');
|
||||
$table->index(['group_project_id', 'sort_order'], 'group_project_artworks_sort_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_project_members', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_project_id')->constrained('group_projects')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('role_label', 80)->nullable();
|
||||
$table->boolean('is_lead')->default(false);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_project_id', 'user_id'], 'group_project_members_unique');
|
||||
$table->index(['group_project_id', 'is_lead'], 'group_project_members_lead_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_challenges', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->string('title', 180);
|
||||
$table->string('slug', 190)->unique();
|
||||
$table->string('summary', 320)->nullable();
|
||||
$table->longText('description')->nullable();
|
||||
$table->string('cover_path', 2048)->nullable();
|
||||
$table->string('visibility', 24)->default('public');
|
||||
$table->string('participation_scope', 24)->default('group_only');
|
||||
$table->string('status', 24)->default('draft');
|
||||
$table->timestamp('start_at')->nullable();
|
||||
$table->timestamp('end_at')->nullable();
|
||||
$table->text('rules_text')->nullable();
|
||||
$table->text('submission_instructions')->nullable();
|
||||
$table->string('judging_mode', 32)->nullable();
|
||||
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
|
||||
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('featured_artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'status', 'visibility'], 'group_challenges_group_status_visibility_idx');
|
||||
$table->index(['group_id', 'start_at', 'end_at'], 'group_challenges_group_window_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_challenge_artworks', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_challenge_id')->constrained('group_challenges')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->foreignId('submitted_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_challenge_id', 'artwork_id'], 'group_challenge_artworks_unique');
|
||||
$table->index(['group_challenge_id', 'sort_order'], 'group_challenge_artworks_sort_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->string('title', 180);
|
||||
$table->string('slug', 190)->unique();
|
||||
$table->string('summary', 320)->nullable();
|
||||
$table->longText('description')->nullable();
|
||||
$table->string('event_type', 32)->default('launch');
|
||||
$table->string('visibility', 24)->default('public');
|
||||
$table->timestamp('start_at')->nullable();
|
||||
$table->timestamp('end_at')->nullable();
|
||||
$table->string('timezone', 80)->default('UTC');
|
||||
$table->string('cover_path', 2048)->nullable();
|
||||
$table->string('location', 180)->nullable();
|
||||
$table->string('external_url', 2048)->nullable();
|
||||
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
|
||||
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
|
||||
$table->foreignId('linked_challenge_id')->nullable()->constrained('group_challenges')->nullOnDelete();
|
||||
$table->string('status', 24)->default('draft');
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'status', 'visibility'], 'group_events_group_status_visibility_idx');
|
||||
$table->index(['group_id', 'start_at'], 'group_events_group_start_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_assets', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->string('title', 180);
|
||||
$table->text('description')->nullable();
|
||||
$table->string('category', 32)->default('misc');
|
||||
$table->string('file_path', 2048);
|
||||
$table->string('preview_path', 2048)->nullable();
|
||||
$table->string('visibility', 24)->default('members_only');
|
||||
$table->string('status', 24)->default('active');
|
||||
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
|
||||
$table->foreignId('uploaded_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->foreignId('approved_by_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->json('file_meta_json')->nullable();
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'visibility', 'status'], 'group_assets_group_visibility_status_idx');
|
||||
$table->index(['group_id', 'category'], 'group_assets_group_category_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_activity_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->string('type', 48);
|
||||
$table->string('visibility', 24)->default('public');
|
||||
$table->foreignId('actor_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('subject_type', 64);
|
||||
$table->unsignedBigInteger('subject_id')->nullable();
|
||||
$table->string('headline', 255);
|
||||
$table->string('summary', 500)->nullable();
|
||||
$table->boolean('is_pinned')->default(false);
|
||||
$table->timestamp('occurred_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'visibility', 'occurred_at'], 'group_activity_items_group_visibility_occurred_idx');
|
||||
$table->index(['group_id', 'type', 'occurred_at'], 'group_activity_items_group_type_occurred_idx');
|
||||
$table->index(['group_id', 'subject_type', 'subject_id'], 'group_activity_items_subject_idx');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('group_activity_items');
|
||||
Schema::dropIfExists('group_assets');
|
||||
Schema::dropIfExists('group_events');
|
||||
Schema::dropIfExists('group_challenge_artworks');
|
||||
Schema::dropIfExists('group_challenges');
|
||||
Schema::dropIfExists('group_project_members');
|
||||
Schema::dropIfExists('group_project_artworks');
|
||||
Schema::dropIfExists('group_projects');
|
||||
}
|
||||
};
|
||||
164
database/migrations/2026_04_05_000000_add_groups_v4_schema.php
Normal file
164
database/migrations/2026_04_05_000000_add_groups_v4_schema.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?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('group_releases', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->string('title', 180);
|
||||
$table->string('slug', 190)->unique();
|
||||
$table->string('summary', 320)->nullable();
|
||||
$table->longText('description')->nullable();
|
||||
$table->string('cover_path', 2048)->nullable();
|
||||
$table->string('status', 32)->default('planned');
|
||||
$table->string('current_stage', 32)->default('concept');
|
||||
$table->string('visibility', 24)->default('public');
|
||||
$table->timestamp('planned_release_at')->nullable();
|
||||
$table->timestamp('released_at')->nullable();
|
||||
$table->foreignId('lead_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->foreignId('linked_project_id')->nullable()->constrained('group_projects')->nullOnDelete();
|
||||
$table->foreignId('linked_collection_id')->nullable()->constrained('collections')->nullOnDelete();
|
||||
$table->foreignId('featured_artwork_id')->nullable()->constrained('artworks')->nullOnDelete();
|
||||
$table->longText('release_notes')->nullable();
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->boolean('is_featured')->default(false);
|
||||
$table->softDeletes();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_id', 'status', 'visibility'], 'group_releases_group_status_visibility_idx');
|
||||
$table->index(['group_id', 'current_stage'], 'group_releases_group_stage_idx');
|
||||
$table->index(['group_id', 'planned_release_at'], 'group_releases_group_planned_idx');
|
||||
$table->index(['group_id', 'released_at'], 'group_releases_group_released_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_release_artworks', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_release_id')->constrained('group_releases')->cascadeOnDelete();
|
||||
$table->foreignId('artwork_id')->constrained('artworks')->cascadeOnDelete();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_release_id', 'artwork_id'], 'group_release_artworks_unique');
|
||||
$table->index(['group_release_id', 'sort_order'], 'group_release_artworks_sort_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_release_contributors', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_release_id')->constrained('group_releases')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('role_label', 80)->nullable();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_release_id', 'user_id'], 'group_release_contributors_unique');
|
||||
$table->index(['group_release_id', 'sort_order'], 'group_release_contributors_sort_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_project_milestones', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_project_id')->constrained('group_projects')->cascadeOnDelete();
|
||||
$table->string('title', 180);
|
||||
$table->string('summary', 320)->nullable();
|
||||
$table->string('status', 24)->default('pending');
|
||||
$table->date('due_date')->nullable();
|
||||
$table->foreignId('owner_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_project_id', 'status'], 'group_project_milestones_project_status_idx');
|
||||
$table->index(['group_project_id', 'due_date'], 'group_project_milestones_project_due_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_release_milestones', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_release_id')->constrained('group_releases')->cascadeOnDelete();
|
||||
$table->string('title', 180);
|
||||
$table->string('summary', 320)->nullable();
|
||||
$table->string('status', 24)->default('pending');
|
||||
$table->date('due_date')->nullable();
|
||||
$table->foreignId('owner_user_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['group_release_id', 'status'], 'group_release_milestones_release_status_idx');
|
||||
$table->index(['group_release_id', 'due_date'], 'group_release_milestones_release_due_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_contributor_stats', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->unsignedInteger('credited_artworks_count')->default(0);
|
||||
$table->unsignedInteger('release_count')->default(0);
|
||||
$table->unsignedInteger('project_count')->default(0);
|
||||
$table->unsignedInteger('review_actions_count')->default(0);
|
||||
$table->unsignedInteger('approved_submissions_count')->default(0);
|
||||
$table->json('reputation_meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_id', 'user_id'], 'group_contributor_stats_group_user_unique');
|
||||
$table->index(['group_id', 'release_count'], 'group_contributor_stats_group_release_idx');
|
||||
$table->index(['group_id', 'credited_artworks_count'], 'group_contributor_stats_group_artworks_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_badges', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->string('badge_key', 80);
|
||||
$table->timestamp('awarded_at');
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_id', 'badge_key'], 'group_badges_group_badge_unique');
|
||||
});
|
||||
|
||||
Schema::create('group_member_badges', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('badge_key', 80);
|
||||
$table->timestamp('awarded_at');
|
||||
$table->json('meta_json')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['group_id', 'user_id', 'badge_key'], 'group_member_badges_group_user_badge_unique');
|
||||
$table->index(['group_id', 'user_id'], 'group_member_badges_group_user_idx');
|
||||
});
|
||||
|
||||
Schema::create('group_discovery_metrics', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('group_id')->constrained('groups')->cascadeOnDelete();
|
||||
$table->decimal('freshness_score', 8, 2)->default(0);
|
||||
$table->decimal('activity_score', 8, 2)->default(0);
|
||||
$table->decimal('release_score', 8, 2)->default(0);
|
||||
$table->decimal('trust_score', 8, 2)->default(0);
|
||||
$table->decimal('collaboration_score', 8, 2)->default(0);
|
||||
$table->timestamp('last_calculated_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('group_id', 'group_discovery_metrics_group_unique');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('group_discovery_metrics');
|
||||
Schema::dropIfExists('group_member_badges');
|
||||
Schema::dropIfExists('group_badges');
|
||||
Schema::dropIfExists('group_contributor_stats');
|
||||
Schema::dropIfExists('group_release_milestones');
|
||||
Schema::dropIfExists('group_project_milestones');
|
||||
Schema::dropIfExists('group_release_contributors');
|
||||
Schema::dropIfExists('group_release_artworks');
|
||||
Schema::dropIfExists('group_releases');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
<?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
|
||||
{
|
||||
if (Schema::hasTable('news_articles')) {
|
||||
$this->addNewsArticleColumns();
|
||||
$this->backfillNewsArticleColumns();
|
||||
}
|
||||
|
||||
if (! Schema::hasTable('news_article_relations')) {
|
||||
Schema::create('news_article_relations', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('article_id')->index();
|
||||
$table->string('entity_type', 40)->index();
|
||||
$table->unsignedBigInteger('entity_id')->index();
|
||||
$table->string('context_label', 120)->nullable();
|
||||
$table->unsignedSmallInteger('sort_order')->default(0);
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('article_id')->references('id')->on('news_articles')->onDelete('cascade');
|
||||
$table->unique(['article_id', 'entity_type', 'entity_id'], 'news_article_relations_unique');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('news_article_relations');
|
||||
|
||||
if (! Schema::hasTable('news_articles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$columns = [
|
||||
'editorial_status',
|
||||
'type',
|
||||
'is_pinned',
|
||||
'canonical_url',
|
||||
];
|
||||
|
||||
$existing = array_values(array_filter($columns, static fn (string $column): bool => Schema::hasColumn('news_articles', $column)));
|
||||
|
||||
if ($existing !== []) {
|
||||
Schema::table('news_articles', function (Blueprint $table) use ($existing): void {
|
||||
$table->dropColumn($existing);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function addNewsArticleColumns(): void
|
||||
{
|
||||
$needsTableChange = false;
|
||||
|
||||
foreach (['editorial_status', 'type', 'is_pinned', 'canonical_url'] as $column) {
|
||||
if (! Schema::hasColumn('news_articles', $column)) {
|
||||
$needsTableChange = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $needsTableChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('news_articles', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('news_articles', 'editorial_status')) {
|
||||
$table->string('editorial_status', 30)->default('draft')->after('status')->index();
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('news_articles', 'type')) {
|
||||
$table->string('type', 40)->default('announcement')->after('cover_image')->index();
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('news_articles', 'is_pinned')) {
|
||||
$table->boolean('is_pinned')->default(false)->after('is_featured')->index();
|
||||
}
|
||||
|
||||
if (! Schema::hasColumn('news_articles', 'canonical_url')) {
|
||||
$table->string('canonical_url')->nullable()->after('meta_description');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function backfillNewsArticleColumns(): void
|
||||
{
|
||||
DB::table('news_articles')
|
||||
->select(['id', 'status', 'editorial_status', 'type'])
|
||||
->orderBy('id')
|
||||
->chunkById(200, function ($rows): void {
|
||||
foreach ($rows as $row) {
|
||||
$payload = [];
|
||||
|
||||
if (property_exists($row, 'editorial_status') && ($row->editorial_status === null || $row->editorial_status === '')) {
|
||||
$payload['editorial_status'] = match ((string) $row->status) {
|
||||
'published' => 'published',
|
||||
'scheduled' => 'scheduled',
|
||||
default => 'draft',
|
||||
};
|
||||
}
|
||||
|
||||
if (property_exists($row, 'type') && ($row->type === null || $row->type === '')) {
|
||||
$payload['type'] = 'announcement';
|
||||
}
|
||||
|
||||
if ($payload !== []) {
|
||||
DB::table('news_articles')->where('id', $row->id)->update($payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user