Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/sim/app/api/v1/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
* DELETE /api/v1/admin/workflows/:id - Delete workflow
* GET /api/v1/admin/workflows/:id/export - Export workflow (JSON)
* POST /api/v1/admin/workflows/import - Import single workflow
* POST /api/v1/admin/workflows/:id/deploy - Deploy workflow
* DELETE /api/v1/admin/workflows/:id/deploy - Undeploy workflow
* GET /api/v1/admin/workflows/:id/versions - List deployment versions
* POST /api/v1/admin/workflows/:id/versions/:vid/activate - Activate specific version
*
* Organizations:
* GET /api/v1/admin/organizations - List all organizations
Expand Down Expand Up @@ -65,6 +69,8 @@ export {
unauthorizedResponse,
} from '@/app/api/v1/admin/responses'
export type {
AdminDeploymentVersion,
AdminDeployResult,
AdminErrorResponse,
AdminFolder,
AdminListResponse,
Expand All @@ -76,6 +82,7 @@ export type {
AdminSeatAnalytics,
AdminSingleResponse,
AdminSubscription,
AdminUndeployResult,
AdminUser,
AdminUserBilling,
AdminUserBillingWithSubscription,
Expand Down
20 changes: 20 additions & 0 deletions apps/sim/app/api/v1/admin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,3 +599,23 @@ export interface AdminSeatAnalytics {
lastActive: string | null
}>
}

export interface AdminDeploymentVersion {
id: string
version: number
name: string | null
isActive: boolean
createdAt: string
createdBy: string | null
deployedByName: string | null
}

export interface AdminDeployResult {
isDeployed: boolean
version: number
deployedAt: string
}

export interface AdminUndeployResult {
isDeployed: boolean
}
111 changes: 111 additions & 0 deletions apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { db, workflow } from '@sim/db'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import {
deployWorkflow,
loadWorkflowFromNormalizedTables,
undeployWorkflow,
} from '@/lib/workflows/persistence/utils'
import { createSchedulesForDeploy, validateWorkflowSchedules } from '@/lib/workflows/schedules'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
badRequestResponse,
internalErrorResponse,
notFoundResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'
import type { AdminDeployResult, AdminUndeployResult } from '@/app/api/v1/admin/types'

const logger = createLogger('AdminWorkflowDeployAPI')

const ADMIN_ACTOR_ID = 'admin-api'

interface RouteParams {
id: string
}

export const POST = withAdminAuthParams<RouteParams>(async (request, context) => {
const { id: workflowId } = await context.params

try {
const [workflowRecord] = await db
.select({ id: workflow.id, name: workflow.name })
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)

if (!workflowRecord) {
return notFoundResponse('Workflow')
}

const normalizedData = await loadWorkflowFromNormalizedTables(workflowId)
if (!normalizedData) {
return badRequestResponse('Workflow has no saved state')
}

const scheduleValidation = validateWorkflowSchedules(normalizedData.blocks)
if (!scheduleValidation.isValid) {
return badRequestResponse(`Invalid schedule configuration: ${scheduleValidation.error}`)
}

const deployResult = await deployWorkflow({
workflowId,
deployedBy: ADMIN_ACTOR_ID,
workflowName: workflowRecord.name,
})

if (!deployResult.success) {
return internalErrorResponse(deployResult.error || 'Failed to deploy workflow')
}

const scheduleResult = await createSchedulesForDeploy(workflowId, normalizedData.blocks, db)
if (!scheduleResult.success) {
logger.warn(`Schedule creation failed for workflow ${workflowId}: ${scheduleResult.error}`)
}

logger.info(`Admin API: Deployed workflow ${workflowId} as v${deployResult.version}`)

const response: AdminDeployResult = {
isDeployed: true,
version: deployResult.version!,
deployedAt: deployResult.deployedAt!.toISOString(),
}

return singleResponse(response)
} catch (error) {
logger.error(`Admin API: Failed to deploy workflow ${workflowId}`, { error })
return internalErrorResponse('Failed to deploy workflow')
}
})

export const DELETE = withAdminAuthParams<RouteParams>(async (request, context) => {
const { id: workflowId } = await context.params

try {
const [workflowRecord] = await db
.select({ id: workflow.id })
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)

if (!workflowRecord) {
return notFoundResponse('Workflow')
}

const result = await undeployWorkflow({ workflowId })
if (!result.success) {
return internalErrorResponse(result.error || 'Failed to undeploy workflow')
}

logger.info(`Admin API: Undeployed workflow ${workflowId}`)

const response: AdminUndeployResult = {
isDeployed: false,
}

return singleResponse(response)
} catch (error) {
logger.error(`Admin API: Failed to undeploy workflow ${workflowId}`, { error })
return internalErrorResponse('Failed to undeploy workflow')
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { db, workflow } from '@sim/db'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { activateWorkflowVersion } from '@/lib/workflows/persistence/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
badRequestResponse,
internalErrorResponse,
notFoundResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'

const logger = createLogger('AdminWorkflowActivateVersionAPI')

interface RouteParams {
id: string
versionId: string
}

export const POST = withAdminAuthParams<RouteParams>(async (request, context) => {
const { id: workflowId, versionId } = await context.params

try {
const [workflowRecord] = await db
.select({ id: workflow.id })
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)

if (!workflowRecord) {
return notFoundResponse('Workflow')
}

const versionNum = Number(versionId)
if (!Number.isFinite(versionNum) || versionNum < 1) {
return badRequestResponse('Invalid version number')
}

const result = await activateWorkflowVersion({ workflowId, version: versionNum })
if (!result.success) {
if (result.error === 'Deployment version not found') {
return notFoundResponse('Deployment version')
}
return internalErrorResponse(result.error || 'Failed to activate version')
}

logger.info(`Admin API: Activated version ${versionNum} for workflow ${workflowId}`)

return singleResponse({
success: true,
version: versionNum,
deployedAt: result.deployedAt!.toISOString(),
})
} catch (error) {
logger.error(`Admin API: Failed to activate version for workflow ${workflowId}`, { error })
return internalErrorResponse('Failed to activate deployment version')
}
})
52 changes: 52 additions & 0 deletions apps/sim/app/api/v1/admin/workflows/[id]/versions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { db, workflow } from '@sim/db'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { listWorkflowVersions } from '@/lib/workflows/persistence/utils'
import { withAdminAuthParams } from '@/app/api/v1/admin/middleware'
import {
internalErrorResponse,
notFoundResponse,
singleResponse,
} from '@/app/api/v1/admin/responses'
import type { AdminDeploymentVersion } from '@/app/api/v1/admin/types'

const logger = createLogger('AdminWorkflowVersionsAPI')

interface RouteParams {
id: string
}

export const GET = withAdminAuthParams<RouteParams>(async (request, context) => {
const { id: workflowId } = await context.params

try {
const [workflowRecord] = await db
.select({ id: workflow.id })
.from(workflow)
.where(eq(workflow.id, workflowId))
.limit(1)

if (!workflowRecord) {
return notFoundResponse('Workflow')
}

const { versions } = await listWorkflowVersions(workflowId)

const response: AdminDeploymentVersion[] = versions.map((v) => ({
id: v.id,
version: v.version,
name: v.name,
isActive: v.isActive,
createdAt: v.createdAt.toISOString(),
createdBy: v.createdBy,
deployedByName: v.deployedByName ?? (v.createdBy === 'admin-api' ? 'Admin' : null),
}))

logger.info(`Admin API: Listed ${versions.length} versions for workflow ${workflowId}`)

return singleResponse({ versions: response })
} catch (error) {
logger.error(`Admin API: Failed to list versions for workflow ${workflowId}`, { error })
return internalErrorResponse('Failed to list deployment versions')
}
})
28 changes: 9 additions & 19 deletions apps/sim/app/api/workflows/[id]/deploy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { and, desc, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { generateRequestId } from '@/lib/core/utils/request'
import { removeMcpToolsForWorkflow, syncMcpToolsForWorkflow } from '@/lib/mcp/workflow-mcp-sync'
import { deployWorkflow, loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import {
createSchedulesForDeploy,
deleteSchedulesForWorkflow,
validateWorkflowSchedules,
} from '@/lib/workflows/schedules'
deployWorkflow,
loadWorkflowFromNormalizedTables,
undeployWorkflow,
} from '@/lib/workflows/persistence/utils'
import { createSchedulesForDeploy, validateWorkflowSchedules } from '@/lib/workflows/schedules'
import { validateWorkflowPermissions } from '@/lib/workflows/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'

Expand Down Expand Up @@ -207,21 +207,11 @@ export async function DELETE(
return createErrorResponse(error.message, error.status)
}

await db.transaction(async (tx) => {
await deleteSchedulesForWorkflow(id, tx)

await tx
.update(workflowDeploymentVersion)
.set({ isActive: false })
.where(eq(workflowDeploymentVersion.workflowId, id))

await tx
.update(workflow)
.set({ isDeployed: false, deployedAt: null })
.where(eq(workflow.id, id))
})
const result = await undeployWorkflow({ workflowId: id })
if (!result.success) {
return createErrorResponse(result.error || 'Failed to undeploy workflow', 500)
}

// Remove all MCP tools that reference this workflow
await removeMcpToolsForWorkflow(id, requestId)

logger.info(`[${requestId}] Workflow undeployed successfully: ${id}`)
Expand Down
Loading