Refactor document versioning to extract fields from uploaded .docx files on the server side

- Removed the `fields` parameter from the version upload form, as it is now extracted from the uploaded .docx file using the `extractContentControls` function.
- Updated related types and interfaces to reflect the new structure of version fields.
- Modified the OpenAPI specification to document the changes in the versioning process.
- Adjusted tests to ensure compliance with the new behavior, including validation for the absence of the `fields` parameter.
- Implemented a new function `extractDocumentSnapshot` to handle the extraction of content controls and document properties from .docx files.
- Updated metadata and versioning services to align with the new extraction logic.
This commit is contained in:
Suphonchai Phoonsawat 2026-10-31 10:21:17 +07:00
parent bc61f0fd2e
commit cdae1ceedd
17 changed files with 380 additions and 179 deletions

View file

@ -25,12 +25,13 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
/** field ที่รับใน multipart ของ POST …/versions — อะไรนอกนี้ = 400 กัน typo เงียบ ๆ
* (userId/userName/templateId ถอดออกแล้ว — identity/template มาจาก Bearer token
* ผ่าน lib/docver-token.ts ส่ง field เก่ามา = 400 ตาม convention) */
* ผ่าน lib/docver-token.ts · `fields` ถอดออก 2026-10-30 — server แกะจากไฟล์ .docx
* ด้วย extractContentControls ของ lib/lib-tmpl.ts ตัวเดียวกับ /api/template/extract
* ส่ง field เก่ามา = 400 ตาม convention) */
const FORM_FIELDS = [
"file",
"pdf",
"clientVersionId",
"fields",
"message",
"fileName",
] as const;
@ -108,39 +109,10 @@ export function sanitizeFilePartName(raw: string): string | null {
return sanitizeDisplayFileName(decodeUploadFilename(raw));
}
/** fields ของ multipart (JSON string) → map ดิบ { [sdtTag]: text } —
* `{}` หรือไม่ส่ง = null · ไม่ใช่ object ล้วน/key หรือ value ผิดกติกา = 400 */
export function parseFieldsJson(raw: string):
| { ok: true; fields: Record<string, string> | null }
| DocverError {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return { ok: false, error: 'field "fields" ต้องเป็น JSON object เช่น { "tag": "ค่า" }' };
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
return { ok: false, error: 'field "fields" ต้องเป็น JSON object เช่น { "tag": "ค่า" }' };
}
const record = parsed as Record<string, unknown>;
const keys = Object.keys(record);
// เหลือ guard เชิงโครงสร้างเท่านั้น — ความยาวค่าไม่จำกัดแล้ว (2026-10-27 ตามคำขอผู้ใช้:
// เอกสารจริง 100–200 หน้า มี CC ยาวหลายแสนตัวอักษร; ผู้เรียกผ่าน Bearer เท่านั้น + jsonb
// ของ Postgres รับได้ถึงระดับ 255MB จึงไม่มีคอขวดฝั่งเก็บ)
if (keys.length > 1_000) {
return { ok: false, error: 'field "fields" มี key เกิน 1,000 รายการ' };
}
for (const key of keys) {
if (key.length > 200) {
return { ok: false, error: 'key แต่ละรายการใน "fields" ยาวเกิน 200 ตัวอักษร' };
}
const value = record[key];
if (typeof value !== "string") {
return { ok: false, error: 'ค่าของแต่ละรายการใน "fields" ต้องเป็น string' };
}
}
return { ok: true, fields: keys.length === 0 ? null : (parsed as Record<string, string>) };
}
/** fields ของ multipart (JSON string) → map ดิบ { [sdtTag]: text } — ถอดออกแล้ว
* (2026-10-30): form field "fields" ยกเลิก ไม่รับจากผู้เรียกอีก — fields ของ version
* แกะจากไฟล์ .docx ที่อัปโหลดฝั่ง server ด้วย extractContentControls ของ
* lib/lib-tmpl.ts (ตัวเดียวกับ /api/template/extract) ที่ route ของ POST versions */
export type ParsedVersionUploadForm = {
clientVersionId: string | null;
@ -148,7 +120,6 @@ export type ParsedVersionUploadForm = {
/** ชื่อไฟล์ดั้งเดิมจาก field "fileName" (แยกจากชื่อของ File เอง — add-in ส่งมาเอง
* เพราะ desktop Word ได้ชื่อจาก path จริง) — strip path + cap แล้ว */
fileName: string | null;
fields: Record<string, string> | null;
};
/** ตรวจ text field ทั้งหมดของ POST …/versions (file/pdf เช็คที่ route เพราะต้องอ่าน
@ -180,15 +151,6 @@ export function parseVersionUploadForm(form: FormData):
return { ok: false, error: `message ยาวเกิน ${DOCVER_MESSAGE_MAX} ตัวอักษร` };
}
const rawFields = formString(form, "fields");
if (!rawFields.ok) return rawFields;
let fields: Record<string, string> | null = null;
if (rawFields.value !== null) {
const parsed = parseFieldsJson(rawFields.value);
if (!parsed.ok) return parsed;
fields = parsed.fields;
}
const rawFileName = formString(form, "fileName");
if (!rawFileName.ok) return rawFileName;
@ -198,7 +160,6 @@ export function parseVersionUploadForm(form: FormData):
clientVersionId: clientVersionId.value,
message: message.value,
fileName: sanitizeDisplayFileName(rawFileName.value),
fields,
},
};
}