88
99// Use relative path for API calls - Vite proxy will handle dev, production uses same origin
1010export const API_BASE = '/api' ;
11+ export const API_V2_BASE = '/api/v2' ;
1112export const ADMIN_UI_MUTATION_EVENT = 'windshift:admin-ui-mutation' ;
1213
1314// Ensure the clock-drift warning toast fires at most once per session
@@ -54,7 +55,7 @@ function normalizeGETEndpoint(endpoint) {
5455 }
5556}
5657
57- function inFlightGETKey ( endpoint , options ) {
58+ function inFlightGETKey ( base , endpoint , options ) {
5859 if ( ! apiRequestSessionKey ) return null ;
5960 const method = String ( options ?. method || 'GET' ) . toUpperCase ( ) ;
6061 if ( method !== 'GET' ) return null ;
@@ -64,7 +65,7 @@ function inFlightGETKey(endpoint, options) {
6465 const optionKeys = Object . keys ( options || { } ) ;
6566 if ( optionKeys . some ( ( key ) => key !== 'method' ) ) return null ;
6667
67- return `${ apiRequestSessionKey } |${ requestLocale ( ) } |${ normalizeGETEndpoint ( endpoint ) } ` ;
68+ return `${ apiRequestSessionKey } |${ requestLocale ( ) } |${ base } | ${ normalizeGETEndpoint ( endpoint ) } ` ;
6869}
6970
7071function isAdminUIPath ( ) {
@@ -110,9 +111,11 @@ function createApiError(response, responseText) {
110111 // Try to parse structured error from response
111112 try {
112113 const parsed = JSON . parse ( responseText ) ;
113- /** @type {any } */ ( error ) . code = parsed . code ;
114- /** @type {any } */ ( error ) . errorCode = parsed . code ; // Alias for compatibility
115- /** @type {any } */ ( error ) . details = parsed . details || { } ;
114+ const payload =
115+ typeof parsed . error === 'object' && parsed . error !== null ? parsed . error : parsed ;
116+ /** @type {any } */ ( error ) . code = payload . code ;
117+ /** @type {any } */ ( error ) . errorCode = payload . code ; // Alias for compatibility
118+ /** @type {any } */ ( error ) . details = payload . details || { } ;
116119 /** @type {any } */ ( error ) . requestId = parsed . request_id ;
117120 /** @type {any } */ ( error ) . body = parsed ;
118121 // Authentication policy responses carry flow-control fields alongside the
@@ -122,7 +125,8 @@ function createApiError(response, responseText) {
122125 /** @type {any } */ ( error ) . enrollment_required = parsed . enrollment_required === true ;
123126 /** @type {any } */ ( error ) . sso_required = parsed . sso_required === true ;
124127 /** @type {any } */ ( error ) . policy_message = parsed . policy_message ;
125- error . message = parsed . error || parsed . message || error . message ;
128+ error . message =
129+ ( typeof parsed . error === 'string' ? parsed . error : payload . message ) || error . message ;
126130 } catch {
127131 // Response is not JSON, keep original message
128132 }
@@ -135,10 +139,11 @@ function createApiError(response, responseText) {
135139}
136140
137141/**
142+ * @param {string } base
138143 * @param {string } endpoint
139144 * @param {RequestInit & { timeout?: number } } [options]
140145 */
141- async function performFetchAPI ( endpoint , options = { } ) {
146+ async function performFetchAPI ( base , endpoint , options = { } ) {
142147 const { timeout : requestedTimeout = 0 , signal : callerSignal , ...fetchOptions } = options ;
143148 const isFormData = typeof FormData !== 'undefined' && fetchOptions . body instanceof FormData ;
144149 const headers = isFormData
@@ -187,7 +192,7 @@ async function performFetchAPI(endpoint, options = {}) {
187192
188193 let response ;
189194 try {
190- response = await fetch ( `${ API_BASE } ${ endpoint } ` , {
195+ response = await fetch ( `${ base } ${ endpoint } ` , {
191196 ...fetchOptions ,
192197 credentials : 'same-origin' , // Include cookies for session auth
193198 headers,
@@ -301,14 +306,43 @@ async function performFetchAPI(endpoint, options = {}) {
301306}
302307
303308export function fetchAPI ( endpoint , options = { } ) {
304- const key = inFlightGETKey ( endpoint , options ) ;
305- if ( ! key ) return performFetchAPI ( endpoint , options ) ;
309+ return fetchFromAPI ( API_BASE , endpoint , options ) ;
310+ }
311+
312+ export function fetchAPIV2 ( endpoint , options = { } ) {
313+ return fetchFromAPI ( API_V2_BASE , endpoint , options ) ;
314+ }
315+
316+ export async function fetchV2Data ( endpoint , options = { } ) {
317+ const document = await fetchAPIV2 ( endpoint , options ) ;
318+ return document ?. data ;
319+ }
320+
321+ export async function fetchAllV2Pages ( endpoint , options = { } ) {
322+ const url = new URL ( endpoint , 'https://windshift.invalid' ) ;
323+ url . searchParams . set ( 'page_size' , '100' ) ;
324+ const items = [ ] ;
325+ let page = 1 ;
326+ let totalPages = 1 ;
327+ do {
328+ url . searchParams . set ( 'page' , String ( page ) ) ;
329+ const document = await fetchAPIV2 ( `${ url . pathname } ${ url . search } ` , options ) ;
330+ items . push ( ...( document ?. data ?? [ ] ) ) ;
331+ totalPages = document ?. pagination ?. total_pages ?? 0 ;
332+ page += 1 ;
333+ } while ( page <= totalPages ) ;
334+ return items ;
335+ }
336+
337+ function fetchFromAPI ( base , endpoint , options ) {
338+ const key = inFlightGETKey ( base , endpoint , options ) ;
339+ if ( ! key ) return performFetchAPI ( base , endpoint , options ) ;
306340
307341 const existing = inFlightGetRequests . get ( key ) ;
308342 if ( existing ) return existing ;
309343
310344 let trackedRequest ;
311- trackedRequest = performFetchAPI ( endpoint , options ) . finally ( ( ) => {
345+ trackedRequest = performFetchAPI ( base , endpoint , options ) . finally ( ( ) => {
312346 if ( inFlightGetRequests . get ( key ) === trackedRequest ) {
313347 inFlightGetRequests . delete ( key ) ;
314348 }
0 commit comments