Fix project detail 500 error and round deletion data integrity
Build and Push Docker Image / build (push) Has been cancelled
Details
Build and Push Docker Image / build (push) Has been cancelled
Details
- Add missing migration for FileRequirement table and ProjectFile.requirementId
column (existed in Prisma schema but had no migration, causing all queries
with `include: { files: true }` to fail with column not found)
- Make projectTags query resilient with try-catch in project.get
- Reset project status to SUBMITTED when round is deleted (prevents orphaned
ASSIGNED status after ON DELETE SET NULL nullifies roundId)
- Fix round creation/update to allow requiredReviews=0 for filtering rounds
- Parse Zod validation errors in round creation error display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0631dbb64f
commit
7e3d600eed
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "FileRequirement" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"acceptedMimeTypes" TEXT[],
|
||||||
|
"maxSizeMB" INTEGER,
|
||||||
|
"isRequired" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "FileRequirement_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "FileRequirement_roundId_idx" ON "FileRequirement"("roundId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "FileRequirement" ADD CONSTRAINT "FileRequirement_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AlterTable: add requirementId to ProjectFile
|
||||||
|
ALTER TABLE "ProjectFile" ADD COLUMN "requirementId" TEXT;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ProjectFile_requirementId_idx" ON "ProjectFile"("requirementId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_requirementId_fkey" FOREIGN KEY ("requirementId") REFERENCES "FileRequirement"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
@ -417,7 +417,20 @@ function CreateRoundContent() {
|
||||||
<CardContent className="flex items-center gap-2 py-4">
|
<CardContent className="flex items-center gap-2 py-4">
|
||||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||||
<p className="text-sm text-destructive">
|
<p className="text-sm text-destructive">
|
||||||
{createRound.error.message}
|
{(() => {
|
||||||
|
const msg = createRound.error.message
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(msg)
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
return parsed.map((e: { message?: string; path?: string[] }) =>
|
||||||
|
e.message || 'Validation error'
|
||||||
|
).join('. ')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not JSON, use as-is
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
})()}
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -327,17 +327,21 @@ export const projectRouter = router({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
projectTags: {
|
|
||||||
include: {
|
|
||||||
tag: {
|
|
||||||
select: { id: true, name: true, category: true, color: true },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
orderBy: { confidence: 'desc' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Fetch project tags separately (table may not exist if migrations are pending)
|
||||||
|
let projectTags: { id: string; projectId: string; tagId: string; confidence: number; tag: { id: string; name: string; category: string | null; color: string | null } }[] = []
|
||||||
|
try {
|
||||||
|
projectTags = await ctx.prisma.projectTag.findMany({
|
||||||
|
where: { projectId: input.id },
|
||||||
|
include: { tag: { select: { id: true, name: true, category: true, color: true } } },
|
||||||
|
orderBy: { confidence: 'desc' },
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// ProjectTag table may not exist yet
|
||||||
|
}
|
||||||
|
|
||||||
// Check access for jury members
|
// Check access for jury members
|
||||||
if (ctx.user.role === 'JURY_MEMBER') {
|
if (ctx.user.role === 'JURY_MEMBER') {
|
||||||
const assignment = await ctx.prisma.assignment.findFirst({
|
const assignment = await ctx.prisma.assignment.findFirst({
|
||||||
|
|
@ -381,6 +385,7 @@ export const projectRouter = router({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
|
projectTags,
|
||||||
teamMembers: teamMembersWithAvatars,
|
teamMembers: teamMembersWithAvatars,
|
||||||
mentorAssignment: mentorWithAvatar,
|
mentorAssignment: mentorWithAvatar,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export const roundRouter = router({
|
||||||
programId: z.string(),
|
programId: z.string(),
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
roundType: z.enum(['FILTERING', 'EVALUATION', 'LIVE_EVENT']).default('EVALUATION'),
|
roundType: z.enum(['FILTERING', 'EVALUATION', 'LIVE_EVENT']).default('EVALUATION'),
|
||||||
requiredReviews: z.number().int().min(1).max(10).default(3),
|
requiredReviews: z.number().int().min(0).max(10).default(3),
|
||||||
minAssignmentsPerJuror: z.number().int().min(1).max(50).default(5),
|
minAssignmentsPerJuror: z.number().int().min(1).max(50).default(5),
|
||||||
maxAssignmentsPerJuror: z.number().int().min(1).max(100).default(20),
|
maxAssignmentsPerJuror: z.number().int().min(1).max(100).default(20),
|
||||||
sortOrder: z.number().int().optional(),
|
sortOrder: z.number().int().optional(),
|
||||||
|
|
@ -208,7 +208,7 @@ export const roundRouter = router({
|
||||||
name: z.string().min(1).max(255).optional(),
|
name: z.string().min(1).max(255).optional(),
|
||||||
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional().nullable(),
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional().nullable(),
|
||||||
roundType: z.enum(['FILTERING', 'EVALUATION', 'LIVE_EVENT']).optional(),
|
roundType: z.enum(['FILTERING', 'EVALUATION', 'LIVE_EVENT']).optional(),
|
||||||
requiredReviews: z.number().int().min(1).max(10).optional(),
|
requiredReviews: z.number().int().min(0).max(10).optional(),
|
||||||
minAssignmentsPerJuror: z.number().int().min(1).max(50).optional(),
|
minAssignmentsPerJuror: z.number().int().min(1).max(50).optional(),
|
||||||
maxAssignmentsPerJuror: z.number().int().min(1).max(100).optional(),
|
maxAssignmentsPerJuror: z.number().int().min(1).max(100).optional(),
|
||||||
submissionDeadline: z.date().optional().nullable(),
|
submissionDeadline: z.date().optional().nullable(),
|
||||||
|
|
@ -615,6 +615,12 @@ export const roundRouter = router({
|
||||||
userAgent: ctx.userAgent,
|
userAgent: ctx.userAgent,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Reset status for projects that will lose their roundId (ON DELETE SET NULL)
|
||||||
|
await tx.project.updateMany({
|
||||||
|
where: { roundId: input.id },
|
||||||
|
data: { status: 'SUBMITTED' },
|
||||||
|
})
|
||||||
|
|
||||||
// Delete evaluations first to avoid FK constraint on Evaluation.formId
|
// Delete evaluations first to avoid FK constraint on Evaluation.formId
|
||||||
// (formId FK may not have CASCADE in older DB schemas)
|
// (formId FK may not have CASCADE in older DB schemas)
|
||||||
await tx.evaluation.deleteMany({
|
await tx.evaluation.deleteMany({
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue