feat: add unit tests and Gitea Actions CI pipeline
- Extract pure utility functions to lib/utils.ts for testability - Add 28 unit tests covering: computeNextDueDate, resetSubtasks, parseTaskIdentifier, validators, statusSortOrder - Add Gitea Actions workflow (.gitea/workflows/ci.yml) that runs tests and type checking on push/PR to main - Refactor tasks.ts to use extracted utils
This commit is contained in:
32
.gitea/workflows/ci.yml
Normal file
32
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-backend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: backend
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: bun test --bail
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: bun x tsc --noEmit
|
||||||
@@ -7,7 +7,9 @@
|
|||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"test": "bun test",
|
||||||
|
"test:ci": "bun test --bail"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elysiajs/cors": "^1.2.0",
|
"@elysiajs/cors": "^1.2.0",
|
||||||
|
|||||||
242
backend/src/lib/utils.test.ts
Normal file
242
backend/src/lib/utils.test.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
import { describe, test, expect, beforeAll } from "bun:test";
|
||||||
|
import {
|
||||||
|
computeNextDueDate,
|
||||||
|
resetSubtasks,
|
||||||
|
parseTaskIdentifier,
|
||||||
|
isValidTaskStatus,
|
||||||
|
isValidTaskPriority,
|
||||||
|
isValidTaskSource,
|
||||||
|
isValidRecurrenceFrequency,
|
||||||
|
statusSortOrder,
|
||||||
|
} from "./utils";
|
||||||
|
import type { Subtask } from "../db/schema";
|
||||||
|
|
||||||
|
// ── computeNextDueDate ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("computeNextDueDate", () => {
|
||||||
|
test("daily adds 1 day from now when no fromDate", () => {
|
||||||
|
const before = new Date();
|
||||||
|
const result = computeNextDueDate("daily");
|
||||||
|
const after = new Date();
|
||||||
|
// Should be roughly 1 day from now
|
||||||
|
const diffMs = result.getTime() - before.getTime();
|
||||||
|
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||||
|
expect(diffMs).toBeGreaterThanOrEqual(oneDayMs - 1000);
|
||||||
|
expect(diffMs).toBeLessThanOrEqual(oneDayMs + 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("weekly adds 7 days", () => {
|
||||||
|
const before = new Date();
|
||||||
|
const result = computeNextDueDate("weekly");
|
||||||
|
const diffMs = result.getTime() - before.getTime();
|
||||||
|
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
expect(diffMs).toBeGreaterThanOrEqual(sevenDaysMs - 1000);
|
||||||
|
expect(diffMs).toBeLessThanOrEqual(sevenDaysMs + 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("biweekly adds 14 days", () => {
|
||||||
|
const before = new Date();
|
||||||
|
const result = computeNextDueDate("biweekly");
|
||||||
|
const diffMs = result.getTime() - before.getTime();
|
||||||
|
const fourteenDaysMs = 14 * 24 * 60 * 60 * 1000;
|
||||||
|
expect(diffMs).toBeGreaterThanOrEqual(fourteenDaysMs - 1000);
|
||||||
|
expect(diffMs).toBeLessThanOrEqual(fourteenDaysMs + 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("monthly adds approximately 1 month", () => {
|
||||||
|
const before = new Date();
|
||||||
|
const result = computeNextDueDate("monthly");
|
||||||
|
// Should be roughly 28-31 days from now
|
||||||
|
const diffDays = (result.getTime() - before.getTime()) / (24 * 60 * 60 * 1000);
|
||||||
|
expect(diffDays).toBeGreaterThanOrEqual(27);
|
||||||
|
expect(diffDays).toBeLessThanOrEqual(32);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses fromDate when it is in the future", () => {
|
||||||
|
const futureDate = new Date();
|
||||||
|
futureDate.setDate(futureDate.getDate() + 10); // 10 days from now
|
||||||
|
const result = computeNextDueDate("daily", futureDate);
|
||||||
|
// Should be futureDate + 1 day
|
||||||
|
const expected = new Date(futureDate);
|
||||||
|
expected.setDate(expected.getDate() + 1);
|
||||||
|
expect(result.getDate()).toBe(expected.getDate());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("ignores fromDate when it is in the past", () => {
|
||||||
|
const pastDate = new Date("2020-01-01");
|
||||||
|
const before = new Date();
|
||||||
|
const result = computeNextDueDate("daily", pastDate);
|
||||||
|
// Should be ~1 day from now, not from 2020
|
||||||
|
expect(result.getFullYear()).toBeGreaterThanOrEqual(before.getFullYear());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles null fromDate", () => {
|
||||||
|
const before = new Date();
|
||||||
|
const result = computeNextDueDate("weekly", null);
|
||||||
|
const diffMs = result.getTime() - before.getTime();
|
||||||
|
const sevenDaysMs = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
expect(diffMs).toBeGreaterThanOrEqual(sevenDaysMs - 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── resetSubtasks ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("resetSubtasks", () => {
|
||||||
|
test("resets all subtasks to uncompleted", () => {
|
||||||
|
const subtasks: Subtask[] = [
|
||||||
|
{ id: "st-1", title: "Do thing", completed: true, completedAt: "2025-01-01T00:00:00Z", createdAt: "2024-12-01T00:00:00Z" },
|
||||||
|
{ id: "st-2", title: "Do other thing", completed: false, createdAt: "2024-12-01T00:00:00Z" },
|
||||||
|
{ id: "st-3", title: "Done thing", completed: true, completedAt: "2025-01-15T00:00:00Z", createdAt: "2024-12-15T00:00:00Z" },
|
||||||
|
];
|
||||||
|
const result = resetSubtasks(subtasks);
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
for (const s of result) {
|
||||||
|
expect(s.completed).toBe(false);
|
||||||
|
expect(s.completedAt).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves other fields", () => {
|
||||||
|
const subtasks: Subtask[] = [
|
||||||
|
{ id: "st-1", title: "My task", completed: true, completedAt: "2025-01-01T00:00:00Z", createdAt: "2024-12-01T00:00:00Z" },
|
||||||
|
];
|
||||||
|
const result = resetSubtasks(subtasks);
|
||||||
|
expect(result[0].id).toBe("st-1");
|
||||||
|
expect(result[0].title).toBe("My task");
|
||||||
|
expect(result[0].createdAt).toBe("2024-12-01T00:00:00Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty array", () => {
|
||||||
|
expect(resetSubtasks([])).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── parseTaskIdentifier ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("parseTaskIdentifier", () => {
|
||||||
|
test("parses plain number", () => {
|
||||||
|
const result = parseTaskIdentifier("42");
|
||||||
|
expect(result).toEqual({ type: "number", value: 42 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses HQ- prefixed number", () => {
|
||||||
|
const result = parseTaskIdentifier("HQ-7");
|
||||||
|
expect(result).toEqual({ type: "number", value: 7 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses hq- prefixed number (case insensitive)", () => {
|
||||||
|
const result = parseTaskIdentifier("hq-15");
|
||||||
|
expect(result).toEqual({ type: "number", value: 15 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parses UUID", () => {
|
||||||
|
const uuid = "550e8400-e29b-41d4-a716-446655440000";
|
||||||
|
const result = parseTaskIdentifier(uuid);
|
||||||
|
expect(result).toEqual({ type: "uuid", value: uuid });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("treats non-numeric strings as UUID", () => {
|
||||||
|
const result = parseTaskIdentifier("abc123");
|
||||||
|
expect(result).toEqual({ type: "uuid", value: "abc123" });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("treats mixed number-string as UUID", () => {
|
||||||
|
const result = parseTaskIdentifier("12abc");
|
||||||
|
expect(result).toEqual({ type: "uuid", value: "12abc" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Validators ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("isValidTaskStatus", () => {
|
||||||
|
test("accepts valid statuses", () => {
|
||||||
|
expect(isValidTaskStatus("active")).toBe(true);
|
||||||
|
expect(isValidTaskStatus("queued")).toBe(true);
|
||||||
|
expect(isValidTaskStatus("blocked")).toBe(true);
|
||||||
|
expect(isValidTaskStatus("completed")).toBe(true);
|
||||||
|
expect(isValidTaskStatus("cancelled")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects invalid statuses", () => {
|
||||||
|
expect(isValidTaskStatus("done")).toBe(false);
|
||||||
|
expect(isValidTaskStatus("")).toBe(false);
|
||||||
|
expect(isValidTaskStatus("ACTIVE")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValidTaskPriority", () => {
|
||||||
|
test("accepts valid priorities", () => {
|
||||||
|
expect(isValidTaskPriority("critical")).toBe(true);
|
||||||
|
expect(isValidTaskPriority("high")).toBe(true);
|
||||||
|
expect(isValidTaskPriority("medium")).toBe(true);
|
||||||
|
expect(isValidTaskPriority("low")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects invalid priorities", () => {
|
||||||
|
expect(isValidTaskPriority("urgent")).toBe(false);
|
||||||
|
expect(isValidTaskPriority("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValidTaskSource", () => {
|
||||||
|
test("accepts valid sources", () => {
|
||||||
|
expect(isValidTaskSource("donovan")).toBe(true);
|
||||||
|
expect(isValidTaskSource("hammer")).toBe(true);
|
||||||
|
expect(isValidTaskSource("heartbeat")).toBe(true);
|
||||||
|
expect(isValidTaskSource("cron")).toBe(true);
|
||||||
|
expect(isValidTaskSource("other")).toBe(true);
|
||||||
|
expect(isValidTaskSource("david")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects invalid sources", () => {
|
||||||
|
expect(isValidTaskSource("system")).toBe(false);
|
||||||
|
expect(isValidTaskSource("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isValidRecurrenceFrequency", () => {
|
||||||
|
test("accepts valid frequencies", () => {
|
||||||
|
expect(isValidRecurrenceFrequency("daily")).toBe(true);
|
||||||
|
expect(isValidRecurrenceFrequency("weekly")).toBe(true);
|
||||||
|
expect(isValidRecurrenceFrequency("biweekly")).toBe(true);
|
||||||
|
expect(isValidRecurrenceFrequency("monthly")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("rejects invalid frequencies", () => {
|
||||||
|
expect(isValidRecurrenceFrequency("yearly")).toBe(false);
|
||||||
|
expect(isValidRecurrenceFrequency("")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── statusSortOrder ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("statusSortOrder", () => {
|
||||||
|
test("active sorts first", () => {
|
||||||
|
expect(statusSortOrder("active")).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cancelled sorts last among known statuses", () => {
|
||||||
|
expect(statusSortOrder("cancelled")).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("maintains correct ordering", () => {
|
||||||
|
const statuses = ["cancelled", "active", "blocked", "queued", "completed"];
|
||||||
|
const sorted = [...statuses].sort(
|
||||||
|
(a, b) => statusSortOrder(a) - statusSortOrder(b)
|
||||||
|
);
|
||||||
|
expect(sorted).toEqual([
|
||||||
|
"active",
|
||||||
|
"queued",
|
||||||
|
"blocked",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("unknown status gets highest sort value", () => {
|
||||||
|
expect(statusSortOrder("unknown")).toBe(5);
|
||||||
|
expect(statusSortOrder("unknown")).toBeGreaterThan(
|
||||||
|
statusSortOrder("cancelled")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
101
backend/src/lib/utils.ts
Normal file
101
backend/src/lib/utils.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* Pure utility functions extracted for testability.
|
||||||
|
* No database or external dependencies.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { RecurrenceFrequency, Subtask } from "../db/schema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the next due date for a recurring task based on frequency.
|
||||||
|
*/
|
||||||
|
export function computeNextDueDate(
|
||||||
|
frequency: RecurrenceFrequency,
|
||||||
|
fromDate?: Date | null
|
||||||
|
): Date {
|
||||||
|
const base =
|
||||||
|
fromDate && fromDate > new Date() ? new Date(fromDate) : new Date();
|
||||||
|
switch (frequency) {
|
||||||
|
case "daily":
|
||||||
|
base.setDate(base.getDate() + 1);
|
||||||
|
break;
|
||||||
|
case "weekly":
|
||||||
|
base.setDate(base.getDate() + 7);
|
||||||
|
break;
|
||||||
|
case "biweekly":
|
||||||
|
base.setDate(base.getDate() + 14);
|
||||||
|
break;
|
||||||
|
case "monthly":
|
||||||
|
base.setMonth(base.getMonth() + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset subtasks for a new recurrence instance (uncheck all).
|
||||||
|
*/
|
||||||
|
export function resetSubtasks(subtasks: Subtask[]): Subtask[] {
|
||||||
|
return subtasks.map((s) => ({
|
||||||
|
...s,
|
||||||
|
completed: false,
|
||||||
|
completedAt: undefined,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a task identifier - could be a UUID, a number, or "HQ-<number>".
|
||||||
|
* Returns { type: "number", value: number } or { type: "uuid", value: string }.
|
||||||
|
*/
|
||||||
|
export function parseTaskIdentifier(idOrNumber: string):
|
||||||
|
| { type: "number"; value: number }
|
||||||
|
| { type: "uuid"; value: string } {
|
||||||
|
const cleaned = idOrNumber.replace(/^HQ-/i, "");
|
||||||
|
const asNumber = parseInt(cleaned, 10);
|
||||||
|
if (!isNaN(asNumber) && String(asNumber) === cleaned) {
|
||||||
|
return { type: "number", value: asNumber };
|
||||||
|
}
|
||||||
|
return { type: "uuid", value: cleaned };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a status string is a valid task status.
|
||||||
|
*/
|
||||||
|
export function isValidTaskStatus(status: string): boolean {
|
||||||
|
return ["active", "queued", "blocked", "completed", "cancelled"].includes(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a priority string is a valid task priority.
|
||||||
|
*/
|
||||||
|
export function isValidTaskPriority(priority: string): boolean {
|
||||||
|
return ["critical", "high", "medium", "low"].includes(priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a source string is a valid task source.
|
||||||
|
*/
|
||||||
|
export function isValidTaskSource(source: string): boolean {
|
||||||
|
return ["donovan", "david", "hammer", "heartbeat", "cron", "other"].includes(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a recurrence frequency string.
|
||||||
|
*/
|
||||||
|
export function isValidRecurrenceFrequency(freq: string): boolean {
|
||||||
|
return ["daily", "weekly", "biweekly", "monthly"].includes(freq);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort status values by priority order (active first).
|
||||||
|
* Returns a numeric sort key.
|
||||||
|
*/
|
||||||
|
export function statusSortOrder(status: string): number {
|
||||||
|
const order: Record<string, number> = {
|
||||||
|
active: 0,
|
||||||
|
queued: 1,
|
||||||
|
blocked: 2,
|
||||||
|
completed: 3,
|
||||||
|
cancelled: 4,
|
||||||
|
};
|
||||||
|
return order[status] ?? 5;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { db } from "../db";
|
|||||||
import { tasks, type ProgressNote, type Subtask, type Recurrence, type RecurrenceFrequency } from "../db/schema";
|
import { tasks, type ProgressNote, type Subtask, type Recurrence, type RecurrenceFrequency } from "../db/schema";
|
||||||
import { eq, asc, desc, sql, inArray, or } from "drizzle-orm";
|
import { eq, asc, desc, sql, inArray, or } from "drizzle-orm";
|
||||||
import { auth } from "../lib/auth";
|
import { auth } from "../lib/auth";
|
||||||
|
import { computeNextDueDate, resetSubtasks, parseTaskIdentifier } from "../lib/utils";
|
||||||
|
|
||||||
const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token";
|
const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token";
|
||||||
const CLAWDBOT_HOOK_URL = process.env.CLAWDBOT_HOOK_URL || "https://hammer.donovankelly.xyz/hooks/agent";
|
const CLAWDBOT_HOOK_URL = process.env.CLAWDBOT_HOOK_URL || "https://hammer.donovankelly.xyz/hooks/agent";
|
||||||
@@ -41,26 +42,6 @@ async function notifyTaskActivated(task: { id: string; title: string; descriptio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute the next due date for a recurring task
|
|
||||||
function computeNextDueDate(frequency: RecurrenceFrequency, fromDate?: Date | null): Date {
|
|
||||||
const base = fromDate && fromDate > new Date() ? new Date(fromDate) : new Date();
|
|
||||||
switch (frequency) {
|
|
||||||
case "daily":
|
|
||||||
base.setDate(base.getDate() + 1);
|
|
||||||
break;
|
|
||||||
case "weekly":
|
|
||||||
base.setDate(base.getDate() + 7);
|
|
||||||
break;
|
|
||||||
case "biweekly":
|
|
||||||
base.setDate(base.getDate() + 14);
|
|
||||||
break;
|
|
||||||
case "monthly":
|
|
||||||
base.setMonth(base.getMonth() + 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the next instance of a recurring task
|
// Create the next instance of a recurring task
|
||||||
async function spawnNextRecurrence(completedTask: typeof tasks.$inferSelect) {
|
async function spawnNextRecurrence(completedTask: typeof tasks.$inferSelect) {
|
||||||
const recurrence = completedTask.recurrence as Recurrence | null;
|
const recurrence = completedTask.recurrence as Recurrence | null;
|
||||||
@@ -96,11 +77,7 @@ async function spawnNextRecurrence(completedTask: typeof tasks.$inferSelect) {
|
|||||||
estimatedHours: completedTask.estimatedHours,
|
estimatedHours: completedTask.estimatedHours,
|
||||||
tags: completedTask.tags,
|
tags: completedTask.tags,
|
||||||
recurrence: recurrence,
|
recurrence: recurrence,
|
||||||
subtasks: (completedTask.subtasks as Subtask[] || []).map(s => ({
|
subtasks: resetSubtasks(completedTask.subtasks as Subtask[] || []),
|
||||||
...s,
|
|
||||||
completed: false,
|
|
||||||
completedAt: undefined,
|
|
||||||
})),
|
|
||||||
progressNotes: [],
|
progressNotes: [],
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@@ -161,17 +138,12 @@ async function requireAdmin(request: Request, headers: Record<string, string | u
|
|||||||
|
|
||||||
// Resolve a task by UUID or sequential number (e.g. "5" or "HQ-5")
|
// Resolve a task by UUID or sequential number (e.g. "5" or "HQ-5")
|
||||||
async function resolveTask(idOrNumber: string) {
|
async function resolveTask(idOrNumber: string) {
|
||||||
// Strip "HQ-" prefix if present
|
const parsed = parseTaskIdentifier(idOrNumber);
|
||||||
const cleaned = idOrNumber.replace(/^HQ-/i, "");
|
|
||||||
const asNumber = parseInt(cleaned, 10);
|
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
if (!isNaN(asNumber) && String(asNumber) === cleaned) {
|
if (parsed.type === "number") {
|
||||||
// Lookup by task_number
|
result = await db.select().from(tasks).where(eq(tasks.taskNumber, parsed.value));
|
||||||
result = await db.select().from(tasks).where(eq(tasks.taskNumber, asNumber));
|
|
||||||
} else {
|
} else {
|
||||||
// Lookup by UUID
|
result = await db.select().from(tasks).where(eq(tasks.id, parsed.value));
|
||||||
result = await db.select().from(tasks).where(eq(tasks.id, cleaned));
|
|
||||||
}
|
}
|
||||||
return result[0] || null;
|
return result[0] || null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user