209 lines
8.8 KiB
Rust
209 lines
8.8 KiB
Rust
use sqlx::AnyPool;
|
|
|
|
pub async fn init_schema(pool: &AnyPool) {
|
|
sqlx::query("PRAGMA foreign_keys = ON")
|
|
.execute(pool)
|
|
.await
|
|
.expect("Failed to enable SQLite foreign keys");
|
|
|
|
// Rename the legacy "space" tables/columns to the "project" vocabulary on
|
|
// existing databases. Best-effort: on a fresh database (or one already
|
|
// migrated) the old names don't exist, so these fail silently and the
|
|
// CREATE TABLE IF NOT EXISTS statements below take over.
|
|
let rename_migrations = [
|
|
"ALTER TABLE spaces RENAME TO projects",
|
|
"ALTER TABLE space_files RENAME TO project_files",
|
|
"ALTER TABLE space_collaborators RENAME TO project_collaborators",
|
|
"ALTER TABLE project_files RENAME COLUMN space_id TO project_id",
|
|
"ALTER TABLE project_collaborators RENAME COLUMN space_id TO project_id",
|
|
];
|
|
for stmt in &rename_migrations {
|
|
let _ = sqlx::query(stmt).execute(pool).await;
|
|
}
|
|
|
|
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 INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"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 (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"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 BLOB,
|
|
thumbnail_svg TEXT,
|
|
public_role TEXT DEFAULT NULL,
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"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 BLOB NOT NULL,
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"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 (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
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 (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
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 INTEGER DEFAULT 0,
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"CREATE TABLE IF NOT EXISTS document_history (
|
|
id TEXT PRIMARY KEY,
|
|
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
|
content BLOB NOT NULL,
|
|
created_by TEXT NOT NULL REFERENCES users(id) ON DELETE SET NULL,
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"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 (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"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 BLOB 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 projects (
|
|
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 (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))
|
|
)",
|
|
"CREATE TABLE IF NOT EXISTS project_files (
|
|
id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
path TEXT NOT NULL,
|
|
kind TEXT NOT NULL DEFAULT 'text',
|
|
content BLOB,
|
|
mime_type TEXT NOT NULL DEFAULT 'text/plain',
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
UNIQUE(project_id, path)
|
|
)",
|
|
"CREATE TABLE IF NOT EXISTS project_collaborators (
|
|
id TEXT PRIMARY KEY,
|
|
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL,
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
UNIQUE(project_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 (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
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 BLOB,
|
|
created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now')),
|
|
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 BLOB NOT NULL,
|
|
UNIQUE(version_id, path)
|
|
)",
|
|
"CREATE TABLE IF NOT EXISTS device_tokens (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL,
|
|
last_used_at TEXT
|
|
)",
|
|
];
|
|
|
|
for stmt in &statements {
|
|
sqlx::query(stmt)
|
|
.execute(pool)
|
|
.await
|
|
.expect("Failed to execute SQLite schema");
|
|
}
|
|
|
|
// Idempotent migrations for existing databases
|
|
let migrations = [
|
|
"ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0",
|
|
"ALTER TABLE users ADD COLUMN created_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))",
|
|
"ALTER TABLE project_files ADD COLUMN updated_at TEXT DEFAULT (strftime('%Y-%m-%d %H:%M:%S', 'now'))",
|
|
];
|
|
for stmt in &migrations {
|
|
let _ = sqlx::query(stmt).execute(pool).await;
|
|
}
|
|
}
|