feat: Create UserAdapter and complete Phase 1 & 2 of migration

- Created new UserAdapter for user profile and favorites operations
- Updated routes/teams/explore and routes/[username] to use adapters directly
- Deleted resource facade files (parties.ts, grid.ts, users.ts)
- All services and initial routes now use adapters without backward compatibility
- Updated migration plan to track completed work
This commit is contained in:
Justin Edmund 2025-09-20 00:42:13 -07:00
parent 2605a539b6
commit 683c28e172
7 changed files with 164 additions and 504 deletions

View file

@ -51,5 +51,12 @@ export type {
Summon
} from './entity.adapter'
export { UserAdapter, userAdapter } from './user.adapter'
export type {
UserInfo,
UserProfile,
UserProfileResponse
} from './user.adapter'
// Reactive resources using Svelte 5 runes
export * from './resources'

View file

@ -0,0 +1,131 @@
import { BaseAdapter } from './base.adapter'
import type { Party } from '$lib/types/api/party'
export interface UserInfo {
id: string
username: string
language: string
private: boolean
gender: number
theme: string
role: number
avatar: {
picture: string
element: string
}
}
export interface UserProfile extends UserInfo {
parties?: Party[]
}
export interface UserProfileResponse {
user: UserProfile
items: Party[]
page: number
total?: number
totalPages?: number
perPage?: number
}
/**
* Adapter for user-related API operations
*/
export class UserAdapter extends BaseAdapter {
/**
* Get user information
*/
async getInfo(username: string): Promise<UserInfo> {
return this.request<UserInfo>(`/users/info/${encodeURIComponent(username)}`)
}
/**
* Get user profile with their parties
*/
async getProfile(username: string, page = 1): Promise<UserProfileResponse> {
const params = page > 1 ? { page } : undefined
const response = await this.request<{
profile: UserProfile
meta?: { count?: number; total_pages?: number; per_page?: number }
}>(`/users/${encodeURIComponent(username)}`, { params })
const items = Array.isArray(response.profile?.parties) ? response.profile.parties : []
return {
user: response.profile,
items,
page,
total: response.meta?.count,
totalPages: response.meta?.total_pages,
perPage: response.meta?.per_page
}
}
/**
* Get user's favorite parties
*/
async getFavorites(options: { page?: number } = {}): Promise<{
items: Party[]
page: number
total: number
totalPages: number
perPage: number
}> {
const { page = 1 } = options
const params = page > 1 ? { page } : undefined
const response = await this.request<{
results: Party[]
total: number
total_pages: number
per?: number
}>('/parties/favorites', { params })
return {
items: response.results,
page,
total: response.total,
totalPages: response.total_pages,
perPage: response.per || 20
}
}
/**
* Check username availability
*/
async checkUsernameAvailability(username: string): Promise<{ available: boolean }> {
return this.request<{ available: boolean }>(`/users/check-username`, {
method: 'POST',
body: JSON.stringify({ username })
})
}
/**
* Check email availability
*/
async checkEmailAvailability(email: string): Promise<{ available: boolean }> {
return this.request<{ available: boolean }>(`/users/check-email`, {
method: 'POST',
body: JSON.stringify({ email })
})
}
/**
* Update user profile
*/
async updateProfile(updates: Partial<UserInfo>): Promise<UserInfo> {
return this.request<UserInfo>('/users/me', {
method: 'PUT',
body: JSON.stringify(updates)
})
}
/**
* Get current user
*/
async getCurrentUser(): Promise<UserInfo> {
return this.request<UserInfo>('/users/me')
}
}
export const userAdapter = new UserAdapter()

View file

@ -1,232 +0,0 @@
/**
* Grid API resource functions - Facade layer for migration
*
* This module provides backward compatibility during the migration
* from api/core to the adapter pattern. Services can continue using
* these functions while we migrate them incrementally.
*/
import { gridAdapter } from '$lib/api/adapters'
import type {
GridWeapon,
GridCharacter,
GridSummon
} from '$lib/api/adapters'
// FetchLike type for backward compatibility
export type FetchLike = typeof fetch
// Weapon grid operations
export async function addWeapon(
fetch: FetchLike,
partyId: string,
weaponId: string, // Granblue ID
position: number,
options?: {
mainhand?: boolean
uncapLevel?: number
transcendenceStep?: number
element?: number
},
headers?: Record<string, string>
): Promise<GridWeapon> {
return gridAdapter.createWeapon({
partyId,
weaponId,
position,
mainhand: position === -1 || options?.mainhand,
uncapLevel: options?.uncapLevel ?? 3,
transcendenceStage: options?.transcendenceStep ?? 0
})
}
export async function updateWeapon(
fetch: FetchLike,
partyId: string,
gridWeaponId: string,
updates: {
position?: number
uncapLevel?: number
transcendenceStep?: number
element?: number
},
headers?: Record<string, string>
): Promise<GridWeapon> {
return gridAdapter.updateWeapon(gridWeaponId, {
position: updates.position,
uncapLevel: updates.uncapLevel,
transcendenceStage: updates.transcendenceStep,
element: updates.element
})
}
export async function removeWeapon(
fetch: FetchLike,
partyId: string,
gridWeaponId: string,
headers?: Record<string, string>
): Promise<void> {
return gridAdapter.deleteWeapon({
id: gridWeaponId,
partyId
})
}
// Summon grid operations
export async function addSummon(
fetch: FetchLike,
partyId: string,
summonId: string, // Granblue ID
position: number,
options?: {
main?: boolean
friend?: boolean
quickSummon?: boolean
uncapLevel?: number
transcendenceStep?: number
},
headers?: Record<string, string>
): Promise<GridSummon> {
return gridAdapter.createSummon({
partyId,
summonId,
position,
main: position === -1 || options?.main,
friend: position === 6 || options?.friend,
quickSummon: options?.quickSummon ?? false,
uncapLevel: options?.uncapLevel ?? 3,
transcendenceStage: options?.transcendenceStep ?? 0
})
}
export async function updateSummon(
fetch: FetchLike,
partyId: string,
gridSummonId: string,
updates: {
position?: number
quickSummon?: boolean
uncapLevel?: number
transcendenceStep?: number
},
headers?: Record<string, string>
): Promise<GridSummon> {
return gridAdapter.updateSummon(gridSummonId, {
position: updates.position,
quickSummon: updates.quickSummon,
uncapLevel: updates.uncapLevel,
transcendenceStage: updates.transcendenceStep
})
}
export async function removeSummon(
fetch: FetchLike,
partyId: string,
gridSummonId: string,
headers?: Record<string, string>
): Promise<void> {
return gridAdapter.deleteSummon({
id: gridSummonId,
partyId
})
}
// Character grid operations
export async function addCharacter(
fetch: FetchLike,
partyId: string,
characterId: string, // Granblue ID
position: number,
options?: {
uncapLevel?: number
transcendenceStep?: number
perpetuity?: boolean
},
headers?: Record<string, string>
): Promise<GridCharacter> {
return gridAdapter.createCharacter({
partyId,
characterId,
position,
uncapLevel: options?.uncapLevel ?? 3,
transcendenceStage: options?.transcendenceStep ?? 0
})
}
export async function updateCharacter(
fetch: FetchLike,
partyId: string,
gridCharacterId: string,
updates: {
position?: number
uncapLevel?: number
transcendenceStep?: number
perpetuity?: boolean
},
headers?: Record<string, string>
): Promise<GridCharacter> {
return gridAdapter.updateCharacter(gridCharacterId, {
position: updates.position,
uncapLevel: updates.uncapLevel,
transcendenceStage: updates.transcendenceStep,
perpetualModifiers: updates.perpetuity ? {} : undefined
})
}
export async function removeCharacter(
fetch: FetchLike,
partyId: string,
gridCharacterId: string,
headers?: Record<string, string>
): Promise<void> {
return gridAdapter.deleteCharacter({
id: gridCharacterId,
partyId
})
}
// Uncap update methods - these use special endpoints
export async function updateCharacterUncap(
gridCharacterId: string,
uncapLevel?: number,
transcendenceStep?: number,
headers?: Record<string, string>
): Promise<GridCharacter> {
// For uncap updates, we need the partyId which isn't passed here
// This is a limitation of the current API design
// For now, we'll use the update method with a fake partyId
return gridAdapter.updateCharacterUncap({
id: gridCharacterId,
partyId: 'unknown', // This is a hack - the API should be redesigned
uncapLevel: uncapLevel ?? 3,
transcendenceStep
})
}
export async function updateWeaponUncap(
gridWeaponId: string,
uncapLevel?: number,
transcendenceStep?: number,
headers?: Record<string, string>
): Promise<GridWeapon> {
return gridAdapter.updateWeaponUncap({
id: gridWeaponId,
partyId: 'unknown', // This is a hack - the API should be redesigned
uncapLevel: uncapLevel ?? 3,
transcendenceStep
})
}
export async function updateSummonUncap(
gridSummonId: string,
uncapLevel?: number,
transcendenceStep?: number,
headers?: Record<string, string>
): Promise<GridSummon> {
return gridAdapter.updateSummonUncap({
id: gridSummonId,
partyId: 'unknown', // This is a hack - the API should be redesigned
uncapLevel: uncapLevel ?? 3,
transcendenceStep
})
}

View file

@ -1,214 +0,0 @@
/**
* Party API resource functions - Facade layer for migration
*
* This module provides backward compatibility during the migration
* from api/core to the adapter pattern. Services can continue using
* these functions while we migrate them incrementally.
*/
import { partyAdapter } from '$lib/api/adapters'
import type { Party } from '$lib/types/api/party'
import { z } from 'zod'
// FetchLike type for backward compatibility
export type FetchLike = typeof fetch
// API functions - Now using PartyAdapter
export async function getByShortcode(fetch: FetchLike, shortcode: string): Promise<Party> {
// Ignore fetch parameter - adapter handles its own fetching
return partyAdapter.getByShortcode(shortcode)
}
export async function create(
fetch: FetchLike,
payload: Partial<Party>,
headers?: Record<string, string>
): Promise<{ party: Party; editKey?: string }> {
// The adapter returns the party directly, we need to wrap it
// to maintain backward compatibility with editKey
const party = await partyAdapter.create(payload, headers)
// Note: editKey is returned in headers by the adapter if present
// For now, we'll return just the party
return {
party,
editKey: undefined // Edit key handling may need adjustment
}
}
export async function update(
fetch: FetchLike,
id: string,
payload: Partial<Party>,
headers?: Record<string, string>
): Promise<Party> {
return partyAdapter.update({ shortcode: id, ...payload }, headers)
}
export async function remix(
fetch: FetchLike,
shortcode: string,
localId?: string,
headers?: Record<string, string>
): Promise<{ party: Party; editKey?: string }> {
const party = await partyAdapter.remix(shortcode, headers)
return {
party,
editKey: undefined // Edit key handling may need adjustment
}
}
export async function deleteParty(
fetch: FetchLike,
id: string,
headers?: Record<string, string>
): Promise<void> {
return partyAdapter.delete(id, headers)
}
/**
* List public parties for explore page
*/
export async function list(
fetch: FetchLike,
params?: {
page?: number
per_page?: number
raid_id?: string
element?: number
}
): Promise<{
items: Party[]
total: number
totalPages: number
perPage: number
}> {
// Map parameters to adapter format
const adapterParams = {
page: params?.page,
per: params?.per_page,
raidId: params?.raid_id,
element: params?.element
}
const response = await partyAdapter.list(adapterParams)
// Map adapter response to expected format
return {
items: response.results,
total: response.total,
totalPages: response.totalPages,
perPage: response.per || 20
}
}
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
}
}> {
// Map parameters to adapter format
const adapterParams = {
username,
page: filters?.page,
per: 20, // Default page size
visibility: undefined, // Not specified in original
raidId: filters?.raid,
element: filters?.element,
recency: filters?.recency
}
const response = await partyAdapter.listUserParties(adapterParams)
// Map adapter response to expected format
return {
parties: response.results,
meta: {
count: response.total,
totalPages: response.totalPages,
perPage: response.per || 20
}
}
}
// Grid operations - These should eventually move to GridAdapter
export async function updateWeaponGrid(
fetch: FetchLike,
partyId: string,
payload: any,
headers?: Record<string, string>
): Promise<Party> {
// For now, use gridUpdate with a single operation
// This is a temporary implementation until GridAdapter is fully integrated
const operation = {
type: 'add' as const,
entity: 'weapon' as const,
...payload
}
const response = await partyAdapter.gridUpdate(partyId, [operation])
// Check for conflicts
if ('conflicts' in response && response.conflicts) {
const error = new Error('Weapon conflict') as any
error.conflicts = response
throw error
}
return response.party
}
export async function updateSummonGrid(
fetch: FetchLike,
partyId: string,
payload: any,
headers?: Record<string, string>
): Promise<Party> {
// For now, use gridUpdate with a single operation
const operation = {
type: 'add' as const,
entity: 'summon' as const,
...payload
}
const response = await partyAdapter.gridUpdate(partyId, [operation])
return response.party
}
export async function updateCharacterGrid(
fetch: FetchLike,
partyId: string,
payload: any,
headers?: Record<string, string>
): Promise<Party> {
// For now, use gridUpdate with a single operation
const operation = {
type: 'add' as const,
entity: 'character' as const,
...payload
}
const response = await partyAdapter.gridUpdate(partyId, [operation])
// Check for conflicts
if ('conflicts' in response && response.conflicts) {
const error = new Error('Character conflict') as any
error.conflicts = response
throw error
}
return response.party
}

View file

@ -1,47 +0,0 @@
import type { FetchLike } from '../core'
import { get, buildUrl } from '../core'
export interface UserInfoResponse {
id: string
username: string
language: string
private: boolean
gender: number
theme: string
role: number
avatar: {
picture: string
element: string
}
}
export const users = {
info: (f: FetchLike, username: string, init?: RequestInit) =>
get<UserInfoResponse>(f, `/users/info/${encodeURIComponent(username)}`, undefined, init)
}
export interface UserProfileResponse {
profile: UserInfoResponse & { parties?: any[] }
meta?: { count?: number; total_pages?: number; per_page?: number }
}
export async function profile(
f: FetchLike,
username: string,
page?: number
): Promise<{ user: UserInfoResponse; items: any[]; page: number; total?: number; totalPages?: number; perPage?: number }> {
const qs = page && page > 1 ? { page } : undefined
const url = buildUrl(`/users/${encodeURIComponent(username)}`, qs as any)
const resp = await f(url, { credentials: 'include' })
if (!resp.ok) throw new Error(resp.statusText || 'Failed to load profile')
const json = (await resp.json()) as UserProfileResponse
const items = Array.isArray(json.profile?.parties) ? json.profile.parties : []
return {
user: json.profile as any,
items,
page: page || 1,
total: json.meta?.count,
totalPages: json.meta?.total_pages,
perPage: json.meta?.per_page
}
}

View file

@ -1,10 +1,9 @@
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
import { profile } from '$lib/api/resources/users'
import { userAdapter } from '$lib/api/adapters'
import { parseParty } from '$lib/api/schemas/party'
import * as partiesApi from '$lib/api/resources/parties'
export const load: PageServerLoad = async ({ fetch, params, url, depends, locals }) => {
export const load: PageServerLoad = async ({ params, url, depends, locals }) => {
depends('app:profile')
const username = params.username
const pageParam = url.searchParams.get('page')
@ -14,11 +13,20 @@ export const load: PageServerLoad = async ({ fetch, params, url, depends, locals
try {
if (tab === 'favorites' && isOwner) {
const fav = await partiesApi.favorites(fetch as any, { page })
return { user: { username } as any, items: fav.items, page: fav.page, total: fav.total, totalPages: fav.totalPages, perPage: fav.perPage, tab, isOwner }
const fav = await userAdapter.getFavorites({ page })
return {
user: { username } as any,
items: fav.items,
page: fav.page,
total: fav.total,
totalPages: fav.totalPages,
perPage: fav.perPage,
tab,
isOwner
}
}
const { user, items, total, totalPages, perPage } = await profile(fetch as any, username, page)
const { user, items, total, totalPages, perPage } = await userAdapter.getProfile(username, page)
const parties = items.map((p) => parseParty(p))
return { user, items: parties, page, total, totalPages, perPage, tab, isOwner }
} catch (e: any) {

View file

@ -1,8 +1,8 @@
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
import * as parties from '$lib/api/resources/parties'
import { partyAdapter } from '$lib/api/adapters'
export const load: PageServerLoad = async ({ fetch, url, depends }) => {
export const load: PageServerLoad = async ({ url, depends }) => {
depends('app:parties:list')
const pageParam = url.searchParams.get('page')
@ -12,9 +12,16 @@ export const load: PageServerLoad = async ({ fetch, url, depends }) => {
console.log('[explore/+page.server.ts] Full URL:', url.toString())
try {
const { items, total, totalPages, perPage } = await parties.list(fetch, { page })
console.log('[explore/+page.server.ts] Successfully loaded', items.length, 'parties')
return { items, page, total, totalPages, perPage }
const response = await partyAdapter.list({ page })
console.log('[explore/+page.server.ts] Successfully loaded', response.results.length, 'parties')
return {
items: response.results,
page,
total: response.total,
totalPages: response.totalPages,
perPage: response.per || 20
}
} catch (e: any) {
console.error('[explore/+page.server.ts] Failed to load teams:', {
error: e,