Files
cms-SiteForge/app/db.py
T

94 lines
3.9 KiB
Python
Raw Normal View History

"""Local SQLite data store for SiteForge MVP. One database, hard site_id isolation."""
import os
import sqlite3
import json
from pathlib import Path
from datetime import datetime, timezone
ROOT = Path(__file__).resolve().parent.parent
DATA = Path(os.getenv('SITEFORGE_DATA_DIR', str(ROOT / 'storage'))).resolve()
DB_FILE = DATA / 'siteforge.sqlite3'
def now():
return datetime.now(timezone.utc).isoformat(timespec='seconds')
def connect():
DATA.mkdir(parents=True, exist_ok=True)
c = sqlite3.connect(DB_FILE)
c.row_factory = sqlite3.Row
c.execute('PRAGMA foreign_keys = ON')
c.execute('PRAGMA busy_timeout=5000')
return c
def init():
with connect() as c:
c.executescript('''
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS sites (
id TEXT PRIMARY KEY, name TEXT NOT NULL, domain TEXT NOT NULL DEFAULT '',
tagline TEXT NOT NULL DEFAULT '', theme TEXT NOT NULL DEFAULT 'corporate',
accent TEXT NOT NULL DEFAULT '#2563eb', logo TEXT NOT NULL DEFAULT '',
mode TEXT NOT NULL DEFAULT 'new',
settings_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL, updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS entries (
id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
type TEXT NOT NULL, title TEXT NOT NULL, route TEXT NOT NULL, slug TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '',
cover TEXT NOT NULL DEFAULT '', seo_title TEXT NOT NULL DEFAULT '',
seo_description TEXT NOT NULL DEFAULT '', canonical TEXT NOT NULL DEFAULT '',
robots TEXT NOT NULL DEFAULT 'index,follow',
meta_json TEXT NOT NULL DEFAULT '{}', status TEXT NOT NULL DEFAULT 'published',
source TEXT NOT NULL DEFAULT 'manual', created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
UNIQUE(site_id, route)
);
CREATE INDEX IF NOT EXISTS idx_entries_site ON entries(site_id, type, status);
CREATE TABLE IF NOT EXISTS redirects (
id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
old_path TEXT NOT NULL, new_path TEXT NOT NULL, status_code INTEGER NOT NULL DEFAULT 301,
UNIQUE(site_id, old_path)
);
CREATE TABLE IF NOT EXISTS builds (
id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
created_at TEXT NOT NULL, status TEXT NOT NULL, total_pages INTEGER NOT NULL DEFAULT 0,
report_json TEXT NOT NULL DEFAULT '{}', zip_path TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_builds_site ON builds(site_id, created_at);
CREATE TABLE IF NOT EXISTS presets (
id TEXT PRIMARY KEY,name TEXT NOT NULL,theme TEXT NOT NULL,
settings_json TEXT NOT NULL,home_blocks_json TEXT NOT NULL,created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS content_schemas (
site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
content_type TEXT NOT NULL, fields_json TEXT NOT NULL DEFAULT '[]',
PRIMARY KEY(site_id,content_type)
);
CREATE TABLE IF NOT EXISTS imports (
id TEXT PRIMARY KEY, site_id TEXT NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
created_at TEXT NOT NULL, source TEXT NOT NULL,
result_json TEXT NOT NULL DEFAULT '{}'
);
''')
def rows(sql, params=()):
with connect() as c:
return [dict(r) for r in c.execute(sql, params).fetchall()]
def one(sql, params=()):
with connect() as c:
r=c.execute(sql, params).fetchone()
return dict(r) if r else None
def execute(sql, params=()):
with connect() as c:
cur = c.execute(sql, params)
return cur.rowcount
def insert(sql, params=()):
with connect() as c:
c.execute(sql, params)
if __name__ == '__main__':
init()