UNPKG

@fluxgraph/knowledge

Version:

A flexible, database-agnostic knowledge graph implementation for TypeScript

1,302 lines (1,295 loc) 55.6 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/adapters/index.ts var adapters_exports = {}; __export(adapters_exports, { BaseAdapter: () => BaseAdapter, D1Adapter: () => D1Adapter, SQLiteAdapter: () => SQLiteAdapter, SqlStorageAdapter: () => SqlStorageAdapter, createAdapter: () => createAdapter }); module.exports = __toCommonJS(adapters_exports); // src/adapters/base.ts var BaseAdapter = class { config; tablePrefix; constructor(config = {}) { this.config = config; this.tablePrefix = config.tablePrefix || ""; } getTableName(table) { return this.tablePrefix ? `${this.tablePrefix}_${table}` : table; } log(_message, ..._args) { } error(message, error) { console.error(`[KnowledgeGraph Error] ${message}`, error); } }; // src/adapters/sqlite.ts var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1); var import_better_sqlite32 = require("drizzle-orm/better-sqlite3"); var import_drizzle_orm = require("drizzle-orm"); // src/schema/index.ts var import_sqlite_core = require("drizzle-orm/sqlite-core"); var nodes = (0, import_sqlite_core.sqliteTable)("kg_nodes", { id: (0, import_sqlite_core.text)("id").primaryKey(), type: (0, import_sqlite_core.text)("type").notNull(), label: (0, import_sqlite_core.text)("label").notNull(), properties: (0, import_sqlite_core.text)("properties").notNull(), // JSON string confidence: (0, import_sqlite_core.real)("confidence").notNull().default(1), createdAt: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), updatedAt: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), sourceSessionIds: (0, import_sqlite_core.text)("source_session_ids") // JSON array of session IDs }, (table) => ({ typeIdx: (0, import_sqlite_core.index)("idx_nodes_type").on(table.type), labelIdx: (0, import_sqlite_core.index)("idx_nodes_label").on(table.label), createdAtIdx: (0, import_sqlite_core.index)("idx_nodes_created_at").on(table.createdAt) })); var edges = (0, import_sqlite_core.sqliteTable)("kg_edges", { id: (0, import_sqlite_core.text)("id").primaryKey(), type: (0, import_sqlite_core.text)("type").notNull(), fromNodeId: (0, import_sqlite_core.text)("from_node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), toNodeId: (0, import_sqlite_core.text)("to_node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), properties: (0, import_sqlite_core.text)("properties").notNull().default("{}"), // JSON string confidence: (0, import_sqlite_core.real)("confidence").notNull().default(1), createdAt: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()), sourceSessionIds: (0, import_sqlite_core.text)("source_session_ids") // JSON array of session IDs }, (table) => ({ typeIdx: (0, import_sqlite_core.index)("idx_edges_type").on(table.type), fromNodeIdx: (0, import_sqlite_core.index)("idx_edges_from_node").on(table.fromNodeId), toNodeIdx: (0, import_sqlite_core.index)("idx_edges_to_node").on(table.toNodeId), fromTypeIdx: (0, import_sqlite_core.index)("idx_edges_from_type").on(table.fromNodeId, table.type), toTypeIdx: (0, import_sqlite_core.index)("idx_edges_to_type").on(table.toNodeId, table.type) })); var nodeIndices = (0, import_sqlite_core.sqliteTable)("kg_node_indices", { indexKey: (0, import_sqlite_core.text)("index_key").notNull(), nodeId: (0, import_sqlite_core.text)("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), createdAt: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }, (table) => ({ pk: (0, import_sqlite_core.primaryKey)({ columns: [table.indexKey, table.nodeId] }), keyIdx: (0, import_sqlite_core.index)("idx_node_indices_key").on(table.indexKey), nodeIdx: (0, import_sqlite_core.index)("idx_node_indices_node").on(table.nodeId) })); var edgeIndices = (0, import_sqlite_core.sqliteTable)("kg_edge_indices", { indexKey: (0, import_sqlite_core.text)("index_key").notNull(), edgeId: (0, import_sqlite_core.text)("edge_id").notNull().references(() => edges.id, { onDelete: "cascade" }), createdAt: (0, import_sqlite_core.integer)("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }, (table) => ({ pk: (0, import_sqlite_core.primaryKey)({ columns: [table.indexKey, table.edgeId] }), keyIdx: (0, import_sqlite_core.index)("idx_edge_indices_key").on(table.indexKey), edgeIdx: (0, import_sqlite_core.index)("idx_edge_indices_edge").on(table.edgeId) })); var searchIndex = (0, import_sqlite_core.sqliteTable)("kg_search_index", { term: (0, import_sqlite_core.text)("term").notNull(), nodeId: (0, import_sqlite_core.text)("node_id").notNull().references(() => nodes.id, { onDelete: "cascade" }), field: (0, import_sqlite_core.text)("field").notNull(), // 'label', 'property:key', etc. weight: (0, import_sqlite_core.real)("weight").notNull().default(1) }, (table) => ({ pk: (0, import_sqlite_core.primaryKey)({ columns: [table.term, table.nodeId, table.field] }), termIdx: (0, import_sqlite_core.index)("idx_search_term").on(table.term), nodeIdx: (0, import_sqlite_core.index)("idx_search_node").on(table.nodeId) })); var graphMetadata = (0, import_sqlite_core.sqliteTable)("kg_graph_metadata", { key: (0, import_sqlite_core.text)("key").primaryKey(), value: (0, import_sqlite_core.text)("value").notNull(), updatedAt: (0, import_sqlite_core.integer)("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => /* @__PURE__ */ new Date()) }); // src/adapters/sqlite.ts var SQLiteAdapter = class extends BaseAdapter { db = null; drizzle = null; constructor(config = {}) { super(config); } async initialize() { try { const dbPath = this.config.connection || ":memory:"; this.db = new import_better_sqlite3.default(dbPath); this.drizzle = (0, import_better_sqlite32.drizzle)(this.db); this.db.exec("PRAGMA foreign_keys = ON"); if (this.config.autoCreate !== false) { await this.createTables(); } this.log("SQLite adapter initialized", { path: dbPath }); } catch (error) { this.error("Failed to initialize SQLite adapter", error); throw error; } } async createTables() { if (!this.db) throw new Error("Database not initialized"); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ); CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type); CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label); CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, to_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT ); CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type); CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id); CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id); CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type); CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id) ); CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key); CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL REFERENCES kg_edges(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id) ); CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key); CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field) ); CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term); CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id); `); this.db.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); `); } async execute(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query); return stmt.all(...params); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async executeUpdate(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query); return stmt.run(...params); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.db) throw new Error("Database not initialized"); return new Promise((resolve, reject) => { try { if (!this.db) throw new Error("Database not initialized"); const result = this.db.transaction(async () => { const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("Transaction rollback"); } }; return await fn(tx); })(); resolve(result); } catch (error) { reject(error); } }); } // Node operations async insertNode(node) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(nodes).values(node).returning(); const insertedNode = result[0]; if (!insertedNode) throw new Error("Failed to create node"); return this.deserializeNode(insertedNode); } async updateNode(id, updates) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.update(nodes).set({ ...updates, updatedAt: /* @__PURE__ */ new Date() }).where((0, import_drizzle_orm.eq)(nodes.id, id)).returning(); return result[0] ? this.deserializeNode(result[0]) : null; } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; const result = await this.executeUpdate(query, [id]); return result.changes > 0; } async getNode(id) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.select().from(nodes).where((0, import_drizzle_orm.eq)(nodes.id, id)).limit(1); return result[0] ? this.deserializeNode(result[0]) : null; } async getNodes(ids) { if (!this.drizzle || ids.length === 0) return []; const result = await this.drizzle.select().from(nodes).where((0, import_drizzle_orm.inArray)(nodes.id, ids)); return result.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { if (!this.drizzle) throw new Error("Database not initialized"); const whereConditions = Object.entries(conditions).map(([key, value]) => { const column = nodes[key]; return (0, import_drizzle_orm.eq)(column, value); }); const result = await this.drizzle.select().from(nodes).where((0, import_drizzle_orm.and)(...whereConditions)).limit(limit).offset(offset); return result.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(edges).values(edge).returning(); const insertedEdge = result[0]; if (!insertedEdge) throw new Error("Failed to create edge"); return this.deserializeEdge(insertedEdge); } async updateEdge(id, updates) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.update(edges).set(updates).where((0, import_drizzle_orm.eq)(edges.id, id)).returning(); return result[0] ? this.deserializeEdge(result[0]) : null; } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.select().from(edges).where((0, import_drizzle_orm.eq)(edges.id, id)).limit(1); return result[0] ? this.deserializeEdge(result[0]) : null; } async getEdges(ids) { if (!this.drizzle || ids.length === 0) return []; const result = await this.drizzle.select().from(edges).where((0, import_drizzle_orm.inArray)(edges.id, ids)); return result.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { if (!this.drizzle) throw new Error("Database not initialized"); const whereConditions = Object.entries(conditions).map(([key, value]) => { const fieldMap = { "from_node_id": "fromNodeId", "to_node_id": "toNodeId", "created_at": "createdAt", "updated_at": "updatedAt", "source_session_ids": "sourceSessionIds" }; const schemaKey = fieldMap[key] || key; const column = edges[schemaKey]; return (0, import_drizzle_orm.eq)(column, value); }); const result = await this.drizzle.select().from(edges).where((0, import_drizzle_orm.and)(...whereConditions)).limit(limit).offset(offset); return result.map((e) => this.deserializeEdge(e)); } // Index operations async insertNodeIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(nodeIndices).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create node index"); return insertedIndex; } async deleteNodeIndex(indexKey, nodeId) { const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`; const params = nodeId ? [indexKey, nodeId] : [indexKey]; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(params); return info.changes; } async getNodeIndices(indexKey) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(nodeIndices).where((0, import_drizzle_orm.eq)(nodeIndices.indexKey, indexKey)); } async insertEdgeIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(edgeIndices).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create edge index"); return insertedIndex; } async deleteEdgeIndex(indexKey, edgeId) { const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`; const params = edgeId ? [indexKey, edgeId] : [indexKey]; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(params); return info.changes; } async getEdgeIndices(indexKey) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(edgeIndices).where((0, import_drizzle_orm.eq)(edgeIndices.indexKey, indexKey)); } // Search operations async insertSearchIndex(index2) { if (!this.drizzle) throw new Error("Database not initialized"); const result = await this.drizzle.insert(searchIndex).values(index2).returning(); const insertedIndex = result[0]; if (!insertedIndex) throw new Error("Failed to create search index"); return insertedIndex; } async deleteSearchIndex(nodeId) { const query = `DELETE FROM kg_search_index WHERE node_id = ?`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run([nodeId]); return info.changes; } async searchNodes(term, limit = 50) { if (!this.drizzle) throw new Error("Database not initialized"); return await this.drizzle.select().from(searchIndex).where((0, import_drizzle_orm.like)(searchIndex.term, `%${term}%`)).limit(limit); } // Batch operations async batchInsertNodes(nodes2) { if (!this.drizzle || nodes2.length === 0) return []; const result = await this.drizzle.insert(nodes).values(nodes2).returning(); return result.map((n) => this.deserializeNode(n)); } async batchInsertEdges(edges2) { if (!this.drizzle || edges2.length === 0) return []; const result = await this.drizzle.insert(edges).values(edges2).returning(); return result.map((e) => this.deserializeEdge(e)); } async batchDeleteNodes(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(ids); return info.changes; } async batchDeleteEdges(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`; if (!this.db) throw new Error("Database not initialized"); const stmt = this.db.prepare(query); const info = stmt.run(ids); return info.changes; } // Maintenance operations async vacuum() { if (!this.db) throw new Error("Database not initialized"); this.db.exec("VACUUM"); this.log("Database vacuumed"); } async getStats() { if (!this.db) throw new Error("Database not initialized"); const nodeCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_nodes").get(); const edgeCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_edges").get(); const indexCount = this.db.prepare("SELECT COUNT(*) as count FROM kg_node_indices").get(); return { nodeCount: nodeCount.count, edgeCount: edgeCount.count, indexCount: indexCount.count }; } async close() { if (this.db) { this.db.close(); this.db = null; this.drizzle = null; this.log("Database connection closed"); } } // Helper methods deserializeNode(node) { return { ...node, properties: JSON.parse(node.properties), sourceSessionIds: node.sourceSessionIds ? JSON.parse(node.sourceSessionIds) : void 0 }; } deserializeEdge(edge) { return { ...edge, properties: JSON.parse(edge.properties), sourceSessionIds: edge.sourceSessionIds ? JSON.parse(edge.sourceSessionIds) : void 0 }; } }; // src/adapters/d1.ts var D1Adapter = class extends BaseAdapter { db = null; constructor(config) { super(config); if (config.database) { this.db = config.database; } } setDatabase(db) { this.db = db; } async initialize() { if (!this.db) { throw new Error("D1 database not provided. Use setDatabase() or pass it in config."); } if (this.config.autoCreate !== false) { await this.createTables(); } this.log("D1 adapter initialized"); } async createTables() { if (!this.db) throw new Error("Database not initialized"); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL, to_node_id TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT, FOREIGN KEY (from_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE, FOREIGN KEY (to_node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id), FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id), FOREIGN KEY (edge_id) REFERENCES kg_edges(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field), FOREIGN KEY (node_id) REFERENCES kg_nodes(id) ON DELETE CASCADE ); `); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term);`); await this.db.exec(`CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id);`); await this.db.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ); `); } async execute(query, params = []) { if (!this.db) throw new Error("Database not initialized"); try { const stmt = this.db.prepare(query).bind(...params); const result = await stmt.all(); return result.results; } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.db) throw new Error("Database not initialized"); const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("Transaction rollback"); } }; try { return await fn(tx); } catch (error) { this.error("Transaction failed", error); throw error; } } // Node operations async insertNode(node) { const id = node.id || crypto.randomUUID(); const now = Date.now(); const query = ` INSERT INTO kg_nodes (id, type, label, properties, confidence, created_at, updated_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, node.type, node.label, JSON.stringify(node.properties || {}), node.confidence || 1, node.createdAt?.getTime() || now, node.updatedAt?.getTime() || now, node.sourceSessionIds ? JSON.stringify(node.sourceSessionIds) : null ]); return this.getNode(id); } async updateNode(id, updates) { const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.label !== void 0) { setClauses.push("label = ?"); params.push(updates.label); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } setClauses.push("updated_at = ?"); params.push(Date.now()); params.push(id); const query = `UPDATE kg_nodes SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getNode(id); } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; await this.execute(query, [id]); return true; } async getNode(id) { const query = `SELECT * FROM kg_nodes WHERE id = ? LIMIT 1`; const results = await this.execute(query, [id]); if (results.length === 0) return null; return this.deserializeNode(results[0]); } async getNodes(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_nodes WHERE id IN (${placeholders})`; const results = await this.execute(query, ids); return results.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; for (const [key, value] of Object.entries(conditions)) { whereClauses.push(`${key} = ?`); params.push(value); } params.push(limit, offset); const query = ` SELECT * FROM kg_nodes ${whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : ""} LIMIT ? OFFSET ? `; const results = await this.execute(query, params); return results.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { const id = edge.id || crypto.randomUUID(); const now = Date.now(); const query = ` INSERT INTO kg_edges (id, type, from_node_id, to_node_id, properties, confidence, created_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, edge.type, edge.fromNodeId, edge.toNodeId, JSON.stringify(edge.properties || {}), edge.confidence || 1, edge.createdAt?.getTime() || now, edge.sourceSessionIds ? JSON.stringify(edge.sourceSessionIds) : null ]); return this.getEdge(id); } async updateEdge(id, updates) { const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.fromNodeId !== void 0) { setClauses.push("from_node_id = ?"); params.push(updates.fromNodeId); } if (updates.toNodeId !== void 0) { setClauses.push("to_node_id = ?"); params.push(updates.toNodeId); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } params.push(id); const query = `UPDATE kg_edges SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getEdge(id); } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { const query = `SELECT * FROM kg_edges WHERE id = ? LIMIT 1`; const results = await this.execute(query, [id]); if (results.length === 0) return null; return this.deserializeEdge(results[0]); } async getEdges(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_edges WHERE id IN (${placeholders})`; const results = await this.execute(query, ids); return results.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; const fieldMap = { "fromNodeId": "from_node_id", "toNodeId": "to_node_id", "from_node_id": "from_node_id", "to_node_id": "to_node_id", "createdAt": "created_at", "updatedAt": "updated_at", "sourceSessionIds": "source_session_ids" }; for (const [key, value] of Object.entries(conditions)) { const dbKey = fieldMap[key] || key; whereClauses.push(`${dbKey} = ?`); params.push(value); } params.push(limit, offset); const query = ` SELECT * FROM kg_edges ${whereClauses.length > 0 ? "WHERE " + whereClauses.join(" AND ") : ""} LIMIT ? OFFSET ? `; const results = await this.execute(query, params); return results.map((e) => this.deserializeEdge(e)); } // Index operations async insertNodeIndex(index2) { const query = ` INSERT INTO kg_node_indices (index_key, node_id, created_at) VALUES (?, ?, ?) `; await this.execute(query, [ index2.indexKey, index2.nodeId, index2.createdAt?.getTime() || Date.now() ]); return index2; } async deleteNodeIndex(indexKey, nodeId) { const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`; const params = nodeId ? [indexKey, nodeId] : [indexKey]; await this.execute(query, params); return 1; } async getNodeIndices(indexKey) { const query = `SELECT * FROM kg_node_indices WHERE index_key = ?`; const results = await this.execute(query, [indexKey]); return results.map((r) => this.deserializeNodeIndex(r)); } async insertEdgeIndex(index2) { const query = ` INSERT INTO kg_edge_indices (index_key, edge_id, created_at) VALUES (?, ?, ?) `; await this.execute(query, [ index2.indexKey, index2.edgeId, index2.createdAt?.getTime() || Date.now() ]); return index2; } async deleteEdgeIndex(indexKey, edgeId) { const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`; const params = edgeId ? [indexKey, edgeId] : [indexKey]; await this.execute(query, params); return 1; } async getEdgeIndices(indexKey) { const query = `SELECT * FROM kg_edge_indices WHERE index_key = ?`; const results = await this.execute(query, [indexKey]); return results.map((r) => this.deserializeEdgeIndex(r)); } // Search operations async insertSearchIndex(index2) { const query = ` INSERT OR REPLACE INTO kg_search_index (term, node_id, field, weight) VALUES (?, ?, ?, ?) `; await this.execute(query, [ index2.term, index2.nodeId, index2.field, index2.weight || 1 ]); return index2; } async deleteSearchIndex(nodeId) { const query = `DELETE FROM kg_search_index WHERE node_id = ?`; await this.execute(query, [nodeId]); return 1; } async searchNodes(term, limit = 50) { const query = ` SELECT * FROM kg_search_index WHERE term LIKE ? ORDER BY weight DESC LIMIT ? `; const results = await this.execute(query, [`%${term}%`, limit]); return results.map((r) => this.deserializeSearchIndex(r)); } // Batch operations async batchInsertNodes(nodes2) { const insertedNodes = []; for (const node of nodes2) { const inserted = await this.insertNode(node); insertedNodes.push(inserted); } return insertedNodes; } async batchInsertEdges(edges2) { const insertedEdges = []; for (const edge of edges2) { const inserted = await this.insertEdge(edge); insertedEdges.push(inserted); } return insertedEdges; } async batchDeleteNodes(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`; await this.execute(query, ids); return ids.length; } async batchDeleteEdges(ids) { if (ids.length === 0) return 0; const placeholders = ids.map(() => "?").join(","); const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`; await this.execute(query, ids); return ids.length; } // Maintenance operations async vacuum() { this.log("Vacuum not needed for D1 (handled automatically)"); } async getStats() { const nodeCount = await this.execute("SELECT COUNT(*) as count FROM kg_nodes"); const edgeCount = await this.execute("SELECT COUNT(*) as count FROM kg_edges"); const indexCount = await this.execute("SELECT COUNT(*) as count FROM kg_node_indices"); return { nodeCount: nodeCount[0]?.count || 0, edgeCount: edgeCount[0]?.count || 0, indexCount: indexCount[0]?.count || 0 }; } async close() { this.db = null; this.log("D1 adapter closed"); } // Helper methods deserializeNode(row) { return { id: row.id, type: row.type, label: row.label, properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties || {}, confidence: row.confidence, createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at), sourceSessionIds: row.source_session_ids ? JSON.parse(row.source_session_ids) : void 0 }; } deserializeEdge(row) { return { id: row.id, type: row.type, fromNodeId: row.from_node_id, toNodeId: row.to_node_id, properties: typeof row.properties === "string" ? JSON.parse(row.properties) : row.properties || {}, confidence: row.confidence, createdAt: new Date(row.created_at), sourceSessionIds: row.source_session_ids ? JSON.parse(row.source_session_ids) : void 0 }; } deserializeNodeIndex(row) { return { indexKey: row.index_key, nodeId: row.node_id, createdAt: new Date(row.created_at) }; } deserializeEdgeIndex(row) { return { indexKey: row.index_key, edgeId: row.edge_id, createdAt: new Date(row.created_at) }; } deserializeSearchIndex(row) { return { term: row.term, nodeId: row.node_id, field: row.field, weight: row.weight }; } }; // src/adapters/sql-storage.ts var SqlStorageAdapter = class extends BaseAdapter { sql = null; constructor(config = {}) { super(config); } /** * Set the SqlStorage instance * Must be called before using the adapter */ setSqlStorage(sql) { this.sql = sql; } async initialize() { if (!this.sql) { throw new Error("SqlStorage not set. Call setSqlStorage() first."); } try { if (this.config.autoCreate !== false) { await this.createTables(); } this.log("SqlStorage adapter initialized"); } catch (error) { this.error("Failed to initialize SqlStorage adapter", error); throw error; } } async createTables() { if (!this.sql) throw new Error("SqlStorage not initialized"); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_nodes ( id TEXT PRIMARY KEY, type TEXT NOT NULL, label TEXT NOT NULL, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session_ids TEXT ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_edges ( id TEXT PRIMARY KEY, type TEXT NOT NULL, from_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, to_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, properties TEXT NOT NULL DEFAULT '{}', confidence REAL NOT NULL DEFAULT 1.0, created_at INTEGER NOT NULL, source_session_ids TEXT ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_node_indices ( index_key TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, node_id) ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_edge_indices ( index_key TEXT NOT NULL, edge_id TEXT NOT NULL REFERENCES kg_edges(id) ON DELETE CASCADE, created_at INTEGER NOT NULL, PRIMARY KEY (index_key, edge_id) ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_search_index ( term TEXT NOT NULL, node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE, field TEXT NOT NULL, weight REAL NOT NULL DEFAULT 1.0, PRIMARY KEY (term, node_id, field) ) `); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term)`); this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id)`); this.sql.exec(` CREATE TABLE IF NOT EXISTS kg_graph_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at INTEGER NOT NULL ) `); } async execute(query, params = []) { if (!this.sql) throw new Error("SqlStorage not initialized"); try { const result = this.sql.exec(query, ...params); return result.toArray(); } catch (error) { this.error("Query execution failed", { query, params, error }); throw error; } } async transaction(fn) { if (!this.sql) throw new Error("SqlStorage not initialized"); const tx = { execute: async (query, params = []) => { return this.execute(query, params); }, rollback: async () => { throw new Error("SqlStorage does not support transaction rollback"); } }; try { return await fn(tx); } catch (error) { this.error("Transaction failed", error); throw error; } } // Node operations async insertNode(node) { const now = Date.now(); const id = node.id || this.generateId(); const properties = JSON.stringify(node.properties || {}); const sourceSessionIds = node.sourceSessionIds ? JSON.stringify(node.sourceSessionIds) : null; const query = ` INSERT INTO kg_nodes (id, type, label, properties, confidence, created_at, updated_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, node.type, node.label, properties, node.confidence || 1, node.createdAt?.getTime() || now, node.updatedAt?.getTime() || now, sourceSessionIds ]); const result = await this.getNode(id); if (!result) throw new Error("Failed to create node"); return result; } async updateNode(id, updates) { const current = await this.getNode(id); if (!current) return null; const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.label !== void 0) { setClauses.push("label = ?"); params.push(updates.label); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } setClauses.push("updated_at = ?"); params.push(Date.now()); params.push(id); const query = `UPDATE kg_nodes SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getNode(id); } async deleteNode(id) { const query = `DELETE FROM kg_nodes WHERE id = ?`; const result = await this.execute(query, [id]); return Array.isArray(result) || result !== void 0; } async getNode(id) { const query = `SELECT * FROM kg_nodes WHERE id = ? LIMIT 1`; const result = await this.execute(query, [id]); if (!result || result.length === 0) return null; return this.deserializeNode(result[0]); } async getNodes(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_nodes WHERE id IN (${placeholders})`; const result = await this.execute(query, ids); return result.map((n) => this.deserializeNode(n)); } async queryNodes(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; Object.entries(conditions).forEach(([key, value]) => { whereClauses.push(`${key} = ?`); params.push(value); }); params.push(limit, offset); const query = ` SELECT * FROM kg_nodes ${whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : ""} LIMIT ? OFFSET ? `; const result = await this.execute(query, params); return result.map((n) => this.deserializeNode(n)); } // Edge operations async insertEdge(edge) { const now = Date.now(); const id = edge.id || this.generateId(); const properties = JSON.stringify(edge.properties || {}); const sourceSessionIds = edge.sourceSessionIds ? JSON.stringify(edge.sourceSessionIds) : null; const query = ` INSERT INTO kg_edges (id, type, from_node_id, to_node_id, properties, confidence, created_at, source_session_ids) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; await this.execute(query, [ id, edge.type, edge.fromNodeId, edge.toNodeId, properties, edge.confidence || 1, edge.createdAt?.getTime() || now, sourceSessionIds ]); const result = await this.getEdge(id); if (!result) throw new Error("Failed to create edge"); return result; } async updateEdge(id, updates) { const current = await this.getEdge(id); if (!current) return null; const setClauses = []; const params = []; if (updates.type !== void 0) { setClauses.push("type = ?"); params.push(updates.type); } if (updates.fromNodeId !== void 0) { setClauses.push("from_node_id = ?"); params.push(updates.fromNodeId); } if (updates.toNodeId !== void 0) { setClauses.push("to_node_id = ?"); params.push(updates.toNodeId); } if (updates.properties !== void 0) { setClauses.push("properties = ?"); params.push(JSON.stringify(updates.properties)); } if (updates.confidence !== void 0) { setClauses.push("confidence = ?"); params.push(updates.confidence); } if (updates.sourceSessionIds !== void 0) { setClauses.push("source_session_ids = ?"); params.push(JSON.stringify(updates.sourceSessionIds)); } params.push(id); const query = `UPDATE kg_edges SET ${setClauses.join(", ")} WHERE id = ?`; await this.execute(query, params); return this.getEdge(id); } async deleteEdge(id) { const query = `DELETE FROM kg_edges WHERE id = ?`; await this.execute(query, [id]); return true; } async getEdge(id) { const query = `SELECT * FROM kg_edges WHERE id = ? LIMIT 1`; const result = await this.execute(query, [id]); if (!result || result.length === 0) return null; return this.deserializeEdge(result[0]); } async getEdges(ids) { if (ids.length === 0) return []; const placeholders = ids.map(() => "?").join(","); const query = `SELECT * FROM kg_edges WHERE id IN (${placeholders})`; const result = await this.execute(query, ids); return result.map((e) => this.deserializeEdge(e)); } async queryEdges(conditions, limit = 100, offset = 0) { const whereClauses = []; const params = []; Object.entries(conditions).forEach(([key, value]) => { const dbKey = key === "fromNodeId" ? "from_node_id" : key === "toNodeId" ? "to_node_id" : key; whereClauses.push(`${dbKey} = ?`); params.push(value); }); params.push(limit, offset); const query = ` SELECT * FROM kg_edges ${whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : ""} LIMIT ? OFFSET ? `; const result = await this.execute(query, params); return result.map((e) => this.deserializeEdge(e)); } // Index operations async insertNodeIndex(index2) { const now = Date.now(); const query = ` INSERT INTO kg_node_indices (index_key, node_id, created_at) VALUES (?, ?, ?) `; await this.execute(query, [index2.indexKey, index2.nodeId, index2.createdAt?.getTime() || now]); return { ...index2, createdAt: index2.createdAt || new Date(now) }; } async deleteNodeIndex(indexKey, nodeId) { const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`; const params = nodeId ? [indexKey, nodeId] : [indexKey]; const result = await this.execute(query, params); return Array.isArray(result) ? result.length : 0; } async getNodeIndices(indexKey) { const query = `SELECT * FROM kg_node_indices WHERE index_key = ?`; const result = await this.execute(query, [indexKey]); return result.map((idx) => ({ indexKey: idx.index_key, nodeId: idx.node_id, createdAt: new Date(idx.created_at) })); } async insertEdgeIndex(i