2026-04-06 09:24:22 +02:00
import { API _PATHS } from '../app/config.js' ;
2026-04-06 10:48:03 +02:00
import { resolveApiRequestUrl } from './url.js' ;
2026-04-06 10:30:37 +02:00
async function parseJsonResponse ( response ) {
const text = await response . text ( ) ;
if ( ! text ) {
return null ;
}
try {
return JSON . parse ( text ) ;
} catch {
return text ;
}
2026-04-06 09:24:22 +02:00
}
async function parseResponse ( response ) {
2026-04-06 10:30:37 +02:00
if ( response . status === 204 ) {
return null ;
}
2026-04-06 09:24:22 +02:00
const contentType = response . headers . get ( 'content-type' ) || '' ;
if ( contentType . includes ( 'application/json' ) ) {
2026-04-06 10:30:37 +02:00
return parseJsonResponse ( response ) ;
2026-04-06 09:24:22 +02:00
}
if ( contentType . includes ( 'image/' ) ) {
return response . blob ( ) ;
}
2026-04-06 10:30:37 +02:00
const text = await response . text ( ) ;
if ( ! text ) {
2026-04-06 09:24:22 +02:00
return null ;
}
2026-04-06 10:30:37 +02:00
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 } . ` ;
2026-04-06 09:24:22 +02:00
}
2026-04-06 10:30:37 +02:00
function normalizeError ( response , payload , request ) {
const details = flattenErrorObject ( payload ? . errors || payload ? . details ) || null ;
2026-04-06 10:48:03 +02:00
const payloadMessage = extractErrorMessage ( response , payload ) ;
const statusHint =
response . status === 404
? 'This usually means the request hit the wrong origin, port, or path.'
: response . status === 401 || response . status === 403
? 'Authentication or application-key validation failed.'
: response . status >= 500
? 'The backend reported a server error.'
: '' ;
const message = [
` ${ request . method } ${ request . resolvedUrl } returned ${ response . status } . ` ,
statusHint ,
payloadMessage && ! payloadMessage . startsWith ( 'Request failed with status' )
? payloadMessage
: '' ,
]
. filter ( Boolean )
. join ( ' ' ) ;
2026-04-06 09:24:22 +02:00
2026-04-06 10:30:37 +02:00
const error = new Error ( message , {
2026-04-06 09:24:22 +02:00
cause : {
status : response . status ,
2026-04-06 10:30:37 +02:00
details ,
2026-04-06 10:48:03 +02:00
url : request . resolvedUrl ,
fetchUrl : request . fetchUrl ,
2026-04-06 10:30:37 +02:00
method : request . method ,
payload ,
2026-04-06 09:24:22 +02:00
} ,
} ) ;
2026-04-06 10:30:37 +02:00
error . name = 'ApiRequestError' ;
error . status = response . status ;
2026-04-06 10:48:03 +02:00
error . url = request . resolvedUrl ;
error . fetchUrl = request . fetchUrl ;
2026-04-06 10:30:37 +02:00
error . method = request . method ;
error . details = details ;
error . payload = payload ;
return error ;
}
function logApiFailure ( message , context ) {
console . error ( message , context ) ;
2026-04-06 09:24:22 +02:00
}
export async function apiRequest ( store , path , options = { } ) {
const { config , session , activeKitchen } = store ;
2026-04-06 10:30:37 +02:00
if ( ! config . database ) {
throw new Error ( 'Database name is required.' ) ;
2026-04-06 09:24:22 +02:00
}
2026-04-06 10:30:37 +02:00
const method = options . method || 'GET' ;
2026-04-06 10:48:03 +02:00
const currentOrigin = typeof window !== 'undefined' ? window . location . origin : '' ;
const requestTarget = resolveApiRequestUrl ( {
2026-04-06 10:30:37 +02:00
baseUrl : config . baseUrl ,
2026-04-06 10:48:03 +02:00
currentOrigin ,
2026-04-06 10:30:37 +02:00
database : config . database ,
kitchenId : activeKitchen ? . id ,
path ,
query : options . query ,
includeKitchen : options . includeKitchen !== false ,
} ) ;
2026-04-06 09:24:22 +02:00
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 } ` ) ;
}
2026-04-06 10:30:37 +02:00
let response ;
try {
2026-04-06 10:48:03 +02:00
response = await fetch ( requestTarget . fetchUrl , {
2026-04-06 10:30:37 +02:00
method ,
headers ,
credentials : options . credentials || 'same-origin' ,
body :
options . body && ! options . isFormData
? JSON . stringify ( options . body )
: options . body || undefined ,
} ) ;
} catch ( error ) {
2026-04-06 10:48:03 +02:00
const networkHint = requestTarget . isCrossOrigin
? 'Cross-origin request failed before a usable response was returned. Check nginx CORS/preflight handling and that the configured API origin is reachable.'
: 'Request failed before a response was returned. Check that the current origin serves the API or configure the Tryton server URL explicitly.' ;
2026-04-06 10:30:37 +02:00
const networkError = new Error (
2026-04-06 10:48:03 +02:00
` ${ method } ${ requestTarget . resolvedUrl } failed before a response was returned. ${ networkHint } ` ,
2026-04-06 10:30:37 +02:00
{
cause : {
2026-04-06 10:48:03 +02:00
url : requestTarget . resolvedUrl ,
fetchUrl : requestTarget . fetchUrl ,
2026-04-06 10:30:37 +02:00
method ,
2026-04-06 10:48:03 +02:00
isCrossOrigin : requestTarget . isCrossOrigin ,
2026-04-06 10:30:37 +02:00
originalError : error ,
} ,
} ,
) ;
networkError . name = 'ApiNetworkError' ;
2026-04-06 10:48:03 +02:00
networkError . url = requestTarget . resolvedUrl ;
networkError . fetchUrl = requestTarget . fetchUrl ;
2026-04-06 10:30:37 +02:00
networkError . method = method ;
2026-04-06 10:48:03 +02:00
networkError . isCrossOrigin = requestTarget . isCrossOrigin ;
2026-04-06 10:30:37 +02:00
logApiFailure ( 'API network request failed' , {
2026-04-06 10:48:03 +02:00
url : requestTarget . resolvedUrl ,
fetchUrl : requestTarget . fetchUrl ,
2026-04-06 10:30:37 +02:00
method ,
error ,
} ) ;
throw networkError ;
}
2026-04-06 09:24:22 +02:00
const payload = await parseResponse ( response ) ;
if ( ! response . ok ) {
2026-04-06 10:48:03 +02:00
const apiError = normalizeError ( response , payload , {
resolvedUrl : requestTarget . resolvedUrl ,
fetchUrl : requestTarget . fetchUrl ,
method ,
} ) ;
2026-04-06 10:30:37 +02:00
logApiFailure ( 'API request returned an error response' , {
2026-04-06 10:48:03 +02:00
url : requestTarget . resolvedUrl ,
fetchUrl : requestTarget . fetchUrl ,
2026-04-06 10:30:37 +02:00
method ,
status : response . status ,
payload ,
} ) ;
throw apiError ;
2026-04-06 09:24:22 +02:00
}
return payload ;
}
export function getPath ( key ) {
return API _PATHS [ key ] ;
}
export function buildKitchenApiUrl ( store , path , query = { } ) {
2026-04-06 10:48:03 +02:00
return resolveApiRequestUrl ( {
2026-04-06 09:24:22 +02:00
baseUrl : store . config . baseUrl ,
2026-04-06 10:48:03 +02:00
currentOrigin : typeof window !== 'undefined' ? window . location . origin : '' ,
2026-04-06 09:24:22 +02:00
database : store . config . database ,
kitchenId : store . activeKitchen ? . id ,
path ,
query ,
includeKitchen : true ,
2026-04-06 10:48:03 +02:00
} ) . resolvedUrl ;
2026-04-06 09:24:22 +02:00
}