import { createHash } from "node:crypto"; import type { RowDataPacket } from "mysql2"; import pool from "../../db.js"; import type { DemandGap, GapReason, GapStatus, } from "../../../shared/assets/assistant.js"; // Store the unmet request and concise analysis context for product feedback, not full sessions or result rows. let ready: Promise | undefined; const purge = () => pool.query( "DELETE FROM bi_asset_ai_gaps WHERE last_seen < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 90 DAY)", ); const SCHEMA = `CREATE TABLE IF NOT EXISTS bi_asset_ai_gaps ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(64) NOT NULL, fingerprint CHAR(64) NOT NULL, demand VARCHAR(600) NOT NULL, reason VARCHAR(24) NOT NULL, status VARCHAR(24) NOT NULL DEFAULT 'open', occurrences INT NOT NULL DEFAULT 1, first_seen DATETIME NOT NULL, last_seen DATETIME NOT NULL, UNIQUE KEY user_demand (user_id, fingerprint), KEY last_seen (last_seen) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`; async function ensure() { if (!ready) ready = (async () => { // Precreated production tables need no DDL permission. try { await pool.query("SELECT id FROM bi_asset_ai_gaps LIMIT 0"); } catch (error) { if ((error as { code?: string }).code !== "ER_NO_SUCH_TABLE") throw error; await pool.query(SCHEMA); } await purge(); setInterval( () => { void purge().catch(() => console.warn("asset_ai_gap_retention_failed"), ); }, 60 * 60 * 1000, ).unref(); })().catch((error) => { ready = undefined; throw error; }); await ready; } export const gapStore = { async record(userId: string, demand: string, reason: GapReason) { await ensure(); const fingerprint = createHash("sha256") .update(reason + "|" + demand) .digest("hex"); await pool.query( `INSERT INTO bi_asset_ai_gaps (user_id,fingerprint,demand,reason,first_seen,last_seen) VALUES (?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP()) ON DUPLICATE KEY UPDATE occurrences=occurrences+1, last_seen=UTC_TIMESTAMP(), status=IF(status='resolved','open',status)`, [userId, fingerprint, demand, reason], ); }, async list(userId: string | null, options: { readOnly?: boolean } = {}): Promise { if (!options.readOnly) { await ensure(); await purge(); } const [rows] = await pool.query( `SELECT id,demand,reason,status,occurrences, DATE_FORMAT(first_seen,'%Y-%m-%dT%H:%i:%sZ') firstSeen, DATE_FORMAT(last_seen,'%Y-%m-%dT%H:%i:%sZ') lastSeen FROM bi_asset_ai_gaps WHERE ${userId ? "user_id = ? AND " : ""}last_seen >= DATE_SUB(UTC_TIMESTAMP(), INTERVAL 90 DAY) ORDER BY last_seen DESC LIMIT 200`, userId ? [userId] : [], ); return rows as DemandGap[]; }, async update(id: number, status: GapStatus) { await ensure(); await pool.query("UPDATE bi_asset_ai_gaps SET status=? WHERE id=?", [ status, id, ]); }, };