Skip to content

Commit 7125eac

Browse files
committed
refactor(v2): collapse duplicated cursor and validation mechanisms, drop dead exports
One rule, one implementation: - `parseRequest` hand-inlined the "caller envelope or default" validation-error projection four times. Extract `projectValidationError` and route all four through it. - Nine keyset lists hand-rolled the `present` half of the cursor pair that `readSortedCursor` already owns the read half of. Add the symmetric `writeSortedCursor` and use it everywhere. - `GET /workflows/{id}/runs` re-derived `readSortedCursor`'s invalid/refiltered ladder from `decodeSortedCursor`; it now calls the shared reader and keeps only the key-arity check that is genuinely its own. Files and exports that no longer earn their place: - Inline `credentials/utils.ts` into its single consumer. - Delete symbols with zero references repo-wide: `v2CustomToolWriteError`, `secretCredentialTypes`, `v2CursorList`, `v2WorkspaceAccessError`, `resolveFolderPathIdentity`, `folderPathForId`, `v2FolderPathMutationError`, and seven of twelve `tables/utils.ts` exports. - Drop `export` from symbols used only inside their own module. No behavior change; every response body and error message is byte-identical.
1 parent f2f8311 commit 7125eac

21 files changed

Lines changed: 187 additions & 479 deletions

File tree

apps/sim/app/api/v2/credentials/route.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { V2Credential } from '@/lib/api/contracts/v2/credentials'
12
import { v2ListCredentialsContract } from '@/lib/api/contracts/v2/credentials'
23
import { cursorScopeKey } from '@/lib/api/cursor-binding'
34
import {
@@ -8,12 +9,32 @@ import {
89
} from '@/lib/api/server/routes'
910
import { listWorkspaceCredentials } from '@/lib/credentials/application/list-workspace-credentials'
1011
import { credentialOperations } from '@/lib/credentials/application/operations'
11-
import { toV2Credential } from '@/app/api/v2/credentials/utils'
12-
import { cursorSortKey, encodeSortedCursor, readSortedCursor } from '@/app/api/v2/lib/response'
12+
import type { VisibleWorkspaceCredential } from '@/lib/credentials/queries'
13+
import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response'
1314

1415
export const dynamic = 'force-dynamic'
1516
export const revalidate = 0
1617

18+
/** Serialize connection metadata field by field so encrypted columns can never reach the wire. */
19+
function toV2Credential(row: VisibleWorkspaceCredential): V2Credential {
20+
if (row.type !== 'oauth' && row.type !== 'service_account') {
21+
throw new Error(`Secret credential type ${row.type} reached the credentials API`)
22+
}
23+
24+
return {
25+
id: row.id,
26+
type: row.type,
27+
displayName: row.displayName,
28+
description: row.description,
29+
providerId: row.providerId,
30+
accountId: row.accountId,
31+
hasServiceAccountKey: row.hasServiceAccountKey,
32+
role: row.role,
33+
createdAt: row.createdAt.toISOString(),
34+
updatedAt: row.updatedAt.toISOString(),
35+
}
36+
}
37+
1738
/** Every param that changes which credentials, in which order, this list returns. */
1839
function credentialCursorFilters(query: {
1940
workspaceId: string
@@ -48,12 +69,11 @@ export const GET = defineV2JsonRoute({
4869
useCase: listWorkspaceCredentials,
4970
present: ({ credentials, nextCursorKeys }, { query }) => ({
5071
data: credentials.map(toV2Credential),
51-
nextCursor: nextCursorKeys
52-
? encodeSortedCursor(
53-
cursorSortKey(query.sortBy, query.sortOrder),
54-
nextCursorKeys,
55-
credentialCursorFilters(query)
56-
)
57-
: null,
72+
nextCursor: writeSortedCursor(
73+
nextCursorKeys,
74+
query.sortBy,
75+
query.sortOrder,
76+
credentialCursorFilters(query)
77+
),
5878
}),
5979
})

apps/sim/app/api/v2/credentials/utils.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

apps/sim/app/api/v2/custom-tools/route.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
listWorkspaceCustomToolsUseCase,
1616
} from '@/lib/custom-tools/application/use-cases'
1717
import { toV2CustomTool } from '@/app/api/v2/custom-tools/utils'
18-
import { cursorSortKey, encodeSortedCursor, readSortedCursor } from '@/app/api/v2/lib/response'
18+
import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response'
1919

2020
export const dynamic = 'force-dynamic'
2121
export const revalidate = 0
@@ -47,13 +47,12 @@ export const GET = defineV2JsonRoute({
4747
useCase: listWorkspaceCustomToolsUseCase,
4848
present: ({ tools, nextCursorKeys }, { query }) => ({
4949
data: tools.map(toV2CustomTool),
50-
nextCursor: nextCursorKeys
51-
? encodeSortedCursor(
52-
cursorSortKey(query.sortBy, query.sortOrder),
53-
nextCursorKeys,
54-
customToolCursorFilters(query)
55-
)
56-
: null,
50+
nextCursor: writeSortedCursor(
51+
nextCursorKeys,
52+
query.sortBy,
53+
query.sortOrder,
54+
customToolCursorFilters(query)
55+
),
5756
}),
5857
})
5958

apps/sim/app/api/v2/custom-tools/utils.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,8 @@
11
import type { customTools } from '@sim/db/schema'
2-
import { getErrorMessage, getPostgresErrorCode } from '@sim/utils/errors'
3-
import type { NextResponse } from 'next/server'
42
import type { V2CustomTool } from '@/lib/api/contracts/v2/custom-tools'
5-
import { v2Error } from '@/app/api/v2/lib/response'
63

74
/** Shared serialization + error mapping for the v2 custom tool surface. */
85

9-
/**
10-
* Classifies a title collision as a conflict so it surfaces as 409 rather than a
11-
* generic 500. Two distinct failures reach here and both must be covered:
12-
*
13-
* - `upsertCustomTools` throws its own message when its in-transaction duplicate
14-
* `SELECT` finds one.
15-
* - Under a concurrent create or rename, both callers pass that `SELECT` too, and
16-
* the loser is rejected by `custom_tools_workspace_title_unique` as a raw
17-
* Postgres `23505` — whose message matches nothing, which is exactly the race
18-
* the message check alone cannot see.
19-
*/
20-
export function v2CustomToolWriteError(error: unknown): NextResponse | null {
21-
if (getPostgresErrorCode(error) === '23505') {
22-
return v2Error('CONFLICT', 'A custom tool with that title already exists in this workspace')
23-
}
24-
const message = getErrorMessage(error, '')
25-
if (/already exists in this workspace/i.test(message)) {
26-
return v2Error('CONFLICT', message)
27-
}
28-
return null
29-
}
30-
316
type CustomToolRow = typeof customTools.$inferSelect
327

338
/**

apps/sim/app/api/v2/files/route.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { queryWorkspaceFilePage } from '@/lib/workspace-files/application/list-w
1212
import { fileOperations } from '@/lib/workspace-files/application/operations'
1313
import { MAX_WORKSPACE_FILE_INLINE_BODY_BYTES } from '@/lib/workspace-files/orchestration'
1414
import { toV2File, toV2Files } from '@/app/api/v2/files/utils'
15-
import { cursorSortKey, encodeSortedCursor, readSortedCursor } from '@/app/api/v2/lib/response'
15+
import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response'
1616

1717
export const dynamic = 'force-dynamic'
1818
export const revalidate = 0
@@ -54,13 +54,12 @@ export const GET = defineV2JsonRoute({
5454
const items: V2File[] = await toV2Files(files)
5555
return {
5656
data: items,
57-
nextCursor: nextKeys
58-
? encodeSortedCursor(
59-
cursorSortKey(query.sortBy, query.sortOrder),
60-
nextKeys,
61-
fileCursorFilters(query)
62-
)
63-
: null,
57+
nextCursor: writeSortedCursor(
58+
nextKeys,
59+
query.sortBy,
60+
query.sortOrder,
61+
fileCursorFilters(query)
62+
),
6463
}
6564
},
6665
})

apps/sim/app/api/v2/knowledge/route.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
import { knowledgeOperations } from '@/lib/knowledge/application/operations'
1818
import { captureServerEvent } from '@/lib/posthog/server'
1919
import { toV2KnowledgeBase, toV2KnowledgeBases } from '@/app/api/v2/knowledge/utils'
20-
import { cursorSortKey, encodeSortedCursor, readSortedCursor } from '@/app/api/v2/lib/response'
20+
import { readSortedCursor, writeSortedCursor } from '@/app/api/v2/lib/response'
2121

2222
export const dynamic = 'force-dynamic'
2323
export const revalidate = 0
@@ -59,13 +59,12 @@ export const GET = defineV2JsonRoute({
5959
useCase: listKnowledgeBases,
6060
present: async ({ knowledgeBases, nextCursorKeys }, { query }) => ({
6161
data: await toV2KnowledgeBases(knowledgeBases),
62-
nextCursor: nextCursorKeys
63-
? encodeSortedCursor(
64-
cursorSortKey(query.sortBy, query.sortOrder),
65-
nextCursorKeys,
66-
knowledgeCursorFilters(query)
67-
)
68-
: null,
62+
nextCursor: writeSortedCursor(
63+
nextCursorKeys,
64+
query.sortBy,
65+
query.sortOrder,
66+
knowledgeCursorFilters(query)
67+
),
6968
}),
7069
})
7170

apps/sim/app/api/v2/knowledge/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type V2DocumentProcessingStatus = (typeof PROCESSING_STATUSES)[number]
4848
* reads as `pending`, matching the column default; an unrecognised one is a
4949
* producer bug rather than a caller-reachable failure, so it throws.
5050
*/
51-
export function toProcessingStatus(status: string | null | undefined): V2DocumentProcessingStatus {
51+
function toProcessingStatus(status: string | null | undefined): V2DocumentProcessingStatus {
5252
if (status === null || status === undefined) return 'pending'
5353
const known = PROCESSING_STATUSES.find((candidate) => candidate === status)
5454
if (!known) throw new Error(`Unexpected knowledge document processing status: ${status}`)
@@ -59,7 +59,7 @@ export function toProcessingStatus(status: string | null | undefined): V2Documen
5959
* The document columns every v2 document projection reads. `uploadedAt` is
6060
* accepted as nullable because the column is nullable in storage.
6161
*/
62-
export interface V2DocumentSummarySource {
62+
interface V2DocumentSummarySource {
6363
id: string
6464
knowledgeBaseId: string
6565
filename: string

apps/sim/app/api/v2/lib/folders.ts

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,12 @@
11
import type { folder } from '@sim/db/schema'
2-
import type { NextResponse } from 'next/server'
3-
import type { FolderResourceType } from '@/lib/api/contracts/folders'
4-
import type { OrchestrationErrorCode } from '@/lib/core/orchestration/types'
5-
import { withFolderTreeLock } from '@/lib/folders/locks'
62
import {
73
type FolderPathIndex,
84
isFolderPathEffectivelyLocked,
9-
ROOT_FOLDER_PATH,
105
toFolderPathView,
116
} from '@/lib/folders/paths'
12-
import { loadActiveFolderPathIndex } from '@/lib/folders/queries'
13-
import { v2ErrorForOrchestration } from '@/app/api/v2/lib/response'
147

158
type FolderRow = typeof folder.$inferSelect
169

17-
export function resolveFolderPathId(
18-
index: FolderPathIndex<FolderRow>,
19-
path: string
20-
): string | null | undefined {
21-
return path === ROOT_FOLDER_PATH ? null : index.idByPath.get(path)
22-
}
23-
24-
export type ResolvedFolderPathIdentity =
25-
| { found: false }
26-
| { found: true; folderId: string | null; index: FolderPathIndex<FolderRow> }
27-
28-
/** Resolves a path to its stable internal identity under a short-lived folder tree lock. */
29-
export async function resolveFolderPathIdentity(params: {
30-
workspaceId: string
31-
resourceType: FolderResourceType
32-
path: string
33-
}): Promise<ResolvedFolderPathIdentity> {
34-
return withFolderTreeLock(params.workspaceId, params.resourceType, async (tx) => {
35-
const index = await loadActiveFolderPathIndex(params.workspaceId, params.resourceType, tx)
36-
const folderId = resolveFolderPathId(index, params.path)
37-
return folderId === undefined ? { found: false } : { found: true, folderId, index }
38-
})
39-
}
40-
41-
export function folderPathForId(
42-
index: FolderPathIndex<FolderRow>,
43-
folderId: string | null | undefined
44-
): string {
45-
if (!folderId) return ROOT_FOLDER_PATH
46-
const path = index.pathById.get(folderId)
47-
if (!path) throw new Error('Resource references an inactive or missing folder')
48-
return path
49-
}
50-
5110
export function toV2PathFolder(
5211
row: FolderRow,
5312
index: FolderPathIndex<FolderRow>,
@@ -58,10 +17,3 @@ export function toV2PathFolder(
5817
const base = toFolderPathView(row, path)
5918
return includeLocked ? { ...base, locked: isFolderPathEffectivelyLocked(index, row.id) } : base
6019
}
61-
62-
export function v2FolderPathMutationError(
63-
errorCode: OrchestrationErrorCode | undefined,
64-
message: string
65-
): NextResponse {
66-
return v2ErrorForOrchestration(errorCode, message)
67-
}

0 commit comments

Comments
 (0)