feat(docver): add document versioning API (docx+pdf in MinIO)

Stand-alone API group at /api/documents/{docId}/versions — one document
per docId (unique), each version stores .docx + optional .pdf as MinIO
objects with uploader userId/userName/createdAt (no auth, like
api/template; callers identify themselves per docs/PLAN-DOCVER.md).

- POST versions (multipart): validates fields/magic bytes/100MB cap,
  uploads to MinIO first, cleans up objects if the DB insert fails;
  returns {hasPdf, versionId, versionNumber} with server-authoritative
  numbering (tx: upsert + SELECT FOR UPDATE, safe under parallel calls)
- GET versions: DocumentVersionDTO[] newest-first (404 for unknown docId)
- GET versions/{versionId}/download-link?format=docx|pdf: presigned URL
  (docId+versionId matched together; pdf-missing = 404)
- tables documents/document_versions + migration drizzle/0005
- docker-compose.yml: judes-note-db + judes-note-minio (quay.io image,
  host ports 19000/19001, POSTGRES_DB ecourt-note, pinned volume names)
- lib/openapi.ts: documents tag + paths for Swagger UI
- middleware: exempt api/documents from login redirect
- unit tests tests/lib/document-versioning.test.ts (23 cases)

wordaddin untouched — swap the mock later via NEXT_PUBLIC_ADDIN_API_BASE_URL.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Suphonchai Phoonsawat 2026-10-20 10:52:35 +07:00
parent 5917c0532c
commit 765946f0d5
19 changed files with 2711 additions and 185 deletions

View file

@ -0,0 +1,48 @@
// PLAN-DOCVER.md §1 — GET /api/documents/{docId}/versions/{versionId}/download-link?format=docx|pdf
// คืน presigned URL อายุสั้น (env DOCVER_LINK_TTL_SECONDS) ให้ UI เปิดในเบราว์เซอร์จริง
// ของผู้ใช้ — เช็คทั้ง docId และ versionId พร้อมกันกันโยน id ข้ามเอกสาร
import {
docverError,
parseDocId,
parseDownloadFormat,
parseVersionIdParam,
} from "@/lib/document-versioning";
import { findVersionForDoc, presignVersionUrl } from "@/lib/docver-store";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function GET(
request: Request,
{ params }: { params: { docId: string; versionId: string } },
) {
const docId = parseDocId(params.docId);
if (!docId.ok) return docverError(docId.error, 400);
const versionId = parseVersionIdParam(params.versionId);
if (!versionId.ok) return docverError(versionId.error, 400);
let format: string | null = null;
try {
format = new URL(request.url).searchParams.get("format");
} catch {
return docverError("URL ของ request ไม่ถูกต้อง", 400);
}
const parsedFormat = parseDownloadFormat(format);
if (!parsedFormat.ok) return docverError(parsedFormat.error, 400);
try {
const row = await findVersionForDoc(docId.docId, versionId.versionId);
if (!row) {
return docverError("ไม่พบ version นี้ของเอกสาร", 404);
}
if (parsedFormat.format === "pdf" && row.pdfObjectKey === null) {
return docverError("version นี้ไม่มีสำเนา PDF", 404);
}
const link = await presignVersionUrl(row.id, parsedFormat.format);
return new Response(JSON.stringify(link), {
headers: { "content-type": "application/json; charset=utf-8" },
});
} catch (err) {
console.error("docver: สร้าง download link ล้ม", err);
return docverError("เกิดข้อผิดพลาดของระบบ โปรดลองใหม่อีกครั้ง", 500);
}
}