@fluxgraph/knowledge
Version:
A flexible, database-agnostic knowledge graph implementation for TypeScript
1 lines • 190 kB
Source Map (JSON)
{"version":3,"sources":["../src/core/KnowledgeGraph.ts","../src/types/index.ts","../src/adapters/base.ts","../src/adapters/sqlite.ts","../src/schema/index.ts","../src/adapters/d1.ts","../src/adapters/sql-storage.ts","../src/adapters/index.ts","../src/extraction/index.ts","../src/visualization/index.ts"],"sourcesContent":["import { DatabaseAdapter } from '../adapters/base';\nimport {\n KnowledgeNode,\n KnowledgeEdge,\n NodeType,\n EdgeType,\n QueryResult,\n QueryOptions,\n NodeOptions,\n EdgeOptions,\n TraversalOptions,\n Path,\n GraphStats,\n SearchOptions,\n BatchResult,\n} from '../types';\nimport type { NewNode, NewEdge } from '../schema';\n\n/**\n * Main KnowledgeGraph class that provides high-level graph operations\n */\nexport class KnowledgeGraph<TNodeType extends string = string> {\n private adapter: DatabaseAdapter;\n private initialized = false;\n\n constructor(adapter: DatabaseAdapter) {\n this.adapter = adapter;\n }\n\n /**\n * Initialize the knowledge graph\n */\n async initialize(): Promise<void> {\n if (this.initialized) return;\n\n await this.adapter.initialize();\n this.initialized = true;\n }\n\n /**\n * Ensure the graph is initialized\n */\n private async ensureInitialized(): Promise<void> {\n if (!this.initialized) {\n await this.initialize();\n }\n }\n\n // ============ Node Operations ============\n\n /**\n * Add a new node to the graph\n */\n async addNode(options: NodeOptions<TNodeType>): Promise<KnowledgeNode> {\n await this.ensureInitialized();\n\n const id = crypto.randomUUID();\n const now = new Date();\n\n const newNode: NewNode = {\n id,\n type: options.type,\n label: options.label,\n properties: JSON.stringify(options.properties || {}),\n confidence: options.confidence || 1.0,\n createdAt: now,\n updatedAt: now,\n sourceSessionIds: options.sourceSessionId ? JSON.stringify([options.sourceSessionId]) : undefined,\n };\n\n const node = await this.adapter.insertNode(newNode);\n\n // Add to search index\n await this.indexNodeForSearch(node);\n\n // Add to type index\n await this.adapter.insertNodeIndex({\n indexKey: `type:${node.type}`,\n nodeId: node.id,\n createdAt: now,\n });\n\n return this.normalizeNode(node);\n }\n\n /**\n * Update an existing node\n */\n async updateNode(nodeId: string, updates: Partial<NodeOptions<TNodeType>>, mergeProperties = true): Promise<KnowledgeNode | null> {\n await this.ensureInitialized();\n\n const existingNode = await this.adapter.getNode(nodeId);\n if (!existingNode) return null;\n\n const existingProperties = existingNode.properties as unknown as Record<string, any>;\n const properties = mergeProperties && updates.properties ? { ...existingProperties, ...updates.properties } : updates.properties || existingProperties;\n\n const nodeUpdates: Partial<NewNode> = {\n type: updates.type,\n label: updates.label,\n properties: JSON.stringify(properties),\n confidence: updates.confidence,\n updatedAt: new Date(),\n };\n\n if (updates.sourceSessionId) {\n const existingSessionIds = Array.isArray(existingNode.sourceSessionIds) ? (existingNode.sourceSessionIds as string[]) : [];\n if (!existingSessionIds.includes(updates.sourceSessionId)) {\n existingSessionIds.push(updates.sourceSessionId);\n nodeUpdates.sourceSessionIds = JSON.stringify(existingSessionIds);\n }\n }\n\n const updatedNode = await this.adapter.updateNode(nodeId, nodeUpdates);\n if (!updatedNode) return null;\n\n // Update search index\n await this.adapter.deleteSearchIndex(nodeId);\n await this.indexNodeForSearch(updatedNode);\n\n return this.normalizeNode(updatedNode);\n }\n\n /**\n * Delete a node and all its edges\n */\n async deleteNode(nodeId: string): Promise<boolean> {\n await this.ensureInitialized();\n\n // Edges are cascade deleted via foreign key constraints\n return await this.adapter.deleteNode(nodeId);\n }\n\n /**\n * Get a node by ID\n */\n async getNode(nodeId: string): Promise<KnowledgeNode | null> {\n await this.ensureInitialized();\n\n const node = await this.adapter.getNode(nodeId);\n return node ? this.normalizeNode(node) : null;\n }\n\n /**\n * Find nodes by label (exact or partial match)\n */\n async findNodesByLabel(label: string, exact = false): Promise<KnowledgeNode[]> {\n await this.ensureInitialized();\n\n if (exact) {\n const nodes = await this.adapter.queryNodes({ label }, 100);\n return nodes.map((n) => this.normalizeNode(n));\n }\n\n // Use search index for partial matching\n const searchResults = await this.adapter.searchNodes(label.toLowerCase());\n const nodeIds = [...new Set(searchResults.map((r) => r.nodeId))];\n\n if (nodeIds.length === 0) return [];\n\n const nodes = await this.adapter.getNodes(nodeIds);\n return nodes.map((n) => this.normalizeNode(n));\n }\n\n // ============ Edge Operations ============\n\n /**\n * Add an edge between two nodes\n */\n async addEdge(options: EdgeOptions): Promise<KnowledgeEdge> {\n await this.ensureInitialized();\n\n // Verify both nodes exist\n const [fromNode, toNode] = await Promise.all([this.adapter.getNode(options.fromNodeId), this.adapter.getNode(options.toNodeId)]);\n\n if (!fromNode) {\n throw new Error(`From node ${options.fromNodeId} does not exist`);\n }\n if (!toNode) {\n throw new Error(`To node ${options.toNodeId} does not exist`);\n }\n\n const id = crypto.randomUUID();\n const now = new Date();\n\n const newEdge: NewEdge = {\n id,\n type: options.type,\n fromNodeId: options.fromNodeId,\n toNodeId: options.toNodeId,\n properties: JSON.stringify(options.properties || {}),\n confidence: options.confidence || 1.0,\n createdAt: now,\n sourceSessionIds: options.sourceSessionId ? JSON.stringify([options.sourceSessionId]) : undefined,\n };\n\n const edge = await this.adapter.insertEdge(newEdge);\n\n // Add to indices\n await Promise.all([\n this.adapter.insertEdgeIndex({\n indexKey: `from:${options.fromNodeId}:${options.type}`,\n edgeId: id,\n createdAt: now,\n }),\n this.adapter.insertEdgeIndex({\n indexKey: `to:${options.toNodeId}:${options.type}`,\n edgeId: id,\n createdAt: now,\n }),\n this.adapter.insertEdgeIndex({\n indexKey: `type:${options.type}`,\n edgeId: id,\n createdAt: now,\n }),\n ]);\n\n // Add bidirectional edge if requested\n if (options.bidirectional) {\n await this.addEdge({\n ...options,\n fromNodeId: options.toNodeId,\n toNodeId: options.fromNodeId,\n bidirectional: false, // Prevent infinite recursion\n });\n }\n\n return this.normalizeEdge(edge);\n }\n\n /**\n * Delete an edge\n */\n async deleteEdge(edgeId: string): Promise<boolean> {\n await this.ensureInitialized();\n return await this.adapter.deleteEdge(edgeId);\n }\n\n /**\n * Get edges between two nodes\n */\n async getEdgesBetween(fromNodeId: string, toNodeId: string, edgeType?: EdgeType | string): Promise<KnowledgeEdge[]> {\n await this.ensureInitialized();\n\n const conditions: Record<string, any> = {\n fromNodeId: fromNodeId,\n toNodeId: toNodeId,\n };\n\n if (edgeType) {\n conditions.type = edgeType;\n }\n\n const edges = await this.adapter.queryEdges(conditions);\n return edges.map((e) => this.normalizeEdge(e));\n }\n\n // ============ Query Operations ============\n\n /**\n * Query nodes by type\n */\n async queryByType(nodeType: NodeType | string, options?: QueryOptions): Promise<QueryResult> {\n await this.ensureInitialized();\n\n const nodes = await this.adapter.queryNodes({ type: nodeType }, options?.limit || 100, options?.offset || 0);\n\n const edges: KnowledgeEdge[] = [];\n\n if (options?.includeEdges && nodes.length > 0) {\n const nodeIds = nodes.map((n) => n.id);\n const edgeResults = await Promise.all([this.adapter.queryEdges({ fromNodeId: nodeIds[0] }), this.adapter.queryEdges({ toNodeId: nodeIds[0] })]);\n\n edges.push(...edgeResults.flat().map((e) => this.normalizeEdge(e)));\n }\n\n return {\n nodes: nodes.map((n) => this.normalizeNode(n)),\n edges,\n relevanceScore: 1.0,\n };\n }\n\n /**\n * Query related nodes starting from a given node\n */\n async queryRelated(nodeId: string, options?: QueryOptions): Promise<QueryResult> {\n await this.ensureInitialized();\n\n const startNode = await this.adapter.getNode(nodeId);\n if (!startNode) {\n return { nodes: [], edges: [], relevanceScore: 0 };\n }\n\n const visitedNodes = new Map<string, KnowledgeNode>();\n const visitedEdges = new Map<string, KnowledgeEdge>();\n\n visitedNodes.set(nodeId, this.normalizeNode(startNode));\n\n await this.traverseGraph(nodeId, options?.depth || 1, options?.direction || 'both', options?.edgeTypes, visitedNodes, visitedEdges);\n\n return {\n nodes: Array.from(visitedNodes.values()),\n edges: Array.from(visitedEdges.values()),\n relevanceScore: this.calculateRelevance(visitedNodes.size, visitedEdges.size),\n };\n }\n\n /**\n * Search nodes using text query\n */\n async search(options: SearchOptions): Promise<QueryResult> {\n await this.ensureInitialized();\n\n const searchTerms = options.query.toLowerCase().split(/\\s+/);\n const nodeScores = new Map<string, number>();\n\n // Search for each term\n for (const term of searchTerms) {\n const results = await this.adapter.searchNodes(term, options.limit || 50);\n\n for (const result of results) {\n const currentScore = nodeScores.get(result.nodeId) || 0;\n nodeScores.set(result.nodeId, currentScore + result.weight);\n }\n }\n\n // Filter by minimum score if specified\n const minScore = options.minScore || 0;\n const qualifiedNodeIds = Array.from(nodeScores.entries())\n .filter(([_, score]) => score >= minScore)\n .sort((a, b) => b[1] - a[1])\n .slice(0, options.limit || 50)\n .map(([id]) => id);\n\n if (qualifiedNodeIds.length === 0) {\n return { nodes: [], edges: [], relevanceScore: 0 };\n }\n\n const nodes = await this.adapter.getNodes(qualifiedNodeIds);\n\n // Filter by node types if specified\n const filteredNodes = options.nodeTypes ? nodes.filter((n) => options.nodeTypes!.includes(n.type)) : nodes;\n\n return {\n nodes: filteredNodes.map((n) => this.normalizeNode(n)),\n edges: [],\n relevanceScore: qualifiedNodeIds[0] ? nodeScores.get(qualifiedNodeIds[0]) || 0 : 0,\n };\n }\n\n // ============ Graph Traversal ============\n\n /**\n * Traverse the graph from a starting node\n */\n async traverse(options: TraversalOptions): Promise<QueryResult> {\n await this.ensureInitialized();\n\n const visitedNodes = new Map<string, KnowledgeNode>();\n const visitedEdges = new Map<string, KnowledgeEdge>();\n const queue: Array<{ nodeId: string; depth: number }> = [{ nodeId: options.startNodeId, depth: 0 }];\n const visited = new Set<string>();\n\n while (queue.length > 0) {\n const { nodeId, depth } = queue.shift()!;\n\n if (visited.has(nodeId) && options.visitOnce !== false) continue;\n if (depth > (options.maxDepth || Infinity)) continue;\n\n visited.add(nodeId);\n\n const node = await this.adapter.getNode(nodeId);\n if (!node) continue;\n\n const normalizedNode = this.normalizeNode(node);\n\n if (!options.nodeFilter || options.nodeFilter(normalizedNode)) {\n visitedNodes.set(nodeId, normalizedNode);\n }\n\n // Get edges based on direction\n const edges: KnowledgeEdge[] = [];\n\n if (options.direction === 'out' || options.direction === 'both') {\n const outEdges = await this.adapter.queryEdges({ from_node_id: nodeId });\n edges.push(...outEdges.map((e) => this.normalizeEdge(e)));\n }\n\n if (options.direction === 'in' || options.direction === 'both') {\n const inEdges = await this.adapter.queryEdges({ to_node_id: nodeId });\n edges.push(...inEdges.map((e) => this.normalizeEdge(e)));\n }\n\n for (const edge of edges) {\n if (options.edgeTypes && !options.edgeTypes.includes(edge.type)) continue;\n if (options.edgeFilter && !options.edgeFilter(edge)) continue;\n\n visitedEdges.set(edge.id, edge);\n\n const nextNodeId = edge.fromNodeId === nodeId ? edge.toNodeId : edge.fromNodeId;\n if (!visited.has(nextNodeId) || options.visitOnce === false) {\n queue.push({ nodeId: nextNodeId, depth: depth + 1 });\n }\n }\n }\n\n return {\n nodes: Array.from(visitedNodes.values()),\n edges: Array.from(visitedEdges.values()),\n relevanceScore: 1.0,\n };\n }\n\n /**\n * Find shortest path between two nodes\n */\n async findShortestPath(fromNodeId: string, toNodeId: string, options?: { edgeTypes?: (EdgeType | string)[] }): Promise<Path | null> {\n await this.ensureInitialized();\n\n const queue: Array<{ nodeId: string; path: string[]; edges: string[] }> = [{ nodeId: fromNodeId, path: [fromNodeId], edges: [] }];\n const visited = new Set<string>();\n\n while (queue.length > 0) {\n const { nodeId, path, edges } = queue.shift()!;\n\n if (nodeId === toNodeId) {\n // Found the target node\n const nodes = await this.adapter.getNodes(path);\n const edgeObjects = edges.length > 0 ? await this.adapter.getEdges(edges) : [];\n\n return {\n nodes: nodes.map((n) => this.normalizeNode(n)),\n edges: edgeObjects.map((e) => this.normalizeEdge(e)),\n length: path.length - 1,\n };\n }\n\n if (visited.has(nodeId)) continue;\n visited.add(nodeId);\n\n // Get all outgoing edges\n const outEdges = await this.adapter.queryEdges({ fromNodeId: nodeId });\n\n for (const edge of outEdges) {\n if (options?.edgeTypes && !options.edgeTypes.includes(edge.type)) continue;\n\n const nextNodeId = edge.toNodeId;\n if (!visited.has(nextNodeId)) {\n queue.push({\n nodeId: nextNodeId,\n path: [...path, nextNodeId],\n edges: [...edges, edge.id],\n });\n }\n }\n }\n\n return null; // No path found\n }\n\n // ============ Statistics ============\n\n /**\n * Get graph statistics\n */\n async getStats(): Promise<GraphStats> {\n await this.ensureInitialized();\n\n const dbStats = await this.adapter.getStats();\n\n // Count nodes and edges by type\n const nodeTypes = await this.execute<{ type: string; count: number }>('SELECT type, COUNT(*) as count FROM kg_nodes GROUP BY type');\n\n const edgeTypes = await this.execute<{ type: string; count: number }>('SELECT type, COUNT(*) as count FROM kg_edges GROUP BY type');\n\n const nodesByType: Record<string, number> = {};\n for (const { type, count } of nodeTypes) {\n nodesByType[type] = count;\n }\n\n const edgesByType: Record<string, number> = {};\n for (const { type, count } of edgeTypes) {\n edgesByType[type] = count;\n }\n\n // Calculate average degree\n const averageDegree = dbStats.nodeCount > 0 ? (dbStats.edgeCount * 2) / dbStats.nodeCount : 0;\n\n // Calculate density (actual edges / possible edges)\n const possibleEdges = dbStats.nodeCount * (dbStats.nodeCount - 1);\n const density = possibleEdges > 0 ? dbStats.edgeCount / possibleEdges : 0;\n\n return {\n nodeCount: dbStats.nodeCount,\n edgeCount: dbStats.edgeCount,\n nodesByType,\n edgesByType,\n averageDegree,\n density,\n lastUpdated: new Date(),\n };\n }\n\n // ============ Batch Operations ============\n\n /**\n * Batch insert nodes\n */\n async batchAddNodes(nodes: NodeOptions<TNodeType>[]): Promise<BatchResult> {\n await this.ensureInitialized();\n\n const successful: string[] = [];\n const errors: Array<{ item: any; error: Error }> = [];\n\n for (const nodeOptions of nodes) {\n try {\n const node = await this.addNode(nodeOptions);\n successful.push(node.id);\n } catch (error) {\n errors.push({ item: nodeOptions, error: error as Error });\n }\n }\n\n return {\n successful: successful.length,\n failed: errors.length,\n errors: errors.length > 0 ? errors : undefined,\n };\n }\n\n /**\n * Batch insert edges\n */\n async batchAddEdges(edges: EdgeOptions[]): Promise<BatchResult> {\n await this.ensureInitialized();\n\n const successful: string[] = [];\n const errors: Array<{ item: any; error: Error }> = [];\n\n for (const edgeOptions of edges) {\n try {\n const edge = await this.addEdge(edgeOptions);\n successful.push(edge.id);\n } catch (error) {\n errors.push({ item: edgeOptions, error: error as Error });\n }\n }\n\n return {\n successful: successful.length,\n failed: errors.length,\n errors: errors.length > 0 ? errors : undefined,\n };\n }\n\n // ============ Maintenance ============\n\n /**\n * Vacuum the database to reclaim space\n */\n async vacuum(): Promise<void> {\n await this.ensureInitialized();\n await this.adapter.vacuum();\n }\n\n /**\n * Close the database connection\n */\n async close(): Promise<void> {\n await this.adapter.close();\n this.initialized = false;\n }\n\n // ============ Helper Methods ============\n\n /**\n * Execute raw SQL query (for advanced use cases)\n */\n private async execute<T>(query: string, params: any[] = []): Promise<T[]> {\n return await this.adapter.execute<T>(query, params);\n }\n\n /**\n * Normalize a node from database format\n */\n private normalizeNode(node: any): KnowledgeNode {\n return {\n id: node.id,\n type: node.type,\n label: node.label,\n properties: typeof node.properties === 'string' ? JSON.parse(node.properties) : node.properties,\n confidence: node.confidence,\n createdAt: node.createdAt instanceof Date ? node.createdAt : new Date(node.createdAt),\n updatedAt: node.updatedAt instanceof Date ? node.updatedAt : new Date(node.updatedAt),\n sourceSessionIds: node.sourceSessionIds ? (typeof node.sourceSessionIds === 'string' ? JSON.parse(node.sourceSessionIds) : node.sourceSessionIds) : undefined,\n };\n }\n\n /**\n * Normalize an edge from database format\n */\n private normalizeEdge(edge: any): KnowledgeEdge {\n return {\n id: edge.id,\n type: edge.type,\n fromNodeId: edge.fromNodeId,\n toNodeId: edge.toNodeId,\n properties: typeof edge.properties === 'string' ? JSON.parse(edge.properties) : edge.properties,\n confidence: edge.confidence,\n createdAt: edge.createdAt instanceof Date ? edge.createdAt : new Date(edge.createdAt),\n sourceSessionIds: edge.sourceSessionIds ? (typeof edge.sourceSessionIds === 'string' ? JSON.parse(edge.sourceSessionIds) : edge.sourceSessionIds) : undefined,\n };\n }\n\n /**\n * Index a node for search\n */\n private async indexNodeForSearch(node: any): Promise<void> {\n const searchTerms = new Set<string>();\n\n // Index label\n const labelTerms = node.label.toLowerCase().split(/\\s+/);\n labelTerms.forEach((term: string) => searchTerms.add(term));\n\n // Index important properties\n const properties = typeof node.properties === 'string' ? JSON.parse(node.properties) : node.properties;\n\n for (const [, value] of Object.entries(properties)) {\n if (typeof value === 'string') {\n const terms = value.toLowerCase().split(/\\s+/);\n terms.forEach((term: string) => searchTerms.add(term));\n }\n }\n\n // Insert search indices\n for (const term of searchTerms) {\n await this.adapter.insertSearchIndex({\n term,\n nodeId: node.id,\n field: 'label',\n weight: 1.0,\n });\n }\n }\n\n /**\n * Traverse graph helper\n */\n private async traverseGraph(\n nodeId: string,\n depth: number,\n direction: 'in' | 'out' | 'both',\n edgeTypes: (EdgeType | string)[] | undefined,\n visitedNodes: Map<string, KnowledgeNode>,\n visitedEdges: Map<string, KnowledgeEdge>,\n currentDepth = 0\n ): Promise<void> {\n if (currentDepth >= depth) return;\n\n // Get edges based on direction\n const edges: any[] = [];\n\n if (direction === 'out' || direction === 'both') {\n const outEdges = await this.adapter.queryEdges({ fromNodeId: nodeId });\n edges.push(...outEdges);\n }\n\n if (direction === 'in' || direction === 'both') {\n const inEdges = await this.adapter.queryEdges({ toNodeId: nodeId });\n edges.push(...inEdges);\n }\n\n for (const edge of edges) {\n if (edgeTypes && !edgeTypes.includes(edge.type)) continue;\n\n visitedEdges.set(edge.id, this.normalizeEdge(edge));\n\n const nextNodeId = edge.fromNodeId === nodeId ? edge.toNodeId : edge.fromNodeId;\n\n if (!visitedNodes.has(nextNodeId)) {\n const nextNode = await this.adapter.getNode(nextNodeId);\n if (nextNode) {\n visitedNodes.set(nextNodeId, this.normalizeNode(nextNode));\n\n await this.traverseGraph(nextNodeId, depth, direction, edgeTypes, visitedNodes, visitedEdges, currentDepth + 1);\n }\n }\n }\n }\n\n /**\n * Calculate relevance score\n */\n private calculateRelevance(nodeCount: number, edgeCount: number): number {\n return Math.min(1, (nodeCount + edgeCount) / 10);\n }\n}\n","/**\n * Core types for the knowledge graph system\n */\n\n/**\n * Type for node types - users define their own enums\n */\nexport type NodeType = string;\n\n/**\n * Common edge types that users can extend\n */\nexport enum CommonEdgeType {\n // Generic relationships\n RELATED_TO = 'RELATED_TO',\n SIMILAR_TO = 'SIMILAR_TO',\n OPPOSITE_OF = 'OPPOSITE_OF',\n PART_OF = 'PART_OF',\n HAS_PART = 'HAS_PART',\n\n // Personal relationships\n KNOWS = 'KNOWS',\n FRIEND_OF = 'FRIEND_OF',\n COLLEAGUE_OF = 'COLLEAGUE_OF',\n REPORTS_TO = 'REPORTS_TO',\n MANAGES = 'MANAGES',\n\n // Family relationships\n HAS_FAMILY_MEMBER = 'HAS_FAMILY_MEMBER',\n PARENT_OF = 'PARENT_OF',\n CHILD_OF = 'CHILD_OF',\n SPOUSE_OF = 'SPOUSE_OF',\n SIBLING_OF = 'SIBLING_OF',\n\n // Location relationships\n LIVES_AT = 'LIVES_AT',\n WORKS_AT = 'WORKS_AT',\n LOCATED_IN = 'LOCATED_IN',\n VISITED = 'VISITED',\n PLANS_TO_VISIT = 'PLANS_TO_VISIT',\n\n // Ownership relationships\n OWNS = 'OWNS',\n OWNED_BY = 'OWNED_BY',\n CREATED_BY = 'CREATED_BY',\n CREATED = 'CREATED',\n\n // Financial relationships\n PAID_TO = 'PAID_TO',\n RECEIVED_FROM = 'RECEIVED_FROM',\n SAVED_FOR = 'SAVED_FOR',\n SPENT_ON = 'SPENT_ON',\n EARNS_FROM = 'EARNS_FROM',\n INVESTS_IN = 'INVESTS_IN',\n\n // Career relationships\n EMPLOYED_BY = 'EMPLOYED_BY',\n EMPLOYS = 'EMPLOYS',\n HAS_SKILL = 'HAS_SKILL',\n REQUIRES_SKILL = 'REQUIRES_SKILL',\n STUDIED_AT = 'STUDIED_AT',\n GRADUATED_FROM = 'GRADUATED_FROM',\n\n // Temporal relationships\n HAPPENED_BEFORE = 'HAPPENED_BEFORE',\n HAPPENED_AFTER = 'HAPPENED_AFTER',\n HAPPENED_DURING = 'HAPPENED_DURING',\n CAUSED = 'CAUSED',\n CAUSED_BY = 'CAUSED_BY',\n\n // Preference relationships\n LIKES = 'LIKES',\n DISLIKES = 'DISLIKES',\n INTERESTED_IN = 'INTERESTED_IN',\n PREFERS = 'PREFERS',\n\n // Action relationships\n PARTICIPATED_IN = 'PARTICIPATED_IN',\n ATTENDED = 'ATTENDED',\n ORGANIZED = 'ORGANIZED',\n MENTIONED = 'MENTIONED',\n REFERENCED = 'REFERENCED',\n\n // Document/Information relationships\n CONTAINS = 'CONTAINS',\n CONTAINED_IN = 'CONTAINED_IN',\n DERIVED_FROM = 'DERIVED_FROM',\n BASED_ON = 'BASED_ON',\n}\n\n/**\n * Type for edge types - users can extend CommonEdgeType or define their own\n */\nexport type EdgeType = CommonEdgeType | string;\n\n/**\n * Core knowledge node interface\n */\nexport interface KnowledgeNode<T = Record<string, unknown>> {\n id: string;\n type: NodeType | string;\n label: string;\n properties: T;\n confidence: number;\n createdAt: Date;\n updatedAt: Date;\n sourceSessionIds?: string[];\n}\n\n/**\n * Core knowledge edge interface\n */\nexport interface KnowledgeEdge<T = Record<string, unknown>> {\n id: string;\n type: EdgeType | string;\n fromNodeId: string;\n toNodeId: string;\n properties: T;\n confidence: number;\n createdAt: Date;\n sourceSessionIds?: string[];\n}\n\n/**\n * Query result containing nodes and edges\n */\nexport interface QueryResult<N = KnowledgeNode, E = KnowledgeEdge> {\n nodes: N[];\n edges: E[];\n relevanceScore?: number;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Options for graph queries\n */\nexport interface QueryOptions {\n limit?: number;\n offset?: number;\n minConfidence?: number;\n includeEdges?: boolean;\n depth?: number;\n direction?: 'in' | 'out' | 'both';\n nodeTypes?: (NodeType | string)[];\n edgeTypes?: (EdgeType | string)[];\n orderBy?: 'confidence' | 'createdAt' | 'updatedAt' | 'relevance';\n orderDirection?: 'asc' | 'desc';\n}\n\n/**\n * Options for node creation/update\n */\nexport interface NodeOptions<TNodeType extends string = string, TProperties = Record<string, unknown>> {\n type: TNodeType;\n label: string;\n properties?: TProperties;\n confidence?: number;\n sourceSessionId?: string;\n mergeStrategy?: 'replace' | 'merge' | 'skip';\n}\n\n/**\n * Options for edge creation/update\n */\nexport interface EdgeOptions<TEdgeType extends string = string, TProperties = Record<string, unknown>> {\n type: TEdgeType;\n fromNodeId: string;\n toNodeId: string;\n properties?: TProperties;\n confidence?: number;\n sourceSessionId?: string;\n bidirectional?: boolean;\n}\n\n/**\n * Graph traversal options\n */\nexport interface TraversalOptions {\n startNodeId: string;\n direction?: 'in' | 'out' | 'both';\n maxDepth?: number;\n edgeTypes?: (EdgeType | string)[];\n nodeFilter?: (node: KnowledgeNode) => boolean;\n edgeFilter?: (edge: KnowledgeEdge) => boolean;\n visitOnce?: boolean;\n}\n\n/**\n * Path finding result\n */\nexport interface Path<N = KnowledgeNode, E = KnowledgeEdge> {\n nodes: N[];\n edges: E[];\n length: number;\n weight?: number;\n}\n\n/**\n * Graph statistics\n */\nexport interface GraphStats {\n nodeCount: number;\n edgeCount: number;\n nodesByType: Record<string, number>;\n edgesByType: Record<string, number>;\n averageDegree: number;\n density: number;\n lastUpdated: Date;\n}\n\n/**\n * Extracted knowledge data for processing\n */\nexport interface ExtractedNodeData<T = Record<string, unknown>> {\n type: NodeType | string;\n label: string;\n properties: T;\n confidence: number;\n sourceSessionIds?: string[];\n}\n\nexport interface ExtractedEdgeData<T = Record<string, unknown>> {\n type: EdgeType | string;\n fromNodeLabel: string;\n toNodeLabel: string;\n properties: T;\n confidence: number;\n sourceSessionIds?: string[];\n}\n\nexport interface ExtractedKnowledge<N = ExtractedNodeData, E = ExtractedEdgeData> {\n nodes: N[];\n edges: E[];\n confidence: number;\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Search options\n */\nexport interface SearchOptions {\n query: string;\n fields?: ('label' | 'properties' | string)[];\n nodeTypes?: (NodeType | string)[];\n fuzzy?: boolean;\n limit?: number;\n minScore?: number;\n}\n\n/**\n * Batch operation results\n */\nexport interface BatchResult {\n successful: number;\n failed: number;\n errors?: Array<{ item: unknown; error: Error }>;\n}\n\n/**\n * Migration interface for version upgrades\n */\nexport interface Migration {\n version: string;\n up: (db: unknown) => Promise<void>;\n down: (db: unknown) => Promise<void>;\n description?: string;\n}\n","import type { Node, Edge, NodeIndex, EdgeIndex, SearchIndex, NewNode, NewEdge, NewNodeIndex, NewEdgeIndex, NewSearchIndex } from '../schema';\n\n/**\n * Base adapter interface for database operations\n * All adapters must implement this interface\n */\nexport interface DatabaseAdapter {\n /**\n * Initialize the database schema\n */\n initialize(): Promise<void>;\n\n /**\n * Execute a raw SQL query\n */\n execute<T = unknown>(query: string, params?: unknown[]): Promise<T[]>;\n\n /**\n * Begin a transaction\n */\n transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T>;\n\n /**\n * Node operations\n */\n insertNode(node: NewNode): Promise<Node>;\n updateNode(id: string, updates: Partial<NewNode>): Promise<Node | null>;\n deleteNode(id: string): Promise<boolean>;\n getNode(id: string): Promise<Node | null>;\n getNodes(ids: string[]): Promise<Node[]>;\n queryNodes(conditions: Record<string, unknown>, limit?: number, offset?: number): Promise<Node[]>;\n\n /**\n * Edge operations\n */\n insertEdge(edge: NewEdge): Promise<Edge>;\n updateEdge(id: string, updates: Partial<NewEdge>): Promise<Edge | null>;\n deleteEdge(id: string): Promise<boolean>;\n getEdge(id: string): Promise<Edge | null>;\n getEdges(ids: string[]): Promise<Edge[]>;\n queryEdges(conditions: Record<string, unknown>, limit?: number, offset?: number): Promise<Edge[]>;\n\n /**\n * Index operations\n */\n insertNodeIndex(index: NewNodeIndex): Promise<NodeIndex>;\n deleteNodeIndex(indexKey: string, nodeId?: string): Promise<number>;\n getNodeIndices(indexKey: string): Promise<NodeIndex[]>;\n\n insertEdgeIndex(index: NewEdgeIndex): Promise<EdgeIndex>;\n deleteEdgeIndex(indexKey: string, edgeId?: string): Promise<number>;\n getEdgeIndices(indexKey: string): Promise<EdgeIndex[]>;\n\n /**\n * Search operations\n */\n insertSearchIndex(index: NewSearchIndex): Promise<SearchIndex>;\n deleteSearchIndex(nodeId: string): Promise<number>;\n searchNodes(term: string, limit?: number): Promise<SearchIndex[]>;\n\n /**\n * Batch operations\n */\n batchInsertNodes(nodes: NewNode[]): Promise<Node[]>;\n batchInsertEdges(edges: NewEdge[]): Promise<Edge[]>;\n batchDeleteNodes(ids: string[]): Promise<number>;\n batchDeleteEdges(ids: string[]): Promise<number>;\n\n /**\n * Cleanup and maintenance\n */\n vacuum(): Promise<void>;\n getStats(): Promise<DatabaseStats>;\n close(): Promise<void>;\n}\n\n/**\n * Transaction context for atomic operations\n */\nexport interface TransactionContext {\n execute<T = unknown>(query: string, params?: unknown[]): Promise<T[]>;\n rollback(): Promise<void>;\n}\n\n/**\n * Database statistics\n */\nexport interface DatabaseStats {\n nodeCount: number;\n edgeCount: number;\n indexCount: number;\n sizeInBytes?: number;\n lastVacuum?: Date;\n}\n\n/**\n * Adapter configuration\n */\nexport interface AdapterConfig {\n /**\n * Database connection string or configuration\n */\n connection?: string | Record<string, unknown>;\n\n /**\n * Enable debug logging\n */\n debug?: boolean;\n\n /**\n * Custom table prefix\n */\n tablePrefix?: string;\n\n /**\n * Auto-create tables if they don't exist\n */\n autoCreate?: boolean;\n\n /**\n * Additional adapter-specific options\n */\n options?: Record<string, unknown>;\n}\n\n/**\n * Base adapter class with common functionality\n */\nexport abstract class BaseAdapter implements DatabaseAdapter {\n protected config: AdapterConfig;\n protected tablePrefix: string;\n\n constructor(config: AdapterConfig = {}) {\n this.config = config;\n this.tablePrefix = config.tablePrefix || '';\n }\n\n protected getTableName(table: string): string {\n return this.tablePrefix ? `${this.tablePrefix}_${table}` : table;\n }\n\n\n protected log(_message: string, ..._args: unknown[]): void {\n // Logging disabled for now to avoid console warnings\n // if (this.config.debug) {\n // console.log(`[KnowledgeGraph] ${message}`, ...args);\n // }\n }\n\n protected error(message: string, error?: unknown): void {\n // Use console.error which is allowed by linter\n console.error(`[KnowledgeGraph Error] ${message}`, error);\n }\n\n // Abstract methods that must be implemented by subclasses\n abstract initialize(): Promise<void>;\n abstract execute<T = unknown>(query: string, params?: any[]): Promise<T[]>;\n abstract transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T>;\n\n abstract insertNode(node: NewNode): Promise<Node>;\n abstract updateNode(id: string, updates: Partial<NewNode>): Promise<Node | null>;\n abstract deleteNode(id: string): Promise<boolean>;\n abstract getNode(id: string): Promise<Node | null>;\n abstract getNodes(ids: string[]): Promise<Node[]>;\n abstract queryNodes(conditions: Record<string, any>, limit?: number, offset?: number): Promise<Node[]>;\n\n abstract insertEdge(edge: NewEdge): Promise<Edge>;\n abstract updateEdge(id: string, updates: Partial<NewEdge>): Promise<Edge | null>;\n abstract deleteEdge(id: string): Promise<boolean>;\n abstract getEdge(id: string): Promise<Edge | null>;\n abstract getEdges(ids: string[]): Promise<Edge[]>;\n abstract queryEdges(conditions: Record<string, any>, limit?: number, offset?: number): Promise<Edge[]>;\n\n abstract insertNodeIndex(index: NewNodeIndex): Promise<NodeIndex>;\n abstract deleteNodeIndex(indexKey: string, nodeId?: string): Promise<number>;\n abstract getNodeIndices(indexKey: string): Promise<NodeIndex[]>;\n\n abstract insertEdgeIndex(index: NewEdgeIndex): Promise<EdgeIndex>;\n abstract deleteEdgeIndex(indexKey: string, edgeId?: string): Promise<number>;\n abstract getEdgeIndices(indexKey: string): Promise<EdgeIndex[]>;\n\n abstract insertSearchIndex(index: NewSearchIndex): Promise<SearchIndex>;\n abstract deleteSearchIndex(nodeId: string): Promise<number>;\n abstract searchNodes(term: string, limit?: number): Promise<SearchIndex[]>;\n\n abstract batchInsertNodes(nodes: NewNode[]): Promise<Node[]>;\n abstract batchInsertEdges(edges: NewEdge[]): Promise<Edge[]>;\n abstract batchDeleteNodes(ids: string[]): Promise<number>;\n abstract batchDeleteEdges(ids: string[]): Promise<number>;\n\n abstract vacuum(): Promise<void>;\n abstract getStats(): Promise<DatabaseStats>;\n abstract close(): Promise<void>;\n}\n","import Database from 'better-sqlite3';\nimport { drizzle } from 'drizzle-orm/better-sqlite3';\nimport { eq, and, inArray, like } from 'drizzle-orm';\nimport { BaseAdapter, TransactionContext, DatabaseStats, AdapterConfig } from './base';\nimport * as schema from '../schema';\nimport type { Node, Edge, NodeIndex, EdgeIndex, SearchIndex, NewNode, NewEdge, NewNodeIndex, NewEdgeIndex, NewSearchIndex } from '../schema';\n\n/**\n * SQLite adapter implementation using better-sqlite3\n */\nexport class SQLiteAdapter extends BaseAdapter {\n private db: Database.Database | null = null;\n private drizzle: ReturnType<typeof drizzle> | null = null;\n\n constructor(config: AdapterConfig = {}) {\n super(config);\n }\n\n async initialize(): Promise<void> {\n try {\n const dbPath = (this.config.connection as string) || ':memory:';\n this.db = new Database(dbPath);\n this.drizzle = drizzle(this.db);\n\n // Enable foreign keys\n this.db.exec('PRAGMA foreign_keys = ON');\n\n if (this.config.autoCreate !== false) {\n await this.createTables();\n }\n\n this.log('SQLite adapter initialized', { path: dbPath });\n } catch (error) {\n this.error('Failed to initialize SQLite adapter', error);\n throw error;\n }\n }\n\n private async createTables(): Promise<void> {\n if (!this.db) throw new Error('Database not initialized');\n\n // Create nodes table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_nodes (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n label TEXT NOT NULL,\n properties TEXT NOT NULL DEFAULT '{}',\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n source_session_ids TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_nodes_type ON kg_nodes(type);\n CREATE INDEX IF NOT EXISTS idx_nodes_label ON kg_nodes(label);\n CREATE INDEX IF NOT EXISTS idx_nodes_created_at ON kg_nodes(created_at);\n `);\n\n // Create edges table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_edges (\n id TEXT PRIMARY KEY,\n type TEXT NOT NULL,\n from_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n to_node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n properties TEXT NOT NULL DEFAULT '{}',\n confidence REAL NOT NULL DEFAULT 1.0,\n created_at INTEGER NOT NULL,\n source_session_ids TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_edges_type ON kg_edges(type);\n CREATE INDEX IF NOT EXISTS idx_edges_from_node ON kg_edges(from_node_id);\n CREATE INDEX IF NOT EXISTS idx_edges_to_node ON kg_edges(to_node_id);\n CREATE INDEX IF NOT EXISTS idx_edges_from_type ON kg_edges(from_node_id, type);\n CREATE INDEX IF NOT EXISTS idx_edges_to_type ON kg_edges(to_node_id, type);\n `);\n\n // Create node indices table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_node_indices (\n index_key TEXT NOT NULL,\n node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (index_key, node_id)\n );\n CREATE INDEX IF NOT EXISTS idx_node_indices_key ON kg_node_indices(index_key);\n CREATE INDEX IF NOT EXISTS idx_node_indices_node ON kg_node_indices(node_id);\n `);\n\n // Create edge indices table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_edge_indices (\n index_key TEXT NOT NULL,\n edge_id TEXT NOT NULL REFERENCES kg_edges(id) ON DELETE CASCADE,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (index_key, edge_id)\n );\n CREATE INDEX IF NOT EXISTS idx_edge_indices_key ON kg_edge_indices(index_key);\n CREATE INDEX IF NOT EXISTS idx_edge_indices_edge ON kg_edge_indices(edge_id);\n `);\n\n // Create search index table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_search_index (\n term TEXT NOT NULL,\n node_id TEXT NOT NULL REFERENCES kg_nodes(id) ON DELETE CASCADE,\n field TEXT NOT NULL,\n weight REAL NOT NULL DEFAULT 1.0,\n PRIMARY KEY (term, node_id, field)\n );\n CREATE INDEX IF NOT EXISTS idx_search_term ON kg_search_index(term);\n CREATE INDEX IF NOT EXISTS idx_search_node ON kg_search_index(node_id);\n `);\n\n // Create graph metadata table\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS kg_graph_metadata (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n updated_at INTEGER NOT NULL\n );\n `);\n }\n\n async execute<T = unknown>(query: string, params: unknown[] = []): Promise<T[]> {\n if (!this.db) throw new Error('Database not initialized');\n\n try {\n const stmt = this.db.prepare(query);\n return stmt.all(...params) as T[];\n } catch (error) {\n this.error('Query execution failed', { query, params, error });\n throw error;\n }\n }\n\n async executeUpdate(query: string, params: unknown[] = []): Promise<{ changes: number }> {\n if (!this.db) throw new Error('Database not initialized');\n\n try {\n const stmt = this.db.prepare(query);\n return stmt.run(...params);\n } catch (error) {\n this.error('Query execution failed', { query, params, error });\n throw error;\n }\n }\n\n async transaction<T>(fn: (tx: TransactionContext) => Promise<T>): Promise<T> {\n if (!this.db) throw new Error('Database not initialized');\n\n return new Promise((resolve, reject) => {\n try {\n if (!this.db) throw new Error('Database not initialized');\n const result = this.db.transaction(async () => {\n const tx: TransactionContext = {\n execute: async <U = unknown>(query: string, params: unknown[] = []): Promise<U[]> => {\n return this.execute<U>(query, params);\n },\n rollback: async () => {\n throw new Error('Transaction rollback');\n },\n };\n\n return await fn(tx);\n })();\n\n resolve(result);\n } catch (error) {\n reject(error);\n }\n });\n }\n\n // Node operations\n async insertNode(node: NewNode): Promise<Node> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.nodes).values(node).returning();\n const insertedNode = result[0];\n if (!insertedNode) throw new Error('Failed to create node');\n return this.deserializeNode(insertedNode);\n }\n\n async updateNode(id: string, updates: Partial<NewNode>): Promise<Node | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle\n .update(schema.nodes)\n .set({ ...updates, updatedAt: new Date() })\n .where(eq(schema.nodes.id, id))\n .returning();\n\n return result[0] ? this.deserializeNode(result[0]) : null;\n }\n\n async deleteNode(id: string): Promise<boolean> {\n const query = `DELETE FROM kg_nodes WHERE id = ?`;\n const result = await this.executeUpdate(query, [id]);\n return result.changes > 0;\n }\n\n async getNode(id: string): Promise<Node | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.select().from(schema.nodes).where(eq(schema.nodes.id, id)).limit(1);\n\n return result[0] ? this.deserializeNode(result[0]) : null;\n }\n\n async getNodes(ids: string[]): Promise<Node[]> {\n if (!this.drizzle || ids.length === 0) return [];\n\n const result = await this.drizzle.select().from(schema.nodes).where(inArray(schema.nodes.id, ids));\n\n return result.map((n) => this.deserializeNode(n));\n }\n\n async queryNodes(conditions: Record<string, unknown>, limit = 100, offset = 0): Promise<Node[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const whereConditions = Object.entries(conditions).map(([key, value]) => {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const column = (schema.nodes as any)[key];\n return eq(column, value);\n });\n\n const result = await this.drizzle\n .select()\n .from(schema.nodes)\n .where(and(...whereConditions))\n .limit(limit)\n .offset(offset);\n\n return result.map((n) => this.deserializeNode(n));\n }\n\n // Edge operations\n async insertEdge(edge: NewEdge): Promise<Edge> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.edges).values(edge).returning();\n const insertedEdge = result[0];\n if (!insertedEdge) throw new Error('Failed to create edge');\n return this.deserializeEdge(insertedEdge);\n }\n\n async updateEdge(id: string, updates: Partial<NewEdge>): Promise<Edge | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.update(schema.edges).set(updates).where(eq(schema.edges.id, id)).returning();\n\n return result[0] ? this.deserializeEdge(result[0]) : null;\n }\n\n async deleteEdge(id: string): Promise<boolean> {\n const query = `DELETE FROM kg_edges WHERE id = ?`;\n await this.execute(query, [id]);\n return true;\n }\n\n async getEdge(id: string): Promise<Edge | null> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.select().from(schema.edges).where(eq(schema.edges.id, id)).limit(1);\n\n return result[0] ? this.deserializeEdge(result[0]) : null;\n }\n\n async getEdges(ids: string[]): Promise<Edge[]> {\n if (!this.drizzle || ids.length === 0) return [];\n\n const result = await this.drizzle.select().from(schema.edges).where(inArray(schema.edges.id, ids));\n\n return result.map((e) => this.deserializeEdge(e));\n }\n\n async queryEdges(conditions: Record<string, unknown>, limit = 100, offset = 0): Promise<Edge[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const whereConditions = Object.entries(conditions).map(([key, value]) => {\n // Map camelCase to snake_case for schema fields (Drizzle schema uses camelCase in TypeScript)\n const fieldMap: Record<string, string> = {\n 'from_node_id': 'fromNodeId',\n 'to_node_id': 'toNodeId',\n 'created_at': 'createdAt',\n 'updated_at': 'updatedAt',\n 'source_session_ids': 'sourceSessionIds'\n };\n const schemaKey = fieldMap[key] || key;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const column = (schema.edges as any)[schemaKey];\n return eq(column, value);\n });\n\n const result = await this.drizzle\n .select()\n .from(schema.edges)\n .where(and(...whereConditions))\n .limit(limit)\n .offset(offset);\n\n return result.map((e) => this.deserializeEdge(e));\n }\n\n // Index operations\n async insertNodeIndex(index: NewNodeIndex): Promise<NodeIndex> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.nodeIndices).values(index).returning();\n const insertedIndex = result[0];\n if (!insertedIndex) throw new Error('Failed to create node index');\n return insertedIndex;\n }\n\n async deleteNodeIndex(indexKey: string, nodeId?: string): Promise<number> {\n const query = nodeId ? `DELETE FROM kg_node_indices WHERE index_key = ? AND node_id = ?` : `DELETE FROM kg_node_indices WHERE index_key = ?`;\n const params = nodeId ? [indexKey, nodeId] : [indexKey];\n\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run(params);\n return info.changes;\n }\n\n async getNodeIndices(indexKey: string): Promise<NodeIndex[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n return await this.drizzle.select().from(schema.nodeIndices).where(eq(schema.nodeIndices.indexKey, indexKey));\n }\n\n async insertEdgeIndex(index: NewEdgeIndex): Promise<EdgeIndex> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.edgeIndices).values(index).returning();\n const insertedIndex = result[0];\n if (!insertedIndex) throw new Error('Failed to create edge index');\n return insertedIndex;\n }\n\n async deleteEdgeIndex(indexKey: string, edgeId?: string): Promise<number> {\n const query = edgeId ? `DELETE FROM kg_edge_indices WHERE index_key = ? AND edge_id = ?` : `DELETE FROM kg_edge_indices WHERE index_key = ?`;\n const params = edgeId ? [indexKey, edgeId] : [indexKey];\n\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run(params);\n return info.changes;\n }\n\n async getEdgeIndices(indexKey: string): Promise<EdgeIndex[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n return await this.drizzle.select().from(schema.edgeIndices).where(eq(schema.edgeIndices.indexKey, indexKey));\n }\n\n // Search operations\n async insertSearchIndex(index: NewSearchIndex): Promise<SearchIndex> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n const result = await this.drizzle.insert(schema.searchIndex).values(index).returning();\n const insertedIndex = result[0];\n if (!insertedIndex) throw new Error('Failed to create search index');\n return insertedIndex;\n }\n\n async deleteSearchIndex(nodeId: string): Promise<number> {\n const query = `DELETE FROM kg_search_index WHERE node_id = ?`;\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run([nodeId]);\n return info.changes;\n }\n\n async searchNodes(term: string, limit = 50): Promise<SearchIndex[]> {\n if (!this.drizzle) throw new Error('Database not initialized');\n\n return await this.drizzle\n .select()\n .from(schema.searchIndex)\n .where(like(schema.searchIndex.term, `%${term}%`))\n .limit(limit);\n }\n\n // Batch operations\n async batchInsertNodes(nodes: NewNode[]): Promise<Node[]> {\n if (!this.drizzle || nodes.length === 0) return [];\n\n const result = await this.drizzle.insert(schema.nodes).values(nodes).returning();\n return result.map((n) => this.deserializeNode(n));\n }\n\n async batchInsertEdges(edges: NewEdge[]): Promise<Edge[]> {\n if (!this.drizzle || edges.length === 0) return [];\n\n const result = await this.drizzle.insert(schema.edges).values(edges).returning();\n return result.map((e) => this.deserializeEdge(e));\n }\n\n async batchDeleteNodes(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n\n const placeholders = ids.map(() => '?').join(',');\n const query = `DELETE FROM kg_nodes WHERE id IN (${placeholders})`;\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(query);\n const info = stmt.run(ids);\n return info.changes;\n }\n\n async batchDeleteEdges(ids: string[]): Promise<number> {\n if (ids.length === 0) return 0;\n\n const placeholders = ids.map(() => '?').join(',');\n const query = `DELETE FROM kg_edges WHERE id IN (${placeholders})`;\n if (!this.db) throw new Error('Database not initialized');\n const stmt = this.db.prepare(qu