Persist observed tender source revisions and expose paginated history
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
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 id<? ORDER BY id DESC LIMIT ?')
|
||||
.all(k,before || Number.MAX_SAFE_INTEGER,limit+1);
|
||||
const more=rows.length>limit, 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();}
|
||||
}
|
||||
Reference in New Issue
Block a user