vibes
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user