@tanstack/query-db-collection 0.0.2 → 0.0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kyle Mathews
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const query = require("./query.cjs");
4
+ exports.queryCollectionOptions = query.queryCollectionOptions;
5
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1 @@
1
+ export { queryCollectionOptions, type QueryCollectionConfig, type QueryCollectionUtils, } from './query.cjs';
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const queryCore = require("@tanstack/query-core");
4
+ function queryCollectionOptions(config) {
5
+ const {
6
+ queryKey,
7
+ queryFn,
8
+ queryClient,
9
+ enabled,
10
+ refetchInterval,
11
+ retry,
12
+ retryDelay,
13
+ staleTime,
14
+ getKey,
15
+ onInsert,
16
+ onUpdate,
17
+ onDelete,
18
+ ...baseCollectionConfig
19
+ } = config;
20
+ if (!queryKey) {
21
+ throw new Error(`[QueryCollection] queryKey must be provided.`);
22
+ }
23
+ if (!queryFn) {
24
+ throw new Error(`[QueryCollection] queryFn must be provided.`);
25
+ }
26
+ if (!queryClient) {
27
+ throw new Error(`[QueryCollection] queryClient must be provided.`);
28
+ }
29
+ if (!getKey) {
30
+ throw new Error(`[QueryCollection] getKey must be provided.`);
31
+ }
32
+ const internalSync = (params) => {
33
+ const { begin, write, commit, collection } = params;
34
+ const observerOptions = {
35
+ queryKey,
36
+ queryFn,
37
+ enabled,
38
+ refetchInterval,
39
+ retry,
40
+ retryDelay,
41
+ staleTime,
42
+ structuralSharing: true,
43
+ notifyOnChangeProps: `all`
44
+ };
45
+ const localObserver = new queryCore.QueryObserver(queryClient, observerOptions);
46
+ const actualUnsubscribeFn = localObserver.subscribe((result) => {
47
+ if (result.isSuccess) {
48
+ const newItemsArray = result.data;
49
+ if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
50
+ console.error(
51
+ `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,
52
+ newItemsArray
53
+ );
54
+ return;
55
+ }
56
+ const currentSyncedItems = new Map(collection.syncedData);
57
+ const newItemsMap = /* @__PURE__ */ new Map();
58
+ newItemsArray.forEach((item) => {
59
+ const key = getKey(item);
60
+ newItemsMap.set(key, item);
61
+ });
62
+ begin();
63
+ const shallowEqual = (obj1, obj2) => {
64
+ const keys1 = Object.keys(obj1);
65
+ const keys2 = Object.keys(obj2);
66
+ if (keys1.length !== keys2.length) return false;
67
+ return keys1.every((key) => {
68
+ if (typeof obj1[key] === `function`) return true;
69
+ if (typeof obj1[key] === `object` && obj1[key] !== null) {
70
+ return obj1[key] === obj2[key];
71
+ }
72
+ return obj1[key] === obj2[key];
73
+ });
74
+ };
75
+ currentSyncedItems.forEach((oldItem, key) => {
76
+ const newItem = newItemsMap.get(key);
77
+ if (!newItem) {
78
+ write({ type: `delete`, value: oldItem });
79
+ } else if (!shallowEqual(
80
+ oldItem,
81
+ newItem
82
+ )) {
83
+ write({ type: `update`, value: newItem });
84
+ }
85
+ });
86
+ newItemsMap.forEach((newItem, key) => {
87
+ if (!currentSyncedItems.has(key)) {
88
+ write({ type: `insert`, value: newItem });
89
+ }
90
+ });
91
+ commit();
92
+ } else if (result.isError) {
93
+ console.error(
94
+ `[QueryCollection] Error observing query ${String(queryKey)}:`,
95
+ result.error
96
+ );
97
+ }
98
+ });
99
+ return async () => {
100
+ actualUnsubscribeFn();
101
+ await queryClient.cancelQueries({ queryKey });
102
+ queryClient.removeQueries({ queryKey });
103
+ };
104
+ };
105
+ const refetch = async () => {
106
+ return queryClient.refetchQueries({
107
+ queryKey
108
+ });
109
+ };
110
+ const wrappedOnInsert = onInsert ? async (params) => {
111
+ const handlerResult = await onInsert(params) ?? {};
112
+ const shouldRefetch = handlerResult.refetch !== false;
113
+ if (shouldRefetch) {
114
+ await refetch();
115
+ }
116
+ return handlerResult;
117
+ } : void 0;
118
+ const wrappedOnUpdate = onUpdate ? async (params) => {
119
+ const handlerResult = await onUpdate(params) ?? {};
120
+ const shouldRefetch = handlerResult.refetch !== false;
121
+ if (shouldRefetch) {
122
+ await refetch();
123
+ }
124
+ return handlerResult;
125
+ } : void 0;
126
+ const wrappedOnDelete = onDelete ? async (params) => {
127
+ const handlerResult = await onDelete(params) ?? {};
128
+ const shouldRefetch = handlerResult.refetch !== false;
129
+ if (shouldRefetch) {
130
+ await refetch();
131
+ }
132
+ return handlerResult;
133
+ } : void 0;
134
+ return {
135
+ ...baseCollectionConfig,
136
+ getKey,
137
+ sync: { sync: internalSync },
138
+ onInsert: wrappedOnInsert,
139
+ onUpdate: wrappedOnUpdate,
140
+ onDelete: wrappedOnDelete,
141
+ utils: {
142
+ refetch
143
+ }
144
+ };
145
+ }
146
+ exports.queryCollectionOptions = queryCollectionOptions;
147
+ //# sourceMappingURL=query.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.cjs","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n queryKey: TQueryKey\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\n queryClient: QueryClient\n\n // Query-specific options\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 id?: string\n getKey: CollectionConfig<TItem>[`getKey`]\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/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Query collection utilities type\n */\nexport interface QueryCollectionUtils extends UtilsRecord {\n refetch: RefetchFn\n}\n\n/**\n * Creates query collection options for use with a standard Collection\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & { utils: QueryCollectionUtils } {\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 ...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 Error(`[QueryCollection] queryKey must be provided.`)\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new Error(`[QueryCollection] queryFn must be provided.`)\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new Error(`[QueryCollection] queryClient must be provided.`)\n }\n\n if (!getKey) {\n throw new Error(`[QueryCollection] getKey must be provided.`)\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, 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 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 } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\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 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: internalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n },\n }\n}\n"],"names":["QueryObserver"],"mappings":"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"}
@@ -0,0 +1,174 @@
1
+ import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
2
+ import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
3
+ export interface QueryCollectionConfig<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
4
+ queryKey: TQueryKey;
5
+ queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>;
6
+ queryClient: QueryClient;
7
+ enabled?: boolean;
8
+ refetchInterval?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`refetchInterval`];
9
+ retry?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retry`];
10
+ retryDelay?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retryDelay`];
11
+ staleTime?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`staleTime`];
12
+ id?: string;
13
+ getKey: CollectionConfig<TItem>[`getKey`];
14
+ schema?: CollectionConfig<TItem>[`schema`];
15
+ sync?: CollectionConfig<TItem>[`sync`];
16
+ startSync?: CollectionConfig<TItem>[`startSync`];
17
+ /**
18
+ * Optional asynchronous handler function called before an insert operation
19
+ * @param params Object containing transaction and collection information
20
+ * @returns Promise resolving to void or { refetch?: boolean } to control refetching
21
+ * @example
22
+ * // Basic query collection insert handler
23
+ * onInsert: async ({ transaction }) => {
24
+ * const newItem = transaction.mutations[0].modified
25
+ * await api.createTodo(newItem)
26
+ * // Automatically refetches query after insert
27
+ * }
28
+ *
29
+ * @example
30
+ * // Insert handler with refetch control
31
+ * onInsert: async ({ transaction }) => {
32
+ * const newItem = transaction.mutations[0].modified
33
+ * await api.createTodo(newItem)
34
+ * return { refetch: false } // Skip automatic refetch
35
+ * }
36
+ *
37
+ * @example
38
+ * // Insert handler with multiple items
39
+ * onInsert: async ({ transaction }) => {
40
+ * const items = transaction.mutations.map(m => m.modified)
41
+ * await api.createTodos(items)
42
+ * // Will refetch query to get updated data
43
+ * }
44
+ *
45
+ * @example
46
+ * // Insert handler with error handling
47
+ * onInsert: async ({ transaction }) => {
48
+ * try {
49
+ * const newItem = transaction.mutations[0].modified
50
+ * await api.createTodo(newItem)
51
+ * } catch (error) {
52
+ * console.error('Insert failed:', error)
53
+ * throw error // Transaction will rollback optimistic changes
54
+ * }
55
+ * }
56
+ */
57
+ onInsert?: InsertMutationFn<TItem>;
58
+ /**
59
+ * Optional asynchronous handler function called before an update operation
60
+ * @param params Object containing transaction and collection information
61
+ * @returns Promise resolving to void or { refetch?: boolean } to control refetching
62
+ * @example
63
+ * // Basic query collection update handler
64
+ * onUpdate: async ({ transaction }) => {
65
+ * const mutation = transaction.mutations[0]
66
+ * await api.updateTodo(mutation.original.id, mutation.changes)
67
+ * // Automatically refetches query after update
68
+ * }
69
+ *
70
+ * @example
71
+ * // Update handler with multiple items
72
+ * onUpdate: async ({ transaction }) => {
73
+ * const updates = transaction.mutations.map(m => ({
74
+ * id: m.key,
75
+ * changes: m.changes
76
+ * }))
77
+ * await api.updateTodos(updates)
78
+ * // Will refetch query to get updated data
79
+ * }
80
+ *
81
+ * @example
82
+ * // Update handler with manual refetch
83
+ * onUpdate: async ({ transaction, collection }) => {
84
+ * const mutation = transaction.mutations[0]
85
+ * await api.updateTodo(mutation.original.id, mutation.changes)
86
+ *
87
+ * // Manually trigger refetch
88
+ * await collection.utils.refetch()
89
+ *
90
+ * return { refetch: false } // Skip automatic refetch
91
+ * }
92
+ *
93
+ * @example
94
+ * // Update handler with related collection refetch
95
+ * onUpdate: async ({ transaction, collection }) => {
96
+ * const mutation = transaction.mutations[0]
97
+ * await api.updateTodo(mutation.original.id, mutation.changes)
98
+ *
99
+ * // Refetch related collections when this item changes
100
+ * await Promise.all([
101
+ * collection.utils.refetch(), // Refetch this collection
102
+ * usersCollection.utils.refetch(), // Refetch users
103
+ * tagsCollection.utils.refetch() // Refetch tags
104
+ * ])
105
+ *
106
+ * return { refetch: false } // Skip automatic refetch since we handled it manually
107
+ * }
108
+ */
109
+ onUpdate?: UpdateMutationFn<TItem>;
110
+ /**
111
+ * Optional asynchronous handler function called before a delete operation
112
+ * @param params Object containing transaction and collection information
113
+ * @returns Promise resolving to void or { refetch?: boolean } to control refetching
114
+ * @example
115
+ * // Basic query collection delete handler
116
+ * onDelete: async ({ transaction }) => {
117
+ * const mutation = transaction.mutations[0]
118
+ * await api.deleteTodo(mutation.original.id)
119
+ * // Automatically refetches query after delete
120
+ * }
121
+ *
122
+ * @example
123
+ * // Delete handler with refetch control
124
+ * onDelete: async ({ transaction }) => {
125
+ * const mutation = transaction.mutations[0]
126
+ * await api.deleteTodo(mutation.original.id)
127
+ * return { refetch: false } // Skip automatic refetch
128
+ * }
129
+ *
130
+ * @example
131
+ * // Delete handler with multiple items
132
+ * onDelete: async ({ transaction }) => {
133
+ * const keysToDelete = transaction.mutations.map(m => m.key)
134
+ * await api.deleteTodos(keysToDelete)
135
+ * // Will refetch query to get updated data
136
+ * }
137
+ *
138
+ * @example
139
+ * // Delete handler with related collection refetch
140
+ * onDelete: async ({ transaction, collection }) => {
141
+ * const mutation = transaction.mutations[0]
142
+ * await api.deleteTodo(mutation.original.id)
143
+ *
144
+ * // Refetch related collections when this item is deleted
145
+ * await Promise.all([
146
+ * collection.utils.refetch(), // Refetch this collection
147
+ * usersCollection.utils.refetch(), // Refetch users
148
+ * projectsCollection.utils.refetch() // Refetch projects
149
+ * ])
150
+ *
151
+ * return { refetch: false } // Skip automatic refetch since we handled it manually
152
+ * }
153
+ */
154
+ onDelete?: DeleteMutationFn<TItem>;
155
+ }
156
+ /**
157
+ * Type for the refetch utility function
158
+ */
159
+ export type RefetchFn = () => Promise<void>;
160
+ /**
161
+ * Query collection utilities type
162
+ */
163
+ export interface QueryCollectionUtils extends UtilsRecord {
164
+ refetch: RefetchFn;
165
+ }
166
+ /**
167
+ * Creates query collection options for use with a standard Collection
168
+ *
169
+ * @param config - Configuration options for the Query collection
170
+ * @returns Collection options with utilities
171
+ */
172
+ export declare function queryCollectionOptions<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey>(config: QueryCollectionConfig<TItem, TError, TQueryKey>): CollectionConfig<TItem> & {
173
+ utils: QueryCollectionUtils;
174
+ };
@@ -0,0 +1 @@
1
+ export { queryCollectionOptions, type QueryCollectionConfig, type QueryCollectionUtils, } from './query.js';
@@ -0,0 +1,5 @@
1
+ import { queryCollectionOptions } from "./query.js";
2
+ export {
3
+ queryCollectionOptions
4
+ };
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -0,0 +1,174 @@
1
+ import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
2
+ import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
3
+ export interface QueryCollectionConfig<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
4
+ queryKey: TQueryKey;
5
+ queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>;
6
+ queryClient: QueryClient;
7
+ enabled?: boolean;
8
+ refetchInterval?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`refetchInterval`];
9
+ retry?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retry`];
10
+ retryDelay?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retryDelay`];
11
+ staleTime?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`staleTime`];
12
+ id?: string;
13
+ getKey: CollectionConfig<TItem>[`getKey`];
14
+ schema?: CollectionConfig<TItem>[`schema`];
15
+ sync?: CollectionConfig<TItem>[`sync`];
16
+ startSync?: CollectionConfig<TItem>[`startSync`];
17
+ /**
18
+ * Optional asynchronous handler function called before an insert operation
19
+ * @param params Object containing transaction and collection information
20
+ * @returns Promise resolving to void or { refetch?: boolean } to control refetching
21
+ * @example
22
+ * // Basic query collection insert handler
23
+ * onInsert: async ({ transaction }) => {
24
+ * const newItem = transaction.mutations[0].modified
25
+ * await api.createTodo(newItem)
26
+ * // Automatically refetches query after insert
27
+ * }
28
+ *
29
+ * @example
30
+ * // Insert handler with refetch control
31
+ * onInsert: async ({ transaction }) => {
32
+ * const newItem = transaction.mutations[0].modified
33
+ * await api.createTodo(newItem)
34
+ * return { refetch: false } // Skip automatic refetch
35
+ * }
36
+ *
37
+ * @example
38
+ * // Insert handler with multiple items
39
+ * onInsert: async ({ transaction }) => {
40
+ * const items = transaction.mutations.map(m => m.modified)
41
+ * await api.createTodos(items)
42
+ * // Will refetch query to get updated data
43
+ * }
44
+ *
45
+ * @example
46
+ * // Insert handler with error handling
47
+ * onInsert: async ({ transaction }) => {
48
+ * try {
49
+ * const newItem = transaction.mutations[0].modified
50
+ * await api.createTodo(newItem)
51
+ * } catch (error) {
52
+ * console.error('Insert failed:', error)
53
+ * throw error // Transaction will rollback optimistic changes
54
+ * }
55
+ * }
56
+ */
57
+ onInsert?: InsertMutationFn<TItem>;
58
+ /**
59
+ * Optional asynchronous handler function called before an update operation
60
+ * @param params Object containing transaction and collection information
61
+ * @returns Promise resolving to void or { refetch?: boolean } to control refetching
62
+ * @example
63
+ * // Basic query collection update handler
64
+ * onUpdate: async ({ transaction }) => {
65
+ * const mutation = transaction.mutations[0]
66
+ * await api.updateTodo(mutation.original.id, mutation.changes)
67
+ * // Automatically refetches query after update
68
+ * }
69
+ *
70
+ * @example
71
+ * // Update handler with multiple items
72
+ * onUpdate: async ({ transaction }) => {
73
+ * const updates = transaction.mutations.map(m => ({
74
+ * id: m.key,
75
+ * changes: m.changes
76
+ * }))
77
+ * await api.updateTodos(updates)
78
+ * // Will refetch query to get updated data
79
+ * }
80
+ *
81
+ * @example
82
+ * // Update handler with manual refetch
83
+ * onUpdate: async ({ transaction, collection }) => {
84
+ * const mutation = transaction.mutations[0]
85
+ * await api.updateTodo(mutation.original.id, mutation.changes)
86
+ *
87
+ * // Manually trigger refetch
88
+ * await collection.utils.refetch()
89
+ *
90
+ * return { refetch: false } // Skip automatic refetch
91
+ * }
92
+ *
93
+ * @example
94
+ * // Update handler with related collection refetch
95
+ * onUpdate: async ({ transaction, collection }) => {
96
+ * const mutation = transaction.mutations[0]
97
+ * await api.updateTodo(mutation.original.id, mutation.changes)
98
+ *
99
+ * // Refetch related collections when this item changes
100
+ * await Promise.all([
101
+ * collection.utils.refetch(), // Refetch this collection
102
+ * usersCollection.utils.refetch(), // Refetch users
103
+ * tagsCollection.utils.refetch() // Refetch tags
104
+ * ])
105
+ *
106
+ * return { refetch: false } // Skip automatic refetch since we handled it manually
107
+ * }
108
+ */
109
+ onUpdate?: UpdateMutationFn<TItem>;
110
+ /**
111
+ * Optional asynchronous handler function called before a delete operation
112
+ * @param params Object containing transaction and collection information
113
+ * @returns Promise resolving to void or { refetch?: boolean } to control refetching
114
+ * @example
115
+ * // Basic query collection delete handler
116
+ * onDelete: async ({ transaction }) => {
117
+ * const mutation = transaction.mutations[0]
118
+ * await api.deleteTodo(mutation.original.id)
119
+ * // Automatically refetches query after delete
120
+ * }
121
+ *
122
+ * @example
123
+ * // Delete handler with refetch control
124
+ * onDelete: async ({ transaction }) => {
125
+ * const mutation = transaction.mutations[0]
126
+ * await api.deleteTodo(mutation.original.id)
127
+ * return { refetch: false } // Skip automatic refetch
128
+ * }
129
+ *
130
+ * @example
131
+ * // Delete handler with multiple items
132
+ * onDelete: async ({ transaction }) => {
133
+ * const keysToDelete = transaction.mutations.map(m => m.key)
134
+ * await api.deleteTodos(keysToDelete)
135
+ * // Will refetch query to get updated data
136
+ * }
137
+ *
138
+ * @example
139
+ * // Delete handler with related collection refetch
140
+ * onDelete: async ({ transaction, collection }) => {
141
+ * const mutation = transaction.mutations[0]
142
+ * await api.deleteTodo(mutation.original.id)
143
+ *
144
+ * // Refetch related collections when this item is deleted
145
+ * await Promise.all([
146
+ * collection.utils.refetch(), // Refetch this collection
147
+ * usersCollection.utils.refetch(), // Refetch users
148
+ * projectsCollection.utils.refetch() // Refetch projects
149
+ * ])
150
+ *
151
+ * return { refetch: false } // Skip automatic refetch since we handled it manually
152
+ * }
153
+ */
154
+ onDelete?: DeleteMutationFn<TItem>;
155
+ }
156
+ /**
157
+ * Type for the refetch utility function
158
+ */
159
+ export type RefetchFn = () => Promise<void>;
160
+ /**
161
+ * Query collection utilities type
162
+ */
163
+ export interface QueryCollectionUtils extends UtilsRecord {
164
+ refetch: RefetchFn;
165
+ }
166
+ /**
167
+ * Creates query collection options for use with a standard Collection
168
+ *
169
+ * @param config - Configuration options for the Query collection
170
+ * @returns Collection options with utilities
171
+ */
172
+ export declare function queryCollectionOptions<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey>(config: QueryCollectionConfig<TItem, TError, TQueryKey>): CollectionConfig<TItem> & {
173
+ utils: QueryCollectionUtils;
174
+ };
@@ -0,0 +1,147 @@
1
+ import { QueryObserver } from "@tanstack/query-core";
2
+ function queryCollectionOptions(config) {
3
+ const {
4
+ queryKey,
5
+ queryFn,
6
+ queryClient,
7
+ enabled,
8
+ refetchInterval,
9
+ retry,
10
+ retryDelay,
11
+ staleTime,
12
+ getKey,
13
+ onInsert,
14
+ onUpdate,
15
+ onDelete,
16
+ ...baseCollectionConfig
17
+ } = config;
18
+ if (!queryKey) {
19
+ throw new Error(`[QueryCollection] queryKey must be provided.`);
20
+ }
21
+ if (!queryFn) {
22
+ throw new Error(`[QueryCollection] queryFn must be provided.`);
23
+ }
24
+ if (!queryClient) {
25
+ throw new Error(`[QueryCollection] queryClient must be provided.`);
26
+ }
27
+ if (!getKey) {
28
+ throw new Error(`[QueryCollection] getKey must be provided.`);
29
+ }
30
+ const internalSync = (params) => {
31
+ const { begin, write, commit, collection } = params;
32
+ const observerOptions = {
33
+ queryKey,
34
+ queryFn,
35
+ enabled,
36
+ refetchInterval,
37
+ retry,
38
+ retryDelay,
39
+ staleTime,
40
+ structuralSharing: true,
41
+ notifyOnChangeProps: `all`
42
+ };
43
+ const localObserver = new QueryObserver(queryClient, observerOptions);
44
+ const actualUnsubscribeFn = localObserver.subscribe((result) => {
45
+ if (result.isSuccess) {
46
+ const newItemsArray = result.data;
47
+ if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
48
+ console.error(
49
+ `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,
50
+ newItemsArray
51
+ );
52
+ return;
53
+ }
54
+ const currentSyncedItems = new Map(collection.syncedData);
55
+ const newItemsMap = /* @__PURE__ */ new Map();
56
+ newItemsArray.forEach((item) => {
57
+ const key = getKey(item);
58
+ newItemsMap.set(key, item);
59
+ });
60
+ begin();
61
+ const shallowEqual = (obj1, obj2) => {
62
+ const keys1 = Object.keys(obj1);
63
+ const keys2 = Object.keys(obj2);
64
+ if (keys1.length !== keys2.length) return false;
65
+ return keys1.every((key) => {
66
+ if (typeof obj1[key] === `function`) return true;
67
+ if (typeof obj1[key] === `object` && obj1[key] !== null) {
68
+ return obj1[key] === obj2[key];
69
+ }
70
+ return obj1[key] === obj2[key];
71
+ });
72
+ };
73
+ currentSyncedItems.forEach((oldItem, key) => {
74
+ const newItem = newItemsMap.get(key);
75
+ if (!newItem) {
76
+ write({ type: `delete`, value: oldItem });
77
+ } else if (!shallowEqual(
78
+ oldItem,
79
+ newItem
80
+ )) {
81
+ write({ type: `update`, value: newItem });
82
+ }
83
+ });
84
+ newItemsMap.forEach((newItem, key) => {
85
+ if (!currentSyncedItems.has(key)) {
86
+ write({ type: `insert`, value: newItem });
87
+ }
88
+ });
89
+ commit();
90
+ } else if (result.isError) {
91
+ console.error(
92
+ `[QueryCollection] Error observing query ${String(queryKey)}:`,
93
+ result.error
94
+ );
95
+ }
96
+ });
97
+ return async () => {
98
+ actualUnsubscribeFn();
99
+ await queryClient.cancelQueries({ queryKey });
100
+ queryClient.removeQueries({ queryKey });
101
+ };
102
+ };
103
+ const refetch = async () => {
104
+ return queryClient.refetchQueries({
105
+ queryKey
106
+ });
107
+ };
108
+ const wrappedOnInsert = onInsert ? async (params) => {
109
+ const handlerResult = await onInsert(params) ?? {};
110
+ const shouldRefetch = handlerResult.refetch !== false;
111
+ if (shouldRefetch) {
112
+ await refetch();
113
+ }
114
+ return handlerResult;
115
+ } : void 0;
116
+ const wrappedOnUpdate = onUpdate ? async (params) => {
117
+ const handlerResult = await onUpdate(params) ?? {};
118
+ const shouldRefetch = handlerResult.refetch !== false;
119
+ if (shouldRefetch) {
120
+ await refetch();
121
+ }
122
+ return handlerResult;
123
+ } : void 0;
124
+ const wrappedOnDelete = onDelete ? async (params) => {
125
+ const handlerResult = await onDelete(params) ?? {};
126
+ const shouldRefetch = handlerResult.refetch !== false;
127
+ if (shouldRefetch) {
128
+ await refetch();
129
+ }
130
+ return handlerResult;
131
+ } : void 0;
132
+ return {
133
+ ...baseCollectionConfig,
134
+ getKey,
135
+ sync: { sync: internalSync },
136
+ onInsert: wrappedOnInsert,
137
+ onUpdate: wrappedOnUpdate,
138
+ onDelete: wrappedOnDelete,
139
+ utils: {
140
+ refetch
141
+ }
142
+ };
143
+ }
144
+ export {
145
+ queryCollectionOptions
146
+ };
147
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n queryKey: TQueryKey\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\n queryClient: QueryClient\n\n // Query-specific options\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 id?: string\n getKey: CollectionConfig<TItem>[`getKey`]\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/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Query collection utilities type\n */\nexport interface QueryCollectionUtils extends UtilsRecord {\n refetch: RefetchFn\n}\n\n/**\n * Creates query collection options for use with a standard Collection\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & { utils: QueryCollectionUtils } {\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 ...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 Error(`[QueryCollection] queryKey must be provided.`)\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new Error(`[QueryCollection] queryFn must be provided.`)\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new Error(`[QueryCollection] queryClient must be provided.`)\n }\n\n if (!getKey) {\n throw new Error(`[QueryCollection] getKey must be provided.`)\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, 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 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 } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\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 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: internalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n },\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tanstack/query-db-collection",
3
3
  "description": "TanStack Query collection for TanStack DB",
4
- "version": "0.0.2",
4
+ "version": "0.0.4",
5
5
  "dependencies": {
6
- "@tanstack/db": "workspace:*",
7
- "@tanstack/query-core": "^5.75.7"
6
+ "@tanstack/query-core": "^5.75.7",
7
+ "@tanstack/db": "0.0.22"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@vitest/coverage-istanbul": "^3.0.9"
@@ -28,7 +28,6 @@
28
28
  ],
29
29
  "main": "dist/cjs/index.cjs",
30
30
  "module": "dist/esm/index.js",
31
- "packageManager": "pnpm@10.6.3",
32
31
  "peerDependencies": {
33
32
  "typescript": ">=4.7"
34
33
  },
@@ -46,14 +45,13 @@
46
45
  "optimistic",
47
46
  "typescript"
48
47
  ],
48
+ "sideEffects": false,
49
+ "type": "module",
50
+ "types": "dist/esm/index.d.ts",
49
51
  "scripts": {
50
52
  "build": "vite build",
51
53
  "dev": "vite build --watch",
52
54
  "lint": "eslint . --fix",
53
55
  "test": "npx vitest --run"
54
- },
55
- "sideEffects": false,
56
- "type": "module",
57
- "types": "dist/esm/index.d.ts"
58
- }
59
-
56
+ }
57
+ }
package/src/query.ts CHANGED
@@ -265,7 +265,6 @@ export function queryCollectionOptions<
265
265
  throw new Error(`[QueryCollection] queryClient must be provided.`)
266
266
  }
267
267
 
268
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
269
268
  if (!getKey) {
270
269
  throw new Error(`[QueryCollection] getKey must be provided.`)
271
270
  }