Skip to content

Commit 1862a84

Browse files
committed
fix(tables): give the tables prefetch the same wire shape the route returns
GET /api/table does not return listTables rows: it drops metadata, runs every column through normalizeColumn, serializes the three dates, and defaults the job fields. The prefetch called listTables directly, so a hydrated entry held un-normalized columns plus a field the client never sees, and swapped them out on the first refetch. Extracts listTablesForWorkspace so the route and the prefetch produce one shape, matching what files, knowledge and pinned items already do here.
1 parent c8cc8ad commit 1862a84

4 files changed

Lines changed: 66 additions & 46 deletions

File tree

apps/sim/app/api/table/route.ts

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ import { captureServerEvent } from '@/lib/posthog/server'
1111
import {
1212
createTable,
1313
getWorkspaceTableLimits,
14-
listTables,
1514
type TableSchema,
1615
type TableScope,
1716
} from '@/lib/table'
17+
import { listTablesForWorkspace } from '@/lib/table/queries'
1818
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1919
import { normalizeColumn } from '@/app/api/table/utils'
2020

@@ -204,46 +204,20 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
204204
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
205205
}
206206

207-
const tables = await listTables(params.workspaceId, { scope: params.scope as TableScope })
208-
209-
logger.info(`[${requestId}] Listed ${tables.length} tables in workspace ${params.workspaceId}`)
207+
const responseTables = await listTablesForWorkspace(
208+
params.workspaceId,
209+
params.scope as TableScope
210+
)
210211

211-
const responseTables = tables.map((t) => {
212-
const schemaData = t.schema as TableSchema
213-
return {
214-
id: t.id,
215-
name: t.name,
216-
description: t.description,
217-
schema: {
218-
columns: schemaData.columns.map(normalizeColumn),
219-
},
220-
rowCount: t.rowCount,
221-
maxRows: t.maxRows,
222-
locks: t.locks,
223-
workspaceId: t.workspaceId,
224-
folderId: t.folderId ?? null,
225-
createdBy: t.createdBy,
226-
createdAt: t.createdAt instanceof Date ? t.createdAt.toISOString() : String(t.createdAt),
227-
updatedAt: t.updatedAt instanceof Date ? t.updatedAt.toISOString() : String(t.updatedAt),
228-
archivedAt:
229-
t.archivedAt instanceof Date
230-
? t.archivedAt.toISOString()
231-
: t.archivedAt
232-
? String(t.archivedAt)
233-
: null,
234-
jobStatus: t.jobStatus ?? null,
235-
jobId: t.jobId ?? null,
236-
jobType: t.jobType ?? null,
237-
jobError: t.jobError ?? null,
238-
jobRowsProcessed: t.jobRowsProcessed ?? 0,
239-
}
240-
})
212+
logger.info(
213+
`[${requestId}] Listed ${responseTables.length} tables in workspace ${params.workspaceId}`
214+
)
241215

242216
return NextResponse.json({
243217
success: true,
244218
data: {
245219
tables: responseTables,
246-
totalCount: tables.length,
220+
totalCount: responseTables.length,
247221
},
248222
})
249223
} catch (error) {

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const {
1010
mockGetWorkspaceMemberProfiles,
1111
mockListFoldersForWorkspace,
1212
mockListPinnedItemsForViewer,
13-
mockListTables,
13+
mockListTablesForWorkspace,
1414
mockListWorkspaceFileFolders,
1515
mockListWorkspaceFilesWithShares,
1616
} = vi.hoisted(() => ({
@@ -19,7 +19,7 @@ const {
1919
mockGetWorkspaceMemberProfiles: vi.fn(),
2020
mockListFoldersForWorkspace: vi.fn(),
2121
mockListPinnedItemsForViewer: vi.fn(),
22-
mockListTables: vi.fn(),
22+
mockListTablesForWorkspace: vi.fn(),
2323
mockListWorkspaceFileFolders: vi.fn(),
2424
mockListWorkspaceFilesWithShares: vi.fn(),
2525
}))
@@ -38,7 +38,7 @@ vi.mock('@/lib/workspace-files/queries', () => ({
3838
vi.mock('@/lib/uploads/contexts/workspace', () => ({
3939
listWorkspaceFileFolders: mockListWorkspaceFileFolders,
4040
}))
41-
vi.mock('@/lib/table', () => ({ listTables: mockListTables }))
41+
vi.mock('@/lib/table/queries', () => ({ listTablesForWorkspace: mockListTablesForWorkspace }))
4242
vi.mock('@/lib/knowledge/queries', () => ({
4343
listKnowledgeBasesForViewer: mockListKnowledgeBasesForViewer,
4444
}))
@@ -75,7 +75,7 @@ describe('workspace list prefetches', () => {
7575
mockListFoldersForWorkspace.mockResolvedValue([])
7676
mockListWorkspaceFilesWithShares.mockResolvedValue([])
7777
mockListWorkspaceFileFolders.mockResolvedValue([])
78-
mockListTables.mockResolvedValue([])
78+
mockListTablesForWorkspace.mockResolvedValue([])
7979
mockListKnowledgeBasesForViewer.mockResolvedValue([])
8080
})
8181

@@ -95,12 +95,12 @@ describe('workspace list prefetches', () => {
9595
describe('prefetchTables', () => {
9696
it('primes the exact key useTablesList reads', async () => {
9797
const tables = [{ id: 't-1' }]
98-
mockListTables.mockResolvedValue(tables)
98+
mockListTablesForWorkspace.mockResolvedValue(tables)
9999
const client = makeClient()
100100

101101
await prefetchTables(client, WORKSPACE_ID, USER_ID)
102102

103-
expect(mockListTables).toHaveBeenCalledWith(WORKSPACE_ID, { scope: 'active' })
103+
expect(mockListTablesForWorkspace).toHaveBeenCalledWith(WORKSPACE_ID, 'active')
104104
expect(client.getQueryData(tableKeys.list(WORKSPACE_ID, 'active'))).toEqual(tables)
105105
})
106106
})
@@ -177,7 +177,7 @@ describe('workspace list prefetches', () => {
177177

178178
expect(client.getQueryCache().getAll()).toHaveLength(0)
179179
expect(mockListWorkspaceFilesWithShares).not.toHaveBeenCalled()
180-
expect(mockListTables).not.toHaveBeenCalled()
180+
expect(mockListTablesForWorkspace).not.toHaveBeenCalled()
181181
expect(mockListKnowledgeBasesForViewer).not.toHaveBeenCalled()
182182
expect(mockListPinnedItemsForViewer).not.toHaveBeenCalled()
183183
})

apps/sim/app/workspace/[workspaceId]/tables/prefetch.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { QueryClient } from '@tanstack/react-query'
22
import { listFoldersForWorkspace } from '@/lib/folders/queries'
3-
import { listTables } from '@/lib/table'
3+
import { listTablesForWorkspace } from '@/lib/table/queries'
44
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
55
import { prefetchResourceListChrome } from '@/app/workspace/[workspaceId]/lib/prefetch-resource-list-chrome'
66
import { FOLDER_LIST_STALE_TIME, folderKeys, mapFolder } from '@/hooks/queries/utils/folder-keys'
@@ -14,8 +14,9 @@ import { TABLE_LIST_STALE_TIME, tableKeys } from '@/hooks/queries/utils/table-ke
1414
* only placed correctly relative to the folder rows it sits beside, so
1515
* prefetching one without the other still flashes an ungrouped list.
1616
*
17-
* Folders are mapped with the same `mapFolder` the hook applies so the hydrated entry
18-
* matches a client fetch exactly.
17+
* `listTablesForWorkspace` returns the same wire shape `GET /api/table` does, and folders
18+
* are mapped with the same `mapFolder` the hook applies — so both hydrated entries match a
19+
* client fetch exactly.
1920
*/
2021
export async function prefetchTables(
2122
queryClient: QueryClient,
@@ -28,7 +29,7 @@ export async function prefetchTables(
2829
await Promise.all([
2930
queryClient.prefetchQuery({
3031
queryKey: tableKeys.list(workspaceId, 'active'),
31-
queryFn: () => listTables(workspaceId, { scope: 'active' }),
32+
queryFn: () => listTablesForWorkspace(workspaceId, 'active'),
3233
staleTime: TABLE_LIST_STALE_TIME,
3334
}),
3435
queryClient.prefetchQuery({

apps/sim/lib/table/queries.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { listTables, type TableScope } from '@/lib/table/service'
2+
import type { TableDefinition, TableSchema } from '@/lib/table/types'
3+
import { normalizeColumn } from '@/app/api/table/utils'
4+
5+
/** Serializes a stored date to the ISO string the wire carries. */
6+
function toWireDate(value: Date | string): string {
7+
return value instanceof Date ? value.toISOString() : String(value)
8+
}
9+
10+
/**
11+
* Lists a workspace's tables in the wire shape `GET /api/table` returns.
12+
*
13+
* Shared by that route and the Tables page's server prefetch so a hydrated cache entry and a
14+
* client fetch cannot disagree. The shaping is not incidental: the route drops `metadata`,
15+
* runs every column through {@link normalizeColumn}, serializes the three dates, and defaults
16+
* the job fields — so caching raw `listTables` rows would hydrate un-normalized columns and a
17+
* field the client never sees, then swap them out on the first refetch.
18+
*/
19+
export async function listTablesForWorkspace(
20+
workspaceId: string,
21+
scope: TableScope = 'active'
22+
): Promise<TableDefinition[]> {
23+
const tables = await listTables(workspaceId, { scope })
24+
25+
return tables.map((table) => ({
26+
id: table.id,
27+
name: table.name,
28+
description: table.description,
29+
schema: { columns: (table.schema as TableSchema).columns.map(normalizeColumn) },
30+
rowCount: table.rowCount,
31+
maxRows: table.maxRows,
32+
locks: table.locks,
33+
workspaceId: table.workspaceId,
34+
folderId: table.folderId ?? null,
35+
createdBy: table.createdBy,
36+
createdAt: toWireDate(table.createdAt),
37+
updatedAt: toWireDate(table.updatedAt),
38+
archivedAt: table.archivedAt ? toWireDate(table.archivedAt) : null,
39+
jobStatus: table.jobStatus ?? null,
40+
jobId: table.jobId ?? null,
41+
jobType: table.jobType ?? null,
42+
jobError: table.jobError ?? null,
43+
jobRowsProcessed: table.jobRowsProcessed ?? 0,
44+
}))
45+
}

0 commit comments

Comments
 (0)