Files
skeps/backend/handlers.go
T
2026-09-13 15:08:12 +02:00

341 lines
9.2 KiB
Go

package main
import (
"database/sql"
"encoding/json"
"net/http"
"github.com/coder/websocket"
)
type List struct {
ID string `json:"id"`
CreatedAt string `json:"created_at"`
}
type Item struct {
ID int64 `json:"id"`
Name string `json:"name"`
Quantity int `json:"quantity"`
Done bool `json:"done"`
Position float64 `json:"position"`
SublistID *int64 `json:"sublist_id"`
CreatedAt string `json:"created_at"`
}
type Sublist struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
CreatedAt string `json:"created_at"`
}
type api struct {
db *sql.DB
hub *hub
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
func (a *api) listExists(id string) (bool, error) {
var exists bool
err := a.db.QueryRow("SELECT EXISTS(SELECT 1 FROM lists WHERE id = ?)", id).Scan(&exists)
return exists, err
}
func (a *api) createList(w http.ResponseWriter, r *http.Request) {
for attempt := 0; attempt < 5; attempt++ {
id := generateSlug()
if _, err := a.db.Exec("INSERT INTO lists (id) VALUES (?)", id); err == nil {
writeJSON(w, http.StatusCreated, List{ID: id})
return
}
}
writeError(w, http.StatusInternalServerError, "failed to create list")
}
func (a *api) getList(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("listId")
var l List
err := a.db.QueryRow("SELECT id, created_at FROM lists WHERE id = ?", id).Scan(&l.ID, &l.CreatedAt)
if err == sql.ErrNoRows {
writeError(w, http.StatusNotFound, "list not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, l)
}
func (a *api) listSublists(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
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())
return
}
defer rows.Close()
sublists := []Sublist{}
for rows.Next() {
var s Sublist
if err := rows.Scan(&s.ID, &s.Name, &s.Color, &s.CreatedAt); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
sublists = append(sublists, s)
}
writeJSON(w, http.StatusOK, sublists)
}
func (a *api) createSublist(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"`
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 == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if in.Color == "" {
writeError(w, http.StatusBadRequest, "color is required")
return
}
res, err := a.db.Exec("INSERT INTO sublists (list_id, name, color) VALUES (?, ?, ?)", listID, in.Name, in.Color)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
id, _ := res.LastInsertId()
var s Sublist
err = a.db.QueryRow("SELECT id, name, color, created_at FROM sublists WHERE id = ?", id).
Scan(&s.ID, &s.Name, &s.Color, &s.CreatedAt)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
a.hub.broadcast(listID)
writeJSON(w, http.StatusCreated, s)
}
func (a *api) listSocket(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
}
// No auth beyond the list id itself, same as the REST endpoints, so any origin may connect.
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{OriginPatterns: []string{"*"}})
if err != nil {
return
}
defer c.CloseNow()
a.hub.add(listID, c)
defer a.hub.remove(listID, c)
ctx := c.CloseRead(r.Context())
<-ctx.Done()
}
func (a *api) listItems(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
rows, err := a.db.Query("SELECT id, name, quantity, done, position, sublist_id, created_at FROM items WHERE list_id = ? ORDER BY position, id", listID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
defer rows.Close()
items := []Item{}
for rows.Next() {
var it Item
if err := rows.Scan(&it.ID, &it.Name, &it.Quantity, &it.Done, &it.Position, &it.SublistID, &it.CreatedAt); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
items = append(items, it)
}
writeJSON(w, http.StatusOK, items)
}
func (a *api) createItem(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"`
Quantity int `json:"quantity"`
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
}
if in.Quantity <= 0 {
in.Quantity = 1
}
var maxPosition sql.NullFloat64
if err := a.db.QueryRow("SELECT MAX(position) FROM items WHERE list_id = ?", listID).Scan(&maxPosition); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
position := maxPosition.Float64 + 1
res, err := a.db.Exec("INSERT INTO items (list_id, name, quantity, position, sublist_id) VALUES (?, ?, ?, ?, ?)", listID, in.Name, in.Quantity, position, in.SublistID)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
id, _ := res.LastInsertId()
var it Item
err = a.db.QueryRow("SELECT id, name, quantity, done, position, sublist_id, created_at FROM items WHERE id = ?", id).
Scan(&it.ID, &it.Name, &it.Quantity, &it.Done, &it.Position, &it.SublistID, &it.CreatedAt)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
a.hub.broadcast(listID)
writeJSON(w, http.StatusCreated, it)
}
func (a *api) updateItem(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
id := r.PathValue("id")
var in struct {
Name *string `json:"name"`
Quantity *int `json:"quantity"`
Done *bool `json:"done"`
Position *float64 `json:"position"`
SublistID *int64 `json:"sublist_id"`
MoveSublist bool `json:"move_sublist"`
}
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if in.Name != nil {
if _, err := a.db.Exec("UPDATE items SET name = ? WHERE id = ? AND list_id = ?", *in.Name, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
if in.Quantity != nil {
if _, err := a.db.Exec("UPDATE items SET quantity = ? WHERE id = ? AND list_id = ?", *in.Quantity, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
if in.Done != nil {
if _, err := a.db.Exec("UPDATE items SET done = ? WHERE id = ? AND list_id = ?", *in.Done, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
if in.Position != nil {
if _, err := a.db.Exec("UPDATE items SET position = ? WHERE id = ? AND list_id = ?", *in.Position, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
if in.MoveSublist {
if _, err := a.db.Exec("UPDATE items SET sublist_id = ? WHERE id = ? AND list_id = ?", in.SublistID, id, listID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
}
var it Item
err := a.db.QueryRow("SELECT id, name, quantity, done, position, sublist_id, created_at FROM items WHERE id = ? AND list_id = ?", id, listID).
Scan(&it.ID, &it.Name, &it.Quantity, &it.Done, &it.Position, &it.SublistID, &it.CreatedAt)
if err == sql.ErrNoRows {
writeError(w, http.StatusNotFound, "item not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
a.hub.broadcast(listID)
writeJSON(w, http.StatusOK, it)
}
func (a *api) deleteItem(w http.ResponseWriter, r *http.Request) {
listID := r.PathValue("listId")
id := r.PathValue("id")
res, err := a.db.Exec("DELETE FROM 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, "item not found")
return
}
a.hub.broadcast(listID)
w.WriteHeader(http.StatusNoContent)
}