Skip to content
Merged
31 changes: 28 additions & 3 deletions scripts/enrich-documents.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,7 +131,12 @@ async function loadEnrichmentCoverage(supabase: SupabaseAdmin, documentIds: stri
return coverage;
}

async function loadRowsForDocuments(supabase: SupabaseAdmin, table: string, select: string, documentIds: string[]) {
async function loadRowsForDocuments(
supabase: SupabaseAdmin,
table: "document_sections" | "document_memory_cards",
select: string,
documentIds: string[],
) {
const rows: MetadataRow[] = [];
for (let start = 0; start < documentIds.length; start += 5) {
const ids = documentIds.slice(start, start + 5);
Expand DownExpand Up@@ -173,8 +178,28 @@ async function loadDeepMemoryCoverage(supabase: SupabaseAdmin, documentIds: stri
}

async function loadEvidence(supabase: SupabaseAdmin, documentId: string) {
const chunks = [];
const images = [];
const chunks: Array<{
id: string;
document_id: string;
page_number: number | null;
chunk_index: number;
section_heading: string | null;
section_path: string[];
anchor_id: string | null;
content: string;
image_ids: string[];
metadata: Record<string, unknown> | null;
}> = [];
const images: Array<{
id: string;
page_number: number | null;
caption: string;
image_type: string;
labels: string[];
source_kind: string;
clinical_relevance_score: number;
metadata: Record<string, unknown> | null;
}> = [];

for (let start = 0; ; start += 1000) {
const { data, error } = await supabase
Expand Down
13 changes: 7 additions & 6 deletions src/app/api/documents/[id]/reindex/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -74,13 +74,14 @@ async function selectReindexRowsInPages<T>(args: {
searchableOnly?: boolean;
}) {
const rows: T[] = [];
if (args.searchableOnly && args.table !== "document_images") {
throw new Error("searchableOnly reindex paging only supports the document_images table.");
}
for (let offset = 0; ; offset += reindexPageSize) {
// Dynamic table/select strings need the untyped client surface.
let query = (args.supabase as unknown as SupabaseClient)
.from(args.table)
.select(args.select)
.eq("document_id", args.documentId);
if (args.searchableOnly) query = query.eq("searchable", true);
const dynamicSupabase = args.supabase as unknown as SupabaseClient;
const query = args.searchableOnly
? dynamicSupabase.from("document_images").select(args.select).eq("document_id", args.documentId).eq("searchable", true)
: dynamicSupabase.from(args.table).select(args.select).eq("document_id", args.documentId);
const { data, error } = await query.range(offset, offset + reindexPageSize - 1);
if (error) throw new Error(error.message);

Expand Down
13 changes: 7 additions & 6 deletions src/app/api/documents/bulk/reindex/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,13 +66,14 @@ async function selectRowsInPages<T>(args: {
searchableOnly?: boolean;
}) {
const rows: T[] = [];
if (args.searchableOnly && args.table !== "document_images") {
throw new Error("searchableOnly reindex paging only supports the document_images table.");
}
for (let offset = 0; ; offset += pageSize) {
// Dynamic table/select strings need the untyped client surface.
let query = (args.supabase as unknown as SupabaseClient)
.from(args.table)
.select(args.select)
.eq("document_id", args.documentId);
if (args.searchableOnly) query = query.eq("searchable", true);
const dynamicSupabase = args.supabase as unknown as SupabaseClient;
const query = args.searchableOnly
? dynamicSupabase.from("document_images").select(args.select).eq("document_id", args.documentId).eq("searchable", true)
: dynamicSupabase.from(args.table).select(args.select).eq("document_id", args.documentId);
const { data, error } = await query.range(offset, offset + pageSize - 1);
if (error) throw new Error(error.message);
const page = (data ?? []) as T[];
Expand Down
16 changes: 10 additions & 6 deletions src/app/api/upload/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import { env } from "@/lib/env";
import { assertAllowedFile, assertFileContentSignature, jsonError, PublicApiError } from "@/lib/http";
import { logger } from "@/lib/logger";
import { writeAuditLog } from "@/lib/audit";
import { planDocumentName } from "@/lib/document-naming";
import { planDocumentName, type DocumentNameSupabase } from "@/lib/document-naming";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { probeSupabaseHealth } from "@/lib/supabase/health";
Expand All@@ -27,7 +27,8 @@ export async function POST(request: Request) {

try {
supabase = createAdminClient();
const user = await requireAuthenticatedUser(request, supabase);
const adminSupabase = supabase;
const user = await requireAuthenticatedUser(request, adminSupabase);
const formData = await request.formData().catch((cause) => {
throw new PublicApiError("Invalid upload form data.", 400, {
code: "invalid_form_data",
Expand DownExpand Up@@ -57,7 +58,7 @@ export async function POST(request: Request) {
assertFileContentSignature(file.type, buffer);
const contentHash = createHash("sha256").update(buffer).digest("hex");

const { data: duplicate, error: duplicateError } = await supabase
const { data: duplicate, error: duplicateError } = await adminSupabase
.from("documents")
.select("id,title,file_name,status,page_count,chunk_count,image_count,created_at")
.eq("owner_id", user.id)
Expand All@@ -74,19 +75,22 @@ export async function POST(request: Request) {
});
}

const health = await probeSupabaseHealth(supabase);
const health = await probeSupabaseHealth(adminSupabase);
if (!health.ok) return NextResponse.json({ error: `Upload is paused. ${health.message}` }, { status: 503 });

const upload = await supabase.storage.from(env.SUPABASE_DOCUMENT_BUCKET).upload(storagePath, buffer, {
const upload = await adminSupabase.storage.from(env.SUPABASE_DOCUMENT_BUCKET).upload(storagePath, buffer, {
contentType: file.type,
upsert: false,
});

if (upload.error) throw new Error(upload.error.message);
uploadedPath = storagePath;

const namingSupabase: DocumentNameSupabase = {
from: ((table) => adminSupabase.from(table)) as DocumentNameSupabase["from"],
};
const namePlan = await planDocumentName({
supabase,
supabase: namingSupabase,
ownerId: user.id,
fileName: file.name,
requestedTitle: uploadMetadata.title,
Expand Down
15 changes: 11 additions & 4 deletions src/lib/document-naming.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "@/lib/supabase/database.types";

export type DocumentNamePlan = {
title: string;
baseTitle: string;
Expand All@@ -19,6 +16,16 @@ export type ExistingDocumentName = {
metadata?: unknown;
};

export type DocumentNameSupabase = {
from: (table: "documents") => {
select: (columns: string) => {
eq: (column: "owner_id", value: string) => {
limit: (count: number) => PromiseLike<{ data: unknown[] | null; error: { message: string } | null }>;
};
};
};
};

const titleAbbreviations = new Map<string, string>([
["admin", "Administering"],
["assoc", "Associated"],
Expand DownExpand Up@@ -153,7 +160,7 @@ function uniqueTitle(
}

export async function planDocumentName(args: {
supabase?: SupabaseClient<Database>;
supabase?: DocumentNameSupabase;
ownerId: string;
fileName: string;
requestedTitle?: string | null;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/evidence.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,7 @@ import type {
ConflictOrGap,
DocumentBreakdown,
EvidenceSummary,
ImageEvidenceCategory,
QuoteCard,
SearchResult,
SmartPanel,
Expand DownExpand Up@@ -391,7 +392,7 @@ export function buildVisualEvidence(results: SearchResult[], limit = 8) {
source_chunk_id: result.id,
chunk_index: result.chunk_index,
viewer_href: `/documents/${result.document_id}?page=${pageNumber ?? 1}&chunk=${result.id}`,
image_type: image.image_type,
image_type: image.image_type as ImageEvidenceCategory | undefined,
clinical_relevance_score: image.clinical_relevance_score,
source_kind: sourceKind,
tableLabel: image.tableLabel ?? metadataText(metadata, "table_label"),
Expand Down
10 changes: 5 additions & 5 deletions src/lib/rag.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -1790,11 +1790,11 @@ export function invalidateRagCachesForOwner(ownerId?: string | null) {
}
void (async () => {
try {
const deletion = createAdminClient().from("rag_response_cache").delete();
await (sharedCacheOwnerId ? deletion.eq("owner_id", sharedCacheOwnerId) : deletion.is("owner_id", null)).in(
"cache_kind",
["search", "answer"],
);
const deleteQuery = createAdminClient().from("rag_response_cache").delete();
const scopedQuery = sharedCacheOwnerId
? deleteQuery.eq("owner_id", sharedCacheOwnerId)
: deleteQuery.is("owner_id", null);
await scopedQuery.in("cache_kind", ["search", "answer"]);
} catch (error) {
// Shared cache invalidation is best effort.
console.warn("Shared cache invalidation failed for owner:", error);
Expand Down
Loading
Loading