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

@ -125,10 +125,12 @@ function unescapeXml(value) {
.replace(/&/g, "&");
}
/** ถอด <Property name value> ทั้งชุดจาก custom.xml — ไว้ --list และหา pid ที่ใช้แล้ว */
/** ถอด <property name value> ทั้งชุดจาก custom.xml — ไว้ --list และหา pid ที่ใช้แล้ว
* (match ทั้งพิมพ์เล็กของตัว stamp จริง/setCustomDocProps และพิมพ์ใหญ่ <Property>
* ที่ script นี้เคยเขียนก่อน 2026-10-30 — อ่านไฟล์เก่าที่ stampค้างไว้ได้) */
function readProperties(xml) {
const properties = [];
const regex = /<Property\s+[^>]*name="([^"]*)"[^>]*>([\s\S]*?)<\/Property>|<Property\s+[^>]*name="([^"]*)"[^>]*\/>/g;
const regex = /<property\s+[^>]*name="([^"]*)"[^>]*>([\s\S]*?)<\/property>|<property\s+[^>]*name="([^"]*)"[^>]*\/>/gi;
let match;
while ((match = regex.exec(xml)) !== null) {
const name = match[1] ?? match[3];
@ -148,7 +150,10 @@ function usedPids(xml) {
}
function propertyXml(name, pid, value) {
return ` <Property fmtid="${CUSTOM_FMTID}" pid="${pid}" name="${escapeXml(name)}"><vt:lpwstr>${escapeXml(value)}</vt:lpwstr></Property>\n`;
// `<property>` พิมพ์เล็กตาม schema OOXML (ct_Property) — ตรงกับตัว stamp จริง
// setCustomDocProps ของ lib/lib-tmpl.ts เพื่อให้ getCustomDocProps/getDocProps
// (ที่ /api/template/extract กับ fields ของ version ใช้อ่าน) มองเห็นด้วย
return ` <property fmtid="${CUSTOM_FMTID}" pid="${pid}" name="${escapeXml(name)}"><vt:lpwstr>${escapeXml(value)}</vt:lpwstr></property>\n`;
}
/** upsert property ลง custom.xml (สร้างไฟล์ใหม่ถ้ายังไม่มี) — คืน XML ที่พร้อมเขียนกลับ zip */
@ -156,9 +161,10 @@ function upsertProperties(existingXml, entries) {
// เอา Property เก่าของชื่อที่จะเขียนทิ้งก่อน (replace แบบตัวเอง)
let xml = existingXml;
for (const entry of entries) {
// gi — ถอดของเก่าทั้งพิมพ์เล็ก (ตัวจริง) และพิมพ์ใหญ่ (ที่ script นี้เคยเขียน) ก่อนแทรกใหม่
const removal = new RegExp(
`\\s*<Property\\s+[^>]*name="${escapeXml(entry.name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^>]*>([\\s\\S]*?)</Property>`,
"g",
`\\s*<property\\s+[^>]*name="${escapeXml(entry.name).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"[^>]*>([\\s\\S]*?)</property>`,
"gi",
);
xml = xml.replace(removal, "");
}
@ -189,7 +195,7 @@ function ensurePartRegistered(zip) {
.asText()
.replace(
/<\/Types>/,
` <Override PartName="/docProps/custom.xml" ContentType="application/vnd.openxmlformats-officedocument.document-properties.custom+xml"/>\n</Types>`,
` <Override PartName="/docProps/custom.xml" ContentType="application/vnd.openxmlformats-officedocument.custom-properties+xml"/>\n</Types>`,
);
zip.file(CT_PATH, updated);
}