From dcde2d635f57ed18a08ac263435c555aa40d5e14 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 15 Dec 2025 19:35:37 +0000 Subject: [PATCH 1/9] test(query-db-collection): add failing tests for select + writeInsert/writeUpsert bug Add tests that reproduce the bug where using writeInsert or writeUpsert with a collection that has a select option causes an error: "select() must return an array of objects. Got: undefined" The bug occurs because performWriteOperations sets the query cache with a raw array, but the select function expects the wrapped response format. Related issue: https://github.com/TanStack/db/issues/xyz --- .../query-db-collection/tests/query.test.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index 429547f43..f76a2cc77 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -692,6 +692,109 @@ describe(`QueryCollection`, () => { ) as MetaDataType expect(initialCache).toEqual(initialMetaData) }) + + it(`should not throw error when using writeInsert with select option`, async () => { + const queryKey = [`select-writeInsert-test`] + const consoleErrorSpy = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + + const queryFn = vi.fn().mockResolvedValue(initialMetaData) + const select = vi.fn((data: MetaDataType) => data.data) + + const options = queryCollectionOptions({ + id: `select-writeInsert-test`, + queryClient, + queryKey, + queryFn, + select, + getKey, + startSync: true, + }) + const collection = createCollection(options) + + // Wait for collection to be ready + await vi.waitFor(() => { + expect(collection.status).toBe(`ready`) + expect(collection.size).toBe(2) + }) + + // This should NOT cause an error - but with the bug it does + const newItem: TestItem = { id: `3`, name: `New Item` } + collection.utils.writeInsert(newItem) + + // Verify the item was inserted + expect(collection.size).toBe(3) + expect(collection.get(`3`)).toEqual(newItem) + + // Wait a tick to allow any async error handlers to run + await flushPromises() + + // Verify no error was logged about select returning non-array + const errorCallArgs = consoleErrorSpy.mock.calls.find((call) => + call[0]?.includes?.( + `@tanstack/query-db-collection: select() must return an array of objects`, + ), + ) + expect(errorCallArgs).toBeUndefined() + + consoleErrorSpy.mockRestore() + }) + + it(`should not throw error when using writeUpsert with select option`, async () => { + const queryKey = [`select-writeUpsert-test`] + const consoleErrorSpy = vi + .spyOn(console, `error`) + .mockImplementation(() => {}) + + const queryFn = vi.fn().mockResolvedValue(initialMetaData) + const select = vi.fn((data: MetaDataType) => data.data) + + const options = queryCollectionOptions({ + id: `select-writeUpsert-test`, + queryClient, + queryKey, + queryFn, + select, + getKey, + startSync: true, + }) + const collection = createCollection(options) + + // Wait for collection to be ready + await vi.waitFor(() => { + expect(collection.status).toBe(`ready`) + expect(collection.size).toBe(2) + }) + + // This should NOT cause an error - but with the bug it does + // Test upsert for new item + const newItem: TestItem = { id: `3`, name: `Upserted New Item` } + collection.utils.writeUpsert(newItem) + + // Verify the item was inserted + expect(collection.size).toBe(3) + expect(collection.get(`3`)).toEqual(newItem) + + // Test upsert for existing item + collection.utils.writeUpsert({ id: `1`, name: `Updated First Item` }) + + // Verify the item was updated + expect(collection.get(`1`)?.name).toBe(`Updated First Item`) + + // Wait a tick to allow any async error handlers to run + await flushPromises() + + // Verify no error was logged about select returning non-array + const errorCallArgs = consoleErrorSpy.mock.calls.find((call) => + call[0]?.includes?.( + `@tanstack/query-db-collection: select() must return an array of objects`, + ), + ) + expect(errorCallArgs).toBeUndefined() + + consoleErrorSpy.mockRestore() + }) }) describe(`Direct persistence handlers`, () => { it(`should pass through direct persistence handlers to collection options`, () => { From 9927ce21b847b2ff264c42251e96bbcb1498754b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 15 Dec 2025 19:38:41 +0000 Subject: [PATCH 2/9] fix(query-db-collection): fix select option breaking writeInsert/writeUpsert When using the `select` option to extract items from a wrapped API response (e.g., `{ data: [...], meta: {...} }`), calling `writeInsert()` or `writeUpsert()` would corrupt the query cache by setting it to a raw array. This caused the `select` function to receive the wrong data format and return `undefined`, triggering the error: "select() must return an array of objects. Got: undefined" The fix adds a `hasSelect` flag to the SyncContext and skips the `setQueryData` call when `select` is configured. This is the correct behavior because: 1. The collection's synced store is already updated 2. The query cache stores the wrapped response format, not the raw items 3. Overwriting the cache with raw items would break the select function --- packages/query-db-collection/src/manual-sync.ts | 15 +++++++++++++-- packages/query-db-collection/src/query.ts | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/query-db-collection/src/manual-sync.ts b/packages/query-db-collection/src/manual-sync.ts index c5d3c0874..8a30db187 100644 --- a/packages/query-db-collection/src/manual-sync.ts +++ b/packages/query-db-collection/src/manual-sync.ts @@ -38,6 +38,12 @@ export interface SyncContext< begin: () => void write: (message: Omit, `key`>) => void commit: () => void + /** + * Whether the collection uses a `select` option to transform query data. + * When true, we skip updating the query cache directly since the cache format + * (wrapped response) differs from the collection format (extracted items). + */ + hasSelect?: boolean } interface NormalizedOperation< @@ -204,8 +210,13 @@ export function performWriteOperations< ctx.commit() // Update query cache after successful commit - const updatedData = Array.from(ctx.collection._state.syncedData.values()) - ctx.queryClient.setQueryData(ctx.queryKey, updatedData) + // Skip when `select` is used because the cache format (wrapped response) + // differs from the collection format (extracted items array). + // Setting the cache with a raw array would break the select function. + if (!ctx.hasSelect) { + const updatedData = Array.from(ctx.collection._state.syncedData.values()) + ctx.queryClient.setQueryData(ctx.queryKey, updatedData) + } } // Factory function to create write utils diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 2c81267dd..448f315f5 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1139,6 +1139,7 @@ export function queryCollectionOptions( begin: () => void write: (message: Omit, `key`>) => void commit: () => void + hasSelect: boolean } | null = null // Enhanced internalSync that captures write functions for manual use @@ -1154,6 +1155,7 @@ export function queryCollectionOptions( begin, write, commit, + hasSelect: !!select, } // Call the original internalSync logic From 23db85684c25c9d401b77f98762a6ea88bdf7e9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 15 Dec 2025 19:39:22 +0000 Subject: [PATCH 3/9] fix(query-db-collection): fix select option breaking writeInsert/writeUpsert When using the `select` option to extract items from a wrapped API response (e.g., `{ data: [...], meta: {...} }`), calling `writeInsert()` or `writeUpsert()` would corrupt the query cache by setting it to a raw array. This caused the `select` function to receive the wrong data format and return `undefined`, triggering the error: "select() must return an array of objects. Got: undefined" The fix adds an `updateCacheData` function to the SyncContext that properly handles cache updates for both cases: - Without `select`: sets the cache directly with the raw array - With `select`: uses setQueryData with an updater function to preserve the wrapper structure while updating the items array inside it Also added a comprehensive test that verifies the wrapped response format (including metadata) is preserved after write operations. --- .../query-db-collection/src/manual-sync.ts | 16 ++--- packages/query-db-collection/src/query.ts | 41 ++++++++++- .../query-db-collection/tests/query.test.ts | 70 +++++++++++++++++++ 3 files changed, 114 insertions(+), 13 deletions(-) diff --git a/packages/query-db-collection/src/manual-sync.ts b/packages/query-db-collection/src/manual-sync.ts index 8a30db187..5cc01c777 100644 --- a/packages/query-db-collection/src/manual-sync.ts +++ b/packages/query-db-collection/src/manual-sync.ts @@ -39,11 +39,10 @@ export interface SyncContext< write: (message: Omit, `key`>) => void commit: () => void /** - * Whether the collection uses a `select` option to transform query data. - * When true, we skip updating the query cache directly since the cache format - * (wrapped response) differs from the collection format (extracted items). + * Function to update the query cache with the latest synced data. + * Handles both direct array caches and wrapped response formats (when `select` is used). */ - hasSelect?: boolean + updateCacheData: (items: Array) => void } interface NormalizedOperation< @@ -210,13 +209,8 @@ export function performWriteOperations< ctx.commit() // Update query cache after successful commit - // Skip when `select` is used because the cache format (wrapped response) - // differs from the collection format (extracted items array). - // Setting the cache with a raw array would break the select function. - if (!ctx.hasSelect) { - const updatedData = Array.from(ctx.collection._state.syncedData.values()) - ctx.queryClient.setQueryData(ctx.queryKey, updatedData) - } + const updatedData = Array.from(ctx.collection._state.syncedData.values()) + ctx.updateCacheData(updatedData) } // Factory function to create write utils diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 448f315f5..878d47cfd 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1139,7 +1139,7 @@ export function queryCollectionOptions( begin: () => void write: (message: Omit, `key`>) => void commit: () => void - hasSelect: boolean + updateCacheData: (items: Array) => void } | null = null // Enhanced internalSync that captures write functions for manual use @@ -1155,7 +1155,44 @@ export function queryCollectionOptions( begin, write, commit, - hasSelect: !!select, + updateCacheData: (items: Array) => { + if (select) { + // When `select` is used, the cache contains a wrapped response (e.g., { data: [...], meta: {...} }) + // We need to update the cache while preserving the wrapper structure + queryClient.setQueryData( + queryKey as unknown as Array, + (oldData: any) => { + if (!oldData || typeof oldData !== `object`) { + // No existing cache or not an object - just set the raw array + return items + } + + if (Array.isArray(oldData)) { + // Cache is already a raw array (shouldn't happen with select, but handle it) + return items + } + + // Find the property that contains the array by checking each property + // The select function extracts from this property, so we need to find and update it + for (const key of Object.keys(oldData)) { + if (Array.isArray(oldData[key])) { + // Found the array property - create a shallow copy with updated items + return { ...oldData, [key]: items } + } + } + + // Couldn't find an array property - fallback to raw array + return items + }, + ) + } else { + // No select - cache contains raw array, just set it directly + queryClient.setQueryData( + queryKey as unknown as Array, + items, + ) + } + }, } // Call the original internalSync logic diff --git a/packages/query-db-collection/tests/query.test.ts b/packages/query-db-collection/tests/query.test.ts index f76a2cc77..1845641aa 100644 --- a/packages/query-db-collection/tests/query.test.ts +++ b/packages/query-db-collection/tests/query.test.ts @@ -795,6 +795,76 @@ describe(`QueryCollection`, () => { consoleErrorSpy.mockRestore() }) + + it(`should update query cache with wrapped format preserved when using writeInsert with select option`, async () => { + const queryKey = [`select-cache-update-test`] + + const queryFn = vi.fn().mockResolvedValue(initialMetaData) + const select = vi.fn((data: MetaDataType) => data.data) + + const options = queryCollectionOptions({ + id: `select-cache-update-test`, + queryClient, + queryKey, + queryFn, + select, + getKey, + startSync: true, + }) + const collection = createCollection(options) + + // Wait for collection to be ready + await vi.waitFor(() => { + expect(collection.status).toBe(`ready`) + expect(collection.size).toBe(2) + }) + + // Verify initial cache has wrapped format + const initialCache = queryClient.getQueryData( + queryKey, + ) as MetaDataType + expect(initialCache.metaDataOne).toBe(`example metadata`) + expect(initialCache.metaDataTwo).toBe(`example metadata`) + expect(initialCache.data).toHaveLength(2) + + // Insert a new item + const newItem: TestItem = { id: `3`, name: `New Item` } + collection.utils.writeInsert(newItem) + + // Verify the cache still has wrapped format with metadata preserved + const cacheAfterInsert = queryClient.getQueryData( + queryKey, + ) as MetaDataType + expect(cacheAfterInsert.metaDataOne).toBe(`example metadata`) + expect(cacheAfterInsert.metaDataTwo).toBe(`example metadata`) + expect(cacheAfterInsert.data).toHaveLength(3) + expect(cacheAfterInsert.data).toContainEqual(newItem) + + // Update an existing item + collection.utils.writeUpdate({ id: `1`, name: `Updated First Item` }) + + // Verify the cache still has wrapped format + const cacheAfterUpdate = queryClient.getQueryData( + queryKey, + ) as MetaDataType + expect(cacheAfterUpdate.metaDataOne).toBe(`example metadata`) + expect(cacheAfterUpdate.data).toHaveLength(3) + const updatedItem = cacheAfterUpdate.data.find((item) => item.id === `1`) + expect(updatedItem?.name).toBe(`Updated First Item`) + + // Delete an item + collection.utils.writeDelete(`2`) + + // Verify the cache still has wrapped format + const cacheAfterDelete = queryClient.getQueryData( + queryKey, + ) as MetaDataType + expect(cacheAfterDelete.metaDataOne).toBe(`example metadata`) + expect(cacheAfterDelete.data).toHaveLength(2) + expect(cacheAfterDelete.data).not.toContainEqual( + expect.objectContaining({ id: `2` }), + ) + }) }) describe(`Direct persistence handlers`, () => { it(`should pass through direct persistence handlers to collection options`, () => { From 45e9f1c43c5a185998a99ac3a7bf6f7bf82155d9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 19:55:43 +0000 Subject: [PATCH 4/9] ci: apply automated fixes --- packages/query-db-collection/src/query.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 878d47cfd..daa38273f 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1187,10 +1187,7 @@ export function queryCollectionOptions( ) } else { // No select - cache contains raw array, just set it directly - queryClient.setQueryData( - queryKey as unknown as Array, - items, - ) + queryClient.setQueryData(queryKey as unknown as Array, items) } }, } From d355fcf51bc3bd0283bfdcb3df4034bf9cfe29eb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 15 Dec 2025 20:07:43 +0000 Subject: [PATCH 5/9] refactor(query-db-collection): lift updateCacheData out of inline context Move the updateCacheData function from being inline in the writeContext object to a standalone function for better readability and maintainability. --- packages/query-db-collection/src/query.ts | 75 ++++++++++++----------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index daa38273f..7217fdeba 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1130,6 +1130,45 @@ export function queryCollectionOptions( await Promise.all(refetchPromises) } + /** + * Updates the query cache with new items, handling both direct arrays + * and wrapped response formats (when `select` is used). + */ + const updateCacheData = (items: Array): void => { + const key = queryKey as unknown as Array + + if (select) { + // When `select` is used, the cache contains a wrapped response (e.g., { data: [...], meta: {...} }) + // We need to update the cache while preserving the wrapper structure + queryClient.setQueryData(key, (oldData: any) => { + if (!oldData || typeof oldData !== `object`) { + // No existing cache or not an object - just set the raw array + return items + } + + if (Array.isArray(oldData)) { + // Cache is already a raw array (shouldn't happen with select, but handle it) + return items + } + + // Find the property that contains the array by checking each property + // The select function extracts from this property, so we need to find and update it + for (const propKey of Object.keys(oldData)) { + if (Array.isArray(oldData[propKey])) { + // Found the array property - create a shallow copy with updated items + return { ...oldData, [propKey]: items } + } + } + + // Couldn't find an array property - fallback to raw array + return items + }) + } else { + // No select - cache contains raw array, just set it directly + queryClient.setQueryData(key, items) + } + } + // Create write context for manual write operations let writeContext: { collection: any @@ -1155,41 +1194,7 @@ export function queryCollectionOptions( begin, write, commit, - updateCacheData: (items: Array) => { - if (select) { - // When `select` is used, the cache contains a wrapped response (e.g., { data: [...], meta: {...} }) - // We need to update the cache while preserving the wrapper structure - queryClient.setQueryData( - queryKey as unknown as Array, - (oldData: any) => { - if (!oldData || typeof oldData !== `object`) { - // No existing cache or not an object - just set the raw array - return items - } - - if (Array.isArray(oldData)) { - // Cache is already a raw array (shouldn't happen with select, but handle it) - return items - } - - // Find the property that contains the array by checking each property - // The select function extracts from this property, so we need to find and update it - for (const key of Object.keys(oldData)) { - if (Array.isArray(oldData[key])) { - // Found the array property - create a shallow copy with updated items - return { ...oldData, [key]: items } - } - } - - // Couldn't find an array property - fallback to raw array - return items - }, - ) - } else { - // No select - cache contains raw array, just set it directly - queryClient.setQueryData(queryKey as unknown as Array, items) - } - }, + updateCacheData, } // Call the original internalSync logic From 0c312f18c1749b21bd2605501379758d00f96a21 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 15 Dec 2025 21:46:50 +0000 Subject: [PATCH 6/9] fix(query-db-collection): improve updateCacheData with select-based property detection Address review feedback: 1. Use select(oldData) to identify the correct array property by reference equality instead of "first array property wins" heuristic 2. Fallback to common property names (data, items, results) before scanning 3. Return oldData unchanged instead of raw array when property can't be found to avoid breaking select 4. Make updateCacheData optional in SyncContext to avoid breaking changes 5. Add changeset for release --- .changeset/fix-select-write-operations.md | 9 ++++ .../query-db-collection/src/manual-sync.ts | 12 +++-- packages/query-db-collection/src/query.ts | 50 +++++++++++++------ 3 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 .changeset/fix-select-write-operations.md diff --git a/.changeset/fix-select-write-operations.md b/.changeset/fix-select-write-operations.md new file mode 100644 index 000000000..ac73dc7f8 --- /dev/null +++ b/.changeset/fix-select-write-operations.md @@ -0,0 +1,9 @@ +--- +"@tanstack/query-db-collection": patch +--- + +Fix writeInsert/writeUpsert throwing error when collection uses select option + +When a Query Collection was configured with a `select` option to extract items from a wrapped API response (e.g., `{ data: [...], meta: {...} }`), calling `writeInsert()` or `writeUpsert()` would corrupt the query cache and trigger the error: "select() must return an array of objects". + +The fix routes cache updates through a new `updateCacheData` function that preserves the wrapper structure by using the `select` function to identify which property contains the items array (via reference equality), then updates only that property while keeping metadata intact. diff --git a/packages/query-db-collection/src/manual-sync.ts b/packages/query-db-collection/src/manual-sync.ts index 5cc01c777..b772550e3 100644 --- a/packages/query-db-collection/src/manual-sync.ts +++ b/packages/query-db-collection/src/manual-sync.ts @@ -39,10 +39,11 @@ export interface SyncContext< write: (message: Omit, `key`>) => void commit: () => void /** - * Function to update the query cache with the latest synced data. + * Optional function to update the query cache with the latest synced data. * Handles both direct array caches and wrapped response formats (when `select` is used). + * If not provided, falls back to directly setting the cache with the raw array. */ - updateCacheData: (items: Array) => void + updateCacheData?: (items: Array) => void } interface NormalizedOperation< @@ -210,7 +211,12 @@ export function performWriteOperations< // Update query cache after successful commit const updatedData = Array.from(ctx.collection._state.syncedData.values()) - ctx.updateCacheData(updatedData) + if (ctx.updateCacheData) { + ctx.updateCacheData(updatedData) + } else { + // Fallback: directly set the cache with raw array (for non-Query Collection consumers) + ctx.queryClient.setQueryData(ctx.queryKey, updatedData) + } } // Factory function to create write utils diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 7217fdeba..069d11833 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1135,15 +1135,13 @@ export function queryCollectionOptions( * and wrapped response formats (when `select` is used). */ const updateCacheData = (items: Array): void => { - const key = queryKey as unknown as Array - if (select) { // When `select` is used, the cache contains a wrapped response (e.g., { data: [...], meta: {...} }) // We need to update the cache while preserving the wrapper structure - queryClient.setQueryData(key, (oldData: any) => { + queryClient.setQueryData(queryKey, (oldData: any) => { if (!oldData || typeof oldData !== `object`) { - // No existing cache or not an object - just set the raw array - return items + // No existing cache or not an object - don't corrupt the cache + return oldData } if (Array.isArray(oldData)) { @@ -1151,21 +1149,45 @@ export function queryCollectionOptions( return items } - // Find the property that contains the array by checking each property - // The select function extracts from this property, so we need to find and update it + // Use the select function to identify which property contains the items array. + // This is more robust than guessing based on property order. + const selectedArray = select(oldData) + + if (Array.isArray(selectedArray)) { + // Find the property that matches the selected array by reference equality + for (const propKey of Object.keys(oldData)) { + if (oldData[propKey] === selectedArray) { + // Found the exact property - create a shallow copy with updated items + return { ...oldData, [propKey]: items } + } + } + } + + // Fallback: check common property names used for data arrays + if (Array.isArray(oldData.data)) { + return { ...oldData, data: items } + } + if (Array.isArray(oldData.items)) { + return { ...oldData, items: items } + } + if (Array.isArray(oldData.results)) { + return { ...oldData, results: items } + } + + // Last resort: find first array property for (const propKey of Object.keys(oldData)) { if (Array.isArray(oldData[propKey])) { - // Found the array property - create a shallow copy with updated items return { ...oldData, [propKey]: items } } } - // Couldn't find an array property - fallback to raw array - return items + // Couldn't safely identify the array property - don't corrupt the cache + // Return oldData unchanged to avoid breaking select + return oldData }) } else { // No select - cache contains raw array, just set it directly - queryClient.setQueryData(key, items) + queryClient.setQueryData(queryKey, items) } } @@ -1173,12 +1195,12 @@ export function queryCollectionOptions( let writeContext: { collection: any queryClient: QueryClient - queryKey: Array + queryKey: QueryKey getKey: (item: any) => string | number begin: () => void write: (message: Omit, `key`>) => void commit: () => void - updateCacheData: (items: Array) => void + updateCacheData?: (items: Array) => void } | null = null // Enhanced internalSync that captures write functions for manual use @@ -1189,7 +1211,7 @@ export function queryCollectionOptions( writeContext = { collection, queryClient, - queryKey: queryKey as unknown as Array, + queryKey: queryKey as QueryKey, getKey: getKey as (item: any) => string | number, begin, write, From 7d260fa5241c815cfc5387dcb5d0bbb688a9e50c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:47:43 +0000 Subject: [PATCH 7/9] ci: apply automated fixes --- .changeset/fix-select-write-operations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-select-write-operations.md b/.changeset/fix-select-write-operations.md index ac73dc7f8..bbf28f6b6 100644 --- a/.changeset/fix-select-write-operations.md +++ b/.changeset/fix-select-write-operations.md @@ -1,5 +1,5 @@ --- -"@tanstack/query-db-collection": patch +'@tanstack/query-db-collection': patch --- Fix writeInsert/writeUpsert throwing error when collection uses select option From eab633e1117b4b8165e29ecd6d12e96f97669670 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 15 Dec 2025 22:07:46 +0000 Subject: [PATCH 8/9] fix: resolve TypeScript type errors for queryKey Handle both static and function-based queryKey types properly: - Get base query key before passing to setQueryData - Keep writeContext.queryKey as Array for SyncContext compatibility --- packages/query-db-collection/src/query.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/query-db-collection/src/query.ts b/packages/query-db-collection/src/query.ts index 069d11833..4cbdae5a2 100644 --- a/packages/query-db-collection/src/query.ts +++ b/packages/query-db-collection/src/query.ts @@ -1135,10 +1135,16 @@ export function queryCollectionOptions( * and wrapped response formats (when `select` is used). */ const updateCacheData = (items: Array): void => { + // Get the base query key (handle both static and function-based keys) + const key = + typeof queryKey === `function` + ? queryKey({}) + : (queryKey as unknown as QueryKey) + if (select) { // When `select` is used, the cache contains a wrapped response (e.g., { data: [...], meta: {...} }) // We need to update the cache while preserving the wrapper structure - queryClient.setQueryData(queryKey, (oldData: any) => { + queryClient.setQueryData(key, (oldData: any) => { if (!oldData || typeof oldData !== `object`) { // No existing cache or not an object - don't corrupt the cache return oldData @@ -1187,7 +1193,7 @@ export function queryCollectionOptions( }) } else { // No select - cache contains raw array, just set it directly - queryClient.setQueryData(queryKey, items) + queryClient.setQueryData(key, items) } } @@ -1195,7 +1201,7 @@ export function queryCollectionOptions( let writeContext: { collection: any queryClient: QueryClient - queryKey: QueryKey + queryKey: Array getKey: (item: any) => string | number begin: () => void write: (message: Omit, `key`>) => void @@ -1207,11 +1213,17 @@ export function queryCollectionOptions( const enhancedInternalSync: SyncConfig[`sync`] = (params) => { const { begin, write, commit, collection } = params + // Get the base query key for the context (handle both static and function-based keys) + const contextQueryKey = + typeof queryKey === `function` + ? (queryKey({}) as unknown as Array) + : (queryKey as unknown as Array) + // Store references for manual write operations writeContext = { collection, queryClient, - queryKey: queryKey as QueryKey, + queryKey: contextQueryKey, getKey: getKey as (item: any) => string | number, begin, write, From 68cca4f27fc9a78dcac3c9dbabace94626cea327 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 15 Dec 2025 22:14:15 +0000 Subject: [PATCH 9/9] chore: fix version inconsistencies in example apps Update example apps to use consistent versions of: - @tanstack/query-db-collection: ^1.0.7 - @tanstack/react-db: ^0.1.56 Fixes sherif multiple-dependency-versions check. --- examples/react/paced-mutations-demo/package.json | 2 +- examples/react/todo/package.json | 4 ++-- examples/solid/todo/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/react/paced-mutations-demo/package.json b/examples/react/paced-mutations-demo/package.json index 850bb2f95..1bace882e 100644 --- a/examples/react/paced-mutations-demo/package.json +++ b/examples/react/paced-mutations-demo/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@tanstack/db": "^0.5.11", - "@tanstack/react-db": "^0.1.55", + "@tanstack/react-db": "^0.1.56", "mitt": "^3.0.1", "react": "^19.2.1", "react-dom": "^19.2.1" diff --git a/examples/react/todo/package.json b/examples/react/todo/package.json index eb81b798c..3dc6432fa 100644 --- a/examples/react/todo/package.json +++ b/examples/react/todo/package.json @@ -5,8 +5,8 @@ "dependencies": { "@tanstack/electric-db-collection": "^0.2.12", "@tanstack/query-core": "^5.90.12", - "@tanstack/query-db-collection": "^1.0.6", - "@tanstack/react-db": "^0.1.55", + "@tanstack/query-db-collection": "^1.0.7", + "@tanstack/react-db": "^0.1.56", "@tanstack/react-router": "^1.140.0", "@tanstack/react-start": "^1.140.0", "@tanstack/trailbase-db-collection": "^0.1.55", diff --git a/examples/solid/todo/package.json b/examples/solid/todo/package.json index 5fb3d60ef..438b61a11 100644 --- a/examples/solid/todo/package.json +++ b/examples/solid/todo/package.json @@ -5,7 +5,7 @@ "dependencies": { "@tanstack/electric-db-collection": "^0.2.12", "@tanstack/query-core": "^5.90.12", - "@tanstack/query-db-collection": "^1.0.6", + "@tanstack/query-db-collection": "^1.0.7", "@tanstack/solid-db": "^0.1.54", "@tanstack/solid-router": "^1.140.0", "@tanstack/solid-start": "^1.140.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6a532eb9..7d45b0b0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -257,7 +257,7 @@ importers: specifier: ^0.5.11 version: link:../../../packages/db '@tanstack/react-db': - specifier: ^0.1.55 + specifier: ^0.1.56 version: link:../../../packages/react-db mitt: specifier: ^3.0.1 @@ -433,10 +433,10 @@ importers: specifier: ^5.90.12 version: 5.90.12 '@tanstack/query-db-collection': - specifier: ^1.0.6 + specifier: ^1.0.7 version: link:../../../packages/query-db-collection '@tanstack/react-db': - specifier: ^0.1.55 + specifier: ^0.1.56 version: link:../../../packages/react-db '@tanstack/react-router': specifier: ^1.140.0 @@ -554,7 +554,7 @@ importers: specifier: ^5.90.12 version: 5.90.12 '@tanstack/query-db-collection': - specifier: ^1.0.6 + specifier: ^1.0.7 version: link:../../../packages/query-db-collection '@tanstack/solid-db': specifier: ^0.1.54