fix(api/tasks): apply status and sprintId query filters
This commit is contained in:
parent
be3476fd1a
commit
b1fb8284b1
@ -86,11 +86,45 @@ const TASK_BASE_FIELDS = [
|
|||||||
const TASK_DETAIL_ONLY_FIELDS = ["attachments"];
|
const TASK_DETAIL_ONLY_FIELDS = ["attachments"];
|
||||||
|
|
||||||
type TaskQueryScope = "all" | "active-sprint";
|
type TaskQueryScope = "all" | "active-sprint";
|
||||||
|
type SprintQueryFilter =
|
||||||
|
| { mode: "none" }
|
||||||
|
| { mode: "current" }
|
||||||
|
| { mode: "backlog" }
|
||||||
|
| { mode: "id"; value: string };
|
||||||
|
|
||||||
function parseTaskQueryScope(raw: string | null): TaskQueryScope {
|
function parseTaskQueryScope(raw: string | null): TaskQueryScope {
|
||||||
return raw === "active-sprint" ? "active-sprint" : "all";
|
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 {
|
class HttpError extends Error {
|
||||||
readonly status: number;
|
readonly status: number;
|
||||||
readonly details?: Record<string, unknown>;
|
readonly details?: Record<string, unknown>;
|
||||||
@ -290,6 +324,8 @@ export async function GET(request: Request) {
|
|||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const includeFullTaskData = url.searchParams.get("include") === "detail";
|
const includeFullTaskData = url.searchParams.get("include") === "detail";
|
||||||
const scope = parseTaskQueryScope(url.searchParams.get("scope"));
|
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"));
|
const requestedTaskId = toNonEmptyString(url.searchParams.get("taskId"));
|
||||||
if (requestedTaskId && !isUuid(requestedTaskId)) {
|
if (requestedTaskId && !isUuid(requestedTaskId)) {
|
||||||
throw new HttpError(400, "taskId must be a UUID", { taskId: 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);
|
if (usersError) throwQueryError("users", usersError);
|
||||||
|
|
||||||
const mappedSprints = (sprints || []).map((row) => mapSprintRow(row as Record<string, unknown>));
|
const mappedSprints = (sprints || []).map((row) => mapSprintRow(row as Record<string, unknown>));
|
||||||
|
const currentSprint = findCurrentSprint(mappedSprints);
|
||||||
|
const currentSprintId = currentSprint?.id;
|
||||||
|
|
||||||
let taskRows: Record<string, unknown>[] = [];
|
let taskRows: Record<string, unknown>[] = [];
|
||||||
if (requestedTaskId) {
|
if (requestedTaskId) {
|
||||||
const { data, error } = await supabase
|
let skipTaskQuery = false;
|
||||||
|
let query = supabase
|
||||||
.from("tasks")
|
.from("tasks")
|
||||||
.select(taskFieldSet.join(", "))
|
.select(taskFieldSet.join(", "))
|
||||||
.eq("id", requestedTaskId)
|
.eq("id", requestedTaskId);
|
||||||
.maybeSingle();
|
|
||||||
|
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);
|
if (error) throwQueryError("tasks", error);
|
||||||
taskRows = data ? [data as unknown as Record<string, unknown>] : [];
|
taskRows = data ? [data as unknown as Record<string, unknown>] : [];
|
||||||
|
}
|
||||||
} else if (scope === "all") {
|
} else if (scope === "all") {
|
||||||
const { data, error } = await supabase
|
let skipTaskQuery = false;
|
||||||
|
let query = supabase
|
||||||
.from("tasks")
|
.from("tasks")
|
||||||
.select(taskFieldSet.join(", "))
|
.select(taskFieldSet.join(", "))
|
||||||
.order("updated_at", { ascending: false });
|
.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) {
|
if (statusFilter.length > 0) {
|
||||||
const { data, error } = await supabase
|
query = query.in("status", statusFilter);
|
||||||
.from("tasks")
|
}
|
||||||
.select(taskFieldSet.join(", "))
|
if (sprintFilter.mode === "backlog") {
|
||||||
.eq("sprint_id", currentSprint.id)
|
query = query.is("sprint_id", null);
|
||||||
.order("updated_at", { ascending: false });
|
} 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);
|
if (error) throwQueryError("tasks", error);
|
||||||
taskRows = (data as unknown as Record<string, unknown>[] | null) || [];
|
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>();
|
const usersById = new Map<string, UserProfile>();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user