api.js (3629B)
1 import { STR } from './strings.js'; 2 3 const API_BASE = import.meta.env.VITE_API_BASE ?? ''; 4 5 function getPublicOrigin() { 6 const configured = import.meta.env.VITE_PUBLIC_ORIGIN?.trim(); 7 if (configured) { 8 return configured.replace(/\/$/, ''); 9 } 10 return window.location.origin; 11 } 12 13 export class ApiError extends Error { 14 constructor(status, code, message, payload = {}) { 15 super(message); 16 this.name = 'ApiError'; 17 this.status = status; 18 this.code = code; 19 this.payload = payload; 20 } 21 } 22 23 const ERROR_MESSAGES = { 24 NOT_FOUND: STR.NOT_FOUND, 25 EXPIRED: STR.EXPIRED, 26 DOCUMENT_LOCKED: STR.DOCUMENT_LOCKED, 27 LOCK_REQUIRED: STR.LOCK_REQUIRED, 28 CONTENT_TOO_LARGE: STR.CONTENT_TOO_LARGE, 29 RATE_LIMIT: STR.RATE_LIMIT, 30 ORIGIN_NOT_ALLOWED: STR.ORIGIN_NOT_ALLOWED, 31 NETWORK: STR.NETWORK, 32 }; 33 34 export function friendlyErrorMessage(error) { 35 if (error instanceof ApiError) { 36 return error.message || ERROR_MESSAGES[error.code] || STR.GENERIC_ERROR; 37 } 38 return ERROR_MESSAGES.NETWORK; 39 } 40 41 async function parseResponse(res) { 42 let data = null; 43 const text = await res.text(); 44 if (text) { 45 try { 46 data = JSON.parse(text); 47 } catch { 48 data = { message: text }; 49 } 50 } 51 52 if (!res.ok) { 53 const code = data?.error ?? 'UNKNOWN'; 54 const message = 55 data?.message ?? ERROR_MESSAGES[code] ?? STR.UNEXPECTED_ERROR; 56 throw new ApiError(res.status, code, message, data ?? {}); 57 } 58 59 return data; 60 } 61 62 async function request(path, options = {}) { 63 let res; 64 try { 65 res = await fetch(`${API_BASE}${path}`, { 66 ...options, 67 headers: { 68 'Content-Type': 'application/json', 69 ...(options.headers ?? {}), 70 }, 71 }); 72 } catch { 73 throw new ApiError(0, 'NETWORK', ERROR_MESSAGES.NETWORK); 74 } 75 return parseResponse(res); 76 } 77 78 export function createDocument(body) { 79 return request('/api/documents', { 80 method: 'POST', 81 body: JSON.stringify(body), 82 }); 83 } 84 85 export function fetchViewDocument(token) { 86 return request(`/api/documents/view/${encodeURIComponent(token)}`); 87 } 88 89 export function fetchEditDocument(token) { 90 return request(`/api/documents/edit/${encodeURIComponent(token)}`); 91 } 92 93 export function acquireEditLock(token, clientId) { 94 return request(`/api/documents/edit/${encodeURIComponent(token)}/lock`, { 95 method: 'POST', 96 body: JSON.stringify({ clientId }), 97 }); 98 } 99 100 export function refreshEditLock(token, clientId, lockToken) { 101 return request( 102 `/api/documents/edit/${encodeURIComponent(token)}/lock/refresh`, 103 { 104 method: 'POST', 105 body: JSON.stringify({ clientId, lockToken }), 106 }, 107 ); 108 } 109 110 export function releaseEditLock(token, clientId, lockToken) { 111 return request(`/api/documents/edit/${encodeURIComponent(token)}/lock`, { 112 method: 'DELETE', 113 body: JSON.stringify({ clientId, lockToken }), 114 }); 115 } 116 117 export function updateDocument(token, body) { 118 return request(`/api/documents/edit/${encodeURIComponent(token)}`, { 119 method: 'PUT', 120 body: JSON.stringify(body), 121 }); 122 } 123 124 export function fetchDocumentVersions(token, { clientId, lockToken }) { 125 const params = new URLSearchParams({ clientId, lockToken }); 126 return request( 127 `/api/documents/edit/${encodeURIComponent(token)}/versions?${params}`, 128 ); 129 } 130 131 export function restoreDocumentVersion(token, versionId, body) { 132 return request( 133 `/api/documents/edit/${encodeURIComponent(token)}/versions/${encodeURIComponent(versionId)}/restore`, 134 { 135 method: 'POST', 136 body: JSON.stringify(body), 137 }, 138 ); 139 } 140 141 export function toAbsoluteUrl(path) { 142 if (path.startsWith('http')) return path; 143 return `${getPublicOrigin()}${path}`; 144 }