feat: enhance document versioning and metadata handling in Word add-in
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m21s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Successful in 4s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Successful in 1s

- Updated API documentation for document versioning to clarify usage and requirements.
- Improved metadata service to support all content control types in Word API 1.5+.
- Added tests for reading content control fields to ensure compatibility with various Word versions.
- Refactored template service to utilize shared metadata functions for better maintainability.
This commit is contained in:
Suphonchai Phoonsawat 2026-10-28 11:22:52 +07:00
parent 677de2fc29
commit 3726cb742f
7 changed files with 187 additions and 58 deletions

View file

@ -37,13 +37,20 @@ export const openApiSpec = {
{
name: "documents",
description:
"จัด version เอกสาร (docver): เอกสารเดียวต่อ docId แต่ละ " +
"version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — auth ด้วย access_token " +
"ของกลุ่มนี้: ขอที่ POST /api/documents/token ได้ 2 ทาง (x-api-key + body claims " +
"สำหรับระบบภายนอก หรือ body ticket_data+HmacSha256 สำหรับ add-in — PLAN-ADDIN-AUTH.md) " +
"แล้วแนบ Authorization: Bearer ทุก request — identity/template " +
"และ docId (= fid — fileId ของ e-Court, string) แกะจาก claims ของ token ทั้งหมด " +
"ไม่รับจาก URL/multipart form อีก",
"จัด version เอกสาร Word (.docx + .pdf) — ใช้งานจริงโดย Word add-in " +
'"e-Court Documents Versioning" และระบบภายนอกที่ต้องการดึงไฟล์\n\n' +
"**แนวคิด**\n" +
"- เอกสารหนึ่งฉบับถูกระบุด้วย `docId` ตัวเดียว ซึ่งก็คือ `fid` (fileId ของ e-Court)\n" +
"- ผู้ใช้กดบันทึกใน Word แต่ละครั้ง = เพิ่ม 1 version ที่เก็บไฟล์ .docx " +
"(+ สำเนา .pdf ถ้ามี) พร้อมชื่อผู้อัปโหลดและเวลา — ไฟล์เก่าของ version ก่อนหน้าไม่ถูกแก้\n\n" +
"**วิธีเรียกใช้ 3 ขั้น**\n" +
"1. ขอ `access_token` ที่ `POST /api/documents/token`\n" +
"2. แนบ `Authorization: Bearer <access_token>` กับทุก request ถัดไปของกลุ่มนี้\n" +
"3. เรียก endpoint ที่ต้องการ: อัปโหลด version ใหม่ / ดูประวัติ version / ขอลิงก์ดาวน์โหลด\n\n" +
"**ข้อควรรู้**\n" +
"- identity (userId/userName), templateId และ docId อ่านจาก claims ใน token ทั้งหมด — " +
"ไม่รับจาก URL หรือ form (ส่งมา = 400)\n" +
"- อัปโหลด version แรกของ docId ที่ยังไม่เคยมี = ระบบสร้างเอกสารให้อัตโนมัติ",
},
{
name: "Judge's Note",
@ -613,16 +620,18 @@ export const openApiSpec = {
tags: ["documents"],
summary: "ขอ access_token ของกลุ่ม documents (docver)",
description:
"ออก access_token ใช้กับ endpoint อื่นของกลุ่ม documents เท่านั้น — token เป็น " +
"JWT HS256 ฝัง claims {userid, username, fid, template} อายุตาม env " +
"DOCVER_TOKEN_TTL_SECONDS (default 3600 วินาที) · identity (userId/userName), " +
"templateId และ docId (= fid) ตอนทำงานแกะจาก claims ของ token นี้ — " +
"fid จึงบังคับ · ทางเข้า 2 ทาง (header x-api-key เป็นตัวตั้ง): " +
"(1) ระบบภายนอก server-to-server — header x-api-key (env DOCVER_API_KEY) + body " +
"claims ตรง ๆ (2) add-in ใน Word — ไม่มี x-api-key + body {ticket_data, " +
"HmacSha256} ตามที่ e-Court stamp มากับไฟล์ — server ตรวจ HMAC-SHA256 ด้วย env " +
"ADDIN_TICKET_SECRET (ไม่ตั้ง = env SECRET ไม่ตั้ง = \"super_secret\") และ exp " +
"ของ ticket ก่อนออก token จาก claims ในตัว ticket เอง",
"ออก `access_token` (JWT HS256) ไว้เรียก endpoint อื่นของกลุ่ม documents — " +
"token ฝัง claims `{userid, username, fid, template}` อายุตาม env " +
"`DOCVER_TOKEN_TTL_SECONDS` (default 3600 วินาที)\n\n" +
"**ทางเข้า 2 แบบ** — เลือกตามว่าใครเป็นคนเรียก (ดูตัวอย่างใน Request body ได้ทั้งคู่):\n\n" +
"1. **ระบบภายนอก (server-to-server)** — แนบ header `x-api-key` (ค่าจาก env " +
"`DOCVER_API_KEY`) แล้วส่ง claims ตรง ๆ ใน body ตาม schema `TokenRequest`\n" +
"2. **add-in ใน Word** — ไม่ต้องมี `x-api-key` ส่ง body `{ticket_data, HmacSha256}` " +
"ตาม schema `TicketTokenRequest` (คู่ค่าที่ e-Court stamp ไว้ใน custom properties " +
"ของไฟล์ .docx) — server ตรวจลายเซ็น HMAC-SHA256 ด้วย env `ADDIN_TICKET_SECRET` " +
'(ไม่ตั้ง → env `SECRET` → `"super_secret"`) และเวลาหมดอายุ (`exp`) ของ ticket ' +
"ก่อน แล้วออก token จาก claims ที่อยู่ในตัว ticket เอง\n\n" +
"ได้ token แล้วแนบเป็น `Authorization: Bearer <token>` กับทุก request ถัดไป",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
@ -687,12 +696,15 @@ export const openApiSpec = {
tags: ["documents"],
summary: "อัปโหลด version ใหม่ของเอกสาร (docx + pdf)",
description:
"เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — สร้างเอกสาร " +
"อัตโนมัติเมื่อ docId ยังไม่เคยมี แล้วเก็บไฟล์ .docx (+ .pdf ถ้าแนบมา) ลง MinIO " +
"พร้อมผู้อัปโหลด — versionNumber ที่ server จ่ายคือค่าจริงเสมอ (client ที่เลขไม่ตรง " +
"ต้องแก้ XML part ของตัวเอง) · pdf เป็น optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มี " +
"สำเนา PDF (ดู hasPdf ใน response) · ผู้อัปโหลด (userId/userName) และ templateId " +
"แกะจาก Bearer token ไม่รับใน form (ส่ง field เหล่านั้นมา = 400)",
"บันทึกไฟล์ปัจจุบันของเอกสารเป็น **version ใหม่** — เอกสารถูกระบุด้วย `fid` " +
"ใน Bearer token ถ้า docId นี้ยังไม่เคยมีในระบบ จะสร้างเอกสารให้อัตโนมัติ (version แรก = 1)\n\n" +
"- ไฟล์เก็บใน MinIO แยก object ต่อ version พร้อมบันทึกผู้อัปโหลดจาก claims ใน token\n" +
"- `pdf` เป็น optional — ไม่แนบหรือแนบไฟล์เปล่า = version นี้ไม่มีสำเนา PDF " +
"(ดู `hasPdf` ใน response)\n" +
"- `versionNumber` ใน response คือเลขจริงที่ server จ่าย (ปลอดภัยแม้อัปโหลดพร้อมกัน " +
"หลาย client) — ถ้าต่างจากที่ client คาดไว้ ให้ใช้เลขจาก response เสมอ\n" +
"- ผู้อัปโหลด (userId/userName) และ templateId อ่านจาก token — " +
"**ห้ามส่ง field เหล่านี้ใน form (ส่งมา = 400)**",
security: [{ DocverBearer: [] }],
requestBody: {
required: true,
@ -720,7 +732,9 @@ export const openApiSpec = {
fields: {
type: "string",
description:
'JSON string ของค่า Content Control เช่น {"blackno":"คดีหมายเลขดำที่"}',
'JSON string ของค่า Content Control **ทุกตัว** ในเอกสาร รูปแบบ ' +
'`{ [tag]: ข้อความ }` (add-in อ่านมาตอนบันทึก version) ' +
'เช่น {"blackno":"คดีหมายเลขดำที่","detail1":"…"}',
},
message: {
type: "string",
@ -762,9 +776,11 @@ export const openApiSpec = {
tags: ["documents"],
summary: "รายการ version ทั้งหมดของเอกสาร (ใหม่ → เก่า)",
description:
"เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — version ทุกตัว " +
"ของเอกสารนั้น พร้อมผู้อัปโหลด (userId/userName), เวลา, commit message, ค่า fields " +
"และ hasPdf — ใช้เขียน UI version history ของ add-in",
"version ทุกตัวของเอกสารที่ระบุด้วย `fid` ใน Bearer token เรียง **ใหม่ → เก่า** — " +
"แต่ละรายการมีผู้อัปโหลด, เวลา, commit message, ค่า Content Control ตอนอัปโหลด " +
"(`fields`) และว่ามีสำเนา PDF ไหม (`hasPdf`) — ใช้เขียน UI version history ของ add-in\n\n" +
"docId นี้ยังไม่เคยมี version เลย = 404 (สำหรับ add-in ถือเหมือน list ว่าง — " +
"กดบันทึกเพื่อสร้าง version 1 ได้ทันที)",
security: [{ DocverBearer: [] }],
responses: {
200: {
@ -797,10 +813,12 @@ export const openApiSpec = {
tags: ["documents"],
summary: "ขอ presigned URL ดาวน์โหลดไฟล์ของ version (docx/pdf)",
description:
"เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) · คืน URL อายุสั้น " +
"(env DOCVER_LINK_TTL_SECONDS, default 900 วินาที) ให้เปิดในเบราว์เซอร์/Word ของ " +
"ผู้ใช้ — URL อ้าง endpoint MinIO ตรง ๆ จึงต้องเป็นโฮสต์ที่ผู้ใช้เข้าถึงได้ · ขอ pdf " +
"ใน version ที่ไม่มีสำเนา = 404",
"ขอ **presigned URL อายุสั้น** สำหรับดาวน์โหลดไฟล์ของ version หนึ่ง — URL ชี้ที่ " +
"MinIO ตรง ๆ ให้เปิดในเบราว์เซอร์/Word ของผู้ใช้\n\n" +
"- อายุ URL ตาม env `DOCVER_LINK_TTL_SECONDS` (default 900 วินาที) — " +
"หมดอายุแล้วขอใหม่ได้เรื่อย ๆ\n" +
"- ต้องเป็น version ของเอกสารเดียวกับ `fid` ใน token (โยน versionId ข้ามเอกสารไม่ได้)\n" +
"- ขอ `format=pdf` ใน version ที่ไม่มีสำเนา PDF = 404",
security: [{ DocverBearer: [] }],
parameters: [
{
@ -859,8 +877,9 @@ export const openApiSpec = {
scheme: "bearer",
bearerFormat: "JWT",
description:
"access_token จาก POST /api/documents/token — แนบเป็น Authorization: Bearer " +
"กับ endpoint อื่นของกลุ่ม documents ทุกตัว",
"access_token จาก POST /api/documents/token — แนบเป็น " +
"`Authorization: Bearer <token>` กับ endpoint อื่นของกลุ่ม documents ทุกตัว\n" +
"(ตอนกรอกใน Swagger: ใส่เฉพาะค่า token — คำว่า Bearer ไม่ต้องพิมพ์เอง)",
},
},
responses: {
@ -1162,7 +1181,8 @@ export const openApiSpec = {
fields: {
type: "object",
nullable: true,
description: "ค่า Content Control ตอนอัปโหลด (ไม่ส่ง = null)",
description:
"ค่า Content Control ทุกตัวตอนอัปโหลด รูปแบบ `{ [tag]: ข้อความ }` (ไม่ส่ง = null)",
additionalProperties: { type: "string" },
},
message: { type: "string", nullable: true, description: "commit message" },