From 4aab66113aede02a74a84aa9b54b2102c29454bd Mon Sep 17 00:00:00 2001 From: Hermes Date: Tue, 23 Jun 2026 01:31:08 -0700 Subject: [PATCH] moved to root - api/ dir flattened --- api/store.go | 269 --------------------------------------------------- 1 file changed, 269 deletions(-) delete mode 100644 api/store.go diff --git a/api/store.go b/api/store.go deleted file mode 100644 index 7000048..0000000 --- a/api/store.go +++ /dev/null @@ -1,269 +0,0 @@ -package main - -import ( - "crypto/rand" - "database/sql" - "encoding/hex" - "fmt" - "time" -) - -// Store — the persistence layer. Postgres-only. -type Store struct { - db *sql.DB -} - -func NewStore(db *sql.DB) *Store { return &Store{db: db} } - -// newID — 8 bytes crypto/rand → 16 hex chars. Cheap and unique enough. -func newID() string { - b := make([]byte, 8) - _, _ = rand.Read(b) - return hex.EncodeToString(b) -} - -// Now — central time helper, UTC. -func Now() time.Time { return time.Now().UTC() } - -// ---------- Authors ---------- - -func (s *Store) GetAuthor(id string) (*Author, error) { - row := s.db.QueryRow( - `SELECT id, kind, display_name, COALESCE(avatar_url,''), created_at, updated_at - FROM authors WHERE id=$1`, id) - var a Author - if err := row.Scan(&a.ID, &a.Kind, &a.DisplayName, &a.AvatarURL, &a.CreatedAt, &a.UpdatedAt); err != nil { - return nil, err - } - return &a, nil -} - -func (s *Store) UpsertAuthor(a *Author) error { - _, err := s.db.Exec( - `INSERT INTO authors (id, kind, display_name, avatar_url, created_at, updated_at) - VALUES ($1,$2,$3,$4,$5,$6) - ON CONFLICT (id) DO UPDATE - SET kind=EXCLUDED.kind, - display_name=EXCLUDED.display_name, - avatar_url=EXCLUDED.avatar_url, - updated_at=NOW()`, - a.ID, a.Kind, a.DisplayName, a.AvatarURL, Now(), Now()) - return err -} - -// ---------- Channels ---------- - -func (s *Store) ListChannels() ([]Channel, error) { - rows, err := s.db.Query( - `SELECT slug, name, description, created_at FROM channels ORDER BY slug`) - if err != nil { - return nil, err - } - defer rows.Close() - var out []Channel - for rows.Next() { - var c Channel - if err := rows.Scan(&c.Slug, &c.Name, &c.Description, &c.CreatedAt); err != nil { - return nil, err - } - out = append(out, c) - } - return out, rows.Err() -} - -func (s *Store) UpsertChannel(c *Channel) error { - _, err := s.db.Exec( - `INSERT INTO channels (slug, name, description, created_at) - VALUES ($1,$2,$3,$4) - ON CONFLICT (slug) DO UPDATE SET name=EXCLUDED.name, description=EXCLUDED.description`, - c.Slug, c.Name, c.Description, Now()) - return err -} - -// ---------- Posts ---------- - -// CreatePost — inserts a post and links it to channels atomically. -func (s *Store) CreatePost(authorID, title, body string, channels []string) (*Post, error) { - tx, err := s.db.Begin() - if err != nil { - return nil, err - } - defer tx.Rollback() //nolint:errcheck - - p := &Post{ - ID: newID(), - AuthorID: authorID, - Title: title, - Body: body, - BumpedAt: Now(), - CreatedAt: Now(), - UpdatedAt: Now(), - Channels: channels, - } - _, err = tx.Exec( - `INSERT INTO posts (id, author_id, title, body, bumped_at, created_at, updated_at) - VALUES ($1,$2,$3,$4,$5,$6,$7)`, - p.ID, p.AuthorID, p.Title, p.Body, p.BumpedAt, p.CreatedAt, p.UpdatedAt) - if err != nil { - return nil, fmt.Errorf("insert post: %w", err) - } - for _, ch := range channels { - _, err = tx.Exec( - `INSERT INTO post_channels (post_id, channel, added_by) VALUES ($1,$2,$3) - ON CONFLICT DO NOTHING`, - p.ID, ch, authorID) - if err != nil { - return nil, fmt.Errorf("link channel %s: %w", ch, err) - } - } - if err := tx.Commit(); err != nil { - return nil, err - } - return p, nil -} - -// GetPost — fetch one post and all its channels. -func (s *Store) GetPost(id string) (*Post, error) { - row := s.db.QueryRow( - `SELECT id, author_id, title, body, bumped_at, created_at, updated_at, is_deleted, COALESCE(parent_post,'') - FROM posts WHERE id=$1`, id) - var p Post - if err := row.Scan(&p.ID, &p.AuthorID, &p.Title, &p.Body, &p.BumpedAt, &p.CreatedAt, &p.UpdatedAt, &p.IsDeleted, &p.ParentPost); err != nil { - return nil, err - } - rows, err := s.db.Query(`SELECT channel FROM post_channels WHERE post_id=$1 ORDER BY channel`, p.ID) - if err != nil { - return nil, err - } - defer rows.Close() - for rows.Next() { - var ch string - if err := rows.Scan(&ch); err != nil { - return nil, err - } - p.Channels = append(p.Channels, ch) - } - return &p, rows.Err() -} - -// ListPosts — newest-bumped first, optional channel filter. -func (s *Store) ListPosts(channel string, limit int) ([]Post, error) { - if limit <= 0 || limit > 100 { - limit = 20 - } - var ( - rows *sql.Rows - err error - ) - if channel == "" { - rows, err = s.db.Query( - `SELECT id, author_id, title, body, bumped_at, created_at, updated_at, is_deleted, COALESCE(parent_post,'') - FROM posts WHERE NOT is_deleted - ORDER BY bumped_at DESC LIMIT $1`, limit) - } else { - rows, err = s.db.Query( - `SELECT p.id, p.author_id, p.title, p.body, p.bumped_at, p.created_at, p.updated_at, p.is_deleted, COALESCE(p.parent_post,'') - FROM posts p - JOIN post_channels pc ON pc.post_id = p.id - WHERE NOT p.is_deleted AND pc.channel = $1 - ORDER BY p.bumped_at DESC LIMIT $2`, channel, limit) - } - if err != nil { - return nil, err - } - defer rows.Close() - var out []Post - for rows.Next() { - var p Post - if err := rows.Scan(&p.ID, &p.AuthorID, &p.Title, &p.Body, &p.BumpedAt, &p.CreatedAt, &p.UpdatedAt, &p.IsDeleted, &p.ParentPost); err != nil { - return nil, err - } - out = append(out, p) - } - return out, rows.Err() -} - -// ---------- Comments ---------- - -// AddComment — inserts a comment, computes path, bumps the post. -func (s *Store) AddComment(postID, parentID, authorID, body string) (*Comment, error) { - tx, err := s.db.Begin() - if err != nil { - return nil, err - } - defer tx.Rollback() //nolint:errcheck - - var path string - var depth int - if parentID == "" { - // top-level: depth 0, path is just the new id - depth = 0 - } else { - // child: inherit parent path, append new id - var ppath string - var pdepth int - if err := tx.QueryRow( - `SELECT path, depth FROM comments WHERE id=$1 AND post_id=$2`, - parentID, postID).Scan(&ppath, &pdepth); err != nil { - return nil, fmt.Errorf("parent comment not found: %w", err) - } - depth = pdepth + 1 - path = ppath - } - - id := newID() - if path == "" { - path = id - } else { - path = path + "." + id - } - - now := Now() - c := &Comment{ - ID: id, - PostID: postID, - ParentID: parentID, - Path: path, - Depth: depth, - AuthorID: authorID, - Body: body, - CreatedAt: now, - UpdatedAt: now, - } - _, err = tx.Exec( - `INSERT INTO comments (id, post_id, parent_id, path, depth, author_id, body, created_at, updated_at) - VALUES ($1,$2,NULLIF($3,''),$4,$5,$6,$7,$8,$9)`, - c.ID, c.PostID, c.ParentID, c.Path, c.Depth, c.AuthorID, c.Body, c.CreatedAt, c.UpdatedAt) - if err != nil { - return nil, fmt.Errorf("insert comment: %w", err) - } - // Bump the post — the bump. - if _, err := tx.Exec( - `UPDATE posts SET bumped_at=$1 WHERE id=$2`, now, postID); err != nil { - return nil, fmt.Errorf("bump post: %w", err) - } - if err := tx.Commit(); err != nil { - return nil, err - } - return c, nil -} - -// ListComments — all comments for a post, ordered by path. -func (s *Store) ListComments(postID string) ([]Comment, error) { - rows, err := s.db.Query( - `SELECT id, post_id, COALESCE(parent_id,''), path, depth, author_id, body, created_at, updated_at, is_deleted - FROM comments WHERE post_id=$1 ORDER BY path`, postID) - if err != nil { - return nil, err - } - defer rows.Close() - var out []Comment - for rows.Next() { - var c Comment - if err := rows.Scan(&c.ID, &c.PostID, &c.ParentID, &c.Path, &c.Depth, &c.AuthorID, &c.Body, &c.CreatedAt, &c.UpdatedAt, &c.IsDeleted); err != nil { - return nil, err - } - out = append(out, c) - } - return out, rows.Err() -}