diff --git a/drizzle/0002_contract_analysis.sql b/drizzle/0002_contract_analysis.sql index 7d861a4..b05a863 100644 --- a/drizzle/0002_contract_analysis.sql +++ b/drizzle/0002_contract_analysis.sql @@ -70,10 +70,10 @@ CREATE INDEX contract_clauses_rating_idx ON contract_clauses(rating); ALTER TABLE contract_documents ENABLE ROW LEVEL SECURITY; CREATE POLICY contract_documents_tenant_isolation ON contract_documents - USING (tenant_id = current_setting('app.tenant_id')::uuid); + USING (tenant_id = current_setting('app.tenant_id', true)::uuid); CREATE POLICY contract_documents_tenant_insert ON contract_documents - FOR INSERT WITH CHECK (tenant_id = current_setting('app.tenant_id')::uuid); + FOR INSERT WITH CHECK (tenant_id = current_setting('app.tenant_id', true)::uuid); -- RLS policies for contract_clauses (via document join) ALTER TABLE contract_clauses ENABLE ROW LEVEL SECURITY; @@ -81,7 +81,11 @@ ALTER TABLE contract_clauses ENABLE ROW LEVEL SECURITY; CREATE POLICY contract_clauses_tenant_isolation ON contract_clauses USING (document_id IN ( SELECT id FROM contract_documents - WHERE tenant_id = current_setting('app.tenant_id')::uuid + WHERE tenant_id = current_setting('app.tenant_id', true)::uuid )); +-- Force RLS for the app role (even table owners are subject to policies) +ALTER TABLE contract_documents FORCE ROW LEVEL SECURITY; +ALTER TABLE contract_clauses FORCE ROW LEVEL SECURITY; + -- Standard clauses are shared reference data (no RLS needed) diff --git a/src/app/api/analyses/[id]/route.ts b/src/app/api/analyses/[id]/route.ts index 2e7afe9..73a3cd0 100644 --- a/src/app/api/analyses/[id]/route.ts +++ b/src/app/api/analyses/[id]/route.ts @@ -1,9 +1,11 @@ // GET /api/analyses/:id — Retrieve a single analysis with its sources import { type NextRequest } from 'next/server'; -import { db } from '@/lib/db'; +import { withTenantDb } from '@/lib/db'; import { analyses, norms, normInstruments, decisions } from '@/lib/db/schema'; import { eq, inArray } from 'drizzle-orm'; +import { logAuditEvent } from '@/lib/auth/audit'; +import type { TenantContext } from '@/lib/auth'; export async function GET( request: NextRequest, @@ -16,60 +18,72 @@ export async function GET( return Response.json({ error: 'Missing x-tenant-id header' }, { status: 401 }); } - const [analysis] = await db - .select() - .from(analyses) - .where(eq(analyses.id, id)) - .limit(1); + // RLS enforces tenant isolation — only rows matching app.tenant_id are visible. + const result = await withTenantDb(tenantId, async (tdb) => { + const [analysis] = await tdb + .select() + .from(analyses) + .where(eq(analyses.id, id)) + .limit(1); - if (!analysis) { + if (!analysis) return null; + + // Fetch referenced norms and decisions + const sources = analysis.sources as { + normIds: string[]; + decisionIds: string[]; + otherSources: string[]; + } | null; + + let referencedNorms: any[] = []; + let referencedDecisions: any[] = []; + + if (sources?.normIds?.length) { + referencedNorms = await tdb + .select({ + id: norms.id, + paragraph: norms.paragraph, + title: norms.title, + instrumentAbbreviation: normInstruments.abbreviation, + sourceRank: normInstruments.sourceRank, + }) + .from(norms) + .innerJoin(normInstruments, eq(norms.instrumentId, normInstruments.id)) + .where(inArray(norms.id, sources.normIds)); + } + + if (sources?.decisionIds?.length) { + referencedDecisions = await tdb + .select({ + id: decisions.id, + caseReference: decisions.caseReference, + court: decisions.court, + decisionDate: decisions.decisionDate, + headnote: decisions.headnote, + }) + .from(decisions) + .where(inArray(decisions.id, sources.decisionIds)); + } + + return { analysis, referencedNorms, referencedDecisions }; + }); + + if (!result) { return Response.json({ error: 'Analysis not found' }, { status: 404 }); } - if (analysis.tenantId !== tenantId) { - return Response.json({ error: 'Forbidden' }, { status: 403 }); - } - - // Fetch referenced norms and decisions - const sources = analysis.sources as { - normIds: string[]; - decisionIds: string[]; - otherSources: string[]; - } | null; - - let referencedNorms: any[] = []; - let referencedDecisions: any[] = []; - - if (sources?.normIds?.length) { - referencedNorms = await db - .select({ - id: norms.id, - paragraph: norms.paragraph, - title: norms.title, - instrumentAbbreviation: normInstruments.abbreviation, - sourceRank: normInstruments.sourceRank, - }) - .from(norms) - .innerJoin(normInstruments, eq(norms.instrumentId, normInstruments.id)) - .where(inArray(norms.id, sources.normIds)); - } - - if (sources?.decisionIds?.length) { - referencedDecisions = await db - .select({ - id: decisions.id, - caseReference: decisions.caseReference, - court: decisions.court, - decisionDate: decisions.decisionDate, - headnote: decisions.headnote, - }) - .from(decisions) - .where(inArray(decisions.id, sources.decisionIds)); - } + const userId = request.headers.get('x-user-id') ?? 'unknown'; + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? request.headers.get('x-real-ip') + ?? undefined; + await logAuditEvent( + { tenantId, userId } as TenantContext, + 'read', 'analysis', id, undefined, ip, + ); return Response.json({ - ...analysis, - referencedNorms, - referencedDecisions, + ...result.analysis, + referencedNorms: result.referencedNorms, + referencedDecisions: result.referencedDecisions, }); } diff --git a/src/app/api/analyses/route.ts b/src/app/api/analyses/route.ts index 93a2532..3eed163 100644 --- a/src/app/api/analyses/route.ts +++ b/src/app/api/analyses/route.ts @@ -2,25 +2,20 @@ // GET /api/analyses — List analyses for the current tenant import { type NextRequest } from 'next/server'; -import { db } from '@/lib/db'; +import { withTenantDb } from '@/lib/db'; import { analyses } from '@/lib/db/schema'; -import { eq, desc } from 'drizzle-orm'; +import { desc } from 'drizzle-orm'; import { runAnalysis } from '@/lib/ai/analysis'; import { AnalyseMode } from '@/types'; +import { logAuditEvent } from '@/lib/auth/audit'; +import { requirePermission } from '@/lib/auth/rbac'; const VALID_MODES = new Set(Object.values(AnalyseMode)); export async function POST(request: NextRequest) { - // TODO: Replace with real auth once DPO implements AIIA-19 - const tenantId = request.headers.get('x-tenant-id'); - const userId = request.headers.get('x-user-id'); - - if (!tenantId || !userId) { - return Response.json( - { error: 'Missing x-tenant-id or x-user-id header' }, - { status: 401 }, - ); - } + const auth = await requirePermission('analyses:create'); + if ('response' in auth) return auth.response; + const { ctx } = auth; const body = await request.json(); const { mode, title, query, caseId, normIds, decisionIds, stichtag } = body; @@ -40,8 +35,8 @@ export async function POST(request: NextRequest) { } const { analysisId, stream } = await runAnalysis({ - tenantId, - userId, + tenantId: ctx.tenantId, + userId: ctx.userId, caseId, mode, title, @@ -51,6 +46,11 @@ export async function POST(request: NextRequest) { stichtag, }); + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? request.headers.get('x-real-ip') + ?? undefined; + await logAuditEvent(ctx, 'create', 'analysis', analysisId, { mode, title }, ip); + // Return streaming response with analysis ID in header const response = stream.toTextStreamResponse(); response.headers.set('X-Analysis-Id', analysisId); @@ -58,22 +58,39 @@ export async function POST(request: NextRequest) { } export async function GET(request: NextRequest) { - const tenantId = request.headers.get('x-tenant-id'); - if (!tenantId) { - return Response.json({ error: 'Missing x-tenant-id header' }, { status: 401 }); - } + const auth = await requirePermission('analyses:read'); + if ('response' in auth) return auth.response; + const { ctx } = auth; const searchParams = request.nextUrl.searchParams; const limit = Math.min(parseInt(searchParams.get('limit') ?? '20', 10), 100); const offset = parseInt(searchParams.get('offset') ?? '0', 10); - const results = await db - .select() - .from(analyses) - .where(eq(analyses.tenantId, tenantId)) - .orderBy(desc(analyses.createdAt)) - .limit(limit) - .offset(offset); + // DSGVO Art. 5(1)(c) Datenminimierung: nur Metadaten zurückgeben. + // Vollständige Daten (query, result) nur über GET /api/analyses/[id]. + // RLS enforces tenant isolation via app.tenant_id — no manual WHERE needed. + const results = await withTenantDb(ctx.tenantId, async (tdb) => + tdb + .select({ + id: analyses.id, + title: analyses.title, + mode: analyses.mode, + status: analyses.status, + createdAt: analyses.createdAt, + updatedAt: analyses.updatedAt, + }) + .from(analyses) + .orderBy(desc(analyses.createdAt)) + .limit(limit) + .offset(offset), + ); + + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? request.headers.get('x-real-ip') + ?? undefined; + await logAuditEvent( + ctx, 'list', 'analysis', null, { limit, offset }, ip, + ); return Response.json(results); } diff --git a/src/app/api/contracts/[id]/analyze/route.ts b/src/app/api/contracts/[id]/analyze/route.ts index 7fb04b6..8b232d7 100644 --- a/src/app/api/contracts/[id]/analyze/route.ts +++ b/src/app/api/contracts/[id]/analyze/route.ts @@ -1,41 +1,37 @@ // POST /api/contracts/:id/analyze — Trigger text extraction and clause analysis import { type NextRequest } from 'next/server'; -import { db } from '@/lib/db'; +import { withTenantDb } from '@/lib/db'; import { contractDocuments } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; import { extractDocumentText, analyzeContractClauses } from '@/lib/contracts'; +import { logAuditEvent } from '@/lib/auth/audit'; +import { requirePermission } from '@/lib/auth/rbac'; export async function POST( request: NextRequest, { params }: { params: Promise<{ id: string }> }, ) { - const tenantId = request.headers.get('x-tenant-id'); - const userId = request.headers.get('x-user-id'); - - if (!tenantId || !userId) { - return Response.json( - { error: 'Missing x-tenant-id or x-user-id header' }, - { status: 401 }, - ); - } + const auth = await requirePermission('analyses:create'); + if ('response' in auth) return auth.response; + const { ctx } = auth; const { id } = await params; - const [doc] = await db - .select() - .from(contractDocuments) - .where(eq(contractDocuments.id, id)) - .limit(1); + // RLS enforces tenant isolation — only documents belonging to ctx.tenantId are visible. + const doc = await withTenantDb(ctx.tenantId, async (tdb) => { + const [d] = await tdb + .select() + .from(contractDocuments) + .where(eq(contractDocuments.id, id)) + .limit(1); + return d ?? null; + }); if (!doc) { return Response.json({ error: 'Dokument nicht gefunden' }, { status: 404 }); } - if (doc.tenantId !== tenantId) { - return Response.json({ error: 'Zugriff verweigert' }, { status: 403 }); - } - if (doc.status === 'analyzing' || doc.status === 'extracting') { return Response.json( { error: 'Analyse läuft bereits', status: doc.status }, @@ -46,11 +42,18 @@ export async function POST( try { // Step 1: Extract text if not yet done if (!doc.extractedText) { - await extractDocumentText(id); + await extractDocumentText(ctx.tenantId, id); } // Step 2: Run clause analysis - await analyzeContractClauses(id); + await analyzeContractClauses(ctx.tenantId, id); + + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? request.headers.get('x-real-ip') + ?? undefined; + await logAuditEvent( + ctx, 'analyze', 'contract', id, undefined, ip, + ); return Response.json({ documentId: id, @@ -59,14 +62,16 @@ export async function POST( }); } catch (err) { const message = err instanceof Error ? err.message : 'Analyse fehlgeschlagen'; - await db - .update(contractDocuments) - .set({ - status: 'failed', - errorMessage: message, - updatedAt: new Date(), - }) - .where(eq(contractDocuments.id, id)); + await withTenantDb(ctx.tenantId, async (tdb) => { + await tdb + .update(contractDocuments) + .set({ + status: 'failed', + errorMessage: message, + updatedAt: new Date(), + }) + .where(eq(contractDocuments.id, id)); + }); return Response.json({ error: message }, { status: 500 }); } diff --git a/src/app/api/contracts/[id]/route.ts b/src/app/api/contracts/[id]/route.ts index 4e113e6..ed9a1da 100644 --- a/src/app/api/contracts/[id]/route.ts +++ b/src/app/api/contracts/[id]/route.ts @@ -2,6 +2,8 @@ import { type NextRequest } from 'next/server'; import { getContractAnalysis } from '@/lib/contracts'; +import { logAuditEvent } from '@/lib/auth/audit'; +import type { TenantContext } from '@/lib/auth'; export async function GET( request: NextRequest, @@ -13,15 +15,22 @@ export async function GET( } const { id } = await params; - const result = await getContractAnalysis(id); + + // RLS enforces tenant isolation — getContractAnalysis uses withTenantDb internally. + const result = await getContractAnalysis(tenantId, id); if (!result) { return Response.json({ error: 'Dokument nicht gefunden' }, { status: 404 }); } - if (result.document.tenantId !== tenantId) { - return Response.json({ error: 'Zugriff verweigert' }, { status: 403 }); - } + const userId = request.headers.get('x-user-id') ?? 'unknown'; + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? request.headers.get('x-real-ip') + ?? undefined; + await logAuditEvent( + { tenantId, userId } as TenantContext, + 'read', 'contract', id, undefined, ip, + ); // Omit extracted text and storage path from response for security const { storagePath, extractedText, ...documentMeta } = result.document; diff --git a/src/app/api/contracts/route.ts b/src/app/api/contracts/route.ts index a7516e5..2106577 100644 --- a/src/app/api/contracts/route.ts +++ b/src/app/api/contracts/route.ts @@ -6,19 +6,15 @@ import { uploadContractDocument, listContractDocuments, } from '@/lib/contracts'; +import { logAuditEvent } from '@/lib/auth/audit'; +import { requirePermission } from '@/lib/auth/rbac'; const MAX_UPLOAD_SIZE = 10 * 1024 * 1024; // 10 MB export async function POST(request: NextRequest) { - const tenantId = request.headers.get('x-tenant-id'); - const userId = request.headers.get('x-user-id'); - - if (!tenantId || !userId) { - return Response.json( - { error: 'Missing x-tenant-id or x-user-id header' }, - { status: 401 }, - ); - } + const auth = await requirePermission('cases:edit'); + if ('response' in auth) return auth.response; + const { ctx } = auth; const formData = await request.formData(); const file = formData.get('file'); @@ -38,13 +34,22 @@ export async function POST(request: NextRequest) { ); } + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? request.headers.get('x-real-ip') + ?? undefined; + try { const result = await uploadContractDocument( - tenantId, - userId, + ctx.tenantId, + ctx.userId, file, caseId ?? undefined, ); + + await logAuditEvent( + ctx, 'create', 'contract', result.documentId, { fileName: file.name, caseId }, ip, + ); + return Response.json(result, { status: 201 }); } catch (err) { const message = err instanceof Error ? err.message : 'Upload fehlgeschlagen'; @@ -53,15 +58,22 @@ export async function POST(request: NextRequest) { } export async function GET(request: NextRequest) { - const tenantId = request.headers.get('x-tenant-id'); - if (!tenantId) { - return Response.json({ error: 'Missing x-tenant-id header' }, { status: 401 }); - } + const auth = await requirePermission('cases:read'); + if ('response' in auth) return auth.response; + const { ctx } = auth; const searchParams = request.nextUrl.searchParams; const limit = Math.min(parseInt(searchParams.get('limit') ?? '20', 10), 100); const offset = parseInt(searchParams.get('offset') ?? '0', 10); - const documents = await listContractDocuments(tenantId, limit, offset); + const documents = await listContractDocuments(ctx.tenantId, limit, offset); + + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() + ?? request.headers.get('x-real-ip') + ?? undefined; + await logAuditEvent( + ctx, 'list', 'contract', null, { limit, offset }, ip, + ); + return Response.json(documents); } diff --git a/src/lib/contracts/index.ts b/src/lib/contracts/index.ts index 5b3b8f8..4ef94a9 100644 --- a/src/lib/contracts/index.ts +++ b/src/lib/contracts/index.ts @@ -1,7 +1,8 @@ // Contract Analysis Module — document upload, text extraction, clause analysis // Handles PDF/DOCX text extraction and AI-powered clause identification +// All DB access uses withTenantDb() for RLS-based tenant isolation (DSGVO Art. 32) -import { db } from '@/lib/db'; +import { withTenantDb } from '@/lib/db'; import { contractDocuments, contractClauses, @@ -82,20 +83,23 @@ export async function uploadContractDocument( const deleteAfter = new Date(); deleteAfter.setDate(deleteAfter.getDate() + 90); - const [doc] = await db - .insert(contractDocuments) - .values({ - tenantId, - userId, - caseId: caseId ?? null, - filename: file.name, - mimeType: file.type, - fileSizeBytes: file.size, - storagePath: filePath, - status: 'uploaded', - deleteAfter, - }) - .returning(); + const doc = await withTenantDb(tenantId, async (tdb) => { + const [d] = await tdb + .insert(contractDocuments) + .values({ + tenantId, + userId, + caseId: caseId ?? null, + filename: file.name, + mimeType: file.type, + fileSizeBytes: file.size, + storagePath: filePath, + status: 'uploaded', + deleteAfter, + }) + .returning(); + return d; + }); return { documentId: doc.id, @@ -108,19 +112,24 @@ export async function uploadContractDocument( * Extract text from a contract document (PDF or DOCX). * Updates the document record with extracted text. */ -export async function extractDocumentText(documentId: string): Promise { - const [doc] = await db - .select() - .from(contractDocuments) - .where(eq(contractDocuments.id, documentId)) - .limit(1); +export async function extractDocumentText(tenantId: string, documentId: string): Promise { + const doc = await withTenantDb(tenantId, async (tdb) => { + const [d] = await tdb + .select() + .from(contractDocuments) + .where(eq(contractDocuments.id, documentId)) + .limit(1); + return d ?? null; + }); if (!doc) throw new Error('Dokument nicht gefunden'); - await db - .update(contractDocuments) - .set({ status: 'extracting', updatedAt: new Date() }) - .where(eq(contractDocuments.id, documentId)); + await withTenantDb(tenantId, async (tdb) => { + await tdb + .update(contractDocuments) + .set({ status: 'extracting', updatedAt: new Date() }) + .where(eq(contractDocuments.id, documentId)); + }); const fs = await import('node:fs/promises'); const fileBuffer = await fs.readFile(doc.storagePath); @@ -139,14 +148,16 @@ export async function extractDocumentText(documentId: string): Promise { text = result.value; } - await db - .update(contractDocuments) - .set({ - extractedText: text, - status: 'extracted', - updatedAt: new Date(), - }) - .where(eq(contractDocuments.id, documentId)); + await withTenantDb(tenantId, async (tdb) => { + await tdb + .update(contractDocuments) + .set({ + extractedText: text, + status: 'extracted', + updatedAt: new Date(), + }) + .where(eq(contractDocuments.id, documentId)); + }); return text; } @@ -155,32 +166,41 @@ export async function extractDocumentText(documentId: string): Promise { * Run AI-powered clause analysis on an extracted contract document. * Identifies clauses, categorizes them, compares with standards, and rates them. */ -export async function analyzeContractClauses(documentId: string): Promise { - const [doc] = await db - .select() - .from(contractDocuments) - .where(eq(contractDocuments.id, documentId)) - .limit(1); +export async function analyzeContractClauses(tenantId: string, documentId: string): Promise { + const doc = await withTenantDb(tenantId, async (tdb) => { + const [d] = await tdb + .select() + .from(contractDocuments) + .where(eq(contractDocuments.id, documentId)) + .limit(1); + return d ?? null; + }); if (!doc) throw new Error('Dokument nicht gefunden'); if (!doc.extractedText) throw new Error('Text wurde noch nicht extrahiert'); - await db - .update(contractDocuments) - .set({ status: 'analyzing', updatedAt: new Date() }) - .where(eq(contractDocuments.id, documentId)); + await withTenantDb(tenantId, async (tdb) => { + await tdb + .update(contractDocuments) + .set({ status: 'analyzing', updatedAt: new Date() }) + .where(eq(contractDocuments.id, documentId)); + }); // Fetch standard clauses for comparison context - const standards = await db - .select({ - id: standardClauses.id, - category: standardClauses.category, - label: standardClauses.label, - body: standardClauses.body, - instrumentAbbr: normInstruments.abbreviation, - }) - .from(standardClauses) - .innerJoin(normInstruments, eq(standardClauses.instrumentId, normInstruments.id)); + // standard_clauses have no RLS (shared reference data), but we access via + // withTenantDb to keep all queries on the tenant-scoped connection. + const standards = await withTenantDb(tenantId, async (tdb) => + tdb + .select({ + id: standardClauses.id, + category: standardClauses.category, + label: standardClauses.label, + body: standardClauses.body, + instrumentAbbr: normInstruments.abbreviation, + }) + .from(standardClauses) + .innerJoin(normInstruments, eq(standardClauses.instrumentId, normInstruments.id)), + ); const standardsContext = standards.length > 0 ? standards @@ -229,14 +249,16 @@ Antworte NUR mit einem JSON-Array von Klausel-Objekten. Kein Fließtext.`, if (!jsonMatch) throw new Error('No JSON array found in AI response'); clauses = JSON.parse(jsonMatch[0]); } catch { - await db - .update(contractDocuments) - .set({ - status: 'failed', - errorMessage: 'KI-Antwort konnte nicht geparst werden', - updatedAt: new Date(), - }) - .where(eq(contractDocuments.id, documentId)); + await withTenantDb(tenantId, async (tdb) => { + await tdb + .update(contractDocuments) + .set({ + status: 'failed', + errorMessage: 'KI-Antwort konnte nicht geparst werden', + updatedAt: new Date(), + }) + .where(eq(contractDocuments.id, documentId)); + }); return; } @@ -247,47 +269,51 @@ Antworte NUR mit einem JSON-Array von Klausel-Objekten. Kein Fließtext.`, // Insert extracted clauses const validRatings = new Set(['standard', 'abweichend', 'kritisch', 'unbekannt']); - for (const clause of clauses) { - const rating = validRatings.has(clause.rating) ? clause.rating : 'unbekannt'; - const matchedStandardId = standardsByCategory.get(clause.category) ?? null; + await withTenantDb(tenantId, async (tdb) => { + for (const clause of clauses) { + const rating = validRatings.has(clause.rating) ? clause.rating : 'unbekannt'; + const matchedStandardId = standardsByCategory.get(clause.category) ?? null; - await db.insert(contractClauses).values({ - documentId, - category: clause.category, - extractedText: clause.extractedText, - standardClauseId: matchedStandardId, - rating: rating as 'standard' | 'abweichend' | 'kritisch' | 'unbekannt', - analysis: clause.analysis, - deviations: clause.deviations ?? [], - riskScore: Math.max(0, Math.min(100, clause.riskScore ?? 0)), - }); - } + await tdb.insert(contractClauses).values({ + documentId, + category: clause.category, + extractedText: clause.extractedText, + standardClauseId: matchedStandardId, + rating: rating as 'standard' | 'abweichend' | 'kritisch' | 'unbekannt', + analysis: clause.analysis, + deviations: clause.deviations ?? [], + riskScore: Math.max(0, Math.min(100, clause.riskScore ?? 0)), + }); + } - await db - .update(contractDocuments) - .set({ status: 'completed', updatedAt: new Date() }) - .where(eq(contractDocuments.id, documentId)); + await tdb + .update(contractDocuments) + .set({ status: 'completed', updatedAt: new Date() }) + .where(eq(contractDocuments.id, documentId)); + }); } /** * Get a contract document with its analyzed clauses. */ -export async function getContractAnalysis(documentId: string) { - const [doc] = await db - .select() - .from(contractDocuments) - .where(eq(contractDocuments.id, documentId)) - .limit(1); +export async function getContractAnalysis(tenantId: string, documentId: string) { + return withTenantDb(tenantId, async (tdb) => { + const [doc] = await tdb + .select() + .from(contractDocuments) + .where(eq(contractDocuments.id, documentId)) + .limit(1); - if (!doc) return null; + if (!doc) return null; - const clauses = await db - .select() - .from(contractClauses) - .where(eq(contractClauses.documentId, documentId)) - .orderBy(contractClauses.category); + const clauses = await tdb + .select() + .from(contractClauses) + .where(eq(contractClauses.documentId, documentId)) + .orderBy(contractClauses.category); - return { document: doc, clauses }; + return { document: doc, clauses }; + }); } /** @@ -298,19 +324,20 @@ export async function listContractDocuments( limit = 20, offset = 0, ) { - return db - .select({ - id: contractDocuments.id, - filename: contractDocuments.filename, - mimeType: contractDocuments.mimeType, - fileSizeBytes: contractDocuments.fileSizeBytes, - status: contractDocuments.status, - caseId: contractDocuments.caseId, - createdAt: contractDocuments.createdAt, - }) - .from(contractDocuments) - .where(eq(contractDocuments.tenantId, tenantId)) - .orderBy(desc(contractDocuments.createdAt)) - .limit(limit) - .offset(offset); + return withTenantDb(tenantId, async (tdb) => + tdb + .select({ + id: contractDocuments.id, + filename: contractDocuments.filename, + mimeType: contractDocuments.mimeType, + fileSizeBytes: contractDocuments.fileSizeBytes, + status: contractDocuments.status, + caseId: contractDocuments.caseId, + createdAt: contractDocuments.createdAt, + }) + .from(contractDocuments) + .orderBy(desc(contractDocuments.createdAt)) + .limit(limit) + .offset(offset), + ); }