feat(schema): multi-mentor per team + change-requests + per-assignment email field

- MentorAssignment: drop projectId @unique -> composite (projectId, mentorId)
- MentorAssignment: add notificationSentAt for idempotent per-team email
- MentorFile: add projectId (primary scope); mentorAssignmentId becomes nullable audit FK
- MentorChangeRequest: new model + status enum
- Migration hand-written with IF EXISTS guards (safe for docker-entrypoint retry)
This commit is contained in:
Matt
2026-05-22 15:58:16 +02:00
parent 3bcbf72ad6
commit e89dca24c3
2 changed files with 207 additions and 84 deletions

View File

@@ -0,0 +1,78 @@
-- Hand-written migration for PR8 (multi-mentor per team).
--
-- All DDL guarded with IF EXISTS / IF NOT EXISTS so the docker-entrypoint
-- retry loop is safe to re-run. No regex (the 2026-05-07 prod incident was
-- caused by Prisma 6 generating regex-based DDL that Postgres rejected).
-- No BEGIN/COMMIT blocks — Prisma wraps the migration in a transaction.
-- Phase 1: MentorAssignment — drop unique, add composite, add notification field
ALTER TABLE "MentorAssignment" DROP CONSTRAINT IF EXISTS "MentorAssignment_projectId_key";
DROP INDEX IF EXISTS "MentorAssignment_projectId_key";
CREATE UNIQUE INDEX IF NOT EXISTS "MentorAssignment_projectId_mentorId_key"
ON "MentorAssignment"("projectId", "mentorId");
CREATE INDEX IF NOT EXISTS "MentorAssignment_projectId_idx"
ON "MentorAssignment"("projectId");
ALTER TABLE "MentorAssignment" ADD COLUMN IF NOT EXISTS "notificationSentAt" TIMESTAMP(3);
-- Phase 2: MentorFile — re-scope to project (two-phase backfill)
ALTER TABLE "MentorFile" ADD COLUMN IF NOT EXISTS "projectId" TEXT;
UPDATE "MentorFile" mf
SET "projectId" = ma."projectId"
FROM "MentorAssignment" ma
WHERE mf."mentorAssignmentId" = ma."id"
AND mf."projectId" IS NULL;
ALTER TABLE "MentorFile" ALTER COLUMN "projectId" SET NOT NULL;
ALTER TABLE "MentorFile" DROP CONSTRAINT IF EXISTS "MentorFile_projectId_fkey";
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_projectId_fkey"
FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE INDEX IF NOT EXISTS "MentorFile_projectId_idx" ON "MentorFile"("projectId");
-- Phase 2b: Make MentorFile.mentorAssignmentId nullable + switch its FK to SetNull
ALTER TABLE "MentorFile" ALTER COLUMN "mentorAssignmentId" DROP NOT NULL;
ALTER TABLE "MentorFile" DROP CONSTRAINT IF EXISTS "MentorFile_mentorAssignmentId_fkey";
ALTER TABLE "MentorFile" ADD CONSTRAINT "MentorFile_mentorAssignmentId_fkey"
FOREIGN KEY ("mentorAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Phase 3: MentorChangeRequest table
-- Postgres < 14 doesn't support CREATE TYPE ... IF NOT EXISTS, so wrap in a
-- DO block that swallows duplicate_object errors (idempotent for re-runs).
DO $$ BEGIN
CREATE TYPE "MentorChangeRequestStatus" AS ENUM ('PENDING', 'RESOLVED', 'DISMISSED');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
CREATE TABLE IF NOT EXISTS "MentorChangeRequest" (
"id" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"targetAssignmentId" TEXT,
"requestedByUserId" TEXT,
"reason" TEXT NOT NULL,
"status" "MentorChangeRequestStatus" NOT NULL DEFAULT 'PENDING',
"resolvedByUserId" TEXT,
"resolvedAt" TIMESTAMP(3),
"resolutionNote" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "MentorChangeRequest_pkey" PRIMARY KEY ("id")
);
ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_projectId_fkey";
ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_projectId_fkey"
FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_targetAssignmentId_fkey";
ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_targetAssignmentId_fkey"
FOREIGN KEY ("targetAssignmentId") REFERENCES "MentorAssignment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_requestedByUserId_fkey";
ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_requestedByUserId_fkey"
FOREIGN KEY ("requestedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "MentorChangeRequest" DROP CONSTRAINT IF EXISTS "MentorChangeRequest_resolvedByUserId_fkey";
ALTER TABLE "MentorChangeRequest" ADD CONSTRAINT "MentorChangeRequest_resolvedByUserId_fkey"
FOREIGN KEY ("resolvedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
CREATE INDEX IF NOT EXISTS "MentorChangeRequest_projectId_idx" ON "MentorChangeRequest"("projectId");
CREATE INDEX IF NOT EXISTS "MentorChangeRequest_status_idx" ON "MentorChangeRequest"("status");
CREATE INDEX IF NOT EXISTS "MentorChangeRequest_targetAssignmentId_idx" ON "MentorChangeRequest"("targetAssignmentId");

View File

@@ -118,7 +118,6 @@ enum NotificationChannel {
NONE NONE
} }
enum PartnerVisibility { enum PartnerVisibility {
ADMIN_ONLY ADMIN_ONLY
JURY_VISIBLE JURY_VISIBLE
@@ -133,7 +132,6 @@ enum PartnerType {
OTHER OTHER
} }
// ============================================================================= // =============================================================================
// COMPETITION / ROUND ENGINE ENUMS // COMPETITION / ROUND ENGINE ENUMS
// ============================================================================= // =============================================================================
@@ -171,7 +169,6 @@ enum ProjectRoundStateValue {
WITHDRAWN WITHDRAWN
} }
enum CapMode { enum CapMode {
HARD HARD
SOFT SOFT
@@ -328,8 +325,8 @@ model User {
inviteTokenExpiresAt DateTime? inviteTokenExpiresAt DateTime?
// Password reset token // Password reset token
passwordResetToken String? @unique passwordResetToken String? @unique
passwordResetExpiresAt DateTime? passwordResetExpiresAt DateTime?
// Digest & availability preferences // Digest & availability preferences
digestFrequency String @default("none") // 'none' | 'daily' | 'weekly' digestFrequency String @default("none") // 'none' | 'daily' | 'weekly'
@@ -363,9 +360,9 @@ model User {
filteringOverrides FilteringResult[] @relation("FilteringOverriddenBy") filteringOverrides FilteringResult[] @relation("FilteringOverriddenBy")
// Award overrides // Award overrides
awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy") awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy")
awardEligibilityConfirms AwardEligibility[] @relation("AwardEligibilityConfirmer") awardEligibilityConfirms AwardEligibility[] @relation("AwardEligibilityConfirmer")
awardWinnerOverrides SpecialAward[] @relation("AwardOverriddenBy") awardWinnerOverrides SpecialAward[] @relation("AwardOverriddenBy")
// In-app notifications // In-app notifications
notifications InAppNotification[] @relation("UserNotifications") notifications InAppNotification[] @relation("UserNotifications")
@@ -413,20 +410,24 @@ model User {
sessions Session[] sessions Session[]
// ── Competition/Round architecture relations ── // ── Competition/Round architecture relations ──
juryGroupMemberships JuryGroupMember[] juryGroupMemberships JuryGroupMember[]
mentorFilesUploaded MentorFile[] @relation("MentorFileUploader") mentorFilesUploaded MentorFile[] @relation("MentorFileUploader")
mentorFilesPromoted MentorFile[] @relation("MentorFilePromoter") mentorFilesPromoted MentorFile[] @relation("MentorFilePromoter")
mentorFileComments MentorFileComment[] @relation("MentorFileCommentAuthor") mentorFileComments MentorFileComment[] @relation("MentorFileCommentAuthor")
resultLocksCreated ResultLock[] @relation("ResultLockCreator") resultLocksCreated ResultLock[] @relation("ResultLockCreator")
resultUnlockEvents ResultUnlockEvent[] @relation("ResultUnlocker") resultUnlockEvents ResultUnlockEvent[] @relation("ResultUnlocker")
submissionPromotions SubmissionPromotionEvent[] @relation("SubmissionPromoter") submissionPromotions SubmissionPromotionEvent[] @relation("SubmissionPromoter")
deliberationReplacements DeliberationParticipant[] @relation("DeliberationReplacement") deliberationReplacements DeliberationParticipant[] @relation("DeliberationReplacement")
// AI Ranking // AI Ranking
rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots") rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots")
// Grand-finale logistics // Grand-finale logistics
finalistAttendances AttendingMember[] finalistAttendances AttendingMember[]
// Mentor change requests
mentorChangeRequestsRequested MentorChangeRequest[] @relation("MentorChangeRequester")
mentorChangeRequestsResolved MentorChangeRequest[] @relation("MentorChangeResolver")
@@index([role]) @@index([role])
@@index([status]) @@index([status])
@@ -629,7 +630,9 @@ model Project {
assignments Assignment[] assignments Assignment[]
submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull) submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
teamMembers TeamMember[] teamMembers TeamMember[]
mentorAssignment MentorAssignment? mentorAssignments MentorAssignment[]
mentorFiles MentorFile[]
mentorChangeRequests MentorChangeRequest[]
filteringResults FilteringResult[] filteringResults FilteringResult[]
awardEligibilities AwardEligibility[] awardEligibilities AwardEligibility[]
awardVotes AwardVote[] awardVotes AwardVote[]
@@ -642,12 +645,12 @@ model Project {
cohortProjects CohortProject[] cohortProjects CohortProject[]
// ── Competition/Round architecture relations ── // ── Competition/Round architecture relations ──
projectRoundStates ProjectRoundState[] projectRoundStates ProjectRoundState[]
assignmentIntents AssignmentIntent[] assignmentIntents AssignmentIntent[]
deliberationVotes DeliberationVote[] deliberationVotes DeliberationVote[]
deliberationResults DeliberationResult[] deliberationResults DeliberationResult[]
submissionPromotions SubmissionPromotionEvent[] submissionPromotions SubmissionPromotionEvent[]
notificationLogs NotificationLog[] notificationLogs NotificationLog[]
// Grand-finale logistics // Grand-finale logistics
waitlistEntry WaitlistEntry? waitlistEntry WaitlistEntry?
@@ -699,9 +702,9 @@ model ProjectFile {
// Document analysis (optional, populated by document-analyzer service) // Document analysis (optional, populated by document-analyzer service)
textPreview String? @db.Text // First ~2000 chars of extracted text textPreview String? @db.Text // First ~2000 chars of extracted text
detectedLang String? // ISO 639-3 code (e.g. 'eng', 'fra', 'und') detectedLang String? // ISO 639-3 code (e.g. 'eng', 'fra', 'und')
langConfidence Float? // 0.01.0 confidence langConfidence Float? // 0.01.0 confidence
analyzedAt DateTime? // When analysis last ran analyzedAt DateTime? // When analysis last ran
// MinIO location // MinIO location
bucket String bucket String
@@ -714,7 +717,7 @@ model ProjectFile {
replacedById String? // FK to the newer file that replaced this one replacedById String? // FK to the newer file that replaced this one
// ── Competition/Round architecture fields ── // ── Competition/Round architecture fields ──
submissionWindowId String? // FK to SubmissionWindow submissionWindowId String? // FK to SubmissionWindow
submissionFileRequirementId String? // FK to SubmissionFileRequirement submissionFileRequirementId String? // FK to SubmissionFileRequirement
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -763,10 +766,10 @@ model Assignment {
juryGroupId String? juryGroupId String?
// Relations // Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
evaluation Evaluation? evaluation Evaluation?
conflictOfInterest ConflictOfInterest? conflictOfInterest ConflictOfInterest?
@@ -1026,12 +1029,12 @@ model NotificationEmailSetting {
// ============================================================================= // =============================================================================
model LearningResource { model LearningResource {
id String @id @default(cuid()) id String @id @default(cuid())
programId String? // null = global resource programId String? // null = global resource
title String title String
description String? @db.Text description String? @db.Text
contentJson Json? @db.JsonB // BlockNote document structure contentJson Json? @db.JsonB // BlockNote document structure
accessJson Json? @db.JsonB // Fine-grained access rules accessJson Json? @db.JsonB // Fine-grained access rules
// File storage (for uploaded resources) // File storage (for uploaded resources)
fileName String? fileName String?
@@ -1270,7 +1273,7 @@ model TeamMember {
model MentorAssignment { model MentorAssignment {
id String @id @default(cuid()) id String @id @default(cuid())
projectId String @unique // One mentor per project projectId String // Team can have multiple mentors; uniqueness enforced via composite below
mentorId String // User with MENTOR role or expertise mentorId String // User with MENTOR role or expertise
// Assignment tracking // Assignment tracking
@@ -1278,6 +1281,9 @@ model MentorAssignment {
assignedAt DateTime @default(now()) assignedAt DateTime @default(now())
assignedBy String? // Admin who assigned assignedBy String? // Admin who assigned
// Per-assignment email idempotency: stamped once the assignment notification email is sent.
notificationSentAt DateTime?
// AI assignment metadata // AI assignment metadata
aiConfidenceScore Float? aiConfidenceScore Float?
expertiseMatchScore Float? expertiseMatchScore Float?
@@ -1304,11 +1310,47 @@ model MentorAssignment {
milestoneCompletions MentorMilestoneCompletion[] milestoneCompletions MentorMilestoneCompletion[]
messages MentorMessage[] messages MentorMessage[]
files MentorFile[] files MentorFile[]
changeRequests MentorChangeRequest[] @relation("MentorChangeRequestTarget")
@@unique([projectId, mentorId])
@@index([projectId])
@@index([mentorId]) @@index([mentorId])
@@index([method]) @@index([method])
} }
// =============================================================================
// MENTOR CHANGE REQUESTS
// =============================================================================
enum MentorChangeRequestStatus {
PENDING
RESOLVED
DISMISSED
}
model MentorChangeRequest {
id String @id @default(cuid())
projectId String
targetAssignmentId String? // Optional: a specific co-mentor the request is about
requestedByUserId String?
reason String @db.Text
status MentorChangeRequestStatus @default(PENDING)
resolvedByUserId String?
resolvedAt DateTime?
resolutionNote String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
targetAssignment MentorAssignment? @relation("MentorChangeRequestTarget", fields: [targetAssignmentId], references: [id], onDelete: SetNull)
requestedBy User? @relation("MentorChangeRequester", fields: [requestedByUserId], references: [id], onDelete: SetNull)
resolvedBy User? @relation("MentorChangeResolver", fields: [resolvedByUserId], references: [id])
@@index([projectId])
@@index([status])
@@index([targetAssignmentId])
}
// ============================================================================= // =============================================================================
// FILTERING ROUND SYSTEM // FILTERING ROUND SYSTEM
// ============================================================================= // =============================================================================
@@ -1443,17 +1485,17 @@ enum AssignmentJobStatus {
// ============================================================================= // =============================================================================
enum RankingTriggerType { enum RankingTriggerType {
MANUAL // Admin clicked "Run ranking" MANUAL // Admin clicked "Run ranking"
AUTO // Auto-triggered by assignment completion AUTO // Auto-triggered by assignment completion
RETROACTIVE // Retroactive scan on deployment RETROACTIVE // Retroactive scan on deployment
QUICK // Quick-rank mode (no preview) QUICK // Quick-rank mode (no preview)
} }
enum RankingMode { enum RankingMode {
PREVIEW // Parsed rules shown to admin (not yet applied) PREVIEW // Parsed rules shown to admin (not yet applied)
CONFIRMED // Admin confirmed rules, ranking applied CONFIRMED // Admin confirmed rules, ranking applied
QUICK // Quick-rank: parse + apply without preview QUICK // Quick-rank: parse + apply without preview
FORMULA // Formula-only: no LLM, pure math ranking FORMULA // Formula-only: no LLM, pure math ranking
} }
enum RankingSnapshotStatus { enum RankingSnapshotStatus {
@@ -1470,7 +1512,7 @@ model RankingSnapshot {
roundId String roundId String
// Trigger metadata // Trigger metadata
triggeredById String? // null = auto-triggered triggeredById String? // null = auto-triggered
triggerType RankingTriggerType @default(MANUAL) triggerType RankingTriggerType @default(MANUAL)
// Criteria used // Criteria used
@@ -1599,7 +1641,7 @@ model SpecialAward {
evaluationRoundId String? evaluationRoundId String?
juryGroupId String? juryGroupId String?
eligibilityMode AwardEligibilityMode @default(STAY_IN_MAIN) eligibilityMode AwardEligibilityMode @default(STAY_IN_MAIN)
decisionMode String? // "JURY_VOTE" | "ADMIN_DECISION" decisionMode String? // "JURY_VOTE" | "ADMIN_DECISION"
shortlistSize Int @default(10) shortlistSize Int @default(10)
// Eligibility job tracking // Eligibility job tracking
@@ -1621,10 +1663,10 @@ model SpecialAward {
votes AwardVote[] votes AwardVote[]
// Competition/Round architecture relations // Competition/Round architecture relations
competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull) competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull)
evaluationRound Round? @relation(fields: [evaluationRoundId], references: [id], onDelete: SetNull) evaluationRound Round? @relation(fields: [evaluationRoundId], references: [id], onDelete: SetNull)
awardJuryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) awardJuryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
rounds Round[] @relation("AwardRounds") rounds Round[] @relation("AwardRounds")
@@index([programId]) @@index([programId])
@@index([status]) @@index([status])
@@ -1688,12 +1730,12 @@ model AwardJuror {
} }
model AwardVote { model AwardVote {
id String @id @default(cuid()) id String @id @default(cuid())
awardId String awardId String
userId String userId String
projectId String projectId String
rank Int? // For RANKED mode rank Int? // For RANKED mode
justification String? @db.Text justification String? @db.Text
votedAt DateTime @default(now()) votedAt DateTime @default(now())
// Relations // Relations
@@ -1810,7 +1852,7 @@ model MentorMessage {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
// ── Competition/Round architecture fields ── // ── Competition/Round architecture fields ──
workspaceId String? // FK to MentorAssignment (used as workspace) workspaceId String? // FK to MentorAssignment (used as workspace)
senderRole MentorMessageRole? senderRole MentorMessageRole?
// Relations // Relations
@@ -2146,9 +2188,9 @@ model Competition {
status CompetitionStatus @default(DRAFT) status CompetitionStatus @default(DRAFT)
// Competition-wide settings // Competition-wide settings
categoryMode String @default("SHARED") categoryMode String @default("SHARED")
startupFinalistCount Int @default(3) startupFinalistCount Int @default(3)
conceptFinalistCount Int @default(3) conceptFinalistCount Int @default(3)
// Notification preferences // Notification preferences
notifyOnRoundAdvance Boolean @default(true) notifyOnRoundAdvance Boolean @default(true)
@@ -2159,7 +2201,7 @@ model Competition {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// Relations // Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade) program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
rounds Round[] rounds Round[]
juryGroups JuryGroup[] juryGroups JuryGroup[]
submissionWindows SubmissionWindow[] submissionWindows SubmissionWindow[]
@@ -2204,10 +2246,10 @@ model Round {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// Relations // Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade) competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
specialAward SpecialAward? @relation("AwardRounds", fields: [specialAwardId], references: [id], onDelete: SetNull) specialAward SpecialAward? @relation("AwardRounds", fields: [specialAwardId], references: [id], onDelete: SetNull)
juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull) juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull) submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull)
projectRoundStates ProjectRoundState[] projectRoundStates ProjectRoundState[]
visibleSubmissionWindows RoundSubmissionVisibility[] visibleSubmissionWindows RoundSubmissionVisibility[]
assignmentIntents AssignmentIntent[] assignmentIntents AssignmentIntent[]
@@ -2226,7 +2268,7 @@ model Round {
filteringResults FilteringResult[] filteringResults FilteringResult[]
filteringJobs FilteringJob[] filteringJobs FilteringJob[]
assignmentJobs AssignmentJob[] assignmentJobs AssignmentJob[]
rankingSnapshots RankingSnapshot[] @relation("RoundRankingSnapshots") rankingSnapshots RankingSnapshot[] @relation("RoundRankingSnapshots")
reminderLogs ReminderLog[] reminderLogs ReminderLog[]
evaluationSummaries EvaluationSummary[] evaluationSummaries EvaluationSummary[]
evaluationDiscussions EvaluationDiscussion[] evaluationDiscussions EvaluationDiscussion[]
@@ -2272,7 +2314,7 @@ model ProjectRoundState {
// ============================================================================= // =============================================================================
model JuryGroup { model JuryGroup {
id String @id @default(cuid()) id String @id @default(cuid())
competitionId String competitionId String
name String name String
slug String slug String
@@ -2330,8 +2372,8 @@ model JuryGroupMember {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// Relations // Relations
juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id], onDelete: Cascade) juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade) user User @relation(fields: [userId], references: [id], onDelete: Cascade)
assignmentIntents AssignmentIntent[] assignmentIntents AssignmentIntent[]
deliberationVotes DeliberationVote[] deliberationVotes DeliberationVote[]
deliberationParticipations DeliberationParticipant[] deliberationParticipations DeliberationParticipant[]
@@ -2369,7 +2411,7 @@ model SubmissionWindow {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// Relations // Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade) competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
fileRequirements SubmissionFileRequirement[] fileRequirements SubmissionFileRequirement[]
projectFiles ProjectFile[] projectFiles ProjectFile[]
rounds Round[] rounds Round[]
@@ -2403,7 +2445,7 @@ model SubmissionFileRequirement {
} }
model RoundSubmissionVisibility { model RoundSubmissionVisibility {
id String @id @default(cuid()) id String @id @default(cuid())
roundId String roundId String
submissionWindowId String submissionWindowId String
canView Boolean @default(true) canView Boolean @default(true)
@@ -2448,8 +2490,9 @@ model AssignmentIntent {
// ============================================================================= // =============================================================================
model MentorFile { model MentorFile {
id String @id @default(cuid()) id String @id @default(cuid())
mentorAssignmentId String projectId String // Primary access scope: files belong to the team
mentorAssignmentId String? // Nullable audit FK: which assignment uploaded; survives mentor drop
uploadedByUserId String uploadedByUserId String
fileName String fileName String
@@ -2468,13 +2511,15 @@ model MentorFile {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
// Relations // Relations
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
uploadedBy User @relation("MentorFileUploader", fields: [uploadedByUserId], references: [id]) mentorAssignment MentorAssignment? @relation(fields: [mentorAssignmentId], references: [id], onDelete: SetNull)
promotedBy User? @relation("MentorFilePromoter", fields: [promotedByUserId], references: [id]) uploadedBy User @relation("MentorFileUploader", fields: [uploadedByUserId], references: [id])
promotedFile ProjectFile? @relation("PromotedFromMentorFile", fields: [promotedToFileId], references: [id], onDelete: SetNull) promotedBy User? @relation("MentorFilePromoter", fields: [promotedByUserId], references: [id])
promotedFile ProjectFile? @relation("PromotedFromMentorFile", fields: [promotedToFileId], references: [id], onDelete: SetNull)
comments MentorFileComment[] comments MentorFileComment[]
promotionEvents SubmissionPromotionEvent[] promotionEvents SubmissionPromotionEvent[]
@@index([projectId])
@@index([mentorAssignmentId]) @@index([mentorAssignmentId])
@@index([uploadedByUserId]) @@index([uploadedByUserId])
} }
@@ -2492,9 +2537,9 @@ model MentorFileComment {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
// Relations // Relations
mentorFile MentorFile @relation(fields: [mentorFileId], references: [id], onDelete: Cascade) mentorFile MentorFile @relation(fields: [mentorFileId], references: [id], onDelete: Cascade)
author User @relation("MentorFileCommentAuthor", fields: [authorId], references: [id]) author User @relation("MentorFileCommentAuthor", fields: [authorId], references: [id])
parentComment MentorFileComment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: Cascade) parentComment MentorFileComment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: Cascade)
replies MentorFileComment[] @relation("CommentThread") replies MentorFileComment[] @relation("CommentThread")
@@index([mentorFileId]) @@index([mentorFileId])
@@ -2503,14 +2548,14 @@ model MentorFileComment {
} }
model SubmissionPromotionEvent { model SubmissionPromotionEvent {
id String @id @default(cuid()) id String @id @default(cuid())
projectId String projectId String
roundId String roundId String
slotKey String slotKey String
sourceType SubmissionPromotionSource sourceType SubmissionPromotionSource
sourceFileId String? sourceFileId String?
promotedById String promotedById String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
// Relations // Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)