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:
Justin Edmund 2025-12-03 15:59:19 -08:00
parent 1e708e1064
commit ab1243190b
2 changed files with 446 additions and 0 deletions

View 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)
}))
}

View 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
}