Files
simstudioai--sim/apps/sim/lib/workflows/schedules/validation.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

189 lines
5.2 KiB
TypeScript

/**
* Client-safe schedule validation functions
* These can be used in both client and server contexts
*/
import { getErrorMessage } from '@sim/utils/errors'
import {
type BlockState,
calculateNextRunTime,
generateCronExpression,
getScheduleTimeValues,
getSubBlockValue,
validateCronExpression,
} from '@/lib/workflows/schedules/utils'
/**
* Result of schedule validation
*/
export interface ScheduleValidationResult {
isValid: boolean
error?: string
scheduleType?: string
cronExpression?: string
nextRunAt?: Date
timezone?: string
}
/**
* Check if a time value is valid (not empty/null/undefined)
*/
function isValidTimeValue(value: string | null | undefined): boolean {
return !!value && value.trim() !== '' && value.includes(':')
}
/**
* Check if a schedule type has valid configuration
* Uses raw block values to avoid false positives from default parsing
*/
function hasValidScheduleConfig(scheduleType: string | undefined, block: BlockState): boolean {
switch (scheduleType) {
case 'minutes': {
const rawValue = getSubBlockValue(block, 'minutesInterval')
const numValue = Number(rawValue)
return !!rawValue && !Number.isNaN(numValue) && numValue >= 1 && numValue <= 1440
}
case 'hourly': {
const rawValue = getSubBlockValue(block, 'hourlyMinute')
const numValue = Number(rawValue)
return rawValue !== '' && !Number.isNaN(numValue) && numValue >= 0 && numValue <= 59
}
case 'daily': {
const rawTime = getSubBlockValue(block, 'dailyTime')
return isValidTimeValue(rawTime)
}
case 'weekly': {
const rawDay = getSubBlockValue(block, 'weeklyDay')
const rawTime = getSubBlockValue(block, 'weeklyDayTime')
return !!rawDay && isValidTimeValue(rawTime)
}
case 'monthly': {
const rawDay = getSubBlockValue(block, 'monthlyDay')
const rawTime = getSubBlockValue(block, 'monthlyTime')
const dayNum = Number(rawDay)
return (
!!rawDay &&
!Number.isNaN(dayNum) &&
dayNum >= 1 &&
dayNum <= 31 &&
isValidTimeValue(rawTime)
)
}
case 'custom':
return !!getSubBlockValue(block, 'cronExpression')
default:
return false
}
}
/**
* Get human-readable error message for missing schedule configuration
*/
function getMissingConfigError(scheduleType: string): string {
switch (scheduleType) {
case 'minutes':
return 'Minutes interval is required for minute-based schedules'
case 'hourly':
return 'Minute value is required for hourly schedules'
case 'daily':
return 'Time is required for daily schedules'
case 'weekly':
return 'Day and time are required for weekly schedules'
case 'monthly':
return 'Day and time are required for monthly schedules'
case 'custom':
return 'Cron expression is required for custom schedules'
default:
return 'Schedule type is required'
}
}
/**
* Find schedule blocks in a workflow's blocks
* Only returns enabled schedule blocks (disabled blocks are skipped)
*/
export function findScheduleBlocks(blocks: Record<string, BlockState>): BlockState[] {
return Object.values(blocks).filter(
(block) => block.type === 'schedule' && block.enabled !== false
)
}
/**
* Validate a schedule block's configuration
* Returns validation result with error details if invalid
*/
export function validateScheduleBlock(block: BlockState): ScheduleValidationResult {
const scheduleType = getSubBlockValue(block, 'scheduleType')
if (!scheduleType) {
return {
isValid: false,
error: 'Schedule type is required',
}
}
const hasValidConfig = hasValidScheduleConfig(scheduleType, block)
if (!hasValidConfig) {
return {
isValid: false,
error: getMissingConfigError(scheduleType),
scheduleType,
}
}
const timezone = getSubBlockValue(block, 'timezone') || 'UTC'
try {
// Get parsed schedule values (safe to use after validation passes)
const scheduleValues = getScheduleTimeValues(block)
const sanitizedScheduleValues =
scheduleType !== 'custom' ? { ...scheduleValues, cronExpression: null } : scheduleValues
const cronExpression = generateCronExpression(scheduleType, sanitizedScheduleValues)
const validation = validateCronExpression(cronExpression, timezone)
if (!validation.isValid) {
return {
isValid: false,
error: `Invalid cron expression: ${validation.error}`,
scheduleType,
}
}
const nextRunAt = calculateNextRunTime(scheduleType, sanitizedScheduleValues)
return {
isValid: true,
scheduleType,
cronExpression,
nextRunAt,
timezone,
}
} catch (error) {
return {
isValid: false,
error: getErrorMessage(error, 'Failed to generate schedule'),
scheduleType,
}
}
}
/**
* Validate all schedule blocks in a workflow
* Returns the first validation error found, or success if all are valid
*/
export function validateWorkflowSchedules(
blocks: Record<string, BlockState>
): ScheduleValidationResult {
const scheduleBlocks = findScheduleBlocks(blocks)
for (const block of scheduleBlocks) {
const result = validateScheduleBlock(block)
if (!result.isValid) {
return result
}
}
return { isValid: true }
}