Skip to content

Commit 4e6b4dc

Browse files
committed
restructure custom tools to persist copilot generated tools
1 parent 272dae2 commit 4e6b4dc

File tree

4 files changed

+185
-185
lines changed

4 files changed

+185
-185
lines changed

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

Lines changed: 10 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { db } from '@sim/db'
22
import { customTools, workflow } from '@sim/db/schema'
3-
import { and, desc, eq, isNull, ne, or } from 'drizzle-orm'
3+
import { and, desc, eq, isNull, or } from 'drizzle-orm'
44
import { type NextRequest, NextResponse } from 'next/server'
55
import { z } from 'zod'
66
import { checkHybridAuth } from '@/lib/auth/hybrid'
7+
import { upsertCustomTools } from '@/lib/custom-tools/operations'
78
import { createLogger } from '@/lib/logs/console/logger'
89
import { getUserEntityPermissions } from '@/lib/permissions/utils'
910
import { generateRequestId } from '@/lib/utils'
@@ -151,126 +152,15 @@ export async function POST(req: NextRequest) {
151152
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
152153
}
153154

154-
// Use a transaction for multi-step database operations
155-
return await db.transaction(async (tx) => {
156-
// Process each tool: either update existing or create new
157-
for (const tool of tools) {
158-
const nowTime = new Date()
159-
160-
if (tool.id) {
161-
// First, check if tool exists in the workspace
162-
const existingWorkspaceTool = await tx
163-
.select()
164-
.from(customTools)
165-
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
166-
.limit(1)
167-
168-
if (existingWorkspaceTool.length > 0) {
169-
// Tool exists in workspace - check if name changed and if new name conflicts
170-
if (existingWorkspaceTool[0].title !== tool.title) {
171-
// Check for duplicate name in workspace (excluding current tool)
172-
const duplicateTool = await tx
173-
.select()
174-
.from(customTools)
175-
.where(
176-
and(
177-
eq(customTools.workspaceId, workspaceId),
178-
eq(customTools.title, tool.title),
179-
ne(customTools.id, tool.id)
180-
)
181-
)
182-
.limit(1)
183-
184-
if (duplicateTool.length > 0) {
185-
return NextResponse.json(
186-
{
187-
error: `A tool with the name "${tool.title}" already exists in this workspace`,
188-
},
189-
{ status: 400 }
190-
)
191-
}
192-
}
193-
194-
// Update existing workspace tool
195-
await tx
196-
.update(customTools)
197-
.set({
198-
title: tool.title,
199-
schema: tool.schema,
200-
code: tool.code,
201-
updatedAt: nowTime,
202-
})
203-
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
204-
continue
205-
}
206-
207-
// Check if this is a legacy tool (no workspaceId, belongs to user)
208-
const existingLegacyTool = await tx
209-
.select()
210-
.from(customTools)
211-
.where(
212-
and(
213-
eq(customTools.id, tool.id),
214-
isNull(customTools.workspaceId),
215-
eq(customTools.userId, userId)
216-
)
217-
)
218-
.limit(1)
219-
220-
if (existingLegacyTool.length > 0) {
221-
// Legacy tool found - update it without migrating to workspace
222-
await tx
223-
.update(customTools)
224-
.set({
225-
title: tool.title,
226-
schema: tool.schema,
227-
code: tool.code,
228-
updatedAt: nowTime,
229-
})
230-
.where(eq(customTools.id, tool.id))
231-
232-
logger.info(`[${requestId}] Updated legacy tool ${tool.id}`)
233-
continue
234-
}
235-
}
236-
237-
// Creating new tool - check for duplicate names in workspace
238-
const duplicateTool = await tx
239-
.select()
240-
.from(customTools)
241-
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.title, tool.title)))
242-
.limit(1)
243-
244-
if (duplicateTool.length > 0) {
245-
return NextResponse.json(
246-
{ error: `A tool with the name "${tool.title}" already exists in this workspace` },
247-
{ status: 400 }
248-
)
249-
}
250-
251-
// Create new tool
252-
const newToolId = tool.id || crypto.randomUUID()
253-
await tx.insert(customTools).values({
254-
id: newToolId,
255-
workspaceId,
256-
userId,
257-
title: tool.title,
258-
schema: tool.schema,
259-
code: tool.code,
260-
createdAt: nowTime,
261-
updatedAt: nowTime,
262-
})
263-
}
264-
265-
// Fetch and return the created/updated tools
266-
const resultTools = await tx
267-
.select()
268-
.from(customTools)
269-
.where(eq(customTools.workspaceId, workspaceId))
270-
.orderBy(desc(customTools.createdAt))
271-
272-
return NextResponse.json({ success: true, data: resultTools })
155+
// Use the extracted upsert function
156+
const resultTools = await upsertCustomTools({
157+
tools,
158+
workspaceId,
159+
userId,
160+
requestId,
273161
})
162+
163+
return NextResponse.json({ success: true, data: resultTools })
274164
} catch (validationError) {
275165
if (validationError instanceof z.ZodError) {
276166
logger.warn(`[${requestId}] Invalid custom tools data`, {

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel-new/components/editor/components/sub-block/components/tool-input/tool-input.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ export function ToolInput({
477477
const [draggedIndex, setDraggedIndex] = useState<number | null>(null)
478478
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null)
479479
const customTools = useCustomToolsStore((state) => state.getAllTools())
480+
const fetchCustomTools = useCustomToolsStore((state) => state.fetchTools)
480481
const subBlockStore = useSubBlockStore()
481482

482483
// MCP tools integration
@@ -487,6 +488,13 @@ export function ToolInput({
487488
refreshTools,
488489
} = useMcpTools(workspaceId)
489490

491+
// Fetch custom tools on mount
492+
useEffect(() => {
493+
if (workspaceId) {
494+
fetchCustomTools(workspaceId)
495+
}
496+
}, [workspaceId, fetchCustomTools])
497+
490498
// Get the current model from the 'model' subblock
491499
const modelValue = useSubBlockStore.getState().getValue(blockId, 'model')
492500
const model = typeof modelValue === 'string' ? modelValue : ''
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { db } from '@sim/db'
2+
import { customTools } from '@sim/db/schema'
3+
import { and, desc, eq, isNull } from 'drizzle-orm'
4+
import { createLogger } from '@/lib/logs/console/logger'
5+
import { generateRequestId } from '@/lib/utils'
6+
7+
const logger = createLogger('CustomToolsOperations')
8+
9+
/**
10+
* Internal function to create/update custom tools
11+
* Can be called from API routes or internal services
12+
*/
13+
export async function upsertCustomTools(params: {
14+
tools: Array<{
15+
id?: string
16+
title: string
17+
schema: any
18+
code: string
19+
}>
20+
workspaceId: string
21+
userId: string
22+
requestId?: string
23+
}) {
24+
const { tools, workspaceId, userId, requestId = generateRequestId() } = params
25+
26+
// Use a transaction for multi-step database operations
27+
return await db.transaction(async (tx) => {
28+
// Process each tool: either update existing or create new
29+
for (const tool of tools) {
30+
const nowTime = new Date()
31+
32+
if (tool.id) {
33+
// First, check if tool exists in the workspace
34+
const existingWorkspaceTool = await tx
35+
.select()
36+
.from(customTools)
37+
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
38+
.limit(1)
39+
40+
if (existingWorkspaceTool.length > 0) {
41+
// Tool exists in workspace
42+
const newFunctionName = tool.schema?.function?.name
43+
if (!newFunctionName) {
44+
throw new Error('Tool schema must include a function name')
45+
}
46+
47+
// Check if function name has changed
48+
if (tool.id !== newFunctionName) {
49+
throw new Error(
50+
`Cannot change function name from "${tool.id}" to "${newFunctionName}". Please create a new tool instead.`
51+
)
52+
}
53+
54+
// Update existing workspace tool
55+
await tx
56+
.update(customTools)
57+
.set({
58+
title: tool.title,
59+
schema: tool.schema,
60+
code: tool.code,
61+
updatedAt: nowTime,
62+
})
63+
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
64+
continue
65+
}
66+
67+
// Check if this is a legacy tool (no workspaceId, belongs to user)
68+
const existingLegacyTool = await tx
69+
.select()
70+
.from(customTools)
71+
.where(
72+
and(
73+
eq(customTools.id, tool.id),
74+
isNull(customTools.workspaceId),
75+
eq(customTools.userId, userId)
76+
)
77+
)
78+
.limit(1)
79+
80+
if (existingLegacyTool.length > 0) {
81+
// Legacy tool found - update it without migrating to workspace
82+
await tx
83+
.update(customTools)
84+
.set({
85+
title: tool.title,
86+
schema: tool.schema,
87+
code: tool.code,
88+
updatedAt: nowTime,
89+
})
90+
.where(eq(customTools.id, tool.id))
91+
92+
logger.info(`[${requestId}] Updated legacy tool ${tool.id}`)
93+
continue
94+
}
95+
}
96+
97+
// Creating new tool - use function name as ID for consistency
98+
const functionName = tool.schema?.function?.name
99+
if (!functionName) {
100+
throw new Error('Tool schema must include a function name')
101+
}
102+
103+
// Check for duplicate function names in workspace
104+
const duplicateFunction = await tx
105+
.select()
106+
.from(customTools)
107+
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.id, functionName)))
108+
.limit(1)
109+
110+
if (duplicateFunction.length > 0) {
111+
throw new Error(
112+
`A tool with the function name "${functionName}" already exists in this workspace`
113+
)
114+
}
115+
116+
// Create new tool using function name as ID
117+
await tx.insert(customTools).values({
118+
id: functionName,
119+
workspaceId,
120+
userId,
121+
title: tool.title,
122+
schema: tool.schema,
123+
code: tool.code,
124+
createdAt: nowTime,
125+
updatedAt: nowTime,
126+
})
127+
}
128+
129+
// Fetch and return the created/updated tools
130+
const resultTools = await tx
131+
.select()
132+
.from(customTools)
133+
.where(eq(customTools.workspaceId, workspaceId))
134+
.orderBy(desc(customTools.createdAt))
135+
136+
return resultTools
137+
})
138+
}

0 commit comments

Comments
 (0)