Round system redesign: Phases 1-7 complete
Full pipeline/track/stage architecture replacing the legacy round system. Schema: 11 new models (Pipeline, Track, Stage, StageTransition, ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor, OverrideAction, AudienceVoter) + 8 new enums. Backend: 9 new routers (pipeline, stage, routing, stageFiltering, stageAssignment, cohort, live, decision, award) + 6 new services (stage-engine, routing-engine, stage-filtering, stage-assignment, stage-notifications, live-control). Frontend: Pipeline wizard (17 components), jury stage pages (7), applicant pipeline pages (3), public stage pages (2), admin pipeline pages (5), shared stage components (3), SSE route, live hook. Phase 6 refit: 23 routers/services migrated from roundId to stageId, all frontend components refitted. Deleted round.ts (985 lines), roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx, 10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs. Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing, TypeScript 0 errors, Next.js build succeeds, 13 integrity checks, legacy symbol sweep clean, auto-seed on first Docker startup. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
246
prisma/integrity-checks.ts
Normal file
246
prisma/integrity-checks.ts
Normal file
@@ -0,0 +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()
|
||||
})
|
||||
Reference in New Issue
Block a user