From ab1243190bbd5eaf73c86925202b932b4db9d07d Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 3 Dec 2025 15:59:19 -0800 Subject: [PATCH] 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 --- src/lib/api/mutations/artifact.mutations.ts | 227 ++++++++++++++++++++ src/lib/api/queries/artifact.queries.ts | 219 +++++++++++++++++++ 2 files changed, 446 insertions(+) create mode 100644 src/lib/api/mutations/artifact.mutations.ts create mode 100644 src/lib/api/queries/artifact.queries.ts diff --git a/src/lib/api/mutations/artifact.mutations.ts b/src/lib/api/mutations/artifact.mutations.ts new file mode 100644 index 00000000..b7f6ed9c --- /dev/null +++ b/src/lib/api/mutations/artifact.mutations.ts @@ -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 + * + * ``` + */ +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 }) => + 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( + 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) + })) +} diff --git a/src/lib/api/queries/artifact.queries.ts b/src/lib/api/queries/artifact.queries.ts new file mode 100644 index 00000000..471d9457 --- /dev/null +++ b/src/lib/api/queries/artifact.queries.ts @@ -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, + enabled: boolean = true, + initialData?: ArtifactInitialData + ) => + infiniteQueryOptions({ + queryKey: ['collection', 'artifacts', userId, filters] as const, + queryFn: async ({ pageParam }): Promise => { + 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 + ) => ['collection', 'artifacts', userId, filters] as const, + + /** Single collection artifact */ + collectionArtifact: (id: string) => ['collection', 'artifact', id] as const +}