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
189 lines
5.2 KiB
TypeScript
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 }
|
|
}
|