summaryrefslogtreecommitdiffstats
path: root/internal/db/posts.go
diff options
context:
space:
mode:
authorivar <i@oiee.no>2026-04-04 16:10:52 +0200
committerivar <i@oiee.no>2026-04-04 16:10:52 +0200
commitf045a49e6feb6f448faea073821a9661f8b710a3 (patch)
tree8dcb53284d3f35b8ae2e4d1ca180c223b5d02784 /internal/db/posts.go
parentd239993644aae8c29d5fc37e8c6209850140807c (diff)
downloadnebbet.no-f045a49e6feb6f448faea073821a9661f8b710a3.tar.xz
nebbet.no-f045a49e6feb6f448faea073821a9661f8b710a3.zip
feat: add RenamePost with transactional rename and redirect creation
Implement RenamePost method that atomically: 1. Fetches the post record by old slug 2. Inserts a new record with the new slug 3. Deletes the old slug post 4. Collapses redirect chains (updates any redirects pointing to oldSlug to point to newSlug) 5. Creates a redirect from oldSlug to newSlug All operations are transactional (all-or-nothing). Includes two comprehensive tests: - TestRenamePost: basic rename with redirect creation and old slug deletion - TestRenamePost_CollapsesChain: verifies redirect chains are collapsed correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/db/posts.go')
-rw-r--r--internal/db/posts.go60
1 files changed, 60 insertions, 0 deletions
diff --git a/internal/db/posts.go b/internal/db/posts.go
index f6c23cc..d391c3e 100644
--- a/internal/db/posts.go
+++ b/internal/db/posts.go
@@ -137,3 +137,63 @@ func (m *MetaDB) CollapseRedirects(oldSlug, newSlug string) error {
)
return err
}
+
+// RenamePost atomically renames a post from oldSlug to newSlug,
+// updates any existing redirect chains, and adds a redirect from oldSlug to newSlug.
+func (m *MetaDB) RenamePost(oldSlug, newSlug string) error {
+ tx, err := m.db.Begin()
+ if err != nil {
+ return err
+ }
+ defer tx.Rollback()
+
+ // Fetch existing record
+ 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)
+
+ // Insert with new slug
+ 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
+ }
+
+ // Delete old record
+ if _, err = tx.Exec(`DELETE FROM posts WHERE slug = ?`, oldSlug); err != nil {
+ return err
+ }
+
+ // Collapse existing redirect chains: anything pointing to oldSlug now points to newSlug
+ if _, err = tx.Exec(
+ `UPDATE redirects SET to_slug = ? WHERE to_slug = ?`, newSlug, oldSlug,
+ ); err != nil {
+ return err
+ }
+
+ // Add redirect from oldSlug to newSlug
+ 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()
+}