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