Files
FootPrint/sync-server/src/db.mjs
T

106 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 同步服务存储层:SQLite(node:sqlite 内置,零依赖)
import { DatabaseSync } from 'node:sqlite';
import fs from 'node:fs';
import path from 'node:path';
export const DATA_DIR = process.env.DATA_DIR || path.join(process.cwd(), 'data');
export const BLOB_DIR = path.join(DATA_DIR, 'blobs');
const DB_PATH = path.join(DATA_DIR, 'sync.db');
let db = null;
export function getDb() {
if (db) return db;
fs.mkdirSync(BLOB_DIR, { recursive: true });
db = new DatabaseSync(DB_PATH);
db.exec('PRAGMA journal_mode = WAL;');
db.exec('PRAGMA foreign_keys = ON;');
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS devices (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
name TEXT NOT NULL,
last_seen_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
UNIQUE(user_id, name)
);
CREATE TABLE IF NOT EXISTS tokens (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
device_id TEXT NOT NULL REFERENCES devices(id),
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
expires_at TEXT NOT NULL
);
-- 每行最新状态(按 user + table + row_uuid 唯一,LWW 胜者)
CREATE TABLE IF NOT EXISTS current (
user_id TEXT NOT NULL,
table_name TEXT NOT NULL,
row_uuid TEXT NOT NULL,
op TEXT NOT NULL,
hlc TEXT NOT NULL,
ms INTEGER NOT NULL,
cnt INTEGER NOT NULL,
dev TEXT NOT NULL,
data TEXT,
updated_seq INTEGER,
PRIMARY KEY (user_id, table_name, row_uuid)
);
-- 全量变更历史(seq 单调递增,作为拉取游标基准)
CREATE TABLE IF NOT EXISTS changes (
seq INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
table_name TEXT NOT NULL,
row_uuid TEXT NOT NULL,
op TEXT NOT NULL,
hlc TEXT NOT NULL,
data TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime'))
);
CREATE INDEX IF NOT EXISTS idx_current_seq ON current(user_id, updated_seq);
CREATE INDEX IF NOT EXISTS idx_changes_user ON changes(user_id, seq);
-- 附件:内容寻址去重
CREATE TABLE IF NOT EXISTS attachments (
user_id TEXT NOT NULL,
sha256 TEXT NOT NULL,
size INTEGER NOT NULL,
mime TEXT,
file_name TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
PRIMARY KEY (user_id, sha256)
);
`);
// 管理员与邀请码(动态颁发制;首个注册用户自动成为管理员)
const hasAdmin = db.prepare("SELECT COUNT(*) AS c FROM pragma_table_info('users') WHERE name='is_admin'").get().c;
if (Number(hasAdmin) === 0) db.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0;");
db.exec(`CREATE TABLE IF NOT EXISTS invite_codes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT NOT NULL UNIQUE,
created_by TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now','localtime')),
used_by TEXT,
used_at TEXT,
valid INTEGER NOT NULL DEFAULT 1
);`);
const hasRemark = db.prepare("SELECT COUNT(*) AS c FROM pragma_table_info('invite_codes') WHERE name='remark'").get().c;
if (Number(hasRemark) === 0) db.exec("ALTER TABLE invite_codes ADD COLUMN remark TEXT;");
return db;
}
// 允许同步的表(与客户端 schema 对应;footprint_dimension 以 uuid 对作普通行同步)
export const SYNC_TABLES = new Set([
'footprint', 'footprint_type', 'taxonomy', 'dimension', 'attachment',
'semester', 'course_grade', 'metric_def', 'saved_view', 'footprint_dimension'
]);