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:
parent
5917c0532c
commit
765946f0d5
19 changed files with 2711 additions and 185 deletions
243
lib/openapi.ts
243
lib/openapi.ts
|
|
@ -1,5 +1,6 @@
|
|||
// OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI
|
||||
// แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API
|
||||
// แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API + docver
|
||||
// (จัด version เอกสาร)
|
||||
// route handler GET /api/swagger (app/api/swagger/route.ts) เป็นคนเดียวที่ใช้
|
||||
// — inline เป็น JSON ลงใน HTML ตอน respond (ไม่มี endpoint spec แยก)
|
||||
//
|
||||
|
|
@ -23,8 +24,8 @@ export const openApiSpec = {
|
|||
title: "Document Template & External API ของระบบ Judge's Note",
|
||||
version: "1.0.0",
|
||||
description:
|
||||
"API ทดสอบของระบบ Judge's Note — กลุ่ม Template (DOCX Content Control) " +
|
||||
"และ API ภายนอกสำหรับ backend e-Court (N10)",
|
||||
"API ทดสอบของระบบ Judge's Note — กลุ่ม Template (DOCX Content Control), " +
|
||||
"API ภายนอกสำหรับ backend e-Court (N10) และ API จัด version เอกสาร (docver)",
|
||||
},
|
||||
servers: [{ url: BASE_PATH, description: "origin เดียวกับหน้านี้ (basePath /docedit)" }],
|
||||
tags: [
|
||||
|
|
@ -34,6 +35,13 @@ export const openApiSpec = {
|
|||
"Template เอกสาร DOCX ด้วย Content Control — ส่ง template (.docx) + data (JSON) " +
|
||||
"แล้วได้เอกสารที่กรอกค่าแล้วกลับมา",
|
||||
},
|
||||
{
|
||||
name: "documents",
|
||||
description:
|
||||
"จัด version เอกสาร (docver — docs/PLAN-DOCVER.md): เอกสารเดียวต่อ docId แต่ละ " +
|
||||
"version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — ไม่มี auth (stand-alone): " +
|
||||
"ผู้เรียกบอก userId/userName เองใน multipart",
|
||||
},
|
||||
{
|
||||
name: "Judge's Note",
|
||||
description:
|
||||
|
|
@ -217,6 +225,193 @@ export const openApiSpec = {
|
|||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/{docId}/versions": {
|
||||
post: {
|
||||
tags: ["documents"],
|
||||
summary: "อัปโหลด version ใหม่ของเอกสาร (docx + pdf)",
|
||||
description:
|
||||
"สร้างเอกสารอัตโนมัติเมื่อ docId ยังไม่เคยมี (docId ห้ามซ้ำ) แล้วเก็บไฟล์ " +
|
||||
".docx (+ .pdf ถ้าแนบมา) ลง MinIO พร้อมผู้อัปโหลด — versionNumber ที่ server จ่าย " +
|
||||
"คือค่าจริงเสมอ (client ที่เลขไม่ตรงต้องแก้ XML part ของตัวเอง) · pdf เป็น " +
|
||||
"optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มีสำเนา PDF (ดู hasPdf ใน response)",
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "รหัสเอกสารจากผู้เรียก (add-in GUID / รหัส e-Court) — [A-Za-z0-9._:-]",
|
||||
},
|
||||
],
|
||||
requestBody: {
|
||||
required: true,
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: {
|
||||
type: "object",
|
||||
required: ["file", "userId", "userName"],
|
||||
properties: {
|
||||
file: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
description: "ไฟล์ .docx ของ version นี้ (บังคับ)",
|
||||
},
|
||||
pdf: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
description:
|
||||
"ไฟล์ .pdf ของ version นี้ (optional — ไม่แนบหรือไฟล์เปล่า = ไม่มี)",
|
||||
},
|
||||
userId: {
|
||||
type: "string",
|
||||
description: "รหัสผู้อัปโหลด (ระบบ stand-alone ไม่มี session)",
|
||||
},
|
||||
userName: {
|
||||
type: "string",
|
||||
description: "ชื่อผู้อัปโหลด (แสดงใน version history)",
|
||||
},
|
||||
clientVersionId: {
|
||||
type: "string",
|
||||
description: "GUID ที่ client generate ก่อนเรียก (เก็บไว้สืบสวน ไม่ใช่ identity)",
|
||||
},
|
||||
fields: {
|
||||
type: "string",
|
||||
description:
|
||||
'JSON string ของค่า Content Control เช่น {"blackno":"คดีหมายเลขดำที่"}',
|
||||
},
|
||||
message: {
|
||||
type: "string",
|
||||
description: "commit message ของ version (≤ 2048 ตัวอักษร)",
|
||||
},
|
||||
templateId: {
|
||||
type: "string",
|
||||
description: "รหัส template ที่ใช้สร้างเอกสาร (ถ้ามี)",
|
||||
},
|
||||
fileName: {
|
||||
type: "string",
|
||||
description:
|
||||
"ชื่อไฟล์สำหรับแสดงตอนดาวน์โหลด (ไม่ส่ง = ใช้ชื่อไฟล์ที่แนบ)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "version ถูกบันทึกแล้ว",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/UploadVersionResult" },
|
||||
},
|
||||
},
|
||||
},
|
||||
400: { $ref: "#/components/responses/BadRequest" },
|
||||
413: {
|
||||
description: "ไฟล์แนบใหญ่เกิน 100MB",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
500: { $ref: "#/components/responses/ServerError" },
|
||||
},
|
||||
},
|
||||
get: {
|
||||
tags: ["documents"],
|
||||
summary: "รายการ version ทั้งหมดของเอกสาร (ใหม่ → เก่า)",
|
||||
description:
|
||||
"version ทุกตัวของ docId นี้ พร้อมผู้อัปโหลด (userId/userName), เวลา, commit " +
|
||||
"message, ค่า fields และ hasPdf — ใช้เขียน UI version history ของ add-in",
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "รหัสเอกสารจากผู้เรียก — [A-Za-z0-9._:-]",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "รายการ version เรียงใหม่ → เก่า",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: {
|
||||
type: "array",
|
||||
items: { $ref: "#/components/schemas/VersionRecord" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
400: { $ref: "#/components/responses/BadRequest" },
|
||||
404: {
|
||||
description: "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
500: { $ref: "#/components/responses/ServerError" },
|
||||
},
|
||||
},
|
||||
},
|
||||
"/api/documents/{docId}/versions/{versionId}/download-link": {
|
||||
get: {
|
||||
tags: ["documents"],
|
||||
summary: "ขอ presigned URL ดาวน์โหลดไฟล์ของ version (docx/pdf)",
|
||||
description:
|
||||
"คืน URL อายุสั้น (env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดใน " +
|
||||
"เบราว์เซอร์/Word ของผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ " +
|
||||
"ผู้ใช้เข้าถึงได้ · ขอ pdf ใน version ที่ไม่มีสำเนา = 404",
|
||||
parameters: [
|
||||
{
|
||||
name: "docId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string" },
|
||||
description: "รหัสเอกสาร (ต้องตรงกับ version นี้ — โยน id ข้ามเอกสารไม่ได้)",
|
||||
},
|
||||
{
|
||||
name: "versionId",
|
||||
in: "path",
|
||||
required: true,
|
||||
schema: { type: "string", format: "uuid" },
|
||||
description: "id ของ version (field versionId ใน VersionRecord)",
|
||||
},
|
||||
{
|
||||
name: "format",
|
||||
in: "query",
|
||||
required: false,
|
||||
schema: { type: "string", enum: ["docx", "pdf"], default: "docx" },
|
||||
description: "ชนิดไฟล์ที่จะดาวน์โหลด (ไม่ส่ง = docx)",
|
||||
},
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "presigned URL พร้อมเวลาหมดอายุ",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/DownloadLink" },
|
||||
},
|
||||
},
|
||||
},
|
||||
400: { $ref: "#/components/responses/BadRequest" },
|
||||
404: {
|
||||
description:
|
||||
"ไม่พบ version นี้ของเอกสาร หรือขอ format=pdf ใน version ที่ไม่มีสำเนา PDF",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/Error" },
|
||||
},
|
||||
},
|
||||
},
|
||||
500: { $ref: "#/components/responses/ServerError" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
|
|
@ -407,6 +602,48 @@ export const openApiSpec = {
|
|||
createdAt: { type: "string", format: "date-time" },
|
||||
},
|
||||
},
|
||||
UploadVersionResult: {
|
||||
type: "object",
|
||||
description: "ตอบกลับหลังอัปโหลด version สำเร็จ",
|
||||
properties: {
|
||||
hasPdf: { type: "boolean", description: "true = version นี้มีสำเนา PDF" },
|
||||
versionId: { type: "string", format: "uuid" },
|
||||
versionNumber: {
|
||||
type: "integer",
|
||||
description: "เลข version ที่ server จ่าย (ค่าจริง — ไม่ใช่ที่ client คาด)",
|
||||
},
|
||||
},
|
||||
},
|
||||
VersionRecord: {
|
||||
type: "object",
|
||||
description: "รายการ version หนึ่งตัว (GET /versions คืนเป็น array ใหม่ → เก่า)",
|
||||
properties: {
|
||||
versionId: { type: "string", format: "uuid" },
|
||||
versionNumber: { type: "integer" },
|
||||
createdAt: { type: "string", format: "date-time" },
|
||||
createdByUserId: { type: "string" },
|
||||
createdByUserName: { type: "string" },
|
||||
contentHash: {
|
||||
type: "string",
|
||||
description: "SHA-256 hex ของไฟล์ .docx (คำนวณฝั่ง server เสมอ)",
|
||||
},
|
||||
fields: {
|
||||
type: "object",
|
||||
nullable: true,
|
||||
description: "ค่า Content Control ตอนอัปโหลด (ไม่ส่ง = null)",
|
||||
additionalProperties: { type: "string" },
|
||||
},
|
||||
message: { type: "string", nullable: true, description: "commit message" },
|
||||
hasPdf: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
DownloadLink: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: { type: "string", description: "presigned URL ของ MinIO (GET โดยตรง)" },
|
||||
expiresAt: { type: "string", format: "date-time" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue