writekit/internal/tenant/sqlite.go

274 lines
6.7 KiB
Go
Raw Normal View History

2026-01-09 00:16:46 +02:00
package tenant
import (
"database/sql"
"fmt"
"os"
"path/filepath"
_ "modernc.org/sqlite"
)
func openDB(dataDir, tenantID string, inMemory bool) (*sql.DB, error) {
var dsn string
if inMemory {
// named in-memory DB with shared cache so all 'connections' share the same database
dsn = "file:" + tenantID + "?mode=memory&cache=shared&_pragma=busy_timeout(5000)"
} else {
dbPath := filepath.Join(dataDir, tenantID+".db")
if err := os.MkdirAll(dataDir, 0755); err != nil {
return nil, err
}
dsn = dbPath + "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)"
}
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, err
}
if err := initSchema(db); err != nil {
db.Close()
return nil, err
}
return db, nil
}
func initSchema(db *sql.DB) error {
schema := `
CREATE TABLE IF NOT EXISTS posts (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
title TEXT,
description TEXT,
tags TEXT,
cover_image TEXT,
content_md TEXT,
content_html TEXT,
is_published INTEGER DEFAULT 0,
members_only INTEGER DEFAULT 0,
published_at TEXT,
updated_at TEXT,
aliases TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
modified_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug);
CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published_at DESC) WHERE is_published = 1;
CREATE TABLE IF NOT EXISTS post_drafts (
post_id TEXT PRIMARY KEY REFERENCES posts(id) ON DELETE CASCADE,
slug TEXT NOT NULL,
title TEXT,
description TEXT,
tags TEXT,
cover_image TEXT,
members_only INTEGER DEFAULT 0,
content_md TEXT,
content_html TEXT,
modified_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS post_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id TEXT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
slug TEXT NOT NULL,
title TEXT,
description TEXT,
tags TEXT,
cover_image TEXT,
content_md TEXT,
content_html TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_post_versions_post ON post_versions(post_id);
CREATE TABLE IF NOT EXISTS pages (
path TEXT PRIMARY KEY,
html BLOB,
etag TEXT,
built_at TEXT
);
CREATE TABLE IF NOT EXISTS assets (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
r2_key TEXT NOT NULL,
content_type TEXT,
size INTEGER,
width INTEGER,
height INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS site_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
name TEXT,
avatar_url TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
expires_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL REFERENCES users(id),
post_slug TEXT NOT NULL,
content TEXT NOT NULL,
content_html TEXT,
parent_id INTEGER REFERENCES comments(id),
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_comments_post ON comments(post_slug);
CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_id);
CREATE TABLE IF NOT EXISTS reactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT REFERENCES users(id),
anon_id TEXT,
post_slug TEXT NOT NULL,
emoji TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, post_slug, emoji),
UNIQUE(anon_id, post_slug, emoji)
);
CREATE INDEX IF NOT EXISTS idx_reactions_post ON reactions(post_slug);
CREATE TABLE IF NOT EXISTS page_views (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
post_slug TEXT,
referrer TEXT,
user_agent TEXT,
visitor_hash TEXT,
utm_source TEXT,
utm_medium TEXT,
utm_campaign TEXT,
device_type TEXT,
browser TEXT,
os TEXT,
country TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_page_views_path ON page_views(path);
CREATE INDEX IF NOT EXISTS idx_page_views_created ON page_views(created_at);
CREATE INDEX IF NOT EXISTS idx_page_views_visitor ON page_views(visitor_hash);
CREATE TABLE IF NOT EXISTS daily_analytics (
date TEXT PRIMARY KEY,
requests INTEGER DEFAULT 0,
page_views INTEGER DEFAULT 0,
unique_visitors INTEGER DEFAULT 0,
bandwidth INTEGER DEFAULT 0,
browsers TEXT,
os TEXT,
devices TEXT,
countries TEXT,
paths TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS members (
user_id TEXT PRIMARY KEY REFERENCES users(id),
email TEXT NOT NULL,
name TEXT,
tier TEXT NOT NULL,
status TEXT NOT NULL,
expires_at TEXT,
synced_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS api_keys (
key TEXT PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_used_at TEXT
);
CREATE TABLE IF NOT EXISTS components (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
type TEXT NOT NULL,
source TEXT NOT NULL,
compiled TEXT,
client_directive TEXT DEFAULT 'load',
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS plugins (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
language TEXT NOT NULL,
source TEXT NOT NULL,
wasm BLOB,
hooks TEXT NOT NULL,
enabled INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS webhooks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL,
events TEXT NOT NULL,
secret TEXT,
enabled INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
last_triggered_at TEXT,
last_status TEXT
);
CREATE TABLE IF NOT EXISTS webhook_deliveries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
event TEXT NOT NULL,
payload TEXT,
status TEXT NOT NULL,
response_code INTEGER,
response_body TEXT,
attempts INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id, created_at DESC);
`
_, err := db.Exec(schema)
if err != nil {
return fmt.Errorf("init schema: %w", err)
}
_, err = db.Exec(`
CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(
slug UNINDEXED,
collection_slug UNINDEXED,
title,
description,
content,
type UNINDEXED,
url UNINDEXED,
date UNINDEXED
)
`)
if err != nil {
return fmt.Errorf("init fts5: %w", err)
}
return nil
}