Skip to content

Commit 9596043

Browse files
committed
perf(prefetch): finish removing self-HTTP prefetches and delete the legacy helper
Converts the last four server-render prefetches that called our own API over HTTP, and deletes prefetch-internal-fetch.ts now that nothing imports it. - knowledge bases: runs the route's own listInternalKnowledgeBases use case with a principal from the same internalSessionAuth policy the route declares, then projects through the same presenter and contract. Not a bypass of the application boundary — the same path, called in-process. - tables: extracts the route's list projection into lib/table/wire.ts as toTableListItem, which the route and the prefetch now both call. This matters because listTablesContract's response schema is a passthrough z.custom, so a client fetch caches the route's JSON verbatim. Seeding listTables() directly would have put Date objects and the server-only metadata field under a key the hook never sees them on. - pinned items: extracts the route's inline query into lib/pinned-items/queries.ts as listPinnedItemsForUser, which the route now calls too. - workspace members: getWorkspaceMemberProfiles already existed; the prefetch calls it directly. normalizeColumn moves from app/api/table/utils.ts to lib/table/wire.ts with ten importers repointed. That also removes a pre-existing lib/* -> app/api/* boundary violation in lib/table/import-runner.ts. No response shape changes: the v1/v2 edits are import-path moves only. Every converted read proves the viewer first and caches nothing when that fails, so an unauthorized viewer's client fetch still reaches the route for the real 403. Authorization equivalence was checked by unfolding both paths to checkWorkspaceAccess rather than assumed.
1 parent e3ae8c6 commit 9596043

27 files changed

Lines changed: 398 additions & 262 deletions

File tree

apps/sim/app/api/pinned-items/route.ts

Lines changed: 3 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,21 @@ import { db, pinnedItem } from '@sim/db'
22
import { createLogger } from '@sim/logger'
33
import { getPostgresErrorCode } from '@sim/utils/errors'
44
import { generateId } from '@sim/utils/id'
5-
import { and, eq, ne } from 'drizzle-orm'
65
import { type NextRequest, NextResponse } from 'next/server'
76
import {
87
createPinnedItemContract,
98
listPinnedItemsContract,
109
type PinnedItemApi,
11-
pinnedResourceTypeSchema,
1210
} from '@/lib/api/contracts'
1311
import { parseRequest } from '@/lib/api/server'
1412
import { getSession } from '@/lib/auth'
1513
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
16-
import { filterToActiveResources, pinnableResourceExists } from '@/lib/pinned-items/resources'
14+
import { listPinnedItemsForUser } from '@/lib/pinned-items/queries'
15+
import { pinnableResourceExists } from '@/lib/pinned-items/resources'
1716
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1817

1918
const logger = createLogger('PinnedItemsAPI')
2019

21-
/**
22-
* Narrows a stored row to the wire shape, dropping any row whose `resourceType` this build does
23-
* not recognise.
24-
*
25-
* `pinned_item.resource_type` is plain `text` — deliberately, so the set of pinnable kinds can
26-
* grow — while the contract is a closed enum. During a rolling deploy an older pod can therefore
27-
* read a pin a newer one wrote. Returning it would fail response validation and take the WHOLE
28-
* list down rather than the single row, so the unknown kind is skipped instead.
29-
*
30-
* `filterToActiveResources` already drops these as a side effect of not having a table to look
31-
* them up in; this makes the guarantee explicit and compiler-checked at the wire boundary.
32-
*/
33-
function toPinnedItemApi(row: typeof pinnedItem.$inferSelect): PinnedItemApi | null {
34-
const resourceType = pinnedResourceTypeSchema.safeParse(row.resourceType)
35-
if (!resourceType.success) return null
36-
return { ...row, resourceType: resourceType.data, pinnedAt: row.pinnedAt.toISOString() }
37-
}
38-
3920
/** Lists the session user's pinned items in a workspace, optionally filtered to one `resourceType`. */
4021
export const GET = withRouteHandler(async (request: NextRequest) => {
4122
const session = await getSession()
@@ -52,30 +33,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
5233
return NextResponse.json({ error: 'Access denied to this workspace' }, { status: 403 })
5334
}
5435

55-
const rows = await db
56-
.select()
57-
.from(pinnedItem)
58-
.where(
59-
and(
60-
eq(pinnedItem.userId, session.user.id),
61-
eq(pinnedItem.workspaceId, workspaceId),
62-
/**
63-
* A `workspace` pin stores `workspaceId === resourceId`, so it would otherwise
64-
* appear in this workspace's unscoped listing as a resource *inside* itself.
65-
* It is read from the workspace-list payload instead, so it is excluded here
66-
* rather than left for a future unscoped caller to mistake for a real resource.
67-
*/
68-
resourceType
69-
? eq(pinnedItem.resourceType, resourceType)
70-
: ne(pinnedItem.resourceType, 'workspace')
71-
)
72-
)
73-
74-
const activeRows = await filterToActiveResources(rows, workspaceId)
75-
76-
const pinnedItems = activeRows
77-
.map(toPinnedItemApi)
78-
.filter((item): item is PinnedItemApi => item !== null)
36+
const pinnedItems = await listPinnedItemsForUser(session.user.id, workspaceId, resourceType)
7937

8038
return NextResponse.json({ pinnedItems })
8139
})

apps/sim/app/api/table/[tableId]/columns/route.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@ vi.mock('@/lib/table/columns/service', () => ({
4949
updateColumnOptions: mockUpdateColumnOptions,
5050
updateColumnType: mockUpdateColumnType,
5151
}))
52+
vi.mock('@/lib/table/wire', () => ({
53+
normalizeColumn: (c: unknown) => c,
54+
}))
5255
vi.mock('@/app/api/table/utils', () => ({
5356
accessError: () => new Response('denied', { status: 403 }),
5457
checkAccess: mockCheckAccess,
55-
normalizeColumn: (c: unknown) => c,
5658
orchestrationOutcomeErrorResponse: (
5759
outcome: { error?: string; errorCode?: OrchestrationErrorCode },
5860
fallback: string

apps/sim/app/api/table/[tableId]/columns/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1313
import { addTableColumn, deleteColumn } from '@/lib/table'
1414
import { signalTableSchemaChanged } from '@/lib/table/events'
1515
import { performUpdateTableColumn } from '@/lib/table/orchestration'
16+
import { normalizeColumn } from '@/lib/table/wire'
1617
import {
1718
accessError,
1819
checkAccess,
19-
normalizeColumn,
2020
orchestrationOutcomeErrorResponse,
2121
rootErrorMessage,
2222
tableLockErrorResponse,

apps/sim/app/api/table/[tableId]/groups/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ vi.mock('@/lib/table/application/groups', () => ({
5454
updateTableGroupUseCase: mocks.useCases.update,
5555
}))
5656

57-
vi.mock('@/app/api/table/utils', () => ({
57+
vi.mock('@/lib/table/wire', () => ({
5858
normalizeColumn: vi.fn(),
5959
}))
6060

apps/sim/app/api/table/[tableId]/groups/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@/lib/table/application/groups'
1313
import { tableOperations } from '@/lib/table/application/operations'
1414
import type { TableDefinition } from '@/lib/table/types'
15-
import { normalizeColumn } from '@/app/api/table/utils'
15+
import { normalizeColumn } from '@/lib/table/wire'
1616

1717
const rateLimit = internalRateLimits.none({
1818
reason: 'Existing authenticated table group mutations have no request-rate policy',

apps/sim/app/api/table/[tableId]/route.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({
5151
vi.mock('@/app/api/table/utils', () => ({
5252
accessError: () => new Response('denied', { status: 403 }),
5353
checkAccess: mockCheckAccess,
54-
normalizeColumn: (column: unknown) => column,
5554
tableLockErrorResponse: () => null,
5655
}))
56+
vi.mock('@/lib/table/wire', () => ({
57+
normalizeColumn: (column: unknown) => column,
58+
}))
5759

5860
import { GET, PATCH } from '@/app/api/table/[tableId]/route'
5961

apps/sim/app/api/table/[tableId]/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import {
1818
performUpdateTableLocks,
1919
} from '@/lib/table/orchestration'
2020
import { TABLE_LOCK_FLAGS, TABLE_LOCK_KINDS } from '@/lib/table/types'
21+
import { normalizeColumn } from '@/lib/table/wire'
2122
import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils'
2223
import {
2324
accessError,
2425
checkAccess,
25-
normalizeColumn,
2626
orchestrationOutcomeErrorResponse,
2727
tableLockErrorResponse,
2828
} from '@/app/api/table/utils'

apps/sim/app/api/table/import-csv/route.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ vi.mock('@/app/api/table/utils', async () => {
3434
const { asOrchestrationError, messageForOrchestrationError, statusForOrchestrationError } =
3535
await import('@/lib/core/orchestration/types')
3636
return {
37-
normalizeColumn: (column: unknown) => column,
3837
csvProxyBodyCapResponse: () => null,
3938
multipartErrorResponse: (error: { code: string; message: string }) =>
4039
NextResponse.json(

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

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import {
1515
type TableSchema,
1616
type TableScope,
1717
} from '@/lib/table'
18+
import { normalizeColumn, toTableListItem } from '@/lib/table/wire'
1819
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
19-
import { normalizeColumn, orchestrationErrorResponse } from '@/app/api/table/utils'
20+
import { orchestrationErrorResponse } from '@/app/api/table/utils'
2021

2122
const logger = createLogger('TableAPI')
2223

@@ -198,41 +199,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
198199

199200
logger.info(`[${requestId}] Listed ${tables.length} tables in workspace ${params.workspaceId}`)
200201

201-
const responseTables = tables.map((t) => {
202-
const schemaData = t.schema as TableSchema
203-
return {
204-
id: t.id,
205-
name: t.name,
206-
description: t.description,
207-
schema: {
208-
columns: schemaData.columns.map(normalizeColumn),
209-
},
210-
rowCount: t.rowCount,
211-
maxRows: t.maxRows,
212-
locks: t.locks,
213-
workspaceId: t.workspaceId,
214-
folderId: t.folderId ?? null,
215-
createdBy: t.createdBy,
216-
createdAt: t.createdAt instanceof Date ? t.createdAt.toISOString() : String(t.createdAt),
217-
updatedAt: t.updatedAt instanceof Date ? t.updatedAt.toISOString() : String(t.updatedAt),
218-
archivedAt:
219-
t.archivedAt instanceof Date
220-
? t.archivedAt.toISOString()
221-
: t.archivedAt
222-
? String(t.archivedAt)
223-
: null,
224-
jobStatus: t.jobStatus ?? null,
225-
jobId: t.jobId ?? null,
226-
jobType: t.jobType ?? null,
227-
jobError: t.jobError ?? null,
228-
jobRowsProcessed: t.jobRowsProcessed ?? 0,
229-
}
230-
})
231-
232202
return NextResponse.json({
233203
success: true,
234204
data: {
235-
tables: responseTables,
205+
tables: tables.map(toTableListItem),
236206
totalCount: tables.length,
237207
},
238208
})

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

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
import type { MultipartError } from '@/lib/core/utils/multipart'
1818
import type { ColumnDefinition, Filter, TableDefinition, TablePredicate } from '@/lib/table'
1919
import { buildFilterClause, getTableById, TableQueryValidationError } from '@/lib/table'
20-
import { typeMetadataOf } from '@/lib/table/column-types'
2120
import { USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants'
2221
import { TableLockedError } from '@/lib/table/mutation-locks'
2322
import { isTablePredicate } from '@/lib/table/query-builder/converters'
@@ -358,21 +357,3 @@ export function serverErrorResponse(message = 'Internal server error') {
358357
export const CreateColumnSchema = createTableColumnBodySchema
359358
export const UpdateColumnSchema = updateTableColumnBodySchema
360359
export const DeleteColumnSchema = deleteTableColumnBodySchema
361-
362-
export function normalizeColumn(
363-
col: ColumnDefinition
364-
): ColumnDefinition & { required: boolean; unique: boolean } {
365-
return {
366-
// Preserve the stable column id — it's the row-data storage key, so dropping
367-
// it makes clients fall back to `name` and miss id-keyed cell values.
368-
...(col.id ? { id: col.id } : {}),
369-
name: col.name,
370-
type: col.type,
371-
required: col.required ?? false,
372-
unique: col.unique ?? false,
373-
...(col.workflowGroupId ? { workflowGroupId: col.workflowGroupId } : {}),
374-
// Type-specific metadata is forwarded generically: naming keys here meant a
375-
// new type's metadata was stored server-side but silently never returned.
376-
...typeMetadataOf(col),
377-
}
378-
}

0 commit comments

Comments
 (0)