hensei-web/src/lib/api/adapters/collection.adapter.ts
2025-12-03 09:03:47 -08:00

415 lines
10 KiB
TypeScript

/**
* Collection Adapter
*
* Handles all collection-related API operations including CRUD for
* characters, weapons, summons, and job accessories in a user's collection.
*
* @module adapters/collection
*/
import { BaseAdapter } from './base.adapter'
import type { AdapterOptions, PaginatedResponse } from './types'
import { DEFAULT_ADAPTER_CONFIG } from './config'
import type {
CollectionCharacter,
CollectionWeapon,
CollectionSummon,
CollectionJobAccessory,
CollectionCharacterInput,
CollectionWeaponInput,
CollectionSummonInput,
CollectionJobAccessoryInput,
CollectionFilters
} from '$lib/types/api/collection'
/**
* Parameters for listing collection items with pagination
*/
export interface CollectionListParams extends CollectionFilters {
page?: number
limit?: number
}
/**
* Response structure for paginated collection list
*/
export interface CollectionCharacterListResponse {
characters: CollectionCharacter[]
meta: {
count: number
totalPages: number
perPage: number
currentPage: number
}
}
/**
* Collection adapter for managing user collections
*/
export class CollectionAdapter extends BaseAdapter {
constructor(options?: AdapterOptions) {
super(options)
}
// ============================================
// Collection Characters
// ============================================
/**
* Lists a user's collection characters with optional filters
* Works for any user - privacy is enforced server-side
*/
async listCharacters(
userId: string,
params: CollectionListParams = {}
): Promise<PaginatedResponse<CollectionCharacter>> {
const response = await this.request<CollectionCharacterListResponse>(
`/users/${userId}/collection/characters`,
{
method: 'GET',
query: params
}
)
return {
results: response.characters,
page: response.meta.currentPage,
total: response.meta.count,
totalPages: response.meta.totalPages,
perPage: response.meta.perPage
}
}
/**
* Gets a single collection character by ID
*/
async getCharacter(id: string): Promise<CollectionCharacter> {
return this.request<CollectionCharacter>(`/collection/characters/${id}`, {
method: 'GET'
})
}
/**
* Adds a character to the collection
*/
async addCharacter(input: CollectionCharacterInput): Promise<CollectionCharacter> {
return this.request<CollectionCharacter>('/collection/characters', {
method: 'POST',
body: {
collectionCharacter: input
}
})
}
/**
* Adds multiple characters to the collection in a single batch request
*/
async addCharacters(inputs: CollectionCharacterInput[]): Promise<CollectionCharacter[]> {
if (inputs.length === 0) return []
const response = await this.request<{
characters: CollectionCharacter[]
meta: { created: number; skipped: number; errors: any[] }
}>('/collection/characters/batch', {
method: 'POST',
body: {
collectionCharacters: inputs
}
})
return response.characters
}
/**
* Updates a collection character
*/
async updateCharacter(
id: string,
input: Partial<CollectionCharacterInput>
): Promise<CollectionCharacter> {
return this.request<CollectionCharacter>(`/collection/characters/${id}`, {
method: 'PATCH',
body: {
collectionCharacter: input
}
})
}
/**
* Removes a character from the collection
*/
async removeCharacter(id: string): Promise<void> {
return this.request<void>(`/collection/characters/${id}`, {
method: 'DELETE'
})
}
/**
* Gets the IDs of all characters in a user's collection
* Useful for filtering out already-owned characters in the add modal
*/
async getCollectedCharacterIds(userId: string): Promise<string[]> {
// Fetch all pages to get complete list
const allIds: string[] = []
let page = 1
let hasMore = true
while (hasMore) {
const response = await this.listCharacters(userId, { page, limit: 100 })
allIds.push(...response.results.map((c) => c.character.id))
hasMore = page < response.totalPages
page++
}
return allIds
}
// ============================================
// Collection Weapons
// ============================================
/**
* Lists a user's collection weapons with optional filters
* Works for any user - privacy is enforced server-side
*/
async listWeapons(
userId: string,
params: CollectionListParams = {}
): Promise<PaginatedResponse<CollectionWeapon>> {
const response = await this.request<{
weapons?: CollectionWeapon[]
collectionWeapons?: CollectionWeapon[]
meta: { count: number; totalPages: number; perPage: number; currentPage: number }
}>(`/users/${userId}/collection/weapons`, {
method: 'GET',
query: params
})
// Handle both 'weapons' and 'collectionWeapons' response keys
// (backend currently returns 'collectionWeapons', should return 'weapons')
const weapons = response.weapons ?? response.collectionWeapons ?? []
return {
results: weapons,
page: response.meta.currentPage,
total: response.meta.count,
totalPages: response.meta.totalPages,
perPage: response.meta.perPage
}
}
/**
* Adds a weapon to the collection
*/
async addWeapon(input: CollectionWeaponInput): Promise<CollectionWeapon> {
return this.request<CollectionWeapon>('/collection/weapons', {
method: 'POST',
body: {
collectionWeapon: input
}
})
}
/**
* Adds multiple weapons to the collection in a single batch request
* Handles quantity expansion - each quantity > 1 creates multiple entries
*/
async addWeapons(
inputs: Array<CollectionWeaponInput & { quantity?: number }>
): Promise<CollectionWeapon[]> {
// Expand inputs based on quantity
const expanded = inputs.flatMap((input) => {
const count = input.quantity ?? 1
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { quantity, ...rest } = input
return Array.from({ length: count }, () => ({ ...rest })) as CollectionWeaponInput[]
})
if (expanded.length === 0) return []
const response = await this.request<{
weapons: CollectionWeapon[]
meta: { created: number; errors: any[] }
}>('/collection/weapons/batch', {
method: 'POST',
body: {
collectionWeapons: expanded
}
})
return response.weapons
}
/**
* Updates a collection weapon
*/
async updateWeapon(id: string, input: Partial<CollectionWeaponInput>): Promise<CollectionWeapon> {
return this.request<CollectionWeapon>(`/collection/weapons/${id}`, {
method: 'PATCH',
body: {
collectionWeapon: input
}
})
}
/**
* Removes a weapon from the collection
*/
async removeWeapon(id: string): Promise<void> {
return this.request<void>(`/collection/weapons/${id}`, {
method: 'DELETE'
})
}
// ============================================
// Collection Summons
// ============================================
/**
* Lists a user's collection summons with optional filters
* Works for any user - privacy is enforced server-side
*/
async listSummons(
userId: string,
params: CollectionListParams = {}
): Promise<PaginatedResponse<CollectionSummon>> {
const response = await this.request<{
summons?: CollectionSummon[]
collectionSummons?: CollectionSummon[]
meta: { count: number; totalPages: number; perPage: number; currentPage: number }
}>(`/users/${userId}/collection/summons`, {
method: 'GET',
query: params
})
// Handle both 'summons' and 'collectionSummons' response keys
// (backend currently returns 'collectionSummons', should return 'summons')
const summons = response.summons ?? response.collectionSummons ?? []
return {
results: summons,
page: response.meta.currentPage,
total: response.meta.count,
totalPages: response.meta.totalPages,
perPage: response.meta.perPage
}
}
/**
* Adds a summon to the collection
*/
async addSummon(input: CollectionSummonInput): Promise<CollectionSummon> {
return this.request<CollectionSummon>('/collection/summons', {
method: 'POST',
body: {
collectionSummon: input
}
})
}
/**
* Adds multiple summons to the collection in a single batch request
* Handles quantity expansion - each quantity > 1 creates multiple entries
*/
async addSummons(
inputs: Array<CollectionSummonInput & { quantity?: number }>
): Promise<CollectionSummon[]> {
// Expand inputs based on quantity
const expanded = inputs.flatMap((input) => {
const count = input.quantity ?? 1
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { quantity, ...rest } = input
return Array.from({ length: count }, () => ({ ...rest })) as CollectionSummonInput[]
})
if (expanded.length === 0) return []
const response = await this.request<{
summons: CollectionSummon[]
meta: { created: number; errors: any[] }
}>('/collection/summons/batch', {
method: 'POST',
body: {
collectionSummons: expanded
}
})
return response.summons
}
/**
* Updates a collection summon
*/
async updateSummon(id: string, input: Partial<CollectionSummonInput>): Promise<CollectionSummon> {
return this.request<CollectionSummon>(`/collection/summons/${id}`, {
method: 'PATCH',
body: {
collectionSummon: input
}
})
}
/**
* Removes a summon from the collection
*/
async removeSummon(id: string): Promise<void> {
return this.request<void>(`/collection/summons/${id}`, {
method: 'DELETE'
})
}
// ============================================
// Collection Job Accessories
// ============================================
/**
* Lists the current user's collection job accessories
*/
async listJobAccessories(): Promise<CollectionJobAccessory[]> {
const response = await this.request<{ jobAccessories: CollectionJobAccessory[] }>(
'/collection/job_accessories',
{
method: 'GET'
}
)
return response.jobAccessories
}
/**
* Adds a job accessory to the collection
*/
async addJobAccessory(input: CollectionJobAccessoryInput): Promise<CollectionJobAccessory> {
return this.request<CollectionJobAccessory>('/collection/job_accessories', {
method: 'POST',
body: {
collectionJobAccessory: input
}
})
}
/**
* Removes a job accessory from the collection
*/
async removeJobAccessory(id: string): Promise<void> {
return this.request<void>(`/collection/job_accessories/${id}`, {
method: 'DELETE'
})
}
// ============================================
// Cache Management
// ============================================
/**
* Clears collection-related cache
*/
clearCollectionCache() {
this.clearCache('/collection')
this.clearCache('/users')
}
}
/**
* Default collection adapter instance
*/
export const collectionAdapter = new CollectionAdapter(DEFAULT_ADAPTER_CONFIG)