163 lines
4.3 KiB
TypeScript
163 lines
4.3 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import { all, get, run } from "@/lib/db";
|
|
import type { TaskPriority, TaskRecord, TaskStatus } from "@/lib/types";
|
|
|
|
const VALID_STATUSES: TaskStatus[] = [
|
|
"Backlog",
|
|
"Todo",
|
|
"In Progress",
|
|
"Review",
|
|
"Done",
|
|
];
|
|
|
|
const VALID_PRIORITIES: TaskPriority[] = ["Low", "Medium", "High", "Critical"];
|
|
const WIKI_DIR = process.env.WIKI_DIR || path.join(process.cwd(), "wiki");
|
|
|
|
fs.mkdirSync(WIKI_DIR, { recursive: true });
|
|
|
|
type DatabaseTaskRow = Omit<TaskRecord, "tags"> & { tags: string };
|
|
|
|
export function normalizeTask(row: DatabaseTaskRow): TaskRecord {
|
|
let tags: string[] = [];
|
|
try {
|
|
tags = JSON.parse(row.tags || "[]");
|
|
} catch {
|
|
tags = [];
|
|
}
|
|
|
|
return {
|
|
...row,
|
|
tags,
|
|
};
|
|
}
|
|
|
|
export async function listTasks() {
|
|
const rows = await all<DatabaseTaskRow>("SELECT * FROM tasks ORDER BY id DESC");
|
|
return rows.map(normalizeTask);
|
|
}
|
|
|
|
export async function findTask(id: number) {
|
|
const row = await get<DatabaseTaskRow>("SELECT * FROM tasks WHERE id = ?", [id]);
|
|
return row ? normalizeTask(row) : null;
|
|
}
|
|
|
|
export function validateTaskPayload(
|
|
payload: Partial<TaskRecord> & { tags?: unknown },
|
|
partial = false,
|
|
) {
|
|
const errors: string[] = [];
|
|
|
|
if (!partial || payload.title !== undefined) {
|
|
if (typeof payload.title !== "string" || payload.title.trim().length === 0) {
|
|
errors.push("title is required");
|
|
}
|
|
}
|
|
|
|
if (payload.status !== undefined && !VALID_STATUSES.includes(payload.status)) {
|
|
errors.push(`status must be one of: ${VALID_STATUSES.join(", ")}`);
|
|
}
|
|
|
|
if (payload.priority !== undefined && !VALID_PRIORITIES.includes(payload.priority)) {
|
|
errors.push(`priority must be one of: ${VALID_PRIORITIES.join(", ")}`);
|
|
}
|
|
|
|
if (payload.tags !== undefined && !Array.isArray(payload.tags)) {
|
|
errors.push("tags must be an array of strings");
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
function buildWikiMarkdown(task: TaskRecord) {
|
|
const renderedTags = task.tags.length ? task.tags.join(", ") : "None";
|
|
return `# ${task.title}
|
|
|
|
- Task ID: ${task.id}
|
|
- Assignee: ${task.assignee || "Unassigned"}
|
|
- Priority: ${task.priority}
|
|
- Status: ${task.status}
|
|
- Tags: ${renderedTags}
|
|
- Created: ${task.created_at}
|
|
- Completed: ${task.completed_at || new Date().toISOString()}
|
|
|
|
## Description
|
|
|
|
${task.description || "No description provided."}
|
|
`;
|
|
}
|
|
|
|
async function writeWikiForTask(task: TaskRecord) {
|
|
const safeTitle = task.title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 80);
|
|
const fileName = `${new Date().toISOString().slice(0, 10)}-task-${task.id}-${safeTitle || `task-${task.id}`}.md`;
|
|
fs.writeFileSync(path.join(WIKI_DIR, fileName), buildWikiMarkdown(task), "utf8");
|
|
}
|
|
|
|
export async function createTask(input: Partial<TaskRecord>) {
|
|
const result = await run(
|
|
`INSERT INTO tasks (title, description, assignee, priority, status, tags)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[
|
|
input.title?.trim() || "",
|
|
input.description || "",
|
|
input.assignee || "",
|
|
input.priority || "Medium",
|
|
input.status || "Backlog",
|
|
JSON.stringify(Array.isArray(input.tags) ? input.tags.filter((tag) => typeof tag === "string") : []),
|
|
],
|
|
);
|
|
|
|
const task = await findTask(result.lastID);
|
|
if (!task) {
|
|
throw new Error("failed_to_fetch_created_task");
|
|
}
|
|
|
|
return task;
|
|
}
|
|
|
|
export async function updateTask(id: number, input: Partial<TaskRecord>) {
|
|
const existing = await findTask(id);
|
|
if (!existing) {
|
|
return null;
|
|
}
|
|
|
|
const nextStatus = input.status ?? existing.status;
|
|
const completedAt =
|
|
nextStatus === "Done"
|
|
? existing.completed_at || new Date().toISOString()
|
|
: null;
|
|
|
|
await run(
|
|
`UPDATE tasks
|
|
SET title = ?, description = ?, assignee = ?, priority = ?, status = ?, tags = ?,
|
|
completed_at = ?, updated_at = datetime('now')
|
|
WHERE id = ?`,
|
|
[
|
|
input.title?.trim() || existing.title,
|
|
input.description ?? existing.description,
|
|
input.assignee ?? existing.assignee,
|
|
input.priority ?? existing.priority,
|
|
nextStatus,
|
|
JSON.stringify(input.tags ?? existing.tags),
|
|
completedAt,
|
|
id,
|
|
],
|
|
);
|
|
|
|
const updated = await findTask(id);
|
|
if (!updated) {
|
|
throw new Error("failed_to_fetch_updated_task");
|
|
}
|
|
|
|
if (nextStatus === "Done" && existing.status !== "Done") {
|
|
await writeWikiForTask(updated);
|
|
}
|
|
|
|
return updated;
|
|
}
|