UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

372 lines (369 loc) 12.3 kB
import { initCoreSchemas, validateHandle } from "./chunk-U3FWUPD3.js"; import { MCard } from "./chunk-O2UMNZGF.js"; // src/storage/SqliteNodeEngine.ts import { createRequire } from "module"; var DEFAULT_PAGE_SIZE = 10; var SqliteNodeEngine = class { db; dbPath; /** * Create a new SqliteNodeEngine * @param dbPath Path to database file, or ':memory:' for in-memory database */ constructor(dbPath = ":memory:") { this.dbPath = dbPath; const require2 = createRequire(import.meta.url); const Database = require2("better-sqlite3"); this.db = new Database(dbPath); this.setupDatabase(); } /** * Initialize database schema */ setupDatabase() { initCoreSchemas(this.db); } /** * Close the database connection */ close() { this.db.close(); } /** * Get the database path */ getDbPath() { return this.dbPath; } // =========== Card Operations =========== async add(card) { const stmt = this.db.prepare( "INSERT OR REPLACE INTO card (hash, content, g_time) VALUES (?, ?, ?)" ); stmt.run(card.hash, Buffer.from(card.content), card.g_time); return card.hash; } /** * Add a card synchronously (for performance-critical paths) */ addSync(card) { const stmt = this.db.prepare( "INSERT OR REPLACE INTO card (hash, content, g_time) VALUES (?, ?, ?)" ); stmt.run(card.hash, Buffer.from(card.content), card.g_time); return card.hash; } async get(hash) { return this.getSync(hash); } /** * Get a card synchronously */ getSync(hash) { const stmt = this.db.prepare("SELECT hash, content, g_time FROM card WHERE hash = ?"); const row = stmt.get(hash); if (!row) return null; const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); } async delete(hash) { this.deleteSync(hash); } /** * Delete a card synchronously */ deleteSync(hash) { const stmt = this.db.prepare("DELETE FROM card WHERE hash = ?"); const result = stmt.run(hash); return result.changes > 0; } async getPage(pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { return this.getPageSync(pageNumber, pageSize); } /** * Get a page of cards synchronously */ getPageSync(pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { if (pageNumber < 1) throw new Error("Page number must be >= 1"); if (pageSize < 1) throw new Error("Page size must be >= 1"); const totalItems = this.countSync(); const totalPages = Math.ceil(totalItems / pageSize) || 1; const offset = (pageNumber - 1) * pageSize; const stmt = this.db.prepare( "SELECT hash, content, g_time FROM card ORDER BY g_time DESC LIMIT ? OFFSET ?" ); const rows = stmt.all(pageSize, offset); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: offset + items.length < totalItems, hasPrevious: pageNumber > 1 }; } async count() { return this.countSync(); } /** * Count cards synchronously */ countSync() { const stmt = this.db.prepare("SELECT COUNT(*) as count FROM card"); const row = stmt.get(); return row.count; } async searchByHash(hashPrefix) { const rows = this.db.prepare("SELECT hash, content, g_time FROM card WHERE hash LIKE ?").all(`${hashPrefix}%`); return rows.map( (row) => MCard.fromData(new Uint8Array(row.content), row.hash, row.g_time) ); } async search(query, pageNumber, pageSize) { const offset = (pageNumber - 1) * pageSize; const searchPattern = `%${query}%`; const countResult = this.db.prepare("SELECT COUNT(*) as count FROM card WHERE CAST(content AS TEXT) LIKE ?").get(searchPattern); const totalItems = countResult.count; const totalPages = Math.ceil(totalItems / pageSize) || 1; const rows = this.db.prepare("SELECT hash, content, g_time FROM card WHERE CAST(content AS TEXT) LIKE ? LIMIT ? OFFSET ?").all(searchPattern, pageSize, offset); const items = rows.map( (row) => MCard.fromData(new Uint8Array(row.content), row.hash, row.g_time) ); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: pageNumber < totalPages, hasPrevious: pageNumber > 1 }; } async getAll() { const rows = this.db.prepare("SELECT hash, content, g_time FROM card").all(); return rows.map( (row) => MCard.fromData(new Uint8Array(row.content), row.hash, row.g_time) ); } async clear() { this.clearSync(); } /** * Clear all data synchronously * Delete in FK-safe order: children first, then parents */ clearSync() { this.db.exec("DELETE FROM handle_history"); this.db.exec("DELETE FROM handle_registry"); this.db.exec("DELETE FROM card"); } // =========== Search Operations =========== /** * Search cards by string in content, hash, or g_time */ searchByString(searchString, pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { if (pageNumber < 1) throw new Error("Page number must be >= 1"); if (pageSize < 1) throw new Error("Page size must be >= 1"); const pattern = `%${searchString}%`; const offset = (pageNumber - 1) * pageSize; const countStmt = this.db.prepare(` SELECT COUNT(*) as count FROM card WHERE hash LIKE ? OR g_time LIKE ? OR CAST(content AS TEXT) LIKE ? `); const countRow = countStmt.get(pattern, pattern, pattern); const totalItems = countRow.count; const totalPages = Math.ceil(totalItems / pageSize) || 1; const stmt = this.db.prepare(` SELECT hash, content, g_time FROM card WHERE hash LIKE ? OR g_time LIKE ? OR CAST(content AS TEXT) LIKE ? ORDER BY g_time DESC LIMIT ? OFFSET ? `); const rows = stmt.all(pattern, pattern, pattern, pageSize, offset); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: offset + items.length < totalItems, hasPrevious: pageNumber > 1 }; } /** * Search cards by content pattern (binary-safe) */ searchByContent(searchPattern, pageNumber = 1, pageSize = DEFAULT_PAGE_SIZE) { if (pageNumber < 1) throw new Error("Page number must be >= 1"); if (pageSize < 1) throw new Error("Page size must be >= 1"); if (!searchPattern || typeof searchPattern === "string" && searchPattern.length === 0) { throw new Error("Search pattern cannot be empty"); } const searchBytes = typeof searchPattern === "string" ? Buffer.from(searchPattern, "utf-8") : Buffer.from(searchPattern); const offset = (pageNumber - 1) * pageSize; const countStmt = this.db.prepare("SELECT COUNT(*) as count FROM card WHERE INSTR(content, ?) > 0"); const countRow = countStmt.get(searchBytes); const totalItems = countRow.count; const totalPages = Math.ceil(totalItems / pageSize) || 1; const stmt = this.db.prepare(` SELECT hash, content, g_time FROM card WHERE INSTR(content, ?) > 0 ORDER BY g_time DESC LIMIT ? OFFSET ? `); const rows = stmt.all(searchBytes, pageSize, offset); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems, pageNumber, pageSize, totalPages, hasNext: offset + items.length < totalItems, hasPrevious: pageNumber > 1 }; } /** * Get all cards (single page with all items) */ getAllCards() { const stmt = this.db.prepare("SELECT hash, content, g_time FROM card ORDER BY g_time DESC"); const rows = stmt.all(); const items = rows.map((row) => { const content = row.content instanceof Buffer ? new Uint8Array(row.content) : new Uint8Array(row.content); return MCard.fromData(content, row.hash, row.g_time); }); return { items, totalItems: items.length, pageNumber: 1, pageSize: items.length || 1, totalPages: 1, hasNext: false, hasPrevious: false }; } // =========== Handle Operations =========== async registerHandle(handle, hash) { this.registerHandleSync(handle, hash); } /** * Register a handle synchronously */ registerHandleSync(handle, hash) { const normalized = validateHandle(handle); const now = (/* @__PURE__ */ new Date()).toISOString(); const checkStmt = this.db.prepare("SELECT handle FROM handle_registry WHERE handle = ?"); const existing = checkStmt.get(normalized); if (existing) { throw new Error(`Handle '${handle}' already exists.`); } const stmt = this.db.prepare( "INSERT INTO handle_registry (handle, current_hash, created_at, updated_at) VALUES (?, ?, ?, ?)" ); stmt.run(normalized, hash, now, now); } async resolveHandle(handle) { return this.resolveHandleSync(handle); } /** * Resolve a handle synchronously */ resolveHandleSync(handle) { const normalized = validateHandle(handle); const stmt = this.db.prepare("SELECT current_hash FROM handle_registry WHERE handle = ?"); const row = stmt.get(normalized); return row?.current_hash || null; } async getByHandle(handle) { return this.getByHandleSync(handle); } /** * Get card by handle synchronously */ getByHandleSync(handle) { const hash = this.resolveHandleSync(handle); if (!hash) return null; return this.getSync(hash); } async updateHandle(handle, newHash) { return this.updateHandleSync(handle, newHash); } /** * Update a handle synchronously */ updateHandleSync(handle, newHash) { const normalized = validateHandle(handle); const now = (/* @__PURE__ */ new Date()).toISOString(); const getStmt = this.db.prepare("SELECT current_hash FROM handle_registry WHERE handle = ?"); const row = getStmt.get(normalized); if (!row) { throw new Error(`Handle '${handle}' not found.`); } const previousHash = row.current_hash; const historyStmt = this.db.prepare( "INSERT INTO handle_history (handle, previous_hash, changed_at) VALUES (?, ?, ?)" ); historyStmt.run(normalized, previousHash, now); const updateStmt = this.db.prepare( "UPDATE handle_registry SET current_hash = ?, updated_at = ? WHERE handle = ?" ); updateStmt.run(newHash, now, normalized); return previousHash; } async getHandleHistory(handle) { return this.getHandleHistorySync(handle); } /** * Get handle history synchronously */ getHandleHistorySync(handle) { const normalized = validateHandle(handle); const stmt = this.db.prepare( "SELECT previous_hash, changed_at FROM handle_history WHERE handle = ? ORDER BY id DESC" ); const rows = stmt.all(normalized); return rows.map((row) => ({ previousHash: row.previous_hash, changedAt: row.changed_at })); } async pruneHandleHistory(handle, options = {}) { return this.pruneHandleHistorySync(handle, options); } /** * Prune handle history synchronously */ pruneHandleHistorySync(handle, options = {}) { const normalized = validateHandle(handle); let stmt; if (options.deleteAll) { stmt = this.db.prepare("DELETE FROM handle_history WHERE handle = ?"); const result = stmt.run(normalized); return result.changes; } else if (options.olderThan) { stmt = this.db.prepare("DELETE FROM handle_history WHERE handle = ? AND changed_at < ?"); const result = stmt.run(normalized, options.olderThan); return result.changes; } return 0; } }; export { SqliteNodeEngine };