Files
ihaleist/server/tender-history.mjs

57 lines
3.1 KiB
JavaScript

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();}
}