From b1fb8284b1376df0714f497c56314649b6998881 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Thu, 26 Feb 2026 12:16:35 -0600 Subject: [PATCH] fix(api/tasks): apply status and sprintId query filters --- src/app/api/tasks/route.ts | 121 ++++++++++++++++++++++++++++++++----- 1 file changed, 105 insertions(+), 16 deletions(-) diff --git a/src/app/api/tasks/route.ts b/src/app/api/tasks/route.ts index bb9dabc..fe4dbab 100644 --- a/src/app/api/tasks/route.ts +++ b/src/app/api/tasks/route.ts @@ -86,11 +86,45 @@ const TASK_BASE_FIELDS = [ const TASK_DETAIL_ONLY_FIELDS = ["attachments"]; type TaskQueryScope = "all" | "active-sprint"; +type SprintQueryFilter = + | { mode: "none" } + | { mode: "current" } + | { mode: "backlog" } + | { mode: "id"; value: string }; function parseTaskQueryScope(raw: string | null): TaskQueryScope { return raw === "active-sprint" ? "active-sprint" : "all"; } +function parseCsvQuery(raw: string | null): string[] { + if (!raw) return []; + return raw + .split(",") + .map((token) => token.trim()) + .filter((token) => token.length > 0); +} + +function parseStatusQuery(raw: string | null): Task["status"][] { + const values = parseCsvQuery(raw).filter((token): token is Task["status"] => + TASK_STATUSES.includes(token as Task["status"]) + ); + return [...new Set(values)]; +} + +function parseSprintIdQuery(raw: string | null): SprintQueryFilter { + const value = toNonEmptyString(raw); + if (!value) return { mode: "none" }; + + const normalized = value.toLowerCase(); + if (normalized === "current") return { mode: "current" }; + if (normalized === "backlog" || normalized === "none" || normalized === "unsprinted") { + return { mode: "backlog" }; + } + if (isUuid(value)) return { mode: "id", value }; + + throw new HttpError(400, "sprintId must be a UUID, current, or backlog", { sprintId: raw }); +} + class HttpError extends Error { readonly status: number; readonly details?: Record; @@ -290,6 +324,8 @@ export async function GET(request: Request) { const url = new URL(request.url); const includeFullTaskData = url.searchParams.get("include") === "detail"; const scope = parseTaskQueryScope(url.searchParams.get("scope")); + const statusFilter = parseStatusQuery(url.searchParams.get("status")); + const sprintFilter = parseSprintIdQuery(url.searchParams.get("sprintId")); const requestedTaskId = toNonEmptyString(url.searchParams.get("taskId")); if (requestedTaskId && !isUuid(requestedTaskId)) { throw new HttpError(400, "taskId must be a UUID", { taskId: requestedTaskId }); @@ -315,35 +351,88 @@ export async function GET(request: Request) { if (usersError) throwQueryError("users", usersError); const mappedSprints = (sprints || []).map((row) => mapSprintRow(row as Record)); + const currentSprint = findCurrentSprint(mappedSprints); + const currentSprintId = currentSprint?.id; let taskRows: Record[] = []; if (requestedTaskId) { - const { data, error } = await supabase + let skipTaskQuery = false; + let query = supabase .from("tasks") .select(taskFieldSet.join(", ")) - .eq("id", requestedTaskId) - .maybeSingle(); - if (error) throwQueryError("tasks", error); - taskRows = data ? [data as unknown as Record] : []; + .eq("id", requestedTaskId); + + if (statusFilter.length > 0) { + query = query.in("status", statusFilter); + } + if (sprintFilter.mode === "backlog") { + query = query.is("sprint_id", null); + } else if (sprintFilter.mode === "id") { + query = query.eq("sprint_id", sprintFilter.value); + } else if (sprintFilter.mode === "current") { + if (!currentSprintId) { + skipTaskQuery = true; + } else { + query = query.eq("sprint_id", currentSprintId); + } + } + + if (!skipTaskQuery) { + const { data, error } = await query.maybeSingle(); + if (error) throwQueryError("tasks", error); + taskRows = data ? [data as unknown as Record] : []; + } } else if (scope === "all") { - const { data, error } = await supabase + let skipTaskQuery = false; + let query = supabase .from("tasks") .select(taskFieldSet.join(", ")) .order("updated_at", { ascending: false }); - if (error) throwQueryError("tasks", error); - taskRows = (data as unknown as Record[] | null) || []; - } else { - const currentSprint = findCurrentSprint(mappedSprints); - if (currentSprint?.id) { - const { data, error } = await supabase - .from("tasks") - .select(taskFieldSet.join(", ")) - .eq("sprint_id", currentSprint.id) - .order("updated_at", { ascending: false }); + if (statusFilter.length > 0) { + query = query.in("status", statusFilter); + } + if (sprintFilter.mode === "backlog") { + query = query.is("sprint_id", null); + } else if (sprintFilter.mode === "id") { + query = query.eq("sprint_id", sprintFilter.value); + } else if (sprintFilter.mode === "current") { + if (!currentSprintId) { + skipTaskQuery = true; + } else { + query = query.eq("sprint_id", currentSprintId); + } + } + + if (!skipTaskQuery) { + const { data, error } = await query; if (error) throwQueryError("tasks", error); taskRows = (data as unknown as Record[] | null) || []; } + } else { + if (currentSprintId) { + let skipTaskQuery = false; + let query = supabase + .from("tasks") + .select(taskFieldSet.join(", ")) + .eq("sprint_id", currentSprintId) + .order("updated_at", { ascending: false }); + + if (statusFilter.length > 0) { + query = query.in("status", statusFilter); + } + if (sprintFilter.mode === "backlog") { + skipTaskQuery = true; + } else if (sprintFilter.mode === "id" && sprintFilter.value !== currentSprintId) { + skipTaskQuery = true; + } + + if (!skipTaskQuery) { + const { data, error } = await query; + if (error) throwQueryError("tasks", error); + taskRows = (data as unknown as Record[] | null) || []; + } + } } const usersById = new Map();