feat: mobil için Google Drive ince-köprü endpoint'i ekle (Faz 4)
Yeni GET /api/mobile-relay/case/:caseId/summary — kullanıcının Drive'ındaki case.sqlite'ı anlık olarak açıp sadece belge metadata'sı (extracted_text HARİÇ) ve AI analiz özetlerini döner, hiçbir şey kalıcı olarak saklanmaz (geçici dosya işlem sonunda silinir). google_drive_files'ı reuse eden yeni /google-drive/register-case-path endpoint'i, case_id ↔ Drive'daki local_path eşleşmesini tutan yeni case_drive_files tablosuna yazıyor (migration: laawos/sql/21_case_drive_files.sql, Supabase'de elle çalıştırılması gerekiyor). better-sqlite3 + @types/better-sqlite3 eklendi; node:22-alpine (musl) için gerekli prebuild paketle geliyor, ek derleme adımı gerekmiyor (doğrulandı). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
ab20d88422
commit
746848606a
Generated
+33
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.112.2",
|
||||
"adm-zip": "^0.6.0",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
@@ -26,6 +27,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.8",
|
||||
"@types/better-sqlite3": "^9.6.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/multer": "^2.2.0",
|
||||
@@ -831,6 +833,16 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/better-sqlite3": {
|
||||
"version": "9.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-9.6.0.tgz",
|
||||
"integrity": "sha512-ZEEwBSgMu7GYJOynoagg5X9JbxfL6dTJDsgViJIqh67jV44kyOr9RXfmFjLK5rzC4MWssP06t9hu/JwGDnUbCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||
@@ -1365,6 +1377,18 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "13.0.3",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
|
||||
"integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/bluebird": {
|
||||
"version": "3.4.7",
|
||||
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
|
||||
@@ -2319,6 +2343,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.9.2",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.2.tgz",
|
||||
"integrity": "sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.112.2",
|
||||
"adm-zip": "^0.6.0",
|
||||
"better-sqlite3": "^13.0.3",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
@@ -31,6 +32,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/adm-zip": "^0.5.8",
|
||||
"@types/better-sqlite3": "^9.6.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/multer": "^2.2.0",
|
||||
|
||||
@@ -381,3 +381,32 @@ export const downloadFile = async (req: AuthenticatedRequest, res: Response) =>
|
||||
res.status(500).json({ error: 'Dosya indirilemedi.' });
|
||||
}
|
||||
};
|
||||
|
||||
// Faz 4 (mobil ince-köprü) — Electron, bir dava için case_id ↔ yerel SQLite
|
||||
// dosyasının Drive'daki local_path'i eşleşmesini burada kaydeder. Bunu asıl
|
||||
// Drive yükleme başarısından BAĞIMSIZ olarak, dava ilk dokunulduğunda
|
||||
// (localDataStore.js::upsertCaseMeta) çağırıyoruz — çünkü local_path zaten
|
||||
// deterministik (sanitizeName(caseTitle) + "/case.sqlite"), yüklemenin
|
||||
// tamamlanmasını beklemeye gerek yok. Mobil relay endpoint'i bu eşleşmeyi
|
||||
// kullanarak google_drive_files'ta doğru dosyayı bulur. Best-effort — Electron
|
||||
// bu çağrı başarısız olsa bile yerel işlemi bloklamaz (bkz. çağıran taraf).
|
||||
export const registerCasePath = async (req: AuthenticatedRequest, res: Response) => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
const { case_id, local_path } = req.body || {};
|
||||
if (!case_id || !local_path) {
|
||||
return res.status(400).json({ error: 'case_id ve local_path zorunludur.' });
|
||||
}
|
||||
const { error } = await supabase
|
||||
.from('case_drive_files')
|
||||
.upsert(
|
||||
{ user_id: userId, case_id, local_path, updated_at: new Date().toISOString() },
|
||||
{ onConflict: 'user_id,case_id' }
|
||||
);
|
||||
if (error) throw error;
|
||||
res.json({ ok: true });
|
||||
} catch (error: any) {
|
||||
console.error('[DRIVE] Register Case Path Error:', error);
|
||||
res.status(500).json({ error: 'Eşleşme kaydedilemedi.' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
// Faz 4 — Mobil İnce Köprü. Mobil (legal-mobile) artık documents/analyses'i
|
||||
// doğrudan Supabase'den okumuyor (RLS ile tam extracted_text/summary_json
|
||||
// erişimi vardı) — bunun yerine bu endpoint'e istek atıyor, biz kullanıcının
|
||||
// Google Drive'ındaki case.sqlite'ı ANLIK olarak açıp SADECE özet niteliğindeki
|
||||
// bilgiyi (belge metni HARİÇ) döneriz. Hiçbir şey burada kalıcı olarak
|
||||
// SAKLANMAZ — geçici dosya işlem sonunda silinir.
|
||||
import { Response } from 'express';
|
||||
import { AuthenticatedRequest } from '../middleware/auth';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { getValidAccessToken } from './googleDrive.controller';
|
||||
import * as googleDrive from '../lib/googleDriveClient';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
const EMPTY_RESULT = { documents: [], analyses: [] };
|
||||
|
||||
export const getCaseRelaySummary = async (req: AuthenticatedRequest, res: Response) => {
|
||||
const userId = req.user?.id as string;
|
||||
const { caseId } = req.params;
|
||||
let tmpPath: string | null = null;
|
||||
|
||||
try {
|
||||
if (!caseId) {
|
||||
return res.status(400).json({ error: 'Geçersiz caseId' });
|
||||
}
|
||||
|
||||
const { data: connection } = await supabase
|
||||
.from('google_drive_connections')
|
||||
.select('status')
|
||||
.eq('user_id', userId)
|
||||
.maybeSingle();
|
||||
if (!connection || connection.status !== 'connected') {
|
||||
return res.status(409).json({ error: 'Bu dosyanın analizlerine erişmek için önce masaüstü uygulamasında Google Drive bağlantınızı kurmalısınız.' });
|
||||
}
|
||||
|
||||
const { data: mapping } = await supabase
|
||||
.from('case_drive_files')
|
||||
.select('local_path')
|
||||
.eq('user_id', userId)
|
||||
.eq('case_id', caseId)
|
||||
.maybeSingle();
|
||||
if (!mapping) {
|
||||
// Bu dava masaüstünde henüz hiç açılmamış/senkron olmamış — hata değil,
|
||||
// mobil boş durumu gösterebilsin.
|
||||
return res.json(EMPTY_RESULT);
|
||||
}
|
||||
|
||||
const { data: fileRow } = await supabase
|
||||
.from('google_drive_files')
|
||||
.select('drive_file_id')
|
||||
.eq('user_id', userId)
|
||||
.eq('local_path', mapping.local_path)
|
||||
.maybeSingle();
|
||||
if (!fileRow) {
|
||||
// Eşleşme var ama Drive'a henüz hiç yüklenmemiş (ilk senkron bekleniyor).
|
||||
return res.json(EMPTY_RESULT);
|
||||
}
|
||||
|
||||
let accessToken: string;
|
||||
try {
|
||||
accessToken = await getValidAccessToken(userId);
|
||||
} catch (err: any) {
|
||||
return res.status(409).json({ error: err.message || 'Google Drive bağlantınızın yenilenmesi gerekiyor.' });
|
||||
}
|
||||
|
||||
const buffer = await googleDrive.downloadFile(accessToken, fileRow.drive_file_id);
|
||||
|
||||
tmpPath = path.join(os.tmpdir(), `mobile-relay-${crypto.randomUUID()}.sqlite`);
|
||||
fs.writeFileSync(tmpPath, buffer);
|
||||
|
||||
const db = new Database(tmpPath, { readonly: true });
|
||||
try {
|
||||
// extracted_text BİLEREK seçilmiyor — mobil relay sadece metadata +
|
||||
// AI özetlerini görür, ham belge metnini asla.
|
||||
const documents = db.prepare('SELECT id, filename, uploaded_at, file_size, category FROM documents ORDER BY uploaded_at DESC').all() as any[];
|
||||
const analyses = db.prepare('SELECT id, document_id, analysis_type, summary_json, model_used, created_at FROM analyses ORDER BY created_at DESC').all() as any[];
|
||||
|
||||
res.json({
|
||||
// extracted_text alanı mobil istemcinin DocumentRow tipiyle uyumlu
|
||||
// olsun diye açıkça null olarak eklenir (yok sayılmıyor, bilinçli boş).
|
||||
documents: documents.map((d) => ({ ...d, extracted_text: null })),
|
||||
analyses: analyses.map((a) => ({
|
||||
id: a.id,
|
||||
case_id: caseId,
|
||||
document_id: a.document_id,
|
||||
summary_json: safeParseJson(a.summary_json),
|
||||
model_used: a.model_used,
|
||||
created_at: a.created_at,
|
||||
})),
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[MobileRelay] Get Case Summary Error:', error);
|
||||
res.status(500).json({ error: 'Dosya özeti alınamadı.' });
|
||||
} finally {
|
||||
if (tmpPath) {
|
||||
fs.unlink(tmpPath, () => {});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function safeParseJson(s: unknown) {
|
||||
if (typeof s !== 'string') return s;
|
||||
try { return JSON.parse(s); } catch { return s; }
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import { requireAuth } from '../middleware/auth';
|
||||
import { connect, oauthCallback, getStatus, disconnect, refresh, uploadFile, getBackupStatus, listBackupFiles, downloadFile } from '../controllers/googleDrive.controller';
|
||||
import { connect, oauthCallback, getStatus, disconnect, refresh, uploadFile, getBackupStatus, listBackupFiles, downloadFile, registerCasePath } from '../controllers/googleDrive.controller';
|
||||
|
||||
const router = Router();
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
@@ -17,5 +17,6 @@ router.post('/upload', upload.single('file'), requireAuth, uploadFile);
|
||||
router.get('/backup-status', requireAuth, getBackupStatus);
|
||||
router.get('/backup-files', requireAuth, listBackupFiles);
|
||||
router.get('/download', requireAuth, downloadFile);
|
||||
router.post('/register-case-path', requireAuth, registerCasePath);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -8,6 +8,7 @@ import draftingRoutes from './drafting.routes';
|
||||
import googleDriveRoutes from './googleDrive.routes';
|
||||
import authRoutes from './auth.routes';
|
||||
import whatsappRoutes from './whatsapp.routes';
|
||||
import mobileRelayRoutes from './mobileRelay.routes';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -23,6 +24,7 @@ router.use('/templates', templateRoutes);
|
||||
router.use('/drafting', draftingRoutes);
|
||||
router.use('/google-drive', googleDriveRoutes);
|
||||
router.use('/whatsapp', whatsappRoutes); // POST /api/whatsapp/message (n8n webhook, ayrı X-Webhook-Secret doğrulaması)
|
||||
router.use('/mobile-relay', mobileRelayRoutes); // GET /api/mobile-relay/case/:caseId/summary (Faz 4 — legal-mobile)
|
||||
router.use('/', precedentRoutes); // Keeps the same paths like /api/search-precedents
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { requireAuth } from '../middleware/auth';
|
||||
import { getCaseRelaySummary } from '../controllers/mobileRelay.controller';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/mobile-relay/case/:caseId/summary — requireAuth zaten :caseId
|
||||
// param'ını görüp dosya sahipliğini doğruluyor (bkz. middleware/auth.ts).
|
||||
router.get('/case/:caseId/summary', requireAuth, getCaseRelaySummary);
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user