fix(api/tasks): apply status and sprintId query filters

This commit is contained in:
OpenClaw Bot 2026-02-26 12:16:35 -06:00
parent be3476fd1a
commit b1fb8284b1

View File

@ -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<string, unknown>;
@ -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<string, unknown>));
const currentSprint = findCurrentSprint(mappedSprints);
const currentSprintId = currentSprint?.id;
let taskRows: Record<string, unknown>[] = [];
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<string, unknown>] : [];
.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<string, unknown>] : [];
}
} 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<string, unknown>[] | 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<string, unknown>[] | 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<string, unknown>[] | null) || [];
}
}
}
const usersById = new Map<string, UserProfile>();