fix(admin): index redirect uses auth cookies and Spatie roles
- Gate redirect on userData + accessToken; map org roles to events route - Keep legacy admin/client role redirects for compatibility - Rename organizer app HTML title to Event Crew - App - Add Cursor database rules (ULID, JSON, indexes, soft deletes) Made-with: Cursor
This commit is contained in:
283
.cursor/rules/103_database.mdc
Normal file
283
.cursor/rules/103_database.mdc
Normal file
@@ -0,0 +1,283 @@
|
||||
---
|
||||
description: Database schema conventions, ULID primary keys, JSON column rules, soft delete strategy, and index requirements for EventCrew
|
||||
globs: ["api/database/**/*.php", "api/app/Models/**/*.php"]
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Database & Schema Rules
|
||||
|
||||
## Primary Keys: ULID (NOT UUID v4)
|
||||
|
||||
All business tables use ULID (Universally Unique Lexicographically Sortable Identifier). UUID v4 is forbidden because it is random, causing B-tree index fragmentation in InnoDB. ULID is monotonically increasing (time-ordered).
|
||||
|
||||
### Migration Pattern
|
||||
```php
|
||||
$table->ulid('id')->primary();
|
||||
```
|
||||
|
||||
### Model Pattern
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUlids;
|
||||
|
||||
class Event extends Model
|
||||
{
|
||||
use HasUlids;
|
||||
}
|
||||
```
|
||||
|
||||
### Foreign Keys
|
||||
```php
|
||||
$table->foreignUlid('organisation_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUlid('event_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUlid('location_id')->nullable()->constrained()->nullOnDelete();
|
||||
```
|
||||
|
||||
### Exception: Pure Pivot Tables
|
||||
Pure pivot tables (no business logic, only joins) use auto-increment integer PK for join performance:
|
||||
```php
|
||||
// organisation_user, event_user_roles, crowd_list_persons, access_zone_days, etc.
|
||||
$table->id(); // auto-increment integer
|
||||
```
|
||||
|
||||
## JSON Column Rules
|
||||
|
||||
### ALLOWED for JSON
|
||||
- Opaque configuration: `settings`, `blocks`, `fields`
|
||||
- Toggle-sets and item lists: `items`, `milestone_flags`
|
||||
- Free-text arrays: `top_feedback`
|
||||
- Conditional logic definitions: `conditional_logic`
|
||||
|
||||
### NEVER use JSON for
|
||||
- Dates or periods (use separate columns or pivot tables)
|
||||
- Status values (use enum columns)
|
||||
- Foreign keys or references (use FK columns)
|
||||
- Boolean flags (use bool columns)
|
||||
- Anything that needs to be filtered, sorted, or aggregated in queries
|
||||
|
||||
### Example: Access Zone Days
|
||||
```php
|
||||
// ❌ WRONG: JSON array of dates
|
||||
$table->json('active_days'); // Can't index or query efficiently
|
||||
|
||||
// ✅ CORRECT: Separate pivot table
|
||||
Schema::create('access_zone_days', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignUlid('access_zone_id')->constrained()->cascadeOnDelete();
|
||||
$table->date('day_date');
|
||||
$table->unique(['access_zone_id', 'day_date']);
|
||||
$table->index('day_date');
|
||||
});
|
||||
```
|
||||
|
||||
## Soft Delete Strategy
|
||||
|
||||
### Soft Delete ON (recoverable business records)
|
||||
- `organisations`
|
||||
- `events`
|
||||
- `festival_sections`
|
||||
- `shifts`
|
||||
- `shift_assignments`
|
||||
- `persons`
|
||||
- `artists`
|
||||
- `companies`
|
||||
- `production_requests`
|
||||
|
||||
```php
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Event extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
}
|
||||
|
||||
// Migration:
|
||||
$table->softDeletes();
|
||||
```
|
||||
|
||||
### Soft Delete OFF (immutable audit records)
|
||||
These are historical records that must never be modified or hidden:
|
||||
- `check_ins` (who checked in, when, by whom)
|
||||
- `briefing_sends` (delivery tracking)
|
||||
- `message_replies` (communication audit)
|
||||
- `shift_waitlist` (position tracking)
|
||||
- `volunteer_festival_history` (coordinator assessments)
|
||||
- `show_day_absence_alerts` (operational audit)
|
||||
|
||||
## Required Composite Indexes
|
||||
|
||||
All list queries must target < 50ms. These composite indexes are mandatory:
|
||||
|
||||
### persons
|
||||
```php
|
||||
$table->index(['event_id', 'crowd_type_id', 'status']);
|
||||
$table->index(['email', 'event_id']);
|
||||
$table->index(['user_id', 'event_id']);
|
||||
```
|
||||
|
||||
### shift_assignments
|
||||
```php
|
||||
$table->index(['shift_id', 'status']);
|
||||
$table->index(['person_id', 'status']);
|
||||
$table->unique(['person_id', 'time_slot_id']); // DB-enforced conflict detection
|
||||
```
|
||||
|
||||
### check_ins
|
||||
```php
|
||||
$table->index(['event_id', 'person_id', 'scanned_at']);
|
||||
$table->index(['event_id', 'scanned_at']);
|
||||
```
|
||||
|
||||
### briefing_sends
|
||||
```php
|
||||
$table->index(['status', 'briefing_id']);
|
||||
$table->index(['person_id']);
|
||||
```
|
||||
|
||||
### shift_waitlist
|
||||
```php
|
||||
$table->unique(['shift_id', 'person_id']);
|
||||
$table->index(['shift_id', 'position']);
|
||||
```
|
||||
|
||||
### performances (B2B detection via overlap query)
|
||||
```php
|
||||
$table->index(['stage_id', 'date', 'start_time', 'end_time']);
|
||||
```
|
||||
|
||||
### events
|
||||
```php
|
||||
$table->index(['organisation_id', 'status']);
|
||||
```
|
||||
|
||||
### festival_sections
|
||||
```php
|
||||
$table->index(['event_id', 'sort_order']);
|
||||
```
|
||||
|
||||
### time_slots
|
||||
```php
|
||||
$table->index(['event_id', 'person_type', 'date']);
|
||||
```
|
||||
|
||||
### shifts
|
||||
```php
|
||||
$table->index(['festival_section_id', 'time_slot_id']);
|
||||
$table->index(['time_slot_id', 'status']);
|
||||
```
|
||||
|
||||
## Migration Ordering
|
||||
|
||||
Migrations must be created in dependency order. Foundation tables first, then dependent tables:
|
||||
|
||||
1. `users` (update: add timezone, locale, deleted_at)
|
||||
2. `organisations`
|
||||
3. `organisation_user` (pivot)
|
||||
4. `user_invitations`
|
||||
5. `events`
|
||||
6. `event_user_roles` (pivot)
|
||||
7. `locations`
|
||||
8. `crowd_types`
|
||||
9. `companies`
|
||||
10. `festival_sections`
|
||||
11. `time_slots`
|
||||
12. `persons`
|
||||
13. `shifts`
|
||||
14. `shift_assignments`
|
||||
15. `volunteer_availabilities`
|
||||
16. `shift_waitlist`
|
||||
17. `accreditation_categories`
|
||||
18. `accreditation_items`
|
||||
19. `event_accreditation_items`
|
||||
20. `accreditation_assignments`
|
||||
21. `access_zones` + `access_zone_days` + `person_access_zones`
|
||||
22. `stages` + `stage_days`
|
||||
23. `artists`
|
||||
24. `performances`
|
||||
25. `advance_sections` + `advance_submissions`
|
||||
26. `briefing_templates` + `briefings` + `briefing_sends`
|
||||
27. `public_forms` + `form_submissions`
|
||||
28. `check_ins`
|
||||
29. `scanners`
|
||||
|
||||
## Migration Template
|
||||
|
||||
```php
|
||||
<?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('shifts', function (Blueprint $table) {
|
||||
// ULID primary key
|
||||
$table->ulid('id')->primary();
|
||||
|
||||
// Foreign keys with constraints
|
||||
$table->foreignUlid('festival_section_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUlid('time_slot_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignUlid('location_id')->nullable()->constrained()->nullOnDelete();
|
||||
|
||||
// Business columns
|
||||
$table->unsignedSmallInteger('slots_total');
|
||||
$table->unsignedSmallInteger('slots_open_for_claiming')->default(0);
|
||||
$table->string('status')->default('open');
|
||||
$table->json('events_during_shift')->nullable(); // opaque config = OK for JSON
|
||||
|
||||
// Timestamps + soft delete
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// Composite indexes (from design document section 3.5)
|
||||
$table->index(['festival_section_id', 'time_slot_id']);
|
||||
$table->index(['time_slot_id', 'status']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('shifts');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Audit Logging
|
||||
|
||||
Use Spatie laravel-activitylog on these models:
|
||||
- `persons`
|
||||
- `accreditation_assignments`
|
||||
- `shift_assignments`
|
||||
- `check_ins`
|
||||
- `production_requests`
|
||||
|
||||
```php
|
||||
use Spatie\Activitylog\Traits\LogsActivity;
|
||||
use Spatie\Activitylog\LogOptions;
|
||||
|
||||
class Person extends Model
|
||||
{
|
||||
use LogsActivity;
|
||||
|
||||
public function getActivitylogOptions(): LogOptions
|
||||
{
|
||||
return LogOptions::defaults()
|
||||
->logOnly(['status', 'is_blacklisted', 'crowd_type_id'])
|
||||
->logOnlyDirty();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Out of Scope (Do NOT create tables for)
|
||||
|
||||
These features are explicitly excluded from v1.x. No placeholders, no empty JSON fields, no migrations:
|
||||
- Travel management (flights, buses)
|
||||
- Accommodation management (hotels)
|
||||
- Travel Party management
|
||||
- CO2/sustainability reporting
|
||||
- Native mobile apps
|
||||
- Ticket sales (own ticketing system)
|
||||
Reference in New Issue
Block a user