@tanstack/query-db-collection 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"query.cjs","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n/**\n * Configuration options for creating a Query Collection\n * @template TItem - The type of items stored in the collection\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<TItem>[`getKey`]\n /** Schema for validating items */\n schema?: CollectionConfig<TItem>[`schema`]\n sync?: CollectionConfig<TItem>[`sync`]\n startSync?: CollectionConfig<TItem>[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<TItem>\n // TODO type returning { refetch: boolean }\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Basic usage\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & {\n utils: QueryCollectionUtils<TItem, TKey, TInsertInput>\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
1
+ {"version":3,"file":"query.cjs","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n// QueryFn return type inference helper\ntype InferQueryFnOutput<TQueryFn> = TQueryFn extends (\n context: QueryFunctionContext<any>\n) => Promise<Array<infer TItem>>\n ? TItem extends object\n ? TItem\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Type resolution system with priority order (matches electric.ts pattern)\ntype ResolveType<\n TExplicit extends object | unknown = unknown,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn = unknown,\n> = unknown extends TExplicit\n ? [TSchema] extends [never]\n ? InferQueryFnOutput<TQueryFn>\n : InferSchemaOutput<TSchema>\n : TExplicit\n\n/**\n * Configuration options for creating a Query Collection\n * @template TExplicit - The explicit type of items stored in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? TQueryFn\n : (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>\n\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`]\n /** Schema for validating items */\n schema?: TSchema\n sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`]\n startSync?: CollectionConfig<\n ResolveType<TExplicit, TSchema, TQueryFn>\n >[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Explicit type (highest priority)\n * 2. Schema inference (second priority)\n * 3. QueryFn return type inference (third priority)\n * 4. Fallback to Record<string, unknown>\n *\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type (highest priority)\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference (second priority)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>,\n>(\n config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>\n): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {\n utils: QueryCollectionUtils<\n ResolveType<TExplicit, TSchema, TQueryFn>,\n TKey,\n TInsertInput\n >\n} {\n type TItem = ResolveType<TExplicit, TSchema, TQueryFn>\n\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
@@ -1,33 +1,39 @@
1
1
  import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
2
2
  import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
3
+ import { StandardSchemaV1 } from '@standard-schema/spec';
3
4
  export type { SyncOperation } from './manual-sync.cjs';
5
+ type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
6
+ type InferQueryFnOutput<TQueryFn> = TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<infer TItem>> ? TItem extends object ? TItem : Record<string, unknown> : Record<string, unknown>;
7
+ type ResolveType<TExplicit extends object | unknown = unknown, TSchema extends StandardSchemaV1 = never, TQueryFn = unknown> = unknown extends TExplicit ? [TSchema] extends [never] ? InferQueryFnOutput<TQueryFn> : InferSchemaOutput<TSchema> : TExplicit;
4
8
  /**
5
9
  * Configuration options for creating a Query Collection
6
- * @template TItem - The type of items stored in the collection
10
+ * @template TExplicit - The explicit type of items stored in the collection (highest priority)
11
+ * @template TSchema - The schema type for validation and type inference (second priority)
12
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
7
13
  * @template TError - The type of errors that can occur during queries
8
14
  * @template TQueryKey - The type of the query key
9
15
  */
10
- export interface QueryCollectionConfig<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
16
+ export interface QueryCollectionConfig<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
11
17
  /** The query key used by TanStack Query to identify this query */
12
18
  queryKey: TQueryKey;
13
19
  /** Function that fetches data from the server. Must return the complete collection state */
14
- queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>;
20
+ queryFn: TQueryFn extends (context: QueryFunctionContext<TQueryKey>) => Promise<Array<any>> ? TQueryFn : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>;
15
21
  /** The TanStack Query client instance */
16
22
  queryClient: QueryClient;
17
23
  /** Whether the query should automatically run (default: true) */
18
24
  enabled?: boolean;
19
- refetchInterval?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`refetchInterval`];
20
- retry?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retry`];
21
- retryDelay?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retryDelay`];
22
- staleTime?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`staleTime`];
25
+ refetchInterval?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`refetchInterval`];
26
+ retry?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retry`];
27
+ retryDelay?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retryDelay`];
28
+ staleTime?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`staleTime`];
23
29
  /** Unique identifier for the collection */
24
30
  id?: string;
25
31
  /** Function to extract the unique key from an item */
26
- getKey: CollectionConfig<TItem>[`getKey`];
32
+ getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`];
27
33
  /** Schema for validating items */
28
- schema?: CollectionConfig<TItem>[`schema`];
29
- sync?: CollectionConfig<TItem>[`sync`];
30
- startSync?: CollectionConfig<TItem>[`startSync`];
34
+ schema?: TSchema;
35
+ sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`];
36
+ startSync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`startSync`];
31
37
  /**
32
38
  * Optional asynchronous handler function called before an insert operation
33
39
  * @param params Object containing transaction and collection information
@@ -68,7 +74,7 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
68
74
  * }
69
75
  * }
70
76
  */
71
- onInsert?: InsertMutationFn<TItem>;
77
+ onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
72
78
  /**
73
79
  * Optional asynchronous handler function called before an update operation
74
80
  * @param params Object containing transaction and collection information
@@ -120,7 +126,7 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
120
126
  * return { refetch: false } // Skip automatic refetch since we handled it manually
121
127
  * }
122
128
  */
123
- onUpdate?: UpdateMutationFn<TItem>;
129
+ onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
124
130
  /**
125
131
  * Optional asynchronous handler function called before a delete operation
126
132
  * @param params Object containing transaction and collection information
@@ -165,7 +171,7 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
165
171
  * return { refetch: false } // Skip automatic refetch since we handled it manually
166
172
  * }
167
173
  */
168
- onDelete?: DeleteMutationFn<TItem>;
174
+ onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
169
175
  /**
170
176
  * Metadata to pass to the query.
171
177
  * Available in queryFn via context.meta
@@ -217,16 +223,55 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
217
223
  * Creates query collection options for use with a standard Collection.
218
224
  * This integrates TanStack Query with TanStack DB for automatic synchronization.
219
225
  *
226
+ * Supports automatic type inference following the priority order:
227
+ * 1. Explicit type (highest priority)
228
+ * 2. Schema inference (second priority)
229
+ * 3. QueryFn return type inference (third priority)
230
+ * 4. Fallback to Record<string, unknown>
231
+ *
232
+ * @template TExplicit - The explicit type of items in the collection (highest priority)
233
+ * @template TSchema - The schema type for validation and type inference (second priority)
234
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
235
+ * @template TError - The type of errors that can occur during queries
236
+ * @template TQueryKey - The type of the query key
237
+ * @template TKey - The type of the item keys
238
+ * @template TInsertInput - The type accepted for insert operations
220
239
  * @param config - Configuration options for the Query collection
221
240
  * @returns Collection options with utilities for direct writes and manual operations
222
241
  *
223
242
  * @example
224
- * // Basic usage
243
+ * // Type inferred from queryFn return type (NEW!)
244
+ * const todosCollection = createCollection(
245
+ * queryCollectionOptions({
246
+ * queryKey: ['todos'],
247
+ * queryFn: async () => {
248
+ * const response = await fetch('/api/todos')
249
+ * return response.json() as Todo[] // Type automatically inferred!
250
+ * },
251
+ * queryClient,
252
+ * getKey: (item) => item.id, // item is typed as Todo
253
+ * })
254
+ * )
255
+ *
256
+ * @example
257
+ * // Explicit type (highest priority)
258
+ * const todosCollection = createCollection<Todo>(
259
+ * queryCollectionOptions({
260
+ * queryKey: ['todos'],
261
+ * queryFn: async () => fetch('/api/todos').then(r => r.json()),
262
+ * queryClient,
263
+ * getKey: (item) => item.id,
264
+ * })
265
+ * )
266
+ *
267
+ * @example
268
+ * // Schema inference (second priority)
225
269
  * const todosCollection = createCollection(
226
270
  * queryCollectionOptions({
227
271
  * queryKey: ['todos'],
228
272
  * queryFn: async () => fetch('/api/todos').then(r => r.json()),
229
273
  * queryClient,
274
+ * schema: todoSchema, // Type inferred from schema
230
275
  * getKey: (item) => item.id,
231
276
  * })
232
277
  * )
@@ -251,6 +296,6 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
251
296
  * })
252
297
  * )
253
298
  */
254
- export declare function queryCollectionOptions<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = TItem>(config: QueryCollectionConfig<TItem, TError, TQueryKey>): CollectionConfig<TItem> & {
255
- utils: QueryCollectionUtils<TItem, TKey, TInsertInput>;
299
+ export declare function queryCollectionOptions<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>>(config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {
300
+ utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput>;
256
301
  };
@@ -1,33 +1,39 @@
1
1
  import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
2
2
  import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
3
+ import { StandardSchemaV1 } from '@standard-schema/spec';
3
4
  export type { SyncOperation } from './manual-sync.js';
5
+ type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
6
+ type InferQueryFnOutput<TQueryFn> = TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<infer TItem>> ? TItem extends object ? TItem : Record<string, unknown> : Record<string, unknown>;
7
+ type ResolveType<TExplicit extends object | unknown = unknown, TSchema extends StandardSchemaV1 = never, TQueryFn = unknown> = unknown extends TExplicit ? [TSchema] extends [never] ? InferQueryFnOutput<TQueryFn> : InferSchemaOutput<TSchema> : TExplicit;
4
8
  /**
5
9
  * Configuration options for creating a Query Collection
6
- * @template TItem - The type of items stored in the collection
10
+ * @template TExplicit - The explicit type of items stored in the collection (highest priority)
11
+ * @template TSchema - The schema type for validation and type inference (second priority)
12
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
7
13
  * @template TError - The type of errors that can occur during queries
8
14
  * @template TQueryKey - The type of the query key
9
15
  */
10
- export interface QueryCollectionConfig<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
16
+ export interface QueryCollectionConfig<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
11
17
  /** The query key used by TanStack Query to identify this query */
12
18
  queryKey: TQueryKey;
13
19
  /** Function that fetches data from the server. Must return the complete collection state */
14
- queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>;
20
+ queryFn: TQueryFn extends (context: QueryFunctionContext<TQueryKey>) => Promise<Array<any>> ? TQueryFn : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>;
15
21
  /** The TanStack Query client instance */
16
22
  queryClient: QueryClient;
17
23
  /** Whether the query should automatically run (default: true) */
18
24
  enabled?: boolean;
19
- refetchInterval?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`refetchInterval`];
20
- retry?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retry`];
21
- retryDelay?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retryDelay`];
22
- staleTime?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`staleTime`];
25
+ refetchInterval?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`refetchInterval`];
26
+ retry?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retry`];
27
+ retryDelay?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retryDelay`];
28
+ staleTime?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`staleTime`];
23
29
  /** Unique identifier for the collection */
24
30
  id?: string;
25
31
  /** Function to extract the unique key from an item */
26
- getKey: CollectionConfig<TItem>[`getKey`];
32
+ getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`];
27
33
  /** Schema for validating items */
28
- schema?: CollectionConfig<TItem>[`schema`];
29
- sync?: CollectionConfig<TItem>[`sync`];
30
- startSync?: CollectionConfig<TItem>[`startSync`];
34
+ schema?: TSchema;
35
+ sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`];
36
+ startSync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`startSync`];
31
37
  /**
32
38
  * Optional asynchronous handler function called before an insert operation
33
39
  * @param params Object containing transaction and collection information
@@ -68,7 +74,7 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
68
74
  * }
69
75
  * }
70
76
  */
71
- onInsert?: InsertMutationFn<TItem>;
77
+ onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
72
78
  /**
73
79
  * Optional asynchronous handler function called before an update operation
74
80
  * @param params Object containing transaction and collection information
@@ -120,7 +126,7 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
120
126
  * return { refetch: false } // Skip automatic refetch since we handled it manually
121
127
  * }
122
128
  */
123
- onUpdate?: UpdateMutationFn<TItem>;
129
+ onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
124
130
  /**
125
131
  * Optional asynchronous handler function called before a delete operation
126
132
  * @param params Object containing transaction and collection information
@@ -165,7 +171,7 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
165
171
  * return { refetch: false } // Skip automatic refetch since we handled it manually
166
172
  * }
167
173
  */
168
- onDelete?: DeleteMutationFn<TItem>;
174
+ onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
169
175
  /**
170
176
  * Metadata to pass to the query.
171
177
  * Available in queryFn via context.meta
@@ -217,16 +223,55 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
217
223
  * Creates query collection options for use with a standard Collection.
218
224
  * This integrates TanStack Query with TanStack DB for automatic synchronization.
219
225
  *
226
+ * Supports automatic type inference following the priority order:
227
+ * 1. Explicit type (highest priority)
228
+ * 2. Schema inference (second priority)
229
+ * 3. QueryFn return type inference (third priority)
230
+ * 4. Fallback to Record<string, unknown>
231
+ *
232
+ * @template TExplicit - The explicit type of items in the collection (highest priority)
233
+ * @template TSchema - The schema type for validation and type inference (second priority)
234
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
235
+ * @template TError - The type of errors that can occur during queries
236
+ * @template TQueryKey - The type of the query key
237
+ * @template TKey - The type of the item keys
238
+ * @template TInsertInput - The type accepted for insert operations
220
239
  * @param config - Configuration options for the Query collection
221
240
  * @returns Collection options with utilities for direct writes and manual operations
222
241
  *
223
242
  * @example
224
- * // Basic usage
243
+ * // Type inferred from queryFn return type (NEW!)
244
+ * const todosCollection = createCollection(
245
+ * queryCollectionOptions({
246
+ * queryKey: ['todos'],
247
+ * queryFn: async () => {
248
+ * const response = await fetch('/api/todos')
249
+ * return response.json() as Todo[] // Type automatically inferred!
250
+ * },
251
+ * queryClient,
252
+ * getKey: (item) => item.id, // item is typed as Todo
253
+ * })
254
+ * )
255
+ *
256
+ * @example
257
+ * // Explicit type (highest priority)
258
+ * const todosCollection = createCollection<Todo>(
259
+ * queryCollectionOptions({
260
+ * queryKey: ['todos'],
261
+ * queryFn: async () => fetch('/api/todos').then(r => r.json()),
262
+ * queryClient,
263
+ * getKey: (item) => item.id,
264
+ * })
265
+ * )
266
+ *
267
+ * @example
268
+ * // Schema inference (second priority)
225
269
  * const todosCollection = createCollection(
226
270
  * queryCollectionOptions({
227
271
  * queryKey: ['todos'],
228
272
  * queryFn: async () => fetch('/api/todos').then(r => r.json()),
229
273
  * queryClient,
274
+ * schema: todoSchema, // Type inferred from schema
230
275
  * getKey: (item) => item.id,
231
276
  * })
232
277
  * )
@@ -251,6 +296,6 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
251
296
  * })
252
297
  * )
253
298
  */
254
- export declare function queryCollectionOptions<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = TItem>(config: QueryCollectionConfig<TItem, TError, TQueryKey>): CollectionConfig<TItem> & {
255
- utils: QueryCollectionUtils<TItem, TKey, TInsertInput>;
299
+ export declare function queryCollectionOptions<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>>(config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {
300
+ utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput>;
256
301
  };
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n/**\n * Configuration options for creating a Query Collection\n * @template TItem - The type of items stored in the collection\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<TItem>[`getKey`]\n /** Schema for validating items */\n schema?: CollectionConfig<TItem>[`schema`]\n sync?: CollectionConfig<TItem>[`sync`]\n startSync?: CollectionConfig<TItem>[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<TItem>\n // TODO type returning { refetch: boolean }\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Basic usage\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & {\n utils: QueryCollectionUtils<TItem, TKey, TInsertInput>\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"query.js","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n// QueryFn return type inference helper\ntype InferQueryFnOutput<TQueryFn> = TQueryFn extends (\n context: QueryFunctionContext<any>\n) => Promise<Array<infer TItem>>\n ? TItem extends object\n ? TItem\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Type resolution system with priority order (matches electric.ts pattern)\ntype ResolveType<\n TExplicit extends object | unknown = unknown,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn = unknown,\n> = unknown extends TExplicit\n ? [TSchema] extends [never]\n ? InferQueryFnOutput<TQueryFn>\n : InferSchemaOutput<TSchema>\n : TExplicit\n\n/**\n * Configuration options for creating a Query Collection\n * @template TExplicit - The explicit type of items stored in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? TQueryFn\n : (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>\n\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`]\n /** Schema for validating items */\n schema?: TSchema\n sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`]\n startSync?: CollectionConfig<\n ResolveType<TExplicit, TSchema, TQueryFn>\n >[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Explicit type (highest priority)\n * 2. Schema inference (second priority)\n * 3. QueryFn return type inference (third priority)\n * 4. Fallback to Record<string, unknown>\n *\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type (highest priority)\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference (second priority)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>,\n>(\n config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>\n): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {\n utils: QueryCollectionUtils<\n ResolveType<TExplicit, TSchema, TQueryFn>,\n TKey,\n TInsertInput\n >\n} {\n type TItem = ResolveType<TExplicit, TSchema, TQueryFn>\n\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@tanstack/query-db-collection",
3
3
  "description": "TanStack Query collection for TanStack DB",
4
- "version": "0.2.3",
4
+ "version": "0.2.4",
5
5
  "dependencies": {
6
- "@tanstack/db": "0.1.4"
6
+ "@standard-schema/spec": "^1.0.0",
7
+ "@tanstack/db": "0.1.5"
7
8
  },
8
9
  "devDependencies": {
9
10
  "@tanstack/query-core": "^5.0.5",
package/src/query.ts CHANGED
@@ -24,25 +24,68 @@ import type {
24
24
  UpdateMutationFnParams,
25
25
  UtilsRecord,
26
26
  } from "@tanstack/db"
27
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
27
28
 
28
29
  // Re-export for external use
29
30
  export type { SyncOperation } from "./manual-sync"
30
31
 
32
+ // Schema output type inference helper (matches electric.ts pattern)
33
+ type InferSchemaOutput<T> = T extends StandardSchemaV1
34
+ ? StandardSchemaV1.InferOutput<T> extends object
35
+ ? StandardSchemaV1.InferOutput<T>
36
+ : Record<string, unknown>
37
+ : Record<string, unknown>
38
+
39
+ // QueryFn return type inference helper
40
+ type InferQueryFnOutput<TQueryFn> = TQueryFn extends (
41
+ context: QueryFunctionContext<any>
42
+ ) => Promise<Array<infer TItem>>
43
+ ? TItem extends object
44
+ ? TItem
45
+ : Record<string, unknown>
46
+ : Record<string, unknown>
47
+
48
+ // Type resolution system with priority order (matches electric.ts pattern)
49
+ type ResolveType<
50
+ TExplicit extends object | unknown = unknown,
51
+ TSchema extends StandardSchemaV1 = never,
52
+ TQueryFn = unknown,
53
+ > = unknown extends TExplicit
54
+ ? [TSchema] extends [never]
55
+ ? InferQueryFnOutput<TQueryFn>
56
+ : InferSchemaOutput<TSchema>
57
+ : TExplicit
58
+
31
59
  /**
32
60
  * Configuration options for creating a Query Collection
33
- * @template TItem - The type of items stored in the collection
61
+ * @template TExplicit - The explicit type of items stored in the collection (highest priority)
62
+ * @template TSchema - The schema type for validation and type inference (second priority)
63
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
34
64
  * @template TError - The type of errors that can occur during queries
35
65
  * @template TQueryKey - The type of the query key
36
66
  */
37
67
  export interface QueryCollectionConfig<
38
- TItem extends object,
68
+ TExplicit extends object = object,
69
+ TSchema extends StandardSchemaV1 = never,
70
+ TQueryFn extends (
71
+ context: QueryFunctionContext<any>
72
+ ) => Promise<Array<any>> = (
73
+ context: QueryFunctionContext<any>
74
+ ) => Promise<Array<any>>,
39
75
  TError = unknown,
40
76
  TQueryKey extends QueryKey = QueryKey,
41
77
  > {
42
78
  /** The query key used by TanStack Query to identify this query */
43
79
  queryKey: TQueryKey
44
80
  /** Function that fetches data from the server. Must return the complete collection state */
45
- queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>
81
+ queryFn: TQueryFn extends (
82
+ context: QueryFunctionContext<TQueryKey>
83
+ ) => Promise<Array<any>>
84
+ ? TQueryFn
85
+ : (
86
+ context: QueryFunctionContext<TQueryKey>
87
+ ) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>
88
+
46
89
  /** The TanStack Query client instance */
47
90
  queryClient: QueryClient
48
91
 
@@ -50,31 +93,31 @@ export interface QueryCollectionConfig<
50
93
  /** Whether the query should automatically run (default: true) */
51
94
  enabled?: boolean
52
95
  refetchInterval?: QueryObserverOptions<
53
- Array<TItem>,
96
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
54
97
  TError,
55
- Array<TItem>,
56
- Array<TItem>,
98
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
99
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
57
100
  TQueryKey
58
101
  >[`refetchInterval`]
59
102
  retry?: QueryObserverOptions<
60
- Array<TItem>,
103
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
61
104
  TError,
62
- Array<TItem>,
63
- Array<TItem>,
105
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
106
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
64
107
  TQueryKey
65
108
  >[`retry`]
66
109
  retryDelay?: QueryObserverOptions<
67
- Array<TItem>,
110
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
68
111
  TError,
69
- Array<TItem>,
70
- Array<TItem>,
112
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
113
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
71
114
  TQueryKey
72
115
  >[`retryDelay`]
73
116
  staleTime?: QueryObserverOptions<
74
- Array<TItem>,
117
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
75
118
  TError,
76
- Array<TItem>,
77
- Array<TItem>,
119
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
120
+ Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
78
121
  TQueryKey
79
122
  >[`staleTime`]
80
123
 
@@ -82,11 +125,13 @@ export interface QueryCollectionConfig<
82
125
  /** Unique identifier for the collection */
83
126
  id?: string
84
127
  /** Function to extract the unique key from an item */
85
- getKey: CollectionConfig<TItem>[`getKey`]
128
+ getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`]
86
129
  /** Schema for validating items */
87
- schema?: CollectionConfig<TItem>[`schema`]
88
- sync?: CollectionConfig<TItem>[`sync`]
89
- startSync?: CollectionConfig<TItem>[`startSync`]
130
+ schema?: TSchema
131
+ sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`]
132
+ startSync?: CollectionConfig<
133
+ ResolveType<TExplicit, TSchema, TQueryFn>
134
+ >[`startSync`]
90
135
 
91
136
  // Direct persistence handlers
92
137
  /**
@@ -129,7 +174,7 @@ export interface QueryCollectionConfig<
129
174
  * }
130
175
  * }
131
176
  */
132
- onInsert?: InsertMutationFn<TItem>
177
+ onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>
133
178
 
134
179
  /**
135
180
  * Optional asynchronous handler function called before an update operation
@@ -182,7 +227,7 @@ export interface QueryCollectionConfig<
182
227
  * return { refetch: false } // Skip automatic refetch since we handled it manually
183
228
  * }
184
229
  */
185
- onUpdate?: UpdateMutationFn<TItem>
230
+ onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>
186
231
 
187
232
  /**
188
233
  * Optional asynchronous handler function called before a delete operation
@@ -228,8 +273,7 @@ export interface QueryCollectionConfig<
228
273
  * return { refetch: false } // Skip automatic refetch since we handled it manually
229
274
  * }
230
275
  */
231
- onDelete?: DeleteMutationFn<TItem>
232
- // TODO type returning { refetch: boolean }
276
+ onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>
233
277
 
234
278
  /**
235
279
  * Metadata to pass to the query.
@@ -289,16 +333,55 @@ export interface QueryCollectionUtils<
289
333
  * Creates query collection options for use with a standard Collection.
290
334
  * This integrates TanStack Query with TanStack DB for automatic synchronization.
291
335
  *
336
+ * Supports automatic type inference following the priority order:
337
+ * 1. Explicit type (highest priority)
338
+ * 2. Schema inference (second priority)
339
+ * 3. QueryFn return type inference (third priority)
340
+ * 4. Fallback to Record<string, unknown>
341
+ *
342
+ * @template TExplicit - The explicit type of items in the collection (highest priority)
343
+ * @template TSchema - The schema type for validation and type inference (second priority)
344
+ * @template TQueryFn - The queryFn type for inferring return type (third priority)
345
+ * @template TError - The type of errors that can occur during queries
346
+ * @template TQueryKey - The type of the query key
347
+ * @template TKey - The type of the item keys
348
+ * @template TInsertInput - The type accepted for insert operations
292
349
  * @param config - Configuration options for the Query collection
293
350
  * @returns Collection options with utilities for direct writes and manual operations
294
351
  *
295
352
  * @example
296
- * // Basic usage
353
+ * // Type inferred from queryFn return type (NEW!)
354
+ * const todosCollection = createCollection(
355
+ * queryCollectionOptions({
356
+ * queryKey: ['todos'],
357
+ * queryFn: async () => {
358
+ * const response = await fetch('/api/todos')
359
+ * return response.json() as Todo[] // Type automatically inferred!
360
+ * },
361
+ * queryClient,
362
+ * getKey: (item) => item.id, // item is typed as Todo
363
+ * })
364
+ * )
365
+ *
366
+ * @example
367
+ * // Explicit type (highest priority)
368
+ * const todosCollection = createCollection<Todo>(
369
+ * queryCollectionOptions({
370
+ * queryKey: ['todos'],
371
+ * queryFn: async () => fetch('/api/todos').then(r => r.json()),
372
+ * queryClient,
373
+ * getKey: (item) => item.id,
374
+ * })
375
+ * )
376
+ *
377
+ * @example
378
+ * // Schema inference (second priority)
297
379
  * const todosCollection = createCollection(
298
380
  * queryCollectionOptions({
299
381
  * queryKey: ['todos'],
300
382
  * queryFn: async () => fetch('/api/todos').then(r => r.json()),
301
383
  * queryClient,
384
+ * schema: todoSchema, // Type inferred from schema
302
385
  * getKey: (item) => item.id,
303
386
  * })
304
387
  * )
@@ -324,16 +407,28 @@ export interface QueryCollectionUtils<
324
407
  * )
325
408
  */
326
409
  export function queryCollectionOptions<
327
- TItem extends object,
410
+ TExplicit extends object = object,
411
+ TSchema extends StandardSchemaV1 = never,
412
+ TQueryFn extends (
413
+ context: QueryFunctionContext<any>
414
+ ) => Promise<Array<any>> = (
415
+ context: QueryFunctionContext<any>
416
+ ) => Promise<Array<any>>,
328
417
  TError = unknown,
329
418
  TQueryKey extends QueryKey = QueryKey,
330
419
  TKey extends string | number = string | number,
331
- TInsertInput extends object = TItem,
420
+ TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>,
332
421
  >(
333
- config: QueryCollectionConfig<TItem, TError, TQueryKey>
334
- ): CollectionConfig<TItem> & {
335
- utils: QueryCollectionUtils<TItem, TKey, TInsertInput>
422
+ config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>
423
+ ): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {
424
+ utils: QueryCollectionUtils<
425
+ ResolveType<TExplicit, TSchema, TQueryFn>,
426
+ TKey,
427
+ TInsertInput
428
+ >
336
429
  } {
430
+ type TItem = ResolveType<TExplicit, TSchema, TQueryFn>
431
+
337
432
  const {
338
433
  queryKey,
339
434
  queryFn,