add collection API layer

Types, adapter, queries, and mutations for managing user collections
(characters, weapons, summons, job accessories). Supports both private
collection management and public collection viewing with privacy.
This commit is contained in:
Justin Edmund 2025-12-02 09:28:22 -08:00
parent 51db7f7604
commit 60ac5d4ab2
4 changed files with 1019 additions and 0 deletions

View file

@ -0,0 +1,366 @@
/**
* 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,
CollectionResponse
} 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 the current user's collection characters with optional filters
*/
async listCharacters(
params: CollectionListParams = {}
): Promise<PaginatedResponse<CollectionCharacter>> {
const response = await this.request<CollectionCharacterListResponse>('/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
* Makes parallel requests for each character
*/
async addCharacters(inputs: CollectionCharacterInput[]): Promise<CollectionCharacter[]> {
const results = await Promise.all(inputs.map((input) => this.addCharacter(input)))
return results
}
/**
* 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 the current user's collection
* Useful for filtering out already-owned characters in the add modal
*/
async getCollectedCharacterIds(): 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({ page, limit: 100 })
allIds.push(...response.results.map((c) => c.character.id))
hasMore = page < response.totalPages
page++
}
return allIds
}
// ============================================
// Collection Weapons
// ============================================
/**
* Lists the current user's collection weapons with optional filters
*/
async listWeapons(params: CollectionListParams = {}): Promise<PaginatedResponse<CollectionWeapon>> {
const response = await this.request<{
weapons: CollectionWeapon[]
meta: { count: number; totalPages: number; perPage: number; currentPage: number }
}>('/collection/weapons', {
method: 'GET',
query: params
})
return {
results: response.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
}
})
}
/**
* 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 the current user's collection summons with optional filters
*/
async listSummons(params: CollectionListParams = {}): Promise<PaginatedResponse<CollectionSummon>> {
const response = await this.request<{
summons: CollectionSummon[]
meta: { count: number; totalPages: number; perPage: number; currentPage: number }
}>('/collection/summons', {
method: 'GET',
query: params
})
return {
results: response.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
}
})
}
/**
* 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'
})
}
// ============================================
// Public Collection (viewing other users)
// ============================================
/**
* Gets a user's public collection (respects privacy settings)
* @param userId - The user's ID
* @param type - Optional type filter: 'characters', 'weapons', 'summons', 'job_accessories'
*/
async getPublicCollection(
userId: string,
type?: 'characters' | 'weapons' | 'summons' | 'job_accessories'
): Promise<CollectionResponse> {
return this.request<CollectionResponse>(`/users/${userId}/collection`, {
method: 'GET',
query: type ? { type } : undefined
})
}
/**
* Gets a user's public character collection
*/
async getPublicCharacters(userId: string): Promise<CollectionCharacter[]> {
const response = await this.getPublicCollection(userId, 'characters')
return response.characters || []
}
/**
* Gets a user's public weapon collection
*/
async getPublicWeapons(userId: string): Promise<CollectionWeapon[]> {
const response = await this.getPublicCollection(userId, 'weapons')
return response.weapons || []
}
/**
* Gets a user's public summon collection
*/
async getPublicSummons(userId: string): Promise<CollectionSummon[]> {
const response = await this.getPublicCollection(userId, 'summons')
return response.summons || []
}
// ============================================
// 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)

View file

@ -0,0 +1,256 @@
/**
* Collection Mutation Configurations
*
* Provides mutation configurations for collection operations
* with cache invalidation using TanStack Query v6.
*
* @module api/mutations/collection
*/
import { useQueryClient, createMutation } from '@tanstack/svelte-query'
import { collectionAdapter } from '$lib/api/adapters/collection.adapter'
import { collectionKeys } from '$lib/api/queries/collection.queries'
import type {
CollectionCharacter,
CollectionCharacterInput,
CollectionWeaponInput,
CollectionSummonInput,
CollectionJobAccessoryInput
} from '$lib/types/api/collection'
// ============================================================================
// Character Mutations
// ============================================================================
/**
* Add characters to collection mutation
*
* Adds one or more characters to the user's collection.
*
* @example
* ```svelte
* <script lang="ts">
* import { useAddToCollection } from '$lib/api/mutations/collection.mutations'
*
* const addToCollection = useAddToCollection()
*
* function handleAdd(characterIds: string[]) {
* addToCollection.mutate(
* characterIds.map(id => ({ characterId: id }))
* )
* }
* </script>
* ```
*/
export function useAddCharactersToCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (inputs: CollectionCharacterInput[]) => collectionAdapter.addCharacters(inputs),
onSuccess: () => {
// Invalidate all character-related collection queries
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
queryClient.invalidateQueries({ queryKey: collectionKeys.characterIds() })
}
}))
}
/**
* Add single character to collection mutation
*/
export function useAddCharacterToCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (input: CollectionCharacterInput) => collectionAdapter.addCharacter(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
queryClient.invalidateQueries({ queryKey: collectionKeys.characterIds() })
}
}))
}
/**
* Update collection character mutation
*
* Updates a character's customizations (uncap, rings, etc.) in the collection.
*/
export function useUpdateCollectionCharacter() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: ({ id, input }: { id: string; input: Partial<CollectionCharacterInput> }) =>
collectionAdapter.updateCharacter(id, input),
onMutate: async ({ id, input }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: collectionKeys.character(id) })
// Snapshot the previous value
const previousCharacter = queryClient.getQueryData<CollectionCharacter>(
collectionKeys.character(id)
)
// Optimistically update the cache
if (previousCharacter) {
queryClient.setQueryData(collectionKeys.character(id), {
...previousCharacter,
...input
})
}
return { previousCharacter }
},
onError: (_err, { id }, context) => {
// Rollback on error
if (context?.previousCharacter) {
queryClient.setQueryData(collectionKeys.character(id), context.previousCharacter)
}
},
onSettled: (_data, _err, { id }) => {
// Always refetch after mutation
queryClient.invalidateQueries({ queryKey: collectionKeys.character(id) })
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
}
}))
}
/**
* Remove character from collection mutation
*/
export function useRemoveCharacterFromCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (id: string) => collectionAdapter.removeCharacter(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
queryClient.invalidateQueries({ queryKey: collectionKeys.characterIds() })
}
}))
}
// ============================================================================
// Weapon Mutations
// ============================================================================
/**
* Add weapon to collection mutation
*/
export function useAddWeaponToCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (input: CollectionWeaponInput) => collectionAdapter.addWeapon(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.weapons() })
}
}))
}
/**
* Update collection weapon mutation
*/
export function useUpdateCollectionWeapon() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: ({ id, input }: { id: string; input: Partial<CollectionWeaponInput> }) =>
collectionAdapter.updateWeapon(id, input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.weapons() })
}
}))
}
/**
* Remove weapon from collection mutation
*/
export function useRemoveWeaponFromCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (id: string) => collectionAdapter.removeWeapon(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.weapons() })
}
}))
}
// ============================================================================
// Summon Mutations
// ============================================================================
/**
* Add summon to collection mutation
*/
export function useAddSummonToCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (input: CollectionSummonInput) => collectionAdapter.addSummon(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.summons() })
}
}))
}
/**
* Update collection summon mutation
*/
export function useUpdateCollectionSummon() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: ({ id, input }: { id: string; input: Partial<CollectionSummonInput> }) =>
collectionAdapter.updateSummon(id, input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.summons() })
}
}))
}
/**
* Remove summon from collection mutation
*/
export function useRemoveSummonFromCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (id: string) => collectionAdapter.removeSummon(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.summons() })
}
}))
}
// ============================================================================
// Job Accessory Mutations
// ============================================================================
/**
* Add job accessory to collection mutation
*/
export function useAddJobAccessoryToCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (input: CollectionJobAccessoryInput) => collectionAdapter.addJobAccessory(input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.all })
}
}))
}
/**
* Remove job accessory from collection mutation
*/
export function useRemoveJobAccessoryFromCollection() {
const queryClient = useQueryClient()
return createMutation(() => ({
mutationFn: (id: string) => collectionAdapter.removeJobAccessory(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: collectionKeys.all })
}
}))
}

View file

@ -0,0 +1,233 @@
/**
* Collection Query Options Factory
*
* Provides type-safe, reusable query configurations for collection operations
* using TanStack Query v6 patterns.
*
* @module api/queries/collection
*/
import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
import { collectionAdapter } from '$lib/api/adapters/collection.adapter'
import type {
CollectionCharacter,
CollectionWeapon,
CollectionSummon,
CollectionFilters
} from '$lib/types/api/collection'
/**
* Page result format for collection infinite queries
*/
export interface CollectionPageResult<T> {
results: T[]
page: number
totalPages: number
total: number
perPage: number
}
/**
* Collection query options factory
*
* @example
* ```typescript
* import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
* import { collectionQueries } from '$lib/api/queries/collection.queries'
*
* // Own collection characters
* const characters = createInfiniteQuery(() => collectionQueries.characters())
*
* // Public collection
* const publicChars = createQuery(() => collectionQueries.publicCharacters(userId))
*
* // Collected character IDs (for filtering add modal)
* const ownedIds = createQuery(() => collectionQueries.collectedCharacterIds())
* ```
*/
export const collectionQueries = {
/**
* Current user's collection characters with infinite scroll
*/
characters: (filters?: CollectionFilters) =>
infiniteQueryOptions({
queryKey: ['collection', 'characters', filters] as const,
queryFn: async ({ pageParam }): Promise<CollectionPageResult<CollectionCharacter>> => {
const response = await collectionAdapter.listCharacters({
...filters,
page: pageParam
})
return {
results: response.results,
page: response.page,
totalPages: response.totalPages,
total: response.total,
perPage: response.perPage ?? 50
}
},
initialPageParam: 1,
getNextPageParam: (lastPage) => {
if (lastPage.page < lastPage.totalPages) {
return lastPage.page + 1
}
return undefined
},
staleTime: 1000 * 60 * 2, // 2 minutes
gcTime: 1000 * 60 * 15 // 15 minutes
}),
/**
* Current user's collection weapons with infinite scroll
*/
weapons: (filters?: CollectionFilters) =>
infiniteQueryOptions({
queryKey: ['collection', 'weapons', filters] as const,
queryFn: async ({ pageParam }): Promise<CollectionPageResult<CollectionWeapon>> => {
const response = await collectionAdapter.listWeapons({
...filters,
page: pageParam
})
return {
results: response.results,
page: response.page,
totalPages: response.totalPages,
total: response.total,
perPage: response.perPage ?? 50
}
},
initialPageParam: 1,
getNextPageParam: (lastPage) => {
if (lastPage.page < lastPage.totalPages) {
return lastPage.page + 1
}
return undefined
},
staleTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 15
}),
/**
* Current user's collection summons with infinite scroll
*/
summons: (filters?: CollectionFilters) =>
infiniteQueryOptions({
queryKey: ['collection', 'summons', filters] as const,
queryFn: async ({ pageParam }): Promise<CollectionPageResult<CollectionSummon>> => {
const response = await collectionAdapter.listSummons({
...filters,
page: pageParam
})
return {
results: response.results,
page: response.page,
totalPages: response.totalPages,
total: response.total,
perPage: response.perPage ?? 50
}
},
initialPageParam: 1,
getNextPageParam: (lastPage) => {
if (lastPage.page < lastPage.totalPages) {
return lastPage.page + 1
}
return undefined
},
staleTime: 1000 * 60 * 2,
gcTime: 1000 * 60 * 15
}),
/**
* Get IDs of characters already in the user's collection
* Used to filter out owned characters in the add modal
*/
collectedCharacterIds: () =>
queryOptions({
queryKey: ['collection', 'characters', 'ids'] as const,
queryFn: () => collectionAdapter.getCollectedCharacterIds(),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 30 // 30 minutes
}),
/**
* Single collection character by ID
*/
character: (id: string) =>
queryOptions({
queryKey: ['collection', 'character', id] as const,
queryFn: () => collectionAdapter.getCharacter(id),
enabled: !!id,
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 30
}),
/**
* Public collection for a user (respects privacy)
*/
publicCharacters: (userId: string) =>
queryOptions({
queryKey: ['collection', 'public', userId, 'characters'] as const,
queryFn: () => collectionAdapter.getPublicCharacters(userId),
enabled: !!userId,
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 30
}),
/**
* Public weapon collection for a user
*/
publicWeapons: (userId: string) =>
queryOptions({
queryKey: ['collection', 'public', userId, 'weapons'] as const,
queryFn: () => collectionAdapter.getPublicWeapons(userId),
enabled: !!userId,
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 30
}),
/**
* Public summon collection for a user
*/
publicSummons: (userId: string) =>
queryOptions({
queryKey: ['collection', 'public', userId, 'summons'] as const,
queryFn: () => collectionAdapter.getPublicSummons(userId),
enabled: !!userId,
staleTime: 1000 * 60 * 5,
gcTime: 1000 * 60 * 30
})
}
/**
* Query key helpers for cache invalidation
*
* @example
* ```typescript
* import { useQueryClient } from '@tanstack/svelte-query'
* import { collectionKeys } from '$lib/api/queries/collection.queries'
*
* const queryClient = useQueryClient()
*
* // Invalidate all collection data
* queryClient.invalidateQueries({ queryKey: collectionKeys.all })
*
* // Invalidate only characters
* queryClient.invalidateQueries({ queryKey: collectionKeys.characters() })
* ```
*/
export const collectionKeys = {
all: ['collection'] as const,
characters: () => [...collectionKeys.all, 'characters'] as const,
characterList: (filters?: CollectionFilters) =>
[...collectionKeys.characters(), filters] as const,
character: (id: string) => [...collectionKeys.all, 'character', id] as const,
characterIds: () => [...collectionKeys.characters(), 'ids'] as const,
weapons: () => [...collectionKeys.all, 'weapons'] as const,
weaponList: (filters?: CollectionFilters) => [...collectionKeys.weapons(), filters] as const,
summons: () => [...collectionKeys.all, 'summons'] as const,
summonList: (filters?: CollectionFilters) => [...collectionKeys.summons(), filters] as const,
public: (userId: string) => [...collectionKeys.all, 'public', userId] as const,
publicCharacters: (userId: string) =>
[...collectionKeys.public(userId), 'characters'] as const,
publicWeapons: (userId: string) => [...collectionKeys.public(userId), 'weapons'] as const,
publicSummons: (userId: string) => [...collectionKeys.public(userId), 'summons'] as const
}

View file

@ -0,0 +1,164 @@
// Collection types based on Rails CollectionCharacter/Weapon/Summon blueprints
// These define user-owned items with customizations
import type { Character, Weapon, Summon, JobAccessory, Awakening } from './entities'
/**
* Extended mastery modifier (used for rings and earrings)
*/
export interface ExtendedMastery {
modifier: number
strength: number
}
/**
* Collection character from CollectionCharacterBlueprint
* Represents a user's owned character with customizations
*/
export interface CollectionCharacter {
id: string
uncapLevel: number
transcendenceStep: number
perpetuity: boolean
ring1: ExtendedMastery | null
ring2: ExtendedMastery | null
ring3: ExtendedMastery | null
ring4: ExtendedMastery | null
earring: ExtendedMastery | null
awakening: {
type: Awakening
level: number
} | null
character: Character
createdAt: string
updatedAt: string
}
/**
* Collection weapon from CollectionWeaponBlueprint
* Represents a user's owned weapon with customizations
*/
export interface CollectionWeapon {
id: string
uncapLevel: number
transcendenceStep: number
element?: number // For element-changeable weapons
ax?: Array<{ modifier: number; strength: number }>
awakening: {
type: Awakening
level: number
} | null
weaponKeys?: Array<{
id: string
name: { en: string; ja: string }
slot: number
}>
weapon: Weapon
createdAt: string
updatedAt: string
}
/**
* Collection summon from CollectionSummonBlueprint
* Represents a user's owned summon with customizations
*/
export interface CollectionSummon {
id: string
uncapLevel: number
transcendenceStep: number
summon: Summon
createdAt: string
updatedAt: string
}
/**
* Collection job accessory from CollectionJobAccessoryBlueprint
*/
export interface CollectionJobAccessory {
id: string
jobAccessory: JobAccessory
createdAt: string
updatedAt: string
}
/**
* Full collection response (when no type filter is applied)
*/
export interface CollectionResponse {
characters?: CollectionCharacter[]
weapons?: CollectionWeapon[]
summons?: CollectionSummon[]
jobAccessories?: CollectionJobAccessory[]
}
/**
* Input for creating a collection character
*/
export interface CollectionCharacterInput {
characterId: string
uncapLevel?: number
transcendenceStep?: number
perpetuity?: boolean
awakeningId?: string
awakeningLevel?: number
ring1?: ExtendedMastery
ring2?: ExtendedMastery
ring3?: ExtendedMastery
ring4?: ExtendedMastery
earring?: ExtendedMastery
}
/**
* Input for creating a collection weapon
*/
export interface CollectionWeaponInput {
weaponId: string
uncapLevel?: number
transcendenceStep?: number
element?: number
weaponKey1Id?: string
weaponKey2Id?: string
weaponKey3Id?: string
weaponKey4Id?: string
awakeningId?: string
awakeningLevel?: number
axModifier1?: number
axStrength1?: number
axModifier2?: number
axStrength2?: number
}
/**
* Input for creating a collection summon
*/
export interface CollectionSummonInput {
summonId: string
uncapLevel?: number
transcendenceStep?: number
}
/**
* Input for creating a collection job accessory
*/
export interface CollectionJobAccessoryInput {
jobAccessoryId: string
}
/**
* Filters for listing collection items
*/
export interface CollectionFilters {
element?: number[]
rarity?: number[]
page?: number
limit?: number
}
/**
* Collection privacy levels (matches Rails enum)
*/
export enum CollectionPrivacy {
Everyone = 0,
CrewOnly = 1,
Private = 2
}