Persist versioned document text reviews against source file hashes
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {openDb,BELGE_DIR} from './user-store.mjs';
|
||||
const hash=value=>createHash('sha256').update(value).digest('hex');
|
||||
const fail=(status,message)=>Object.assign(new Error(message),{status});
|
||||
function source(db,email,id) {
|
||||
const record=db.prepare('SELECT * FROM belgeler WHERE id=? AND email=?').get(id,email);
|
||||
if(!record || record.tur !== 'ihale') throw fail(404,'İhale dokümanı bulunamadı.');
|
||||
let bytes;
|
||||
try {bytes=fs.readFileSync(path.join(BELGE_DIR,record.dosya));}
|
||||
catch {throw fail(409,'Kaynak dosya okunamıyor; inceleme kaydedilemez.');}
|
||||
return hash(bytes);
|
||||
}
|
||||
function latest(db,email,id) {
|
||||
return db.prepare('SELECT revision,file_hash,text_hash,corrected_text,review_note,reviewed_at FROM document_text_reviews WHERE document_id=? AND email=? ORDER BY revision DESC LIMIT 1').get(id,email) || null;
|
||||
}
|
||||
export function documentReview(email,id) {
|
||||
const db=openDb();
|
||||
try {
|
||||
const fileHash=source(db,email,id), review=latest(db,email,id);
|
||||
return {documentId:id,fileHash,review,stale:Boolean(review && review.file_hash!==fileHash)};
|
||||
} finally {db.close();}
|
||||
}
|
||||
export function saveDocumentReview(email,id,input) {
|
||||
if(typeof input.correctedText !== 'string' || input.correctedText.trim().length<40 || Buffer.byteLength(input.correctedText)>1024*1024)
|
||||
throw fail(400,'Kontrol edilen metin 40 karakter ile 1 MB arasında olmalı.');
|
||||
if(typeof input.note !== 'string' || input.note.trim().length<20 || input.note.length>2000)
|
||||
throw fail(400,'Karşılaştırılan sayfaları ve yapılan düzeltmeleri en az 20 karakterle açıklayın.');
|
||||
if(input.allPagesCompared !== true) throw fail(400,'Tüm kaynak sayfalarıyla karşılaştırma gerekli.');
|
||||
if(!Number.isInteger(input.expectedRevision) || input.expectedRevision<0) throw fail(400,'Geçerli inceleme sürümü gerekli.');
|
||||
const db=openDb();
|
||||
try {
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
const fileHash=source(db,email,id), previous=latest(db,email,id);
|
||||
if(input.fileHash!==fileHash || input.expectedRevision!==(previous?.revision || 0)) throw fail(409,'Dosya veya inceleme sürümü değişti. Güncel kaydı yeniden açın.');
|
||||
const text=input.correctedText.trim();
|
||||
db.prepare('INSERT INTO document_text_reviews VALUES (?,?,?,?,?,?,?,?)').run(id,email,input.expectedRevision+1,fileHash,hash(text),text,input.note.trim(),new Date().toISOString());
|
||||
const review=latest(db,email,id);
|
||||
db.exec('COMMIT');return {documentId:id,fileHash,review,stale:false};
|
||||
} catch(error) {try {db.exec('ROLLBACK');} catch {} throw error;}
|
||||
finally {db.close();}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import {documentReview,saveDocumentReview} from './document-review.mjs';
|
||||
import { documentMetadataPatch, decodeDocumentUpload } from './document-metadata.mjs';
|
||||
import { deadlineInfo } from '../src/services/tenderCalendar.js';
|
||||
import { exchangeRates } from './exchange-rates.mjs';
|
||||
@@ -1064,6 +1065,17 @@ const server = http.createServer(async (req, res) => {
|
||||
return json(res, 200, { success: true, belgeler: store.listDocuments(user.email) });
|
||||
} catch (e) { return json(res, 400, { success: false, error: e.message }); }
|
||||
}
|
||||
if (url.pathname === '/api/me/documents/text-review') {
|
||||
const user=userFromAuthHeader(req.headers['authorization']);
|
||||
if(!user) return json(res,401,{success:false,error:'oturum yok'});
|
||||
try {
|
||||
if(req.method === 'GET') return json(res,200,{success:true,...documentReview(user.email,url.searchParams.get('id'))});
|
||||
if(req.method !== 'POST') return json(res,405,{success:false,error:'desteklenmeyen metot'});
|
||||
const body=await readBody(req,1100*1024);
|
||||
if(typeof body.id !== 'string') return json(res,400,{success:false,error:'belge id gerekli'});
|
||||
return json(res,200,{success:true,...saveDocumentReview(user.email,body.id,body)});
|
||||
} catch(error) {return json(res,error.status || 500,{success:false,error:error.message});}
|
||||
}
|
||||
if (url.pathname === '/api/me/documents/read-error' && req.method === 'POST') {
|
||||
const user = userFromAuthHeader(req.headers['authorization']);
|
||||
if (!user) return json(res,401,{success:false,error:'oturum yok'});
|
||||
|
||||
+17
-1
@@ -65,6 +65,13 @@ CREATE TABLE IF NOT EXISTS api_anahtarlari (
|
||||
CREATE INDEX IF NOT EXISTS idx_apikey_hash ON api_anahtarlari(hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_apikey_email ON api_anahtarlari(email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS document_text_reviews (
|
||||
document_id TEXT NOT NULL, email TEXT NOT NULL, revision INTEGER NOT NULL,
|
||||
file_hash TEXT NOT NULL, text_hash TEXT NOT NULL, corrected_text TEXT NOT NULL,
|
||||
review_note TEXT NOT NULL, reviewed_at TEXT NOT NULL,
|
||||
PRIMARY KEY (document_id,email,revision)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS belgeler (
|
||||
id TEXT PRIMARY KEY,
|
||||
email TEXT NOT NULL,
|
||||
@@ -107,6 +114,8 @@ export function openDb() {
|
||||
.run('20260906_document_type', new Date().toISOString());
|
||||
db.prepare('INSERT OR IGNORE INTO schema_migrations VALUES (?,?)')
|
||||
.run('20260906_document_processing', new Date().toISOString());
|
||||
db.prepare('INSERT OR IGNORE INTO schema_migrations VALUES (?,?)')
|
||||
.run('20260906_document_text_reviews', new Date().toISOString());
|
||||
db.exec('COMMIT');
|
||||
} catch (error) {
|
||||
try { db.exec('ROLLBACK'); } catch { /* original error is authoritative */ }
|
||||
@@ -315,7 +324,14 @@ export function deleteDocument(email, id) {
|
||||
if (!rec) return false;
|
||||
try { fs.unlinkSync(path.join(BELGE_DIR, rec.dosya)); }
|
||||
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
||||
return withDb((db) => db.prepare('DELETE FROM belgeler WHERE id=? AND email=?').run(id, email).changes > 0, false);
|
||||
return withDb((db) => {
|
||||
db.exec('BEGIN IMMEDIATE');
|
||||
try {
|
||||
db.prepare('DELETE FROM document_text_reviews WHERE document_id=? AND email=?').run(id,email);
|
||||
const changed=db.prepare('DELETE FROM belgeler WHERE id=? AND email=?').run(id,email).changes>0;
|
||||
db.exec('COMMIT');return changed;
|
||||
} catch(error) {db.exec('ROLLBACK');throw error;}
|
||||
});
|
||||
}
|
||||
|
||||
export function documentUsage(email) {
|
||||
|
||||
Reference in New Issue
Block a user