refactor api adapters to use new pattern

This commit is contained in:
Justin Edmund 2025-09-23 22:06:42 -07:00
parent 9ed6a00f5f
commit fc32e18ea8
5 changed files with 282 additions and 153 deletions

View file

@ -18,6 +18,8 @@ import {
calculateRetryDelay, calculateRetryDelay,
CancelledError CancelledError
} from './errors' } from './errors'
import { authStore } from '$lib/stores/auth.store'
import { browser } from '$app/environment'
/** /**
* Base adapter class that all resource-specific adapters extend from. * Base adapter class that all resource-specific adapters extend from.
@ -46,6 +48,9 @@ export abstract class BaseAdapter {
/** Configuration options for the adapter */ /** Configuration options for the adapter */
protected options: Required<AdapterOptions> protected options: Required<AdapterOptions>
/** Flag to disable caching entirely */
protected disableCache: boolean = false
/** /**
* Creates a new adapter instance * Creates a new adapter instance
* *
@ -97,8 +102,8 @@ export abstract class BaseAdapter {
// Check cache first if caching is enabled (support both cache and cacheTTL) // Check cache first if caching is enabled (support both cache and cacheTTL)
const cacheTime = options.cacheTTL ?? options.cache ?? this.options.cacheTime const cacheTime = options.cacheTTL ?? options.cache ?? this.options.cacheTime
// Allow caching for any method if explicitly set // Allow caching for any method if explicitly set (unless cache is disabled)
if (cacheTime > 0) { if (!this.disableCache && cacheTime > 0) {
const cached = this.getFromCache(requestId) const cached = this.getFromCache(requestId)
if (cached !== null) { if (cached !== null) {
return cached as T return cached as T
@ -112,26 +117,53 @@ export abstract class BaseAdapter {
const controller = new AbortController() const controller = new AbortController()
this.abortControllers.set(requestId, controller) this.abortControllers.set(requestId, controller)
// Get Bearer token from auth store (only in browser)
let authHeaders: Record<string, string> = {}
if (browser) {
const token = await authStore.checkAndRefresh()
if (token) {
authHeaders['Authorization'] = `Bearer ${token}`
} else {
console.warn('[BaseAdapter] No auth token available in authStore for request:', path)
}
}
// Prepare request options // Prepare request options
const fetchOptions: RequestInit = { const fetchOptions: RequestInit = {
credentials: 'include', // Default: Include cookies for authentication
...options, // Allow overriding defaults ...options, // Allow overriding defaults
credentials: 'include', // Still include cookies for CORS and refresh token
signal: controller.signal, signal: controller.signal,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...authHeaders,
...(options.headers || {}) ...(options.headers || {})
} }
} }
// Debug logging for auth issues
if (browser && path.includes('grid_')) {
console.log('[BaseAdapter] Request to:', path, 'Headers:', fetchOptions.headers)
}
// Transform request body from camelCase to snake_case if present // Transform request body from camelCase to snake_case if present
if (options.body) { if (options.body) {
if (typeof options.body === 'object') { if (typeof options.body === 'object') {
// Body is an object, transform and stringify // Body is an object, transform and stringify
fetchOptions.body = JSON.stringify(this.transformRequest(options.body)) const transformed = this.transformRequest(options.body)
fetchOptions.body = JSON.stringify(transformed)
// Debug logging for 422 errors
if (browser && path.includes('grid_')) {
console.log('[BaseAdapter] Request body:', transformed)
}
} else if (typeof options.body === 'string') { } else if (typeof options.body === 'string') {
try { try {
const bodyData = JSON.parse(options.body) const bodyData = JSON.parse(options.body)
fetchOptions.body = JSON.stringify(this.transformRequest(bodyData)) const transformed = this.transformRequest(bodyData)
fetchOptions.body = JSON.stringify(transformed)
// Debug logging for 422 errors
if (browser && path.includes('grid_')) {
console.log('[BaseAdapter] Request body:', transformed)
}
} catch { } catch {
// If body is not valid JSON, use as-is // If body is not valid JSON, use as-is
fetchOptions.body = options.body fetchOptions.body = options.body
@ -145,10 +177,17 @@ export abstract class BaseAdapter {
// Parse and transform the response // Parse and transform the response
const data = await response.json() const data = await response.json()
// Debug logging for grid operations
if (browser && path.includes('grid_')) {
console.log('[BaseAdapter] Response status:', response.status)
console.log('[BaseAdapter] Response data:', data)
}
const transformed = this.transformResponse<T>(data) const transformed = this.transformResponse<T>(data)
// Cache the successful response if caching is enabled (use cacheTTL or cache) // Cache the successful response if caching is enabled (use cacheTTL or cache)
if (cacheTime > 0) { if (!this.disableCache && cacheTime > 0) {
this.setCache(requestId, transformed, cacheTime) this.setCache(requestId, transformed, cacheTime)
} }
@ -340,10 +379,32 @@ export abstract class BaseAdapter {
// Build URL from base URL and path // Build URL from base URL and path
const baseURL = this.options.baseURL.replace(/\/$/, '') // Remove trailing slash const baseURL = this.options.baseURL.replace(/\/$/, '') // Remove trailing slash
const cleanPath = path.startsWith('/') ? path : `/${path}` const cleanPath = path.startsWith('/') ? path : `/${path}`
const url = new URL(`${baseURL}${cleanPath}`) const fullPath = `${baseURL}${cleanPath}`
this.addQueryParams(url, params) // Check if we have a relative URL (starts with /)
return url.toString() if (baseURL.startsWith('/')) {
// For relative URLs, we need to provide a base for the URL constructor
// but we'll return just the relative path for fetch
if (typeof window !== 'undefined') {
// In browser, use window.location.origin for URL construction
const url = new URL(fullPath, window.location.origin)
this.addQueryParams(url, params)
// Return just the pathname and search for relative fetch
return `${url.pathname}${url.search}`
} else {
// On server, construct the query string manually for relative paths
if (params && Object.keys(params).length > 0) {
const queryString = new URLSearchParams(this.transformRequest(params)).toString()
return `${fullPath}?${queryString}`
}
return fullPath
}
} else {
// For absolute base URLs, use the normal URL constructor
const url = new URL(fullPath)
this.addQueryParams(url, params)
return url.toString()
}
} }
/** /**

View file

@ -5,10 +5,10 @@ import { PUBLIC_SIERO_API_URL } from '$env/static/public'
/** /**
* Get the base URL for API requests * Get the base URL for API requests
* Handles both server and client environments * Always use direct API URL for both server and client
*/ */
export function getApiBaseUrl(): string { export function getApiBaseUrl(): string {
// Use environment variable if available, otherwise default to localhost // Always use direct API URL
const base = PUBLIC_SIERO_API_URL || 'http://localhost:3000' const base = PUBLIC_SIERO_API_URL || 'http://localhost:3000'
return `${base}/api/v1` return `${base}/api/v1`
} }

View file

@ -115,10 +115,10 @@ export interface UpdateUncapParams {
* Parameters for updating positions * Parameters for updating positions
*/ */
export interface UpdatePositionParams { export interface UpdatePositionParams {
partyId: string partyId: string
id: string id: string
position: number position: number
container?: string container?: string
} }
/** /**
@ -150,196 +150,259 @@ export class GridAdapter extends BaseAdapter {
/** /**
* Creates a new grid weapon instance * Creates a new grid weapon instance
*/ */
async createWeapon(params: CreateGridWeaponParams): Promise<GridWeapon> { async createWeapon(params: CreateGridWeaponParams, headers?: Record<string, string>): Promise<GridWeapon> {
return this.request<GridWeapon>('/grid_weapons', { return this.request<GridWeapon>('/grid_weapons', {
method: 'POST', method: 'POST',
body: params body: { weapon: params },
}) headers
} })
}
/** /**
* Updates a grid weapon instance * Updates a grid weapon instance
*/ */
async updateWeapon(id: string, params: Partial<GridWeapon>): Promise<GridWeapon> { async updateWeapon(id: string, params: Partial<GridWeapon>, headers?: Record<string, string>): Promise<GridWeapon> {
return this.request<GridWeapon>(`/grid_weapons/${id}`, { return this.request<GridWeapon>(`/grid_weapons/${id}`, {
method: 'PUT', method: 'PUT',
body: params body: { weapon: params },
}) headers
} })
}
/** /**
* Deletes a grid weapon instance * Deletes a grid weapon instance
*/ */
async deleteWeapon(params: { id?: string; partyId: string; position?: number }): Promise<void> { async deleteWeapon(params: { id?: string; partyId: string; position?: number }, headers?: Record<string, string>): Promise<void> {
return this.request<void>('/grid_weapons', { // If we have an ID, use it in the URL (standard Rails REST)
method: 'DELETE', if (params.id) {
body: params return this.request<void>(`/grid_weapons/${params.id}`, {
}) method: 'DELETE',
} headers
})
}
// Otherwise, send params in body for position-based delete
return this.request<void>('/grid_weapons/delete_by_position', {
method: 'DELETE',
body: params,
headers
})
}
/** /**
* Updates weapon uncap level * Updates weapon uncap level
*/ */
async updateWeaponUncap(params: UpdateUncapParams): Promise<GridWeapon> { async updateWeaponUncap(params: UpdateUncapParams, headers?: Record<string, string>): Promise<GridWeapon> {
return this.request<GridWeapon>('/grid_weapons/update_uncap', { return this.request<GridWeapon>('/grid_weapons/update_uncap', {
method: 'POST', method: 'POST',
body: params body: {
}) weapon: {
} id: params.id,
partyId: params.partyId,
uncapLevel: params.uncapLevel,
transcendenceStep: params.transcendenceStep
}
},
headers
})
}
/** /**
* Resolves weapon conflicts * Resolves weapon conflicts
*/ */
async resolveWeaponConflict(params: ResolveConflictParams): Promise<GridWeapon> { async resolveWeaponConflict(params: ResolveConflictParams, headers?: Record<string, string>): Promise<GridWeapon> {
return this.request<GridWeapon>('/grid_weapons/resolve', { return this.request<GridWeapon>('/grid_weapons/resolve', {
method: 'POST', method: 'POST',
body: params body: { resolve: params },
}) headers
} })
}
/** /**
* Updates weapon position * Updates weapon position
*/ */
async updateWeaponPosition(params: UpdatePositionParams): Promise<GridWeapon> { async updateWeaponPosition(params: UpdatePositionParams, headers?: Record<string, string>): Promise<GridWeapon> {
const { partyId, id, ...positionData } = params const { id, position, container, partyId } = params
return this.request<GridWeapon>(`/parties/${partyId}/grid_weapons/${id}/position`, { return this.request<GridWeapon>(`/parties/${partyId}/grid_weapons/${id}/position`, {
method: 'PUT', method: 'PUT',
body: positionData body: { position, container },
}) headers
} })
}
/** /**
* Swaps two weapon positions * Swaps two weapon positions
*/ */
async swapWeapons(params: SwapPositionsParams): Promise<{ async swapWeapons(params: SwapPositionsParams, headers?: Record<string, string>): Promise<{
source: GridWeapon source: GridWeapon
target: GridWeapon target: GridWeapon
}> { }> {
const { partyId, ...swapData } = params const { partyId, sourceId, targetId } = params
return this.request(`/parties/${partyId}/grid_weapons/swap`, { return this.request(`/parties/${partyId}/grid_weapons/swap`, {
method: 'POST', method: 'POST',
body: swapData body: { source_id: sourceId, target_id: targetId },
}) headers
} })
}
// Character operations // Character operations
/** /**
* Creates a new grid character instance * Creates a new grid character instance
*/ */
async createCharacter(params: CreateGridCharacterParams): Promise<GridCharacter> { async createCharacter(params: CreateGridCharacterParams, headers?: Record<string, string>): Promise<GridCharacter> {
return this.request<GridCharacter>('/grid_characters', { return this.request<GridCharacter>('/grid_characters', {
method: 'POST', method: 'POST',
body: params body: { character: params },
}) headers
} })
}
/** /**
* Updates a grid character instance * Updates a grid character instance
*/ */
async updateCharacter(id: string, params: Partial<GridCharacter>): Promise<GridCharacter> { async updateCharacter(id: string, params: Partial<GridCharacter>, headers?: Record<string, string>): Promise<GridCharacter> {
return this.request<GridCharacter>(`/grid_characters/${id}`, { return this.request<GridCharacter>(`/grid_characters/${id}`, {
method: 'PUT', method: 'PUT',
body: params body: { character: params },
}) headers
} })
}
/** /**
* Deletes a grid character instance * Deletes a grid character instance
*/ */
async deleteCharacter(params: { id?: string; partyId: string; position?: number }): Promise<void> { async deleteCharacter(params: { id?: string; partyId: string; position?: number }, headers?: Record<string, string>): Promise<void> {
return this.request<void>('/grid_characters', { // If we have an ID, use it in the URL (standard Rails REST)
method: 'DELETE', if (params.id) {
body: params return this.request<void>(`/grid_characters/${params.id}`, {
}) method: 'DELETE',
} headers
})
}
// Otherwise, send params in body for position-based delete
return this.request<void>('/grid_characters/delete_by_position', {
method: 'DELETE',
body: params,
headers
})
}
/** /**
* Updates character uncap level * Updates character uncap level
*/ */
async updateCharacterUncap(params: UpdateUncapParams): Promise<GridCharacter> { async updateCharacterUncap(params: UpdateUncapParams, headers?: Record<string, string>): Promise<GridCharacter> {
return this.request<GridCharacter>('/grid_characters/update_uncap', { return this.request<GridCharacter>('/grid_characters/update_uncap', {
method: 'POST', method: 'POST',
body: params body: {
}) character: {
} id: params.id,
partyId: params.partyId,
uncapLevel: params.uncapLevel,
transcendenceStep: params.transcendenceStep
}
},
headers
})
}
/** /**
* Resolves character conflicts * Resolves character conflicts
*/ */
async resolveCharacterConflict(params: ResolveConflictParams): Promise<GridCharacter> { async resolveCharacterConflict(params: ResolveConflictParams, headers?: Record<string, string>): Promise<GridCharacter> {
return this.request<GridCharacter>('/grid_characters/resolve', { return this.request<GridCharacter>('/grid_characters/resolve', {
method: 'POST', method: 'POST',
body: params body: { resolve: params },
}) headers
} })
}
/** /**
* Updates character position * Updates character position
*/ */
async updateCharacterPosition(params: UpdatePositionParams): Promise<GridCharacter> { async updateCharacterPosition(params: UpdatePositionParams, headers?: Record<string, string>): Promise<GridCharacter> {
const { partyId, id, ...positionData } = params const { id, position, container, partyId } = params
return this.request<GridCharacter>(`/parties/${partyId}/grid_characters/${id}/position`, { return this.request<GridCharacter>(`/parties/${partyId}/grid_characters/${id}/position`, {
method: 'PUT', method: 'PUT',
body: positionData body: { position, container },
}) headers
} })
}
/** /**
* Swaps two character positions * Swaps two character positions
*/ */
async swapCharacters(params: SwapPositionsParams): Promise<{ async swapCharacters(params: SwapPositionsParams, headers?: Record<string, string>): Promise<{
source: GridCharacter source: GridCharacter
target: GridCharacter target: GridCharacter
}> { }> {
const { partyId, ...swapData } = params const { partyId, sourceId, targetId } = params
return this.request(`/parties/${partyId}/grid_characters/swap`, { return this.request(`/parties/${partyId}/grid_characters/swap`, {
method: 'POST', method: 'POST',
body: swapData body: { source_id: sourceId, target_id: targetId },
}) headers
} })
}
// Summon operations // Summon operations
/** /**
* Creates a new grid summon instance * Creates a new grid summon instance
*/ */
async createSummon(params: CreateGridSummonParams): Promise<GridSummon> { async createSummon(params: CreateGridSummonParams, headers?: Record<string, string>): Promise<GridSummon> {
return this.request<GridSummon>('/grid_summons', { return this.request<GridSummon>('/grid_summons', {
method: 'POST', method: 'POST',
body: params body: { summon: params },
}) headers
} })
}
/** /**
* Updates a grid summon instance * Updates a grid summon instance
*/ */
async updateSummon(id: string, params: Partial<GridSummon>): Promise<GridSummon> { async updateSummon(id: string, params: Partial<GridSummon>, headers?: Record<string, string>): Promise<GridSummon> {
return this.request<GridSummon>(`/grid_summons/${id}`, { return this.request<GridSummon>(`/grid_summons/${id}`, {
method: 'PUT', method: 'PUT',
body: params body: { summon: params },
}) headers
} })
}
/** /**
* Deletes a grid summon instance * Deletes a grid summon instance
*/ */
async deleteSummon(params: { id?: string; partyId: string; position?: number }): Promise<void> { async deleteSummon(params: { id?: string; partyId: string; position?: number }, headers?: Record<string, string>): Promise<void> {
return this.request<void>('/grid_summons', { // If we have an ID, use it in the URL (standard Rails REST)
method: 'DELETE', if (params.id) {
body: params return this.request<void>(`/grid_summons/${params.id}`, {
}) method: 'DELETE',
} headers
})
}
// Otherwise, send params in body for position-based delete
return this.request<void>('/grid_summons/delete_by_position', {
method: 'DELETE',
body: params,
headers
})
}
/** /**
* Updates summon uncap level * Updates summon uncap level
*/ */
async updateSummonUncap(params: UpdateUncapParams): Promise<GridSummon> { async updateSummonUncap(params: UpdateUncapParams, headers?: Record<string, string>): Promise<GridSummon> {
return this.request<GridSummon>('/grid_summons/update_uncap', { return this.request<GridSummon>('/grid_summons/update_uncap', {
method: 'POST', method: 'POST',
body: params body: {
}) summon: {
} id: params.id,
partyId: params.partyId,
uncapLevel: params.uncapLevel,
transcendenceStep: params.transcendenceStep
}
},
headers
})
}
/** /**
* Updates summon quick summon setting * Updates summon quick summon setting
@ -359,27 +422,29 @@ export class GridAdapter extends BaseAdapter {
/** /**
* Updates summon position * Updates summon position
*/ */
async updateSummonPosition(params: UpdatePositionParams): Promise<GridSummon> { async updateSummonPosition(params: UpdatePositionParams, headers?: Record<string, string>): Promise<GridSummon> {
const { partyId, id, ...positionData } = params const { id, position, container, partyId } = params
return this.request<GridSummon>(`/parties/${partyId}/grid_summons/${id}/position`, { return this.request<GridSummon>(`/parties/${partyId}/grid_summons/${id}/position`, {
method: 'PUT', method: 'PUT',
body: positionData body: { position, container },
}) headers
} })
}
/** /**
* Swaps two summon positions * Swaps two summon positions
*/ */
async swapSummons(params: SwapPositionsParams): Promise<{ async swapSummons(params: SwapPositionsParams, headers?: Record<string, string>): Promise<{
source: GridSummon source: GridSummon
target: GridSummon target: GridSummon
}> { }> {
const { partyId, ...swapData } = params const { partyId, sourceId, targetId } = params
return this.request(`/parties/${partyId}/grid_summons/swap`, { return this.request(`/parties/${partyId}/grid_summons/swap`, {
method: 'POST', method: 'POST',
body: swapData body: { source_id: sourceId, target_id: targetId },
}) headers
} })
}
/** /**
* Clears grid-specific cache * Clears grid-specific cache
@ -396,4 +461,4 @@ export class GridAdapter extends BaseAdapter {
/** /**
* Default grid adapter instance * Default grid adapter instance
*/ */
export const gridAdapter = new GridAdapter(DEFAULT_ADAPTER_CONFIG) export const gridAdapter = new GridAdapter(DEFAULT_ADAPTER_CONFIG)

View file

@ -196,6 +196,8 @@ export interface GridUpdateResponse {
export class PartyAdapter extends BaseAdapter { export class PartyAdapter extends BaseAdapter {
constructor(options?: AdapterOptions) { constructor(options?: AdapterOptions) {
super(options) super(options)
// Temporarily disable cache until cache invalidation is fixed
this.disableCache = true
} }
/** /**

View file

@ -1,5 +1,6 @@
import { BaseAdapter } from './base.adapter' import { BaseAdapter } from './base.adapter'
import type { Party } from '$lib/types/api/party' import type { Party } from '$lib/types/api/party'
import type { RequestOptions } from './types'
import { DEFAULT_ADAPTER_CONFIG } from './config' import { DEFAULT_ADAPTER_CONFIG } from './config'
export interface UserInfo { export interface UserInfo {
@ -36,8 +37,8 @@ export class UserAdapter extends BaseAdapter {
/** /**
* Get user information * Get user information
*/ */
async getInfo(username: string): Promise<UserInfo> { async getInfo(username: string, options?: RequestOptions): Promise<UserInfo> {
return this.request<UserInfo>(`/users/info/${encodeURIComponent(username)}`) return this.request<UserInfo>(`/users/info/${encodeURIComponent(username)}`, options)
} }
/** /**