Skip to content

Commit 21a640a

Browse files
authored
fix(custom-tools): add composite index on custom tool names & workspace id (#2131)
1 parent a10e1a6 commit 21a640a

File tree

6 files changed

+7717
-46
lines changed

6 files changed

+7717
-46
lines changed

apps/sim/lib/custom-tools/operations.ts

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { db } from '@sim/db'
22
import { customTools } from '@sim/db/schema'
33
import { and, desc, eq, isNull } from 'drizzle-orm'
4+
import { nanoid } from 'nanoid'
45
import { createLogger } from '@/lib/logs/console/logger'
56
import { generateRequestId } from '@/lib/utils'
67

@@ -23,35 +24,18 @@ export async function upsertCustomTools(params: {
2324
}) {
2425
const { tools, workspaceId, userId, requestId = generateRequestId() } = params
2526

26-
// Use a transaction for multi-step database operations
2727
return await db.transaction(async (tx) => {
28-
// Process each tool: either update existing or create new
2928
for (const tool of tools) {
3029
const nowTime = new Date()
3130

3231
if (tool.id) {
33-
// First, check if tool exists in the workspace
3432
const existingWorkspaceTool = await tx
3533
.select()
3634
.from(customTools)
3735
.where(and(eq(customTools.id, tool.id), eq(customTools.workspaceId, workspaceId)))
3836
.limit(1)
3937

4038
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
5539
await tx
5640
.update(customTools)
5741
.set({
@@ -64,7 +48,6 @@ export async function upsertCustomTools(params: {
6448
continue
6549
}
6650

67-
// Check if this is a legacy tool (no workspaceId, belongs to user)
6851
const existingLegacyTool = await tx
6952
.select()
7053
.from(customTools)
@@ -78,7 +61,6 @@ export async function upsertCustomTools(params: {
7861
.limit(1)
7962

8063
if (existingLegacyTool.length > 0) {
81-
// Legacy tool found - update it without migrating to workspace
8264
await tx
8365
.update(customTools)
8466
.set({
@@ -94,28 +76,18 @@ export async function upsertCustomTools(params: {
9476
}
9577
}
9678

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
79+
const duplicateTitle = await tx
10580
.select()
10681
.from(customTools)
107-
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.id, functionName)))
82+
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.title, tool.title)))
10883
.limit(1)
10984

110-
if (duplicateFunction.length > 0) {
111-
throw new Error(
112-
`A tool with the function name "${functionName}" already exists in this workspace`
113-
)
85+
if (duplicateTitle.length > 0) {
86+
throw new Error(`A tool with the title "${tool.title}" already exists in this workspace`)
11487
}
11588

116-
// Create new tool using function name as ID
11789
await tx.insert(customTools).values({
118-
id: functionName,
90+
id: nanoid(),
11991
workspaceId,
12092
userId,
12193
title: tool.title,
@@ -126,7 +98,6 @@ export async function upsertCustomTools(params: {
12698
})
12799
}
128100

129-
// Fetch and return the created/updated tools
130101
const resultTools = await tx
131102
.select()
132103
.from(customTools)

apps/sim/lib/workflows/custom-tools-persistence.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
3333
try {
3434
const blockData = block as any
3535

36-
// Only process agent blocks
3736
if (!blockData || blockData.type !== 'agent') {
3837
continue
3938
}
@@ -47,7 +46,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
4746

4847
let tools = toolsSubBlock.value
4948

50-
// Parse if it's a string
5149
if (typeof tools === 'string') {
5250
try {
5351
tools = JSON.parse(tools)
@@ -61,7 +59,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
6159
continue
6260
}
6361

64-
// Extract custom tools
6562
for (const tool of tools) {
6663
if (
6764
tool &&
@@ -71,10 +68,8 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
7168
tool.schema?.function &&
7269
tool.code
7370
) {
74-
// Use toolId if available, otherwise generate one from title
7571
const toolKey = tool.toolId || tool.title
7672

77-
// Deduplicate by toolKey (if same tool appears in multiple blocks)
7873
if (!customToolsMap.has(toolKey)) {
7974
customToolsMap.set(toolKey, tool as CustomTool)
8075
}
@@ -101,8 +96,6 @@ export async function persistCustomToolsToDatabase(
10196
return { saved: 0, errors: [] }
10297
}
10398

104-
// Only persist if workspaceId is provided (new workspace-scoped tools)
105-
// Skip persistence for existing user-scoped tools to maintain backward compatibility
10699
if (!workspaceId) {
107100
logger.debug('Skipping custom tools persistence - no workspaceId provided (user-scoped tools)')
108101
return { saved: 0, errors: [] }
@@ -111,7 +104,6 @@ export async function persistCustomToolsToDatabase(
111104
const errors: string[] = []
112105
let saved = 0
113106

114-
// Filter out tools without function names
115107
const validTools = customToolsList.filter((tool) => {
116108
if (!tool.schema?.function?.name) {
117109
logger.warn(`Skipping custom tool without function name: ${tool.title}`)
@@ -125,10 +117,9 @@ export async function persistCustomToolsToDatabase(
125117
}
126118

127119
try {
128-
// Call the upsert function from lib
129120
await upsertCustomTools({
130121
tools: validTools.map((tool) => ({
131-
id: tool.schema.function.name, // Use function name as ID for updates
122+
id: tool.toolId,
132123
title: tool.title,
133124
schema: tool.schema,
134125
code: tool.code,
@@ -149,7 +140,7 @@ export async function persistCustomToolsToDatabase(
149140
}
150141

151142
/**
152-
* Extract and persist custom tools from workflow state in one operation
143+
* Extract and persist custom tools from workflow state
153144
*/
154145
export async function extractAndPersistCustomTools(
155146
workflowState: any,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
-- Step 1: Convert non-UUID IDs to UUIDs (preserve existing UUIDs)
2+
-- This allows same title in different workspaces by removing function-name-based IDs
3+
UPDATE "custom_tools"
4+
SET "id" = gen_random_uuid()::text
5+
WHERE workspace_id IS NOT NULL -- Only update workspace-scoped tools
6+
AND "id" !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'; -- Skip if already UUID
7+
8+
-- Step 2: Add composite unique constraint on (workspace_id, title)
9+
-- This enforces uniqueness per workspace, not globally
10+
CREATE UNIQUE INDEX "custom_tools_workspace_title_unique" ON "custom_tools" USING btree ("workspace_id","title");

0 commit comments

Comments
 (0)