fix(custom-tools): add composite index on custom tool names & workspace id (#2131)

This commit is contained in:
Waleed
2025-11-28 15:33:08 -08:00
committed by GitHub
parent a10e1a63af
commit 21a640af50
6 changed files with 7717 additions and 46 deletions

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { customTools } from '@sim/db/schema'
import { and, desc, eq, isNull } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { createLogger } from '@/lib/logs/console/logger'
import { generateRequestId } from '@/lib/utils'
@@ -23,14 +24,11 @@ export async function upsertCustomTools(params: {
}) {
const { tools, workspaceId, userId, requestId = generateRequestId() } = params
// Use a transaction for multi-step database operations
return await db.transaction(async (tx) => {
// Process each tool: either update existing or create new
for (const tool of tools) {
const nowTime = new Date()
if (tool.id) {
// First, check if tool exists in the workspace
const existingWorkspaceTool = await tx
.select()
.from(customTools)
@@ -38,20 +36,6 @@ export async function upsertCustomTools(params: {
.limit(1)
if (existingWorkspaceTool.length > 0) {
// Tool exists in workspace
const newFunctionName = tool.schema?.function?.name
if (!newFunctionName) {
throw new Error('Tool schema must include a function name')
}
// Check if function name has changed
if (tool.id !== newFunctionName) {
throw new Error(
`Cannot change function name from "${tool.id}" to "${newFunctionName}". Please create a new tool instead.`
)
}
// Update existing workspace tool
await tx
.update(customTools)
.set({
@@ -64,7 +48,6 @@ export async function upsertCustomTools(params: {
continue
}
// Check if this is a legacy tool (no workspaceId, belongs to user)
const existingLegacyTool = await tx
.select()
.from(customTools)
@@ -78,7 +61,6 @@ export async function upsertCustomTools(params: {
.limit(1)
if (existingLegacyTool.length > 0) {
// Legacy tool found - update it without migrating to workspace
await tx
.update(customTools)
.set({
@@ -94,28 +76,18 @@ export async function upsertCustomTools(params: {
}
}
// Creating new tool - use function name as ID for consistency
const functionName = tool.schema?.function?.name
if (!functionName) {
throw new Error('Tool schema must include a function name')
}
// Check for duplicate function names in workspace
const duplicateFunction = await tx
const duplicateTitle = await tx
.select()
.from(customTools)
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.id, functionName)))
.where(and(eq(customTools.workspaceId, workspaceId), eq(customTools.title, tool.title)))
.limit(1)
if (duplicateFunction.length > 0) {
throw new Error(
`A tool with the function name "${functionName}" already exists in this workspace`
)
if (duplicateTitle.length > 0) {
throw new Error(`A tool with the title "${tool.title}" already exists in this workspace`)
}
// Create new tool using function name as ID
await tx.insert(customTools).values({
id: functionName,
id: nanoid(),
workspaceId,
userId,
title: tool.title,
@@ -126,7 +98,6 @@ export async function upsertCustomTools(params: {
})
}
// Fetch and return the created/updated tools
const resultTools = await tx
.select()
.from(customTools)

View File

@@ -33,7 +33,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
try {
const blockData = block as any
// Only process agent blocks
if (!blockData || blockData.type !== 'agent') {
continue
}
@@ -47,7 +46,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
let tools = toolsSubBlock.value
// Parse if it's a string
if (typeof tools === 'string') {
try {
tools = JSON.parse(tools)
@@ -61,7 +59,6 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
continue
}
// Extract custom tools
for (const tool of tools) {
if (
tool &&
@@ -71,10 +68,8 @@ export function extractCustomToolsFromWorkflowState(workflowState: any): CustomT
tool.schema?.function &&
tool.code
) {
// Use toolId if available, otherwise generate one from title
const toolKey = tool.toolId || tool.title
// Deduplicate by toolKey (if same tool appears in multiple blocks)
if (!customToolsMap.has(toolKey)) {
customToolsMap.set(toolKey, tool as CustomTool)
}
@@ -101,8 +96,6 @@ export async function persistCustomToolsToDatabase(
return { saved: 0, errors: [] }
}
// Only persist if workspaceId is provided (new workspace-scoped tools)
// Skip persistence for existing user-scoped tools to maintain backward compatibility
if (!workspaceId) {
logger.debug('Skipping custom tools persistence - no workspaceId provided (user-scoped tools)')
return { saved: 0, errors: [] }
@@ -111,7 +104,6 @@ export async function persistCustomToolsToDatabase(
const errors: string[] = []
let saved = 0
// Filter out tools without function names
const validTools = customToolsList.filter((tool) => {
if (!tool.schema?.function?.name) {
logger.warn(`Skipping custom tool without function name: ${tool.title}`)
@@ -125,10 +117,9 @@ export async function persistCustomToolsToDatabase(
}
try {
// Call the upsert function from lib
await upsertCustomTools({
tools: validTools.map((tool) => ({
id: tool.schema.function.name, // Use function name as ID for updates
id: tool.toolId,
title: tool.title,
schema: tool.schema,
code: tool.code,
@@ -149,7 +140,7 @@ export async function persistCustomToolsToDatabase(
}
/**
* Extract and persist custom tools from workflow state in one operation
* Extract and persist custom tools from workflow state
*/
export async function extractAndPersistCustomTools(
workflowState: any,

View File

@@ -0,0 +1,10 @@
-- Step 1: Convert non-UUID IDs to UUIDs (preserve existing UUIDs)
-- This allows same title in different workspaces by removing function-name-based IDs
UPDATE "custom_tools"
SET "id" = gen_random_uuid()::text
WHERE workspace_id IS NOT NULL -- Only update workspace-scoped tools
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
-- Step 2: Add composite unique constraint on (workspace_id, title)
-- This enforces uniqueness per workspace, not globally
CREATE UNIQUE INDEX "custom_tools_workspace_title_unique" ON "custom_tools" USING btree ("workspace_id","title");

File diff suppressed because it is too large Load Diff

View File

@@ -785,6 +785,13 @@
"when": 1764095386986,
"tag": "0112_tired_blink",
"breakpoints": true
},
{
"idx": 113,
"version": "7",
"when": 1764370369484,
"tag": "0113_calm_tiger_shark",
"breakpoints": true
}
]
}

View File

@@ -656,6 +656,10 @@ export const customTools = pgTable(
},
(table) => ({
workspaceIdIdx: index('custom_tools_workspace_id_idx').on(table.workspaceId),
workspaceTitleUnique: uniqueIndex('custom_tools_workspace_title_unique').on(
table.workspaceId,
table.title
),
})
)