Skip to content

Commit 15e4e04

Browse files
committed
added additional validation
1 parent ce71008 commit 15e4e04

File tree

3 files changed

+124
-17
lines changed

3 files changed

+124
-17
lines changed

apps/sim/app/api/schedules/route.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
generateCronExpression,
1111
getScheduleTimeValues,
1212
getSubBlockValue,
13+
validateCronExpression,
1314
} from '@/lib/schedules/utils'
1415
import { db } from '@/db'
1516
import { workflowSchedule } from '@/db/schema'
@@ -192,6 +193,18 @@ export async function POST(req: NextRequest) {
192193

193194
cronExpression = generateCronExpression(defaultScheduleType, scheduleValues)
194195

196+
// Additional validation for custom cron expressions
197+
if (defaultScheduleType === 'custom' && cronExpression) {
198+
const validation = validateCronExpression(cronExpression)
199+
if (!validation.isValid) {
200+
logger.error(`[${requestId}] Invalid cron expression: ${validation.error}`)
201+
return NextResponse.json(
202+
{ error: `Invalid cron expression: ${validation.error}` },
203+
{ status: 400 }
204+
)
205+
}
206+
}
207+
195208
nextRunAt = calculateNextRunTime(defaultScheduleType, scheduleValues)
196209

197210
logger.debug(

apps/sim/lib/schedules/utils.test.ts

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getSubBlockValue,
1212
parseCronToHumanReadable,
1313
parseTimeString,
14+
validateCronExpression,
1415
} from '@/lib/schedules/utils'
1516

1617
describe('Schedule Utilities', () => {
@@ -102,7 +103,7 @@ describe('Schedule Utilities', () => {
102103
weeklyTime: [12, 0],
103104
monthlyDay: 15,
104105
monthlyTime: [14, 30],
105-
cronExpression: '',
106+
cronExpression: null,
106107
})
107108
})
108109

@@ -128,7 +129,7 @@ describe('Schedule Utilities', () => {
128129
weeklyTime: [9, 0], // Default
129130
monthlyDay: 1, // Default
130131
monthlyTime: [9, 0], // Default
131-
cronExpression: '',
132+
cronExpression: null,
132133
})
133134
})
134135
})
@@ -145,7 +146,7 @@ describe('Schedule Utilities', () => {
145146
monthlyDay: 15,
146147
monthlyTime: [14, 30] as [number, number],
147148
timezone: 'UTC',
148-
cronExpression: '',
149+
cronExpression: null,
149150
}
150151

151152
// Minutes (every 15 minutes)
@@ -199,7 +200,7 @@ describe('Schedule Utilities', () => {
199200
monthlyDay: 15,
200201
monthlyTime: [14, 30] as [number, number],
201202
timezone: 'UTC',
202-
cronExpression: '',
203+
cronExpression: null,
203204
}
204205

205206
expect(generateCronExpression('minutes', standardScheduleValues)).toBe('*/15 * * * *')
@@ -234,7 +235,7 @@ describe('Schedule Utilities', () => {
234235
weeklyTime: [9, 0] as [number, number],
235236
monthlyDay: 1,
236237
monthlyTime: [9, 0] as [number, number],
237-
cronExpression: '',
238+
cronExpression: null,
238239
}
239240

240241
const nextRun = calculateNextRunTime('minutes', scheduleValues)
@@ -259,7 +260,7 @@ describe('Schedule Utilities', () => {
259260
weeklyTime: [9, 0] as [number, number],
260261
monthlyDay: 1,
261262
monthlyTime: [9, 0] as [number, number],
262-
cronExpression: '',
263+
cronExpression: null,
263264
}
264265

265266
const nextRun = calculateNextRunTime('minutes', scheduleValues)
@@ -281,7 +282,7 @@ describe('Schedule Utilities', () => {
281282
weeklyTime: [9, 0] as [number, number],
282283
monthlyDay: 1,
283284
monthlyTime: [9, 0] as [number, number],
284-
cronExpression: '',
285+
cronExpression: null,
285286
}
286287

287288
const nextRun = calculateNextRunTime('hourly', scheduleValues)
@@ -304,7 +305,7 @@ describe('Schedule Utilities', () => {
304305
weeklyTime: [9, 0] as [number, number],
305306
monthlyDay: 1,
306307
monthlyTime: [9, 0] as [number, number],
307-
cronExpression: '',
308+
cronExpression: null,
308309
}
309310

310311
const nextRun = calculateNextRunTime('daily', scheduleValues)
@@ -328,7 +329,7 @@ describe('Schedule Utilities', () => {
328329
weeklyTime: [10, 0] as [number, number],
329330
monthlyDay: 1,
330331
monthlyTime: [9, 0] as [number, number],
331-
cronExpression: '',
332+
cronExpression: null,
332333
}
333334

334335
const nextRun = calculateNextRunTime('weekly', scheduleValues)
@@ -351,7 +352,7 @@ describe('Schedule Utilities', () => {
351352
weeklyTime: [9, 0] as [number, number],
352353
monthlyDay: 15,
353354
monthlyTime: [14, 30] as [number, number],
354-
cronExpression: '',
355+
cronExpression: null,
355356
}
356357

357358
const nextRun = calculateNextRunTime('monthly', scheduleValues)
@@ -376,7 +377,7 @@ describe('Schedule Utilities', () => {
376377
weeklyTime: [9, 0] as [number, number],
377378
monthlyDay: 1,
378379
monthlyTime: [9, 0] as [number, number],
379-
cronExpression: '',
380+
cronExpression: null,
380381
}
381382

382383
// Last ran 10 minutes ago
@@ -404,7 +405,7 @@ describe('Schedule Utilities', () => {
404405
weeklyTime: [9, 0] as [number, number],
405406
monthlyDay: 1,
406407
monthlyTime: [9, 0] as [number, number],
407-
cronExpression: '',
408+
cronExpression: null,
408409
}
409410

410411
const nextRun = calculateNextRunTime('minutes', scheduleValues)
@@ -425,7 +426,7 @@ describe('Schedule Utilities', () => {
425426
weeklyTime: [9, 0] as [number, number],
426427
monthlyDay: 1,
427428
monthlyTime: [9, 0] as [number, number],
428-
cronExpression: '',
429+
cronExpression: null,
429430
}
430431

431432
const nextRun = calculateNextRunTime('minutes', scheduleValues)
@@ -436,6 +437,50 @@ describe('Schedule Utilities', () => {
436437
})
437438
})
438439

440+
describe('validateCronExpression', () => {
441+
it.concurrent('should validate correct cron expressions', () => {
442+
expect(validateCronExpression('0 9 * * *')).toEqual({
443+
isValid: true,
444+
nextRun: expect.any(Date),
445+
})
446+
expect(validateCronExpression('*/15 * * * *')).toEqual({
447+
isValid: true,
448+
nextRun: expect.any(Date),
449+
})
450+
expect(validateCronExpression('30 14 15 * *')).toEqual({
451+
isValid: true,
452+
nextRun: expect.any(Date),
453+
})
454+
})
455+
456+
it.concurrent('should reject invalid cron expressions', () => {
457+
expect(validateCronExpression('invalid')).toEqual({
458+
isValid: false,
459+
error: expect.stringContaining('invalid'),
460+
})
461+
expect(validateCronExpression('60 * * * *')).toEqual({
462+
isValid: false,
463+
error: expect.any(String),
464+
})
465+
expect(validateCronExpression('')).toEqual({
466+
isValid: false,
467+
error: 'Cron expression cannot be empty',
468+
})
469+
expect(validateCronExpression(' ')).toEqual({
470+
isValid: false,
471+
error: 'Cron expression cannot be empty',
472+
})
473+
})
474+
475+
it.concurrent('should detect impossible cron expressions', () => {
476+
// This would be February 31st - impossible date
477+
expect(validateCronExpression('0 0 31 2 *')).toEqual({
478+
isValid: false,
479+
error: 'Cron expression produces no future occurrences',
480+
})
481+
})
482+
})
483+
439484
describe('parseCronToHumanReadable', () => {
440485
it.concurrent('should parse common cron patterns', () => {
441486
expect(parseCronToHumanReadable('* * * * *')).toBe('Every minute')

apps/sim/lib/schedules/utils.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,49 @@
1+
import { Cron } from 'croner'
12
import { createLogger } from '@/lib/logs/console-logger'
23
import { formatDateTime } from '@/lib/utils'
34

45
const logger = createLogger('ScheduleUtils')
56

7+
/**
8+
* Validates a cron expression and returns validation results
9+
* @param cronExpression - The cron expression to validate
10+
* @returns Validation result with isValid flag, error message, and next run date
11+
*/
12+
export function validateCronExpression(cronExpression: string): {
13+
isValid: boolean
14+
error?: string
15+
nextRun?: Date
16+
} {
17+
if (!cronExpression?.trim()) {
18+
return {
19+
isValid: false,
20+
error: 'Cron expression cannot be empty',
21+
}
22+
}
23+
24+
try {
25+
const cron = new Cron(cronExpression)
26+
const nextRun = cron.nextRun()
27+
28+
if (!nextRun) {
29+
return {
30+
isValid: false,
31+
error: 'Cron expression produces no future occurrences',
32+
}
33+
}
34+
35+
return {
36+
isValid: true,
37+
nextRun,
38+
}
39+
} catch (error) {
40+
return {
41+
isValid: false,
42+
error: error instanceof Error ? error.message : 'Invalid cron expression syntax',
43+
}
44+
}
45+
}
46+
647
export interface SubBlockValue {
748
value: string
849
}
@@ -60,7 +101,7 @@ export function getScheduleTimeValues(starterBlock: BlockState): {
60101
weeklyTime: [number, number]
61102
monthlyDay: number
62103
monthlyTime: [number, number]
63-
cronExpression: string
104+
cronExpression: string | null
64105
timezone: string
65106
} {
66107
// Extract schedule time (common field that can override others)
@@ -93,7 +134,15 @@ export function getScheduleTimeValues(starterBlock: BlockState): {
93134
const monthlyDay = Number.parseInt(monthlyDayStr) || 1
94135
const monthlyTime = parseTimeString(getSubBlockValue(starterBlock, 'monthlyTime'))
95136

96-
const cronExpression = getSubBlockValue(starterBlock, 'cronExpression') || ''
137+
const cronExpression = getSubBlockValue(starterBlock, 'cronExpression') || null
138+
139+
// Validate cron expression if provided
140+
if (cronExpression) {
141+
const validation = validateCronExpression(cronExpression)
142+
if (!validation.isValid) {
143+
throw new Error(`Invalid cron expression: ${validation.error}`)
144+
}
145+
}
97146

98147
return {
99148
scheduleTime,
@@ -246,8 +295,8 @@ export function generateCronExpression(
246295
}
247296

248297
case 'custom': {
249-
if (!scheduleValues.cronExpression) {
250-
throw new Error('No cron expression provided for custom schedule')
298+
if (!scheduleValues.cronExpression?.trim()) {
299+
throw new Error('Custom schedule requires a valid cron expression')
251300
}
252301
return scheduleValues.cronExpression
253302
}

0 commit comments

Comments
 (0)