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: 4 additions & 3 deletions apps/sim/app/api/folders/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const updateFolderSchema = z.object({
color: z.string().optional(),
isExpanded: z.boolean().optional(),
parentId: z.string().nullable().optional(),
sortOrder: z.number().int().min(0).optional(),
})

// PUT - Update a folder
Expand All @@ -38,7 +39,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: `Validation failed: ${errorMessages}` }, { status: 400 })
}

const { name, color, isExpanded, parentId } = validationResult.data
const { name, color, isExpanded, parentId, sortOrder } = validationResult.data

// Verify the folder exists
const existingFolder = await db
Expand Down Expand Up @@ -81,12 +82,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
}
}

// Update the folder
const updates: any = { updatedAt: new Date() }
const updates: Record<string, unknown> = { updatedAt: new Date() }
if (name !== undefined) updates.name = name.trim()
if (color !== undefined) updates.color = color
if (isExpanded !== undefined) updates.isExpanded = isExpanded
if (parentId !== undefined) updates.parentId = parentId || null
if (sortOrder !== undefined) updates.sortOrder = sortOrder

const [updatedFolder] = await db
.update(workflowFolder)
Expand Down
91 changes: 91 additions & 0 deletions apps/sim/app/api/folders/reorder/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { db } from '@sim/db'
import { workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('FolderReorderAPI')

const ReorderSchema = z.object({
workspaceId: z.string(),
updates: z.array(
z.object({
id: z.string(),
sortOrder: z.number().int().min(0),
parentId: z.string().nullable().optional(),
})
),
})

export async function PUT(req: NextRequest) {
const requestId = generateRequestId()
const session = await getSession()

if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized folder reorder attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

try {
const body = await req.json()
const { workspaceId, updates } = ReorderSchema.parse(body)

const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (!permission || permission === 'read') {
logger.warn(
`[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}`
)
return NextResponse.json({ error: 'Write access required' }, { status: 403 })
}

const folderIds = updates.map((u) => u.id)
const existingFolders = await db
.select({ id: workflowFolder.id, workspaceId: workflowFolder.workspaceId })
.from(workflowFolder)
.where(inArray(workflowFolder.id, folderIds))

const validIds = new Set(
existingFolders.filter((f) => f.workspaceId === workspaceId).map((f) => f.id)
)

const validUpdates = updates.filter((u) => validIds.has(u.id))

if (validUpdates.length === 0) {
return NextResponse.json({ error: 'No valid folders to update' }, { status: 400 })
}

await db.transaction(async (tx) => {
for (const update of validUpdates) {
const updateData: Record<string, unknown> = {
sortOrder: update.sortOrder,
updatedAt: new Date(),
}
if (update.parentId !== undefined) {
updateData.parentId = update.parentId
}
await tx.update(workflowFolder).set(updateData).where(eq(workflowFolder.id, update.id))
}
})

logger.info(
`[${requestId}] Reordered ${validUpdates.length} folders in workspace ${workspaceId}`
)

return NextResponse.json({ success: true, updated: validUpdates.length })
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid folder reorder data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}

logger.error(`[${requestId}] Error reordering folders`, error)
return NextResponse.json({ error: 'Failed to reorder folders' }, { status: 500 })
}
}
35 changes: 18 additions & 17 deletions apps/sim/app/api/folders/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export async function POST(request: NextRequest) {
}

const body = await request.json()
const { name, workspaceId, parentId, color } = body
const { name, workspaceId, parentId, color, sortOrder: providedSortOrder } = body

if (!name || !workspaceId) {
return NextResponse.json({ error: 'Name and workspace ID are required' }, { status: 400 })
Expand All @@ -81,25 +81,26 @@ export async function POST(request: NextRequest) {
// Generate a new ID
const id = crypto.randomUUID()

// Use transaction to ensure sortOrder consistency
const newFolder = await db.transaction(async (tx) => {
// Get the next sort order for the parent (or root level)
// Consider all folders in the workspace, not just those created by current user
const existingFolders = await tx
.select({ sortOrder: workflowFolder.sortOrder })
.from(workflowFolder)
.where(
and(
eq(workflowFolder.workspaceId, workspaceId),
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
let sortOrder: number
if (providedSortOrder !== undefined) {
sortOrder = providedSortOrder
} else {
const existingFolders = await tx
.select({ sortOrder: workflowFolder.sortOrder })
.from(workflowFolder)
.where(
and(
eq(workflowFolder.workspaceId, workspaceId),
parentId ? eq(workflowFolder.parentId, parentId) : isNull(workflowFolder.parentId)
)
)
)
.orderBy(desc(workflowFolder.sortOrder))
.limit(1)
.orderBy(desc(workflowFolder.sortOrder))
.limit(1)

const nextSortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
sortOrder = existingFolders.length > 0 ? existingFolders[0].sortOrder + 1 : 0
}

// Insert the new folder within the same transaction
const [folder] = await tx
.insert(workflowFolder)
.values({
Expand All @@ -109,7 +110,7 @@ export async function POST(request: NextRequest) {
workspaceId,
parentId: parentId || null,
color: color || '#6B7280',
sortOrder: nextSortOrder,
sortOrder,
})
.returning()

Expand Down
5 changes: 3 additions & 2 deletions apps/sim/app/api/workflows/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const UpdateWorkflowSchema = z.object({
description: z.string().optional(),
color: z.string().optional(),
folderId: z.string().nullable().optional(),
sortOrder: z.number().int().min(0).optional(),
})

/**
Expand Down Expand Up @@ -438,12 +439,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}

// Build update object
const updateData: any = { updatedAt: new Date() }
const updateData: Record<string, unknown> = { updatedAt: new Date() }
if (updates.name !== undefined) updateData.name = updates.name
if (updates.description !== undefined) updateData.description = updates.description
if (updates.color !== undefined) updateData.color = updates.color
if (updates.folderId !== undefined) updateData.folderId = updates.folderId
if (updates.sortOrder !== undefined) updateData.sortOrder = updates.sortOrder

// Update the workflow
const [updatedWorkflow] = await db
Expand Down
91 changes: 91 additions & 0 deletions apps/sim/app/api/workflows/reorder/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { db } from '@sim/db'
import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('WorkflowReorderAPI')

const ReorderSchema = z.object({
workspaceId: z.string(),
updates: z.array(
z.object({
id: z.string(),
sortOrder: z.number().int().min(0),
folderId: z.string().nullable().optional(),
})
),
})

export async function PUT(req: NextRequest) {
const requestId = generateRequestId()
const session = await getSession()

if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized reorder attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

try {
const body = await req.json()
const { workspaceId, updates } = ReorderSchema.parse(body)

const permission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (!permission || permission === 'read') {
logger.warn(
`[${requestId}] User ${session.user.id} lacks write permission for workspace ${workspaceId}`
)
return NextResponse.json({ error: 'Write access required' }, { status: 403 })
}

const workflowIds = updates.map((u) => u.id)
const existingWorkflows = await db
.select({ id: workflow.id, workspaceId: workflow.workspaceId })
.from(workflow)
.where(inArray(workflow.id, workflowIds))

const validIds = new Set(
existingWorkflows.filter((w) => w.workspaceId === workspaceId).map((w) => w.id)
)

const validUpdates = updates.filter((u) => validIds.has(u.id))

if (validUpdates.length === 0) {
return NextResponse.json({ error: 'No valid workflows to update' }, { status: 400 })
}

await db.transaction(async (tx) => {
for (const update of validUpdates) {
const updateData: Record<string, unknown> = {
sortOrder: update.sortOrder,
updatedAt: new Date(),
}
if (update.folderId !== undefined) {
updateData.folderId = update.folderId
}
await tx.update(workflow).set(updateData).where(eq(workflow.id, update.id))
}
})

logger.info(
`[${requestId}] Reordered ${validUpdates.length} workflows in workspace ${workspaceId}`
)

return NextResponse.json({ success: true, updated: validUpdates.length })
} catch (error) {
if (error instanceof z.ZodError) {
logger.warn(`[${requestId}] Invalid reorder data`, { errors: error.errors })
return NextResponse.json(
{ error: 'Invalid request data', details: error.errors },
{ status: 400 }
)
}

logger.error(`[${requestId}] Error reordering workflows`, error)
return NextResponse.json({ error: 'Failed to reorder workflows' }, { status: 500 })
}
}
30 changes: 28 additions & 2 deletions apps/sim/app/api/workflows/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from '@sim/db'
import { workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { and, eq, isNull, max } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
Expand All @@ -17,6 +17,7 @@ const CreateWorkflowSchema = z.object({
color: z.string().optional().default('#3972F6'),
workspaceId: z.string().optional(),
folderId: z.string().nullable().optional(),
sortOrder: z.number().int().optional(),
})

// GET /api/workflows - Get workflows for user (optionally filtered by workspaceId)
Expand Down Expand Up @@ -89,7 +90,14 @@ export async function POST(req: NextRequest) {

try {
const body = await req.json()
const { name, description, color, workspaceId, folderId } = CreateWorkflowSchema.parse(body)
const {
name,
description,
color,
workspaceId,
folderId,
sortOrder: providedSortOrder,
} = CreateWorkflowSchema.parse(body)

if (workspaceId) {
const workspacePermission = await getUserEntityPermissions(
Expand Down Expand Up @@ -127,11 +135,28 @@ export async function POST(req: NextRequest) {
// Silently fail
})

let sortOrder: number
if (providedSortOrder !== undefined) {
sortOrder = providedSortOrder
} else {
const folderCondition = folderId ? eq(workflow.folderId, folderId) : isNull(workflow.folderId)
const [maxResult] = await db
.select({ maxOrder: max(workflow.sortOrder) })
.from(workflow)
.where(
workspaceId
? and(eq(workflow.workspaceId, workspaceId), folderCondition)
: and(eq(workflow.userId, session.user.id), folderCondition)
)
sortOrder = (maxResult?.maxOrder ?? -1) + 1
}

await db.insert(workflow).values({
id: workflowId,
userId: session.user.id,
workspaceId: workspaceId || null,
folderId: folderId || null,
sortOrder,
name,
description,
color,
Expand All @@ -152,6 +177,7 @@ export async function POST(req: NextRequest) {
color,
workspaceId,
folderId,
sortOrder,
createdAt: now,
updatedAt: now,
})
Expand Down
Loading