Single-binary service: push raw markdown via REST/gRPC API, read rendered HTML through mobile-friendly web UI. MCIAS auth on all endpoints, SQLite storage, goldmark rendering with GFM and syntax highlighting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
122 lines
2.9 KiB
Go
122 lines
2.9 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// ErrNotFound is returned when a document does not exist.
|
|
var ErrNotFound = errors.New("db: not found")
|
|
|
|
// Document represents a queued document.
|
|
type Document struct {
|
|
ID int64 `json:"id"`
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Body string `json:"body"`
|
|
PushedBy string `json:"pushed_by"`
|
|
PushedAt string `json:"pushed_at"`
|
|
Read bool `json:"read"`
|
|
}
|
|
|
|
// ListDocuments returns all documents ordered by most recently pushed.
|
|
func (d *DB) ListDocuments() ([]Document, error) {
|
|
rows, err := d.Query(`SELECT id, slug, title, body, pushed_by, pushed_at, read FROM documents ORDER BY pushed_at DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var docs []Document
|
|
for rows.Next() {
|
|
var doc Document
|
|
if err := rows.Scan(&doc.ID, &doc.Slug, &doc.Title, &doc.Body, &doc.PushedBy, &doc.PushedAt, &doc.Read); err != nil {
|
|
return nil, err
|
|
}
|
|
docs = append(docs, doc)
|
|
}
|
|
return docs, rows.Err()
|
|
}
|
|
|
|
// GetDocument returns a single document by slug.
|
|
func (d *DB) GetDocument(slug string) (*Document, error) {
|
|
var doc Document
|
|
err := d.QueryRow(
|
|
`SELECT id, slug, title, body, pushed_by, pushed_at, read FROM documents WHERE slug = ?`,
|
|
slug,
|
|
).Scan(&doc.ID, &doc.Slug, &doc.Title, &doc.Body, &doc.PushedBy, &doc.PushedAt, &doc.Read)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &doc, nil
|
|
}
|
|
|
|
// PutDocument creates or updates a document by slug (upsert).
|
|
func (d *DB) PutDocument(slug, title, body, pushedBy string) (*Document, error) {
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
_, err := d.Exec(`
|
|
INSERT INTO documents (slug, title, body, pushed_by, pushed_at, read)
|
|
VALUES (?, ?, ?, ?, ?, 0)
|
|
ON CONFLICT(slug) DO UPDATE SET
|
|
title = excluded.title,
|
|
body = excluded.body,
|
|
pushed_by = excluded.pushed_by,
|
|
pushed_at = excluded.pushed_at,
|
|
read = 0`,
|
|
slug, title, body, pushedBy, now,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return d.GetDocument(slug)
|
|
}
|
|
|
|
// DeleteDocument removes a document by slug.
|
|
func (d *DB) DeleteDocument(slug string) error {
|
|
res, err := d.Exec(`DELETE FROM documents WHERE slug = ?`, slug)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, err := res.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarkRead sets the read flag on a document.
|
|
func (d *DB) MarkRead(slug string) (*Document, error) {
|
|
return d.setRead(slug, true)
|
|
}
|
|
|
|
// MarkUnread clears the read flag on a document.
|
|
func (d *DB) MarkUnread(slug string) (*Document, error) {
|
|
return d.setRead(slug, false)
|
|
}
|
|
|
|
func (d *DB) setRead(slug string, read bool) (*Document, error) {
|
|
val := 0
|
|
if read {
|
|
val = 1
|
|
}
|
|
res, err := d.Exec(`UPDATE documents SET read = ? WHERE slug = ?`, val, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
n, err := res.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n == 0 {
|
|
return nil, ErrNotFound
|
|
}
|
|
return d.GetDocument(slug)
|
|
}
|