Fix 413 error - send individual tasks instead of bulk
This commit is contained in:
parent
b3015e8bb8
commit
5b7efa2c18
@ -1,9 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getData, saveData, type DataStore, type Task } from "@/lib/server/taskDb";
|
||||
import { getServiceSupabase } from "@/lib/supabase/client";
|
||||
import { getAuthenticatedUser } from "@/lib/server/auth";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
interface Task {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
type: "idea" | "task" | "bug" | "research" | "plan";
|
||||
status: "open" | "todo" | "blocked" | "in-progress" | "review" | "validate" | "archived" | "canceled" | "done";
|
||||
priority: "low" | "medium" | "high" | "urgent";
|
||||
projectId: string;
|
||||
sprintId?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
createdById?: string;
|
||||
createdByName?: string;
|
||||
createdByAvatarUrl?: string;
|
||||
updatedById?: string;
|
||||
updatedByName?: string;
|
||||
updatedByAvatarUrl?: string;
|
||||
assigneeId?: string;
|
||||
assigneeName?: string;
|
||||
assigneeEmail?: string;
|
||||
assigneeAvatarUrl?: string;
|
||||
dueDate?: string;
|
||||
comments?: unknown[];
|
||||
tags?: string[];
|
||||
attachments?: unknown[];
|
||||
}
|
||||
|
||||
// GET - fetch all tasks, projects, and sprints
|
||||
export async function GET() {
|
||||
try {
|
||||
@ -11,15 +38,29 @@ export async function GET() {
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
const data = await getData();
|
||||
return NextResponse.json(data);
|
||||
|
||||
const supabase = getServiceSupabase();
|
||||
|
||||
const [{ data: projects }, { data: sprints }, { data: tasks }, { data: meta }] = await Promise.all([
|
||||
supabase.from("projects").select("*").order("created_at", { ascending: true }),
|
||||
supabase.from("sprints").select("*").order("start_date", { ascending: true }),
|
||||
supabase.from("tasks").select("*").order("created_at", { ascending: true }),
|
||||
supabase.from("meta").select("value").eq("key", "lastUpdated").maybeSingle(),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
projects: projects || [],
|
||||
sprints: sprints || [],
|
||||
tasks: tasks || [],
|
||||
lastUpdated: Number(meta?.value ?? Date.now()),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(">>> API GET: database error:", error);
|
||||
console.error(">>> API GET error:", error);
|
||||
return NextResponse.json({ error: "Failed to fetch data" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// POST - create or update tasks, projects, or sprints
|
||||
// POST - create or update a single task (lightweight)
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const user = await getAuthenticatedUser();
|
||||
@ -28,70 +69,83 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { task, tasks, projects, sprints } = body as {
|
||||
task?: Task;
|
||||
tasks?: Task[];
|
||||
projects?: DataStore["projects"];
|
||||
sprints?: DataStore["sprints"];
|
||||
const { task } = body as { task?: Task };
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: "Missing task" }, { status: 400 });
|
||||
}
|
||||
|
||||
const supabase = getServiceSupabase();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
// Check if task exists
|
||||
const { data: existing } = await supabase
|
||||
.from("tasks")
|
||||
.select("id")
|
||||
.eq("id", task.id)
|
||||
.maybeSingle();
|
||||
|
||||
const taskData = {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description || null,
|
||||
type: task.type || "task",
|
||||
status: task.status || "todo",
|
||||
priority: task.priority || "medium",
|
||||
project_id: task.projectId,
|
||||
sprint_id: task.sprintId || null,
|
||||
created_at: existing ? undefined : (task.createdAt || now),
|
||||
updated_at: now,
|
||||
created_by_id: existing ? undefined : (task.createdById || user.id),
|
||||
created_by_name: existing ? undefined : (task.createdByName || user.name),
|
||||
created_by_avatar_url: existing ? undefined : task.createdByAvatarUrl,
|
||||
updated_by_id: user.id,
|
||||
updated_by_name: user.name,
|
||||
updated_by_avatar_url: user.avatarUrl,
|
||||
assignee_id: task.assigneeId || null,
|
||||
assignee_name: task.assigneeName || null,
|
||||
assignee_email: task.assigneeEmail || null,
|
||||
assignee_avatar_url: task.assigneeAvatarUrl || null,
|
||||
due_date: task.dueDate || null,
|
||||
comments: task.comments || [],
|
||||
tags: task.tags || [],
|
||||
attachments: task.attachments || [],
|
||||
};
|
||||
|
||||
const data = await getData();
|
||||
|
||||
if (projects) data.projects = projects;
|
||||
if (sprints) data.sprints = sprints;
|
||||
|
||||
if (task) {
|
||||
const existingIndex = data.tasks.findIndex((t) => t.id === task.id);
|
||||
if (existingIndex >= 0) {
|
||||
const existingTask = data.tasks[existingIndex];
|
||||
data.tasks[existingIndex] = {
|
||||
...existingTask,
|
||||
...task,
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedById: user.id,
|
||||
updatedByName: user.name,
|
||||
updatedByAvatarUrl: user.avatarUrl,
|
||||
};
|
||||
} else {
|
||||
data.tasks.push({
|
||||
...task,
|
||||
id: task.id || Date.now().toString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
createdById: task.createdById || user.id,
|
||||
createdByName: task.createdByName || user.name,
|
||||
createdByAvatarUrl: task.createdByAvatarUrl || user.avatarUrl,
|
||||
updatedById: user.id,
|
||||
updatedByName: user.name,
|
||||
updatedByAvatarUrl: user.avatarUrl,
|
||||
assigneeId: task.assigneeId || user.id,
|
||||
assigneeName: task.assigneeName || user.name,
|
||||
assigneeEmail: task.assigneeEmail || user.email,
|
||||
});
|
||||
}
|
||||
let result;
|
||||
if (existing) {
|
||||
// Update existing
|
||||
const { data, error } = await supabase
|
||||
.from("tasks")
|
||||
.update(taskData)
|
||||
.eq("id", task.id)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
result = data;
|
||||
} else {
|
||||
// Insert new
|
||||
const { data, error } = await supabase
|
||||
.from("tasks")
|
||||
.insert(taskData)
|
||||
.select()
|
||||
.single();
|
||||
if (error) throw error;
|
||||
result = data;
|
||||
}
|
||||
|
||||
if (tasks && Array.isArray(tasks)) {
|
||||
data.tasks = tasks.map((entry) => ({
|
||||
...entry,
|
||||
createdById: entry.createdById || user.id,
|
||||
createdByName: entry.createdByName || user.name,
|
||||
createdByAvatarUrl: entry.createdByAvatarUrl || (entry.createdById === user.id ? user.avatarUrl : undefined),
|
||||
updatedById: entry.updatedById || user.id,
|
||||
updatedByName: entry.updatedByName || user.name,
|
||||
updatedByAvatarUrl: entry.updatedByAvatarUrl || (entry.updatedById === user.id ? user.avatarUrl : undefined),
|
||||
assigneeId: entry.assigneeId || undefined,
|
||||
assigneeName: entry.assigneeName || undefined,
|
||||
assigneeEmail: entry.assigneeEmail || undefined,
|
||||
assigneeAvatarUrl: undefined,
|
||||
}));
|
||||
}
|
||||
// Update lastUpdated
|
||||
await supabase.from("meta").upsert({
|
||||
key: "lastUpdated",
|
||||
value: String(Date.now()),
|
||||
updated_at: now,
|
||||
});
|
||||
|
||||
const saved = await saveData(data);
|
||||
return NextResponse.json({ success: true, data: saved });
|
||||
} catch (error) {
|
||||
console.error(">>> API POST: database error:", error);
|
||||
return NextResponse.json({ error: "Failed to save" }, { status: 500 });
|
||||
return NextResponse.json({ success: true, task: result });
|
||||
} catch (error: unknown) {
|
||||
console.error(">>> API POST error:", error);
|
||||
const message = error instanceof Error ? error.message : "Failed to save";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,12 +158,23 @@ export async function DELETE(request: Request) {
|
||||
}
|
||||
|
||||
const { id } = (await request.json()) as { id: string };
|
||||
const data = await getData();
|
||||
data.tasks = data.tasks.filter((t) => t.id !== id);
|
||||
await saveData(data);
|
||||
|
||||
const supabase = getServiceSupabase();
|
||||
const { error } = await supabase.from("tasks").delete().eq("id", id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Update lastUpdated
|
||||
await supabase.from("meta").upsert({
|
||||
key: "lastUpdated",
|
||||
value: String(Date.now()),
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(">>> API DELETE: database error:", error);
|
||||
return NextResponse.json({ error: "Failed to delete" }, { status: 500 });
|
||||
} catch (error: unknown) {
|
||||
console.error(">>> API DELETE error:", error);
|
||||
const message = error instanceof Error ? error.message : "Failed to delete";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
@ -552,27 +552,67 @@ const removeCommentFromThread = (comments: Comment[], targetId: string): Comment
|
||||
replies: removeCommentFromThread(normalizeComments(comment.replies), targetId),
|
||||
}))
|
||||
|
||||
// Helper to sync to server
|
||||
async function syncToServer(projects: Project[], tasks: Task[], sprints: Sprint[]) {
|
||||
console.log('>>> syncToServer: saving', tasks.length, 'tasks,', projects.length, 'projects,', sprints.length, 'sprints')
|
||||
const t2 = tasks.find(t => t.id === '2')
|
||||
if (t2) console.log('>>> syncToServer: task 2 sprintId:', t2.sprintId)
|
||||
// Helper to sync a single task to server (lightweight)
|
||||
async function syncTaskToServer(task: Task) {
|
||||
console.log('>>> syncTaskToServer: saving task', task.id, task.title)
|
||||
try {
|
||||
const body = JSON.stringify({ projects, tasks, sprints })
|
||||
console.log('>>> syncToServer: sending body with keys:', Object.keys(JSON.parse(body)))
|
||||
const res = await fetch('/api/tasks', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: body,
|
||||
body: JSON.stringify({ task }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const responseData = await res.json()
|
||||
console.log('>>> syncToServer: saved successfully, server now has', responseData.data?.tasks?.length, 'tasks')
|
||||
console.log('>>> syncTaskToServer: saved successfully')
|
||||
return true
|
||||
} else {
|
||||
console.error('>>> syncTaskToServer: failed with status', res.status)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('>>> syncTaskToServer: Failed to sync:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to delete a task from server
|
||||
async function deleteTaskFromServer(taskId: string) {
|
||||
console.log('>>> deleteTaskFromServer: deleting task', taskId)
|
||||
try {
|
||||
const res = await fetch('/api/tasks', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: taskId }),
|
||||
})
|
||||
if (res.ok) {
|
||||
console.log('>>> deleteTaskFromServer: deleted successfully')
|
||||
return true
|
||||
} else {
|
||||
console.error('>>> deleteTaskFromServer: failed with status', res.status)
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('>>> deleteTaskFromServer: Failed to delete:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy bulk sync (for projects/sprints only)
|
||||
async function syncToServer(projects: Project[], tasks: Task[], sprints: Sprint[]) {
|
||||
console.log('>>> syncToServer: legacy bulk sync - projects/sprints only')
|
||||
// Only sync projects and sprints in bulk, tasks are handled individually
|
||||
try {
|
||||
const res = await fetch('/api/projects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projects, sprints }),
|
||||
})
|
||||
if (res.ok) {
|
||||
console.log('>>> syncToServer: saved projects/sprints successfully')
|
||||
} else {
|
||||
console.error('>>> syncToServer: failed with status', res.status)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('>>> syncToServer: Failed to sync to server:', error)
|
||||
console.error('>>> syncToServer: Failed to sync:', error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -699,7 +739,8 @@ export const useTaskStore = create<TaskStore>()(
|
||||
}
|
||||
set((state) => {
|
||||
const newTasks = [...state.tasks, newTask]
|
||||
syncToServer(state.projects, newTasks, state.sprints)
|
||||
// Sync individual task to server (lightweight)
|
||||
syncTaskToServer(newTask)
|
||||
return { tasks: newTasks }
|
||||
})
|
||||
},
|
||||
@ -724,7 +765,10 @@ export const useTaskStore = create<TaskStore>()(
|
||||
)
|
||||
const updatedTask = newTasks.find(t => t.id === id)
|
||||
console.log('updateTask: updated task:', updatedTask)
|
||||
syncToServer(state.projects, newTasks, state.sprints)
|
||||
// Sync individual task to server (lightweight)
|
||||
if (updatedTask) {
|
||||
syncTaskToServer(updatedTask)
|
||||
}
|
||||
return { tasks: newTasks }
|
||||
})
|
||||
},
|
||||
@ -732,7 +776,8 @@ export const useTaskStore = create<TaskStore>()(
|
||||
deleteTask: (id) => {
|
||||
set((state) => {
|
||||
const newTasks = state.tasks.filter((t) => t.id !== id)
|
||||
syncToServer(state.projects, newTasks, state.sprints)
|
||||
// Delete individual task from server (lightweight)
|
||||
deleteTaskFromServer(id)
|
||||
return {
|
||||
tasks: newTasks,
|
||||
selectedTaskId: state.selectedTaskId === id ? null : state.selectedTaskId,
|
||||
@ -810,7 +855,10 @@ export const useTaskStore = create<TaskStore>()(
|
||||
}
|
||||
: t
|
||||
)
|
||||
syncToServer(state.projects, newTasks, state.sprints)
|
||||
const updatedTask = newTasks.find(t => t.id === taskId)
|
||||
if (updatedTask) {
|
||||
syncTaskToServer(updatedTask)
|
||||
}
|
||||
return { tasks: newTasks }
|
||||
})
|
||||
},
|
||||
@ -830,7 +878,10 @@ export const useTaskStore = create<TaskStore>()(
|
||||
}
|
||||
: t
|
||||
)
|
||||
syncToServer(state.projects, newTasks, state.sprints)
|
||||
const updatedTask = newTasks.find(t => t.id === taskId)
|
||||
if (updatedTask) {
|
||||
syncTaskToServer(updatedTask)
|
||||
}
|
||||
return { tasks: newTasks }
|
||||
})
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user