improve api adapters
This commit is contained in:
parent
1298ae1a35
commit
cf34092ccc
9 changed files with 94 additions and 126 deletions
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T>(
|
||||
path: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<T> {
|
||||
protected async request<T>(path: string, options: RequestOptions = {}): Promise<T> {
|
||||
// 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<void> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')) {
|
||||
|
|
|
|||
|
|
@ -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<string, string>
|
||||
skills: Array<{
|
||||
id: string
|
||||
name: Record<string, string>
|
||||
slot: number
|
||||
}>
|
||||
accessory?: {
|
||||
id: string
|
||||
name: Record<string, string>
|
||||
}
|
||||
}
|
||||
raid?: {
|
||||
id: string
|
||||
name: Record<string, string>
|
||||
group?: {
|
||||
id: string
|
||||
name: Record<string, string>
|
||||
}
|
||||
}
|
||||
weapons: GridWeapon[]
|
||||
summons: GridSummon[]
|
||||
characters: GridCharacter[]
|
||||
guidebook?: {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
extras?: Record<string, any>
|
||||
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<Party> {
|
||||
async updateJob(shortcode: string, jobId: string): Promise<Party> {
|
||||
return this.request<Party>(`/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<Party> {
|
||||
return this.request<Party>(`/parties/${shortcode}/job_skills`, {
|
||||
console.log('[updateJobSkills] Input skills array:', skills)
|
||||
|
||||
// Convert skills array to Rails expected format
|
||||
const party: Record<string, string | null> = {}
|
||||
|
||||
// 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<Party>(`/parties/${partyId}/job_skills`, {
|
||||
method: 'PUT',
|
||||
body: {
|
||||
skills
|
||||
}
|
||||
body: requestBody
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a job skill from a party
|
||||
*/
|
||||
async removeJobSkill(
|
||||
shortcode: string,
|
||||
skillSlot: number
|
||||
): Promise<Party> {
|
||||
return this.request<Party>(`/parties/${shortcode}/job_skills`, {
|
||||
async removeJobSkill(partyId: string, skillSlot: number): Promise<Party> {
|
||||
return this.request<Party>(`/parties/${partyId}/job_skills`, {
|
||||
method: 'DELETE',
|
||||
body: {
|
||||
slot: skillSlot
|
||||
|
|
@ -310,7 +275,7 @@ export class PartyAdapter extends BaseAdapter {
|
|||
return this.request<Blob>(`/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)
|
||||
export const partyAdapter = new PartyAdapter(DEFAULT_ADAPTER_CONFIG)
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ export interface InfiniteScrollOptions<T> {
|
|||
* @example
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { createInfiniteScrollResource } from '$lib/api/adapters/resources'
|
||||
* import { partyAdapter } from '$lib/api/adapters'
|
||||
* import { createInfiniteScrollResource } from '$lib/api/adapters/resources/infiniteScroll.resource.svelte'
|
||||
* import { partyAdapter } from '$lib/api/adapters/party.adapter'
|
||||
*
|
||||
* const resource = createInfiniteScrollResource({
|
||||
* fetcher: (page) => partyAdapter.list({ page }),
|
||||
|
|
@ -243,7 +243,7 @@ export class InfiniteScrollResource<T> {
|
|||
} finally {
|
||||
this.loadingMore = false
|
||||
if (this.abortController) {
|
||||
this.abortController = undefined
|
||||
delete this.abortController
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@
|
|||
* @module adapters/resources/party
|
||||
*/
|
||||
|
||||
import { PartyAdapter, partyAdapter, type Party, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter'
|
||||
import { PartyAdapter, partyAdapter, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import type { AdapterError } from '../types'
|
||||
|
||||
/**
|
||||
|
|
@ -48,7 +49,7 @@ interface PartyListState {
|
|||
* @example
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { createPartyResource } from '$lib/api/adapters/resources'
|
||||
* import { createPartyResource } from '$lib/api/adapters/resources/party.resource.svelte'
|
||||
*
|
||||
* const party = createPartyResource()
|
||||
*
|
||||
|
|
@ -299,7 +300,7 @@ export class PartyResource {
|
|||
* Updates the job for the current party
|
||||
*/
|
||||
async updateJob(
|
||||
shortcode: string,
|
||||
partyId: string,
|
||||
jobId: string,
|
||||
skills?: Array<{ id: string; slot: number }>,
|
||||
accessoryId?: string
|
||||
|
|
@ -307,7 +308,16 @@ export class PartyResource {
|
|||
this.current = { ...this.current, updating: true, error: undefined }
|
||||
|
||||
try {
|
||||
const party = await this.adapter.updateJob(shortcode, jobId, skills, accessoryId)
|
||||
// Update job first
|
||||
let party = await this.adapter.updateJob(partyId, jobId)
|
||||
|
||||
// Update skills if provided
|
||||
if (skills) {
|
||||
party = await this.adapter.updateJobSkills(partyId, skills)
|
||||
}
|
||||
|
||||
// TODO: Handle accessory update when API supports it
|
||||
|
||||
this.current = { data: party, loading: false, updating: false }
|
||||
return party
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ interface SearchState {
|
|||
* @example
|
||||
* ```svelte
|
||||
* <script>
|
||||
* import { createSearchResource } from '$lib/api/adapters/resources'
|
||||
* import { createSearchResource } from '$lib/api/adapters/resources/search.resource.svelte'
|
||||
*
|
||||
* const search = createSearchResource({
|
||||
* debounceMs: 300,
|
||||
|
|
|
|||
|
|
@ -359,4 +359,4 @@ export interface ResourceOptions<T> {
|
|||
|
||||
/** Dependencies that trigger refetch when changed */
|
||||
dependencies?: any[]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,11 +321,8 @@ const GridCharacterSchema = z.object({
|
|||
perpetuity: z.boolean().nullish().default(false),
|
||||
|
||||
// Rings and earring
|
||||
ring1: RingSchema,
|
||||
ring2: RingSchema,
|
||||
ring3: RingSchema,
|
||||
ring4: RingSchema,
|
||||
earring: RingSchema,
|
||||
over_mastery: z.array(RingSchema).nullish(),
|
||||
aetherial_mastery: RingSchema.nullish(),
|
||||
|
||||
// Awakening
|
||||
awakening_id: z.string().nullish(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue