feat(addin): unmock versioning — real upload/download against docver API
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 5s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m27s
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
All checks were successful
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 5s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 1m27s
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
Replace the in-memory versioningService with real calls to the docver group (/api/documents/*) via apiClient with the ticket-issued Bearer; keep the mock only for ?addindemo (authService.isDemoMode). - uploadVersion: multipart POST /api/documents/versions (file, optional pdf when non-empty, clientVersionId, fields JSON, trimmed message, fileName) — identity fields are never sent (userId/userName/templateId in the form = 400 per the 2026-10-22 contract; docId comes from the fid claim) - getVersions: GET → DocumentVersionDTO mapped to VersionRecord (adds optional createdByUserName; history panel shows it with userId fallback) - getDownloadLink: GET presigned URL (+ ?format=pdf) - apiClient.toUrl: empty NEXT_PUBLIC_ADDIN_API_BASE_URL = same-origin through withBasePath (/docedit/api/...); the env is now only for a split deployment (must include basePath, HTTPS) - documentId/templateId params stay in signatures but are not sent - tests: versioningService.test.ts covers both modes (real sign-in via signInWithTicket, FormData contract assertions, DTO mapping, format query, 401 -> AuthRequiredError, demo mock without network) - docs sync: WORDADDIN-DEV (new "การรันกับ backend จริง" section), addin-ui-flow status note, PLAN-DOCVER §8, PLAN-ADDIN-AUTH §12, CLAUDE.md Verified: tsc clean, 286/286 tests, full E2E against MinIO (ticket -> token -> upload docx / docx+pdf -> history -> presigned download of both formats). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
4200176239
commit
37b18c58c2
11 changed files with 373 additions and 68 deletions
|
|
@ -1,26 +1,146 @@
|
|||
/**
|
||||
* MOCK — version API ฝั่ง client (รอ backend จริงตาม contract ใน docs/addin-ui-flow.md §7)
|
||||
* ต้นฉบับ: D:\word-addin-poc\addin\src\services\versioningService.ts (multipart POST ผ่าน apiClient)
|
||||
* Version API ฝั่ง client — **ของจริง** (กลุ่ม docver /api/documents/* ผ่าน apiClient
|
||||
* ซึ่งแนบ Bearer แล้ว — contract ตาม docs/PLAN-DOCVER.md) · เหลือ mock เฉพาะโหมด
|
||||
* `?addindemo` (dev-only ดู UI ใน browser ไม่ต้องมี backend/MinIO — authService.isDemoMode)
|
||||
*
|
||||
* - คงชื่อ/ลายเซ็น uploadVersion/getVersions/getDownloadLink/newClientVersionId ไว้เป๊ะ —
|
||||
* วันปลด mock: ก้อบ body ของต้นฉบับกลับมาได้เลย
|
||||
* - ต้นฉบับมีอีก 2 ตัวที่ไม่มี caller ใน UI จึงไม่พอร์ต: buildDownloadUrl, downloadVersion
|
||||
* - **วันปลด mock ห้ามส่ง userId/userName/templateId ใน FormData** — contract ของ
|
||||
* POST /api/documents/versions ถอด 3 field นี้ออกแล้ว (ส่งมา = 400) และไม่มี
|
||||
* segment {docId} ใน URL อีกต่อไป: server แกะ identity/template และ docId
|
||||
* (= fid — fileId ของ e-Court, string) จาก Bearer token (ออกที่ POST
|
||||
* /api/documents/token ด้วย x-api-key, fid บังคับ) ที่ apiClient.ts แนบให้อยู่แล้ว
|
||||
* - **metadata ของ version ถูก persist ลง localStorage** (record เล็ก ๆ ต่อ documentId) —
|
||||
* ไม่งั้นปิดเอกสาร (taskpane ปิดตาม) แล้วเปิดใหม่ history ว่างเปล่า ทั้งที่ metadata
|
||||
* การ track อยู่ในไฟล์จริง ทำให้ทดสอบวง FR-18 ปิด→เปิดใหม่ไม่ได้; blob ของไฟล์จริง
|
||||
* ยังเก็บเฉพาะ session (mock download คืน about:blank อยู่แล้ว) — localStorage เข้า
|
||||
* ไม่ได้ (webview บางตัว block) = ใช้ memory อย่างเดียวแบบเดิม ไม่ throw
|
||||
* - คงชื่อ/ลายเซ็น uploadVersion/getVersions/getDownloadLink/newClientVersionId ตามต้นฉบับ
|
||||
* D:\word-addin-poc\addin\src\services\versioningService.ts — versionFlow เรียกต่อได้เดิม
|
||||
* - **เอกสารถูกระบุด้วย fid ใน Bearer token** (ไม่ใช่ documentId GUID ที่ stamp ในไฟล์ —
|
||||
* 2026-10-26 contract ถอด {docId} ออกจาก URL ทุก endpoint) พารามิเตอร์ documentId จึงเหลือ
|
||||
* ไว้เพื่อ signature เดิมเท่านั้น ไม่ถูกส่งถึง server · templateId ก็เช่นกัน (มาจาก claims
|
||||
* ของ token — ส่งมาใน form = 400)
|
||||
* - **ห้ามส่ง userId/userName/templateId ใน FormData** (contract 2026-10-22 — ส่งมา = 400)
|
||||
* - mock ยัง persist metadata ลง localStorage ต่อ documentId เพื่อทดสอบวงปิดเอกสาร→เปิดใหม่
|
||||
* ในโหมด demo — blob ไฟล์จริงเฉพาะ session; localStorage เข้าไม่ได้ (webview บางตัว block)
|
||||
* = ใช้ memory อย่างเดียว ไม่ throw
|
||||
*/
|
||||
|
||||
import * as authService from "./authService";
|
||||
import { generateGuid } from "./metadataService";
|
||||
import { apiGetJson, apiSendExpectJson } from "./apiClient";
|
||||
import { UploadVersionResult, VersionRecord } from "../models/versionRecord";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ของจริง — กลุ่ม docver (Bearer จาก ticket auth แนบอัตโนมัติโดย apiClient)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** DTO ของ GET /api/documents/versions (ใหม่ → เก่า) — PLAN-DOCVER.md §1 */
|
||||
interface DocumentVersionDTO {
|
||||
versionId: string;
|
||||
versionNumber: number;
|
||||
createdAt: string;
|
||||
createdByUserId: string;
|
||||
createdByUserName: string;
|
||||
contentHash: string;
|
||||
fields: Record<string, string> | null;
|
||||
message: string | null;
|
||||
hasPdf: boolean;
|
||||
}
|
||||
|
||||
/** FR-5 — อัปโหลดไฟล์ปัจจุบันเป็น version ใหม่ (multipart POST)
|
||||
* + FR-13: message คือ commit message ทางเลือก
|
||||
* + FR-16: templateId เก็บไว้ตาม signature เดิม — ไม่ส่งถึง server (มาจาก claims ของ token) */
|
||||
export async function uploadVersion(
|
||||
documentId: string,
|
||||
file: Blob,
|
||||
clientVersionId: string,
|
||||
fields?: Record<string, string>,
|
||||
fileName?: string,
|
||||
message?: string,
|
||||
pdf?: Blob,
|
||||
templateId?: string
|
||||
): Promise<UploadVersionResult> {
|
||||
if (!authService.isDemoMode()) {
|
||||
void templateId; // contract 2026-10-22 — ห้ามส่ง (identity ทั้งหมดจาก Bearer token)
|
||||
const form = new FormData();
|
||||
form.append("file", file, fileName ?? "document.docx");
|
||||
// pdf optional: ไม่แนบ/ไฟล์เปล่า = version นี้ไม่มีสำเนา PDF
|
||||
if (pdf && pdf.size > 0) {
|
||||
form.append("pdf", pdf, "document.pdf");
|
||||
}
|
||||
if (clientVersionId) {
|
||||
form.append("clientVersionId", clientVersionId);
|
||||
}
|
||||
if (fields && Object.keys(fields).length > 0) {
|
||||
form.append("fields", JSON.stringify(fields));
|
||||
}
|
||||
const trimmedMessage = message?.trim();
|
||||
if (trimmedMessage) {
|
||||
form.append("message", trimmedMessage);
|
||||
}
|
||||
if (fileName) {
|
||||
form.append("fileName", fileName);
|
||||
}
|
||||
return apiSendExpectJson<UploadVersionResult>(
|
||||
"/api/documents/versions",
|
||||
{ method: "POST", body: form },
|
||||
"อัปโหลด version ไม่สำเร็จ",
|
||||
);
|
||||
}
|
||||
return uploadVersionMock(documentId, file, clientVersionId, fields, message, pdf);
|
||||
}
|
||||
|
||||
/** FR-6 — version history เรียงใหม่ → เก่า (คืนสำเนากัน caller mutate store ตรง ๆ)
|
||||
* docId มาจาก fid ใน token — documentId พารามิเตอร์ไม่ถูกใช้กับ server ของจริง */
|
||||
export async function getVersions(documentId: string): Promise<VersionRecord[]> {
|
||||
if (!authService.isDemoMode()) {
|
||||
const rows = await apiGetJson<DocumentVersionDTO[]>(
|
||||
"/api/documents/versions",
|
||||
"โหลด version history ไม่สำเร็จ",
|
||||
);
|
||||
// server เรียงใหม่ → เก่าอยู่แล้ว — แปลงเป็น VersionRecord ตามที่ UI ใช้
|
||||
return rows.map((row) => ({
|
||||
versionId: row.versionId,
|
||||
versionNumber: row.versionNumber,
|
||||
createdAt: row.createdAt,
|
||||
createdByUserId: row.createdByUserId,
|
||||
createdByUserName: row.createdByUserName,
|
||||
contentHash: row.contentHash,
|
||||
fields: row.fields && Object.keys(row.fields).length > 0 ? row.fields : null,
|
||||
message: row.message,
|
||||
hasPdf: row.hasPdf,
|
||||
}));
|
||||
}
|
||||
return getVersionsMock(documentId);
|
||||
}
|
||||
|
||||
export interface VersionDownloadLink {
|
||||
url: string;
|
||||
/** ISO-8601 เวลาที่ URL หมดอายุ */
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FR-7 — ขอ presigned URL อายุสั้นของ version หนึ่ง เพื่อเปิดในเบราว์เซอร์ของผู้ใช้
|
||||
* (webview ของ Word ทิ้ง blob download เงียบๆ — ให้เบราว์เซอร์จริงจัดการแทน)
|
||||
* URL เป็น MinIO presigned GET (อายุ env DOCVER_LINK_TTL_SECONDS default 900 วิ) —
|
||||
* โฮสต์ MinIO ต้องเข้าถึงได้จากเครื่องผู้ใช้
|
||||
*/
|
||||
export async function getDownloadLink(
|
||||
documentId: string,
|
||||
versionId: string,
|
||||
format?: "docx" | "pdf"
|
||||
): Promise<VersionDownloadLink> {
|
||||
if (!authService.isDemoMode()) {
|
||||
void documentId; // เอกสารมาจาก fid ใน token — versionId เช็คคู่กับ docId ฝั่ง server
|
||||
const query = format && format !== "docx" ? `?format=${encodeURIComponent(format)}` : "";
|
||||
return apiGetJson<VersionDownloadLink>(
|
||||
`/api/documents/versions/${encodeURIComponent(versionId)}/download-link${query}`,
|
||||
"ขอลิงก์ดาวน์โหลดไม่สำเร็จ",
|
||||
);
|
||||
}
|
||||
return getDownloadLinkMock();
|
||||
}
|
||||
|
||||
/** สร้าง clientVersionId ใหม่สำหรับ upload ครั้งถัดไป */
|
||||
export function newClientVersionId(): string {
|
||||
return generateGuid();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock — เฉพาะโหมด ?addindemo (dev-only)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface StoredVersion {
|
||||
record: VersionRecord;
|
||||
docxBlob: Blob | null;
|
||||
|
|
@ -78,22 +198,15 @@ async function mockContentHash(file: Blob): Promise<string> {
|
|||
}
|
||||
}
|
||||
|
||||
/** FR-5 — อัปโหลดไฟล์ปัจจุบันเป็น version ใหม่ (mock: เก็บลง memory)
|
||||
* + FR-13: message คือ commit message ทางเลือก
|
||||
* + FR-16: templateId เฉพาะไฟล์ร่าง (mock ไม่ใช้ — คงไว้ตาม signature ต้นฉบับ) */
|
||||
export async function uploadVersion(
|
||||
async function uploadVersionMock(
|
||||
documentId: string,
|
||||
file: Blob,
|
||||
clientVersionId: string,
|
||||
fields?: Record<string, string>,
|
||||
fileName?: string,
|
||||
message?: string,
|
||||
pdf?: Blob,
|
||||
templateId?: string
|
||||
pdf?: Blob
|
||||
): Promise<UploadVersionResult> {
|
||||
void clientVersionId; // mock ไม่ใช้ — id ของ mock สร้างเอง
|
||||
void fileName;
|
||||
void templateId;
|
||||
await delay(500); // ให้ข้อความ progress ของ flow ขึ้นจริง
|
||||
|
||||
const versions = store.get(documentId) ?? [];
|
||||
|
|
@ -117,8 +230,7 @@ export async function uploadVersion(
|
|||
return { hasPdf, versionId: record.versionId, versionNumber };
|
||||
}
|
||||
|
||||
/** FR-6 — version history เรียงใหม่ → เก่า (คืนสำเนากัน caller mutate store ตรง ๆ) */
|
||||
export async function getVersions(documentId: string): Promise<VersionRecord[]> {
|
||||
async function getVersionsMock(documentId: string): Promise<VersionRecord[]> {
|
||||
await delay(200);
|
||||
const versions = store.get(documentId) ?? [];
|
||||
return versions
|
||||
|
|
@ -127,33 +239,7 @@ export async function getVersions(documentId: string): Promise<VersionRecord[]>
|
|||
.map((item) => ({ ...item.record }));
|
||||
}
|
||||
|
||||
export interface VersionDownloadLink {
|
||||
url: string;
|
||||
/** ISO-8601 เวลาที่ URL หมดอายุ */
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* FR-7 — ขอ presigned URL อายุสั้นของ version หนึ่ง เพื่อเปิดในเบราว์เซอร์ของผู้ใช้
|
||||
* (webview ของ Word ทิ้ง blob download เงียบๆ — ให้เบราว์เซอร์จริงจัดการแทน)
|
||||
* mock: ไม่มี URL จริงให้คืน — blob:/data: ใช้ผ่าน openBrowserWindow ไม่ได้ (blob ผูกกับ
|
||||
* context ของ webview, data: โดนเบราว์เซอร์ block) ส่วน about:blank ที่คืนไปนี้ host จะ
|
||||
* ปฏิเสธเงียบๆ เพราะ openBrowserWindow เปิดเฉพาะ http/https และเป็น void API ไม่รายงานกลับ
|
||||
* — กดปุ่มดาวน์โหลด/PDF ตอน mock จึงไม่มีหน้าต่างเบราว์เซอร์เปิด (UI ยังเดินครบ flow)
|
||||
*/
|
||||
export async function getDownloadLink(
|
||||
documentId: string,
|
||||
versionId: string,
|
||||
format?: "docx" | "pdf"
|
||||
): Promise<VersionDownloadLink> {
|
||||
void documentId;
|
||||
void versionId;
|
||||
void format;
|
||||
async function getDownloadLinkMock(): Promise<VersionDownloadLink> {
|
||||
await delay(150);
|
||||
return { url: "about:blank", expiresAt: new Date(Date.now() + 10 * 60_000).toISOString() };
|
||||
}
|
||||
|
||||
/** สร้าง clientVersionId ใหม่สำหรับ upload ครั้งถัดไป */
|
||||
export function newClientVersionId(): string {
|
||||
return generateGuid();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue