summaryrefslogtreecommitdiffstats
path: root/internal/db/posts.go
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-07 00:23:24 +0200
committerivar <i@oiee.no>2026-04-07 00:23:24 +0200
commit85920b8c7a2696115d1f77c046f48f6f00d639f1 (patch)
tree14ed2043796eadd6ed5b0a95c55e38e48713d638 /internal/db/posts.go
downloadiblog-85920b8c7a2696115d1f77c046f48f6f00d639f1.tar.xz
iblog-85920b8c7a2696115d1f77c046f48f6f00d639f1.zip
Init
Diffstat (limited to 'internal/db/posts.go')
-rw-r--r--internal/db/posts.go248
1 files changed, 248 insertions, 0 deletions
diff --git a/internal/db/posts.go b/internal/db/posts.go
new file mode 100644
index 0000000..3c90b5f
--- /dev/null
+++ b/internal/db/posts.go
@@ -0,0 +1,248 @@
+package db
+
+import (
+ "database/sql"
+ "encoding/json"
+ "time"
+)
+
+type PostRecord struct {
+ Id string
+ Slug string
+ Title string
+ Date string
+ Tags []string
+ Draft bool
+ Blocks string // raw EditorJS JSON
+ UpdatedAt int64 // Unix microseconds
+}
+
+func (d *DB) UpsertPost(p PostRecord) error {
+ tags, _ := json.Marshal(p.Tags)
+ draft := 0
+ if p.Draft {
+ draft = 1
+ }
+ _, err := d.db.Exec(`
+ INSERT INTO posts (slug, title, date, tags, draft, blocks, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(slug) DO UPDATE SET
+ title = excluded.title,
+ date = excluded.date,
+ tags = excluded.tags,
+ draft = excluded.draft,
+ blocks = excluded.blocks,
+ updated_at = excluded.updated_at
+ `, p.Slug, p.Title, p.Date, string(tags), draft, p.Blocks, p.UpdatedAt)
+ return err
+}
+
+func (d *DB) GetPostBySlug(slug string) (*PostRecord, error) {
+ row := d.db.QueryRow(
+ `SELECT id, slug, title, date, tags, draft, blocks, updated_at FROM posts WHERE slug = ?`, slug)
+ var p PostRecord
+ var tagsJSON string
+ var draft int
+ if err := row.Scan(&p.Id, &p.Slug, &p.Title, &p.Date, &tagsJSON, &draft, &p.Blocks, &p.UpdatedAt); err != nil {
+ return nil, err
+ }
+ p.Draft = draft != 0
+ _ = json.Unmarshal([]byte(tagsJSON), &p.Tags)
+ if p.Tags == nil {
+ p.Tags = []string{}
+ }
+ return &p, nil
+}
+
+func (d *DB) GetPostById(id string) (*PostRecord, error) {
+ row := d.db.QueryRow(
+ `SELECT id, slug, title, date, tags, draft, blocks, updated_at FROM posts WHERE id = ?`, id)
+ var p PostRecord
+ var tagsJSON string
+ var draft int
+ if err := row.Scan(&p.Id, &p.Slug, &p.Title, &p.Date, &tagsJSON, &draft, &p.Blocks, &p.UpdatedAt); err != nil {
+ return nil, err
+ }
+ p.Draft = draft != 0
+ _ = json.Unmarshal([]byte(tagsJSON), &p.Tags)
+ if p.Tags == nil {
+ p.Tags = []string{}
+ }
+ return &p, nil
+}
+
+func (d *DB) ListPosts(includeDrafts bool) ([]PostRecord, error) {
+ query := `SELECT id, slug, title, date, tags, draft, blocks, updated_at FROM posts`
+ if !includeDrafts {
+ query += ` WHERE draft = 0`
+ }
+ query += ` ORDER BY date DESC, slug`
+ rows, err := d.db.Query(query)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ return scanPosts(rows)
+}
+
+func (d *DB) DeletePostBySlug(slug string) error {
+ _, err := d.db.Exec(`DELETE FROM posts WHERE slug = ?`, slug)
+ return err
+}
+
+func (d *DB) DeletePostById(id string) error {
+ _, err := d.db.Exec(`DELETE FROM posts WHERE id = ?`, id)
+ return err
+}
+
+func scanPosts(rows *sql.Rows) ([]PostRecord, error) {
+ var posts []PostRecord
+ for rows.Next() {
+ var p PostRecord
+ var tagsJSON string
+ var draft int
+ if err := rows.Scan(&p.Id, &p.Slug, &p.Title, &p.Date, &tagsJSON, &draft, &p.Blocks, &p.UpdatedAt); err != nil {
+ return nil, err
+ }
+ p.Draft = draft != 0
+ _ = json.Unmarshal([]byte(tagsJSON), &p.Tags)
+ if p.Tags == nil {
+ p.Tags = []string{}
+ }
+ posts = append(posts, p)
+ }
+ return posts, rows.Err()
+}
+
+func (p *PostRecord) GetPostRawPath() string {
+ return "/" + p.Slug
+}
+
+func (p *PostRecord) GetUpdatedTime() time.Time {
+ return time.UnixMicro(p.UpdatedAt).UTC()
+}
+
+func (d *DB) AddRedirect(fromSlug, toSlug string) error {
+ _, err := d.db.Exec(
+ `INSERT INTO redirects (from_slug, to_slug) VALUES (?, ?)
+ ON CONFLICT(from_slug) DO UPDATE SET to_slug = excluded.to_slug`,
+ fromSlug, toSlug,
+ )
+ return err
+}
+
+func (d *DB) GetRedirect(fromSlug string) (string, error) {
+ var toSlug string
+ err := d.db.QueryRow(
+ `SELECT to_slug FROM redirects WHERE from_slug = ?`, fromSlug,
+ ).Scan(&toSlug)
+ return toSlug, err
+}
+
+func (d *DB) CollapseRedirects(oldSlug, newSlug string) error {
+ _, err := d.db.Exec(
+ `UPDATE redirects SET to_slug = ? WHERE to_slug = ?`, newSlug, oldSlug,
+ )
+ return err
+}
+
+func (d *DB) RenamePost(oldSlug, newSlug string) error {
+ if oldSlug == newSlug {
+ return nil
+ }
+ tx, err := d.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ var p PostRecord
+ var tagsJSON string
+ var draft int
+ row := tx.QueryRow(
+ `SELECT slug, title, date, tags, draft, blocks, updated_at FROM posts WHERE slug = ?`, oldSlug)
+ if err := row.Scan(&p.Slug, &p.Title, &p.Date, &tagsJSON, &draft, &p.Blocks, &p.UpdatedAt); err != nil {
+ return err
+ }
+ p.Draft = draft != 0
+ _ = json.Unmarshal([]byte(tagsJSON), &p.Tags)
+
+ tags, _ := json.Marshal(p.Tags)
+ draftInt := 0
+ if p.Draft {
+ draftInt = 1
+ }
+ _, err = tx.Exec(`
+ INSERT INTO posts (slug, title, date, tags, draft, blocks, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
+ newSlug, p.Title, p.Date, string(tags), draftInt, p.Blocks, p.UpdatedAt,
+ )
+ if err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(`DELETE FROM posts WHERE slug = ?`, oldSlug); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(
+ `UPDATE redirects SET to_slug = ? WHERE to_slug = ?`, newSlug, oldSlug,
+ ); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(`
+ INSERT INTO redirects (from_slug, to_slug) VALUES (?, ?)
+ ON CONFLICT(from_slug) DO UPDATE SET to_slug = excluded.to_slug`,
+ oldSlug, newSlug,
+ ); err != nil {
+ return err
+ }
+
+ return tx.Commit()
+}
+
+func (d *DB) RenameAndUpsertPost(oldSlug string, p PostRecord) error {
+ if oldSlug == p.Slug {
+ return d.UpsertPost(p)
+ }
+ tx, err := d.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ tags, _ := json.Marshal(p.Tags)
+ draftInt := 0
+ if p.Draft {
+ draftInt = 1
+ }
+
+ if _, err = tx.Exec(`
+ INSERT INTO posts (slug, title, date, tags, draft, blocks, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
+ p.Slug, p.Title, p.Date, string(tags), draftInt, p.Blocks, p.UpdatedAt,
+ ); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(`DELETE FROM posts WHERE slug = ?`, oldSlug); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(
+ `UPDATE redirects SET to_slug = ? WHERE to_slug = ?`, p.Slug, oldSlug,
+ ); err != nil {
+ return err
+ }
+
+ if _, err = tx.Exec(`
+ INSERT INTO redirects (from_slug, to_slug) VALUES (?, ?)
+ ON CONFLICT(from_slug) DO UPDATE SET to_slug = excluded.to_slug`,
+ oldSlug, p.Slug,
+ ); err != nil {
+ return err
+ }
+
+ return tx.Commit()
+}