Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s
This commit is contained in:
@@ -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
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
-- =============================================================================
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
-- =============================================================================
|
||||
|
||||
@@ -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"
|
||||
|
||||
4200
prisma/schema.prisma
4200
prisma/schema.prisma
File diff suppressed because it is too large
Load Diff
2075
prisma/seed.ts
2075
prisma/seed.ts
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user