package main import ( "database/sql" "encoding/json" "log" "net/http" "os" "github.com/gorilla/mux" _ "github.com/mattn/go-sqlite3" ) type Note struct { ID string `json:"id"` X float64 `json:"x"` Y float64 `json:"y"` Content string `json:"content"` } type Space struct { ID string `json:"id"` Notes []Note `json:"notes"` } var db *sql.DB func main() { dbPath := os.Getenv("DB_PATH") if dbPath == "" { dbPath = "./palace.db" } var err error db, err = sql.Open("sqlite3", dbPath) if err != nil { log.Fatal(err) } defer db.Close() if err := initDB(); err != nil { log.Fatal(err) } r := mux.NewRouter() r.HandleFunc("/api/spaces", getSpaces).Methods("GET") r.HandleFunc("/api/spaces/{id}", getSpace).Methods("GET") r.HandleFunc("/api/spaces/{id}", saveSpace).Methods("PUT") r.HandleFunc("/api/spaces/{id}", deleteSpace).Methods("DELETE") r.HandleFunc("/api/spaces/{id}/notes", addNote).Methods("POST") r.HandleFunc("/api/spaces/{spaceId}/notes/{noteId}", updateNote).Methods("PATCH") r.HandleFunc("/api/spaces/{spaceId}/notes/{noteId}", deleteNote).Methods("DELETE") r.Use(corsMiddleware) port := os.Getenv("PORT") if port == "" { port = "3001" } log.Printf("Server starting on port %s", port) log.Fatal(http.ListenAndServe(":"+port, r)) } func initDB() error { query := ` CREATE TABLE IF NOT EXISTS spaces ( id TEXT PRIMARY KEY, notes TEXT DEFAULT '[]' );` _, err := db.Exec(query) return err } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) } func getSpaces(w http.ResponseWriter, r *http.Request) { rows, err := db.Query("SELECT id FROM spaces") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer rows.Close() var spaces []Space for rows.Next() { var id string if err := rows.Scan(&id); err != nil { continue } spaces = append(spaces, Space{ID: id, Notes: []Note{}}) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(spaces) } func getSpace(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] var notesJSON string err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", id).Scan(¬esJSON) if err == sql.ErrNoRows { http.Error(w, "Space not found", http.StatusNotFound) return } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var notes []Note if err := json.Unmarshal([]byte(notesJSON), ¬es); err != nil { notes = []Note{} } space := Space{ID: id, Notes: notes} w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(space) } func saveSpace(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] var space Space if err := json.NewDecoder(r.Body).Decode(&space); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } notesJSON, _ := json.Marshal(space.Notes) _, err := db.Exec("INSERT OR REPLACE INTO spaces (id, notes) VALUES (?, ?)", id, string(notesJSON)) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func deleteSpace(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] _, err := db.Exec("DELETE FROM spaces WHERE id = ?", id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func addNote(w http.ResponseWriter, r *http.Request) { spaceId := mux.Vars(r)["id"] var note Note if err := json.NewDecoder(r.Body).Decode(¬e); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } var notesJSON string err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(¬esJSON) if err != nil { http.Error(w, "Space not found", http.StatusNotFound) return } var notes []Note json.Unmarshal([]byte(notesJSON), ¬es) notes = append(notes, note) newNotesJSON, _ := json.Marshal(notes) _, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func updateNote(w http.ResponseWriter, r *http.Request) { spaceId := mux.Vars(r)["spaceId"] noteId := mux.Vars(r)["noteId"] var updates Note if err := json.NewDecoder(r.Body).Decode(&updates); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } var notesJSON string err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(¬esJSON) if err != nil { http.Error(w, "Space not found", http.StatusNotFound) return } var notes []Note json.Unmarshal([]byte(notesJSON), ¬es) for i, note := range notes { if note.ID == noteId { if updates.X != 0 { notes[i].X = updates.X } if updates.Y != 0 { notes[i].Y = updates.Y } if updates.Content != "" { notes[i].Content = updates.Content } break } } newNotesJSON, _ := json.Marshal(notes) _, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) } func deleteNote(w http.ResponseWriter, r *http.Request) { spaceId := mux.Vars(r)["spaceId"] noteId := mux.Vars(r)["noteId"] var notesJSON string err := db.QueryRow("SELECT notes FROM spaces WHERE id = ?", spaceId).Scan(¬esJSON) if err != nil { http.Error(w, "Space not found", http.StatusNotFound) return } var notes []Note json.Unmarshal([]byte(notesJSON), ¬es) for i, note := range notes { if note.ID == noteId { notes = append(notes[:i], notes[i+1:]...) break } } newNotesJSON, _ := json.Marshal(notes) _, err = db.Exec("UPDATE spaces SET notes = ? WHERE id = ?", string(newNotesJSON), spaceId) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }