Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -1,246 +1,246 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
interface CheckResult {
name: string
passed: boolean
details: string
}
async function runChecks(): Promise<CheckResult[]> {
const results: CheckResult[] = []
// 1. No orphan ProjectStageState (every PSS references valid project, track, stage)
const orphanStates = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "ProjectStageState" pss
WHERE NOT EXISTS (SELECT 1 FROM "Project" p WHERE p.id = pss."projectId")
OR NOT EXISTS (SELECT 1 FROM "Track" t WHERE t.id = pss."trackId")
OR NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = pss."stageId")
`
const orphanCount = Number(orphanStates[0]?.count ?? 0)
results.push({
name: 'No orphan ProjectStageState',
passed: orphanCount === 0,
details: orphanCount === 0 ? 'All PSS records reference valid entities' : `Found ${orphanCount} orphan records`,
})
// 2. Every project has at least one stage state
const projectsWithoutState = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Project" p
WHERE NOT EXISTS (SELECT 1 FROM "ProjectStageState" pss WHERE pss."projectId" = p.id)
`
const noStateCount = Number(projectsWithoutState[0]?.count ?? 0)
const totalProjects = await prisma.project.count()
results.push({
name: 'Every project has at least one stage state',
passed: noStateCount === 0,
details: noStateCount === 0
? `All ${totalProjects} projects have stage states`
: `${noStateCount} projects missing stage states`,
})
// 3. No duplicate active states per (project, track, stage)
const duplicateStates = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM (
SELECT "projectId", "trackId", "stageId", COUNT(*) as cnt
FROM "ProjectStageState"
WHERE "exitedAt" IS NULL
GROUP BY "projectId", "trackId", "stageId"
HAVING COUNT(*) > 1
) dupes
`
const dupeCount = Number(duplicateStates[0]?.count ?? 0)
results.push({
name: 'No duplicate active states per (project, track, stage)',
passed: dupeCount === 0,
details: dupeCount === 0 ? 'No duplicates found' : `Found ${dupeCount} duplicate active states`,
})
// 4. All transitions stay within same pipeline
const crossPipelineTransitions = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "StageTransition" st
JOIN "Stage" sf ON sf.id = st."fromStageId"
JOIN "Track" tf ON tf.id = sf."trackId"
JOIN "Stage" sto ON sto.id = st."toStageId"
JOIN "Track" tt ON tt.id = sto."trackId"
WHERE tf."pipelineId" != tt."pipelineId"
`
const crossCount = Number(crossPipelineTransitions[0]?.count ?? 0)
results.push({
name: 'All transitions stay within same pipeline',
passed: crossCount === 0,
details: crossCount === 0 ? 'All transitions are within pipeline' : `Found ${crossCount} cross-pipeline transitions`,
})
// 5. Stage sortOrder unique per track
const duplicateSortOrders = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM (
SELECT "trackId", "sortOrder", COUNT(*) as cnt
FROM "Stage"
GROUP BY "trackId", "sortOrder"
HAVING COUNT(*) > 1
) dupes
`
const dupeSortCount = Number(duplicateSortOrders[0]?.count ?? 0)
results.push({
name: 'Stage sortOrder unique per track',
passed: dupeSortCount === 0,
details: dupeSortCount === 0 ? 'All sort orders unique' : `Found ${dupeSortCount} duplicate sort orders`,
})
// 6. Track sortOrder unique per pipeline
const duplicateTrackOrders = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM (
SELECT "pipelineId", "sortOrder", COUNT(*) as cnt
FROM "Track"
GROUP BY "pipelineId", "sortOrder"
HAVING COUNT(*) > 1
) dupes
`
const dupeTrackCount = Number(duplicateTrackOrders[0]?.count ?? 0)
results.push({
name: 'Track sortOrder unique per pipeline',
passed: dupeTrackCount === 0,
details: dupeTrackCount === 0 ? 'All track orders unique' : `Found ${dupeTrackCount} duplicate track orders`,
})
// 7. Every Pipeline has at least one Track; every Track has at least one Stage
const emptyPipelines = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Pipeline" p
WHERE NOT EXISTS (SELECT 1 FROM "Track" t WHERE t."pipelineId" = p.id)
`
const emptyPipelineCount = Number(emptyPipelines[0]?.count ?? 0)
const emptyTracks = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Track" t
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s."trackId" = t.id)
`
const emptyTrackCount = Number(emptyTracks[0]?.count ?? 0)
results.push({
name: 'Every Pipeline has Tracks; every Track has Stages',
passed: emptyPipelineCount === 0 && emptyTrackCount === 0,
details: emptyPipelineCount === 0 && emptyTrackCount === 0
? 'All pipelines have tracks and all tracks have stages'
: `${emptyPipelineCount} empty pipelines, ${emptyTrackCount} empty tracks`,
})
// 8. RoutingRule destinations reference valid tracks in same pipeline
const badRoutingRules = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "RoutingRule" rr
WHERE rr."destinationTrackId" IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM "Track" t
WHERE t.id = rr."destinationTrackId"
AND t."pipelineId" = rr."pipelineId"
)
`
const badRouteCount = Number(badRoutingRules[0]?.count ?? 0)
results.push({
name: 'RoutingRule destinations reference valid tracks in same pipeline',
passed: badRouteCount === 0,
details: badRouteCount === 0
? 'All routing rules reference valid destination tracks'
: `Found ${badRouteCount} routing rules with invalid destinations`,
})
// 9. LiveProgressCursor references valid stage
const badCursors = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "LiveProgressCursor" lpc
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = lpc."stageId")
`
const badCursorCount = Number(badCursors[0]?.count ?? 0)
results.push({
name: 'LiveProgressCursor references valid stage',
passed: badCursorCount === 0,
details: badCursorCount === 0
? 'All cursors reference valid stages'
: `Found ${badCursorCount} cursors with invalid stage references`,
})
// 10. Cohort references valid stage
const badCohorts = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Cohort" c
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = c."stageId")
`
const badCohortCount = Number(badCohorts[0]?.count ?? 0)
results.push({
name: 'Cohort references valid stage',
passed: badCohortCount === 0,
details: badCohortCount === 0
? 'All cohorts reference valid stages'
: `Found ${badCohortCount} cohorts with invalid stage references`,
})
// 11. Every EvaluationForm has a valid stageId
const badEvalForms = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "EvaluationForm" ef
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = ef."stageId")
`
const badFormCount = Number(badEvalForms[0]?.count ?? 0)
results.push({
name: 'Every EvaluationForm references valid stage',
passed: badFormCount === 0,
details: badFormCount === 0
? 'All evaluation forms reference valid stages'
: `Found ${badFormCount} forms with invalid stage references`,
})
// 12. Every FileRequirement has a valid stageId
const badFileReqs = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "FileRequirement" fr
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = fr."stageId")
`
const badFileReqCount = Number(badFileReqs[0]?.count ?? 0)
results.push({
name: 'Every FileRequirement references valid stage',
passed: badFileReqCount === 0,
details: badFileReqCount === 0
? 'All file requirements reference valid stages'
: `Found ${badFileReqCount} file requirements with invalid stage references`,
})
// 13. Count validation
const projectCountResult = await prisma.project.count()
const stageCount = await prisma.stage.count()
const trackCount = await prisma.track.count()
const pipelineCount = await prisma.pipeline.count()
const pssCount = await prisma.projectStageState.count()
results.push({
name: 'Count validation',
passed: projectCountResult > 0 && stageCount > 0 && trackCount > 0,
details: `Pipelines: ${pipelineCount}, Tracks: ${trackCount}, Stages: ${stageCount}, Projects: ${projectCountResult}, StageStates: ${pssCount}`,
})
return results
}
async function main() {
console.log('🔍 Running integrity checks...\n')
const results = await runChecks()
let allPassed = true
for (const result of results) {
const icon = result.passed ? '✅' : '❌'
console.log(`${icon} ${result.name}`)
console.log(` ${result.details}\n`)
if (!result.passed) allPassed = false
}
console.log('='.repeat(50))
if (allPassed) {
console.log('✅ All integrity checks passed!')
} else {
console.log('❌ Some integrity checks failed!')
process.exit(1)
}
}
main()
.catch((e) => {
console.error('❌ Integrity check failed:', e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
interface CheckResult {
name: string
passed: boolean
details: string
}
async function runChecks(): Promise<CheckResult[]> {
const results: CheckResult[] = []
// 1. No orphan ProjectStageState (every PSS references valid project, track, stage)
const orphanStates = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "ProjectStageState" pss
WHERE NOT EXISTS (SELECT 1 FROM "Project" p WHERE p.id = pss."projectId")
OR NOT EXISTS (SELECT 1 FROM "Track" t WHERE t.id = pss."trackId")
OR NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = pss."stageId")
`
const orphanCount = Number(orphanStates[0]?.count ?? 0)
results.push({
name: 'No orphan ProjectStageState',
passed: orphanCount === 0,
details: orphanCount === 0 ? 'All PSS records reference valid entities' : `Found ${orphanCount} orphan records`,
})
// 2. Every project has at least one stage state
const projectsWithoutState = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Project" p
WHERE NOT EXISTS (SELECT 1 FROM "ProjectStageState" pss WHERE pss."projectId" = p.id)
`
const noStateCount = Number(projectsWithoutState[0]?.count ?? 0)
const totalProjects = await prisma.project.count()
results.push({
name: 'Every project has at least one stage state',
passed: noStateCount === 0,
details: noStateCount === 0
? `All ${totalProjects} projects have stage states`
: `${noStateCount} projects missing stage states`,
})
// 3. No duplicate active states per (project, track, stage)
const duplicateStates = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM (
SELECT "projectId", "trackId", "stageId", COUNT(*) as cnt
FROM "ProjectStageState"
WHERE "exitedAt" IS NULL
GROUP BY "projectId", "trackId", "stageId"
HAVING COUNT(*) > 1
) dupes
`
const dupeCount = Number(duplicateStates[0]?.count ?? 0)
results.push({
name: 'No duplicate active states per (project, track, stage)',
passed: dupeCount === 0,
details: dupeCount === 0 ? 'No duplicates found' : `Found ${dupeCount} duplicate active states`,
})
// 4. All transitions stay within same pipeline
const crossPipelineTransitions = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "StageTransition" st
JOIN "Stage" sf ON sf.id = st."fromStageId"
JOIN "Track" tf ON tf.id = sf."trackId"
JOIN "Stage" sto ON sto.id = st."toStageId"
JOIN "Track" tt ON tt.id = sto."trackId"
WHERE tf."pipelineId" != tt."pipelineId"
`
const crossCount = Number(crossPipelineTransitions[0]?.count ?? 0)
results.push({
name: 'All transitions stay within same pipeline',
passed: crossCount === 0,
details: crossCount === 0 ? 'All transitions are within pipeline' : `Found ${crossCount} cross-pipeline transitions`,
})
// 5. Stage sortOrder unique per track
const duplicateSortOrders = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM (
SELECT "trackId", "sortOrder", COUNT(*) as cnt
FROM "Stage"
GROUP BY "trackId", "sortOrder"
HAVING COUNT(*) > 1
) dupes
`
const dupeSortCount = Number(duplicateSortOrders[0]?.count ?? 0)
results.push({
name: 'Stage sortOrder unique per track',
passed: dupeSortCount === 0,
details: dupeSortCount === 0 ? 'All sort orders unique' : `Found ${dupeSortCount} duplicate sort orders`,
})
// 6. Track sortOrder unique per pipeline
const duplicateTrackOrders = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM (
SELECT "pipelineId", "sortOrder", COUNT(*) as cnt
FROM "Track"
GROUP BY "pipelineId", "sortOrder"
HAVING COUNT(*) > 1
) dupes
`
const dupeTrackCount = Number(duplicateTrackOrders[0]?.count ?? 0)
results.push({
name: 'Track sortOrder unique per pipeline',
passed: dupeTrackCount === 0,
details: dupeTrackCount === 0 ? 'All track orders unique' : `Found ${dupeTrackCount} duplicate track orders`,
})
// 7. Every Pipeline has at least one Track; every Track has at least one Stage
const emptyPipelines = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Pipeline" p
WHERE NOT EXISTS (SELECT 1 FROM "Track" t WHERE t."pipelineId" = p.id)
`
const emptyPipelineCount = Number(emptyPipelines[0]?.count ?? 0)
const emptyTracks = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Track" t
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s."trackId" = t.id)
`
const emptyTrackCount = Number(emptyTracks[0]?.count ?? 0)
results.push({
name: 'Every Pipeline has Tracks; every Track has Stages',
passed: emptyPipelineCount === 0 && emptyTrackCount === 0,
details: emptyPipelineCount === 0 && emptyTrackCount === 0
? 'All pipelines have tracks and all tracks have stages'
: `${emptyPipelineCount} empty pipelines, ${emptyTrackCount} empty tracks`,
})
// 8. RoutingRule destinations reference valid tracks in same pipeline
const badRoutingRules = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "RoutingRule" rr
WHERE rr."destinationTrackId" IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM "Track" t
WHERE t.id = rr."destinationTrackId"
AND t."pipelineId" = rr."pipelineId"
)
`
const badRouteCount = Number(badRoutingRules[0]?.count ?? 0)
results.push({
name: 'RoutingRule destinations reference valid tracks in same pipeline',
passed: badRouteCount === 0,
details: badRouteCount === 0
? 'All routing rules reference valid destination tracks'
: `Found ${badRouteCount} routing rules with invalid destinations`,
})
// 9. LiveProgressCursor references valid stage
const badCursors = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "LiveProgressCursor" lpc
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = lpc."stageId")
`
const badCursorCount = Number(badCursors[0]?.count ?? 0)
results.push({
name: 'LiveProgressCursor references valid stage',
passed: badCursorCount === 0,
details: badCursorCount === 0
? 'All cursors reference valid stages'
: `Found ${badCursorCount} cursors with invalid stage references`,
})
// 10. Cohort references valid stage
const badCohorts = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "Cohort" c
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = c."stageId")
`
const badCohortCount = Number(badCohorts[0]?.count ?? 0)
results.push({
name: 'Cohort references valid stage',
passed: badCohortCount === 0,
details: badCohortCount === 0
? 'All cohorts reference valid stages'
: `Found ${badCohortCount} cohorts with invalid stage references`,
})
// 11. Every EvaluationForm has a valid stageId
const badEvalForms = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "EvaluationForm" ef
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = ef."stageId")
`
const badFormCount = Number(badEvalForms[0]?.count ?? 0)
results.push({
name: 'Every EvaluationForm references valid stage',
passed: badFormCount === 0,
details: badFormCount === 0
? 'All evaluation forms reference valid stages'
: `Found ${badFormCount} forms with invalid stage references`,
})
// 12. Every FileRequirement has a valid stageId
const badFileReqs = await prisma.$queryRaw<{ count: bigint }[]>`
SELECT COUNT(*) as count FROM "FileRequirement" fr
WHERE NOT EXISTS (SELECT 1 FROM "Stage" s WHERE s.id = fr."stageId")
`
const badFileReqCount = Number(badFileReqs[0]?.count ?? 0)
results.push({
name: 'Every FileRequirement references valid stage',
passed: badFileReqCount === 0,
details: badFileReqCount === 0
? 'All file requirements reference valid stages'
: `Found ${badFileReqCount} file requirements with invalid stage references`,
})
// 13. Count validation
const projectCountResult = await prisma.project.count()
const stageCount = await prisma.stage.count()
const trackCount = await prisma.track.count()
const pipelineCount = await prisma.pipeline.count()
const pssCount = await prisma.projectStageState.count()
results.push({
name: 'Count validation',
passed: projectCountResult > 0 && stageCount > 0 && trackCount > 0,
details: `Pipelines: ${pipelineCount}, Tracks: ${trackCount}, Stages: ${stageCount}, Projects: ${projectCountResult}, StageStates: ${pssCount}`,
})
return results
}
async function main() {
console.log('🔍 Running integrity checks...\n')
const results = await runChecks()
let allPassed = true
for (const result of results) {
const icon = result.passed ? '✅' : '❌'
console.log(`${icon} ${result.name}`)
console.log(` ${result.details}\n`)
if (!result.passed) allPassed = false
}
console.log('='.repeat(50))
if (allPassed) {
console.log('✅ All integrity checks passed!')
} else {
console.log('❌ Some integrity checks failed!')
process.exit(1)
}
}
main()
.catch((e) => {
console.error('❌ Integrity check failed:', e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,91 +1,91 @@
-- Universal Apply Page: Make Project.roundId nullable and add programId FK
-- This migration enables projects to be submitted to a program/edition without being assigned to a specific round
-- NOTE: Written to be idempotent (safe to re-run if partially applied)
-- Step 1: Add Program.slug for edition-wide apply URLs (nullable for existing programs)
ALTER TABLE "Program" ADD COLUMN IF NOT EXISTS "slug" TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS "Program_slug_key" ON "Program"("slug");
-- Step 2: Add programId column (nullable initially to handle existing data)
ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "programId" TEXT;
-- Step 3: Backfill programId from existing round relationships
-- Only update rows where programId is still NULL (idempotent)
UPDATE "Project" p
SET "programId" = r."programId"
FROM "Round" r
WHERE p."roundId" = r.id
AND p."programId" IS NULL;
-- Step 4: Handle orphaned projects (no roundId = no way to derive programId)
-- Assign them to the first available program, or delete them if no program exists
DO $$
DECLARE
null_count INTEGER;
fallback_program_id TEXT;
BEGIN
SELECT COUNT(*) INTO null_count FROM "Project" WHERE "programId" IS NULL;
IF null_count > 0 THEN
SELECT id INTO fallback_program_id FROM "Program" ORDER BY "createdAt" ASC LIMIT 1;
IF fallback_program_id IS NOT NULL THEN
UPDATE "Project" SET "programId" = fallback_program_id WHERE "programId" IS NULL;
RAISE NOTICE 'Assigned % orphaned projects to fallback program %', null_count, fallback_program_id;
ELSE
DELETE FROM "Project" WHERE "programId" IS NULL;
RAISE NOTICE 'Deleted % orphaned projects (no program exists to assign them to)', null_count;
END IF;
END IF;
END $$;
-- Step 5: Make programId required (NOT NULL constraint) - safe if already NOT NULL
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'programId' AND is_nullable = 'YES'
) THEN
ALTER TABLE "Project" ALTER COLUMN "programId" SET NOT NULL;
END IF;
END $$;
-- Step 6: Add foreign key constraint for programId (skip if already exists)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_programId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" ADD CONSTRAINT "Project_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE;
END IF;
END $$;
-- Step 7: Make roundId nullable (allow projects without round assignment)
-- Safe: DROP NOT NULL is idempotent if already nullable
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'roundId' AND is_nullable = 'NO'
) THEN
ALTER TABLE "Project" ALTER COLUMN "roundId" DROP NOT NULL;
END IF;
END $$;
-- Step 8: Update round FK to SET NULL on delete (instead of CASCADE)
-- Projects should remain in the database if their round is deleted
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_roundId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" DROP CONSTRAINT "Project_roundId_fkey";
END IF;
ALTER TABLE "Project" ADD CONSTRAINT "Project_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL;
END $$;
-- Step 9: Add performance indexes
CREATE INDEX IF NOT EXISTS "Project_programId_idx" ON "Project"("programId");
CREATE INDEX IF NOT EXISTS "Project_programId_roundId_idx" ON "Project"("programId", "roundId");
-- Universal Apply Page: Make Project.roundId nullable and add programId FK
-- This migration enables projects to be submitted to a program/edition without being assigned to a specific round
-- NOTE: Written to be idempotent (safe to re-run if partially applied)
-- Step 1: Add Program.slug for edition-wide apply URLs (nullable for existing programs)
ALTER TABLE "Program" ADD COLUMN IF NOT EXISTS "slug" TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS "Program_slug_key" ON "Program"("slug");
-- Step 2: Add programId column (nullable initially to handle existing data)
ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "programId" TEXT;
-- Step 3: Backfill programId from existing round relationships
-- Only update rows where programId is still NULL (idempotent)
UPDATE "Project" p
SET "programId" = r."programId"
FROM "Round" r
WHERE p."roundId" = r.id
AND p."programId" IS NULL;
-- Step 4: Handle orphaned projects (no roundId = no way to derive programId)
-- Assign them to the first available program, or delete them if no program exists
DO $$
DECLARE
null_count INTEGER;
fallback_program_id TEXT;
BEGIN
SELECT COUNT(*) INTO null_count FROM "Project" WHERE "programId" IS NULL;
IF null_count > 0 THEN
SELECT id INTO fallback_program_id FROM "Program" ORDER BY "createdAt" ASC LIMIT 1;
IF fallback_program_id IS NOT NULL THEN
UPDATE "Project" SET "programId" = fallback_program_id WHERE "programId" IS NULL;
RAISE NOTICE 'Assigned % orphaned projects to fallback program %', null_count, fallback_program_id;
ELSE
DELETE FROM "Project" WHERE "programId" IS NULL;
RAISE NOTICE 'Deleted % orphaned projects (no program exists to assign them to)', null_count;
END IF;
END IF;
END $$;
-- Step 5: Make programId required (NOT NULL constraint) - safe if already NOT NULL
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'programId' AND is_nullable = 'YES'
) THEN
ALTER TABLE "Project" ALTER COLUMN "programId" SET NOT NULL;
END IF;
END $$;
-- Step 6: Add foreign key constraint for programId (skip if already exists)
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_programId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" ADD CONSTRAINT "Project_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE;
END IF;
END $$;
-- Step 7: Make roundId nullable (allow projects without round assignment)
-- Safe: DROP NOT NULL is idempotent if already nullable
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'Project' AND column_name = 'roundId' AND is_nullable = 'NO'
) THEN
ALTER TABLE "Project" ALTER COLUMN "roundId" DROP NOT NULL;
END IF;
END $$;
-- Step 8: Update round FK to SET NULL on delete (instead of CASCADE)
-- Projects should remain in the database if their round is deleted
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'Project_roundId_fkey' AND table_name = 'Project'
) THEN
ALTER TABLE "Project" DROP CONSTRAINT "Project_roundId_fkey";
END IF;
ALTER TABLE "Project" ADD CONSTRAINT "Project_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL;
END $$;
-- Step 9: Add performance indexes
CREATE INDEX IF NOT EXISTS "Project_programId_idx" ON "Project"("programId");
CREATE INDEX IF NOT EXISTS "Project_programId_roundId_idx" ON "Project"("programId", "roundId");

View File

@@ -1,41 +1,41 @@
-- Reconciliation migration: Add missing foreign keys and indexes
-- The add_15_features migration omitted some FKs and indexes that the schema expects
-- This migration brings the database in line with the Prisma schema
-- =====================================================
-- Missing Foreign Keys
-- =====================================================
-- RoundTemplate → Program
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RoundTemplate → User (creator)
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Message → Round
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- EvaluationDiscussion → Round
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ProjectFile → ProjectFile (self-relation for file versioning)
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_replacedById_fkey"
FOREIGN KEY ("replacedById") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- =====================================================
-- Missing Indexes
-- =====================================================
CREATE INDEX "RoundTemplate_roundType_idx" ON "RoundTemplate"("roundType");
CREATE INDEX "MentorNote_authorId_idx" ON "MentorNote"("authorId");
CREATE INDEX "MentorMilestoneCompletion_completedById_idx" ON "MentorMilestoneCompletion"("completedById");
CREATE INDEX "Webhook_createdById_idx" ON "Webhook"("createdById");
CREATE INDEX "WebhookDelivery_event_idx" ON "WebhookDelivery"("event");
CREATE INDEX "Message_roundId_idx" ON "Message"("roundId");
CREATE INDEX "EvaluationDiscussion_closedById_idx" ON "EvaluationDiscussion"("closedById");
CREATE INDEX "DiscussionComment_discussionId_idx" ON "DiscussionComment"("discussionId");
CREATE INDEX "DiscussionComment_userId_idx" ON "DiscussionComment"("userId");
-- Reconciliation migration: Add missing foreign keys and indexes
-- The add_15_features migration omitted some FKs and indexes that the schema expects
-- This migration brings the database in line with the Prisma schema
-- =====================================================
-- Missing Foreign Keys
-- =====================================================
-- RoundTemplate → Program
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- RoundTemplate → User (creator)
ALTER TABLE "RoundTemplate" ADD CONSTRAINT "RoundTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- Message → Round
ALTER TABLE "Message" ADD CONSTRAINT "Message_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- EvaluationDiscussion → Round
ALTER TABLE "EvaluationDiscussion" ADD CONSTRAINT "EvaluationDiscussion_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- ProjectFile → ProjectFile (self-relation for file versioning)
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_replacedById_fkey"
FOREIGN KEY ("replacedById") REFERENCES "ProjectFile"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- =====================================================
-- Missing Indexes
-- =====================================================
CREATE INDEX "RoundTemplate_roundType_idx" ON "RoundTemplate"("roundType");
CREATE INDEX "MentorNote_authorId_idx" ON "MentorNote"("authorId");
CREATE INDEX "MentorMilestoneCompletion_completedById_idx" ON "MentorMilestoneCompletion"("completedById");
CREATE INDEX "Webhook_createdById_idx" ON "Webhook"("createdById");
CREATE INDEX "WebhookDelivery_event_idx" ON "WebhookDelivery"("event");
CREATE INDEX "Message_roundId_idx" ON "Message"("roundId");
CREATE INDEX "EvaluationDiscussion_closedById_idx" ON "EvaluationDiscussion"("closedById");
CREATE INDEX "DiscussionComment_discussionId_idx" ON "DiscussionComment"("discussionId");
CREATE INDEX "DiscussionComment_userId_idx" ON "DiscussionComment"("userId");

View File

@@ -1,13 +1,13 @@
-- Fix round deletion FK constraint errors
-- Add CASCADE on Evaluation.formId so deleting EvaluationForm cascades to Evaluations
-- Add SET NULL on ProjectFile.roundId so deleting Round nullifies the reference
-- AlterTable: Evaluation.formId -> onDelete CASCADE
ALTER TABLE "Evaluation" DROP CONSTRAINT "Evaluation_formId_fkey";
ALTER TABLE "Evaluation" ADD CONSTRAINT "Evaluation_formId_fkey"
FOREIGN KEY ("formId") REFERENCES "EvaluationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable: ProjectFile.roundId -> onDelete SET NULL
ALTER TABLE "ProjectFile" DROP CONSTRAINT "ProjectFile_roundId_fkey";
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Fix round deletion FK constraint errors
-- Add CASCADE on Evaluation.formId so deleting EvaluationForm cascades to Evaluations
-- Add SET NULL on ProjectFile.roundId so deleting Round nullifies the reference
-- AlterTable: Evaluation.formId -> onDelete CASCADE
ALTER TABLE "Evaluation" DROP CONSTRAINT "Evaluation_formId_fkey";
ALTER TABLE "Evaluation" ADD CONSTRAINT "Evaluation_formId_fkey"
FOREIGN KEY ("formId") REFERENCES "EvaluationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable: ProjectFile.roundId -> onDelete SET NULL
ALTER TABLE "ProjectFile" DROP CONSTRAINT "ProjectFile_roundId_fkey";
ALTER TABLE "ProjectFile" ADD CONSTRAINT "ProjectFile_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,30 +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;
-- 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;

View File

@@ -1,129 +1,129 @@
-- Migration: Add all missing schema elements not covered by previous migrations
-- This brings the database fully in line with prisma/schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. MISSING TABLE: WizardTemplate
-- =============================================================================
CREATE TABLE IF NOT EXISTS "WizardTemplate" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"config" JSONB NOT NULL,
"isGlobal" BOOLEAN NOT NULL DEFAULT false,
"programId" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WizardTemplate_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "WizardTemplate_programId_idx" ON "WizardTemplate"("programId");
CREATE INDEX IF NOT EXISTS "WizardTemplate_isGlobal_idx" ON "WizardTemplate"("isGlobal");
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 2. MISSING COLUMNS ON SpecialAward: eligibility job tracking fields
-- =============================================================================
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStatus" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobTotal" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobDone" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobError" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStarted" TIMESTAMP(3);
-- =============================================================================
-- 3. Project.referralSource: Already in init migration. No action needed.
-- Round.slug: Already in init migration. No action needed.
-- =============================================================================
-- =============================================================================
-- 5. MISSING INDEXES
-- =============================================================================
-- 5a. Assignment: @@index([projectId, userId])
CREATE INDEX IF NOT EXISTS "Assignment_projectId_userId_idx" ON "Assignment"("projectId", "userId");
-- 5b. AuditLog: @@index([sessionId])
CREATE INDEX IF NOT EXISTS "AuditLog_sessionId_idx" ON "AuditLog"("sessionId");
-- 5c. ProjectFile: @@index([projectId, roundId])
CREATE INDEX IF NOT EXISTS "ProjectFile_projectId_roundId_idx" ON "ProjectFile"("projectId", "roundId");
-- 5d. MessageRecipient: @@index([userId])
CREATE INDEX IF NOT EXISTS "MessageRecipient_userId_idx" ON "MessageRecipient"("userId");
-- 5e. MessageRecipient: @@unique([messageId, userId, channel])
CREATE UNIQUE INDEX IF NOT EXISTS "MessageRecipient_messageId_userId_channel_key" ON "MessageRecipient"("messageId", "userId", "channel");
-- 5f. AwardEligibility: @@index([awardId, eligible]) - composite index
CREATE INDEX IF NOT EXISTS "AwardEligibility_awardId_eligible_idx" ON "AwardEligibility"("awardId", "eligible");
-- =============================================================================
-- 6. REMOVE STALE INDEX: Message_scheduledAt_idx
-- The schema does NOT have @@index([scheduledAt]) on Message.
-- The add_15_features migration created it, but the schema doesn't list it.
-- Leaving it as-is since it's harmless and could be useful.
-- =============================================================================
-- =============================================================================
-- 7. VERIFY: All models from add_15_features are present
-- DigestLog, RoundTemplate, MentorNote, MentorMilestone,
-- MentorMilestoneCompletion, Message, MessageTemplate, MessageRecipient,
-- Webhook, WebhookDelivery, EvaluationDiscussion, DiscussionComment
-- -> All confirmed created in 20260205223133_add_15_features migration.
-- -> All FKs confirmed in add_15_features + 20260208000000_add_missing_fks_indexes.
-- =============================================================================
-- =============================================================================
-- 8. VERIFY: Existing tables from init and subsequent migrations
-- All core tables (User, Account, Session, VerificationToken, Program, Round,
-- EvaluationForm, Project, ProjectFile, Assignment, Evaluation, GracePeriod,
-- SystemSettings, AuditLog, AIUsageLog, NotificationLog, InAppNotification,
-- NotificationEmailSetting, LearningResource, ResourceAccess, Partner,
-- ExpertiseTag, ProjectTag, LiveVotingSession, LiveVote, TeamMember,
-- MentorAssignment, FilteringRule, FilteringResult, FilteringJob,
-- AssignmentJob, TaggingJob, SpecialAward, AwardEligibility, AwardJuror,
-- AwardVote, ReminderLog, ConflictOfInterest, EvaluationSummary,
-- ProjectStatusHistory, MentorMessage, FileRequirement)
-- -> All confirmed present in migrations.
-- =============================================================================
-- =============================================================================
-- SUMMARY OF CHANGES IN THIS MIGRATION:
--
-- NEW TABLE:
-- - WizardTemplate (with programId FK, createdBy FK, indexes)
--
-- NEW COLUMNS:
-- - SpecialAward.eligibilityJobStatus (TEXT, nullable)
-- - SpecialAward.eligibilityJobTotal (INTEGER, nullable)
-- - SpecialAward.eligibilityJobDone (INTEGER, nullable)
-- - SpecialAward.eligibilityJobError (TEXT, nullable)
-- - SpecialAward.eligibilityJobStarted (TIMESTAMP, nullable)
--
-- NEW INDEXES:
-- - Assignment_projectId_userId_idx
-- - AuditLog_sessionId_idx
-- - ProjectFile_projectId_roundId_idx
-- - MessageRecipient_userId_idx
-- - MessageRecipient_messageId_userId_channel_key (UNIQUE)
-- - AwardEligibility_awardId_eligible_idx
-- - WizardTemplate_programId_idx
-- - WizardTemplate_isGlobal_idx
--
-- NEW FOREIGN KEYS:
-- - WizardTemplate_programId_fkey -> Program(id) ON DELETE CASCADE
-- - WizardTemplate_createdBy_fkey -> User(id) ON DELETE RESTRICT
-- =============================================================================
-- Migration: Add all missing schema elements not covered by previous migrations
-- This brings the database fully in line with prisma/schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. MISSING TABLE: WizardTemplate
-- =============================================================================
CREATE TABLE IF NOT EXISTS "WizardTemplate" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"config" JSONB NOT NULL,
"isGlobal" BOOLEAN NOT NULL DEFAULT false,
"programId" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "WizardTemplate_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "WizardTemplate_programId_idx" ON "WizardTemplate"("programId");
CREATE INDEX IF NOT EXISTS "WizardTemplate_isGlobal_idx" ON "WizardTemplate"("isGlobal");
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_programId_fkey"
FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
ALTER TABLE "WizardTemplate" ADD CONSTRAINT "WizardTemplate_createdBy_fkey"
FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 2. MISSING COLUMNS ON SpecialAward: eligibility job tracking fields
-- =============================================================================
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStatus" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobTotal" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobDone" INTEGER;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobError" TEXT;
ALTER TABLE "SpecialAward" ADD COLUMN IF NOT EXISTS "eligibilityJobStarted" TIMESTAMP(3);
-- =============================================================================
-- 3. Project.referralSource: Already in init migration. No action needed.
-- Round.slug: Already in init migration. No action needed.
-- =============================================================================
-- =============================================================================
-- 5. MISSING INDEXES
-- =============================================================================
-- 5a. Assignment: @@index([projectId, userId])
CREATE INDEX IF NOT EXISTS "Assignment_projectId_userId_idx" ON "Assignment"("projectId", "userId");
-- 5b. AuditLog: @@index([sessionId])
CREATE INDEX IF NOT EXISTS "AuditLog_sessionId_idx" ON "AuditLog"("sessionId");
-- 5c. ProjectFile: @@index([projectId, roundId])
CREATE INDEX IF NOT EXISTS "ProjectFile_projectId_roundId_idx" ON "ProjectFile"("projectId", "roundId");
-- 5d. MessageRecipient: @@index([userId])
CREATE INDEX IF NOT EXISTS "MessageRecipient_userId_idx" ON "MessageRecipient"("userId");
-- 5e. MessageRecipient: @@unique([messageId, userId, channel])
CREATE UNIQUE INDEX IF NOT EXISTS "MessageRecipient_messageId_userId_channel_key" ON "MessageRecipient"("messageId", "userId", "channel");
-- 5f. AwardEligibility: @@index([awardId, eligible]) - composite index
CREATE INDEX IF NOT EXISTS "AwardEligibility_awardId_eligible_idx" ON "AwardEligibility"("awardId", "eligible");
-- =============================================================================
-- 6. REMOVE STALE INDEX: Message_scheduledAt_idx
-- The schema does NOT have @@index([scheduledAt]) on Message.
-- The add_15_features migration created it, but the schema doesn't list it.
-- Leaving it as-is since it's harmless and could be useful.
-- =============================================================================
-- =============================================================================
-- 7. VERIFY: All models from add_15_features are present
-- DigestLog, RoundTemplate, MentorNote, MentorMilestone,
-- MentorMilestoneCompletion, Message, MessageTemplate, MessageRecipient,
-- Webhook, WebhookDelivery, EvaluationDiscussion, DiscussionComment
-- -> All confirmed created in 20260205223133_add_15_features migration.
-- -> All FKs confirmed in add_15_features + 20260208000000_add_missing_fks_indexes.
-- =============================================================================
-- =============================================================================
-- 8. VERIFY: Existing tables from init and subsequent migrations
-- All core tables (User, Account, Session, VerificationToken, Program, Round,
-- EvaluationForm, Project, ProjectFile, Assignment, Evaluation, GracePeriod,
-- SystemSettings, AuditLog, AIUsageLog, NotificationLog, InAppNotification,
-- NotificationEmailSetting, LearningResource, ResourceAccess, Partner,
-- ExpertiseTag, ProjectTag, LiveVotingSession, LiveVote, TeamMember,
-- MentorAssignment, FilteringRule, FilteringResult, FilteringJob,
-- AssignmentJob, TaggingJob, SpecialAward, AwardEligibility, AwardJuror,
-- AwardVote, ReminderLog, ConflictOfInterest, EvaluationSummary,
-- ProjectStatusHistory, MentorMessage, FileRequirement)
-- -> All confirmed present in migrations.
-- =============================================================================
-- =============================================================================
-- SUMMARY OF CHANGES IN THIS MIGRATION:
--
-- NEW TABLE:
-- - WizardTemplate (with programId FK, createdBy FK, indexes)
--
-- NEW COLUMNS:
-- - SpecialAward.eligibilityJobStatus (TEXT, nullable)
-- - SpecialAward.eligibilityJobTotal (INTEGER, nullable)
-- - SpecialAward.eligibilityJobDone (INTEGER, nullable)
-- - SpecialAward.eligibilityJobError (TEXT, nullable)
-- - SpecialAward.eligibilityJobStarted (TIMESTAMP, nullable)
--
-- NEW INDEXES:
-- - Assignment_projectId_userId_idx
-- - AuditLog_sessionId_idx
-- - ProjectFile_projectId_roundId_idx
-- - MessageRecipient_userId_idx
-- - MessageRecipient_messageId_userId_channel_key (UNIQUE)
-- - AwardEligibility_awardId_eligible_idx
-- - WizardTemplate_programId_idx
-- - WizardTemplate_isGlobal_idx
--
-- NEW FOREIGN KEYS:
-- - WizardTemplate_programId_fkey -> Program(id) ON DELETE CASCADE
-- - WizardTemplate_createdBy_fkey -> User(id) ON DELETE RESTRICT
-- =============================================================================

View File

@@ -1,2 +1,2 @@
-- CreateIndex
CREATE INDEX "AwardVote_awardId_userId_idx" ON "AwardVote"("awardId", "userId");
-- CreateIndex
CREATE INDEX "AwardVote_awardId_userId_idx" ON "AwardVote"("awardId", "userId");

View File

@@ -1,99 +1,99 @@
-- Migration: Add live voting enhancements (criteria voting, audience voting, AudienceVoter)
-- Brings LiveVotingSession, LiveVote, and new AudienceVoter model in sync with schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. LiveVotingSession: Add criteria-based & audience voting columns
-- =============================================================================
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "votingMode" TEXT NOT NULL DEFAULT 'simple';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "criteriaJson" JSONB;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingMode" TEXT NOT NULL DEFAULT 'disabled';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceMaxFavorites" INTEGER NOT NULL DEFAULT 3;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceRequireId" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingDuration" INTEGER;
-- =============================================================================
-- 2. LiveVote: Add criteria scores, audience voter link, make userId nullable
-- =============================================================================
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "criterionScoresJson" JSONB;
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "audienceVoterId" TEXT;
-- Make userId nullable (was NOT NULL in init migration)
ALTER TABLE "LiveVote" ALTER COLUMN "userId" DROP NOT NULL;
-- =============================================================================
-- 3. AudienceVoter: New table for audience participation
-- =============================================================================
CREATE TABLE IF NOT EXISTS "AudienceVoter" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"identifier" TEXT,
"identifierType" TEXT,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AudienceVoter_pkey" PRIMARY KEY ("id")
);
-- Unique constraint on token
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_token_key" UNIQUE ("token");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Indexes
CREATE INDEX IF NOT EXISTS "AudienceVoter_sessionId_idx" ON "AudienceVoter"("sessionId");
CREATE INDEX IF NOT EXISTS "AudienceVoter_token_idx" ON "AudienceVoter"("token");
-- Foreign key: AudienceVoter.sessionId -> LiveVotingSession.id
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_sessionId_fkey"
FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 4. LiveVote: Foreign key and indexes for audienceVoterId
-- =============================================================================
CREATE INDEX IF NOT EXISTS "LiveVote_audienceVoterId_idx" ON "LiveVote"("audienceVoterId");
-- Foreign key: LiveVote.audienceVoterId -> AudienceVoter.id
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_audienceVoterId_fkey"
FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Unique constraint: sessionId + projectId + audienceVoterId
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_sessionId_projectId_audienceVoterId_key"
UNIQUE ("sessionId", "projectId", "audienceVoterId");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- SUMMARY:
--
-- LiveVotingSession new columns:
-- - votingMode (TEXT, default 'simple')
-- - criteriaJson (JSONB, nullable)
-- - audienceVotingMode (TEXT, default 'disabled')
-- - audienceMaxFavorites (INTEGER, default 3)
-- - audienceRequireId (BOOLEAN, default false)
-- - audienceVotingDuration (INTEGER, nullable)
--
-- LiveVote changes:
-- - criterionScoresJson (JSONB, nullable) - new column
-- - audienceVoterId (TEXT, nullable) - new column
-- - userId changed from NOT NULL to nullable
-- - New unique: (sessionId, projectId, audienceVoterId)
-- - New index: audienceVoterId
-- - New FK: audienceVoterId -> AudienceVoter(id)
--
-- New table: AudienceVoter
-- - id, sessionId, token (unique), identifier, identifierType,
-- ipAddress, userAgent, createdAt
-- - FK: sessionId -> LiveVotingSession(id) CASCADE
-- =============================================================================
-- Migration: Add live voting enhancements (criteria voting, audience voting, AudienceVoter)
-- Brings LiveVotingSession, LiveVote, and new AudienceVoter model in sync with schema.prisma
-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution
-- =============================================================================
-- 1. LiveVotingSession: Add criteria-based & audience voting columns
-- =============================================================================
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "votingMode" TEXT NOT NULL DEFAULT 'simple';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "criteriaJson" JSONB;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingMode" TEXT NOT NULL DEFAULT 'disabled';
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceMaxFavorites" INTEGER NOT NULL DEFAULT 3;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceRequireId" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingDuration" INTEGER;
-- =============================================================================
-- 2. LiveVote: Add criteria scores, audience voter link, make userId nullable
-- =============================================================================
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "criterionScoresJson" JSONB;
ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "audienceVoterId" TEXT;
-- Make userId nullable (was NOT NULL in init migration)
ALTER TABLE "LiveVote" ALTER COLUMN "userId" DROP NOT NULL;
-- =============================================================================
-- 3. AudienceVoter: New table for audience participation
-- =============================================================================
CREATE TABLE IF NOT EXISTS "AudienceVoter" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"token" TEXT NOT NULL,
"identifier" TEXT,
"identifierType" TEXT,
"ipAddress" TEXT,
"userAgent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AudienceVoter_pkey" PRIMARY KEY ("id")
);
-- Unique constraint on token
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_token_key" UNIQUE ("token");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Indexes
CREATE INDEX IF NOT EXISTS "AudienceVoter_sessionId_idx" ON "AudienceVoter"("sessionId");
CREATE INDEX IF NOT EXISTS "AudienceVoter_token_idx" ON "AudienceVoter"("token");
-- Foreign key: AudienceVoter.sessionId -> LiveVotingSession.id
DO $$ BEGIN
ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_sessionId_fkey"
FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- 4. LiveVote: Foreign key and indexes for audienceVoterId
-- =============================================================================
CREATE INDEX IF NOT EXISTS "LiveVote_audienceVoterId_idx" ON "LiveVote"("audienceVoterId");
-- Foreign key: LiveVote.audienceVoterId -> AudienceVoter.id
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_audienceVoterId_fkey"
FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- Unique constraint: sessionId + projectId + audienceVoterId
DO $$ BEGIN
ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_sessionId_projectId_audienceVoterId_key"
UNIQUE ("sessionId", "projectId", "audienceVoterId");
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-- =============================================================================
-- SUMMARY:
--
-- LiveVotingSession new columns:
-- - votingMode (TEXT, default 'simple')
-- - criteriaJson (JSONB, nullable)
-- - audienceVotingMode (TEXT, default 'disabled')
-- - audienceMaxFavorites (INTEGER, default 3)
-- - audienceRequireId (BOOLEAN, default false)
-- - audienceVotingDuration (INTEGER, nullable)
--
-- LiveVote changes:
-- - criterionScoresJson (JSONB, nullable) - new column
-- - audienceVoterId (TEXT, nullable) - new column
-- - userId changed from NOT NULL to nullable
-- - New unique: (sessionId, projectId, audienceVoterId)
-- - New index: audienceVoterId
-- - New FK: audienceVoterId -> AudienceVoter(id)
--
-- New table: AudienceVoter
-- - id, sessionId, token (unique), identifier, identifierType,
-- ipAddress, userAgent, createdAt
-- - FK: sessionId -> LiveVotingSession(id) CASCADE
-- =============================================================================

View File

@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff