Skip to content

Commit 5de6269

Browse files
improvement(api): centralize remaining v2 resource operations (#6412)
* improvement(api): centralize v2 resource operations * fix(api): preserve custom tool conflict errors
1 parent b177b3a commit 5de6269

53 files changed

Lines changed: 4376 additions & 3008 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/v2/custom-tools/[id]/route.test.ts

Lines changed: 124 additions & 269 deletions
Large diffs are not rendered by default.
Lines changed: 45 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,65 @@
1-
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
21
import {
32
v2DeleteCustomToolContract,
43
v2GetCustomToolContract,
54
v2UpdateCustomToolContract,
65
} from '@/lib/api/contracts/v2/custom-tools'
76
import {
8-
deleteWorkspaceCustomTool,
9-
getWorkspaceCustomTool,
10-
getWorkspaceCustomToolByTitle,
11-
updateWorkspaceCustomTool,
12-
} from '@/lib/workflows/custom-tools/operations'
13-
import { withPublicApiRouteHandler } from '@/app/api/public-api-route-handler'
14-
import { resolveWorkspaceAccess } from '@/app/api/v1/middleware'
15-
import { toV2CustomTool, v2CustomToolWriteError } from '@/app/api/v2/custom-tools/utils'
16-
import { v2Data, v2Error, v2WorkspaceAccessError } from '@/app/api/v2/lib/response'
7+
defineV2JsonRoute,
8+
v2ApiKeyAuth,
9+
v2OrchestrationErrorPolicy,
10+
v2RateLimits,
11+
} from '@/lib/api/server/routes'
12+
import { customToolOperations } from '@/lib/custom-tools/application/operations'
13+
import {
14+
deleteWorkspaceCustomToolUseCase,
15+
getWorkspaceCustomToolUseCase,
16+
updateWorkspaceCustomToolUseCase,
17+
} from '@/lib/custom-tools/application/use-cases'
18+
import { toV2CustomTool } from '@/app/api/v2/custom-tools/utils'
1719

1820
export const dynamic = 'force-dynamic'
1921
export const revalidate = 0
2022

21-
interface RouteContext {
22-
params: Promise<{ id: string }>
23-
}
24-
2523
/** GET /api/v2/custom-tools/[id] — Fetch a single custom tool. */
26-
export const GET = withPublicApiRouteHandler({
24+
export const GET = defineV2JsonRoute({
2725
contract: v2GetCustomToolContract,
28-
rateLimitEndpoint: 'custom-tool-detail',
29-
handler: async ({ input, auth: { userId, rateLimit } }) => {
30-
const { id } = input.params
31-
const { workspaceId } = input.query
32-
33-
const access = await resolveWorkspaceAccess(rateLimit, userId, workspaceId, 'read')
34-
if (access) return v2WorkspaceAccessError(access)
35-
36-
const tool = await getWorkspaceCustomTool({ workspaceId, toolId: id })
37-
if (!tool) return v2Error('NOT_FOUND', 'Custom tool not found')
38-
39-
return v2Data({ customTool: toV2CustomTool(tool) }, { rateLimit })
40-
},
26+
operation: customToolOperations.read,
27+
auth: v2ApiKeyAuth,
28+
rateLimit: v2RateLimits.publicApi,
29+
errorPolicy: v2OrchestrationErrorPolicy,
30+
mapInput: ({ params, query }) => ({ workspaceId: query.workspaceId, toolId: params.id }),
31+
useCase: getWorkspaceCustomToolUseCase,
32+
present: ({ tool }) => ({ data: { customTool: toV2CustomTool(tool) } }),
4133
})
4234

43-
/** PATCH /api/v2/custom-tools/[id] — Update a custom tool. Omitted fields keep their values. */
44-
export const PATCH = withPublicApiRouteHandler({
35+
/** PATCH /api/v2/custom-tools/[id] — Update a custom tool. */
36+
export const PATCH = defineV2JsonRoute({
4537
contract: v2UpdateCustomToolContract,
46-
rateLimitEndpoint: 'custom-tool-detail',
47-
handler: async ({ request, input, auth: { userId, rateLimit } }) => {
48-
try {
49-
const { id } = input.params
50-
const { workspaceId, title, schema, code } = input.body
51-
52-
const access = await resolveWorkspaceAccess(rateLimit, userId, workspaceId, 'write')
53-
if (access) return v2WorkspaceAccessError(access)
54-
55-
const current = await getWorkspaceCustomTool({ workspaceId, toolId: id })
56-
if (!current) return v2Error('NOT_FOUND', 'Custom tool not found')
57-
58-
/**
59-
* `upsertCustomTools` replaces title/schema/code wholesale and checks for a
60-
* duplicate title only when inserting, so a rename onto an existing title
61-
* would hit the `custom_tools_workspace_title_unique` index as a 500. Merge
62-
* the partial body against the stored row and check the rename here.
63-
*/
64-
if (title !== undefined && title !== current.title) {
65-
if (await getWorkspaceCustomToolByTitle({ workspaceId, title })) {
66-
return v2Error(
67-
'CONFLICT',
68-
`A custom tool titled "${title}" already exists in this workspace`
69-
)
70-
}
71-
}
72-
73-
const updated = await updateWorkspaceCustomTool({
74-
workspaceId,
75-
toolId: id,
76-
title: title ?? current.title,
77-
schema: schema ?? current.schema,
78-
code: code ?? current.code,
79-
})
80-
if (!updated) return v2Error('NOT_FOUND', 'Custom tool not found')
81-
82-
recordAudit({
83-
workspaceId,
84-
actorId: userId,
85-
action: AuditAction.CUSTOM_TOOL_UPDATED,
86-
resourceType: AuditResourceType.CUSTOM_TOOL,
87-
resourceId: updated.id,
88-
resourceName: updated.title,
89-
description: `Updated custom tool "${updated.title}" via API`,
90-
request,
91-
})
92-
93-
return v2Data({ customTool: toV2CustomTool(updated) }, { rateLimit })
94-
} catch (error) {
95-
const writeError = v2CustomToolWriteError(error)
96-
if (writeError) return writeError
97-
98-
throw error
99-
}
100-
},
38+
operation: customToolOperations.update,
39+
auth: v2ApiKeyAuth,
40+
rateLimit: v2RateLimits.publicApi,
41+
errorPolicy: v2OrchestrationErrorPolicy,
42+
mapInput: ({ params, body }) => ({
43+
...body,
44+
toolId: params.id,
45+
source: 'api' as const,
46+
}),
47+
useCase: updateWorkspaceCustomToolUseCase,
48+
present: ({ tool }) => ({ data: { customTool: toV2CustomTool(tool) } }),
10149
})
10250

10351
/** DELETE /api/v2/custom-tools/[id] — Delete a custom tool. */
104-
export const DELETE = withPublicApiRouteHandler({
52+
export const DELETE = defineV2JsonRoute({
10553
contract: v2DeleteCustomToolContract,
106-
rateLimitEndpoint: 'custom-tool-detail',
107-
handler: async ({ request, input, auth: { userId, rateLimit } }) => {
108-
const { id } = input.params
109-
const { workspaceId } = input.query
110-
111-
const access = await resolveWorkspaceAccess(rateLimit, userId, workspaceId, 'write')
112-
if (access) return v2WorkspaceAccessError(access)
113-
114-
const tool = await getWorkspaceCustomTool({ workspaceId, toolId: id })
115-
if (!tool) return v2Error('NOT_FOUND', 'Custom tool not found')
116-
117-
const deleted = await deleteWorkspaceCustomTool({ workspaceId, toolId: id })
118-
if (!deleted) return v2Error('NOT_FOUND', 'Custom tool not found')
119-
120-
recordAudit({
121-
workspaceId,
122-
actorId: userId,
123-
action: AuditAction.CUSTOM_TOOL_DELETED,
124-
resourceType: AuditResourceType.CUSTOM_TOOL,
125-
resourceId: id,
126-
resourceName: tool.title,
127-
description: `Deleted custom tool "${tool.title}" via API`,
128-
request,
129-
})
130-
131-
return v2Data({ id, deleted: true as const }, { rateLimit })
132-
},
54+
operation: customToolOperations.delete,
55+
auth: v2ApiKeyAuth,
56+
rateLimit: v2RateLimits.publicApi,
57+
errorPolicy: v2OrchestrationErrorPolicy,
58+
mapInput: ({ params, query }) => ({
59+
workspaceId: query.workspaceId,
60+
toolId: params.id,
61+
source: 'api' as const,
62+
}),
63+
useCase: deleteWorkspaceCustomToolUseCase,
64+
present: ({ tool }) => ({ data: { id: tool.id, deleted: true as const } }),
13365
})

0 commit comments

Comments
 (0)