feat: Implement PartyAdapter with comprehensive functionality
- Add PartyAdapter for party CRUD and grid management - Create reactive PartyResource with optimistic updates - Support user parties listing with filters - Handle grid conflicts and job updates - Include comprehensive test coverage
This commit is contained in:
parent
20c6de3834
commit
114427241f
7 changed files with 1326 additions and 21 deletions
508
src/lib/api/adapters/__tests__/party.adapter.test.ts
Normal file
508
src/lib/api/adapters/__tests__/party.adapter.test.ts
Normal file
|
|
@ -0,0 +1,508 @@
|
||||||
|
/**
|
||||||
|
* Tests for PartyAdapter
|
||||||
|
*
|
||||||
|
* These tests verify party CRUD operations, grid management,
|
||||||
|
* and conflict resolution functionality.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { PartyAdapter } from '../party.adapter'
|
||||||
|
import type { Party, GridWeapon, GridSummon, GridCharacter } from '../party.adapter'
|
||||||
|
|
||||||
|
describe('PartyAdapter', () => {
|
||||||
|
let adapter: PartyAdapter
|
||||||
|
let originalFetch: typeof global.fetch
|
||||||
|
|
||||||
|
const mockParty: Party = {
|
||||||
|
id: '123',
|
||||||
|
shortcode: 'ABC123',
|
||||||
|
name: 'Test Party',
|
||||||
|
description: 'Test description',
|
||||||
|
visibility: 'public',
|
||||||
|
user: {
|
||||||
|
id: 'user-1',
|
||||||
|
username: 'testuser'
|
||||||
|
},
|
||||||
|
job: {
|
||||||
|
id: 'job-1',
|
||||||
|
name: { en: 'Warrior' },
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
id: 'skill-1',
|
||||||
|
name: { en: 'Rage' },
|
||||||
|
slot: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
raid: {
|
||||||
|
id: 'raid-1',
|
||||||
|
name: { en: 'Proto Bahamut' },
|
||||||
|
group: {
|
||||||
|
id: 'group-1',
|
||||||
|
name: { en: 'Tier 1' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
gridWeapons: [],
|
||||||
|
gridSummons: [],
|
||||||
|
gridCharacters: [],
|
||||||
|
createdAt: '2024-01-01T00:00:00Z',
|
||||||
|
updatedAt: '2024-01-01T00:00:00Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalFetch = global.fetch
|
||||||
|
adapter = new PartyAdapter({ baseURL: 'https://api.example.com' })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
global.fetch = originalFetch
|
||||||
|
vi.clearAllTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('CRUD operations', () => {
|
||||||
|
it('should create a new party', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockParty
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.create({
|
||||||
|
name: 'Test Party',
|
||||||
|
description: 'Test description',
|
||||||
|
visibility: 'public'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(mockParty)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/parties',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
party: {
|
||||||
|
name: 'Test Party',
|
||||||
|
description: 'Test description',
|
||||||
|
visibility: 'public'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get a party by shortcode', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockParty
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.getByShortcode('ABC123')
|
||||||
|
|
||||||
|
expect(result).toEqual(mockParty)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/parties/ABC123',
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update a party', async () => {
|
||||||
|
const updatedParty = { ...mockParty, name: 'Updated Party' }
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => updatedParty
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.update({
|
||||||
|
shortcode: 'ABC123',
|
||||||
|
name: 'Updated Party'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(updatedParty)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/parties/ABC123',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
|
party: { name: 'Updated Party' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete a party', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({})
|
||||||
|
})
|
||||||
|
|
||||||
|
await adapter.delete('ABC123')
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/parties/ABC123',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remix a party', async () => {
|
||||||
|
const remixedParty = { ...mockParty, id: '456', shortcode: 'DEF456' }
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => remixedParty
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.remix('ABC123')
|
||||||
|
|
||||||
|
expect(result).toEqual(remixedParty)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/parties/ABC123/remix',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user parties listing', () => {
|
||||||
|
it('should list user parties with filters', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
results: [mockParty],
|
||||||
|
total: 1,
|
||||||
|
page: 1,
|
||||||
|
totalPages: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockResponse
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.listUserParties({
|
||||||
|
username: 'testuser',
|
||||||
|
page: 1,
|
||||||
|
per: 20,
|
||||||
|
visibility: 'public',
|
||||||
|
raidId: 'raid-1'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual(mockResponse)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('/users/testuser/parties'),
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify query parameters were included
|
||||||
|
const callUrl = (global.fetch as any).mock.calls[0][0]
|
||||||
|
expect(callUrl).toContain('page=1')
|
||||||
|
expect(callUrl).toContain('per=20')
|
||||||
|
expect(callUrl).toContain('visibility=public')
|
||||||
|
expect(callUrl).toContain('raid_id=raid-1')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('grid management', () => {
|
||||||
|
it('should update grid weapons', async () => {
|
||||||
|
const mockGridWeapons: GridWeapon[] = [
|
||||||
|
{
|
||||||
|
id: 'gw-1',
|
||||||
|
position: 1,
|
||||||
|
mainhand: true,
|
||||||
|
uncapLevel: 5,
|
||||||
|
transcendenceStage: 0,
|
||||||
|
weaponKeys: [],
|
||||||
|
weapon: {
|
||||||
|
id: 'weapon-1',
|
||||||
|
granblueId: 'w-1',
|
||||||
|
name: { en: 'Sword' },
|
||||||
|
element: 1,
|
||||||
|
rarity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
grid_weapons: mockGridWeapons.map(gw => ({
|
||||||
|
...gw,
|
||||||
|
uncap_level: gw.uncapLevel,
|
||||||
|
transcendence_stage: gw.transcendenceStage,
|
||||||
|
weapon_keys: gw.weaponKeys
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.updateGridWeapons({
|
||||||
|
shortcode: 'ABC123',
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
weaponId: 'weapon-1',
|
||||||
|
mainhand: true,
|
||||||
|
uncapLevel: 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.gridWeapons).toEqual(mockGridWeapons)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/parties/ABC123/grid_weapons',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
|
grid_weapons: [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
weapon_id: 'weapon-1',
|
||||||
|
mainhand: true,
|
||||||
|
uncap_level: 5
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update grid summons', async () => {
|
||||||
|
const mockGridSummons: GridSummon[] = [
|
||||||
|
{
|
||||||
|
id: 'gs-1',
|
||||||
|
position: 1,
|
||||||
|
quickSummon: true,
|
||||||
|
transcendenceStage: 2,
|
||||||
|
summon: {
|
||||||
|
id: 'summon-1',
|
||||||
|
granblueId: 's-1',
|
||||||
|
name: { en: 'Bahamut' },
|
||||||
|
element: 6,
|
||||||
|
rarity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
grid_summons: mockGridSummons.map(gs => ({
|
||||||
|
...gs,
|
||||||
|
quick_summon: gs.quickSummon,
|
||||||
|
transcendence_stage: gs.transcendenceStage
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.updateGridSummons({
|
||||||
|
shortcode: 'ABC123',
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
summonId: 'summon-1',
|
||||||
|
quickSummon: true,
|
||||||
|
transcendenceStage: 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.gridSummons).toEqual(mockGridSummons)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update grid characters', async () => {
|
||||||
|
const mockGridCharacters: GridCharacter[] = [
|
||||||
|
{
|
||||||
|
id: 'gc-1',
|
||||||
|
position: 1,
|
||||||
|
uncapLevel: 5,
|
||||||
|
transcendenceStage: 1,
|
||||||
|
character: {
|
||||||
|
id: 'char-1',
|
||||||
|
granblueId: 'c-1',
|
||||||
|
name: { en: 'Katalina' },
|
||||||
|
element: 2,
|
||||||
|
rarity: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
grid_characters: mockGridCharacters.map(gc => ({
|
||||||
|
...gc,
|
||||||
|
uncap_level: gc.uncapLevel,
|
||||||
|
transcendence_stage: gc.transcendenceStage
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.updateGridCharacters({
|
||||||
|
shortcode: 'ABC123',
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
characterId: 'char-1',
|
||||||
|
uncapLevel: 5,
|
||||||
|
transcendenceStage: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.gridCharacters).toEqual(mockGridCharacters)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle grid conflicts', async () => {
|
||||||
|
const conflictResponse = {
|
||||||
|
grid_weapons: [],
|
||||||
|
conflicts: {
|
||||||
|
conflicts: [
|
||||||
|
{
|
||||||
|
type: 'weapon',
|
||||||
|
position: 1,
|
||||||
|
existing: { id: 'weapon-1' },
|
||||||
|
new: { id: 'weapon-2' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
resolved: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => conflictResponse
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.updateGridWeapons({
|
||||||
|
shortcode: 'ABC123',
|
||||||
|
updates: [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
weaponId: 'weapon-2'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result.conflicts).toBeDefined()
|
||||||
|
expect(result.conflicts?.resolved).toBe(false)
|
||||||
|
expect(result.conflicts?.conflicts).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('job management', () => {
|
||||||
|
it('should update party job', async () => {
|
||||||
|
const updatedParty = {
|
||||||
|
...mockParty,
|
||||||
|
job: {
|
||||||
|
id: 'job-2',
|
||||||
|
name: { en: 'Mage' },
|
||||||
|
skills: [
|
||||||
|
{
|
||||||
|
id: 'skill-2',
|
||||||
|
name: { en: 'Fireball' },
|
||||||
|
slot: 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
accessory: {
|
||||||
|
id: 'acc-1',
|
||||||
|
name: { en: 'Magic Ring' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => updatedParty
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await adapter.updateJob(
|
||||||
|
'ABC123',
|
||||||
|
'job-2',
|
||||||
|
[{ id: 'skill-2', slot: 1 }],
|
||||||
|
'acc-1'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result).toEqual(updatedParty)
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith(
|
||||||
|
'https://api.example.com/parties/ABC123',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
|
party: {
|
||||||
|
job_id: 'job-2',
|
||||||
|
job_skills_attributes: [{ id: 'skill-2', slot: 1 }],
|
||||||
|
job_accessory_id: 'acc-1'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('cache management', () => {
|
||||||
|
it('should cache party retrieval', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockParty
|
||||||
|
})
|
||||||
|
|
||||||
|
// First call
|
||||||
|
await adapter.getByShortcode('ABC123')
|
||||||
|
|
||||||
|
// Second call (should use cache)
|
||||||
|
await adapter.getByShortcode('ABC123')
|
||||||
|
|
||||||
|
// Should only call fetch once due to caching
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should clear party cache', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockParty
|
||||||
|
})
|
||||||
|
|
||||||
|
// First call
|
||||||
|
await adapter.getByShortcode('ABC123')
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
adapter.clearPartyCache('ABC123')
|
||||||
|
|
||||||
|
// Second call (should not use cache)
|
||||||
|
await adapter.getByShortcode('ABC123')
|
||||||
|
|
||||||
|
// Should call fetch twice since cache was cleared
|
||||||
|
expect(global.fetch).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('should handle 404 errors', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 404,
|
||||||
|
statusText: 'Not Found',
|
||||||
|
json: async () => ({ error: 'Party not found' })
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(adapter.getByShortcode('INVALID')).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle validation errors', async () => {
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 422,
|
||||||
|
statusText: 'Unprocessable Entity',
|
||||||
|
json: async () => ({
|
||||||
|
errors: {
|
||||||
|
name: ['is too long']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
adapter.create({
|
||||||
|
name: 'A'.repeat(256)
|
||||||
|
})
|
||||||
|
).rejects.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -87,14 +87,14 @@ export abstract class BaseAdapter {
|
||||||
path: string,
|
path: string,
|
||||||
options: RequestOptions = {}
|
options: RequestOptions = {}
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
// Build the full URL with query parameters
|
// Build the full URL with query parameters (support both params and query)
|
||||||
const url = this.buildURL(path, options.params)
|
const url = this.buildURL(path, options.query || options.params)
|
||||||
|
|
||||||
// Generate a unique ID for this request (used for cancellation and caching)
|
// Generate a unique ID for this request (used for cancellation and caching)
|
||||||
const requestId = this.generateRequestId(path, options.method, options.body as string)
|
const requestId = this.generateRequestId(path, options.method, options.body as string)
|
||||||
|
|
||||||
// Check cache first if caching is enabled
|
// Check cache first if caching is enabled (support both cache and cacheTTL)
|
||||||
const cacheTime = options.cache ?? this.options.cacheTime
|
const cacheTime = options.cacheTTL ?? options.cache ?? this.options.cacheTime
|
||||||
// Allow caching for any method if explicitly set
|
// Allow caching for any method if explicitly set
|
||||||
if (cacheTime > 0) {
|
if (cacheTime > 0) {
|
||||||
const cached = this.getFromCache(requestId)
|
const cached = this.getFromCache(requestId)
|
||||||
|
|
@ -122,7 +122,11 @@ export abstract class BaseAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform request body from camelCase to snake_case if present
|
// Transform request body from camelCase to snake_case if present
|
||||||
if (options.body && typeof options.body === 'string') {
|
if (options.body) {
|
||||||
|
if (typeof options.body === 'object') {
|
||||||
|
// Body is an object, transform and stringify
|
||||||
|
fetchOptions.body = JSON.stringify(this.transformRequest(options.body))
|
||||||
|
} else if (typeof options.body === 'string') {
|
||||||
try {
|
try {
|
||||||
const bodyData = JSON.parse(options.body)
|
const bodyData = JSON.parse(options.body)
|
||||||
fetchOptions.body = JSON.stringify(this.transformRequest(bodyData))
|
fetchOptions.body = JSON.stringify(this.transformRequest(bodyData))
|
||||||
|
|
@ -131,6 +135,7 @@ export abstract class BaseAdapter {
|
||||||
fetchOptions.body = options.body
|
fetchOptions.body = options.body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Make the request with retry logic (errors handled inside fetchWithRetry)
|
// Make the request with retry logic (errors handled inside fetchWithRetry)
|
||||||
|
|
@ -140,7 +145,7 @@ export abstract class BaseAdapter {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
const transformed = this.transformResponse<T>(data)
|
const transformed = this.transformResponse<T>(data)
|
||||||
|
|
||||||
// Cache the successful response if caching is enabled
|
// Cache the successful response if caching is enabled (use cacheTTL or cache)
|
||||||
if (cacheTime > 0) {
|
if (cacheTime > 0) {
|
||||||
this.setCache(requestId, transformed, cacheTime)
|
this.setCache(requestId, transformed, cacheTime)
|
||||||
}
|
}
|
||||||
|
|
@ -348,7 +353,10 @@ export abstract class BaseAdapter {
|
||||||
private addQueryParams(url: URL, params?: Record<string, any>): void {
|
private addQueryParams(url: URL, params?: Record<string, any>): void {
|
||||||
if (!params) return
|
if (!params) return
|
||||||
|
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
// Transform query parameters from camelCase to snake_case
|
||||||
|
const transformed = this.transformRequest(params)
|
||||||
|
|
||||||
|
Object.entries(transformed).forEach(([key, value]) => {
|
||||||
// Skip undefined and null values
|
// Skip undefined and null values
|
||||||
if (value === undefined || value === null) return
|
if (value === undefined || value === null) return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,25 @@ export * from './errors'
|
||||||
// Resource-specific adapters
|
// Resource-specific adapters
|
||||||
export { SearchAdapter, searchAdapter } from './search.adapter'
|
export { SearchAdapter, searchAdapter } from './search.adapter'
|
||||||
export type { SearchParams, SearchResult, SearchResponse } from './search.adapter'
|
export type { SearchParams, SearchResult, SearchResponse } from './search.adapter'
|
||||||
// export { PartyAdapter } from './party.adapter'
|
|
||||||
|
export { PartyAdapter, partyAdapter } from './party.adapter'
|
||||||
|
export type {
|
||||||
|
Party,
|
||||||
|
GridWeapon,
|
||||||
|
GridSummon,
|
||||||
|
GridCharacter,
|
||||||
|
CreatePartyParams,
|
||||||
|
UpdatePartyParams,
|
||||||
|
ListUserPartiesParams,
|
||||||
|
UpdateGridParams,
|
||||||
|
GridWeaponUpdate,
|
||||||
|
GridSummonUpdate,
|
||||||
|
GridCharacterUpdate,
|
||||||
|
ConflictResolution
|
||||||
|
} from './party.adapter'
|
||||||
|
|
||||||
// export { GridAdapter } from './grid.adapter'
|
// export { GridAdapter } from './grid.adapter'
|
||||||
|
// export { EntityAdapter } from './entity.adapter'
|
||||||
|
|
||||||
// Reactive resources using Svelte 5 runes
|
// Reactive resources using Svelte 5 runes
|
||||||
export * from './resources'
|
export * from './resources'
|
||||||
381
src/lib/api/adapters/party.adapter.ts
Normal file
381
src/lib/api/adapters/party.adapter.ts
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
/**
|
||||||
|
* Party Adapter
|
||||||
|
*
|
||||||
|
* Handles all party-related API operations including CRUD, grids, and remixing.
|
||||||
|
* Provides a clean interface for party management with automatic
|
||||||
|
* request handling, caching, and error management.
|
||||||
|
*
|
||||||
|
* @module adapters/party
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseAdapter } from './base.adapter'
|
||||||
|
import type { RequestOptions, AdapterOptions, PaginatedResponse } from './types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gridWeapons: GridWeapon[]
|
||||||
|
gridSummons: GridSummon[]
|
||||||
|
gridCharacters: GridCharacter[]
|
||||||
|
guidebook?: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
extras?: Record<string, any>
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid weapon structure
|
||||||
|
*/
|
||||||
|
export interface GridWeapon {
|
||||||
|
id: string
|
||||||
|
position: number
|
||||||
|
mainhand: boolean
|
||||||
|
uncapLevel: number
|
||||||
|
transcendenceStage: number
|
||||||
|
weaponKeys: Array<{
|
||||||
|
id: string
|
||||||
|
slot: number
|
||||||
|
}>
|
||||||
|
weapon: {
|
||||||
|
id: string
|
||||||
|
granblueId: string
|
||||||
|
name: Record<string, string>
|
||||||
|
element: number
|
||||||
|
rarity: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid summon structure
|
||||||
|
*/
|
||||||
|
export interface GridSummon {
|
||||||
|
id: string
|
||||||
|
position: number
|
||||||
|
quickSummon: boolean
|
||||||
|
transcendenceStage: number
|
||||||
|
summon: {
|
||||||
|
id: string
|
||||||
|
granblueId: string
|
||||||
|
name: Record<string, string>
|
||||||
|
element: number
|
||||||
|
rarity: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid character structure
|
||||||
|
*/
|
||||||
|
export interface GridCharacter {
|
||||||
|
id: string
|
||||||
|
position: number
|
||||||
|
uncapLevel: number
|
||||||
|
transcendenceStage: number
|
||||||
|
perpetualModifiers?: Record<string, any>
|
||||||
|
awakenings?: Array<{
|
||||||
|
id: string
|
||||||
|
level: number
|
||||||
|
}>
|
||||||
|
character: {
|
||||||
|
id: string
|
||||||
|
granblueId: string
|
||||||
|
name: Record<string, string>
|
||||||
|
element: number
|
||||||
|
rarity: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for creating a new party
|
||||||
|
*/
|
||||||
|
export interface CreatePartyParams {
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
visibility?: 'public' | 'private' | 'unlisted'
|
||||||
|
jobId?: string
|
||||||
|
raidId?: string
|
||||||
|
guidebookId?: string
|
||||||
|
extras?: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for updating a party
|
||||||
|
*/
|
||||||
|
export interface UpdatePartyParams extends CreatePartyParams {
|
||||||
|
shortcode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for listing user parties
|
||||||
|
*/
|
||||||
|
export interface ListUserPartiesParams {
|
||||||
|
username: string
|
||||||
|
page?: number
|
||||||
|
per?: number
|
||||||
|
visibility?: 'public' | 'private' | 'unlisted' | 'all'
|
||||||
|
raidId?: string
|
||||||
|
characterId?: string
|
||||||
|
weaponId?: string
|
||||||
|
summonId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for updating grid items
|
||||||
|
*/
|
||||||
|
export interface UpdateGridParams<T> {
|
||||||
|
shortcode: string
|
||||||
|
updates: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid weapon update structure
|
||||||
|
*/
|
||||||
|
export interface GridWeaponUpdate {
|
||||||
|
id?: string
|
||||||
|
position: number
|
||||||
|
weaponId: string
|
||||||
|
mainhand?: boolean
|
||||||
|
uncapLevel?: number
|
||||||
|
transcendenceStage?: number
|
||||||
|
weaponKeys?: Array<{
|
||||||
|
id: string
|
||||||
|
slot: number
|
||||||
|
}>
|
||||||
|
_destroy?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid summon update structure
|
||||||
|
*/
|
||||||
|
export interface GridSummonUpdate {
|
||||||
|
id?: string
|
||||||
|
position: number
|
||||||
|
summonId: string
|
||||||
|
quickSummon?: boolean
|
||||||
|
transcendenceStage?: number
|
||||||
|
_destroy?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grid character update structure
|
||||||
|
*/
|
||||||
|
export interface GridCharacterUpdate {
|
||||||
|
id?: string
|
||||||
|
position: number
|
||||||
|
characterId: string
|
||||||
|
uncapLevel?: number
|
||||||
|
transcendenceStage?: number
|
||||||
|
perpetualModifiers?: Record<string, any>
|
||||||
|
awakenings?: Array<{
|
||||||
|
id: string
|
||||||
|
level: number
|
||||||
|
}>
|
||||||
|
_destroy?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conflict resolution result
|
||||||
|
*/
|
||||||
|
export interface ConflictResolution {
|
||||||
|
conflicts: Array<{
|
||||||
|
type: 'weapon' | 'summon' | 'character'
|
||||||
|
position: number
|
||||||
|
existing: any
|
||||||
|
new: any
|
||||||
|
}>
|
||||||
|
resolved: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Party adapter for managing parties and their grids
|
||||||
|
*/
|
||||||
|
export class PartyAdapter extends BaseAdapter {
|
||||||
|
constructor(options?: AdapterOptions) {
|
||||||
|
super({
|
||||||
|
...options,
|
||||||
|
baseURL: options?.baseURL || '/api/v1'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new party
|
||||||
|
*/
|
||||||
|
async create(params: CreatePartyParams): Promise<Party> {
|
||||||
|
return this.request<Party>('/parties', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
party: params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a party by shortcode
|
||||||
|
*/
|
||||||
|
async getByShortcode(shortcode: string): Promise<Party> {
|
||||||
|
return this.request<Party>(`/parties/${shortcode}`, {
|
||||||
|
cacheTTL: 60000 // Cache for 1 minute
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a party
|
||||||
|
*/
|
||||||
|
async update(params: UpdatePartyParams): Promise<Party> {
|
||||||
|
const { shortcode, ...updateParams } = params
|
||||||
|
return this.request<Party>(`/parties/${shortcode}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
party: updateParams
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a party
|
||||||
|
*/
|
||||||
|
async delete(shortcode: string): Promise<void> {
|
||||||
|
return this.request<void>(`/parties/${shortcode}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a remix (copy) of an existing party
|
||||||
|
*/
|
||||||
|
async remix(shortcode: string): Promise<Party> {
|
||||||
|
return this.request<Party>(`/parties/${shortcode}/remix`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists parties for a specific user
|
||||||
|
*/
|
||||||
|
async listUserParties(params: ListUserPartiesParams): Promise<PaginatedResponse<Party>> {
|
||||||
|
const { username, ...queryParams } = params
|
||||||
|
return this.request<PaginatedResponse<Party>>(`/users/${username}/parties`, {
|
||||||
|
method: 'GET',
|
||||||
|
query: queryParams,
|
||||||
|
cacheTTL: 30000 // Cache for 30 seconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates grid weapons for a party
|
||||||
|
*/
|
||||||
|
async updateGridWeapons(
|
||||||
|
params: UpdateGridParams<GridWeaponUpdate>
|
||||||
|
): Promise<{ gridWeapons: GridWeapon[]; conflicts?: ConflictResolution }> {
|
||||||
|
const { shortcode, updates } = params
|
||||||
|
return this.request(`/parties/${shortcode}/grid_weapons`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
grid_weapons: updates
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates grid summons for a party
|
||||||
|
*/
|
||||||
|
async updateGridSummons(
|
||||||
|
params: UpdateGridParams<GridSummonUpdate>
|
||||||
|
): Promise<{ gridSummons: GridSummon[]; conflicts?: ConflictResolution }> {
|
||||||
|
const { shortcode, updates } = params
|
||||||
|
return this.request(`/parties/${shortcode}/grid_summons`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
grid_summons: updates
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates grid characters for a party
|
||||||
|
*/
|
||||||
|
async updateGridCharacters(
|
||||||
|
params: UpdateGridParams<GridCharacterUpdate>
|
||||||
|
): Promise<{ gridCharacters: GridCharacter[]; conflicts?: ConflictResolution }> {
|
||||||
|
const { shortcode, updates } = params
|
||||||
|
return this.request(`/parties/${shortcode}/grid_characters`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
grid_characters: updates
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the job for a party
|
||||||
|
*/
|
||||||
|
async updateJob(
|
||||||
|
shortcode: string,
|
||||||
|
jobId: string,
|
||||||
|
skills?: Array<{ id: string; slot: number }>,
|
||||||
|
accessoryId?: string
|
||||||
|
): Promise<Party> {
|
||||||
|
return this.request<Party>(`/parties/${shortcode}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
party: {
|
||||||
|
job_id: jobId,
|
||||||
|
...(skills && { job_skills_attributes: skills }),
|
||||||
|
...(accessoryId && { job_accessory_id: accessoryId })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the cache for party-related data
|
||||||
|
*/
|
||||||
|
clearPartyCache(shortcode?: string) {
|
||||||
|
if (shortcode) {
|
||||||
|
// Clear specific party cache
|
||||||
|
this.clearCache(`/parties/${shortcode}`)
|
||||||
|
} else {
|
||||||
|
// Clear all party and user caches
|
||||||
|
this.clearCache('/parties')
|
||||||
|
this.clearCache('/users')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default party adapter instance
|
||||||
|
*/
|
||||||
|
export const partyAdapter = new PartyAdapter()
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
export { SearchResource, createSearchResource } from './search.resource.svelte'
|
export { SearchResource, createSearchResource } from './search.resource.svelte'
|
||||||
export type { SearchResourceOptions } from './search.resource.svelte'
|
export type { SearchResourceOptions } from './search.resource.svelte'
|
||||||
|
|
||||||
|
export { PartyResource, createPartyResource } from './party.resource.svelte'
|
||||||
|
export type { PartyResourceOptions } from './party.resource.svelte'
|
||||||
|
|
||||||
// Future resources will be added here
|
// Future resources will be added here
|
||||||
// export { PartyResource, createPartyResource } from './party.resource.svelte'
|
|
||||||
// export { GridResource, createGridResource } from './grid.resource.svelte'
|
// export { GridResource, createGridResource } from './grid.resource.svelte'
|
||||||
|
// export { EntityResource, createEntityResource } from './entity.resource.svelte'
|
||||||
379
src/lib/api/adapters/resources/party.resource.svelte.ts
Normal file
379
src/lib/api/adapters/resources/party.resource.svelte.ts
Normal file
|
|
@ -0,0 +1,379 @@
|
||||||
|
/**
|
||||||
|
* Reactive Party Resource using Svelte 5 Runes
|
||||||
|
*
|
||||||
|
* Provides reactive state management for party operations with
|
||||||
|
* automatic loading states, error handling, and optimistic updates.
|
||||||
|
*
|
||||||
|
* @module adapters/resources/party
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PartyAdapter, type Party, type CreatePartyParams, type UpdatePartyParams } from '../party.adapter'
|
||||||
|
import type { AdapterError } from '../types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Party resource configuration options
|
||||||
|
*/
|
||||||
|
export interface PartyResourceOptions {
|
||||||
|
/** Party adapter instance to use */
|
||||||
|
adapter?: PartyAdapter
|
||||||
|
/** Enable optimistic updates for mutations */
|
||||||
|
optimistic?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource state for a single party
|
||||||
|
*/
|
||||||
|
interface PartyState {
|
||||||
|
data?: Party
|
||||||
|
loading: boolean
|
||||||
|
error?: AdapterError
|
||||||
|
updating?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource state for party lists
|
||||||
|
*/
|
||||||
|
interface PartyListState {
|
||||||
|
parties: Party[]
|
||||||
|
total?: number
|
||||||
|
page?: number
|
||||||
|
totalPages?: number
|
||||||
|
loading: boolean
|
||||||
|
error?: AdapterError
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reactive party resource for managing parties
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```svelte
|
||||||
|
* <script>
|
||||||
|
* import { createPartyResource } from '$lib/api/adapters/resources'
|
||||||
|
*
|
||||||
|
* const party = createPartyResource()
|
||||||
|
*
|
||||||
|
* // Load a party
|
||||||
|
* party.load('ABC123')
|
||||||
|
*
|
||||||
|
* // Update party details
|
||||||
|
* party.update({
|
||||||
|
* shortcode: 'ABC123',
|
||||||
|
* name: 'New Name'
|
||||||
|
* })
|
||||||
|
* </script>
|
||||||
|
*
|
||||||
|
* {#if party.current.loading}
|
||||||
|
* <p>Loading party...</p>
|
||||||
|
* {:else if party.current.error}
|
||||||
|
* <p>Error: {party.current.error.message}</p>
|
||||||
|
* {:else if party.current.data}
|
||||||
|
* <h1>{party.current.data.name}</h1>
|
||||||
|
* {/if}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class PartyResource {
|
||||||
|
private adapter: PartyAdapter
|
||||||
|
private optimistic: boolean
|
||||||
|
|
||||||
|
// Reactive state for current party
|
||||||
|
current = $state<PartyState>({ loading: false })
|
||||||
|
|
||||||
|
// Reactive state for user parties list
|
||||||
|
userParties = $state<PartyListState>({
|
||||||
|
parties: [],
|
||||||
|
loading: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Track active requests for cancellation
|
||||||
|
private activeRequests = new Map<string, AbortController>()
|
||||||
|
|
||||||
|
constructor(options: PartyResourceOptions = {}) {
|
||||||
|
this.adapter = options.adapter || new PartyAdapter()
|
||||||
|
this.optimistic = options.optimistic ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a party by shortcode
|
||||||
|
*/
|
||||||
|
async load(shortcode: string): Promise<Party | undefined> {
|
||||||
|
// Cancel any existing load request
|
||||||
|
this.cancelRequest('load')
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
this.activeRequests.set('load', controller)
|
||||||
|
|
||||||
|
this.current = { ...this.current, loading: true, error: undefined }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await this.adapter.getByShortcode(shortcode)
|
||||||
|
this.current = { data: party, loading: false }
|
||||||
|
return party
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code !== 'CANCELLED') {
|
||||||
|
this.current = {
|
||||||
|
...this.current,
|
||||||
|
loading: false,
|
||||||
|
error: error as AdapterError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.activeRequests.delete('load')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new party
|
||||||
|
*/
|
||||||
|
async create(params: CreatePartyParams): Promise<Party | undefined> {
|
||||||
|
this.current = { ...this.current, updating: true, error: undefined }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await this.adapter.create(params)
|
||||||
|
this.current = { data: party, loading: false, updating: false }
|
||||||
|
|
||||||
|
// Add to user parties if loaded
|
||||||
|
if (this.userParties.parties.length > 0) {
|
||||||
|
this.userParties.parties = [party, ...this.userParties.parties]
|
||||||
|
if (this.userParties.total !== undefined) {
|
||||||
|
this.userParties.total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return party
|
||||||
|
} catch (error: any) {
|
||||||
|
this.current = {
|
||||||
|
...this.current,
|
||||||
|
updating: false,
|
||||||
|
error: error as AdapterError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the current party
|
||||||
|
*/
|
||||||
|
async update(params: UpdatePartyParams): Promise<Party | undefined> {
|
||||||
|
// Optimistic update
|
||||||
|
if (this.optimistic && this.current.data) {
|
||||||
|
const optimisticData = {
|
||||||
|
...this.current.data,
|
||||||
|
...params,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
this.current = {
|
||||||
|
...this.current,
|
||||||
|
data: optimisticData as Party,
|
||||||
|
updating: true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.current = { ...this.current, updating: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await this.adapter.update(params)
|
||||||
|
this.current = { data: party, loading: false, updating: false }
|
||||||
|
|
||||||
|
// Update in user parties list if present
|
||||||
|
const index = this.userParties.parties.findIndex(
|
||||||
|
p => p.shortcode === params.shortcode
|
||||||
|
)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.userParties.parties[index] = party
|
||||||
|
}
|
||||||
|
|
||||||
|
return party
|
||||||
|
} catch (error: any) {
|
||||||
|
// Revert optimistic update on error
|
||||||
|
if (this.optimistic) {
|
||||||
|
await this.load(params.shortcode)
|
||||||
|
}
|
||||||
|
this.current = {
|
||||||
|
...this.current,
|
||||||
|
updating: false,
|
||||||
|
error: error as AdapterError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the current party
|
||||||
|
*/
|
||||||
|
async delete(shortcode: string): Promise<boolean> {
|
||||||
|
this.current = { ...this.current, updating: true, error: undefined }
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.adapter.delete(shortcode)
|
||||||
|
|
||||||
|
// Clear current party
|
||||||
|
this.current = { loading: false, updating: false }
|
||||||
|
|
||||||
|
// Remove from user parties list
|
||||||
|
this.userParties.parties = this.userParties.parties.filter(
|
||||||
|
p => p.shortcode !== shortcode
|
||||||
|
)
|
||||||
|
if (this.userParties.total !== undefined && this.userParties.total > 0) {
|
||||||
|
this.userParties.total--
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error: any) {
|
||||||
|
this.current = {
|
||||||
|
...this.current,
|
||||||
|
updating: false,
|
||||||
|
error: error as AdapterError
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a remix (copy) of a party
|
||||||
|
*/
|
||||||
|
async remix(shortcode: string): Promise<Party | undefined> {
|
||||||
|
this.current = { ...this.current, updating: true, error: undefined }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await this.adapter.remix(shortcode)
|
||||||
|
this.current = { data: party, loading: false, updating: false }
|
||||||
|
|
||||||
|
// Add to user parties if it's the current user's remix
|
||||||
|
if (this.userParties.parties.length > 0) {
|
||||||
|
this.userParties.parties = [party, ...this.userParties.parties]
|
||||||
|
if (this.userParties.total !== undefined) {
|
||||||
|
this.userParties.total++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return party
|
||||||
|
} catch (error: any) {
|
||||||
|
this.current = {
|
||||||
|
...this.current,
|
||||||
|
updating: false,
|
||||||
|
error: error as AdapterError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads parties for a specific user
|
||||||
|
*/
|
||||||
|
async loadUserParties(
|
||||||
|
username: string,
|
||||||
|
params: Omit<Parameters<PartyAdapter['listUserParties']>[0], 'username'> = {}
|
||||||
|
): Promise<void> {
|
||||||
|
// Cancel any existing user parties request
|
||||||
|
this.cancelRequest('userParties')
|
||||||
|
|
||||||
|
const controller = new AbortController()
|
||||||
|
this.activeRequests.set('userParties', controller)
|
||||||
|
|
||||||
|
this.userParties = { ...this.userParties, loading: true, error: undefined }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.adapter.listUserParties({
|
||||||
|
username,
|
||||||
|
...params
|
||||||
|
})
|
||||||
|
|
||||||
|
this.userParties = {
|
||||||
|
parties: response.results,
|
||||||
|
total: response.total,
|
||||||
|
page: response.page,
|
||||||
|
totalPages: response.totalPages,
|
||||||
|
loading: false
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code !== 'CANCELLED') {
|
||||||
|
this.userParties = {
|
||||||
|
...this.userParties,
|
||||||
|
loading: false,
|
||||||
|
error: error as AdapterError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.activeRequests.delete('userParties')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the job for the current party
|
||||||
|
*/
|
||||||
|
async updateJob(
|
||||||
|
shortcode: string,
|
||||||
|
jobId: string,
|
||||||
|
skills?: Array<{ id: string; slot: number }>,
|
||||||
|
accessoryId?: string
|
||||||
|
): Promise<Party | undefined> {
|
||||||
|
this.current = { ...this.current, updating: true, error: undefined }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const party = await this.adapter.updateJob(shortcode, jobId, skills, accessoryId)
|
||||||
|
this.current = { data: party, loading: false, updating: false }
|
||||||
|
return party
|
||||||
|
} catch (error: any) {
|
||||||
|
this.current = {
|
||||||
|
...this.current,
|
||||||
|
updating: false,
|
||||||
|
error: error as AdapterError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels an active request
|
||||||
|
*/
|
||||||
|
private cancelRequest(key: string) {
|
||||||
|
const controller = this.activeRequests.get(key)
|
||||||
|
if (controller) {
|
||||||
|
controller.abort()
|
||||||
|
this.activeRequests.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels all active requests
|
||||||
|
*/
|
||||||
|
cancelAll() {
|
||||||
|
this.activeRequests.forEach(controller => controller.abort())
|
||||||
|
this.activeRequests.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the current party state
|
||||||
|
*/
|
||||||
|
clearCurrent() {
|
||||||
|
this.cancelRequest('load')
|
||||||
|
this.current = { loading: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the user parties state
|
||||||
|
*/
|
||||||
|
clearUserParties() {
|
||||||
|
this.cancelRequest('userParties')
|
||||||
|
this.userParties = { parties: [], loading: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all states
|
||||||
|
*/
|
||||||
|
clearAll() {
|
||||||
|
this.cancelAll()
|
||||||
|
this.current = { loading: false }
|
||||||
|
this.userParties = { parties: [], loading: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the adapter's cache
|
||||||
|
*/
|
||||||
|
clearCache(shortcode?: string) {
|
||||||
|
this.adapter.clearPartyCache(shortcode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function for creating party resources
|
||||||
|
*/
|
||||||
|
export function createPartyResource(options?: PartyResourceOptions): PartyResource {
|
||||||
|
return new PartyResource(options)
|
||||||
|
}
|
||||||
|
|
@ -36,15 +36,21 @@ export interface RequestOptions extends Omit<RequestInit, 'body'> {
|
||||||
/** Query parameters to append to the URL */
|
/** Query parameters to append to the URL */
|
||||||
params?: Record<string, any>
|
params?: Record<string, any>
|
||||||
|
|
||||||
|
/** Alternative alias for query parameters */
|
||||||
|
query?: Record<string, any>
|
||||||
|
|
||||||
/** Request timeout in milliseconds. Overrides the adapter's default timeout */
|
/** Request timeout in milliseconds. Overrides the adapter's default timeout */
|
||||||
timeout?: number
|
timeout?: number
|
||||||
|
|
||||||
/** Number of retry attempts for this specific request */
|
/** Number of retry attempts for this specific request */
|
||||||
retries?: number
|
retries?: number
|
||||||
|
|
||||||
/** Cache duration for this request in milliseconds. Only applies to GET requests */
|
/** Cache duration for this request in milliseconds */
|
||||||
cache?: number
|
cache?: number
|
||||||
|
|
||||||
|
/** Alternative alias for cache duration */
|
||||||
|
cacheTTL?: number
|
||||||
|
|
||||||
/** Request body. Can be any serializable value */
|
/** Request body. Can be any serializable value */
|
||||||
body?: any
|
body?: any
|
||||||
}
|
}
|
||||||
|
|
@ -75,8 +81,11 @@ export interface AdapterError {
|
||||||
* Used for endpoints that return paginated data
|
* Used for endpoints that return paginated data
|
||||||
*/
|
*/
|
||||||
export interface PaginatedResponse<T> {
|
export interface PaginatedResponse<T> {
|
||||||
/** Array of items for the current page */
|
/** Array of items for the current page (can be 'results' or 'items') */
|
||||||
items: T[]
|
results: T[]
|
||||||
|
|
||||||
|
/** Alternative key for items */
|
||||||
|
items?: T[]
|
||||||
|
|
||||||
/** Total number of items across all pages */
|
/** Total number of items across all pages */
|
||||||
total: number
|
total: number
|
||||||
|
|
@ -88,10 +97,10 @@ export interface PaginatedResponse<T> {
|
||||||
totalPages: number
|
totalPages: number
|
||||||
|
|
||||||
/** Number of items per page */
|
/** Number of items per page */
|
||||||
perPage: number
|
perPage?: number
|
||||||
|
|
||||||
/** Whether there are more pages available */
|
/** Whether there are more pages available */
|
||||||
hasMore: boolean
|
hasMore?: boolean
|
||||||
|
|
||||||
/** Cursor or page number for the next page, if available */
|
/** Cursor or page number for the next page, if available */
|
||||||
nextCursor?: string | number
|
nextCursor?: string | number
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue