Files
ink-note-app/electron/db.cjs
T

69 lines
2.6 KiB
JavaScript

'use strict';
// Full-text search index in <vault>/.notes/index.sqlite (SQLite FTS5).
// The Markdown files are the source of truth; this file can be deleted and is rebuilt on launch.
// Uses better-sqlite3 in the app; falls back to node:sqlite so the tests run on plain Node 22.
function loadDriver() {
try { return require('better-sqlite3'); } catch {
const { DatabaseSync } = require('node:sqlite');
return DatabaseSync;
}
}
const likeEscape = (s) => s.replace(/[\\%_]/g, (c) => '\\' + c);
function openDb(file) {
const Database = loadDriver();
const db = new Database(file);
db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS notes USING fts5(
id UNINDEXED, title, tags, body, tokenize = 'unicode61 remove_diacritics 2'
)`);
const del = db.prepare('DELETE FROM notes WHERE id = ?');
const delTree = db.prepare("DELETE FROM notes WHERE id = ? OR id LIKE ? ESCAPE '\\'");
const ins = db.prepare('INSERT INTO notes (id, title, tags, body) VALUES (?, ?, ?, ?)');
const find = db.prepare(`SELECT id, title, snippet(notes, 3, char(1), char(2), '…', 14) AS snip
FROM notes WHERE notes MATCH ? ORDER BY bm25(notes, 0.0, 8.0, 4.0, 1.0) LIMIT ?`);
const row = (p) => [p.id, p.title || '', (p.tags || []).join(' '), p.body || ''];
return {
upsert(p) { del.run(p.id); ins.run(...row(p)); },
remove(id) { delTree.run(id, likeEscape(id) + '/%'); },
rebuild(pages) {
db.exec('BEGIN');
try {
db.exec('DELETE FROM notes');
for (const p of pages) if (p.exists) ins.run(...row(p));
db.exec('COMMIT');
} catch (err) {
db.exec('ROLLBACK');
throw err;
}
},
search(q, limit = 20) {
const parts = String(q).match(/(?:tag:|in:|)\S+/g) || [];
const terms = [];
const fields = [];
for (const p of parts) {
if (p.startsWith('tag:')) { fields.push('tags: ' + p.slice(4).replace(/[^\w-]/g, '')); }
else if (p.startsWith('in:')) { fields.push('title: ' + p.slice(3).replace(/[^\w-]/g, '')); }
else { terms.push(p); }
}
const words = (terms.join(' ').match(/[\p{L}\p{N}_]+/gu) || []).slice(0, 8);
let match = words.map((t) => `"${t}"*`).join(' ');
if (fields.length) {
match = fields.map(f => `${f.split(':')[0]}: "${f.split(':')[1].trim()}"*`).join(' ') + (match ? ' ' + match : '');
}
if (!match) return [];
try {
return find.all(match, limit).map((r) => ({ id: r.id, title: r.title, snip: r.snip }));
} catch {
return [];
}
},
close() { try { db.close(); } catch { /* already closed */ } }
};
}
module.exports = { openDb };