@open-mercato/scheduler 0.7.1-develop.7193.1.910a5b0a1e → 0.7.1-develop.7194.1.ab4fc81f82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ScheduledJob } from "./modules/scheduler/data/entities.js";
2
2
  import { SchedulerService } from "./modules/scheduler/services/schedulerService.js";
3
- import { parseCronExpression, validateCron } from "./modules/scheduler/lib/cronParser.js";
3
+ import { parseCronExpression, validateCron, validateCronExpression } from "./modules/scheduler/lib/cronParser.js";
4
4
  import { parseInterval, validateInterval, intervalToHuman } from "./modules/scheduler/lib/intervalParser.js";
5
5
  import { calculateNextRun, recalculateNextRun } from "./modules/scheduler/lib/nextRunCalculator.js";
6
6
  export {
@@ -12,6 +12,7 @@ export {
12
12
  parseInterval,
13
13
  recalculateNextRun,
14
14
  validateCron,
15
+ validateCronExpression,
15
16
  validateInterval
16
17
  };
17
18
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["/**\n * @open-mercato/scheduler\n * \n * Database-managed scheduled jobs with admin UI\n */\n\nexport { ScheduledJob } from './modules/scheduler/data/entities.js'\nexport { SchedulerService } from './modules/scheduler/services/schedulerService.js'\nexport type { ScheduleRegistration } from './modules/scheduler/services/schedulerService.js'\n\n// Parsers and utilities\nexport { parseCronExpression, validateCron } from './modules/scheduler/lib/cronParser.js'\nexport { parseInterval, validateInterval, intervalToHuman } from './modules/scheduler/lib/intervalParser.js'\nexport { calculateNextRun, recalculateNextRun } from './modules/scheduler/lib/nextRunCalculator.js'\n"],
5
- "mappings": "AAMA,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AAIjC,SAAS,qBAAqB,oBAAoB;AAClD,SAAS,eAAe,kBAAkB,uBAAuB;AACjE,SAAS,kBAAkB,0BAA0B;",
4
+ "sourcesContent": ["/**\n * @open-mercato/scheduler\n * \n * Database-managed scheduled jobs with admin UI\n */\n\nexport { ScheduledJob } from './modules/scheduler/data/entities.js'\nexport { SchedulerService } from './modules/scheduler/services/schedulerService.js'\nexport type { ScheduleRegistration } from './modules/scheduler/services/schedulerService.js'\n\n// Parsers and utilities\nexport { parseCronExpression, validateCron, validateCronExpression } from './modules/scheduler/lib/cronParser.js'\nexport type { CronValidationResult } from './modules/scheduler/lib/cronParser.js'\nexport { parseInterval, validateInterval, intervalToHuman } from './modules/scheduler/lib/intervalParser.js'\nexport { calculateNextRun, recalculateNextRun } from './modules/scheduler/lib/nextRunCalculator.js'\n"],
5
+ "mappings": "AAMA,SAAS,oBAAoB;AAC7B,SAAS,wBAAwB;AAIjC,SAAS,qBAAqB,cAAc,8BAA8B;AAE1E,SAAS,eAAe,kBAAkB,uBAAuB;AACjE,SAAS,kBAAkB,0BAA0B;",
6
6
  "names": []
7
7
  }
@@ -39,6 +39,28 @@ function getNextOccurrences(cronExpression, count, timezone = "UTC", currentDate
39
39
  return [];
40
40
  }
41
41
  }
42
+ function validateCronExpression(cronExpression, options = {}) {
43
+ if (!cronExpression || cronExpression.trim() === "") {
44
+ return { ok: false, error: "Cron expression cannot be empty" };
45
+ }
46
+ const count = Math.max(1, Math.min(options.count ?? 3, 10));
47
+ try {
48
+ const interval = parseExpression(cronExpression, {
49
+ currentDate: options.currentDate || /* @__PURE__ */ new Date(),
50
+ tz: options.timezone ?? "UTC"
51
+ });
52
+ const nextRuns = [];
53
+ for (let i = 0; i < count; i++) {
54
+ nextRuns.push(interval.next().toDate());
55
+ }
56
+ return { ok: true, nextRuns };
57
+ } catch (error) {
58
+ return {
59
+ ok: false,
60
+ error: error instanceof Error ? error.message : "Invalid cron expression"
61
+ };
62
+ }
63
+ }
42
64
  function validateCron(cronExpression) {
43
65
  if (!cronExpression || cronExpression.trim() === "") {
44
66
  return false;
@@ -57,6 +79,7 @@ function validateCron(cronExpression) {
57
79
  export {
58
80
  getNextOccurrences,
59
81
  parseCronExpression,
60
- validateCron
82
+ validateCron,
83
+ validateCronExpression
61
84
  };
62
85
  //# sourceMappingURL=cronParser.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/scheduler/lib/cronParser.ts"],
4
- "sourcesContent": ["import { CronExpressionParser } from 'cron-parser'\n\nconst parseExpression = CronExpressionParser.parse.bind(CronExpressionParser)\n\nexport interface CronParseResult {\n isValid: boolean\n nextRun?: Date\n error?: string\n}\n\n/**\n * Parse and validate a cron expression\n */\nexport function parseCronExpression(\n cronExpression: string,\n timezone: string = 'UTC',\n currentDate?: Date\n): CronParseResult {\n // Explicitly reject empty strings\n if (!cronExpression || cronExpression.trim() === '') {\n return {\n isValid: false,\n error: 'Cron expression cannot be empty',\n }\n }\n \n try {\n const interval = parseExpression(cronExpression, {\n currentDate: currentDate || new Date(),\n tz: timezone,\n })\n \n const nextRun = interval.next().toDate()\n \n return {\n isValid: true,\n nextRun,\n }\n } catch (error) {\n return {\n isValid: false,\n error: error instanceof Error ? error.message : 'Invalid cron expression',\n }\n }\n}\n\n/**\n * Get the next N occurrences of a cron expression\n */\nexport function getNextOccurrences(\n cronExpression: string,\n count: number,\n timezone: string = 'UTC',\n currentDate?: Date\n): Date[] {\n try {\n const interval = parseExpression(cronExpression, {\n currentDate: currentDate || new Date(),\n tz: timezone,\n })\n \n const occurrences: Date[] = []\n for (let i = 0; i < count; i++) {\n occurrences.push(interval.next().toDate())\n }\n \n return occurrences\n } catch (error) {\n return []\n }\n}\n\n/**\n * Validate a cron expression\n * Only supports standard 5-field cron format (minute hour day month weekday)\n */\nexport function validateCron(cronExpression: string): boolean {\n // Explicitly reject empty strings\n if (!cronExpression || cronExpression.trim() === '') {\n return false\n }\n \n // Check for exactly 5 fields (standard cron format)\n // Split by whitespace and filter out empty strings\n const fields = cronExpression.trim().split(/\\s+/).filter(f => f.length > 0)\n if (fields.length !== 5) {\n return false\n }\n \n try {\n parseExpression(cronExpression)\n return true\n } catch {\n return false\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,4BAA4B;AAErC,MAAM,kBAAkB,qBAAqB,MAAM,KAAK,oBAAoB;AAWrE,SAAS,oBACd,gBACA,WAAmB,OACnB,aACiB;AAEjB,MAAI,CAAC,kBAAkB,eAAe,KAAK,MAAM,IAAI;AACnD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI;AACF,UAAM,WAAW,gBAAgB,gBAAgB;AAAA,MAC/C,aAAa,eAAe,oBAAI,KAAK;AAAA,MACrC,IAAI;AAAA,IACN,CAAC;AAED,UAAM,UAAU,SAAS,KAAK,EAAE,OAAO;AAEvC,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD;AAAA,EACF;AACF;AAKO,SAAS,mBACd,gBACA,OACA,WAAmB,OACnB,aACQ;AACR,MAAI;AACF,UAAM,WAAW,gBAAgB,gBAAgB;AAAA,MAC/C,aAAa,eAAe,oBAAI,KAAK;AAAA,MACrC,IAAI;AAAA,IACN,CAAC;AAED,UAAM,cAAsB,CAAC;AAC7B,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,kBAAY,KAAK,SAAS,KAAK,EAAE,OAAO,CAAC;AAAA,IAC3C;AAEA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,CAAC;AAAA,EACV;AACF;AAMO,SAAS,aAAa,gBAAiC;AAE5D,MAAI,CAAC,kBAAkB,eAAe,KAAK,MAAM,IAAI;AACnD,WAAO;AAAA,EACT;AAIA,QAAM,SAAS,eAAe,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC;AAC1E,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,oBAAgB,cAAc;AAC9B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import { CronExpressionParser } from 'cron-parser'\n\nconst parseExpression = CronExpressionParser.parse.bind(CronExpressionParser)\n\nexport interface CronParseResult {\n isValid: boolean\n nextRun?: Date\n error?: string\n}\n\n/**\n * Parse and validate a cron expression\n */\nexport function parseCronExpression(\n cronExpression: string,\n timezone: string = 'UTC',\n currentDate?: Date\n): CronParseResult {\n // Explicitly reject empty strings\n if (!cronExpression || cronExpression.trim() === '') {\n return {\n isValid: false,\n error: 'Cron expression cannot be empty',\n }\n }\n \n try {\n const interval = parseExpression(cronExpression, {\n currentDate: currentDate || new Date(),\n tz: timezone,\n })\n \n const nextRun = interval.next().toDate()\n \n return {\n isValid: true,\n nextRun,\n }\n } catch (error) {\n return {\n isValid: false,\n error: error instanceof Error ? error.message : 'Invalid cron expression',\n }\n }\n}\n\n/**\n * Get the next N occurrences of a cron expression\n */\nexport function getNextOccurrences(\n cronExpression: string,\n count: number,\n timezone: string = 'UTC',\n currentDate?: Date\n): Date[] {\n try {\n const interval = parseExpression(cronExpression, {\n currentDate: currentDate || new Date(),\n tz: timezone,\n })\n \n const occurrences: Date[] = []\n for (let i = 0; i < count; i++) {\n occurrences.push(interval.next().toDate())\n }\n \n return occurrences\n } catch (error) {\n return []\n }\n}\n\nexport interface CronValidationResult {\n ok: boolean\n nextRuns?: Date[]\n error?: string\n}\n\n/**\n * Semantic cron validation with upcoming occurrences \u2014 the one-call helper for\n * form previews and server-side validators. Accepts anything cron-parser can\n * schedule (standard 5-field and 6-field-with-seconds expressions); a shape\n * that merely LOOKS like cron (`foo bar baz qux quux`) fails here even though\n * it passes token-count regexes.\n */\nexport function validateCronExpression(\n cronExpression: string,\n options: { count?: number; timezone?: string; currentDate?: Date } = {}\n): CronValidationResult {\n if (!cronExpression || cronExpression.trim() === '') {\n return { ok: false, error: 'Cron expression cannot be empty' }\n }\n const count = Math.max(1, Math.min(options.count ?? 3, 10))\n try {\n const interval = parseExpression(cronExpression, {\n currentDate: options.currentDate || new Date(),\n tz: options.timezone ?? 'UTC',\n })\n const nextRuns: Date[] = []\n for (let i = 0; i < count; i++) {\n nextRuns.push(interval.next().toDate())\n }\n return { ok: true, nextRuns }\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : 'Invalid cron expression',\n }\n }\n}\n\n/**\n * Validate a cron expression\n * Only supports standard 5-field cron format (minute hour day month weekday)\n */\nexport function validateCron(cronExpression: string): boolean {\n // Explicitly reject empty strings\n if (!cronExpression || cronExpression.trim() === '') {\n return false\n }\n \n // Check for exactly 5 fields (standard cron format)\n // Split by whitespace and filter out empty strings\n const fields = cronExpression.trim().split(/\\s+/).filter(f => f.length > 0)\n if (fields.length !== 5) {\n return false\n }\n \n try {\n parseExpression(cronExpression)\n return true\n } catch {\n return false\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,4BAA4B;AAErC,MAAM,kBAAkB,qBAAqB,MAAM,KAAK,oBAAoB;AAWrE,SAAS,oBACd,gBACA,WAAmB,OACnB,aACiB;AAEjB,MAAI,CAAC,kBAAkB,eAAe,KAAK,MAAM,IAAI;AACnD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI;AACF,UAAM,WAAW,gBAAgB,gBAAgB;AAAA,MAC/C,aAAa,eAAe,oBAAI,KAAK;AAAA,MACrC,IAAI;AAAA,IACN,CAAC;AAED,UAAM,UAAU,SAAS,KAAK,EAAE,OAAO;AAEvC,WAAO;AAAA,MACL,SAAS;AAAA,MACT;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD;AAAA,EACF;AACF;AAKO,SAAS,mBACd,gBACA,OACA,WAAmB,OACnB,aACQ;AACR,MAAI;AACF,UAAM,WAAW,gBAAgB,gBAAgB;AAAA,MAC/C,aAAa,eAAe,oBAAI,KAAK;AAAA,MACrC,IAAI;AAAA,IACN,CAAC;AAED,UAAM,cAAsB,CAAC;AAC7B,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,kBAAY,KAAK,SAAS,KAAK,EAAE,OAAO,CAAC;AAAA,IAC3C;AAEA,WAAO;AAAA,EACT,SAAS,OAAO;AACd,WAAO,CAAC;AAAA,EACV;AACF;AAeO,SAAS,uBACd,gBACA,UAAqE,CAAC,GAChD;AACtB,MAAI,CAAC,kBAAkB,eAAe,KAAK,MAAM,IAAI;AACnD,WAAO,EAAE,IAAI,OAAO,OAAO,kCAAkC;AAAA,EAC/D;AACA,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,QAAQ,SAAS,GAAG,EAAE,CAAC;AAC1D,MAAI;AACF,UAAM,WAAW,gBAAgB,gBAAgB;AAAA,MAC/C,aAAa,QAAQ,eAAe,oBAAI,KAAK;AAAA,MAC7C,IAAI,QAAQ,YAAY;AAAA,IAC1B,CAAC;AACD,UAAM,WAAmB,CAAC;AAC1B,aAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,eAAS,KAAK,SAAS,KAAK,EAAE,OAAO,CAAC;AAAA,IACxC;AACA,WAAO,EAAE,IAAI,MAAM,SAAS;AAAA,EAC9B,SAAS,OAAO;AACd,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,iBAAiB,QAAQ,MAAM,UAAU;AAAA,IAClD;AAAA,EACF;AACF;AAMO,SAAS,aAAa,gBAAiC;AAE5D,MAAI,CAAC,kBAAkB,eAAe,KAAK,MAAM,IAAI;AACnD,WAAO;AAAA,EACT;AAIA,QAAM,SAAS,eAAe,KAAK,EAAE,MAAM,KAAK,EAAE,OAAO,OAAK,EAAE,SAAS,CAAC;AAC1E,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,oBAAgB,cAAc;AAC9B,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/scheduler",
3
- "version": "0.7.1-develop.7193.1.910a5b0a1e",
3
+ "version": "0.7.1-develop.7194.1.ab4fc81f82",
4
4
  "license": "MIT",
5
5
  "description": "Database-managed scheduled jobs with admin UI",
6
6
  "type": "module",
@@ -59,13 +59,13 @@
59
59
  "./*/*/*/*/*/*.json": "./src/*/*/*/*/*/*.json"
60
60
  },
61
61
  "dependencies": {
62
- "@open-mercato/events": "0.7.1-develop.7193.1.910a5b0a1e",
63
- "@open-mercato/queue": "0.7.1-develop.7193.1.910a5b0a1e",
62
+ "@open-mercato/events": "0.7.1-develop.7194.1.ab4fc81f82",
63
+ "@open-mercato/queue": "0.7.1-develop.7194.1.ab4fc81f82",
64
64
  "cron-parser": "^5.6.2"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@mikro-orm/core": "^7.0.14",
68
- "@open-mercato/shared": "0.7.1-develop.7193.1.910a5b0a1e",
68
+ "@open-mercato/shared": "0.7.1-develop.7194.1.ab4fc81f82",
69
69
  "bullmq": "^5.0.0 || ^6.0.0",
70
70
  "ioredis": "^5.0.0 || ^6.0.0",
71
71
  "zod": ">=3.23.0"
@@ -79,7 +79,7 @@
79
79
  }
80
80
  },
81
81
  "devDependencies": {
82
- "@open-mercato/shared": "0.7.1-develop.7193.1.910a5b0a1e",
82
+ "@open-mercato/shared": "0.7.1-develop.7194.1.ab4fc81f82",
83
83
  "@types/jest": "^30.0.0",
84
84
  "@types/node": "^26.4.0",
85
85
  "jest": "^30.4.2",
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ export { SchedulerService } from './modules/scheduler/services/schedulerService.
9
9
  export type { ScheduleRegistration } from './modules/scheduler/services/schedulerService.js'
10
10
 
11
11
  // Parsers and utilities
12
- export { parseCronExpression, validateCron } from './modules/scheduler/lib/cronParser.js'
12
+ export { parseCronExpression, validateCron, validateCronExpression } from './modules/scheduler/lib/cronParser.js'
13
+ export type { CronValidationResult } from './modules/scheduler/lib/cronParser.js'
13
14
  export { parseInterval, validateInterval, intervalToHuman } from './modules/scheduler/lib/intervalParser.js'
14
15
  export { calculateNextRun, recalculateNextRun } from './modules/scheduler/lib/nextRunCalculator.js'
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from '@jest/globals'
2
- import { parseCronExpression, getNextOccurrences, validateCron } from '../cronParser'
2
+ import { parseCronExpression, getNextOccurrences, validateCron, validateCronExpression } from '../cronParser'
3
3
 
4
4
  describe('cronParser', () => {
5
5
  describe('parseCronExpression', () => {
@@ -241,4 +241,54 @@ describe('cronParser', () => {
241
241
  expect(nextRun.getUTCDate()).toBe(1)
242
242
  })
243
243
  })
244
+
245
+ describe('validateCronExpression', () => {
246
+ it('returns ok with three upcoming runs by default for a valid expression', () => {
247
+ const result = validateCronExpression('0 7 * * 1')
248
+
249
+ expect(result.ok).toBe(true)
250
+ expect(result.error).toBeUndefined()
251
+ expect(result.nextRuns).toHaveLength(3)
252
+ for (const run of result.nextRuns!) {
253
+ expect(run).toBeInstanceOf(Date)
254
+ }
255
+ expect(result.nextRuns![0].getTime()).toBeLessThan(result.nextRuns![1].getTime())
256
+ expect(result.nextRuns![1].getTime()).toBeLessThan(result.nextRuns![2].getTime())
257
+ })
258
+
259
+ it('rejects shape-valid garbage that token-count regexes accept', () => {
260
+ const result = validateCronExpression('foo bar baz qux quux')
261
+
262
+ expect(result.ok).toBe(false)
263
+ expect(result.nextRuns).toBeUndefined()
264
+ expect(result.error).toBeTruthy()
265
+ })
266
+
267
+ it('rejects empty input', () => {
268
+ expect(validateCronExpression('').ok).toBe(false)
269
+ expect(validateCronExpression(' ').ok).toBe(false)
270
+ })
271
+
272
+ it('honors a custom count (clamped to 1..10)', () => {
273
+ expect(validateCronExpression('*/5 * * * *', { count: 5 }).nextRuns).toHaveLength(5)
274
+ expect(validateCronExpression('*/5 * * * *', { count: 0 }).nextRuns).toHaveLength(1)
275
+ expect(validateCronExpression('*/5 * * * *', { count: 99 }).nextRuns).toHaveLength(10)
276
+ })
277
+
278
+ it('computes occurrences in the requested timezone from a fixed date', () => {
279
+ const currentDate = new Date('2026-01-01T00:00:00Z')
280
+ const result = validateCronExpression('0 12 * * *', { timezone: 'America/New_York', currentDate, count: 1 })
281
+
282
+ expect(result.ok).toBe(true)
283
+ // Noon in New York (EST, UTC-5) is 17:00 UTC.
284
+ expect(result.nextRuns![0].toISOString()).toBe('2026-01-01T17:00:00.000Z')
285
+ })
286
+
287
+ it('accepts 6-field (seconds) expressions that cron-parser schedules', () => {
288
+ const result = validateCronExpression('*/30 * * * * *', { count: 2 })
289
+
290
+ expect(result.ok).toBe(true)
291
+ expect(result.nextRuns).toHaveLength(2)
292
+ })
293
+ })
244
294
  })
@@ -70,6 +70,45 @@ export function getNextOccurrences(
70
70
  }
71
71
  }
72
72
 
73
+ export interface CronValidationResult {
74
+ ok: boolean
75
+ nextRuns?: Date[]
76
+ error?: string
77
+ }
78
+
79
+ /**
80
+ * Semantic cron validation with upcoming occurrences — the one-call helper for
81
+ * form previews and server-side validators. Accepts anything cron-parser can
82
+ * schedule (standard 5-field and 6-field-with-seconds expressions); a shape
83
+ * that merely LOOKS like cron (`foo bar baz qux quux`) fails here even though
84
+ * it passes token-count regexes.
85
+ */
86
+ export function validateCronExpression(
87
+ cronExpression: string,
88
+ options: { count?: number; timezone?: string; currentDate?: Date } = {}
89
+ ): CronValidationResult {
90
+ if (!cronExpression || cronExpression.trim() === '') {
91
+ return { ok: false, error: 'Cron expression cannot be empty' }
92
+ }
93
+ const count = Math.max(1, Math.min(options.count ?? 3, 10))
94
+ try {
95
+ const interval = parseExpression(cronExpression, {
96
+ currentDate: options.currentDate || new Date(),
97
+ tz: options.timezone ?? 'UTC',
98
+ })
99
+ const nextRuns: Date[] = []
100
+ for (let i = 0; i < count; i++) {
101
+ nextRuns.push(interval.next().toDate())
102
+ }
103
+ return { ok: true, nextRuns }
104
+ } catch (error) {
105
+ return {
106
+ ok: false,
107
+ error: error instanceof Error ? error.message : 'Invalid cron expression',
108
+ }
109
+ }
110
+ }
111
+
73
112
  /**
74
113
  * Validate a cron expression
75
114
  * Only supports standard 5-field cron format (minute hour day month weekday)