Files
TypstDrive/server/src/db/postgres.rs
T

181 lines
7.6 KiB
Rust

use sqlx::AnyPool;
pub async fn init_schema(pool: &AnyPool) {
let statements = [
"CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
email TEXT UNIQUE,
password_hash TEXT NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS folders (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id),
parent_id TEXT REFERENCES folders(id),
name TEXT NOT NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id),
folder_id TEXT REFERENCES folders(id),
title TEXT NOT NULL,
content BYTEA,
thumbnail_svg TEXT,
public_role TEXT DEFAULT NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id),
document_id TEXT REFERENCES documents(id),
folder_id TEXT REFERENCES folders(id),
name TEXT NOT NULL,
mime_type TEXT NOT NULL,
data BYTEA NOT NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS collaborators (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
UNIQUE(document_id, user_id)
)",
"CREATE TABLE IF NOT EXISTS invitations (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
role TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
expires_at TEXT
)",
"CREATE TABLE IF NOT EXISTS comments (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
resolved BOOLEAN DEFAULT FALSE,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS document_history (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
content BYTEA NOT NULL,
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS document_versions (
id TEXT PRIMARY KEY,
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
content TEXT NOT NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL,
created_at TEXT NOT NULL,
last_used_at TEXT,
rate_limit INTEGER NOT NULL DEFAULT 60
)",
"CREATE TABLE IF NOT EXISTS api_render_cache (
id TEXT PRIMARY KEY,
content_hash TEXT NOT NULL UNIQUE,
format TEXT NOT NULL,
data BYTEA NOT NULL,
created_at TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS api_key_usage (
key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
date TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY(key_id, date)
)",
"CREATE TABLE IF NOT EXISTS api_key_usage_detail (
key_id TEXT NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE,
minute TEXT NOT NULL,
count INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY(key_id, minute)
)",
"CREATE TABLE IF NOT EXISTS spaces (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id),
folder_id TEXT REFERENCES folders(id),
name TEXT NOT NULL,
entrypoint TEXT NOT NULL DEFAULT 'main.typ',
thumbnail_svg TEXT,
public_role TEXT DEFAULT NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
updated_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')
)",
"CREATE TABLE IF NOT EXISTS space_files (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
path TEXT NOT NULL,
kind TEXT NOT NULL DEFAULT 'text',
content BYTEA,
mime_type TEXT NOT NULL DEFAULT 'text/plain',
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
UNIQUE(space_id, path)
)",
"CREATE TABLE IF NOT EXISTS space_collaborators (
id TEXT PRIMARY KEY,
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
UNIQUE(space_id, user_id)
)",
"CREATE TABLE IF NOT EXISTS packages (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL REFERENCES users(id),
namespace TEXT NOT NULL DEFAULT 'typstdrive',
name TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
UNIQUE(namespace, name)
)",
"CREATE TABLE IF NOT EXISTS package_versions (
id TEXT PRIMARY KEY,
package_id TEXT NOT NULL REFERENCES packages(id) ON DELETE CASCADE,
version TEXT NOT NULL,
entrypoint TEXT NOT NULL DEFAULT 'lib.typ',
manifest BYTEA,
created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS'),
UNIQUE(package_id, version)
)",
"CREATE TABLE IF NOT EXISTS package_files (
id TEXT PRIMARY KEY,
version_id TEXT NOT NULL REFERENCES package_versions(id) ON DELETE CASCADE,
path TEXT NOT NULL,
data BYTEA NOT NULL,
UNIQUE(version_id, path)
)",
];
for stmt in &statements {
sqlx::query(stmt)
.execute(pool)
.await
.expect("Failed to execute Postgres schema");
}
// Idempotent migrations for existing databases
let migrations = [
"ALTER TABLE documents ADD COLUMN IF NOT EXISTS public_role TEXT",
"ALTER TABLE users ADD COLUMN IF NOT EXISTS is_admin BOOLEAN NOT NULL DEFAULT FALSE",
"ALTER TABLE users ADD COLUMN IF NOT EXISTS created_at TEXT DEFAULT to_char(NOW(), 'YYYY-MM-DD HH24:MI:SS')",
];
for stmt in &migrations {
sqlx::query(stmt).execute(pool).await.unwrap_or_else(|_| Default::default());
}
}