Initial project
This commit is contained in:
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
52
database/factories/ConversationFactory.php
Normal file
52
database/factories/ConversationFactory.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Conversation>
|
||||
*/
|
||||
class ConversationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'created_by_id' => User::factory(),
|
||||
'type' => Conversation::TypeDirect,
|
||||
'name' => null,
|
||||
'description' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function direct(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'type' => Conversation::TypeDirect,
|
||||
'name' => null,
|
||||
'description' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function group(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'type' => Conversation::TypeGroup,
|
||||
'name' => fake()->randomElement([
|
||||
'Design Partners',
|
||||
'Product Launch',
|
||||
'Customer Success',
|
||||
'Engineering Standup',
|
||||
'Growth Studio',
|
||||
]),
|
||||
'description' => fake()->sentence(8),
|
||||
]);
|
||||
}
|
||||
}
|
||||
38
database/factories/ConversationParticipantFactory.php
Normal file
38
database/factories/ConversationParticipantFactory.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\ConversationParticipant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<ConversationParticipant>
|
||||
*/
|
||||
class ConversationParticipantFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'conversation_id' => Conversation::factory(),
|
||||
'user_id' => User::factory(),
|
||||
'role' => ConversationParticipant::RoleMember,
|
||||
'joined_at' => fake()->dateTimeBetween('-2 months', 'now'),
|
||||
'last_read_at' => fake()->optional(0.85)->dateTimeBetween('-2 weeks', 'now'),
|
||||
'muted_until' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function admin(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'role' => ConversationParticipant::RoleAdmin,
|
||||
]);
|
||||
}
|
||||
}
|
||||
38
database/factories/MessageFactory.php
Normal file
38
database/factories/MessageFactory.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\Message;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<Message>
|
||||
*/
|
||||
class MessageFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'conversation_id' => Conversation::factory(),
|
||||
'user_id' => User::factory(),
|
||||
'type' => Message::TypeText,
|
||||
'body' => fake()->randomElement([
|
||||
fake()->sentence(8),
|
||||
fake()->sentence(12),
|
||||
fake()->paragraph(2),
|
||||
'I pushed a cleaner version. Can you take a look?',
|
||||
'This is ready from my side.',
|
||||
'Let me know what you think about the latest update.',
|
||||
]),
|
||||
'metadata' => null,
|
||||
'edited_at' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
60
database/factories/UserFactory.php
Normal file
60
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends Factory<User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
'two_factor_confirmed_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has two-factor authentication configured.
|
||||
*/
|
||||
public function withTwoFactor(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'two_factor_secret' => encrypt('secret'),
|
||||
'two_factor_recovery_codes' => encrypt(json_encode(['recovery-code-1'])),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->bigInteger('expiration')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedSmallInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->text('two_factor_secret')->after('password')->nullable();
|
||||
$table->text('two_factor_recovery_codes')->after('two_factor_secret')->nullable();
|
||||
$table->timestamp('two_factor_confirmed_at')->after('two_factor_recovery_codes')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'two_factor_confirmed_at',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('conversations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('created_by_id')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('type')->default('direct')->index();
|
||||
$table->string('name')->nullable();
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['type', 'updated_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('conversations');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('conversation_participants', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('role')->default('member');
|
||||
$table->timestamp('joined_at')->nullable();
|
||||
$table->timestamp('last_read_at')->nullable();
|
||||
$table->timestamp('muted_until')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['conversation_id', 'user_id']);
|
||||
$table->index(['user_id', 'updated_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('conversation_participants');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('messages', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('conversation_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->string('type')->default('text')->index();
|
||||
$table->text('body');
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamp('edited_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['conversation_id', 'created_at']);
|
||||
$table->index(['user_id', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('messages');
|
||||
}
|
||||
};
|
||||
117
database/seeders/DatabaseSeeder.php
Normal file
117
database/seeders/DatabaseSeeder.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Conversation;
|
||||
use App\Models\ConversationParticipant;
|
||||
use App\Models\Message;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$user = User::query()->firstOrCreate([
|
||||
'email' => 'test@example.com',
|
||||
], [
|
||||
'name' => 'Test User',
|
||||
'email_verified_at' => now(),
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$teammates = User::factory()
|
||||
->count(10)
|
||||
->create();
|
||||
|
||||
$teammates->take(5)->each(function (User $teammate, int $index) use ($user): void {
|
||||
$conversation = Conversation::factory()
|
||||
->direct()
|
||||
->for($user, 'creator')
|
||||
->create(['updated_at' => now()->subMinutes(12 - $index)]);
|
||||
|
||||
ConversationParticipant::factory()
|
||||
->for($conversation)
|
||||
->for($user)
|
||||
->create([
|
||||
'joined_at' => now()->subWeeks(3),
|
||||
'last_read_at' => $index < 2 ? now()->subMinutes(20) : now(),
|
||||
]);
|
||||
|
||||
ConversationParticipant::factory()
|
||||
->for($conversation)
|
||||
->for($teammate)
|
||||
->create([
|
||||
'joined_at' => now()->subWeeks(3),
|
||||
'last_read_at' => now()->subHour(),
|
||||
]);
|
||||
|
||||
$this->seedMessages($conversation, collect([$user, $teammate])->values()->all(), 8, $index);
|
||||
});
|
||||
|
||||
$groups = [
|
||||
['name' => 'Product Launch', 'description' => 'Messaging for launch readiness, blockers, and decisions.'],
|
||||
['name' => 'Design Partners', 'description' => 'Feedback loops with design, research, and product.'],
|
||||
['name' => 'Customer Success', 'description' => 'Escalations, onboarding notes, and customer wins.'],
|
||||
];
|
||||
|
||||
foreach ($groups as $groupIndex => $group) {
|
||||
$conversation = Conversation::factory()
|
||||
->group()
|
||||
->for($user, 'creator')
|
||||
->create([
|
||||
'name' => $group['name'],
|
||||
'description' => $group['description'],
|
||||
'updated_at' => now()->subMinutes(5 + $groupIndex),
|
||||
]);
|
||||
|
||||
$participants = $teammates
|
||||
->slice($groupIndex * 3, 4)
|
||||
->push($user)
|
||||
->values();
|
||||
|
||||
$participants->each(function (User $participant) use ($conversation, $user): void {
|
||||
ConversationParticipant::factory()
|
||||
->for($conversation)
|
||||
->for($participant)
|
||||
->create([
|
||||
'role' => $participant->is($user) ? ConversationParticipant::RoleAdmin : ConversationParticipant::RoleMember,
|
||||
'joined_at' => now()->subMonth(),
|
||||
'last_read_at' => $participant->is($user) ? now()->subMinutes(30) : now()->subDay(),
|
||||
]);
|
||||
});
|
||||
|
||||
$this->seedMessages($conversation, $participants->all(), 14, $groupIndex + 5);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, User> $participants
|
||||
*/
|
||||
private function seedMessages(Conversation $conversation, array $participants, int $count, int $offset): void
|
||||
{
|
||||
$startedAt = now()
|
||||
->subDays(3)
|
||||
->addHours($offset * 2);
|
||||
|
||||
for ($messageIndex = 0; $messageIndex < $count; $messageIndex++) {
|
||||
$sender = $participants[$messageIndex % count($participants)];
|
||||
$createdAt = $startedAt->copy()->addMinutes($messageIndex * 27);
|
||||
|
||||
Message::factory()
|
||||
->for($conversation)
|
||||
->for($sender, 'sender')
|
||||
->create([
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
]);
|
||||
}
|
||||
|
||||
$conversation->forceFill([
|
||||
'updated_at' => $startedAt->copy()->addMinutes($count * 27),
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user