Initial implementation of mcq — document reading queue
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>
This commit is contained in:
121
internal/db/documents.go
Normal file
121
internal/db/documents.go
Normal file
@@ -0,0 +1,121 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user