mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Remove dup code from tool calls
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import { db } from '@sim/db'
|
||||
import { apiKey } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import {
|
||||
decryptApiKey,
|
||||
encryptApiKey,
|
||||
@@ -210,6 +214,52 @@ export async function getEncryptedApiKeyLast4(encryptedKey: string): Promise<str
|
||||
* @param apiKey - The API key to validate
|
||||
* @returns boolean - true if the format appears valid
|
||||
*/
|
||||
export function isValidApiKeyFormat(apiKey: string): boolean {
|
||||
return typeof apiKey === 'string' && apiKey.length > 10 && apiKey.length < 200
|
||||
export function isValidApiKeyFormat(apiKeyValue: string): boolean {
|
||||
return typeof apiKeyValue === 'string' && apiKeyValue.length > 10 && apiKeyValue.length < 200
|
||||
}
|
||||
|
||||
export async function createWorkspaceApiKey(params: {
|
||||
workspaceId: string
|
||||
userId: string
|
||||
name: string
|
||||
}) {
|
||||
const existingKey = await db
|
||||
.select({ id: apiKey.id })
|
||||
.from(apiKey)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKey.workspaceId, params.workspaceId),
|
||||
eq(apiKey.name, params.name),
|
||||
eq(apiKey.type, 'workspace')
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingKey.length > 0) {
|
||||
throw new Error(
|
||||
`A workspace API key named "${params.name}" already exists. Choose a different name.`
|
||||
)
|
||||
}
|
||||
|
||||
const { key: plainKey, encryptedKey } = await createApiKey(true)
|
||||
if (!encryptedKey) {
|
||||
throw new Error('Failed to encrypt API key for storage')
|
||||
}
|
||||
|
||||
const [newKey] = await db
|
||||
.insert(apiKey)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
workspaceId: params.workspaceId,
|
||||
userId: params.userId,
|
||||
createdBy: params.userId,
|
||||
name: params.name,
|
||||
key: encryptedKey,
|
||||
type: 'workspace',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning({ id: apiKey.id, name: apiKey.name, createdAt: apiKey.createdAt })
|
||||
|
||||
return { id: newKey.id, name: newKey.name, key: plainKey, createdAt: newKey.createdAt }
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, workflow, workspace } from '@sim/db/schema'
|
||||
import { and, asc, desc, eq, inArray } from 'drizzle-orm'
|
||||
import { permissions, workspace } from '@sim/db/schema'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import {
|
||||
authorizeWorkflowByWorkspacePermission,
|
||||
getWorkflowById,
|
||||
} from '@/lib/workflows/utils'
|
||||
|
||||
type WorkflowRecord = typeof workflow.$inferSelect
|
||||
type WorkflowRecord = NonNullable<Awaited<ReturnType<typeof getWorkflowById>>>
|
||||
|
||||
export async function ensureWorkflowAccess(
|
||||
workflowId: string,
|
||||
@@ -11,37 +15,21 @@ export async function ensureWorkflowAccess(
|
||||
workflow: WorkflowRecord
|
||||
workspaceId?: string | null
|
||||
}> {
|
||||
const [workflowRecord] = await db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
if (!workflowRecord) {
|
||||
const result = await authorizeWorkflowByWorkspacePermission({
|
||||
workflowId,
|
||||
userId,
|
||||
action: 'read',
|
||||
})
|
||||
|
||||
if (!result.workflow) {
|
||||
throw new Error(`Workflow ${workflowId} not found`)
|
||||
}
|
||||
|
||||
if (!workflowRecord.workspaceId) {
|
||||
throw new Error(
|
||||
'This workflow is not attached to a workspace. Personal workflows are deprecated and cannot be accessed.'
|
||||
)
|
||||
if (!result.allowed) {
|
||||
throw new Error(result.message || 'Unauthorized workflow access')
|
||||
}
|
||||
|
||||
const [permissionRow] = await db
|
||||
.select({ permissionType: permissions.permissionType })
|
||||
.from(permissions)
|
||||
.where(
|
||||
and(
|
||||
eq(permissions.entityType, 'workspace'),
|
||||
eq(permissions.entityId, workflowRecord.workspaceId),
|
||||
eq(permissions.userId, userId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
if (permissionRow) {
|
||||
return { workflow: workflowRecord, workspaceId: workflowRecord.workspaceId }
|
||||
}
|
||||
|
||||
throw new Error('Unauthorized workflow access')
|
||||
return { workflow: result.workflow, workspaceId: result.workflow.workspaceId }
|
||||
}
|
||||
|
||||
export async function getDefaultWorkspaceId(userId: string): Promise<string> {
|
||||
@@ -96,35 +84,3 @@ export async function ensureWorkspaceAccess(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAccessibleWorkflowsForUser(
|
||||
userId: string,
|
||||
options?: { workspaceId?: string; folderId?: string }
|
||||
) {
|
||||
const workspaceIds = await db
|
||||
.select({ entityId: permissions.entityId })
|
||||
.from(permissions)
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
|
||||
const workspaceIdList = workspaceIds.map((row) => row.entityId)
|
||||
if (workspaceIdList.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (options?.workspaceId && !workspaceIdList.includes(options.workspaceId)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const workflowConditions = [inArray(workflow.workspaceId, workspaceIdList)]
|
||||
if (options?.workspaceId) {
|
||||
workflowConditions.push(eq(workflow.workspaceId, options.workspaceId))
|
||||
}
|
||||
if (options?.folderId) {
|
||||
workflowConditions.push(eq(workflow.folderId, options.folderId))
|
||||
}
|
||||
|
||||
return db
|
||||
.select()
|
||||
.from(workflow)
|
||||
.where(and(...workflowConditions))
|
||||
.orderBy(asc(workflow.sortOrder), asc(workflow.createdAt), asc(workflow.id))
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { db } from '@sim/db'
|
||||
import { customTools, workflow } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
||||
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
import type {
|
||||
ExecutionContext,
|
||||
ToolCallResult,
|
||||
@@ -12,7 +10,12 @@ import { routeExecution } from '@/lib/copilot/tools/server/router'
|
||||
import { env } from '@/lib/core/config/env'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||
import {
|
||||
deleteCustomTool,
|
||||
getCustomToolById,
|
||||
listCustomTools,
|
||||
upsertCustomTools,
|
||||
} from '@/lib/workflows/custom-tools/operations'
|
||||
import { getTool, resolveToolId } from '@/tools/utils'
|
||||
import {
|
||||
executeCheckDeploymentStatus,
|
||||
@@ -123,22 +126,10 @@ async function executeManageCustomTool(
|
||||
|
||||
try {
|
||||
if (operation === 'list') {
|
||||
const toolsForUser = workspaceId
|
||||
? await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(
|
||||
or(
|
||||
eq(customTools.workspaceId, workspaceId),
|
||||
and(isNull(customTools.workspaceId), eq(customTools.userId, context.userId))
|
||||
)
|
||||
)
|
||||
.orderBy(desc(customTools.createdAt))
|
||||
: await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(and(isNull(customTools.workspaceId), eq(customTools.userId, context.userId)))
|
||||
.orderBy(desc(customTools.createdAt))
|
||||
const toolsForUser = await listCustomTools({
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -171,13 +162,7 @@ async function executeManageCustomTool(
|
||||
}
|
||||
|
||||
const resultTools = await upsertCustomTools({
|
||||
tools: [
|
||||
{
|
||||
title,
|
||||
schema: params.schema,
|
||||
code: params.code,
|
||||
},
|
||||
],
|
||||
tools: [{ title, schema: params.schema, code: params.code }],
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
})
|
||||
@@ -212,28 +197,11 @@ async function executeManageCustomTool(
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceTool = await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(and(eq(customTools.id, params.toolId), eq(customTools.workspaceId, workspaceId)))
|
||||
.limit(1)
|
||||
|
||||
const legacyTool =
|
||||
workspaceTool.length === 0
|
||||
? await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(
|
||||
and(
|
||||
eq(customTools.id, params.toolId),
|
||||
isNull(customTools.workspaceId),
|
||||
eq(customTools.userId, context.userId)
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
: []
|
||||
|
||||
const existing = workspaceTool[0] || legacyTool[0]
|
||||
const existing = await getCustomToolById({
|
||||
toolId: params.toolId,
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
})
|
||||
if (!existing) {
|
||||
return { success: false, error: `Custom tool not found: ${params.toolId}` }
|
||||
}
|
||||
@@ -243,14 +211,7 @@ async function executeManageCustomTool(
|
||||
const title = params.title || mergedSchema.function?.name || existing.title
|
||||
|
||||
await upsertCustomTools({
|
||||
tools: [
|
||||
{
|
||||
id: params.toolId,
|
||||
title,
|
||||
schema: mergedSchema,
|
||||
code: mergedCode,
|
||||
},
|
||||
],
|
||||
tools: [{ id: params.toolId, title, schema: mergedSchema, code: mergedCode }],
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
})
|
||||
@@ -272,31 +233,11 @@ async function executeManageCustomTool(
|
||||
return { success: false, error: "'toolId' is required for operation 'delete'" }
|
||||
}
|
||||
|
||||
const workspaceDelete =
|
||||
workspaceId != null
|
||||
? await db
|
||||
.delete(customTools)
|
||||
.where(
|
||||
and(eq(customTools.id, params.toolId), eq(customTools.workspaceId, workspaceId))
|
||||
)
|
||||
.returning({ id: customTools.id })
|
||||
: []
|
||||
|
||||
const legacyDelete =
|
||||
workspaceDelete.length === 0
|
||||
? await db
|
||||
.delete(customTools)
|
||||
.where(
|
||||
and(
|
||||
eq(customTools.id, params.toolId),
|
||||
isNull(customTools.workspaceId),
|
||||
eq(customTools.userId, context.userId)
|
||||
)
|
||||
)
|
||||
.returning({ id: customTools.id })
|
||||
: []
|
||||
|
||||
const deleted = workspaceDelete[0] || legacyDelete[0]
|
||||
const deleted = await deleteCustomTool({
|
||||
toolId: params.toolId,
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
})
|
||||
if (!deleted) {
|
||||
return { success: false, error: `Custom tool not found: ${params.toolId}` }
|
||||
}
|
||||
@@ -587,12 +528,8 @@ export async function prepareExecutionContext(
|
||||
userId: string,
|
||||
workflowId: string
|
||||
): Promise<ExecutionContext> {
|
||||
const workflowResult = await db
|
||||
.select({ workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
const workspaceId = workflowResult[0]?.workspaceId ?? undefined
|
||||
const wf = await getWorkflowById(workflowId)
|
||||
const workspaceId = wf?.workspaceId ?? undefined
|
||||
|
||||
const decryptedEnvVars = await getEffectiveDecryptedEnv(userId, workspaceId)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from '@sim/db'
|
||||
import { account, workflow } from '@sim/db/schema'
|
||||
import { account } from '@sim/db/schema'
|
||||
import { and, eq } from 'drizzle-orm'
|
||||
import type {
|
||||
ExecutionContext,
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '@/lib/copilot/orchestrator/types'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -27,12 +28,8 @@ export async function executeIntegrationToolDirect(
|
||||
|
||||
let workspaceId = context.workspaceId
|
||||
if (!workspaceId && workflowId) {
|
||||
const workflowResult = await db
|
||||
.select({ workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
workspaceId = workflowResult[0]?.workspaceId ?? undefined
|
||||
const wf = await getWorkflowById(workflowId)
|
||||
workspaceId = wf?.workspaceId ?? undefined
|
||||
}
|
||||
|
||||
const decryptedEnvVars =
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import crypto from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { apiKey, workflow, workflowFolder } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, isNull, max } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { createApiKey } from '@/lib/api-key/auth'
|
||||
import { createWorkspaceApiKey } from '@/lib/api-key/auth'
|
||||
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||
import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow'
|
||||
import {
|
||||
getExecutionState,
|
||||
getLatestExecutionState,
|
||||
} from '@/lib/workflows/executor/execution-state'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import {
|
||||
createFolderRecord,
|
||||
createWorkflowRecord,
|
||||
deleteFolderRecord,
|
||||
deleteWorkflowRecord,
|
||||
setWorkflowVariables,
|
||||
updateFolderRecord,
|
||||
updateWorkflowRecord,
|
||||
} from '@/lib/workflows/utils'
|
||||
import { ensureWorkflowAccess, ensureWorkspaceAccess, getDefaultWorkspaceId } from '../access'
|
||||
import type {
|
||||
CreateFolderParams,
|
||||
@@ -58,46 +61,21 @@ export async function executeCreateWorkflow(
|
||||
|
||||
await ensureWorkspaceAccess(workspaceId, context.userId, true)
|
||||
|
||||
const workflowId = crypto.randomUUID()
|
||||
const now = new Date()
|
||||
|
||||
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
|
||||
const [maxResult] = await db
|
||||
.select({ maxOrder: max(workflow.sortOrder) })
|
||||
.from(workflow)
|
||||
.where(and(eq(workflow.workspaceId, workspaceId), folderCondition))
|
||||
const sortOrder = (maxResult?.maxOrder ?? 0) + 1
|
||||
|
||||
await db.insert(workflow).values({
|
||||
id: workflowId,
|
||||
const result = await createWorkflowRecord({
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
folderId,
|
||||
sortOrder,
|
||||
name,
|
||||
description,
|
||||
color: '#3972F6',
|
||||
lastSynced: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isDeployed: false,
|
||||
runCount: 0,
|
||||
variables: {},
|
||||
folderId,
|
||||
})
|
||||
|
||||
const { workflowState } = buildDefaultWorkflowArtifacts()
|
||||
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
|
||||
if (!saveResult.success) {
|
||||
throw new Error(saveResult.error || 'Failed to save workflow state')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
workflowId,
|
||||
workflowName: name,
|
||||
workspaceId,
|
||||
folderId,
|
||||
workflowId: result.workflowId,
|
||||
workflowName: result.name,
|
||||
workspaceId: result.workspaceId,
|
||||
folderId: result.folderId,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -123,30 +101,14 @@ export async function executeCreateFolder(
|
||||
|
||||
await ensureWorkspaceAccess(workspaceId, context.userId, true)
|
||||
|
||||
const [maxResult] = await db
|
||||
.select({ maxOrder: max(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowFolder.workspaceId, workspaceId),
|
||||
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
|
||||
)
|
||||
)
|
||||
const sortOrder = (maxResult?.maxOrder ?? 0) + 1
|
||||
|
||||
const folderId = crypto.randomUUID()
|
||||
await db.insert(workflowFolder).values({
|
||||
id: folderId,
|
||||
const result = await createFolderRecord({
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
parentId,
|
||||
name,
|
||||
sortOrder,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
parentId,
|
||||
})
|
||||
|
||||
return { success: true, output: { folderId, name, workspaceId, parentId } }
|
||||
return { success: true, output: result }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
@@ -292,10 +254,7 @@ export async function executeSetGlobalWorkflowVariables(
|
||||
|
||||
const nextVarsRecord = Object.fromEntries(Object.values(byName).map((v) => [String(v.id), v]))
|
||||
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({ variables: nextVarsRecord, updatedAt: new Date() })
|
||||
.where(eq(workflow.id, workflowId))
|
||||
await setWorkflowVariables(workflowId, nextVarsRecord)
|
||||
|
||||
return { success: true, output: { updated: Object.values(byName).length } }
|
||||
} catch (error) {
|
||||
@@ -321,11 +280,7 @@ export async function executeRenameWorkflow(
|
||||
}
|
||||
|
||||
await ensureWorkflowAccess(workflowId, context.userId)
|
||||
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({ name, updatedAt: new Date() })
|
||||
.where(eq(workflow.id, workflowId))
|
||||
await updateWorkflowRecord(workflowId, { name })
|
||||
|
||||
return { success: true, output: { workflowId, name } }
|
||||
} catch (error) {
|
||||
@@ -344,13 +299,8 @@ export async function executeMoveWorkflow(
|
||||
}
|
||||
|
||||
await ensureWorkflowAccess(workflowId, context.userId)
|
||||
|
||||
const folderId = params.folderId || null
|
||||
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({ folderId, updatedAt: new Date() })
|
||||
.where(eq(workflow.id, workflowId))
|
||||
await updateWorkflowRecord(workflowId, { folderId })
|
||||
|
||||
return { success: true, output: { workflowId, folderId } }
|
||||
} catch (error) {
|
||||
@@ -374,10 +324,7 @@ export async function executeMoveFolder(
|
||||
return { success: false, error: 'A folder cannot be moved into itself' }
|
||||
}
|
||||
|
||||
await db
|
||||
.update(workflowFolder)
|
||||
.set({ parentId, updatedAt: new Date() })
|
||||
.where(eq(workflowFolder.id, folderId))
|
||||
await updateFolderRecord(folderId, { parentId })
|
||||
|
||||
return { success: true, output: { folderId, parentId } }
|
||||
} catch (error) {
|
||||
@@ -452,51 +399,18 @@ export async function executeGenerateApiKey(
|
||||
const workspaceId = params.workspaceId || (await getDefaultWorkspaceId(context.userId))
|
||||
await ensureWorkspaceAccess(workspaceId, context.userId, true)
|
||||
|
||||
const existingKey = await db
|
||||
.select({ id: apiKey.id })
|
||||
.from(apiKey)
|
||||
.where(
|
||||
and(
|
||||
eq(apiKey.workspaceId, workspaceId),
|
||||
eq(apiKey.name, name),
|
||||
eq(apiKey.type, 'workspace')
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (existingKey.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `A workspace API key named "${name}" already exists. Choose a different name.`,
|
||||
}
|
||||
}
|
||||
|
||||
const { key: plainKey, encryptedKey } = await createApiKey(true)
|
||||
if (!encryptedKey) {
|
||||
return { success: false, error: 'Failed to encrypt API key for storage' }
|
||||
}
|
||||
|
||||
const [newKey] = await db
|
||||
.insert(apiKey)
|
||||
.values({
|
||||
id: nanoid(),
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
createdBy: context.userId,
|
||||
name,
|
||||
key: encryptedKey,
|
||||
type: 'workspace',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.returning({ id: apiKey.id, name: apiKey.name, createdAt: apiKey.createdAt })
|
||||
const newKey = await createWorkspaceApiKey({
|
||||
workspaceId,
|
||||
userId: context.userId,
|
||||
name,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
id: newKey.id,
|
||||
name: newKey.name,
|
||||
key: plainKey,
|
||||
key: newKey.key,
|
||||
workspaceId,
|
||||
message:
|
||||
'API key created successfully. Copy this key now — it will not be shown again. Use this key in the x-api-key header when calling workflow API endpoints.',
|
||||
@@ -580,7 +494,7 @@ export async function executeUpdateWorkflow(
|
||||
return { success: false, error: 'workflowId is required' }
|
||||
}
|
||||
|
||||
const updates: Record<string, unknown> = { updatedAt: new Date() }
|
||||
const updates: { name?: string; description?: string } = {}
|
||||
|
||||
if (typeof params.name === 'string') {
|
||||
const name = params.name.trim()
|
||||
@@ -596,17 +510,16 @@ export async function executeUpdateWorkflow(
|
||||
updates.description = params.description
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length <= 1) {
|
||||
if (Object.keys(updates).length === 0) {
|
||||
return { success: false, error: 'At least one of name or description is required' }
|
||||
}
|
||||
|
||||
await ensureWorkflowAccess(workflowId, context.userId)
|
||||
|
||||
await db.update(workflow).set(updates).where(eq(workflow.id, workflowId))
|
||||
await updateWorkflowRecord(workflowId, updates)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { workflowId, ...updates, updatedAt: undefined },
|
||||
output: { workflowId, ...updates },
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
@@ -624,8 +537,7 @@ export async function executeDeleteWorkflow(
|
||||
}
|
||||
|
||||
const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId)
|
||||
|
||||
await db.delete(workflow).where(eq(workflow.id, workflowId))
|
||||
await deleteWorkflowRecord(workflowId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -653,10 +565,7 @@ export async function executeRenameFolder(
|
||||
return { success: false, error: 'Folder name must be 200 characters or less' }
|
||||
}
|
||||
|
||||
await db
|
||||
.update(workflowFolder)
|
||||
.set({ name, updatedAt: new Date() })
|
||||
.where(eq(workflowFolder.id, folderId))
|
||||
await updateFolderRecord(folderId, { name })
|
||||
|
||||
return { success: true, output: { folderId, name } }
|
||||
} catch (error) {
|
||||
@@ -674,32 +583,11 @@ export async function executeDeleteFolder(
|
||||
return { success: false, error: 'folderId is required' }
|
||||
}
|
||||
|
||||
// Get the folder to find its parent
|
||||
const [folder] = await db
|
||||
.select({ parentId: workflowFolder.parentId })
|
||||
.from(workflowFolder)
|
||||
.where(eq(workflowFolder.id, folderId))
|
||||
.limit(1)
|
||||
|
||||
if (!folder) {
|
||||
const deleted = await deleteFolderRecord(folderId)
|
||||
if (!deleted) {
|
||||
return { success: false, error: 'Folder not found' }
|
||||
}
|
||||
|
||||
// Move child workflows to parent folder
|
||||
await db
|
||||
.update(workflow)
|
||||
.set({ folderId: folder.parentId, updatedAt: new Date() })
|
||||
.where(eq(workflow.folderId, folderId))
|
||||
|
||||
// Move child folders to parent folder
|
||||
await db
|
||||
.update(workflowFolder)
|
||||
.set({ parentId: folder.parentId, updatedAt: new Date() })
|
||||
.where(eq(workflowFolder.parentId, folderId))
|
||||
|
||||
// Delete the folder
|
||||
await db.delete(workflowFolder).where(eq(workflowFolder.id, folderId))
|
||||
|
||||
return { success: true, output: { folderId, deleted: true } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { db } from '@sim/db'
|
||||
import { customTools, permissions, workflow, workflowFolder, workspace } from '@sim/db/schema'
|
||||
import { and, asc, desc, eq, isNull, or } from 'drizzle-orm'
|
||||
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/orchestrator/types'
|
||||
import { formatNormalizedWorkflowForCopilot } from '@/lib/copilot/tools/shared/workflow-utils'
|
||||
import { listCustomTools } from '@/lib/workflows/custom-tools/operations'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import { listWorkspaceFiles } from '@/lib/uploads/contexts/workspace'
|
||||
import { getEffectiveBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs'
|
||||
@@ -11,6 +9,8 @@ import {
|
||||
loadDeployedWorkflowState,
|
||||
loadWorkflowFromNormalizedTables,
|
||||
} from '@/lib/workflows/persistence/utils'
|
||||
import { getWorkflowById, listFolders } from '@/lib/workflows/utils'
|
||||
import { listUserWorkspaces } from '@/lib/workspaces/utils'
|
||||
import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
@@ -32,25 +32,9 @@ export async function executeListUserWorkspaces(
|
||||
context: ExecutionContext
|
||||
): Promise<ToolCallResult> {
|
||||
try {
|
||||
const workspaces = await db
|
||||
.select({
|
||||
workspaceId: workspace.id,
|
||||
workspaceName: workspace.name,
|
||||
ownerId: workspace.ownerId,
|
||||
permissionType: permissions.permissionType,
|
||||
})
|
||||
.from(permissions)
|
||||
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
|
||||
.where(and(eq(permissions.userId, context.userId), eq(permissions.entityType, 'workspace')))
|
||||
.orderBy(desc(workspace.createdAt))
|
||||
const workspaces = await listUserWorkspaces(context.userId)
|
||||
|
||||
const output = workspaces.map((row) => ({
|
||||
workspaceId: row.workspaceId,
|
||||
workspaceName: row.workspaceName,
|
||||
role: row.ownerId === context.userId ? 'owner' : row.permissionType,
|
||||
}))
|
||||
|
||||
return { success: true, output: { workspaces: output } }
|
||||
return { success: true, output: { workspaces } }
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
@@ -66,16 +50,7 @@ export async function executeListFolders(
|
||||
|
||||
await ensureWorkspaceAccess(workspaceId, context.userId, false)
|
||||
|
||||
const folders = await db
|
||||
.select({
|
||||
folderId: workflowFolder.id,
|
||||
folderName: workflowFolder.name,
|
||||
parentId: workflowFolder.parentId,
|
||||
sortOrder: workflowFolder.sortOrder,
|
||||
})
|
||||
.from(workflowFolder)
|
||||
.where(eq(workflowFolder.workspaceId, workspaceId))
|
||||
.orderBy(asc(workflowFolder.sortOrder), asc(workflowFolder.createdAt))
|
||||
const folders = await listFolders(workspaceId)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -125,15 +100,10 @@ export async function executeGetWorkflowData(
|
||||
if (!workspaceId) {
|
||||
return { success: false, error: 'workspaceId is required' }
|
||||
}
|
||||
const conditions = [
|
||||
eq(customTools.workspaceId, workspaceId),
|
||||
and(eq(customTools.userId, context.userId), isNull(customTools.workspaceId)),
|
||||
]
|
||||
const toolsRows = await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(or(...conditions))
|
||||
.orderBy(desc(customTools.createdAt))
|
||||
const toolsRows = await listCustomTools({
|
||||
userId: context.userId,
|
||||
workspaceId,
|
||||
})
|
||||
|
||||
const customToolsData = toolsRows.map((tool) => {
|
||||
const schema = tool.schema as Record<string, unknown> | null
|
||||
@@ -419,11 +389,7 @@ export async function executeGetBlockUpstreamReferences(
|
||||
async function getWorkflowVariablesForTool(
|
||||
workflowId: string
|
||||
): Promise<Array<{ id: string; name: string; type: string; tag: string }>> {
|
||||
const [workflowRecord] = await db
|
||||
.select({ variables: workflow.variables })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
const workflowRecord = await getWorkflowById(workflowId)
|
||||
|
||||
const variablesRecord = (workflowRecord?.variables as Record<string, unknown>) || {}
|
||||
return Object.values(variablesRecord)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { docsEmbeddings } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { generateSearchEmbedding } from '@/lib/knowledge/embeddings'
|
||||
|
||||
interface DocsSearchParams {
|
||||
query: string
|
||||
@@ -23,7 +24,6 @@ export const searchDocumentationServerTool: BaseServerTool<DocsSearchParams, any
|
||||
|
||||
const similarityThreshold = threshold ?? DEFAULT_DOCS_SIMILARITY_THRESHOLD
|
||||
|
||||
const { generateSearchEmbedding } = await import('@/lib/knowledge/embeddings')
|
||||
const queryEmbedding = await generateSearchEmbedding(query)
|
||||
if (!queryEmbedding || queryEmbedding.length === 0) {
|
||||
return { results: [], query, totalResults: 0 }
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import { db } from '@sim/db'
|
||||
import { credential, environment, workflow, workspaceEnvironment } from '@sim/db/schema'
|
||||
import { credential, workflow } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import { z } from 'zod'
|
||||
import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||
import {
|
||||
syncPersonalEnvCredentialsForUser,
|
||||
syncWorkspaceEnvCredentials,
|
||||
} from '@/lib/credentials/environment'
|
||||
import { upsertPersonalEnvVars, upsertWorkspaceEnvVars } from '@/lib/environment/utils'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
|
||||
interface SetEnvironmentVariablesParams {
|
||||
variables: Record<string, any> | Array<{ name: string; value: string }>
|
||||
@@ -65,11 +62,7 @@ export const setEnvironmentVariablesServerTool: BaseServerTool<SetEnvironmentVar
|
||||
let resolvedWorkspaceId: string | null = null
|
||||
|
||||
if (requestedKeys.length > 0 && workflowId) {
|
||||
const [wf] = await db
|
||||
.select({ workspaceId: workflow.workspaceId })
|
||||
.from(workflow)
|
||||
.where(eq(workflow.id, workflowId))
|
||||
.limit(1)
|
||||
const wf = await getWorkflowById(workflowId)
|
||||
|
||||
if (wf?.workspaceId) {
|
||||
resolvedWorkspaceId = wf.workspaceId
|
||||
@@ -101,112 +94,15 @@ export const setEnvironmentVariablesServerTool: BaseServerTool<SetEnvironmentVar
|
||||
}
|
||||
}
|
||||
|
||||
const added: string[] = []
|
||||
const updated: string[] = []
|
||||
const workspaceUpdated: string[] = []
|
||||
|
||||
if (Object.keys(personalVars).length > 0) {
|
||||
const existingData = await db
|
||||
.select()
|
||||
.from(environment)
|
||||
.where(eq(environment.userId, authenticatedUserId))
|
||||
.limit(1)
|
||||
const existingEncrypted = (existingData[0]?.variables as Record<string, string>) || {}
|
||||
|
||||
const toEncrypt: Record<string, string> = {}
|
||||
for (const [key, newVal] of Object.entries(personalVars)) {
|
||||
if (!(key in existingEncrypted)) {
|
||||
toEncrypt[key] = newVal
|
||||
added.push(key)
|
||||
} else {
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(existingEncrypted[key])
|
||||
if (decrypted !== newVal) {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
}
|
||||
} catch {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newlyEncrypted = await Object.entries(toEncrypt).reduce(
|
||||
async (accP, [key, val]) => {
|
||||
const acc = await accP
|
||||
const { encrypted } = await encryptSecret(val)
|
||||
return { ...acc, [key]: encrypted }
|
||||
},
|
||||
Promise.resolve({} as Record<string, string>)
|
||||
)
|
||||
|
||||
const finalEncrypted = { ...existingEncrypted, ...newlyEncrypted }
|
||||
|
||||
await db
|
||||
.insert(environment)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
userId: authenticatedUserId,
|
||||
variables: finalEncrypted,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [environment.userId],
|
||||
set: { variables: finalEncrypted, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncPersonalEnvCredentialsForUser({
|
||||
userId: authenticatedUserId,
|
||||
envKeys: Object.keys(finalEncrypted),
|
||||
})
|
||||
}
|
||||
const { added, updated } = await upsertPersonalEnvVars(authenticatedUserId, personalVars)
|
||||
|
||||
let workspaceUpdated: string[] = []
|
||||
if (Object.keys(workspaceVars).length > 0 && resolvedWorkspaceId) {
|
||||
const wsRows = await db
|
||||
.select()
|
||||
.from(workspaceEnvironment)
|
||||
.where(eq(workspaceEnvironment.workspaceId, resolvedWorkspaceId))
|
||||
.limit(1)
|
||||
|
||||
const existingWsEncrypted = (wsRows[0]?.variables as Record<string, string>) || {}
|
||||
|
||||
const toEncryptWs: Record<string, string> = {}
|
||||
for (const [key, newVal] of Object.entries(workspaceVars)) {
|
||||
toEncryptWs[key] = newVal
|
||||
workspaceUpdated.push(key)
|
||||
}
|
||||
|
||||
const newlyEncryptedWs = await Object.entries(toEncryptWs).reduce(
|
||||
async (accP, [key, val]) => {
|
||||
const acc = await accP
|
||||
const { encrypted } = await encryptSecret(val)
|
||||
return { ...acc, [key]: encrypted }
|
||||
},
|
||||
Promise.resolve({} as Record<string, string>)
|
||||
workspaceUpdated = await upsertWorkspaceEnvVars(
|
||||
resolvedWorkspaceId,
|
||||
workspaceVars,
|
||||
authenticatedUserId
|
||||
)
|
||||
|
||||
const mergedWs = { ...existingWsEncrypted, ...newlyEncryptedWs }
|
||||
|
||||
await db
|
||||
.insert(workspaceEnvironment)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
variables: mergedWs,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [workspaceEnvironment.workspaceId],
|
||||
set: { variables: mergedWs, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncWorkspaceEnvCredentials({
|
||||
workspaceId: resolvedWorkspaceId,
|
||||
envKeys: Object.keys(workspaceVars),
|
||||
actingUserId: authenticatedUserId,
|
||||
})
|
||||
}
|
||||
|
||||
const totalProcessed = added.length + updated.length + workspaceUpdated.length
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import crypto from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { environment, workspaceEnvironment } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq, inArray } from 'drizzle-orm'
|
||||
import { decryptSecret } from '@/lib/core/security/encryption'
|
||||
import { getAccessibleEnvCredentials } from '@/lib/credentials/environment'
|
||||
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
|
||||
import {
|
||||
getAccessibleEnvCredentials,
|
||||
syncPersonalEnvCredentialsForUser,
|
||||
syncWorkspaceEnvCredentials,
|
||||
} from '@/lib/credentials/environment'
|
||||
|
||||
const logger = createLogger('EnvironmentUtils')
|
||||
|
||||
@@ -177,6 +182,121 @@ export async function getPersonalAndWorkspaceEnv(
|
||||
}
|
||||
}
|
||||
|
||||
export interface EnvUpsertResult {
|
||||
added: string[]
|
||||
updated: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts and upserts personal environment variables, merging with existing.
|
||||
* Only overwrites keys whose decrypted value has actually changed.
|
||||
*/
|
||||
export async function upsertPersonalEnvVars(
|
||||
userId: string,
|
||||
newVars: Record<string, string>
|
||||
): Promise<EnvUpsertResult> {
|
||||
const added: string[] = []
|
||||
const updated: string[] = []
|
||||
if (Object.keys(newVars).length === 0) return { added, updated }
|
||||
|
||||
const existingData = await db
|
||||
.select()
|
||||
.from(environment)
|
||||
.where(eq(environment.userId, userId))
|
||||
.limit(1)
|
||||
const existingEncrypted = (existingData[0]?.variables as Record<string, string>) || {}
|
||||
|
||||
const toEncrypt: Record<string, string> = {}
|
||||
for (const [key, newVal] of Object.entries(newVars)) {
|
||||
if (!(key in existingEncrypted)) {
|
||||
toEncrypt[key] = newVal
|
||||
added.push(key)
|
||||
} else {
|
||||
try {
|
||||
const { decrypted } = await decryptSecret(existingEncrypted[key])
|
||||
if (decrypted !== newVal) {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
}
|
||||
} catch {
|
||||
toEncrypt[key] = newVal
|
||||
updated.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newlyEncrypted: Record<string, string> = {}
|
||||
for (const [key, val] of Object.entries(toEncrypt)) {
|
||||
const { encrypted } = await encryptSecret(val)
|
||||
newlyEncrypted[key] = encrypted
|
||||
}
|
||||
|
||||
const finalEncrypted = { ...existingEncrypted, ...newlyEncrypted }
|
||||
|
||||
await db
|
||||
.insert(environment)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
userId,
|
||||
variables: finalEncrypted,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [environment.userId],
|
||||
set: { variables: finalEncrypted, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncPersonalEnvCredentialsForUser({ userId, envKeys: Object.keys(finalEncrypted) })
|
||||
|
||||
return { added, updated }
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts and upserts workspace environment variables, merging with existing.
|
||||
*/
|
||||
export async function upsertWorkspaceEnvVars(
|
||||
workspaceId: string,
|
||||
newVars: Record<string, string>,
|
||||
actingUserId: string
|
||||
): Promise<string[]> {
|
||||
const updatedKeys: string[] = []
|
||||
if (Object.keys(newVars).length === 0) return updatedKeys
|
||||
|
||||
const wsRows = await db
|
||||
.select()
|
||||
.from(workspaceEnvironment)
|
||||
.where(eq(workspaceEnvironment.workspaceId, workspaceId))
|
||||
.limit(1)
|
||||
const existingWsEncrypted = (wsRows[0]?.variables as Record<string, string>) || {}
|
||||
|
||||
const newlyEncrypted: Record<string, string> = {}
|
||||
for (const [key, val] of Object.entries(newVars)) {
|
||||
const { encrypted } = await encryptSecret(val)
|
||||
newlyEncrypted[key] = encrypted
|
||||
updatedKeys.push(key)
|
||||
}
|
||||
|
||||
const merged = { ...existingWsEncrypted, ...newlyEncrypted }
|
||||
|
||||
await db
|
||||
.insert(workspaceEnvironment)
|
||||
.values({
|
||||
id: crypto.randomUUID(),
|
||||
workspaceId,
|
||||
variables: merged,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [workspaceEnvironment.workspaceId],
|
||||
set: { variables: merged, updatedAt: new Date() },
|
||||
})
|
||||
|
||||
await syncWorkspaceEnvCredentials({ workspaceId, envKeys: Object.keys(newVars), actingUserId })
|
||||
|
||||
return updatedKeys
|
||||
}
|
||||
|
||||
export async function getEffectiveDecryptedEnv(
|
||||
userId: string,
|
||||
workspaceId?: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db } from '@sim/db'
|
||||
import { customTools } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, isNull } from 'drizzle-orm'
|
||||
import { and, desc, eq, isNull, or } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
@@ -107,3 +107,76 @@ export async function upsertCustomTools(params: {
|
||||
return resultTools
|
||||
})
|
||||
}
|
||||
|
||||
export async function listCustomTools(params: {
|
||||
userId: string
|
||||
workspaceId?: string
|
||||
}) {
|
||||
const { userId, workspaceId } = params
|
||||
return workspaceId
|
||||
? db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(
|
||||
or(
|
||||
eq(customTools.workspaceId, workspaceId),
|
||||
and(isNull(customTools.workspaceId), eq(customTools.userId, userId))
|
||||
)
|
||||
)
|
||||
.orderBy(desc(customTools.createdAt))
|
||||
: db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(and(isNull(customTools.workspaceId), eq(customTools.userId, userId)))
|
||||
.orderBy(desc(customTools.createdAt))
|
||||
}
|
||||
|
||||
export async function getCustomToolById(params: {
|
||||
toolId: string
|
||||
userId: string
|
||||
workspaceId?: string
|
||||
}) {
|
||||
const { toolId, userId, workspaceId } = params
|
||||
|
||||
if (workspaceId) {
|
||||
const workspaceTool = await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(and(eq(customTools.id, toolId), eq(customTools.workspaceId, workspaceId)))
|
||||
.limit(1)
|
||||
if (workspaceTool[0]) return workspaceTool[0]
|
||||
}
|
||||
|
||||
const legacyTool = await db
|
||||
.select()
|
||||
.from(customTools)
|
||||
.where(
|
||||
and(eq(customTools.id, toolId), isNull(customTools.workspaceId), eq(customTools.userId, userId))
|
||||
)
|
||||
.limit(1)
|
||||
return legacyTool[0] || null
|
||||
}
|
||||
|
||||
export async function deleteCustomTool(params: {
|
||||
toolId: string
|
||||
userId: string
|
||||
workspaceId?: string
|
||||
}): Promise<boolean> {
|
||||
const { toolId, userId, workspaceId } = params
|
||||
|
||||
if (workspaceId) {
|
||||
const workspaceDelete = await db
|
||||
.delete(customTools)
|
||||
.where(and(eq(customTools.id, toolId), eq(customTools.workspaceId, workspaceId)))
|
||||
.returning({ id: customTools.id })
|
||||
if (workspaceDelete.length > 0) return true
|
||||
}
|
||||
|
||||
const legacyDelete = await db
|
||||
.delete(customTools)
|
||||
.where(
|
||||
and(eq(customTools.id, toolId), isNull(customTools.workspaceId), eq(customTools.userId, userId))
|
||||
)
|
||||
.returning({ id: customTools.id })
|
||||
return legacyDelete.length > 0
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import crypto from 'crypto'
|
||||
import { db } from '@sim/db'
|
||||
import { permissions, userStats, workflow as workflowTable } from '@sim/db/schema'
|
||||
import { permissions, userStats, workflow as workflowTable, workflowFolder } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, asc, eq, inArray } from 'drizzle-orm'
|
||||
import { and, asc, eq, inArray, isNull, max } from 'drizzle-orm'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
|
||||
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import type { PermissionType } from '@/lib/workspaces/permissions/utils'
|
||||
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
|
||||
import type { ExecutionResult } from '@/executor/types'
|
||||
@@ -317,3 +320,160 @@ export async function authorizeWorkflowByWorkspacePermission(params: {
|
||||
workspacePermission,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Workflow CRUD ──
|
||||
|
||||
export interface CreateWorkflowInput {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
name: string
|
||||
description?: string | null
|
||||
color?: string
|
||||
folderId?: string | null
|
||||
}
|
||||
|
||||
export async function createWorkflowRecord(params: CreateWorkflowInput) {
|
||||
const { userId, workspaceId, name, description = null, color = '#3972F6', folderId = null } = params
|
||||
const workflowId = crypto.randomUUID()
|
||||
const now = new Date()
|
||||
|
||||
const folderCondition = folderId ? eq(workflowTable.folderId, folderId) : isNull(workflowTable.folderId)
|
||||
const [maxResult] = await db
|
||||
.select({ maxOrder: max(workflowTable.sortOrder) })
|
||||
.from(workflowTable)
|
||||
.where(and(eq(workflowTable.workspaceId, workspaceId), folderCondition))
|
||||
const sortOrder = (maxResult?.maxOrder ?? 0) + 1
|
||||
|
||||
await db.insert(workflowTable).values({
|
||||
id: workflowId,
|
||||
userId,
|
||||
workspaceId,
|
||||
folderId,
|
||||
sortOrder,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
lastSynced: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isDeployed: false,
|
||||
runCount: 0,
|
||||
variables: {},
|
||||
})
|
||||
|
||||
const { workflowState } = buildDefaultWorkflowArtifacts()
|
||||
const saveResult = await saveWorkflowToNormalizedTables(workflowId, workflowState)
|
||||
if (!saveResult.success) {
|
||||
throw new Error(saveResult.error || 'Failed to save workflow state')
|
||||
}
|
||||
|
||||
return { workflowId, name, workspaceId, folderId, sortOrder, createdAt: now, updatedAt: now }
|
||||
}
|
||||
|
||||
export async function updateWorkflowRecord(
|
||||
workflowId: string,
|
||||
updates: { name?: string; description?: string; color?: string; folderId?: string | null }
|
||||
) {
|
||||
const setData: Record<string, unknown> = { updatedAt: new Date() }
|
||||
if (updates.name !== undefined) setData.name = updates.name
|
||||
if (updates.description !== undefined) setData.description = updates.description
|
||||
if (updates.color !== undefined) setData.color = updates.color
|
||||
if (updates.folderId !== undefined) setData.folderId = updates.folderId
|
||||
await db.update(workflowTable).set(setData).where(eq(workflowTable.id, workflowId))
|
||||
}
|
||||
|
||||
export async function deleteWorkflowRecord(workflowId: string) {
|
||||
await db.delete(workflowTable).where(eq(workflowTable.id, workflowId))
|
||||
}
|
||||
|
||||
export async function setWorkflowVariables(workflowId: string, variables: Record<string, unknown>) {
|
||||
await db
|
||||
.update(workflowTable)
|
||||
.set({ variables, updatedAt: new Date() })
|
||||
.where(eq(workflowTable.id, workflowId))
|
||||
}
|
||||
|
||||
// ── Folder CRUD ──
|
||||
|
||||
export interface CreateFolderInput {
|
||||
userId: string
|
||||
workspaceId: string
|
||||
name: string
|
||||
parentId?: string | null
|
||||
}
|
||||
|
||||
export async function createFolderRecord(params: CreateFolderInput) {
|
||||
const { userId, workspaceId, name, parentId = null } = params
|
||||
|
||||
const [maxResult] = await db
|
||||
.select({ maxOrder: max(workflowFolder.sortOrder) })
|
||||
.from(workflowFolder)
|
||||
.where(
|
||||
and(
|
||||
eq(workflowFolder.workspaceId, workspaceId),
|
||||
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
|
||||
)
|
||||
)
|
||||
const sortOrder = (maxResult?.maxOrder ?? 0) + 1
|
||||
|
||||
const folderId = crypto.randomUUID()
|
||||
await db.insert(workflowFolder).values({
|
||||
id: folderId,
|
||||
userId,
|
||||
workspaceId,
|
||||
parentId,
|
||||
name,
|
||||
sortOrder,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
|
||||
return { folderId, name, workspaceId, parentId }
|
||||
}
|
||||
|
||||
export async function updateFolderRecord(
|
||||
folderId: string,
|
||||
updates: { name?: string; parentId?: string | null }
|
||||
) {
|
||||
const setData: Record<string, unknown> = { updatedAt: new Date() }
|
||||
if (updates.name !== undefined) setData.name = updates.name
|
||||
if (updates.parentId !== undefined) setData.parentId = updates.parentId
|
||||
await db.update(workflowFolder).set(setData).where(eq(workflowFolder.id, folderId))
|
||||
}
|
||||
|
||||
export async function deleteFolderRecord(folderId: string): Promise<boolean> {
|
||||
const [folder] = await db
|
||||
.select({ parentId: workflowFolder.parentId })
|
||||
.from(workflowFolder)
|
||||
.where(eq(workflowFolder.id, folderId))
|
||||
.limit(1)
|
||||
|
||||
if (!folder) return false
|
||||
|
||||
await db
|
||||
.update(workflowTable)
|
||||
.set({ folderId: folder.parentId, updatedAt: new Date() })
|
||||
.where(eq(workflowTable.folderId, folderId))
|
||||
|
||||
await db
|
||||
.update(workflowFolder)
|
||||
.set({ parentId: folder.parentId, updatedAt: new Date() })
|
||||
.where(eq(workflowFolder.parentId, folderId))
|
||||
|
||||
await db.delete(workflowFolder).where(eq(workflowFolder.id, folderId))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function listFolders(workspaceId: string) {
|
||||
return db
|
||||
.select({
|
||||
folderId: workflowFolder.id,
|
||||
folderName: workflowFolder.name,
|
||||
parentId: workflowFolder.parentId,
|
||||
sortOrder: workflowFolder.sortOrder,
|
||||
})
|
||||
.from(workflowFolder)
|
||||
.where(eq(workflowFolder.workspaceId, workspaceId))
|
||||
.orderBy(asc(workflowFolder.sortOrder), asc(workflowFolder.createdAt))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { db } from '@sim/db'
|
||||
import { workspace as workspaceTable } from '@sim/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { permissions, workspace as workspaceTable } from '@sim/db/schema'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
|
||||
interface WorkspaceBillingSettings {
|
||||
billedAccountUserId: string | null
|
||||
@@ -37,3 +37,23 @@ export async function getWorkspaceBilledAccountUserId(workspaceId: string): Prom
|
||||
const settings = await getWorkspaceBillingSettings(workspaceId)
|
||||
return settings?.billedAccountUserId ?? null
|
||||
}
|
||||
|
||||
export async function listUserWorkspaces(userId: string) {
|
||||
const workspaces = await db
|
||||
.select({
|
||||
workspaceId: workspaceTable.id,
|
||||
workspaceName: workspaceTable.name,
|
||||
ownerId: workspaceTable.ownerId,
|
||||
permissionType: permissions.permissionType,
|
||||
})
|
||||
.from(permissions)
|
||||
.innerJoin(workspaceTable, eq(permissions.entityId, workspaceTable.id))
|
||||
.where(and(eq(permissions.userId, userId), eq(permissions.entityType, 'workspace')))
|
||||
.orderBy(desc(workspaceTable.createdAt))
|
||||
|
||||
return workspaces.map((row) => ({
|
||||
workspaceId: row.workspaceId,
|
||||
workspaceName: row.workspaceName,
|
||||
role: row.ownerId === userId ? 'owner' : row.permissionType,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user