|
1 | | -import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' |
2 | 1 | import { |
3 | 2 | v2DeleteCustomToolContract, |
4 | 3 | v2GetCustomToolContract, |
5 | 4 | v2UpdateCustomToolContract, |
6 | 5 | } from '@/lib/api/contracts/v2/custom-tools' |
7 | 6 | 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' |
17 | 19 |
|
18 | 20 | export const dynamic = 'force-dynamic' |
19 | 21 | export const revalidate = 0 |
20 | 22 |
|
21 | | -interface RouteContext { |
22 | | - params: Promise<{ id: string }> |
23 | | -} |
24 | | - |
25 | 23 | /** GET /api/v2/custom-tools/[id] — Fetch a single custom tool. */ |
26 | | -export const GET = withPublicApiRouteHandler({ |
| 24 | +export const GET = defineV2JsonRoute({ |
27 | 25 | 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) } }), |
41 | 33 | }) |
42 | 34 |
|
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({ |
45 | 37 | 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) } }), |
101 | 49 | }) |
102 | 50 |
|
103 | 51 | /** DELETE /api/v2/custom-tools/[id] — Delete a custom tool. */ |
104 | | -export const DELETE = withPublicApiRouteHandler({ |
| 52 | +export const DELETE = defineV2JsonRoute({ |
105 | 53 | 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 } }), |
133 | 65 | }) |
0 commit comments