feat: add REST API for flashcards (/api/v1/flashcards/*)
This commit is contained in:
1 parent
f30d34d20a
commit
cd8e872491
3 files changed
+150
No files matched your search
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerAPI(mux *http.ServeMux, sqlite *sqliteStore, logger *zap.Logger) {
|
||||||
|
mux.HandleFunc("/api/v1/flashcards", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err := sqlite.listAllPublicIDs(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("list flashcards", zap.Error(err))
|
||||||
|
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
urls := make([]string, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
urls = append(urls, fmt.Sprintf("/api/v1/flashcards/%d", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string][]string{"flashcards": urls})
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/v1/flashcards/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
segments := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/flashcards/"), "/")
|
||||||
|
if len(segments) == 0 || segments[0] == "" {
|
||||||
|
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
publicID, err := strconv.ParseInt(segments[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid id"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(segments) >= 2 && segments[1] != "" {
|
||||||
|
version, err := strconv.Atoi(segments[1])
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"invalid version"}`, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e, err := sqlite.getByPublicIDAndVersion(r.Context(), publicID, version)
|
||||||
|
writeEntry(w, e, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e, err := sqlite.getLatestByPublicID(r.Context(), publicID)
|
||||||
|
writeEntry(w, e, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeEntry(w http.ResponseWriter, e *entry, err error) {
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if e == nil {
|
||||||
|
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(e)
|
||||||
|
}
|
||||||
@@ -70,6 +70,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
mux.Handle("/mcp", authMiddleware(token, streamableServer))
|
mux.Handle("/mcp", authMiddleware(token, streamableServer))
|
||||||
|
registerAPI(mux, sqliteStore, logger)
|
||||||
|
|
||||||
httpServer := &http.Server{
|
httpServer := &http.Server{
|
||||||
Addr: ":" + port,
|
Addr: ":" + port,
|
||||||
|
|||||||
@@ -166,6 +166,73 @@ func (s *sqliteStore) getLatestByPublicID(ctx context.Context, publicID int64) (
|
|||||||
return &e, nil
|
return &e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *sqliteStore) listAllPublicIDs(ctx context.Context) ([]int64, error) {
|
||||||
|
rows, err := s.db.QueryContext(ctx,
|
||||||
|
`SELECT f.public_id FROM flashcard f
|
||||||
|
WHERE f.version = (SELECT MAX(f2.version) FROM flashcard f2 WHERE f2.public_id = f.public_id)
|
||||||
|
AND f.delete_timestamp IS NULL
|
||||||
|
AND f.public_id != 0
|
||||||
|
ORDER BY f.public_id`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list ids: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var ids []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, fmt.Errorf("scan id: %w", err)
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("rows: %w", err)
|
||||||
|
}
|
||||||
|
return ids, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *sqliteStore) getByPublicIDAndVersion(ctx context.Context, publicID int64, version int) (*entry, error) {
|
||||||
|
row := s.db.QueryRowContext(ctx,
|
||||||
|
`SELECT _id, public_id, version, note, repos, host, tags, "user", context, delete_timestamp, created
|
||||||
|
FROM flashcard WHERE public_id = ? AND version = ?`, publicID, version,
|
||||||
|
)
|
||||||
|
|
||||||
|
var e entry
|
||||||
|
var reposJSON, tagsJSON sql.NullString
|
||||||
|
var host, user, context_, deleteTimestamp sql.NullString
|
||||||
|
|
||||||
|
err := row.Scan(&e.ID, &e.PublicID, &e.Version, &e.Note, &reposJSON, &host, &tagsJSON, &user, &context_, &deleteTimestamp, &e.Created)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reposJSON.Valid {
|
||||||
|
e.Repos = unmarshalStringSlice(reposJSON.String)
|
||||||
|
}
|
||||||
|
if tagsJSON.Valid {
|
||||||
|
e.Tags = unmarshalStringSlice(tagsJSON.String)
|
||||||
|
}
|
||||||
|
if host.Valid {
|
||||||
|
e.Host = host.String
|
||||||
|
}
|
||||||
|
if user.Valid {
|
||||||
|
e.User = user.String
|
||||||
|
}
|
||||||
|
if context_.Valid {
|
||||||
|
e.Context = context_.String
|
||||||
|
}
|
||||||
|
if deleteTimestamp.Valid {
|
||||||
|
e.DeleteTimestamp = deleteTimestamp.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return &e, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sqliteStore) tombstone(ctx context.Context, publicID int64, version int) (int64, error) {
|
func (s *sqliteStore) tombstone(ctx context.Context, publicID int64, version int) (int64, error) {
|
||||||
result, err := s.db.ExecContext(ctx,
|
result, err := s.db.ExecContext(ctx,
|
||||||
`INSERT INTO flashcard (public_id, version, note, delete_timestamp)
|
`INSERT INTO flashcard (public_id, version, note, delete_timestamp)
|
||||||
|
|||||||
Reference in new issue
Block a user