API
This commit is contained in:
140
app/routers/steps.py
Normal file
140
app/routers/steps.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.middleware.auth import get_current_user_id
|
||||
from app.models import StepOut, StepUpdate
|
||||
from app.services.db import get_pool
|
||||
|
||||
router = APIRouter(prefix="", tags=["steps"])
|
||||
|
||||
STEP_COLUMNS = "s.id, s.task_id, s.sort_order, s.title, s.description, s.estimated_minutes, s.status, s.checkpoint_note, s.last_checked_at, s.completed_at, s.created_at"
|
||||
|
||||
|
||||
class CreateStepRequest(BaseModel):
|
||||
title: str
|
||||
description: str | None = None
|
||||
estimated_minutes: int | None = None
|
||||
|
||||
|
||||
@router.post("/tasks/{task_id}/steps", response_model=StepOut)
|
||||
async def create_step(task_id: UUID, req: CreateStepRequest, user_id: str = Depends(get_current_user_id)):
|
||||
pool = await get_pool()
|
||||
|
||||
task = await pool.fetchrow("SELECT id FROM tasks WHERE id = $1 AND user_id = $2::uuid", task_id, user_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
# Place new step at the end
|
||||
max_order = await pool.fetchval("SELECT COALESCE(MAX(sort_order), 0) FROM steps WHERE task_id = $1", task_id)
|
||||
|
||||
row = await pool.fetchrow(
|
||||
"""INSERT INTO steps (task_id, sort_order, title, description, estimated_minutes)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, task_id, sort_order, title, description, estimated_minutes,
|
||||
status, checkpoint_note, last_checked_at, completed_at, created_at""",
|
||||
task_id,
|
||||
max_order + 1,
|
||||
req.title,
|
||||
req.description,
|
||||
req.estimated_minutes,
|
||||
)
|
||||
return StepOut(**dict(row))
|
||||
|
||||
|
||||
@router.get("/tasks/{task_id}/steps", response_model=list[StepOut])
|
||||
async def list_steps(task_id: UUID, user_id: str = Depends(get_current_user_id)):
|
||||
pool = await get_pool()
|
||||
|
||||
# Verify task belongs to user
|
||||
task = await pool.fetchrow("SELECT id FROM tasks WHERE id = $1 AND user_id = $2::uuid", task_id, user_id)
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
|
||||
rows = await pool.fetch(
|
||||
f"SELECT {STEP_COLUMNS} FROM steps s WHERE s.task_id = $1 ORDER BY s.sort_order",
|
||||
task_id,
|
||||
)
|
||||
return [StepOut(**dict(r)) for r in rows]
|
||||
|
||||
|
||||
@router.patch("/steps/{step_id}", response_model=StepOut)
|
||||
async def update_step(step_id: UUID, req: StepUpdate, user_id: str = Depends(get_current_user_id)):
|
||||
pool = await get_pool()
|
||||
|
||||
# Verify step belongs to user's task
|
||||
step = await pool.fetchrow(
|
||||
"""SELECT s.id FROM steps s
|
||||
JOIN tasks t ON s.task_id = t.id
|
||||
WHERE s.id = $1 AND t.user_id = $2::uuid""",
|
||||
step_id,
|
||||
user_id,
|
||||
)
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail="Step not found")
|
||||
|
||||
fields = []
|
||||
values = []
|
||||
idx = 2 # $1 = step_id
|
||||
|
||||
update_data = req.model_dump(exclude_unset=True)
|
||||
for key, val in update_data.items():
|
||||
fields.append(f"{key} = ${idx}")
|
||||
values.append(val)
|
||||
idx += 1
|
||||
|
||||
if not fields:
|
||||
raise HTTPException(status_code=400, detail="No fields to update")
|
||||
|
||||
set_clause = ", ".join(fields)
|
||||
row = await pool.fetchrow(
|
||||
f"""UPDATE steps SET {set_clause}
|
||||
WHERE id = $1
|
||||
RETURNING id, task_id, sort_order, title, description, estimated_minutes,
|
||||
status, checkpoint_note, last_checked_at, completed_at, created_at""",
|
||||
step_id,
|
||||
*values,
|
||||
)
|
||||
return StepOut(**dict(row))
|
||||
|
||||
|
||||
@router.post("/steps/{step_id}/complete", response_model=StepOut)
|
||||
async def complete_step(step_id: UUID, user_id: str = Depends(get_current_user_id)):
|
||||
pool = await get_pool()
|
||||
|
||||
step = await pool.fetchrow(
|
||||
"""SELECT s.id FROM steps s
|
||||
JOIN tasks t ON s.task_id = t.id
|
||||
WHERE s.id = $1 AND t.user_id = $2::uuid""",
|
||||
step_id,
|
||||
user_id,
|
||||
)
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail="Step not found")
|
||||
|
||||
row = await pool.fetchrow(
|
||||
"""UPDATE steps SET status = 'done', completed_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, task_id, sort_order, title, description, estimated_minutes,
|
||||
status, checkpoint_note, last_checked_at, completed_at, created_at""",
|
||||
step_id,
|
||||
)
|
||||
return StepOut(**dict(row))
|
||||
|
||||
|
||||
@router.delete("/steps/{step_id}", status_code=204)
|
||||
async def delete_step(step_id: UUID, user_id: str = Depends(get_current_user_id)):
|
||||
pool = await get_pool()
|
||||
|
||||
step = await pool.fetchrow(
|
||||
"""SELECT s.id FROM steps s
|
||||
JOIN tasks t ON s.task_id = t.id
|
||||
WHERE s.id = $1 AND t.user_id = $2::uuid""",
|
||||
step_id,
|
||||
user_id,
|
||||
)
|
||||
if not step:
|
||||
raise HTTPException(status_code=404, detail="Step not found")
|
||||
|
||||
await pool.execute("DELETE FROM steps WHERE id = $1", step_id)
|
||||
Reference in New Issue
Block a user