-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/compt 75 typed job definitions ischeduler interface #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,2 @@ | ||
| #!/usr/bin/env sh | ||
| . "$(dirname -- "$0")/_/husky.sh" | ||
|
|
||
| npm run typecheck | ||
| npm run test:cov | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,14 +1,14 @@ | ||||||
| { | ||||||
| "name": "@ciscode/scheduler-kit", | ||||||
| "version": "0.0.0", | ||||||
| "description": "Typed job scheduler for NestJS. @Cron and @Interval and @Timeout decorators. Dynamic runtime scheduling. Concurrent execution guard.", | ||||||
| "description": "NestJS advanced scheduler wrapper with dynamic control and concurrency guards.", | ||||||
| "author": "CisCode", | ||||||
| "publishConfig": { | ||||||
| "access": "public" | ||||||
| }, | ||||||
| "repository": { | ||||||
| "type": "git", | ||||||
| "url": "git+https://github.com/CISCODE-MA/SchedulerKit.git" | ||||||
| "url": "git+https://github.com/CISCODE-MA/" | ||||||
|
||||||
| "url": "git+https://github.com/CISCODE-MA/" | |
| "url": "git+https://github.com/CISCODE-MA/scheduler-kit.git" |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nestjs/schedule version ranges are inconsistent: peerDependencies restrict to "^4 || ^5", but devDependencies installs "^6.1.1". Align these (either bump the peer range to include the dev version, or downgrade devDependency) to avoid local builds passing while consumers hit peer/dependency conflicts.
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "cron" package is listed in devDependencies, but there are no imports/usages of it anywhere in the repo. If this kit doesn’t directly use the library, drop the dependency (or add the intended usage) to avoid unnecessary install surface area.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| import { cron } from "./cron-builder"; | ||
|
|
||
| describe("cron builder", () => { | ||
| describe("every(n).minutes()", () => { | ||
| it("should build every-5-minutes expression", () => { | ||
| expect(cron.every(5).minutes()).toBe("*/5 * * * *"); | ||
| }); | ||
|
|
||
| it("should build every-1-minute expression", () => { | ||
| expect(cron.every(1).minutes()).toBe("*/1 * * * *"); | ||
| }); | ||
|
|
||
| it("should build every-30-minutes expression", () => { | ||
| expect(cron.every(30).minutes()).toBe("*/30 * * * *"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("every(n).hours()", () => { | ||
| it("should build every-2-hours expression", () => { | ||
| expect(cron.every(2).hours()).toBe("0 */2 * * *"); | ||
| }); | ||
|
|
||
| it("should build every-6-hours expression", () => { | ||
| expect(cron.every(6).hours()).toBe("0 */6 * * *"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("dailyAt()", () => { | ||
| it('should parse "9am" → 09:00', () => { | ||
| expect(cron.dailyAt("9am")).toBe("0 9 * * *"); | ||
| }); | ||
|
|
||
| it('should parse "9pm" → 21:00', () => { | ||
| expect(cron.dailyAt("9pm")).toBe("0 21 * * *"); | ||
| }); | ||
|
|
||
| it('should parse "9:30am" → 09:30', () => { | ||
| expect(cron.dailyAt("9:30am")).toBe("30 9 * * *"); | ||
| }); | ||
|
|
||
| it('should parse "9:30pm" → 21:30', () => { | ||
| expect(cron.dailyAt("9:30pm")).toBe("30 21 * * *"); | ||
| }); | ||
|
|
||
| it('should parse "12am" (midnight) → 00:00', () => { | ||
| expect(cron.dailyAt("12am")).toBe("0 0 * * *"); | ||
| }); | ||
|
|
||
| it('should parse "12pm" (noon) → 12:00', () => { | ||
| expect(cron.dailyAt("12pm")).toBe("0 12 * * *"); | ||
| }); | ||
|
|
||
| it('should parse 24-hour "14:30" → 14:30', () => { | ||
| expect(cron.dailyAt("14:30")).toBe("30 14 * * *"); | ||
| }); | ||
|
|
||
| it('should parse 24-hour "00:00" → midnight', () => { | ||
| expect(cron.dailyAt("00:00")).toBe("0 0 * * *"); | ||
| }); | ||
|
|
||
| it("should throw on invalid time format", () => { | ||
| expect(() => cron.dailyAt("invalid")).toThrow(); | ||
| }); | ||
| }); | ||
|
|
||
| describe("weekdaysAt()", () => { | ||
| it("should produce Mon–Fri expression", () => { | ||
| expect(cron.weekdaysAt("9am")).toBe("0 9 * * 1-5"); | ||
| }); | ||
|
|
||
| it("should respect time with minutes", () => { | ||
| expect(cron.weekdaysAt("9:30am")).toBe("30 9 * * 1-5"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("weekendsAt()", () => { | ||
| it("should produce Sat+Sun expression", () => { | ||
| expect(cron.weekendsAt("10am")).toBe("0 10 * * 6,0"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("weeklyOn()", () => { | ||
| it("should build monday expression", () => { | ||
| expect(cron.weeklyOn("monday", "9am")).toBe("0 9 * * 1"); | ||
| }); | ||
|
|
||
| it("should build friday expression", () => { | ||
| expect(cron.weeklyOn("friday", "6pm")).toBe("0 18 * * 5"); | ||
| }); | ||
|
|
||
| it("should build sunday expression", () => { | ||
| expect(cron.weeklyOn("sunday", "12am")).toBe("0 0 * * 0"); | ||
| }); | ||
|
|
||
| it("should build saturday expression", () => { | ||
| expect(cron.weeklyOn("saturday", "8am")).toBe("0 8 * * 6"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("monthlyOn()", () => { | ||
| it("should build 1st of month at midnight", () => { | ||
| expect(cron.monthlyOn(1, "12am")).toBe("0 0 1 * *"); | ||
| }); | ||
|
|
||
| it("should build 15th of month at noon", () => { | ||
| expect(cron.monthlyOn(15, "12pm")).toBe("0 12 15 * *"); | ||
| }); | ||
|
|
||
| it("should build last-ish day at 9am", () => { | ||
| expect(cron.monthlyOn(28, "9am")).toBe("0 9 28 * *"); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,133 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ─── Types ───────────────────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export type DayOfWeek = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | "monday" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | "tuesday" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | "wednesday" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | "thursday" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | "friday" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | "saturday" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | "sunday"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ─── Internal helpers ────────────────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DAY_MAP: Record<DayOfWeek, number> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sunday: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monday: 1, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tuesday: 2, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wednesday: 3, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| thursday: 4, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| friday: 5, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saturday: 6, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Parse a human-readable time string into { hour, minute }. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Accepted formats: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * '9am' → 09:00 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * '9pm' → 21:00 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * '9:30am' → 09:30 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * '9:30pm' → 21:30 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * '09:00' → 09:00 (24-hour) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * '21:30' → 21:30 (24-hour) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function parseTime(time: string): { hour: number; minute: number } { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ampm = /^(\d{1,2})(?::(\d{2}))?(am|pm)$/i.exec(time.trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (ampm) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let hour = parseInt(ampm[1] as string, 10); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const minute = ampm[2] ? parseInt(ampm[2], 10) : 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const period = (ampm[3] as string).toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (period === "pm" && hour !== 12) hour += 12; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (period === "am" && hour === 12) hour = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { hour, minute }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const h24 = /^(\d{1,2}):(\d{2})$/.exec(time.trim()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (h24) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { hour: parseInt(h24[1] as string, 10), minute: parseInt(h24[2] as string, 10) }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+36
to
+47
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ampm = /^(\d{1,2})(?::(\d{2}))?(am|pm)$/i.exec(time.trim()); | |
| if (ampm) { | |
| let hour = parseInt(ampm[1] as string, 10); | |
| const minute = ampm[2] ? parseInt(ampm[2], 10) : 0; | |
| const period = (ampm[3] as string).toLowerCase(); | |
| if (period === "pm" && hour !== 12) hour += 12; | |
| if (period === "am" && hour === 12) hour = 0; | |
| return { hour, minute }; | |
| } | |
| const h24 = /^(\d{1,2}):(\d{2})$/.exec(time.trim()); | |
| if (h24) { | |
| return { hour: parseInt(h24[1] as string, 10), minute: parseInt(h24[2] as string, 10) }; | |
| const trimmedTime = time.trim(); | |
| const ampm = /^(\d{1,2})(?::(\d{2}))?(am|pm)$/i.exec(trimmedTime); | |
| if (ampm) { | |
| const rawHour = parseInt(ampm[1] as string, 10); | |
| const minute = ampm[2] ? parseInt(ampm[2], 10) : 0; | |
| if (rawHour < 1 || rawHour > 12) { | |
| throw new Error(`Cannot parse time: "${time}". Hour must be between 1 and 12 for am/pm input.`); | |
| } | |
| if (minute < 0 || minute > 59) { | |
| throw new Error(`Cannot parse time: "${time}". Minute must be between 0 and 59.`); | |
| } | |
| let hour = rawHour; | |
| const period = (ampm[3] as string).toLowerCase(); | |
| if (period === "pm" && hour !== 12) hour += 12; | |
| if (period === "am" && hour === 12) hour = 0; | |
| return { hour, minute }; | |
| } | |
| const h24 = /^(\d{1,2}):(\d{2})$/.exec(trimmedTime); | |
| if (h24) { | |
| const hour = parseInt(h24[1] as string, 10); | |
| const minute = parseInt(h24[2] as string, 10); | |
| if (hour < 0 || hour > 23) { | |
| throw new Error(`Cannot parse time: "${time}". Hour must be between 0 and 23 for 24-hour input.`); | |
| } | |
| if (minute < 0 || minute > 59) { | |
| throw new Error(`Cannot parse time: "${time}". Minute must be between 0 and 59.`); | |
| } | |
| return { hour, minute }; |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example outputs in this docblock have extra spaces (e.g. "* /5" and "0 * /2") which are not valid cron syntax and don’t match what the functions actually return ("*/5", "0 */2"). Please correct the example strings to avoid misleading consumers.
| * cron.every(5).minutes() // "* /5 * * * *" | |
| * cron.every(2).hours() // "0 * /2 * * *" | |
| * cron.every(5).minutes() // "*/5 * * * * *" | |
| * cron.every(2).hours() // "0 */2 * * * *" |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cron.every(n) and cron.monthlyOn(dayOfMonth, ...) don’t validate inputs (e.g., n <= 0 / non-integers, dayOfMonth outside 1–31). These produce invalid cron strings. Validate these arguments and throw an error for invalid values.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| import { CronExpression } from "./cron-expression"; | ||
|
|
||
| describe("CronExpression", () => { | ||
| it("should export EVERY_MINUTE", () => { | ||
| expect(CronExpression.EVERY_MINUTE).toBe("* * * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_5_MINUTES", () => { | ||
| expect(CronExpression.EVERY_5_MINUTES).toBe("*/5 * * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_10_MINUTES", () => { | ||
| expect(CronExpression.EVERY_10_MINUTES).toBe("*/10 * * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_15_MINUTES", () => { | ||
| expect(CronExpression.EVERY_15_MINUTES).toBe("*/15 * * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_30_MINUTES", () => { | ||
| expect(CronExpression.EVERY_30_MINUTES).toBe("*/30 * * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_HOUR", () => { | ||
| expect(CronExpression.EVERY_HOUR).toBe("0 * * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_2_HOURS", () => { | ||
| expect(CronExpression.EVERY_2_HOURS).toBe("0 */2 * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_6_HOURS", () => { | ||
| expect(CronExpression.EVERY_6_HOURS).toBe("0 */6 * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_12_HOURS", () => { | ||
| expect(CronExpression.EVERY_12_HOURS).toBe("0 */12 * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_DAY_AT_MIDNIGHT", () => { | ||
| expect(CronExpression.EVERY_DAY_AT_MIDNIGHT).toBe("0 0 * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_DAY_AT_9AM", () => { | ||
| expect(CronExpression.EVERY_DAY_AT_9AM).toBe("0 9 * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_DAY_AT_NOON", () => { | ||
| expect(CronExpression.EVERY_DAY_AT_NOON).toBe("0 12 * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_DAY_AT_6PM", () => { | ||
| expect(CronExpression.EVERY_DAY_AT_6PM).toBe("0 18 * * *"); | ||
| }); | ||
|
|
||
| it("should export EVERY_WEEKDAY_9AM", () => { | ||
| expect(CronExpression.EVERY_WEEKDAY_9AM).toBe("0 9 * * 1-5"); | ||
| }); | ||
|
|
||
| it("should export EVERY_WEEKEND_MIDNIGHT", () => { | ||
| expect(CronExpression.EVERY_WEEKEND_MIDNIGHT).toBe("0 0 * * 6,0"); | ||
| }); | ||
|
|
||
| it("should export EVERY_MONDAY_9AM", () => { | ||
| expect(CronExpression.EVERY_MONDAY_9AM).toBe("0 9 * * 1"); | ||
| }); | ||
|
|
||
| it("should export EVERY_SUNDAY_MIDNIGHT", () => { | ||
| expect(CronExpression.EVERY_SUNDAY_MIDNIGHT).toBe("0 0 * * 0"); | ||
| }); | ||
|
|
||
| it("should export FIRST_OF_MONTH", () => { | ||
| expect(CronExpression.FIRST_OF_MONTH).toBe("0 0 1 * *"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This Husky hook no longer has a shebang or Husky bootstrap (the removed lines). Without a shebang, Git may fail to execute the hook with an "exec format" error, and without husky.sh you lose Husky’s environment setup/opt-out behavior. Restore the standard Husky header at the top of this hook.