import { API_PATHS } from '../app/config.js'; function normalizeBaseUrl(baseUrl) { return String(baseUrl || '').trim().replace(/\/+$/, ''); } function isSameOriginBaseUrl(baseUrl) { if (!baseUrl) { return true; } if (typeof window === 'undefined') { return false; } try { return new URL(baseUrl, window.location.origin).origin === window.location.origin; } catch { return false; } } function buildPathname({ database, path }) { const encodedDatabase = encodeURIComponent(database); const rawPath = String(path || '').replace(/^\/+/, ''); const keepTrailingSlash = rawPath.endsWith('/'); const encodedPathSegments = rawPath .split('/') .filter(Boolean) .map((segment) => encodeURIComponent(segment)); const segments = [encodedDatabase]; segments.push(...encodedPathSegments); const pathname = `/${segments.join('/')}`; return keepTrailingSlash ? `${pathname}/` : pathname; } function buildUrl({ baseUrl, database, path, query = {} }) { const cleanBaseUrl = normalizeBaseUrl(baseUrl); const pathname = buildPathname({ database, path, }); const searchParams = new URLSearchParams(); Object.entries(query).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { searchParams.set(key, String(value)); } }); const queryString = searchParams.toString(); const relativeUrl = queryString ? `${pathname}?${queryString}` : pathname; if (!cleanBaseUrl || isSameOriginBaseUrl(cleanBaseUrl)) { return relativeUrl; } return new URL(relativeUrl, `${cleanBaseUrl}/`).toString(); } async function parseJsonResponse(response) { const text = await response.text(); if (!text) { return null; } try { return JSON.parse(text); } catch { return text; } } async function parseResponse(response) { if (response.status === 204) { return null; } const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { return parseJsonResponse(response); } if (contentType.includes('image/')) { return response.blob(); } const text = await response.text(); if (!text) { return null; } try { return JSON.parse(text); } catch { return text; } } function flattenErrorObject(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) { return null; } return Object.fromEntries( Object.entries(value).map(([key, entry]) => { if (Array.isArray(entry)) { return [key, entry.join(', ')]; } return [key, String(entry)]; }), ); } function extractErrorMessage(response, payload) { if (typeof payload === 'string' && payload.trim()) { return payload; } if (Array.isArray(payload) && payload.length) { return payload.map((entry) => String(entry)).join(', '); } if (payload && typeof payload === 'object') { const directMessage = payload.message || payload.error || payload.detail || payload.description || payload.title; if (typeof directMessage === 'string' && directMessage.trim()) { return directMessage; } const fieldErrors = flattenErrorObject(payload.errors || payload.details); if (fieldErrors) { return Object.entries(fieldErrors) .map(([field, message]) => `${field}: ${message}`) .join(' | '); } } return `Request failed with status ${response.status}.`; } function normalizeError(response, payload, request) { const details = flattenErrorObject(payload?.errors || payload?.details) || null; const message = extractErrorMessage(response, payload); const error = new Error(message, { cause: { status: response.status, details, url: request.url, method: request.method, payload, }, }); error.name = 'ApiRequestError'; error.status = response.status; error.url = request.url; error.method = request.method; error.details = details; error.payload = payload; return error; } function logApiFailure(message, context) { console.error(message, context); } function isAuthErrorStatus(status) { return status === 401 || status === 403; } function isKitchensPath(path) { return String(path || '').replace(/^\/+/, '').replace(/\/+$/, '') === API_PATHS.kitchens; } function isKitchenApiPath(path) { return String(path || '').replace(/^\/+/, '').startsWith('kitchen/'); } function shouldInvalidateValidatedSession(store, path, options = {}) { if (options.skipAuthFailureHandler) { return false; } if (!store.session?.applicationKey || !store.session?.hasValidated) { return false; } return ( isKitchensPath(path) || isKitchenApiPath(path) || path === API_PATHS.items || path === API_PATHS.locations || path === API_PATHS.changes || String(path || '').startsWith(`${API_PATHS.items}/`) ); } export async function apiRequest(store, path, options = {}) { const { config, session } = store; if (!config.database) { throw new Error('Database name is required.'); } const method = options.method || 'GET'; const url = buildUrl({ baseUrl: config.baseUrl, database: config.database, path, query: options.query, }); const headers = new Headers(options.headers || {}); headers.set('Accept', options.accept || 'application/json'); if (options.body && !options.isFormData) { headers.set('Content-Type', 'application/json'); } if (session?.applicationKey) { headers.set('Authorization', `Bearer ${session.applicationKey}`); } let response; try { response = await fetch(url, { method, headers, credentials: options.credentials || 'same-origin', body: options.body && !options.isFormData ? JSON.stringify(options.body) : options.body || undefined, }); } catch (error) { const networkError = new Error( `Network request failed for ${method} ${url}. Check the server URL, nginx proxy, and CORS configuration.`, { cause: { url, method, originalError: error, }, }, ); networkError.name = 'ApiNetworkError'; networkError.url = url; networkError.method = method; logApiFailure('API network request failed', { url, method, error, }); if (shouldInvalidateValidatedSession(store, path, options)) { window.__loncApp?.handleAuthFailure?.(networkError); } throw networkError; } const payload = await parseResponse(response); if (!response.ok) { const apiError = normalizeError(response, payload, { url, method }); logApiFailure('API request returned an error response', { url, method, status: response.status, payload, }); if ( isAuthErrorStatus(response.status) && ( shouldInvalidateValidatedSession(store, path, options) || (store.session?.state === 'connected' && response.status === 403 && isKitchensPath(path)) ) ) { window.__loncApp?.handleAuthFailure?.(apiError); } throw apiError; } return payload; } export function getPath(key) { return API_PATHS[key]; } export function buildKitchenApiUrl(store, path, query = {}) { return buildUrl({ baseUrl: store.config.baseUrl, database: store.config.database, path, query, }); }