274 lines
6.7 KiB
Go
274 lines
6.7 KiB
Go
|
|
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
|
||
|
|
}
|