diff --git a/package.json b/package.json index cf1ad36db..e89df64f8 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "cron-parser": "^4.9.0", "currency-codes": "^2.2.0", "date-fns": "^4.1.0", "decimal.js": "^10.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06120631e..44a6c2ade 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: cmdk: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + cron-parser: + specifier: ^4.9.0 + version: 4.9.0 currency-codes: specifier: ^2.2.0 version: 2.2.0 @@ -4093,6 +4096,13 @@ packages: integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==, } + cron-parser@4.9.0: + resolution: + { + integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==, + } + engines: { node: '>=12.0.0' } + cross-spawn@7.0.6: resolution: { @@ -6576,6 +6586,13 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + luxon@3.7.2: + resolution: + { + integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==, + } + engines: { node: '>=12' } + magic-string@0.30.21: resolution: { @@ -11697,6 +11714,10 @@ snapshots: crelt@1.0.6: {} + cron-parser@4.9.0: + dependencies: + luxon: 3.7.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -13309,6 +13330,8 @@ snapshots: dependencies: react: 19.2.4 + luxon@3.7.2: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 diff --git a/src/components/functions/ConfigForm.tsx b/src/components/functions/ConfigForm.tsx index b49303448..73f5f1e4f 100644 --- a/src/components/functions/ConfigForm.tsx +++ b/src/components/functions/ConfigForm.tsx @@ -132,7 +132,12 @@ export const ConfigForm = ({ middlewares, data }: ConfigFormProps) => { )} {form.watch('functionType') === 'cron' && ( - + )} {(form.watch('functionType') === 'webhook' || form.watch('functionType') === 'request') && ( diff --git a/src/components/functions/CreateFunctionForm.tsx b/src/components/functions/CreateFunctionForm.tsx index d792ae6b2..400280b7c 100644 --- a/src/components/functions/CreateFunctionForm.tsx +++ b/src/components/functions/CreateFunctionForm.tsx @@ -144,6 +144,7 @@ export const CreateFunctionForm = ({ functionType: data.functionType, timeout: data.timeout, inputs: { + cronPattern: options.cronString, event: options.cronString, }, }) diff --git a/src/components/functions/EditFunctionForm.tsx b/src/components/functions/EditFunctionForm.tsx index fe4a781ad..d486e6a28 100644 --- a/src/components/functions/EditFunctionForm.tsx +++ b/src/components/functions/EditFunctionForm.tsx @@ -69,7 +69,10 @@ export const EditFunctionForm = ({ ? { options: { functionType: functionData.functionType, - cronString: functionData.inputs?.event!, + cronString: + functionData.inputs?.cronPattern ?? + functionData.inputs?.event ?? + '', }, } : {}), @@ -186,6 +189,7 @@ export const EditFunctionForm = ({ functionType: data.functionType, timeout: data.timeout, inputs: { + cronPattern: options.cronString, event: options.cronString, }, }) diff --git a/src/components/functions/zod.ts b/src/components/functions/zod.ts index 879e4001e..52c8b1188 100644 --- a/src/components/functions/zod.ts +++ b/src/components/functions/zod.ts @@ -1,4 +1,14 @@ import { z } from 'zod'; +import { parseExpression } from 'cron-parser'; + +function validateCronString(value: string): boolean { + try { + parseExpression(value.trim(), { tz: 'UTC' }); + return true; + } catch { + return false; + } +} export enum ParamsEnum { String = 'string', @@ -126,7 +136,13 @@ export const EventOptions = z.object({ }); export const CronOptions = z.object({ functionType: z.literal('cron'), - cronString: z.string(), + cronString: z + .string() + .min(1, 'Cron schedule is required') + .refine(validateCronString, { + message: + 'Invalid cron pattern. Use 5 fields: minute hour day month weekday (UTC). Example: */5 * * * *', + }), }); export const SocketOptions = z.object({ functionType: z.literal('socket'), diff --git a/src/lib/models/functions/schemas.ts b/src/lib/models/functions/schemas.ts index b12c1ea7d..cda692c14 100644 --- a/src/lib/models/functions/schemas.ts +++ b/src/lib/models/functions/schemas.ts @@ -1,6 +1,7 @@ interface IWebInputsInterface { method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; event?: string; + cronPattern?: string; bodyParams?: { [key: string]: any }[]; urlParams?: { [key: string]: any }[]; queryParams?: { [key: string]: any }[];