From cf34092ccc58ed8f52a3dc7b8cc5cf0c9fe591bd Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 29 Sep 2025 23:47:01 -0700 Subject: [PATCH] improve api adapters --- .../adapters/__tests__/party.adapter.test.ts | 16 +-- src/lib/api/adapters/base.adapter.ts | 31 +++--- src/lib/api/adapters/errors.ts | 35 +++--- src/lib/api/adapters/party.adapter.ts | 103 ++++++------------ .../infiniteScroll.resource.svelte.ts | 6 +- .../resources/party.resource.svelte.ts | 18 ++- .../resources/search.resource.svelte.ts | 2 +- src/lib/api/adapters/types.ts | 2 +- src/lib/api/schemas/party.ts | 7 +- 9 files changed, 94 insertions(+), 126 deletions(-) diff --git a/src/lib/api/adapters/__tests__/party.adapter.test.ts b/src/lib/api/adapters/__tests__/party.adapter.test.ts index e8f227c9..14f66246 100644 --- a/src/lib/api/adapters/__tests__/party.adapter.test.ts +++ b/src/lib/api/adapters/__tests__/party.adapter.test.ts @@ -7,7 +7,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { PartyAdapter } from '../party.adapter' -import type { Party } from '../party.adapter' +import type { Party } from '$lib/types/api/party' describe('PartyAdapter', () => { let adapter: PartyAdapter @@ -25,26 +25,26 @@ describe('PartyAdapter', () => { }, job: { id: 'job-1', - name: { en: 'Warrior' }, + name: { en: 'Warrior', ja: 'ウォリアー' }, skills: [ { id: 'skill-1', - name: { en: 'Rage' }, + name: { en: 'Rage', ja: 'レイジ' }, slot: 1 } ] }, raid: { id: 'raid-1', - name: { en: 'Proto Bahamut' }, + name: { en: 'Proto Bahamut', ja: 'プロトバハムート' }, group: { id: 'group-1', - name: { en: 'Tier 1' } + name: { en: 'Tier 1', ja: 'ティア1' } } }, - gridWeapons: [], - gridSummons: [], - gridCharacters: [], + weapons: [], + summons: [], + characters: [], createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z' } diff --git a/src/lib/api/adapters/base.adapter.ts b/src/lib/api/adapters/base.adapter.ts index 2b9bacd6..4d111c24 100644 --- a/src/lib/api/adapters/base.adapter.ts +++ b/src/lib/api/adapters/base.adapter.ts @@ -8,7 +8,6 @@ * @module adapters/base */ -import { snakeToCamel, camelToSnake } from '../schemas/transforms' import { transformResponse, transformRequest } from '../client' import type { AdapterOptions, RequestOptions, AdapterError } from './types' import { @@ -90,10 +89,7 @@ export abstract class BaseAdapter { * }) * ``` */ - protected async request( - path: string, - options: RequestOptions = {} - ): Promise { + protected async request(path: string, options: RequestOptions = {}): Promise { // Build the full URL with query parameters (support both params and query) const url = this.buildURL(path, options.query || options.params) @@ -264,7 +260,7 @@ export abstract class BaseAdapter { */ cancelAll(): void { // Abort all pending requests - this.abortControllers.forEach(controller => controller.abort()) + this.abortControllers.forEach((controller) => controller.abort()) this.abortControllers.clear() } @@ -290,8 +286,9 @@ export abstract class BaseAdapter { let response: Response if (this.options.timeout > 0) { const timeoutId = setTimeout(() => { - const controller = Array.from(this.abortControllers.values()) - .find(c => c.signal === options.signal) + const controller = Array.from(this.abortControllers.values()).find( + (c) => c.signal === options.signal + ) controller?.abort() }, this.options.timeout) @@ -349,7 +346,6 @@ export abstract class BaseAdapter { } } - /** * Delays execution for a specified duration * Used for retry backoff @@ -358,7 +354,7 @@ export abstract class BaseAdapter { * @returns Promise that resolves after the delay */ protected delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)) } /** @@ -425,7 +421,7 @@ export abstract class BaseAdapter { // Handle arrays by adding multiple params with the same key if (Array.isArray(value)) { - value.forEach(item => { + value.forEach((item) => { url.searchParams.append(key, String(item)) }) } else { @@ -465,7 +461,7 @@ export abstract class BaseAdapter { let hash = 0 for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i) - hash = ((hash << 5) - hash) + char + hash = (hash << 5) - hash + char hash = hash & hash // Convert to 32bit integer } return hash.toString(36) @@ -487,10 +483,11 @@ export abstract class BaseAdapter { const errorData = await response.json() // Extract error message from various possible formats - message = errorData.message || - errorData.error || - errorData.errors?.[0]?.message || - response.statusText + message = + errorData.message || + errorData.error || + errorData.errors?.[0]?.message || + response.statusText details = errorData } catch { @@ -552,4 +549,4 @@ export abstract class BaseAdapter { this.cache.clear() } } -} \ No newline at end of file +} diff --git a/src/lib/api/adapters/errors.ts b/src/lib/api/adapters/errors.ts index ac2a29ef..08ab5976 100644 --- a/src/lib/api/adapters/errors.ts +++ b/src/lib/api/adapters/errors.ts @@ -14,7 +14,7 @@ import type { AdapterError } from './types' * Extends the native Error class with additional properties */ export class ApiError extends Error implements AdapterError { - name: 'AdapterError' = 'AdapterError' + name = 'AdapterError' as const code: string status: number details?: any @@ -173,11 +173,7 @@ export class RateLimitError extends ApiError { * @param details - Additional error details * @returns Appropriate error instance based on status code */ -export function createErrorFromStatus( - status: number, - message?: string, - details?: any -): ApiError { +export function createErrorFromStatus(status: number, message?: string, details?: any): ApiError { switch (status) { case 400: return new ApiError('BAD_REQUEST', status, message || 'Bad request', details) @@ -190,7 +186,9 @@ export function createErrorFromStatus( case 404: // Pass the message to NotFoundError if provided - return message ? new ApiError('NOT_FOUND', 404, message, details) : new NotFoundError(undefined, details) + return message + ? new ApiError('NOT_FOUND', 404, message, details) + : new NotFoundError(undefined, details) case 409: return new ConflictError(message, details) @@ -254,7 +252,11 @@ export function isRetryableError(error: any): boolean { // Check by error code (handles both ApiError instances and plain objects) // Note: NetworkError sets name to 'NetworkError' but still has AdapterError structure - if (error instanceof ApiError || error?.name === 'AdapterError' || error?.name === 'NetworkError') { + if ( + error instanceof ApiError || + error?.name === 'AdapterError' || + error?.name === 'NetworkError' + ) { const retryableCodes = [ 'NETWORK_ERROR', 'TIMEOUT', @@ -321,11 +323,7 @@ export function normalizeError(error: any): AdapterError { // Generic Error with status if (error?.status) { - return createErrorFromStatus( - error.status, - error.message || error.statusText, - error - ).toJSON() + return createErrorFromStatus(error.status, error.message || error.statusText, error).toJSON() } // Fallback to generic error @@ -349,11 +347,12 @@ export function getErrorMessage(error: any): string { } // Try to get message from various error formats - const message = error.message || - error.error || - error.errors?.[0]?.message || - error.statusText || - 'An unknown error occurred' + const message = + error.message || + error.error || + error.errors?.[0]?.message || + error.statusText || + 'An unknown error occurred' // Make network errors more user-friendly if (message.includes('NetworkError') || message.includes('Failed to fetch')) { diff --git a/src/lib/api/adapters/party.adapter.ts b/src/lib/api/adapters/party.adapter.ts index bc1e25cc..598c3e58 100644 --- a/src/lib/api/adapters/party.adapter.ts +++ b/src/lib/api/adapters/party.adapter.ts @@ -9,62 +9,10 @@ */ import { BaseAdapter } from './base.adapter' -import type { RequestOptions, AdapterOptions, PaginatedResponse } from './types' +import type { AdapterOptions, PaginatedResponse } from './types' import { DEFAULT_ADAPTER_CONFIG } from './config' import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party' -/** - * Party data structure - */ -export interface Party { - id: string - shortcode: string - name?: string - description?: string - visibility: 'public' | 'private' | 'unlisted' - user: { - id: string - username: string - } - job?: { - id: string - name: Record - skills: Array<{ - id: string - name: Record - slot: number - }> - accessory?: { - id: string - name: Record - } - } - raid?: { - id: string - name: Record - group?: { - id: string - name: Record - } - } - weapons: GridWeapon[] - summons: GridSummon[] - characters: GridCharacter[] - guidebook?: { - id: string - title: string - } - extras?: Record - createdAt: string - updatedAt: string -} - -// GridWeapon type is imported from types/api/party - -// GridSummon type is imported from types/api/party - -// GridCharacter type is imported from types/api/party - /** * Parameters for creating a new party */ @@ -261,10 +209,7 @@ export class PartyAdapter extends BaseAdapter { /** * Updates the job for a party */ - async updateJob( - shortcode: string, - jobId: string - ): Promise { + async updateJob(shortcode: string, jobId: string): Promise { return this.request(`/parties/${shortcode}/jobs`, { method: 'PUT', body: { @@ -277,25 +222,45 @@ export class PartyAdapter extends BaseAdapter { * Updates job skills for a party */ async updateJobSkills( - shortcode: string, + partyId: string, skills: Array<{ id: string; slot: number }> ): Promise { - return this.request(`/parties/${shortcode}/job_skills`, { + console.log('[updateJobSkills] Input skills array:', skills) + + // Convert skills array to Rails expected format + const party: Record = {} + + // Initialize all slots with null + for (let i = 1; i <= 4; i++) { + party[`skill${i}_id`] = null + } + + // Set the provided skills + skills.forEach(skill => { + // Rails expects skill1_id, skill2_id, skill3_id, skill4_id + party[`skill${skill.slot + 1}_id`] = skill.id + }) + + const requestBody = { + party + } + + console.log('[updateJobSkills] Sending to server:', { + url: `/parties/${partyId}/job_skills`, + body: requestBody + }) + + return this.request(`/parties/${partyId}/job_skills`, { method: 'PUT', - body: { - skills - } + body: requestBody }) } /** * Removes a job skill from a party */ - async removeJobSkill( - shortcode: string, - skillSlot: number - ): Promise { - return this.request(`/parties/${shortcode}/job_skills`, { + async removeJobSkill(partyId: string, skillSlot: number): Promise { + return this.request(`/parties/${partyId}/job_skills`, { method: 'DELETE', body: { slot: skillSlot @@ -310,7 +275,7 @@ export class PartyAdapter extends BaseAdapter { return this.request(`/parties/${shortcode}/preview`, { method: 'GET', headers: { - 'Accept': 'image/png' + Accept: 'image/png' } }) } @@ -377,4 +342,4 @@ export class PartyAdapter extends BaseAdapter { /** * Default party adapter instance */ -export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG) \ No newline at end of file +export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG) diff --git a/src/lib/api/adapters/resources/infiniteScroll.resource.svelte.ts b/src/lib/api/adapters/resources/infiniteScroll.resource.svelte.ts index 588d40ca..b243dd7e 100644 --- a/src/lib/api/adapters/resources/infiniteScroll.resource.svelte.ts +++ b/src/lib/api/adapters/resources/infiniteScroll.resource.svelte.ts @@ -42,8 +42,8 @@ export interface InfiniteScrollOptions { * @example * ```svelte *