Skip to content

Commit 2516b5f

Browse files
committed
fix(prefetch): parse the file-folder seed through its contract
The audit found this key was the workspaceFilesKeys bug waiting to recur. The manager's record type and workspaceFileFolderSchema are two independent declarations that agree today by coincidence; the seed had no parse, so adding a column to one would have silently cached a shape a client fetch strips — and three of its fields are z.coerce.date(), the exact divergence that put ISO strings under the file-list key. Its sibling is immune because listWorkspaceFilesWithShares parses at the data layer. This does the same at the seed, and adds the shape-parity assertion the key never had. Verified falsifiable: removing the parse turns it red. Doing so also exposed the existing folder test as fixture-thin — a folder with only an id, which the contract rightly rejects — so it now uses a real row. Also points the credential block's fetchQuery at the exported staleTime constant instead of restating 60 * 1000; it was a fifth producer on that key free to drift from the four that share it.
1 parent 618cee5 commit 2516b5f

3 files changed

Lines changed: 71 additions & 7 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/files/prefetch.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { QueryClient } from '@tanstack/react-query'
2+
import { listWorkspaceFileFoldersContract } from '@/lib/api/contracts/workspace-file-folders'
23
import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager'
34
import { getWorkspaceHostContextForViewer } from '@/lib/workspaces/host-context'
45
import { prefetchResourceListChrome } from '@/app/workspace/[workspaceId]/lib/prefetch-resource-list-chrome'
@@ -40,7 +41,17 @@ export async function prefetchFilesBrowser(
4041
await Promise.all([
4142
queryClient.prefetchQuery({
4243
queryKey: workspaceFileFolderKeys.list(workspaceId, 'active'),
43-
queryFn: () => listWorkspaceFileFolders(workspaceId, { scope: 'active' }),
44+
/**
45+
* Parsed through the route's own response schema rather than seeded raw. The
46+
* manager's record type and `workspaceFileFolderSchema` are two independent
47+
* declarations that happen to agree today; without this parse, adding a column
48+
* to one silently seeds a shape a client fetch would have stripped — the exact
49+
* divergence that put ISO strings under `workspaceFilesKeys.list`.
50+
*/
51+
queryFn: async () => {
52+
const folders = await listWorkspaceFileFolders(workspaceId, { scope: 'active' })
53+
return listWorkspaceFileFoldersContract.response.schema.shape.folders.parse(folders)
54+
},
4455
staleTime: WORKSPACE_FILE_FOLDERS_STALE_TIME,
4556
}),
4657
prefetchResourceListChrome(queryClient, workspaceId, 'file', userId),

‎apps/sim/app/workspace/[workspaceId]/lib/prefetch.test.ts‎

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -290,17 +290,67 @@ describe('workspace list prefetches', () => {
290290
})
291291
})
292292
describe('prefetchFilesBrowser', () => {
293+
/**
294+
* The sibling `workspaceFilesKeys.list` once held ISO strings from one producer and
295+
* `Date`s from another because a seed skipped the contract parse. This key is fed by
296+
* a manager whose record type and the contract schema are independent declarations,
297+
* so the parse — and this assertion — are what stop that recurring here.
298+
*/
299+
it('seeds the shape a client fetch caches, not the raw manager row', async () => {
300+
mockListWorkspaceFileFolders.mockResolvedValue([
301+
{
302+
id: 'folder-1',
303+
workspaceId: WORKSPACE_ID,
304+
userId: USER_ID,
305+
name: 'Docs',
306+
parentId: null,
307+
path: '/Docs',
308+
sortOrder: 0,
309+
deletedAt: null,
310+
createdAt: '2026-01-01T00:00:00.000Z',
311+
updatedAt: '2026-01-02T00:00:00.000Z',
312+
serverOnlyColumn: 'should-be-stripped',
313+
},
314+
])
315+
const client = makeClient()
316+
317+
await prefetchFilesBrowser(client, WORKSPACE_ID, USER_ID)
318+
319+
const [cached] = client.getQueryData(
320+
workspaceFileFolderKeys.list(WORKSPACE_ID, 'active')
321+
) as Array<Record<string, unknown>>
322+
expect(cached.createdAt).toBeInstanceOf(Date)
323+
expect(cached.updatedAt).toBeInstanceOf(Date)
324+
expect(cached).not.toHaveProperty('serverOnlyColumn')
325+
})
326+
293327
it('primes the folder key the client hook reads', async () => {
294-
const folders = [{ id: 'folder-1' }]
328+
const folders = [
329+
{
330+
id: 'folder-1',
331+
workspaceId: WORKSPACE_ID,
332+
userId: USER_ID,
333+
name: 'Docs',
334+
parentId: null,
335+
path: '/Docs',
336+
sortOrder: 0,
337+
deletedAt: null,
338+
createdAt: '2026-01-01T00:00:00.000Z',
339+
updatedAt: '2026-01-02T00:00:00.000Z',
340+
},
341+
]
295342
mockListWorkspaceFileFolders.mockResolvedValue(folders)
296343
const client = makeClient()
297344

298345
await prefetchFilesBrowser(client, WORKSPACE_ID, USER_ID)
299346

300347
expect(mockListWorkspaceFileFolders).toHaveBeenCalledWith(WORKSPACE_ID, { scope: 'active' })
301-
expect(client.getQueryData(workspaceFileFolderKeys.list(WORKSPACE_ID, 'active'))).toEqual(
302-
folders
303-
)
348+
/** Shape parity is asserted by the sibling test; this one pins the key and the args. */
349+
expect(
350+
client.getQueryData(workspaceFileFolderKeys.list(WORKSPACE_ID, 'active')) as Array<{
351+
id: string
352+
}>
353+
).toHaveLength(folders.length)
304354
})
305355

306356
/**

‎apps/sim/blocks/blocks/credential.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { getServiceConfigByProviderId } from '@/lib/oauth/utils'
33
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
44
import type { BlockConfig } from '@/blocks/types'
55
import { workspaceCredentialKeys } from '@/hooks/queries/utils/credential-keys'
6-
import { fetchWorkspaceCredentialList } from '@/hooks/queries/utils/fetch-workspace-credentials'
6+
import {
7+
fetchWorkspaceCredentialList,
8+
WORKSPACE_CREDENTIAL_LIST_STALE_TIME,
9+
} from '@/hooks/queries/utils/fetch-workspace-credentials'
710
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
811

912
interface CredentialBlockOutput {
@@ -72,7 +75,7 @@ export const CredentialBlock: BlockConfig<CredentialBlockOutput> = {
7275
const credentials = await getQueryClient().fetchQuery({
7376
queryKey: workspaceCredentialKeys.list(workspaceId),
7477
queryFn: () => fetchWorkspaceCredentialList(workspaceId),
75-
staleTime: 60 * 1000,
78+
staleTime: WORKSPACE_CREDENTIAL_LIST_STALE_TIME,
7679
})
7780

7881
const seen = new Set<string>()

0 commit comments

Comments
 (0)