// OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI
// แสดงผล: กลุ่ม 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 แยก)
//
// URL ใน paths เป็น path เต็มรวม /api/… ตาม invariant #12 (URL จริง
// /docedit/api/… — servers เติมแค่ basePath ไม่มี /note ซ้อน)
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
const ANNOTATION_TYPE_ENUM = [
"HIGHLIGHT",
"DRAW",
"COMMENT",
"SHAPE",
"REDACTION",
"TEXTBOX",
"STAMP",
] as const;
export const openApiSpec = {
openapi: "3.0.3",
info: {
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 จัด version เอกสาร (docver)",
},
servers: [{ url: BASE_PATH, description: "origin เดียวกับหน้านี้ (basePath /docedit)" }],
tags: [
{
name: "template",
description:
"Template เอกสาร DOCX ด้วย Content Control — ส่ง template (.docx) + data (JSON) " +
"แล้วได้เอกสารที่กรอกค่าแล้วกลับมา",
},
{
name: "documents",
description:
"จัด version เอกสาร (docver — docs/PLAN-DOCVER.md): เอกสารเดียวต่อ docId แต่ละ " +
"version เก็บ .docx + .pdf ใน MinIO พร้อมผู้อัปโหลด — auth ด้วย access_token " +
"ของกลุ่มนี้: ขอที่ POST /api/documents/token (ยืนยัน x-api-key กับ env " +
"DOCVER_API_KEY) แล้วแนบ Authorization: Bearer ทุก request — identity/template " +
"และ docId (= fid — fileId ของ e-Court, string) แกะจาก claims ของ token ทั้งหมด " +
"ไม่รับจาก URL/multipart form อีก",
},
{
name: "Judge's Note",
description:
"API ภายนอก (N10) — backend e-Court เรียกแบบ server-to-server ของ (Judge's Note)" +
"ดึง annotations/bookmarks ของเอกสารที่เคยเปิดในระบบนี้",
},
],
paths: {
"/api/template/test": {
get: {
tags: ["template"],
summary: "ตรวจสอบว่า service มีชีวิต",
responses: {
200: {
description: "service พร้อมใช้งาน",
content: {
"application/json": {
schema: {
type: "object",
properties: {
ok: { type: "boolean" },
service: { type: "string" },
},
},
},
},
},
},
},
},
"/api/template/extract": {
post: {
tags: ["template"],
summary: "ดึงค่า Content Control ทั้งหมดออกมาจากไฟล์ .docx",
description:
"อ่าน w:sdt (Content Control) ทุกตัวใน word/document.xml — คืน map " +
"ชื่อแท็ก (w:tag) → ข้อความปัจจุบันในนั้น (ใช้สำรวจว่า template มีแท็กอะไรบ้างก่อนเรียก render)",
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["file"],
properties: {
file: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ที่มี Content Control",
},
secret: {
type: "string",
description:
"ค่า secret สำหรับ validate HMAC-SHA256 กับข้อมูลใน custom property" ,
example:
"super_secret",
},
public_key: {
type: "string",
description:
"Public key ในรูปแบบ HEX
openssl genpkey -algorithm ED25519 -out private.pem
openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex
openssl pkey -in private.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32 > public.hex",
example:
"66b09b4a1b66271d5728c592c2409657e1c070a7bf371bf9f6ed457855270546",
},
},
},
},
},
},
responses: {
200: {
description: "รายการแท็กพร้อมข้อความ",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ExtractResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/template/render": {
post: {
tags: ["template"],
summary: "สร้างเอกสารจาก template (.docx) + data (JSON)",
description:
"แทนค่า JSON ลงใน Content Control ของ template แล้วคืนไฟล์ .docx ใหม่ " +
'(filename ตอน download = generated-document.docx) — ส่ง data ได้ 2 ทาง: ' +
'field "data" เป็นไฟล์ JSON หรือ field "dataJson" เป็น string ตรง ๆ (เลือกอย่างใดอย่างหนึ่ง)',
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["template"],
properties: {
template: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ที่เป็น template (มี Content Control)",
},
data: {
type: "string",
format: "binary",
description: 'ไฟล์ JSON เช่น { "blackno": "คดีหมายเลขดำที่", … }',
},
dataJson: {
type: "string",
description:
"JSON เป็น string ตรง ๆ (ทางเลือกแทนการแนบไฟล์ data)
{\"info:\"{\"ticket_data\":\"JSON String\"},\"dc:\"{..},\"dc:\"{\"blackyear\": \"๙๐/๒๕๖๙\",...}}",
example:
'',
},
secret: {
type: "string",
description:
"ค่า secret สำหรับทดสอบการทำ HMAC-SHA256",
example:
'super_secret',
},
private_key: {
type: "string",
description:
"Private key ในรูปแบบ HEX
openssl genpkey -algorithm ED25519 -out private.pem
openssl pkey -in private.pem -outform DER | tail -c 32 | xxd -p -c 32 > private.hex
openssl pkey -in private.pem -pubout -outform DER | tail -c 32 | xxd -p -c 32 > public.hex",
example:
'8f2077b57b39399b6a7828f35da03a63ade8e3a36a6bf437b091a8bdb3d18ee1',
},
},
},
},
},
},
responses: {
200: {
description: "ไฟล์ .docx ที่กรอกค่าแล้ว (binary)",
content: {
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": {
schema: { type: "string", format: "binary" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/note/external/documents": {
post: {
tags: ["external"],
summary: "ดึง annotations/bookmarks ของเอกสาร (N10)",
description:
"ถามด้วย fileId (เอกสารเดียว — id ของ e-Court ที่ลิงก์มา) และ/หรือ typeId " +
"(ทุกเอกสารในกลุ่ม) — ส่งทั้งคู่ = AND เหลือเฉพาะเอกสารเดียวที่ตรงทั้งสองเงื่อนไข " +
"· response รูปเดียวเสมอ { documents: […] } (กลุ่มว่าง = array ว่าง) " +
"· read-only: snapshot ข้อมูล ณ ตอนเรียก · แต่ละรายการมี url deep link กลับหน้าอ่านของระบบนี้",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ExternalQuery" },
examples: {
"ถามด้วย fileId (เอกสารเดียว)": {
value: { fileId: "DOC-2569-0001" },
},
"ทุกเอกสารในกลุ่ม": {
value: { typeId: "GROUP-01" },
},
"กลุ่ม + เฉพาะ bookmark ของคนใดคนหนึ่ง": {
value: {
typeId: "GROUP-01",
filter: { type: "bookmarks", userId: "u0001" },
},
},
},
},
},
},
responses: {
200: {
description: "bundle ของเอกสารที่ตรงเงื่อนไข",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/DocumentsResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description: "ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
404: {
description: "ไม่พบเอกสาร (ยังไม่เคยเปิดในระบบ หรือไม่อยู่ในกลุ่ม typeId ที่ระบุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/documents/token": {
post: {
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 จึงบังคับ",
security: [{ ApiKeyAuth: [] }],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/TokenRequest" },
examples: {
"ครบทุก claim": {
value: {
userid: "123",
username: "Sorawit Bholsithi",
fid: "3",
template: "T12",
},
},
"ขั้นต่ำ (template เป็นทางเลือก)": {
value: { userid: "123", username: "Sorawit Bholsithi", fid: "3" },
},
},
},
},
},
responses: {
200: {
description: "access_token พร้อมใช้ (แนบเป็น Authorization: Bearer ทุก request ถัดไป)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/TokenResponse" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: {
description: "ไม่ได้ส่ง header x-api-key หรือค่าไม่ถูกต้อง (env DOCVER_API_KEY)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/documents/versions": {
post: {
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)",
security: [{ DocverBearer: [] }],
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
required: ["file"],
properties: {
file: {
type: "string",
format: "binary",
description: "ไฟล์ .docx ของ version นี้ (บังคับ)",
},
pdf: {
type: "string",
format: "binary",
description:
"ไฟล์ .pdf ของ version นี้ (optional — ไม่แนบหรือไฟล์เปล่า = ไม่มี)",
},
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 ตัวอักษร)",
},
fileName: {
type: "string",
description:
"ชื่อไฟล์สำหรับแสดงตอนดาวน์โหลด (ไม่ส่ง = ใช้ชื่อไฟล์ที่แนบ)",
},
},
},
},
},
},
responses: {
200: {
description: "version ถูกบันทึกแล้ว",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/UploadVersionResult" },
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: { $ref: "#/components/responses/DocverUnauthorized" },
413: {
description: "ไฟล์แนบใหญ่เกิน 100MB",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
get: {
tags: ["documents"],
summary: "รายการ version ทั้งหมดของเอกสาร (ใหม่ → เก่า)",
description:
"เอกสารถูกระบุด้วย fid ใน Bearer token (docId = fid) — version ทุกตัว " +
"ของเอกสารนั้น พร้อมผู้อัปโหลด (userId/userName), เวลา, commit message, ค่า fields " +
"และ hasPdf — ใช้เขียน UI version history ของ add-in",
security: [{ DocverBearer: [] }],
responses: {
200: {
description: "รายการ version เรียงใหม่ → เก่า",
content: {
"application/json": {
schema: {
type: "array",
items: { $ref: "#/components/schemas/VersionRecord" },
},
},
},
},
400: { $ref: "#/components/responses/BadRequest" },
401: { $ref: "#/components/responses/DocverUnauthorized" },
404: {
description: "ไม่พบเอกสารนี้ในระบบ (ยังไม่เคยอัปโหลด version)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
"/api/documents/versions/{versionId}/download-link": {
get: {
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",
security: [{ DocverBearer: [] }],
parameters: [
{
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" },
401: { $ref: "#/components/responses/DocverUnauthorized" },
404: {
description:
"ไม่พบ version นี้ของเอกสาร หรือขอ format=pdf ใน version ที่ไม่มีสำเนา PDF",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
500: { $ref: "#/components/responses/ServerError" },
},
},
},
},
components: {
securitySchemes: {
ApiKeyAuth: {
type: "apiKey",
in: "header",
name: "x-api-key",
description:
"x-api-key ของ N10 = env EXTERNAL_API_KEY · ของการขอ token กลุ่ม documents = env " +
"DOCVER_API_KEY (คนละค่ากับของ N10 — กดปุ่ม Authorize ใส่ครั้งเดียวแล้วใช้ได้ทุก " +
"request ของกลุ่มที่ key ตรง)",
},
DocverBearer: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description:
"access_token จาก POST /api/documents/token — แนบเป็น Authorization: Bearer " +
"กับ endpoint อื่นของกลุ่ม documents ทุกตัว",
},
},
responses: {
BadRequest: {
description:
"request ไม่ถูกต้อง (field หาย / ชนิดไฟล์ผิด / ค่า enum ไม่รู้จัก / " +
"JSON ผิด format / ไฟล์แนบใหญ่เกิน 100MB)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
DocverUnauthorized: {
description:
"ไม่ได้แนบ Authorization: Bearer หรือ token ไม่ถูกต้อง/หมดอายุ (กลุ่ม documents — ข้อความเดียวกันทุกสาเหตุ)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
ServerError: {
description: "เกิดข้อผิดพลาดของระบบ (เช่นไฟล์ .docx ไม่ถูกต้อง / DB ล้ม)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/Error" },
},
},
},
},
schemas: {
Error: {
type: "object",
properties: {
success: { type: "boolean", example: false },
error: { type: "string" },
},
},
TokenRequest: {
type: "object",
description: "claims ที่จะฝังใน access_token ของกลุ่ม documents — key อื่น = 400",
required: ["userid", "username", "fid"],
properties: {
userid: { type: "string", description: "รหัสผู้ใช้ (บังคับ, ≤ 100 ตัวอักษร)" },
username: { type: "string", description: "ชื่อผู้ใช้ (บังคับ, ≤ 200 ตัวอักษร)" },
fid: {
type: "string",
description:
"fileId ของ e-Court (บังคับ, ≤ 200 ตัวอักษร) — เป็น docId ของทุก operation ตรง ๆ",
},
template: {
type: "string",
nullable: true,
description:
"รหัส template ที่ใช้สร้างเอกสาร — กลายเป็น templateId ตอนอัปโหลด version",
},
},
},
TokenResponse: {
type: "object",
description: "access_token ของกลุ่ม documents (JWT HS256 อายุสั้น)",
properties: {
access_token: { type: "string", description: "JWT พร้อมใช้แนบเป็น Bearer" },
token_type: { type: "string", example: "Bearer" },
expires_in: {
type: "integer",
description: "อายุวินาที (env DOCVER_TOKEN_TTL_SECONDS, default 3600)",
},
},
},
ExtractResponse: {
type: "object",
properties: {
success: { type: "boolean", example: true },
data: {
type: "object",
description: "ชื่อแท็ก Content Control → ข้อความในนั้น (หลายบรรทัดคั่นด้วย \\n)",
additionalProperties: { type: "string" },
example: {
blackno: "คดีหมายเลขดำที่",
court: "ศาลปกครองสุพรรณบุรี",
},
},
},
},
ExternalQuery: {
type: "object",
description:
"ต้องมี fileId หรือ typeId อย่างน้อยหนึ่งอย่าง (ส่งทั้งคู่ได้ = AND เหลือเอกสารเดียว)",
properties: {
fileId: {
type: "string",
description: "id เอกสารของ e-Court — ตรงกับที่ส่งมากับลิงก์ ?fileId= ตอนเปิดเอกสาร",
},
typeId: {
type: "string",
description: "รหัสกลุ่มเอกสาร — คืนทุกไฟล์ในกลุ่มนี้ (เรียงตามวันที่สร้าง)",
},
filter: {
type: "object",
properties: {
type: {
type: "string",
enum: ["bookmarks", "annotations"],
description: "เลือกเฉพาะชนิดรายการ (ไม่ส่ง = ทั้งสอง ส่วนที่ไม่เลือกคืนเป็น array ว่าง)",
},
userId: {
description: "กรองตามเจ้าของรายการ (string เดี่ยวหรือ array)",
oneOf: [
{ type: "string" },
{ type: "array", items: { type: "string" } },
],
},
annotationTypes: {
description: "กรองชนิดของ annotation — มีผลเฉพาะเมื่อรวม annotations",
oneOf: [
{ type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
{
type: "array",
items: { type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
},
],
},
},
},
},
},
DocumentsResponse: {
type: "object",
properties: {
documents: {
type: "array",
items: { $ref: "#/components/schemas/DocumentBundle" },
},
},
},
DocumentBundle: {
type: "object",
description: "field ของเอกสารอยู่ระดับเดียวกับ annotations/bookmarks (bundle ยุบชั้น)",
properties: {
id: { type: "string", format: "uuid" },
externalFileId: {
type: "string",
nullable: true,
description: "id ของ e-Court (null = ไฟล์ url:-fallback หรืออัปโหลดในกลุ่ม)",
},
title: { type: "string" },
typeId: { type: "string", nullable: true },
createdAt: { type: "string", format: "date-time" },
annotations: {
type: "array",
items: { $ref: "#/components/schemas/ExternalAnnotation" },
},
bookmarks: {
type: "array",
items: { $ref: "#/components/schemas/ExternalBookmark" },
},
},
},
ExternalAnnotation: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
fileId: { type: "string", format: "uuid" },
pageIndex: { type: "integer", description: "0-based" },
userId: { type: "string" },
userName: { type: "string" },
type: { type: "string", enum: [...ANNOTATION_TYPE_ENUM] },
data: {
type: "object",
description:
"ข้อมูลเฉพาะชนิด (พิกัดเป็น % ของขนาดหน้า) เช่น HIGHLIGHT { text, color, rects }, COMMENT { message, position }",
},
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
resolved: { type: "boolean" },
resolvedAt: { type: "string", format: "date-time", nullable: true },
url: {
type: "string",
description: "deep link กลับหน้าอ่าน พร้อม ?ann= ของรายการนี้",
},
replies: {
type: "array",
items: { $ref: "#/components/schemas/ExternalReply" },
},
},
},
ExternalBookmark: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
fileId: { type: "string", format: "uuid" },
pageIndex: { type: "integer", description: "0-based" },
userId: { type: "string" },
userName: { type: "string" },
title: { type: "string" },
parentId: {
type: "string",
nullable: true,
description: "null = ระดับบนสุด (มีค่า = sub ของ bookmark นั้น)",
},
note: { type: "string", nullable: true },
yOffset: { type: "number", description: "ตำแหน่งบนหน้า (% ของความสูง)" },
createdAt: { type: "string", format: "date-time" },
url: {
type: "string",
description: "deep link กลับหน้าอ่าน พร้อม ?page= 1-based ของหน้านี้",
},
},
},
ExternalReply: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
annotationId: { type: "string", format: "uuid" },
userId: { type: "string" },
userName: { type: "string" },
message: { type: "string" },
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;