This commit is contained in:
root
2026-09-14 20:51:40 +02:00
parent c3bf614901
commit 4195326938
4 changed files with 603 additions and 14 deletions
+261
View File
@@ -30,6 +30,14 @@ type Sublist struct {
CreatedAt string `json:"created_at"`
}
type RecurringItem struct {
ID int64 `json:"id"`
Name string `json:"name"`
SublistID *int64 `json:"sublist_id"`
Position float64 `json:"position"`
CreatedAt string `json:"created_at"`
}
type api struct {
db querier
hub *hub
@@ -79,9 +87,58 @@ func (a *api) getList(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, l)
}
// ensureGeneralSublist guarantees every item lands in a sublist: it makes
// sure the list has at least one (creating a "General" catch-all if it has
// none) and backfills any item/recurring row still missing a sublist_id
// (left over from before sublists were required) onto it.
func (a *api) ensureGeneralSublist(listID string) error {
var orphanItems, orphanRecurring int
if err := a.db.QueryRow("SELECT COUNT(*) FROM items WHERE list_id = ? AND sublist_id IS NULL", listID).Scan(&orphanItems); err != nil {
return err
}
if err := a.db.QueryRow("SELECT COUNT(*) FROM recurring_items WHERE list_id = ? AND sublist_id IS NULL", listID).Scan(&orphanRecurring); err != nil {
return err
}
var sublistCount int
if err := a.db.QueryRow("SELECT COUNT(*) FROM sublists WHERE list_id = ?", listID).Scan(&sublistCount); err != nil {
return err
}
if sublistCount > 0 && orphanItems == 0 && orphanRecurring == 0 {
return nil
}
var generalID int64
err := a.db.QueryRow("SELECT id FROM sublists WHERE list_id = ? AND name = ? ORDER BY id LIMIT 1", listID, "General").Scan(&generalID)
if err == sql.ErrNoRows {
generalID, err = a.db.insertReturningID("INSERT INTO sublists (list_id, name, color) VALUES (?, ?, ?)", listID, "General", "#9ca3af")
}
if err != nil {
return err
}
if orphanItems > 0 {
if _, err := a.db.Exec("UPDATE items SET sublist_id = ? WHERE list_id = ? AND sublist_id IS NULL", generalID, listID); err != nil {
return err
}
}
if orphanRecurring > 0 {
if _, err := a.db.Exec("UPDATE recurring_items SET sublist_id = ? WHERE list_id = ? AND sublist_id IS NULL", generalID, listID); err != nil {
return err
}
}
return nil
}
func (a *api) listSublists(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
if err := a.ensureGeneralSublist(listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
rows, err := a.db.Query("SELECT id, name, color, created_at FROM sublists WHERE list_id = ? ORDER BY id", listID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
@@ -150,6 +207,210 @@ func (a *api) createSublist(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, s)
}
func (a *api) updateSublist(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
id := r.PathValue("id")
var in struct {
Name *string `json:"name"`
Color *string `json:"color"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if in.Name != nil {
if *in.Name == "" {
writeError(w, http.StatusBadRequest, "name cannot be empty")
return
}
if _, err := a.db.Exec("UPDATE sublists SET name = ? WHERE id = ? AND list_id = ?", *in.Name, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
if in.Color != nil {
if *in.Color == "" {
writeError(w, http.StatusBadRequest, "color cannot be empty")
return
}
if _, err := a.db.Exec("UPDATE sublists SET color = ? WHERE id = ? AND list_id = ?", *in.Color, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
var s Sublist
err := a.db.QueryRow("SELECT id, name, color, created_at FROM sublists WHERE id = ? AND list_id = ?", id, listID).
Scan(&s.ID, &s.Name, &s.Color, &s.CreatedAt)
if err == sql.ErrNoRows {
writeError(w, http.StatusNotFound, "sublist not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
a.hub.broadcast(listID)
writeJSON(w, http.StatusOK, s)
}
func (a *api) deleteSublist(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
id := r.PathValue("id")
if _, err := a.db.Exec("DELETE FROM items WHERE sublist_id = ? AND list_id = ?", id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if _, err := a.db.Exec("DELETE FROM recurring_items WHERE sublist_id = ? AND list_id = ?", id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
res, err := a.db.Exec("DELETE FROM sublists WHERE id = ? AND list_id = ?", id, listID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if n, _ := res.RowsAffected(); n == 0 {
writeError(w, http.StatusNotFound, "sublist not found")
return
}
a.hub.broadcast(listID)
w.WriteHeader(http.StatusNoContent)
}
func (a *api) listRecurring(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
rows, err := a.db.Query("SELECT id, name, sublist_id, position, created_at FROM recurring_items WHERE list_id = ? ORDER BY position, id", listID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
recurring := []RecurringItem{}
for rows.Next() {
var ri RecurringItem
if err := rows.Scan(&ri.ID, &ri.Name, &ri.SublistID, &ri.Position, &ri.CreatedAt); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
recurring = append(recurring, ri)
}
writeJSON(w, http.StatusOK, recurring)
}
func (a *api) createRecurring(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
exists, err := a.listExists(listID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if !exists {
writeError(w, http.StatusNotFound, "list not found")
return
}
var in struct {
Name string `json:"name"`
SublistID *int64 `json:"sublist_id"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if in.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
var maxPosition sql.NullFloat64
if err := a.db.QueryRow("SELECT MAX(position) FROM recurring_items WHERE list_id = ?", listID).Scan(&maxPosition); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
position := maxPosition.Float64 + 1
id, err := a.db.insertReturningID("INSERT INTO recurring_items (list_id, name, sublist_id, position) VALUES (?, ?, ?, ?)", listID, in.Name, in.SublistID, position)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
var ri RecurringItem
err = a.db.QueryRow("SELECT id, name, sublist_id, position, created_at FROM recurring_items WHERE id = ?", id).
Scan(&ri.ID, &ri.Name, &ri.SublistID, &ri.Position, &ri.CreatedAt)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
a.hub.broadcast(listID)
writeJSON(w, http.StatusCreated, ri)
}
func (a *api) updateRecurring(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
id := r.PathValue("id")
var in struct {
Position *float64 `json:"position"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if in.Position != nil {
if _, err := a.db.Exec("UPDATE recurring_items SET position = ? WHERE id = ? AND list_id = ?", *in.Position, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
var ri RecurringItem
err := a.db.QueryRow("SELECT id, name, sublist_id, position, created_at FROM recurring_items WHERE id = ? AND list_id = ?", id, listID).
Scan(&ri.ID, &ri.Name, &ri.SublistID, &ri.Position, &ri.CreatedAt)
if err == sql.ErrNoRows {
writeError(w, http.StatusNotFound, "recurring item not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
a.hub.broadcast(listID)
writeJSON(w, http.StatusOK, ri)
}
func (a *api) deleteRecurring(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
id := r.PathValue("id")
res, err := a.db.Exec("DELETE FROM recurring_items WHERE id = ? AND list_id = ?", id, listID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if n, _ := res.RowsAffected(); n == 0 {
writeError(w, http.StatusNotFound, "recurring item not found")
return
}
a.hub.broadcast(listID)
w.WriteHeader(http.StatusNoContent)
}
func (a *api) listSocket(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")