From 9b54039a15f818e4c7df0125144e551816bc09a2 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:09:08 -0800 Subject: [PATCH] Remove legacy API layer (APIClient, core.ts, resources/) (#438) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Remove legacy API layer (APIClient, core.ts, resources/) ## Summary This PR consolidates the API layer by removing unused legacy code. The codebase had two API patterns: 1. **Adapters layer** (`src/lib/api/adapters/`) - The newer, canonical HTTP layer with retry logic, caching, and proper error handling 2. **Legacy layer** (`src/lib/api/client.ts`, `core.ts`, `resources/`) - An older pattern using SvelteKit proxy endpoints that was no longer imported anywhere The legacy layer was confirmed unused via grep analysis. The only exception was `transformResponse` and `transformRequest` functions in `client.ts` which were still used by `BaseAdapter` and `schemas/party.ts` - these have been moved to `schemas/transforms.ts`. **Files deleted:** - `src/lib/api/client.ts` (732 lines - APIClient class was dead code) - `src/lib/api/core.ts` (helper functions, unused) - `src/lib/api/index.ts` (empty file) - `src/lib/api/resources/` directory (search.ts, weapons.ts, characters.ts, summons.ts - all unused) **Files modified:** - `src/lib/api/schemas/transforms.ts` - Added `transformResponse`, `transformRequest`, and their helper functions - `src/lib/api/adapters/base.adapter.ts` - Updated import path - `src/lib/api/schemas/party.ts` - Updated import path ## Review & Testing Checklist for Human - [ ] **Verify transform functions work correctly** - The `transformResponse` and `transformRequest` functions handle critical data transformation (snake_case ↔ camelCase, object ↔ entity field renaming). Test that party data loads correctly with proper field names. - [ ] **Test party CRUD operations** - Create, update, and delete a party to verify the adapters layer still works end-to-end - [ ] **Test grid operations** - Add/remove weapons, characters, and summons to verify the entity field renaming still works correctly - [ ] **Check for any runtime errors** - The local type checking couldn't run due to missing dev dependencies, so CI is the first line of defense **Recommended test plan:** 1. Load an existing party page and verify all data displays correctly 2. Create a new party and add weapons/characters/summons 3. Edit an existing party (update name, add/remove items) 4. Verify search functionality works for weapons/characters/summons ### Notes - Local lint/typecheck commands failed due to missing dependencies (prettier-plugin-svelte, eslint-config-prettier) - relying on CI for type verification - The services layer (`src/lib/services/`) was intentionally kept as it's a business logic layer that wraps the adapters Link to Devin run: https://app.devin.ai/sessions/611580bc2db94e20a48c3692d3cbd432 Requested by: Justin Edmund (justin@jedmund.com) / @jedmund Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Justin Edmund --- src/lib/api/adapters/base.adapter.ts | 5 +- src/lib/api/client.ts | 732 --------------------------- src/lib/api/core.ts | 56 -- src/lib/api/index.ts | 0 src/lib/api/resources/characters.ts | 16 - src/lib/api/resources/search.ts | 171 ------- src/lib/api/resources/summons.ts | 16 - src/lib/api/resources/weapons.ts | 16 - src/lib/api/schemas/party.ts | 4 +- src/lib/api/schemas/transforms.ts | 144 +++++- 10 files changed, 147 insertions(+), 1013 deletions(-) delete mode 100644 src/lib/api/client.ts delete mode 100644 src/lib/api/core.ts delete mode 100644 src/lib/api/index.ts delete mode 100644 src/lib/api/resources/characters.ts delete mode 100644 src/lib/api/resources/search.ts delete mode 100644 src/lib/api/resources/summons.ts delete mode 100644 src/lib/api/resources/weapons.ts diff --git a/src/lib/api/adapters/base.adapter.ts b/src/lib/api/adapters/base.adapter.ts index 2b9bacd6..0b43c556 100644 --- a/src/lib/api/adapters/base.adapter.ts +++ b/src/lib/api/adapters/base.adapter.ts @@ -8,8 +8,7 @@ * @module adapters/base */ -import { snakeToCamel, camelToSnake } from '../schemas/transforms' -import { transformResponse, transformRequest } from '../client' +import { snakeToCamel, camelToSnake, transformResponse, transformRequest } from '../schemas/transforms' import type { AdapterOptions, RequestOptions, AdapterError } from './types' import { createErrorFromStatus, @@ -552,4 +551,4 @@ export abstract class BaseAdapter { this.cache.clear() } } -} \ No newline at end of file +} diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts deleted file mode 100644 index 858c6c0e..00000000 --- a/src/lib/api/client.ts +++ /dev/null @@ -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(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(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): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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() \ No newline at end of file diff --git a/src/lib/api/core.ts b/src/lib/api/core.ts deleted file mode 100644 index 7436ae75..00000000 --- a/src/lib/api/core.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { PUBLIC_SIERO_API_URL } from '$env/static/public' - -export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise -export type Dict = Record - -// 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(fetchFn: FetchLike, url: string, init?: RequestInit): Promise { - 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 -} - -export const get = (f: FetchLike, path: string, params?: Dict, init?: RequestInit) => - json(f, buildUrl(path, params), init) - -export const post = (f: FetchLike, path: string, body?: unknown, init?: RequestInit) => { - const extra = body !== undefined ? { body: JSON.stringify(body) } : {} - return json(f, buildUrl(path), { method: 'POST', ...extra, ...init }) -} - -export const put = (f: FetchLike, path: string, body?: unknown, init?: RequestInit) => { - const extra = body !== undefined ? { body: JSON.stringify(body) } : {} - return json(f, buildUrl(path), { method: 'PUT', ...extra, ...init }) -} - -export const del = (f: FetchLike, path: string, init?: RequestInit) => - json(f, buildUrl(path), { method: 'DELETE', ...init }) diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/lib/api/resources/characters.ts b/src/lib/api/resources/characters.ts deleted file mode 100644 index c67af371..00000000 --- a/src/lib/api/resources/characters.ts +++ /dev/null @@ -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(f, `/characters/${encodeURIComponent(id)}`, undefined, init) -} diff --git a/src/lib/api/resources/search.ts b/src/lib/api/resources/search.ts deleted file mode 100644 index 0cf1909e..00000000 --- a/src/lib/api/resources/search.ts +++ /dev/null @@ -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(fetchFn: FetchLike, url: string, body?: unknown): Promise { - 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 -} - -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 { - 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 { - 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 { - 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 { - 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) -} diff --git a/src/lib/api/resources/summons.ts b/src/lib/api/resources/summons.ts deleted file mode 100644 index 75e400ba..00000000 --- a/src/lib/api/resources/summons.ts +++ /dev/null @@ -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(f, `/summons/${encodeURIComponent(id)}`, undefined, init) -} diff --git a/src/lib/api/resources/weapons.ts b/src/lib/api/resources/weapons.ts deleted file mode 100644 index 9a38f2ba..00000000 --- a/src/lib/api/resources/weapons.ts +++ /dev/null @@ -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(f, `/weapons/${encodeURIComponent(id)}`, undefined, init) -} diff --git a/src/lib/api/schemas/party.ts b/src/lib/api/schemas/party.ts index b4467e54..ec87af61 100644 --- a/src/lib/api/schemas/party.ts +++ b/src/lib/api/schemas/party.ts @@ -464,8 +464,8 @@ export type RaidGroup = CamelCasedKeysDeep> export type User = CamelCasedKeysDeep> export type Guidebook = CamelCasedKeysDeep> -// Import transformation from client -import { transformResponse } from '../client' +// Import transformation from transforms +import { transformResponse } from './transforms' import type { Party as CleanParty } from '$lib/types/api/party' // Helper: parse raw API party (snake_case) and convert to clean types diff --git a/src/lib/api/schemas/transforms.ts b/src/lib/api/schemas/transforms.ts index f2b47f2f..3c865f34 100644 --- a/src/lib/api/schemas/transforms.ts +++ b/src/lib/api/schemas/transforms.ts @@ -40,4 +40,146 @@ export function camelToSnake(obj: T): T { } return obj -} \ No newline at end of file +} + +/** + * 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(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(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) +}