MOPC-App/src/lib/pipeline-validation.ts

113 lines
3.9 KiB
TypeScript

import type { ValidationResult, WizardState, WizardTrackConfig, WizardStageConfig } from '@/types/pipeline-wizard'
function ok(): ValidationResult {
return { valid: true, errors: [], warnings: [] }
}
function fail(errors: string[], warnings: string[] = []): ValidationResult {
return { valid: false, errors, warnings }
}
export function validateBasics(state: WizardState): ValidationResult {
const errors: string[] = []
if (!state.name.trim()) errors.push('Pipeline name is required')
if (!state.slug.trim()) errors.push('Pipeline slug is required')
else if (!/^[a-z0-9-]+$/.test(state.slug)) errors.push('Slug must be lowercase alphanumeric with hyphens only')
if (!state.programId) errors.push('Program must be selected')
return errors.length ? fail(errors) : ok()
}
export function validateStage(stage: WizardStageConfig): ValidationResult {
const errors: string[] = []
if (!stage.name.trim()) errors.push(`Stage name is required`)
if (!stage.slug.trim()) errors.push(`Stage slug is required`)
else if (!/^[a-z0-9-]+$/.test(stage.slug)) errors.push(`Stage slug "${stage.slug}" is invalid`)
return errors.length ? fail(errors) : ok()
}
export function validateTrack(track: WizardTrackConfig): ValidationResult {
const errors: string[] = []
const warnings: string[] = []
if (!track.name.trim()) errors.push('Track name is required')
if (!track.slug.trim()) errors.push('Track slug is required')
if (track.stages.length === 0) errors.push(`Track "${track.name}" must have at least one stage`)
// Check for duplicate slugs within track
const slugs = new Set<string>()
for (const stage of track.stages) {
if (slugs.has(stage.slug)) {
errors.push(`Duplicate stage slug "${stage.slug}" in track "${track.name}"`)
}
slugs.add(stage.slug)
const stageResult = validateStage(stage)
errors.push(...stageResult.errors)
}
// MAIN track should ideally have at least INTAKE and one other stage
if (track.kind === 'MAIN' && track.stages.length < 2) {
warnings.push('Main track should have at least 2 stages')
}
// AWARD tracks need awardConfig
if (track.kind === 'AWARD' && !track.awardConfig?.name) {
errors.push(`Award track "${track.name}" requires an award name`)
}
return errors.length ? fail(errors, warnings) : { valid: true, errors: [], warnings }
}
export function validateTracks(tracks: WizardTrackConfig[]): ValidationResult {
const errors: string[] = []
const warnings: string[] = []
if (tracks.length === 0) {
errors.push('At least one track is required')
return fail(errors)
}
const mainTracks = tracks.filter((t) => t.kind === 'MAIN')
if (mainTracks.length === 0) {
errors.push('At least one MAIN track is required')
} else if (mainTracks.length > 1) {
warnings.push('Multiple MAIN tracks detected — typically only one is needed')
}
// Check for duplicate track slugs
const trackSlugs = new Set<string>()
for (const track of tracks) {
if (trackSlugs.has(track.slug)) {
errors.push(`Duplicate track slug "${track.slug}"`)
}
trackSlugs.add(track.slug)
const trackResult = validateTrack(track)
errors.push(...trackResult.errors)
warnings.push(...trackResult.warnings)
}
return errors.length ? fail(errors, warnings) : { valid: true, errors: [], warnings }
}
export function validateNotifications(config: Record<string, boolean>): ValidationResult {
// Notifications are optional — just validate structure
return ok()
}
export function validateAll(state: WizardState): {
valid: boolean
sections: {
basics: ValidationResult
tracks: ValidationResult
notifications: ValidationResult
}
} {
const basics = validateBasics(state)
const tracks = validateTracks(state.tracks)
const notifications = validateNotifications(state.notificationConfig)
return {
valid: basics.valid && tracks.valid && notifications.valid,
sections: { basics, tracks, notifications },
}
}