import {DatabaseSync} from 'node:sqlite'; import {mkdirSync} from 'node:fs'; import {dirname,join} from 'node:path'; import {fileURLToPath} from 'node:url'; import {createHash} from 'node:crypto'; const root=process.env.IHALEIST_SHARED_DATA_DIR || join(dirname(fileURLToPath(import.meta.url)),'data'); const fields=['baslik','kurum','tarih','saat','durum','alimTuru','aciklama','documentVersion']; function key(t) { const ikn=String(t.ikn || '').trim(); if(!ikn)return null; return /^\d{4}\/\d+$/.test(ikn) ? ikn : `DT:${ikn}:${String(t.kurum || '').trim()}`; } function open() { mkdirSync(root,{recursive:true}); const db=new DatabaseSync(join(root,'tender-history.sqlite')); try { db.exec(`PRAGMA busy_timeout=5000; CREATE TABLE IF NOT EXISTS observations ( id INTEGER PRIMARY KEY AUTOINCREMENT,tender_key TEXT NOT NULL,source_id TEXT NOT NULL, observed_at TEXT NOT NULL,content_hash TEXT NOT NULL,snapshot TEXT NOT NULL,changes TEXT NOT NULL); CREATE INDEX IF NOT EXISTS observation_tender ON observations(tender_key,id); CREATE INDEX IF NOT EXISTS observation_source ON observations(tender_key,source_id,id);`); return db; } catch(error) {db.close();throw error;} } export function observeTenders(tenders) { const db=open(); try { db.exec('BEGIN IMMEDIATE'); const last=db.prepare('SELECT content_hash,snapshot FROM observations WHERE tender_key=? AND source_id=? ORDER BY id DESC LIMIT 1'); const insert=db.prepare('INSERT INTO observations(tender_key,source_id,observed_at,content_hash,snapshot,changes) VALUES (?,?,?,?,?,?)'); for(const t of tenders) { const k=key(t);if(!k)continue; const source=String(t.id || t.source || 'unknown'); const prev=last.get(k,source); // A list response missing detail text must not erase an earlier observed value. const snapshot={...(prev ? JSON.parse(prev.snapshot) : {})}; for(const field of fields)if(t[field]!=null && t[field]!=='')snapshot[field]=String(t[field]); const canonical=JSON.stringify(Object.fromEntries(fields.filter(f=>snapshot[f]!=null).map(f=>[f,snapshot[f]]))); const hash=createHash('sha256').update(canonical).digest('hex'); if(prev?.content_hash===hash)continue; const before=prev ? JSON.parse(prev.snapshot) : {}; const changes=prev ? fields.filter(f=>before[f]!==snapshot[f]).map(field=>({field,before:before[field]??null,after:snapshot[field]??null})) : []; insert.run(k,source,new Date().toISOString(),hash,canonical,JSON.stringify(changes)); } db.exec('COMMIT'); } catch(error) {db.exec('ROLLBACK');throw error;} finally {db.close();} } export function tenderHistory(tender,{before=null,limit=20}={}) { const k=key(tender);if(!k)throw new Error('İhale kimliği gerekli.'); const db=open(); try { const rows=db.prepare('SELECT id,source_id,observed_at,changes FROM observations WHERE tender_key=? AND idlimit, page=rows.slice(0,limit); return {records:page.map(r=>({id:r.id,sourceId:r.source_id,observedAt:r.observed_at,changes:JSON.parse(r.changes)})),nextCursor:more?page.at(-1).id:null}; } finally {db.close();} }