Remove legacy API layer (APIClient, core.ts, resources/)
- Move transformResponse and transformRequest functions from client.ts to schemas/transforms.ts - Update imports in base.adapter.ts and schemas/party.ts - Delete unused APIClient class and related exports from client.ts - Delete src/lib/api/resources/ directory (search.ts, weapons.ts, characters.ts, summons.ts) - Delete src/lib/api/core.ts (unused helper functions) - Delete src/lib/api/index.ts (empty file) The adapters layer (src/lib/api/adapters/) is now the single, canonical HTTP layer. The services layer (src/lib/services/) remains as the business logic layer on top of adapters. Co-Authored-By: Justin Edmund <justin@jedmund.com>
This commit is contained in:
parent
d8eb6b965a
commit
40dc502027
10 changed files with 147 additions and 1013 deletions
|
|
@ -8,8 +8,7 @@
|
||||||
* @module adapters/base
|
* @module adapters/base
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { snakeToCamel, camelToSnake } from '../schemas/transforms'
|
import { snakeToCamel, camelToSnake, transformResponse, transformRequest } from '../schemas/transforms'
|
||||||
import { transformResponse, transformRequest } from '../client'
|
|
||||||
import type { AdapterOptions, RequestOptions, AdapterError } from './types'
|
import type { AdapterOptions, RequestOptions, AdapterError } from './types'
|
||||||
import {
|
import {
|
||||||
createErrorFromStatus,
|
createErrorFromStatus,
|
||||||
|
|
@ -552,4 +551,4 @@ export abstract class BaseAdapter {
|
||||||
this.cache.clear()
|
this.cache.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,732 +0,0 @@
|
||||||
/**
|
|
||||||
* Unified API Client for client-side use
|
|
||||||
* All API calls go through our SvelteKit proxy endpoints
|
|
||||||
* Automatically handles edit keys from localStorage
|
|
||||||
* Automatically transforms data between API format and clean types
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { snakeToCamel, camelToSnake } from './schemas/transforms'
|
|
||||||
|
|
||||||
export interface PartyPayload {
|
|
||||||
name?: string
|
|
||||||
description?: string | null
|
|
||||||
element?: number
|
|
||||||
visibility?: number
|
|
||||||
localId?: string
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GridItemOptions {
|
|
||||||
uncapLevel?: number
|
|
||||||
transcendenceStep?: number
|
|
||||||
element?: number
|
|
||||||
mainhand?: boolean
|
|
||||||
main?: boolean
|
|
||||||
friend?: boolean
|
|
||||||
quickSummon?: boolean
|
|
||||||
perpetuity?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms API response data to match our clean type definitions
|
|
||||||
* - Converts snake_case to camelCase
|
|
||||||
* - Renames "object" to proper entity names (weapon, character, summon)
|
|
||||||
*/
|
|
||||||
export function transformResponse<T>(data: any): T {
|
|
||||||
if (data === null || data === undefined) return data
|
|
||||||
|
|
||||||
// First convert snake_case to camelCase
|
|
||||||
const camelCased = snakeToCamel(data)
|
|
||||||
|
|
||||||
// Then rename "object" fields to proper entity names
|
|
||||||
return renameObjectFields(camelCased) as T
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms request data to match API expectations
|
|
||||||
* - Converts camelCase to snake_case
|
|
||||||
* - Renames entity names back to "object" for API
|
|
||||||
*/
|
|
||||||
export function transformRequest<T>(data: T): any {
|
|
||||||
if (data === null || data === undefined) return data
|
|
||||||
|
|
||||||
// First rename entity fields back to "object"
|
|
||||||
const withObjectFields = renameEntityFields(data)
|
|
||||||
|
|
||||||
// Then convert camelCase to snake_case
|
|
||||||
return camelToSnake(withObjectFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renames "object" fields to proper entity names in response data
|
|
||||||
*/
|
|
||||||
function renameObjectFields(obj: any): any {
|
|
||||||
if (obj === null || obj === undefined) return obj
|
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
return obj.map(renameObjectFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof obj === 'object') {
|
|
||||||
const result: any = {}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
// Handle weapons array
|
|
||||||
if (key === 'weapons' && Array.isArray(value)) {
|
|
||||||
result.weapons = value.map((item: any) => {
|
|
||||||
if (item && typeof item === 'object' && 'object' in item) {
|
|
||||||
const { object, ...rest } = item
|
|
||||||
return { ...rest, weapon: renameObjectFields(object) }
|
|
||||||
}
|
|
||||||
return renameObjectFields(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Handle characters array
|
|
||||||
else if (key === 'characters' && Array.isArray(value)) {
|
|
||||||
result.characters = value.map((item: any) => {
|
|
||||||
if (item && typeof item === 'object' && 'object' in item) {
|
|
||||||
const { object, ...rest } = item
|
|
||||||
return { ...rest, character: renameObjectFields(object) }
|
|
||||||
}
|
|
||||||
return renameObjectFields(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Handle summons array
|
|
||||||
else if (key === 'summons' && Array.isArray(value)) {
|
|
||||||
result.summons = value.map((item: any) => {
|
|
||||||
if (item && typeof item === 'object' && 'object' in item) {
|
|
||||||
const { object, ...rest } = item
|
|
||||||
return { ...rest, summon: renameObjectFields(object) }
|
|
||||||
}
|
|
||||||
return renameObjectFields(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Recursively process other fields
|
|
||||||
else {
|
|
||||||
result[key] = renameObjectFields(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renames entity fields back to "object" for API requests
|
|
||||||
*/
|
|
||||||
function renameEntityFields(obj: any): any {
|
|
||||||
if (obj === null || obj === undefined) return obj
|
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
return obj.map(renameEntityFields)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof obj === 'object') {
|
|
||||||
const result: any = {}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
// Handle weapons array
|
|
||||||
if (key === 'weapons' && Array.isArray(value)) {
|
|
||||||
result.weapons = value.map((item: any) => {
|
|
||||||
if (item && typeof item === 'object' && 'weapon' in item) {
|
|
||||||
const { weapon, ...rest } = item
|
|
||||||
return { ...rest, object: renameEntityFields(weapon) }
|
|
||||||
}
|
|
||||||
return renameEntityFields(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Handle characters array
|
|
||||||
else if (key === 'characters' && Array.isArray(value)) {
|
|
||||||
result.characters = value.map((item: any) => {
|
|
||||||
if (item && typeof item === 'object' && 'character' in item) {
|
|
||||||
const { character, ...rest } = item
|
|
||||||
return { ...rest, object: renameEntityFields(character) }
|
|
||||||
}
|
|
||||||
return renameEntityFields(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Handle summons array
|
|
||||||
else if (key === 'summons' && Array.isArray(value)) {
|
|
||||||
result.summons = value.map((item: any) => {
|
|
||||||
if (item && typeof item === 'object' && 'summon' in item) {
|
|
||||||
const { summon, ...rest } = item
|
|
||||||
return { ...rest, object: renameEntityFields(summon) }
|
|
||||||
}
|
|
||||||
return renameEntityFields(item)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// Recursively process other fields
|
|
||||||
else {
|
|
||||||
result[key] = renameEntityFields(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj
|
|
||||||
}
|
|
||||||
|
|
||||||
export class APIClient {
|
|
||||||
/**
|
|
||||||
* Get edit key for a party from localStorage
|
|
||||||
*/
|
|
||||||
private getEditKey(partyIdOrShortcode: string): string | null {
|
|
||||||
if (typeof window === 'undefined') return null
|
|
||||||
|
|
||||||
// Try both formats - with party ID and shortcode
|
|
||||||
const keyById = localStorage.getItem(`edit_key_${partyIdOrShortcode}`)
|
|
||||||
if (keyById) return keyById
|
|
||||||
|
|
||||||
// Also check if it's stored by shortcode
|
|
||||||
return localStorage.getItem(`edit_key_${partyIdOrShortcode}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store edit key for a party in localStorage
|
|
||||||
*/
|
|
||||||
storeEditKey(partyShortcode: string, editKey: string): void {
|
|
||||||
if (typeof window !== 'undefined' && editKey) {
|
|
||||||
localStorage.setItem(`edit_key_${partyShortcode}`, editKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new party
|
|
||||||
*/
|
|
||||||
async createParty(payload: PartyPayload): Promise<{ party: any; editKey?: string }> {
|
|
||||||
const response = await fetch('/api/parties', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to create party: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// Store edit key if present
|
|
||||||
if (data.edit_key && data.party?.shortcode) {
|
|
||||||
this.storeEditKey(data.party.shortcode, data.edit_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
party: data.party,
|
|
||||||
editKey: data.edit_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a party
|
|
||||||
*/
|
|
||||||
async updateParty(partyId: string, payload: Partial<PartyPayload>): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to update party: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
// The API returns { party: { ... } }, extract the party object
|
|
||||||
const party = data.party || data
|
|
||||||
// Transform the response to match our clean types
|
|
||||||
return transformResponse(party)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a party
|
|
||||||
*/
|
|
||||||
async deleteParty(partyId: string): Promise<void> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to delete party: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a weapon to a party
|
|
||||||
*/
|
|
||||||
async addWeapon(
|
|
||||||
partyId: string,
|
|
||||||
weaponId: string,
|
|
||||||
position: number,
|
|
||||||
options?: GridItemOptions
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/weapons`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
weaponId,
|
|
||||||
position,
|
|
||||||
...options
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to add weapon: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a weapon in a party
|
|
||||||
*/
|
|
||||||
async updateWeapon(
|
|
||||||
partyId: string,
|
|
||||||
gridWeaponId: string,
|
|
||||||
updates: {
|
|
||||||
position?: number
|
|
||||||
uncapLevel?: number
|
|
||||||
transcendenceStep?: number
|
|
||||||
element?: number
|
|
||||||
}
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/weapons/${gridWeaponId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updates)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to update weapon: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a weapon from a party
|
|
||||||
*/
|
|
||||||
async removeWeapon(partyId: string, gridWeaponId: string): Promise<void> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
console.log('Removing weapon:', { partyId, gridWeaponId, editKey })
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/weapons`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ gridWeaponId })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('Remove weapon failed:', response.status, response.statusText)
|
|
||||||
// Try to get the response text to see what the server is returning
|
|
||||||
const text = await response.text()
|
|
||||||
console.error('Response body:', text)
|
|
||||||
let error = { error: 'Failed to remove weapon' }
|
|
||||||
try {
|
|
||||||
error = JSON.parse(text)
|
|
||||||
} catch (e) {
|
|
||||||
// Not JSON, use the text as is
|
|
||||||
}
|
|
||||||
throw new Error(error.error || `Failed to remove weapon: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a summon to a party
|
|
||||||
*/
|
|
||||||
async addSummon(
|
|
||||||
partyId: string,
|
|
||||||
summonId: string,
|
|
||||||
position: number,
|
|
||||||
options?: GridItemOptions
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/summons`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
summonId,
|
|
||||||
position,
|
|
||||||
...options
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to add summon: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a summon in a party
|
|
||||||
*/
|
|
||||||
async updateSummon(
|
|
||||||
partyId: string,
|
|
||||||
gridSummonId: string,
|
|
||||||
updates: {
|
|
||||||
position?: number
|
|
||||||
quickSummon?: boolean
|
|
||||||
uncapLevel?: number
|
|
||||||
transcendenceStep?: number
|
|
||||||
}
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/summons/${gridSummonId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updates)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to update summon: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a summon from a party
|
|
||||||
*/
|
|
||||||
async removeSummon(partyId: string, gridSummonId: string): Promise<void> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/summons`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ gridSummonId })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json().catch(() => ({ error: 'Failed to remove summon' }))
|
|
||||||
throw new Error(error.error || `Failed to remove summon: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a character to a party
|
|
||||||
*/
|
|
||||||
async addCharacter(
|
|
||||||
partyId: string,
|
|
||||||
characterId: string,
|
|
||||||
position: number,
|
|
||||||
options?: GridItemOptions
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/characters`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
characterId,
|
|
||||||
position,
|
|
||||||
...options
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to add character: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a character in a party
|
|
||||||
*/
|
|
||||||
async updateCharacter(
|
|
||||||
partyId: string,
|
|
||||||
gridCharacterId: string,
|
|
||||||
updates: {
|
|
||||||
position?: number
|
|
||||||
uncapLevel?: number
|
|
||||||
transcendenceStep?: number
|
|
||||||
perpetuity?: boolean
|
|
||||||
}
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/characters/${gridCharacterId}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updates)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to update character: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a character from a party
|
|
||||||
*/
|
|
||||||
async removeCharacter(partyId: string, gridCharacterId: string): Promise<void> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/characters`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ gridCharacterId })
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json().catch(() => ({ error: 'Failed to remove character' }))
|
|
||||||
throw new Error(error.error || `Failed to remove character: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update weapon position (drag-drop)
|
|
||||||
*/
|
|
||||||
async updateWeaponPosition(
|
|
||||||
partyId: string,
|
|
||||||
weaponId: string,
|
|
||||||
position: number,
|
|
||||||
container?: string
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/grid_weapons/${weaponId}/position`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
position,
|
|
||||||
...(container ? { container } : {})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to update weapon position: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return transformResponse(data.party || data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swap two weapons (drag-drop)
|
|
||||||
*/
|
|
||||||
async swapWeapons(partyId: string, sourceId: string, targetId: string): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/grid_weapons/swap`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
source_id: sourceId,
|
|
||||||
target_id: targetId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to swap weapons: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return transformResponse(data.party || data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update character position (drag-drop)
|
|
||||||
*/
|
|
||||||
async updateCharacterPosition(
|
|
||||||
partyId: string,
|
|
||||||
characterId: string,
|
|
||||||
position: number,
|
|
||||||
container?: string
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/grid_characters/${characterId}/position`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
position,
|
|
||||||
...(container ? { container } : {})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to update character position: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return transformResponse(data.party || data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swap two characters (drag-drop)
|
|
||||||
*/
|
|
||||||
async swapCharacters(partyId: string, sourceId: string, targetId: string): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/grid_characters/swap`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
source_id: sourceId,
|
|
||||||
target_id: targetId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to swap characters: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return transformResponse(data.party || data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update summon position (drag-drop)
|
|
||||||
*/
|
|
||||||
async updateSummonPosition(
|
|
||||||
partyId: string,
|
|
||||||
summonId: string,
|
|
||||||
position: number,
|
|
||||||
container?: string
|
|
||||||
): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/grid_summons/${summonId}/position`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
position,
|
|
||||||
...(container ? { container } : {})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to update summon position: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return transformResponse(data.party || data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swap two summons (drag-drop)
|
|
||||||
*/
|
|
||||||
async swapSummons(partyId: string, sourceId: string, targetId: string): Promise<any> {
|
|
||||||
const editKey = this.getEditKey(partyId)
|
|
||||||
|
|
||||||
const response = await fetch(`/api/parties/${partyId}/grid_summons/swap`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(editKey ? { 'X-Edit-Key': editKey } : {})
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
source_id: sourceId,
|
|
||||||
target_id: targetId
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const error = await response.json()
|
|
||||||
throw new Error(error.error || `Failed to swap summons: ${response.statusText}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
return transformResponse(data.party || data)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get local ID for anonymous users
|
|
||||||
*/
|
|
||||||
getLocalId(): string {
|
|
||||||
if (typeof window === 'undefined') return ''
|
|
||||||
|
|
||||||
let localId = localStorage.getItem('local_id')
|
|
||||||
if (!localId) {
|
|
||||||
localId = crypto.randomUUID()
|
|
||||||
localStorage.setItem('local_id', localId)
|
|
||||||
}
|
|
||||||
return localId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export a singleton instance for convenience
|
|
||||||
export const apiClient = new APIClient()
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { PUBLIC_SIERO_API_URL } from '$env/static/public'
|
|
||||||
|
|
||||||
export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
|
|
||||||
export type Dict = Record<string, unknown>
|
|
||||||
|
|
||||||
// Compute a stable API base that always includes the versioned prefix.
|
|
||||||
function computeApiBase(): string {
|
|
||||||
const raw = (PUBLIC_SIERO_API_URL || 'http://localhost:3000') as string
|
|
||||||
const u = new URL(raw, raw.startsWith('http') ? undefined : 'http://localhost')
|
|
||||||
const origin = u.origin
|
|
||||||
const path = u.pathname.replace(/\/$/, '')
|
|
||||||
const hasVersion = /(\/api\/v1|\/v1)$/.test(path)
|
|
||||||
const basePath = hasVersion ? path : `${path}/api/v1`
|
|
||||||
return `${origin}${basePath}`
|
|
||||||
}
|
|
||||||
|
|
||||||
export const API_BASE = computeApiBase()
|
|
||||||
|
|
||||||
export function buildUrl(path: string, params?: Dict) {
|
|
||||||
const url = new URL(path.startsWith('http') ? path : `${API_BASE}${path}`, API_BASE)
|
|
||||||
if (params) {
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (value === undefined || value === null) continue
|
|
||||||
if (Array.isArray(value)) value.forEach((x) => url.searchParams.append(key, String(x)))
|
|
||||||
else url.searchParams.set(key, String(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return url.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function json<T>(fetchFn: FetchLike, url: string, init?: RequestInit): Promise<T> {
|
|
||||||
const res = await fetchFn(url, {
|
|
||||||
credentials: 'include',
|
|
||||||
headers: { 'Content-Type': 'application/json', ...(init?.headers || {}) },
|
|
||||||
...init
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status} ${url}`)
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const get = <T>(f: FetchLike, path: string, params?: Dict, init?: RequestInit) =>
|
|
||||||
json<T>(f, buildUrl(path, params), init)
|
|
||||||
|
|
||||||
export const post = <T>(f: FetchLike, path: string, body?: unknown, init?: RequestInit) => {
|
|
||||||
const extra = body !== undefined ? { body: JSON.stringify(body) } : {}
|
|
||||||
return json<T>(f, buildUrl(path), { method: 'POST', ...extra, ...init })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const put = <T>(f: FetchLike, path: string, body?: unknown, init?: RequestInit) => {
|
|
||||||
const extra = body !== undefined ? { body: JSON.stringify(body) } : {}
|
|
||||||
return json<T>(f, buildUrl(path), { method: 'PUT', ...extra, ...init })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const del = <T>(f: FetchLike, path: string, init?: RequestInit) =>
|
|
||||||
json<T>(f, buildUrl(path), { method: 'DELETE', ...init })
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import type { FetchLike } from '../core'
|
|
||||||
import { get } from '../core'
|
|
||||||
|
|
||||||
export interface CharacterEntity {
|
|
||||||
id: string
|
|
||||||
granblue_id: number | string
|
|
||||||
name: { en?: string; ja?: string } | string
|
|
||||||
element?: number
|
|
||||||
rarity?: number
|
|
||||||
uncap?: { flb?: boolean; ulb?: boolean }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const characters = {
|
|
||||||
show: (f: FetchLike, id: string, init?: RequestInit) =>
|
|
||||||
get<CharacterEntity>(f, `/characters/${encodeURIComponent(id)}`, undefined, init)
|
|
||||||
}
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
import type { FetchLike, Dict } from '../core'
|
|
||||||
import { buildUrl, API_BASE } from '../core'
|
|
||||||
|
|
||||||
// Custom JSON fetch without credentials for search endpoints to avoid CORS issues
|
|
||||||
async function searchJson<T>(fetchFn: FetchLike, url: string, body?: unknown): Promise<T> {
|
|
||||||
const res = await fetchFn(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(body)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status} ${url}`)
|
|
||||||
return res.json() as Promise<T>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchParams {
|
|
||||||
query?: string
|
|
||||||
locale?: 'en' | 'ja'
|
|
||||||
exclude?: string[]
|
|
||||||
page?: number
|
|
||||||
per?: number
|
|
||||||
filters?: {
|
|
||||||
element?: number[]
|
|
||||||
rarity?: number[]
|
|
||||||
proficiency1?: number[] // For weapons and characters
|
|
||||||
proficiency2?: number[] // For characters only
|
|
||||||
series?: number[]
|
|
||||||
extra?: boolean
|
|
||||||
subaura?: boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchResult {
|
|
||||||
id: string
|
|
||||||
granblue_id: string
|
|
||||||
name: { en?: string; ja?: string }
|
|
||||||
element?: number
|
|
||||||
rarity?: number
|
|
||||||
proficiency?: number
|
|
||||||
series?: number
|
|
||||||
image_url?: string
|
|
||||||
searchable_type: 'Weapon' | 'Character' | 'Summon'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SearchResponse {
|
|
||||||
results: SearchResult[]
|
|
||||||
total?: number
|
|
||||||
page?: number
|
|
||||||
total_pages?: number
|
|
||||||
meta?: {
|
|
||||||
count: number
|
|
||||||
page: number
|
|
||||||
per_page: number
|
|
||||||
total_pages: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function searchAll(
|
|
||||||
params: SearchParams,
|
|
||||||
init?: RequestInit,
|
|
||||||
fetchFn: FetchLike = fetch
|
|
||||||
): Promise<SearchResponse> {
|
|
||||||
const body = {
|
|
||||||
query: params.query || '',
|
|
||||||
locale: params.locale || 'en',
|
|
||||||
page: params.page || 1,
|
|
||||||
exclude: params.exclude || [],
|
|
||||||
filters: params.filters || {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${API_BASE}/search/all`
|
|
||||||
return searchJson(fetchFn, url, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function searchWeapons(
|
|
||||||
params: SearchParams,
|
|
||||||
init?: RequestInit,
|
|
||||||
fetchFn: FetchLike = fetch
|
|
||||||
): Promise<SearchResponse> {
|
|
||||||
const body: any = {
|
|
||||||
locale: params.locale || 'en',
|
|
||||||
page: params.page || 1,
|
|
||||||
per: params.per || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include query if it's provided and not empty
|
|
||||||
if (params.query) {
|
|
||||||
body.query = params.query
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include filters if they have values
|
|
||||||
const filters: any = {}
|
|
||||||
if (params.filters?.element?.length) filters.element = params.filters.element
|
|
||||||
if (params.filters?.rarity?.length) filters.rarity = params.filters.rarity
|
|
||||||
if (params.filters?.proficiency1?.length) filters.proficiency1 = params.filters.proficiency1
|
|
||||||
if (params.filters?.extra !== undefined) filters.extra = params.filters.extra
|
|
||||||
|
|
||||||
if (Object.keys(filters).length > 0) {
|
|
||||||
body.filters = filters
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${API_BASE}/search/weapons`
|
|
||||||
console.log('[searchWeapons] Making request to:', url)
|
|
||||||
console.log('[searchWeapons] Request body:', body)
|
|
||||||
|
|
||||||
return searchJson(fetchFn, url, body).then(response => {
|
|
||||||
console.log('[searchWeapons] Response received:', response)
|
|
||||||
return response
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function searchCharacters(
|
|
||||||
params: SearchParams,
|
|
||||||
init?: RequestInit,
|
|
||||||
fetchFn: FetchLike = fetch
|
|
||||||
): Promise<SearchResponse> {
|
|
||||||
const body: any = {
|
|
||||||
locale: params.locale || 'en',
|
|
||||||
page: params.page || 1,
|
|
||||||
per: params.per || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include query if it's provided and not empty
|
|
||||||
if (params.query) {
|
|
||||||
body.query = params.query
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include filters if they have values
|
|
||||||
const filters: any = {}
|
|
||||||
if (params.filters?.element?.length) filters.element = params.filters.element
|
|
||||||
if (params.filters?.rarity?.length) filters.rarity = params.filters.rarity
|
|
||||||
if (params.filters?.proficiency1?.length) filters.proficiency1 = params.filters.proficiency1
|
|
||||||
if (params.filters?.proficiency2?.length) filters.proficiency2 = params.filters.proficiency2
|
|
||||||
|
|
||||||
if (Object.keys(filters).length > 0) {
|
|
||||||
body.filters = filters
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${API_BASE}/search/characters`
|
|
||||||
return searchJson(fetchFn, url, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function searchSummons(
|
|
||||||
params: SearchParams,
|
|
||||||
init?: RequestInit,
|
|
||||||
fetchFn: FetchLike = fetch
|
|
||||||
): Promise<SearchResponse> {
|
|
||||||
const body: any = {
|
|
||||||
locale: params.locale || 'en',
|
|
||||||
page: params.page || 1,
|
|
||||||
per: params.per || undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include query if it's provided and not empty
|
|
||||||
if (params.query) {
|
|
||||||
body.query = params.query
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only include filters if they have values
|
|
||||||
const filters: any = {}
|
|
||||||
if (params.filters?.element?.length) filters.element = params.filters.element
|
|
||||||
if (params.filters?.rarity?.length) filters.rarity = params.filters.rarity
|
|
||||||
if (params.filters?.subaura !== undefined) filters.subaura = params.filters.subaura
|
|
||||||
|
|
||||||
if (Object.keys(filters).length > 0) {
|
|
||||||
body.filters = filters
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = `${API_BASE}/search/summons`
|
|
||||||
return searchJson(fetchFn, url, body)
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import type { FetchLike } from '../core'
|
|
||||||
import { get } from '../core'
|
|
||||||
|
|
||||||
export interface SummonEntity {
|
|
||||||
id: string
|
|
||||||
granblue_id: number
|
|
||||||
name: { en?: string; ja?: string } | string
|
|
||||||
element?: number
|
|
||||||
rarity?: number
|
|
||||||
uncap?: { flb?: boolean; ulb?: boolean; transcendence?: boolean }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const summons = {
|
|
||||||
show: (f: FetchLike, id: string, init?: RequestInit) =>
|
|
||||||
get<SummonEntity>(f, `/summons/${encodeURIComponent(id)}`, undefined, init)
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import type { FetchLike } from '../core'
|
|
||||||
import { get } from '../core'
|
|
||||||
|
|
||||||
export interface WeaponEntity {
|
|
||||||
id: string
|
|
||||||
granblue_id: number
|
|
||||||
name: { en?: string; ja?: string } | string
|
|
||||||
element?: number
|
|
||||||
rarity?: number
|
|
||||||
uncap?: { flb?: boolean; ulb?: boolean; transcendence?: boolean }
|
|
||||||
}
|
|
||||||
|
|
||||||
export const weapons = {
|
|
||||||
show: (f: FetchLike, id: string, init?: RequestInit) =>
|
|
||||||
get<WeaponEntity>(f, `/weapons/${encodeURIComponent(id)}`, undefined, init)
|
|
||||||
}
|
|
||||||
|
|
@ -464,8 +464,8 @@ export type RaidGroup = CamelCasedKeysDeep<z.infer<typeof RaidGroupSchema>>
|
||||||
export type User = CamelCasedKeysDeep<z.infer<typeof UserSchema>>
|
export type User = CamelCasedKeysDeep<z.infer<typeof UserSchema>>
|
||||||
export type Guidebook = CamelCasedKeysDeep<z.infer<typeof GuidebookSchema>>
|
export type Guidebook = CamelCasedKeysDeep<z.infer<typeof GuidebookSchema>>
|
||||||
|
|
||||||
// Import transformation from client
|
// Import transformation from transforms
|
||||||
import { transformResponse } from '../client'
|
import { transformResponse } from './transforms'
|
||||||
import type { Party as CleanParty } from '$lib/types/api/party'
|
import type { Party as CleanParty } from '$lib/types/api/party'
|
||||||
|
|
||||||
// Helper: parse raw API party (snake_case) and convert to clean types
|
// Helper: parse raw API party (snake_case) and convert to clean types
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,146 @@ export function camelToSnake<T>(obj: T): T {
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames "object" fields to proper entity names in response data
|
||||||
|
*/
|
||||||
|
function renameObjectFields(obj: any): any {
|
||||||
|
if (obj === null || obj === undefined) return obj
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(renameObjectFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const result: any = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
// Handle weapons array
|
||||||
|
if (key === 'weapons' && Array.isArray(value)) {
|
||||||
|
result.weapons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'object' in item) {
|
||||||
|
const { object, ...rest } = item
|
||||||
|
return { ...rest, weapon: renameObjectFields(object) }
|
||||||
|
}
|
||||||
|
return renameObjectFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle characters array
|
||||||
|
else if (key === 'characters' && Array.isArray(value)) {
|
||||||
|
result.characters = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'object' in item) {
|
||||||
|
const { object, ...rest } = item
|
||||||
|
return { ...rest, character: renameObjectFields(object) }
|
||||||
|
}
|
||||||
|
return renameObjectFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle summons array
|
||||||
|
else if (key === 'summons' && Array.isArray(value)) {
|
||||||
|
result.summons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'object' in item) {
|
||||||
|
const { object, ...rest } = item
|
||||||
|
return { ...rest, summon: renameObjectFields(object) }
|
||||||
|
}
|
||||||
|
return renameObjectFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Recursively process other fields
|
||||||
|
else {
|
||||||
|
result[key] = renameObjectFields(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames entity fields back to "object" for API requests
|
||||||
|
*/
|
||||||
|
function renameEntityFields(obj: any): any {
|
||||||
|
if (obj === null || obj === undefined) return obj
|
||||||
|
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(renameEntityFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const result: any = {}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
// Handle weapons array
|
||||||
|
if (key === 'weapons' && Array.isArray(value)) {
|
||||||
|
result.weapons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'weapon' in item) {
|
||||||
|
const { weapon, ...rest } = item
|
||||||
|
return { ...rest, object: renameEntityFields(weapon) }
|
||||||
|
}
|
||||||
|
return renameEntityFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle characters array
|
||||||
|
else if (key === 'characters' && Array.isArray(value)) {
|
||||||
|
result.characters = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'character' in item) {
|
||||||
|
const { character, ...rest } = item
|
||||||
|
return { ...rest, object: renameEntityFields(character) }
|
||||||
|
}
|
||||||
|
return renameEntityFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Handle summons array
|
||||||
|
else if (key === 'summons' && Array.isArray(value)) {
|
||||||
|
result.summons = value.map((item: any) => {
|
||||||
|
if (item && typeof item === 'object' && 'summon' in item) {
|
||||||
|
const { summon, ...rest } = item
|
||||||
|
return { ...rest, object: renameEntityFields(summon) }
|
||||||
|
}
|
||||||
|
return renameEntityFields(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Recursively process other fields
|
||||||
|
else {
|
||||||
|
result[key] = renameEntityFields(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms API response data to match our clean type definitions
|
||||||
|
* - Converts snake_case to camelCase
|
||||||
|
* - Renames "object" to proper entity names (weapon, character, summon)
|
||||||
|
*/
|
||||||
|
export function transformResponse<T>(data: any): T {
|
||||||
|
if (data === null || data === undefined) return data
|
||||||
|
|
||||||
|
// First convert snake_case to camelCase
|
||||||
|
const camelCased = snakeToCamel(data)
|
||||||
|
|
||||||
|
// Then rename "object" fields to proper entity names
|
||||||
|
return renameObjectFields(camelCased) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms request data to match API expectations
|
||||||
|
* - Converts camelCase to snake_case
|
||||||
|
* - Renames entity names back to "object" for API
|
||||||
|
*/
|
||||||
|
export function transformRequest<T>(data: T): any {
|
||||||
|
if (data === null || data === undefined) return data
|
||||||
|
|
||||||
|
// First rename entity fields back to "object"
|
||||||
|
const withObjectFields = renameEntityFields(data)
|
||||||
|
|
||||||
|
// Then convert camelCase to snake_case
|
||||||
|
return camelToSnake(withObjectFields)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue