add artifact queries and mutations
- artifactQueries: reference data (cached 1hr), collection infinite, skills by slot - artifactKeys: cache invalidation helpers - mutations: collection CRUD, grid CRUD, equip, and grading
This commit is contained in:
parent
1e708e1064
commit
ab1243190b
2 changed files with 446 additions and 0 deletions
227
src/lib/api/mutations/artifact.mutations.ts
Normal file
227
src/lib/api/mutations/artifact.mutations.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
/**
|
||||
* Artifact Mutation Configurations
|
||||
*
|
||||
* Provides mutation configurations for artifact operations
|
||||
* with cache invalidation using TanStack Query v6.
|
||||
*
|
||||
* @module api/mutations/artifact
|
||||
*/
|
||||
|
||||
import { useQueryClient, createMutation } from '@tanstack/svelte-query'
|
||||
import { artifactAdapter } from '$lib/api/adapters/artifact.adapter'
|
||||
import { artifactKeys } from '$lib/api/queries/artifact.queries'
|
||||
import type {
|
||||
CollectionArtifact,
|
||||
CollectionArtifactInput,
|
||||
GridArtifact,
|
||||
GridArtifactInput,
|
||||
GridArtifactUpdateInput,
|
||||
ArtifactGrade,
|
||||
ArtifactGradeInput
|
||||
} from '$lib/types/api/artifact'
|
||||
|
||||
// ============================================================================
|
||||
// Collection Artifact Mutations
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a collection artifact mutation
|
||||
*
|
||||
* Adds a single artifact to the user's collection.
|
||||
*
|
||||
* @example
|
||||
* ```svelte
|
||||
* <script lang="ts">
|
||||
* import { useCreateCollectionArtifact } from '$lib/api/mutations/artifact.mutations'
|
||||
*
|
||||
* const createArtifact = useCreateCollectionArtifact()
|
||||
*
|
||||
* function handleCreate(input: CollectionArtifactInput) {
|
||||
* createArtifact.mutate(input)
|
||||
* }
|
||||
* </script>
|
||||
* ```
|
||||
*/
|
||||
export function useCreateCollectionArtifact() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (input: CollectionArtifactInput) =>
|
||||
artifactAdapter.createCollectionArtifact(input),
|
||||
onSuccess: () => {
|
||||
// Invalidate all collection artifact queries
|
||||
queryClient.invalidateQueries({ queryKey: artifactKeys.collectionBase })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create multiple collection artifacts in a batch
|
||||
*/
|
||||
export function useCreateCollectionArtifactsBatch() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (inputs: CollectionArtifactInput[]) =>
|
||||
artifactAdapter.createCollectionArtifactsBatch(inputs),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: artifactKeys.collectionBase })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a collection artifact mutation
|
||||
*
|
||||
* Updates an artifact's properties (skills, level, nickname, etc.)
|
||||
* Includes optimistic updates for better UX.
|
||||
*/
|
||||
export function useUpdateCollectionArtifact() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, input }: { id: string; input: Partial<CollectionArtifactInput> }) =>
|
||||
artifactAdapter.updateCollectionArtifact(id, input),
|
||||
onMutate: async ({ id, input }) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({ queryKey: artifactKeys.collectionArtifact(id) })
|
||||
|
||||
// Snapshot the previous value
|
||||
const previousArtifact = queryClient.getQueryData<CollectionArtifact>(
|
||||
artifactKeys.collectionArtifact(id)
|
||||
)
|
||||
|
||||
// Optimistically update the cache
|
||||
if (previousArtifact) {
|
||||
queryClient.setQueryData(artifactKeys.collectionArtifact(id), {
|
||||
...previousArtifact,
|
||||
...input
|
||||
})
|
||||
}
|
||||
|
||||
return { previousArtifact }
|
||||
},
|
||||
onError: (_err, { id }, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousArtifact) {
|
||||
queryClient.setQueryData(artifactKeys.collectionArtifact(id), context.previousArtifact)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _err, { id }) => {
|
||||
// Always refetch after mutation
|
||||
queryClient.invalidateQueries({ queryKey: artifactKeys.collectionArtifact(id) })
|
||||
queryClient.invalidateQueries({ queryKey: artifactKeys.collectionBase })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a collection artifact mutation
|
||||
*/
|
||||
export function useDeleteCollectionArtifact() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (id: string) => artifactAdapter.deleteCollectionArtifact(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: artifactKeys.collectionBase })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grid Artifact Mutations (Equipped on Characters)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a grid artifact mutation
|
||||
*
|
||||
* Creates a new artifact and equips it on a character in a party.
|
||||
*/
|
||||
export function useCreateGridArtifact() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (input: GridArtifactInput) => artifactAdapter.createGridArtifact(input),
|
||||
onSuccess: (_data, input) => {
|
||||
// Invalidate party queries to reflect the new artifact
|
||||
queryClient.invalidateQueries({ queryKey: ['parties', input.partyId] })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a grid artifact mutation
|
||||
*
|
||||
* Updates an artifact equipped on a character.
|
||||
*/
|
||||
export function useUpdateGridArtifact() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({ id, input }: { id: string; input: GridArtifactUpdateInput }) =>
|
||||
artifactAdapter.updateGridArtifact(id, input),
|
||||
onSuccess: () => {
|
||||
// Invalidate party queries to reflect the updated artifact
|
||||
queryClient.invalidateQueries({ queryKey: ['parties'] })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a grid artifact mutation
|
||||
*
|
||||
* Removes an artifact from a character.
|
||||
*/
|
||||
export function useDeleteGridArtifact() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: (id: string) => artifactAdapter.deleteGridArtifact(id),
|
||||
onSuccess: () => {
|
||||
// Invalidate party queries to reflect the removal
|
||||
queryClient.invalidateQueries({ queryKey: ['parties'] })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Equip a collection artifact onto a character
|
||||
*
|
||||
* Links an existing collection artifact to a character in a party.
|
||||
*/
|
||||
export function useEquipCollectionArtifact() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return createMutation(() => ({
|
||||
mutationFn: ({
|
||||
partyId,
|
||||
gridCharacterId,
|
||||
collectionArtifactId
|
||||
}: {
|
||||
partyId: string
|
||||
gridCharacterId: string
|
||||
collectionArtifactId: string
|
||||
}) => artifactAdapter.equipCollectionArtifact(partyId, gridCharacterId, collectionArtifactId),
|
||||
onSuccess: (_data, { partyId }) => {
|
||||
// Invalidate party queries to reflect the equipped artifact
|
||||
queryClient.invalidateQueries({ queryKey: ['parties', partyId] })
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Artifact Grading (Stateless)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Grade artifact skills mutation
|
||||
*
|
||||
* Calculates a grade for artifact skills without persisting.
|
||||
* Useful for preview/what-if scenarios.
|
||||
*/
|
||||
export function useGradeArtifact() {
|
||||
return createMutation(() => ({
|
||||
mutationFn: (input: ArtifactGradeInput) => artifactAdapter.gradeArtifact(input)
|
||||
}))
|
||||
}
|
||||
219
src/lib/api/queries/artifact.queries.ts
Normal file
219
src/lib/api/queries/artifact.queries.ts
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
/**
|
||||
* Artifact Query Options Factory
|
||||
*
|
||||
* Provides type-safe, reusable query configurations for artifact operations
|
||||
* using TanStack Query v6 patterns.
|
||||
*
|
||||
* @module api/queries/artifact
|
||||
*/
|
||||
|
||||
import { queryOptions, infiniteQueryOptions } from '@tanstack/svelte-query'
|
||||
import { artifactAdapter, type CollectionArtifactListParams } from '$lib/api/adapters/artifact.adapter'
|
||||
import type { Artifact, ArtifactSkill, CollectionArtifact } from '$lib/types/api/artifact'
|
||||
|
||||
/**
|
||||
* Page result format for collection artifact infinite queries
|
||||
*/
|
||||
export interface ArtifactPageResult {
|
||||
results: CollectionArtifact[]
|
||||
page: number
|
||||
totalPages: number
|
||||
total: number
|
||||
perPage: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial data structure for collection artifact infinite queries
|
||||
*/
|
||||
export interface ArtifactInitialData {
|
||||
pages: ArtifactPageResult[]
|
||||
pageParams: number[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Artifact query options factory
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createQuery, createInfiniteQuery } from '@tanstack/svelte-query'
|
||||
* import { artifactQueries } from '$lib/api/queries/artifact.queries'
|
||||
*
|
||||
* // All artifact reference data (cached for 1 hour)
|
||||
* const artifacts = createQuery(() => artifactQueries.all())
|
||||
*
|
||||
* // All skills (cached for 1 hour)
|
||||
* const skills = createQuery(() => artifactQueries.skills())
|
||||
*
|
||||
* // Skills for a specific slot
|
||||
* const slot1Skills = createQuery(() => artifactQueries.skillsForSlot(1))
|
||||
*
|
||||
* // User's collection artifacts with infinite scroll
|
||||
* const collection = createInfiniteQuery(() => artifactQueries.collection(userId))
|
||||
* ```
|
||||
*/
|
||||
export const artifactQueries = {
|
||||
/**
|
||||
* All artifact reference data (standard and quirk)
|
||||
* Cached for 1 hour as this data rarely changes
|
||||
*/
|
||||
all: (params?: { rarity?: 'standard' | 'quirk'; proficiency?: number }) =>
|
||||
queryOptions({
|
||||
queryKey: ['artifacts', 'all', params] as const,
|
||||
queryFn: () => artifactAdapter.listArtifacts(params),
|
||||
staleTime: 1000 * 60 * 60, // 1 hour
|
||||
gcTime: 1000 * 60 * 60 * 24 // 24 hours
|
||||
}),
|
||||
|
||||
/**
|
||||
* Single artifact by ID
|
||||
*/
|
||||
byId: (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['artifacts', 'detail', id] as const,
|
||||
queryFn: () => artifactAdapter.getArtifact(id),
|
||||
enabled: !!id,
|
||||
staleTime: 1000 * 60 * 60, // 1 hour
|
||||
gcTime: 1000 * 60 * 60 * 24 // 24 hours
|
||||
}),
|
||||
|
||||
/**
|
||||
* All artifact skills
|
||||
* Cached for 1 hour as this data rarely changes
|
||||
*/
|
||||
skills: () =>
|
||||
queryOptions({
|
||||
queryKey: ['artifacts', 'skills', 'all'] as const,
|
||||
queryFn: () => artifactAdapter.listSkills(),
|
||||
staleTime: 1000 * 60 * 60, // 1 hour
|
||||
gcTime: 1000 * 60 * 60 * 24 // 24 hours
|
||||
}),
|
||||
|
||||
/**
|
||||
* Skills for a specific slot (1-4)
|
||||
* Maps slots to skill groups:
|
||||
* - Slots 1-2: group_i
|
||||
* - Slot 3: group_ii
|
||||
* - Slot 4: group_iii
|
||||
*/
|
||||
skillsForSlot: (slot: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['artifacts', 'skills', 'slot', slot] as const,
|
||||
queryFn: () => artifactAdapter.getSkillsForSlot(slot),
|
||||
enabled: slot >= 1 && slot <= 4,
|
||||
staleTime: 1000 * 60 * 60, // 1 hour
|
||||
gcTime: 1000 * 60 * 60 * 24 // 24 hours
|
||||
}),
|
||||
|
||||
/**
|
||||
* User's collection artifacts with infinite scroll
|
||||
*
|
||||
* @param userId - The user whose collection to fetch
|
||||
* @param filters - Optional filters for element, artifact type, etc.
|
||||
* @param enabled - Whether the query is enabled (default: true)
|
||||
* @param initialData - Optional initial data for SSR hydration
|
||||
*/
|
||||
collection: (
|
||||
userId: string,
|
||||
filters?: Omit<CollectionArtifactListParams, 'page' | 'limit'>,
|
||||
enabled: boolean = true,
|
||||
initialData?: ArtifactInitialData
|
||||
) =>
|
||||
infiniteQueryOptions({
|
||||
queryKey: ['collection', 'artifacts', userId, filters] as const,
|
||||
queryFn: async ({ pageParam }): Promise<ArtifactPageResult> => {
|
||||
const response = await artifactAdapter.listCollectionArtifacts(userId, {
|
||||
...filters,
|
||||
page: pageParam
|
||||
})
|
||||
return {
|
||||
results: response.results,
|
||||
page: pageParam,
|
||||
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
|
||||
enabled: !!userId && enabled,
|
||||
initialData
|
||||
}),
|
||||
|
||||
/**
|
||||
* Single collection artifact by ID
|
||||
*/
|
||||
collectionArtifact: (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['collection', 'artifact', id] as const,
|
||||
queryFn: () => artifactAdapter.getCollectionArtifact(id),
|
||||
enabled: !!id,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 30 // 30 minutes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query key helpers for cache invalidation
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { useQueryClient } from '@tanstack/svelte-query'
|
||||
* import { artifactKeys } from '$lib/api/queries/artifact.queries'
|
||||
*
|
||||
* const queryClient = useQueryClient()
|
||||
*
|
||||
* // Invalidate all artifact-related queries
|
||||
* queryClient.invalidateQueries({ queryKey: artifactKeys.all })
|
||||
*
|
||||
* // Invalidate only collection artifacts for a user
|
||||
* queryClient.invalidateQueries({ queryKey: artifactKeys.collection(userId) })
|
||||
*
|
||||
* // Invalidate reference data
|
||||
* queryClient.invalidateQueries({ queryKey: artifactKeys.reference })
|
||||
* ```
|
||||
*/
|
||||
export const artifactKeys = {
|
||||
/** All artifact-related queries */
|
||||
all: ['artifacts'] as const,
|
||||
|
||||
/** All reference data (artifacts and skills) */
|
||||
reference: ['artifacts', 'all'] as const,
|
||||
|
||||
/** Artifact list with optional filters */
|
||||
list: (params?: { rarity?: 'standard' | 'quirk'; proficiency?: number }) =>
|
||||
['artifacts', 'all', params] as const,
|
||||
|
||||
/** Single artifact by ID */
|
||||
detail: (id: string) => ['artifacts', 'detail', id] as const,
|
||||
|
||||
/** All skills */
|
||||
skills: ['artifacts', 'skills'] as const,
|
||||
|
||||
/** Skills for a specific slot */
|
||||
skillsForSlot: (slot: number) => ['artifacts', 'skills', 'slot', slot] as const,
|
||||
|
||||
/** Collection artifacts base key */
|
||||
collectionBase: ['collection', 'artifacts'] as const,
|
||||
|
||||
/** Collection artifacts for a user (all pages) */
|
||||
collection: (userId?: string) =>
|
||||
userId
|
||||
? (['collection', 'artifacts', userId] as const)
|
||||
: (['collection', 'artifacts'] as const),
|
||||
|
||||
/** Collection artifacts with filters */
|
||||
collectionList: (
|
||||
userId: string,
|
||||
filters?: Omit<CollectionArtifactListParams, 'page' | 'limit'>
|
||||
) => ['collection', 'artifacts', userId, filters] as const,
|
||||
|
||||
/** Single collection artifact */
|
||||
collectionArtifact: (id: string) => ['collection', 'artifact', id] as const
|
||||
}
|
||||
Loading…
Reference in a new issue