const {getDB} = require('../db'); const { parseRow } = require ('@nexusai/shared') /******* Entities ********/ // Upsert an entity - insert or update if (name, type) already exists function upsertEntity(name, type, notes = null, metadata = null, source = 'extraction') { const db = getDB(); const stmt = db.prepare(` INSERT INTO entities (name, type, notes, metadata, source, last_seen_at) VALUES (?, ?, ?, ?, ?, unixepoch()) ON CONFLICT(name, type) DO UPDATE SET -- First extraction wins: notes are never overwritten once set. -- Revisit during Memory Consolidation Lifecycle (Phase 2) — once entity -- quality scoring exists, a higher-confidence extraction should be able -- to replace stale notes rather than being silently dropped. notes = COALESCE(entities.notes, excluded.notes), metadata = excluded.metadata, mention_count = entities.mention_count + 1, last_seen_at = unixepoch(), updated_at = unixepoch() `); stmt.run(name, type, notes, metadata ? JSON.stringify(metadata) : null, source); return getEntityByNameType(name, type); } // Get an entity by its ID function getEntity(id) { const db = getDB(); return parseRow(db.prepare(`SELECT * FROM entities WHERE id = ?`).get(id)); } // Get all entities of a given type function getEntitiesByType(type) { const db = getDB(); return db.prepare(`SELECT * FROM entities WHERE type = ? ORDER BY name`).all(type).map(parseRow); } // Delete an entity by ID, cascades to delete relationships involving this entity function deleteEntity(id) { const db = getDB(); db.prepare(`DELETE FROM entities WHERE id = ?`).run(id); } /********* Relationships *********/ // Upsert a relationship, insert or ignore if (from_id, to_id, label) already exists function upsertRelationship(fromId, toId, label, notes = null, metadata = null) { const db = getDB(); const stmt = db.prepare(` INSERT INTO relationships (from_id, to_id, label, notes, metadata) VALUES (?, ?, ?, ?, ?) ON CONFLICT(from_id, to_id, label) DO UPDATE SET mention_count = relationships.mention_count + 1, -- First extraction wins for notes — same policy as entities. notes = COALESCE(relationships.notes, excluded.notes) `); stmt.run(fromId, toId, label, notes, metadata ? JSON.stringify(metadata) : null); return getRelationship(fromId, toId, label); } // Retrieve a relationship by (from_id, to_id, label) function getRelationship(fromId, toId, label) { const db = getDB(); return parseRow( db.prepare(`SELECT * FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`) .get(fromId, toId, label) ); } // Retrieves an entity by its unique (name, type) combination function getEntityByNameType(name, type) { const db = getDB(); return parseRow(db.prepare(`SELECT * FROM entities WHERE name = ? AND type = ?`).get(name, type)); } // Retrive all relationships originating from a given entity function getOutboundRelationships(entityId) { const db = getDB(); return db.prepare(`SELECT * FROM relationships WHERE from_id = ?`).all(entityId).map(parseRow); } // Delete a specific relationship by (from_id, to_id, label) function deleteRelationship(fromId, toId, label) { const db = getDB(); db.prepare(`DELETE FROM relationships WHERE from_id = ? AND to_id = ? AND label = ?`).run(fromId, toId, label); } function linkEntityToEpisode(entityId, episodeId) { const db = getDB(); db.prepare(` INSERT OR IGNORE INTO entity_episodes (entity_id, episode_id) VALUES (?, ?) `).run(entityId, episodeId); } module.exports = { upsertEntity, getEntity, getEntitiesByType, getEntityByNameType, deleteEntity, linkEntityToEpisode, upsertRelationship, getRelationship, getOutboundRelationships, deleteRelationship }