db.js
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getDataDir = getDataDir;
exports.getDb = getDb;
exports.insertNote = insertNote;
exports.updateNote = updateNote;
exports.deleteNote = deleteNote;
exports.getNoteById = getNoteById;
exports.listNotes = listNotes;
exports.searchNotes = searchNotes;
exports.upsertConcept = upsertConcept;
exports.getConceptByName = getConceptByName;
exports.listConcepts = listConcepts;
exports.linkNoteToConcept = linkNoteToConcept;
exports.unlinkNoteConcept = unlinkNoteConcept;
exports.getConceptNotes = getConceptNotes;
exports.getNoteConcepts = getNoteConcepts;
exports.getUnlinkedNotes = getUnlinkedNotes;
exports.countNotes = countNotes;
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const os_1 = require("os");
const fs_1 = require("fs");
const path_1 = require("path");
function getDataDir(configPath) {
const p = configPath.trim() || "~/knowledge-data";
return p.startsWith("~/") ? (0, path_1.join)((0, os_1.homedir)(), p.slice(2)) : p;
}
const _dbCache = new Map();
function getDb(dataDir) {
if (_dbCache.has(dataDir))
return _dbCache.get(dataDir);
(0, fs_1.mkdirSync)(dataDir, { recursive: true });
const db = new better_sqlite3_1.default((0, path_1.join)(dataDir, "knowledge.db"));
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
source TEXT NOT NULL DEFAULT '',
tags TEXT NOT NULL DEFAULT '[]',
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS concepts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS note_concepts (
noteId INTEGER NOT NULL,
conceptId INTEGER NOT NULL,
PRIMARY KEY (noteId, conceptId),
FOREIGN KEY (noteId) REFERENCES notes(id) ON DELETE CASCADE,
FOREIGN KEY (conceptId) REFERENCES concepts(id) ON DELETE CASCADE
);
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
title, content, source, tags,
content='notes',
content_rowid='id'
);
CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, title, content, source, tags)
VALUES (new.id, new.title, new.content, new.source, new.tags);
END;
CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, title, content, source, tags)
VALUES ('delete', old.id, old.title, old.content, old.source, old.tags);
INSERT INTO notes_fts(rowid, title, content, source, tags)
VALUES (new.id, new.title, new.content, new.source, new.tags);
END;
CREATE TRIGGER IF NOT EXISTS notes_ad AFTER DELETE ON notes BEGIN
INSERT INTO notes_fts(notes_fts, rowid, title, content, source, tags)
VALUES ('delete', old.id, old.title, old.content, old.source, old.tags);
END;
CREATE INDEX IF NOT EXISTS idx_notes_createdAt ON notes(createdAt);
CREATE INDEX IF NOT EXISTS idx_concepts_name ON concepts(name);
CREATE INDEX IF NOT EXISTS idx_nc_noteId ON note_concepts(noteId);
CREATE INDEX IF NOT EXISTS idx_nc_conceptId ON note_concepts(conceptId);
`);
_dbCache.set(dataDir, db);
return db;
}
function insertNote(db, title, content, source, tags) {
const now = new Date().toISOString();
const r = db.prepare(`INSERT INTO notes (title, content, source, tags, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?)`).run(title, content, source, JSON.stringify(tags), now, now);
return r.lastInsertRowid;
}
function updateNote(db, id, patch) {
const row = db.prepare("SELECT * FROM notes WHERE id = ?").get(id);
if (!row)
throw new Error(`Note ID ${id} not found.`);
const now = new Date().toISOString();
db.prepare(`UPDATE notes SET title=?, content=?, source=?, tags=?, updatedAt=? WHERE id=?`).run(patch.title ?? row.title, patch.content ?? row.content, patch.source ?? row.source, patch.tags ? JSON.stringify(patch.tags) : row.tags, now, id);
}
function deleteNote(db, id) {
const r = db.prepare("DELETE FROM notes WHERE id = ?").run(id);
if (r.changes === 0)
throw new Error(`Note ID ${id} not found.`);
}
function getNoteById(db, id) {
const row = db.prepare("SELECT * FROM notes WHERE id = ?").get(id);
if (!row)
throw new Error(`Note ID ${id} not found.`);
return row;
}
function listNotes(db, opts = {}) {
let sql = "SELECT * FROM notes";
const params = [];
const clauses = [];
if (opts.tag) {
clauses.push("tags LIKE ?");
params.push(`%"${opts.tag}"%`);
}
if (opts.since) {
clauses.push("createdAt >= ?");
params.push(opts.since);
}
if (clauses.length > 0)
sql += " WHERE " + clauses.join(" AND ");
sql += " ORDER BY createdAt DESC";
if (opts.limit) {
sql += " LIMIT ?";
params.push(opts.limit);
}
return db.prepare(sql).all(...params);
}
function searchNotes(db, query, limit = 20) {
const safeFtsQuery = `"${query.replace(/"/g, '""')}"`;
const rows = db.prepare(`SELECT notes.* FROM notes_fts
JOIN notes ON notes.id = notes_fts.rowid
WHERE notes_fts MATCH ?
ORDER BY rank
LIMIT ?`).all(safeFtsQuery, limit);
return rows;
}
function upsertConcept(db, name, description) {
const now = new Date().toISOString();
db.prepare(`INSERT INTO concepts (name, description, createdAt, updatedAt)
VALUES (?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET description=excluded.description, updatedAt=excluded.updatedAt`).run(name, description, now, now);
return db.prepare("SELECT * FROM concepts WHERE name = ?").get(name);
}
function getConceptByName(db, name) {
return db.prepare("SELECT * FROM concepts WHERE name = ?").get(name);
}
function listConcepts(db) {
return db.prepare(`SELECT c.*, COUNT(nc.noteId) as noteCount
FROM concepts c
LEFT JOIN note_concepts nc ON nc.conceptId = c.id
GROUP BY c.id
ORDER BY noteCount DESC, c.name ASC`).all();
}
function linkNoteToConcept(db, noteId, conceptId) {
db.prepare(`INSERT OR IGNORE INTO note_concepts (noteId, conceptId) VALUES (?, ?)`).run(noteId, conceptId);
}
function unlinkNoteConcept(db, noteId, conceptId) {
db.prepare("DELETE FROM note_concepts WHERE noteId = ? AND conceptId = ?").run(noteId, conceptId);
}
function getConceptNotes(db, conceptId) {
return db.prepare(`SELECT n.* FROM notes n
JOIN note_concepts nc ON nc.noteId = n.id
WHERE nc.conceptId = ?
ORDER BY n.createdAt DESC`).all(conceptId);
}
function getNoteConcepts(db, noteId) {
return db.prepare(`SELECT c.* FROM concepts c
JOIN note_concepts nc ON nc.conceptId = c.id
WHERE nc.noteId = ?`).all(noteId);
}
function getUnlinkedNotes(db, limit = 20) {
return db.prepare(`SELECT n.* FROM notes n
WHERE NOT EXISTS (SELECT 1 FROM note_concepts nc WHERE nc.noteId = n.id)
ORDER BY n.createdAt DESC
LIMIT ?`).all(limit);
}
function countNotes(db) {
const row = db.prepare("SELECT COUNT(*) AS n FROM notes").get();
return row.n;
}