hensei-web/src/lib/api/resources/parties.ts

358 lines
No EOL
8.1 KiB
TypeScript

import { buildUrl, get, post, put, del, type FetchLike } from '$lib/api/core'
import { parseParty } from '$lib/api/schemas/party'
import type { Party } from '$lib/types/api/party'
import { camelToSnake } from '$lib/api/schemas/transforms'
import { z } from 'zod'
/**
* Party API resource functions
*/
// Response schemas
// Note: The API returns snake_case; we validate with raw schemas and
// convert to camelCase via parseParty() at the edge.
const PartyResponseSchema = z.object({
party: z.any() // We'll validate after extracting
})
const PartiesResponseSchema = z.object({
parties: z.array(z.any()),
total: z.number().optional()
})
const ConflictResponseSchema = z.object({
conflicts: z.array(z.string()),
incoming: z.string(),
position: z.number()
})
const PaginatedPartiesSchema = z.object({
results: z.array(z.any()),
meta: z
.object({
count: z.number().optional(),
total_pages: z.number().optional(),
per_page: z.number().optional()
})
.optional()
})
// API functions
export async function getByShortcode(fetch: FetchLike, shortcode: string): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(shortcode)}`)
const res = await fetch(url, { credentials: 'include' })
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
// Extract the party object (API returns { party: {...} })
const partyData = json?.party || json
// Validate and transform snake_case to camelCase
const parsed = parseParty(partyData)
return parsed
}
export async function create(
fetch: FetchLike,
payload: Partial<Party>,
headers?: Record<string, string>
): Promise<{ party: Party; editKey?: string }> {
const url = buildUrl('/parties')
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
const party = parseParty(json.party)
return {
party,
editKey: json.edit_key
}
}
export async function update(
fetch: FetchLike,
id: string,
payload: Partial<Party>,
headers?: Record<string, string>
): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(id)}`)
const res = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
return parseParty(json.party || json)
}
export async function remix(
fetch: FetchLike,
shortcode: string,
localId?: string,
headers?: Record<string, string>
): Promise<{ party: Party; editKey?: string }> {
const url = buildUrl(`/parties/${encodeURIComponent(shortcode)}/remix`)
const payload = localId ? { local_id: localId } : {}
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(payload),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
const party = parseParty(json.party)
return {
party,
editKey: json.edit_key
}
}
export async function deleteParty(
fetch: FetchLike,
id: string,
headers?: Record<string, string>
): Promise<void> {
const url = buildUrl(`/parties/${encodeURIComponent(id)}`)
const res = await fetch(url, {
method: 'DELETE',
headers: {
...headers
},
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
}
export async function getUserParties(
fetch: FetchLike,
username: string,
filters?: {
raid?: string
element?: number
recency?: number
page?: number
}
): Promise<{
parties: Party[]
meta?: {
count?: number
totalPages?: number
perPage?: number
}
}> {
const params = new URLSearchParams()
if (filters?.raid) params.set('raid', filters.raid)
if (filters?.element !== undefined) params.set('element', filters.element.toString())
if (filters?.recency !== undefined) params.set('recency', filters.recency.toString())
if (filters?.page !== undefined) params.set('page', filters.page.toString())
const queryString = params.toString()
const url = buildUrl(`/users/${encodeURIComponent(username)}/parties${queryString ? `?${queryString}` : ''}`)
const res = await fetch(url, { credentials: 'include' })
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
const parsed = PaginatedPartiesSchema.safeParse(json)
if (!parsed.success) {
// Fallback for different response formats
const fallback = PartiesResponseSchema.safeParse(json)
if (fallback.success) {
return {
parties: fallback.data.parties.map(parseParty)
}
}
throw new Error('Invalid response format')
}
return {
parties: parsed.data.results.map(parseParty),
meta: parsed.data.meta
? {
count: parsed.data.meta.count,
totalPages: parsed.data.meta.total_pages,
perPage: parsed.data.meta.per_page
}
: undefined
}
}
// Grid operations
export async function updateWeaponGrid(
fetch: FetchLike,
partyId: string,
payload: any,
headers?: Record<string, string>
): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_weapons`)
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
// Check for conflicts
if (json.conflicts) {
const error = new Error('Weapon conflict') as any
error.conflicts = json
throw error
}
return parseParty(json.party || json)
}
export async function updateSummonGrid(
fetch: FetchLike,
partyId: string,
payload: any,
headers?: Record<string, string>
): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_summons`)
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
return parseParty(json.party || json)
}
export async function updateCharacterGrid(
fetch: FetchLike,
partyId: string,
payload: any,
headers?: Record<string, string>
): Promise<Party> {
const url = buildUrl(`/parties/${encodeURIComponent(partyId)}/grid_characters`)
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...headers
},
body: JSON.stringify(camelToSnake(payload)),
credentials: 'include'
})
if (!res.ok) {
const error = await parseError(res)
throw error
}
const json = await res.json()
// Check for conflicts
if (json.conflicts) {
const error = new Error('Character conflict') as any
error.conflicts = json
throw error
}
return parseParty(json.party || json)
}
// Error parsing
async function parseError(res: Response): Promise<Error & { status: number; details?: any[] }> {
let message = 'Request failed'
let details: any[] = []
try {
const errorData = await res.json()
if (errorData.error) {
message = errorData.error
} else if (errorData.errors) {
if (Array.isArray(errorData.errors)) {
message = errorData.errors.join(', ')
details = errorData.errors
} else if (typeof errorData.errors === 'object') {
const messages: string[] = []
for (const [field, errors] of Object.entries(errorData.errors)) {
if (Array.isArray(errors)) {
messages.push(`${field}: ${errors.join(', ')}`)
}
}
message = messages.join('; ')
details = Object.entries(errorData.errors)
}
}
} catch {
// If JSON parsing fails, use status text
message = res.statusText || message
}
const error = new Error(message) as Error & { status: number; details?: any[] }
error.status = res.status
if (details.length > 0) {
error.details = details
}
return error
}