Files
studyos/server/db.js
2026-06-08 16:53:18 -03:00

182 lines
5.3 KiB
JavaScript

const path = require('path');
const fs = require('fs');
const DATA_DIR = path.resolve(__dirname, '..', 'data');
const DB_PATH = path.join(DATA_DIR, 'studyos.db');
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
let _sqlDb = null;
function saveToDisk() {
if (!_sqlDb) return;
const data = _sqlDb.export();
fs.writeFileSync(DB_PATH, Buffer.from(data));
}
function flatParams(params) {
if (params.length === 1 && Array.isArray(params[0])) return params[0];
return params;
}
function createWrapper(sqlDb) {
return {
prepare(sql) {
return {
run(...params) {
const p = flatParams(params);
sqlDb.run(sql, p);
const rowidResult = sqlDb.exec('SELECT last_insert_rowid()');
const lastInsertRowid = rowidResult.length > 0 ? rowidResult[0].values[0][0] : 0;
const changes = sqlDb.getRowsModified();
saveToDisk();
return { changes, lastInsertRowid };
},
get(...params) {
const p = flatParams(params);
let stmt = null;
try {
stmt = sqlDb.prepare(sql);
stmt.bind(p);
let result = null;
if (stmt.step()) result = stmt.getAsObject();
return result;
} finally {
if (stmt) stmt.free();
}
},
all(...params) {
const p = flatParams(params);
let stmt = null;
try {
stmt = sqlDb.prepare(sql);
stmt.bind(p);
const results = [];
while (stmt.step()) results.push(stmt.getAsObject());
return results;
} finally {
if (stmt) stmt.free();
}
},
};
},
exec(sql) {
sqlDb.exec(sql);
saveToDisk();
},
pragma(sql) {
sqlDb.exec('PRAGMA ' + sql + ';');
},
};
}
const db = createWrapper(null);
async function initDB() {
const SQL = await import('sql.js');
const initSqlJs = SQL.default;
const sqlModule = await initSqlJs();
let sqlDb;
if (fs.existsSync(DB_PATH)) {
const buffer = fs.readFileSync(DB_PATH);
sqlDb = new sqlModule.Database(buffer);
} else {
sqlDb = new sqlModule.Database();
}
_sqlDb = sqlDb;
const real = createWrapper(sqlDb);
db.prepare = real.prepare;
db.exec = real.exec;
db.pragma = real.pragma;
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
db.exec(`
CREATE TABLE IF NOT EXISTS models (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
api_base TEXT NOT NULL,
api_key TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL CHECK(provider IN ('openai', 'anthropic')),
is_default_main INTEGER NOT NULL DEFAULT 0,
is_default_fork INTEGER NOT NULL DEFAULT 0,
is_default_exam INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS conversations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('main', 'fork')),
parent_id INTEGER REFERENCES conversations(id) ON DELETE SET NULL,
model_id INTEGER REFERENCES models(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id INTEGER NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'context_merge')),
content TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS pdfs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
filename TEXT NOT NULL,
original_name TEXT NOT NULL,
content_markdown TEXT NOT NULL DEFAULT '',
pages INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
reorder_index INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL UNIQUE,
exercises_done INTEGER NOT NULL DEFAULT 0,
exercises_correct INTEGER NOT NULL DEFAULT 0,
last_session TEXT,
notes TEXT NOT NULL DEFAULT '[]'
);
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content_markdown TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
);
`);
const modelCount = db.prepare('SELECT COUNT(*) as count FROM models').get();
if (!modelCount || modelCount.count === 0) {
db.prepare(`
INSERT INTO models (name, api_base, api_key, provider, is_default_main)
VALUES (?, ?, ?, ?, ?)
`).run('claude-sonnet-4', 'https://api.anthropic.com', '', 'anthropic', 1);
}
const vlmConfig = db.prepare("SELECT value FROM config WHERE key = ?").get('vlm_endpoint');
if (!vlmConfig) {
db.prepare('INSERT INTO config (key, value) VALUES (?, ?)').run('vlm_endpoint', 'http://localhost:8080/vlm');
}
return db;
}
module.exports = db;
module.exports.initDB = initDB;