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:
2026-03-28 11:53:26 -07:00
commit bc1627915e
36 changed files with 3773 additions and 0 deletions

23
internal/db/db.go Normal file
View File

@@ -0,0 +1,23 @@
package db
import (
"database/sql"
"fmt"
mcdsldb "git.wntrmute.dev/mc/mcdsl/db"
)
// DB wraps a SQLite database connection.
type DB struct {
*sql.DB
}
// Open opens (or creates) a SQLite database at the given path with the
// standard Metacircular pragmas: WAL mode, foreign keys, busy timeout.
func Open(path string) (*DB, error) {
sqlDB, err := mcdsldb.Open(path)
if err != nil {
return nil, fmt.Errorf("db: %w", err)
}
return &DB{sqlDB}, nil
}

121
internal/db/documents.go Normal file
View 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)
}

View File

@@ -0,0 +1,145 @@
package db
import (
"path/filepath"
"testing"
)
func openTestDB(t *testing.T) *DB {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "test.db")
database, err := Open(dbPath)
if err != nil {
t.Fatal(err)
}
if err := database.Migrate(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() { database.Close() })
return database
}
func TestPutAndGetDocument(t *testing.T) {
db := openTestDB(t)
doc, err := db.PutDocument("test-slug", "Test Title", "# Hello", "kyle")
if err != nil {
t.Fatal(err)
}
if doc.Slug != "test-slug" {
t.Errorf("slug = %q, want %q", doc.Slug, "test-slug")
}
if doc.Title != "Test Title" {
t.Errorf("title = %q, want %q", doc.Title, "Test Title")
}
if doc.Body != "# Hello" {
t.Errorf("body = %q, want %q", doc.Body, "# Hello")
}
if doc.PushedBy != "kyle" {
t.Errorf("pushed_by = %q, want %q", doc.PushedBy, "kyle")
}
if doc.Read {
t.Error("new document should not be read")
}
got, err := db.GetDocument("test-slug")
if err != nil {
t.Fatal(err)
}
if got.Title != "Test Title" {
t.Errorf("got title = %q, want %q", got.Title, "Test Title")
}
}
func TestPutDocumentUpsert(t *testing.T) {
db := openTestDB(t)
_, err := db.PutDocument("slug", "V1", "body v1", "alice")
if err != nil {
t.Fatal(err)
}
// Mark as read.
_, err = db.MarkRead("slug")
if err != nil {
t.Fatal(err)
}
// Upsert — should replace and reset read flag.
doc, err := db.PutDocument("slug", "V2", "body v2", "bob")
if err != nil {
t.Fatal(err)
}
if doc.Title != "V2" {
t.Errorf("title = %q, want V2", doc.Title)
}
if doc.Body != "body v2" {
t.Errorf("body = %q, want body v2", doc.Body)
}
if doc.PushedBy != "bob" {
t.Errorf("pushed_by = %q, want bob", doc.PushedBy)
}
if doc.Read {
t.Error("upsert should reset read flag")
}
}
func TestListDocuments(t *testing.T) {
db := openTestDB(t)
_, _ = db.PutDocument("a", "A", "body", "user")
_, _ = db.PutDocument("b", "B", "body", "user")
docs, err := db.ListDocuments()
if err != nil {
t.Fatal(err)
}
if len(docs) != 2 {
t.Fatalf("got %d docs, want 2", len(docs))
}
}
func TestDeleteDocument(t *testing.T) {
db := openTestDB(t)
_, _ = db.PutDocument("del", "Del", "body", "user")
if err := db.DeleteDocument("del"); err != nil {
t.Fatal(err)
}
_, err := db.GetDocument("del")
if err != ErrNotFound {
t.Errorf("got err = %v, want ErrNotFound", err)
}
}
func TestDeleteDocumentNotFound(t *testing.T) {
db := openTestDB(t)
err := db.DeleteDocument("nope")
if err != ErrNotFound {
t.Errorf("got err = %v, want ErrNotFound", err)
}
}
func TestMarkReadUnread(t *testing.T) {
db := openTestDB(t)
_, _ = db.PutDocument("rw", "RW", "body", "user")
doc, err := db.MarkRead("rw")
if err != nil {
t.Fatal(err)
}
if !doc.Read {
t.Error("expected read=true after MarkRead")
}
doc, err = db.MarkUnread("rw")
if err != nil {
t.Fatal(err)
}
if doc.Read {
t.Error("expected read=false after MarkUnread")
}
}

28
internal/db/migrate.go Normal file
View File

@@ -0,0 +1,28 @@
package db
import (
mcdsldb "git.wntrmute.dev/mc/mcdsl/db"
)
// Migrations is the ordered list of MCQ schema migrations.
var Migrations = []mcdsldb.Migration{
{
Version: 1,
Name: "documents",
SQL: `
CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY,
slug TEXT NOT NULL UNIQUE,
title TEXT NOT NULL,
body TEXT NOT NULL,
pushed_by TEXT NOT NULL,
pushed_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
read INTEGER NOT NULL DEFAULT 0
);`,
},
}
// Migrate applies all pending migrations.
func (d *DB) Migrate() error {
return mcdsldb.Migrate(d.DB, Migrations)
}