import { and, asc, eq } from "drizzle-orm"; import { db } from "@/db"; import { files } from "@/db/schema"; import { externalApiError, requireExternalApiKey } from "@/lib/external-api-auth"; import { loadDocumentBundles, parseQueryBody } from "@/lib/external-api"; // N10 (PLAN-API.md D2) — POST /api/external/documents // (เดิมชื่อ /api/external/query ถึงรอบสาม — เปลี่ยนชื่อตาม resource ที่คืน 2026-09-25) // เส้นเดียวถามด้วย JSON body { fileId และ/หรือ typeId, filter } — response // รูปเดียวเสมอ { documents: [bundle…] } (bundle ยุบชั้น — field เอกสารอยู่ // ระดับเดียวกับ annotations/bookmarks): ถาม fileId = ยาว 1 ชิ้น (ไม่เคยเปิด // ในระบบ = 404 — ไฟล์ url: fallback ของ N8 ค้นด้วย ecourt: prefix ไม่ได้ = // 404 ตามสัญญาลิงก์), ถาม typeId = ทุกไฟล์ในกลุ่ม N9 (กลุ่มว่าง = documents: []), // ส่งทั้งคู่ = AND เหลือเฉพาะเอกสารเดียวที่ตรงทั้งสองเงื่อนไข (2026-09-25 — // รอบเดียวกันเคยเป็น OR รวมผลแล้วเปลี่ยนหลังทดสอบจริง: ผู้เรียกต้องการเอกสาร // เดียวตามที่ส่งมา ไม่ใช่ทั้งกลุ่ม) // read-only: ไม่ publish SSE ไม่ revalidatePath — snapshot ณ ตอนเรียก const JSON_HEADERS = { "content-type": "application/json; charset=utf-8" }; /** origin เต็มของ request นี้ (ใช้ประกอบ url เต็มใน response — คำขอผู้ใช้ * 2026-09-25): เชื่อ header ของ reverse proxy ก่อน (x-forwarded-proto / * x-forwarded-host — proxy หลายชั้นส่งเป็น comma list ใช้ค่าแรก) แล้วค่อย * fallback จาก host/protocol ของ request ตรง ๆ */ function requestOrigin(req: Request): string { const url = new URL(req.url); const proto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim() ?? url.protocol.replace(":", ""); const host = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim() ?? req.headers.get("host") ?? url.host; return `${proto}://${host}`; } export async function POST(req: Request) { const unauthorized = requireExternalApiKey(req); if (unauthorized) return unauthorized; let body: unknown; try { body = await req.json(); } catch { return externalApiError("body ต้องเป็น JSON ที่ถูกต้อง", 400); } const query = parseQueryBody(body); if (!query.ok) return externalApiError(query.error, 400); const origin = requestOrigin(req); // ครอบทุก DB call (code review 2026-09-25) — ล้มไม่ว่าเหตุใด (connection // หลุด/timeout/table มีปัญหา) ต้องได้ 500 JSON ตาม contract เดียวกับ error // อื่นของ endpoint นี้ ไม่ใช่ 500 ของ framework ที่ format ต่างออกไป; log // รายละเอียดฝั่ง server อย่างเดียว — ข้อความตอบกลาง ๆ กัน leak SQL/path try { if (query.mode === "file") { // ส่งทั้งคู่ = AND กรองทั้ง externalId และกลุ่มใน query เดียว เหลือเอกสารเดียว // (external_id unique — match ได้มากสุด 1 ฉบับ) ไม่ตรง = 404 โดยแยกสาเหตุ: // ไม่เคยเปิดในระบบ หรืออยู่คนละกลุ่มกับ typeId ที่ระบุ const [file] = await db .select() .from(files) .where( query.typeId ? and( eq(files.externalId, `ecourt:${query.fileId}`), eq(files.typeId, query.typeId), ) : eq(files.externalId, `ecourt:${query.fileId}`), ) .limit(1); if (!file) { return externalApiError( query.typeId ? "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยเปิด หรือไม่ได้อยู่ในกลุ่ม typeId ที่ระบุ)" : "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยเปิด)", 404, ); } const documents = await loadDocumentBundles([file], query.parts, origin); return new Response(JSON.stringify({ documents }), { headers: JSON_HEADERS }); } const fileRows = await db .select() .from(files) .where(eq(files.typeId, query.typeId)) .orderBy(asc(files.createdAt)); const documents = await loadDocumentBundles(fileRows, query.parts, origin); return new Response(JSON.stringify({ documents }), { headers: JSON_HEADERS }); } catch (err) { console.error("external/documents: query ฐานข้อมูลล้ม", err); return externalApiError("เกิดข้อผิดพลาดของระบบ โปรดลองใหม่อีกครั้ง", 500); } }