mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
fix(custom-tools): add composite index on custom tool names & workspace id (#2131)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
10
packages/db/migrations/0113_calm_tiger_shark.sql
Normal file
10
packages/db/migrations/0113_calm_tiger_shark.sql
Normal 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");
|
||||
7688
packages/db/migrations/meta/0113_snapshot.json
Normal file
7688
packages/db/migrations/meta/0113_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user