feat(api): add swagger ui and docx template api
เพิ่มกลุ่ม API docx template สำหรับ wordaddin (WIP): - POST /api/template/extract ดึงค่า Content Control ทั้งหมดจาก .docx - POST /api/template/render แทนค่า JSON ลง template คืน .docx ใหม่ - GET /api/template/test ตรวจว่า service มีชีวิต logic แชร์ที่ app/api/template/lib-tmpl.ts (pizzip+xpath) ไม่แตะ DB เพิ่มหน้า Swagger UI ที่ GET /api/swagger — spec inline จาก lib/openapi.ts ครอบ API ทุกกลุ่ม (template + N10), asset self-host ที่ public/note/swagger-ui/ จาก postinstall scripts/copy-swagger-ui.mjs (intranet ไม่ใช้ CDN — หาไฟล์ไม่เจอหลังอัปเกรด = exit 1 เหมือน patch-pdf-worker, Docker build รัน script นี้ด้วยผ่าน web.Dockerfile) middleware เปิดกว้างให้ api/template, api/swagger, note/swagger-ui จาก code review: จำกัดไฟล์แนบ 100MB ทั้ง extract/render ก่อนอ่าน เข้าหน่วยความจำกัน DoS, คืน 400 แทน 500 เมื่อ JSON ผิด format, sync คำอธิบาย BadRequest ใน openapi spec ให้ตรง behavior ใหม่ Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
8cc15ea75d
commit
073f72db7d
20 changed files with 1479 additions and 10 deletions
412
lib/openapi.ts
Normal file
412
lib/openapi.ts
Normal file
|
|
@ -0,0 +1,412 @@
|
|||
// OpenAPI 3.0 spec ของ API ทดสอบทั้งหมดของ judes-note — เส้นเดียวกับที่ Swagger UI
|
||||
// แสดงผล: กลุ่ม template (DOCX Content Control) + N10 external API
|
||||
// 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)",
|
||||
},
|
||||
servers: [{ url: BASE_PATH, description: "origin เดียวกับหน้านี้ (basePath /docedit)" }],
|
||||
tags: [
|
||||
{
|
||||
name: "template",
|
||||
description:
|
||||
"Template เอกสาร DOCX ด้วย Content Control — ส่ง template (.docx) + data (JSON) " +
|
||||
"แล้วได้เอกสารที่กรอกค่าแล้วกลับมา",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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)",
|
||||
example:
|
||||
'{"notname":"คำสั่งเรียก ให้ทำคำให้การ","blackno":"คดีหมายเลขดำที่","blackyear":"๑๐/๒๕๖๙"}',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
securitySchemes: {
|
||||
ApiKeyAuth: {
|
||||
type: "apiKey",
|
||||
in: "header",
|
||||
name: "x-api-key",
|
||||
description:
|
||||
"ค่าเดียวกับ env EXTERNAL_API_KEY ของ server — กดปุ่ม Authorize ด้านบนเพื่อใส่ครั้งเดียวแล้วใช้ทุก request",
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
BadRequest: {
|
||||
description:
|
||||
"request ไม่ถูกต้อง (field หาย / ชนิดไฟล์ผิด / ค่า enum ไม่รู้จัก / " +
|
||||
"JSON ผิด format / ไฟล์แนบใหญ่เกิน 100MB)",
|
||||
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" },
|
||||
},
|
||||
},
|
||||
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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
Loading…
Add table
Add a link
Reference in a new issue