Skip to content

Commit d4a1bc2

Browse files
merge: staging into feat/table-views
2 parents e2acf9e + 60f2d03 commit d4a1bc2

41 files changed

Lines changed: 5651 additions & 563 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/folders/[id]/duplicate/route.ts

Lines changed: 39 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
22
import { db } from '@sim/db'
3-
import { workflow, workflowFolder } from '@sim/db/schema'
3+
import { folder as folderTable, workflow } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
55
import { FolderLockedError } from '@sim/platform-authz/workflow'
66
import { generateId } from '@sim/utils/id'
@@ -12,6 +12,7 @@ import { getSession } from '@/lib/auth'
1212
import { generateRequestId } from '@/lib/core/utils/request'
1313
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1414
import type { DbOrTx } from '@/lib/db/types'
15+
import { deduplicateFolderName } from '@/lib/folders/naming'
1516
import { toFolderApi } from '@/lib/folders/queries'
1617
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
1718
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
@@ -40,8 +41,14 @@ export const POST = withRouteHandler(
4041

4142
const sourceFolder = await db
4243
.select()
43-
.from(workflowFolder)
44-
.where(and(eq(workflowFolder.id, sourceFolderId), isNull(workflowFolder.archivedAt)))
44+
.from(folderTable)
45+
.where(
46+
and(
47+
eq(folderTable.id, sourceFolderId),
48+
isNull(folderTable.deletedAt),
49+
eq(folderTable.resourceType, 'workflow')
50+
)
51+
)
4552
.then((rows) => rows[0])
4653

4754
if (!sourceFolder) {
@@ -70,17 +77,23 @@ export const POST = withRouteHandler(
7077
await assertTargetParentFolderMutable(tx, targetParentId, targetWorkspaceId, sourceFolderId)
7178

7279
const folderParentCondition = targetParentId
73-
? eq(workflowFolder.parentId, targetParentId)
74-
: isNull(workflowFolder.parentId)
80+
? eq(folderTable.parentId, targetParentId)
81+
: isNull(folderTable.parentId)
7582
const workflowParentCondition = targetParentId
7683
? eq(workflow.folderId, targetParentId)
7784
: isNull(workflow.folderId)
7885

7986
const [[folderResult], [workflowResult]] = await Promise.all([
8087
tx
81-
.select({ minSortOrder: min(workflowFolder.sortOrder) })
82-
.from(workflowFolder)
83-
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
88+
.select({ minSortOrder: min(folderTable.sortOrder) })
89+
.from(folderTable)
90+
.where(
91+
and(
92+
eq(folderTable.workspaceId, targetWorkspaceId),
93+
eq(folderTable.resourceType, 'workflow'),
94+
folderParentCondition
95+
)
96+
),
8497
tx
8598
.select({ minSortOrder: min(workflow.sortOrder) })
8699
.from(workflow)
@@ -102,15 +115,14 @@ export const POST = withRouteHandler(
102115
name
103116
)
104117

105-
await tx.insert(workflowFolder).values({
118+
await tx.insert(folderTable).values({
106119
id: newFolderId,
120+
resourceType: 'workflow',
107121
userId: session.user.id,
108122
workspaceId: targetWorkspaceId,
109123
name: deduplicatedName,
110-
color: sourceFolder.color,
111124
parentId: targetParentId,
112125
sortOrder,
113-
isExpanded: false,
114126
locked: false,
115127
createdAt: now,
116128
updatedAt: now,
@@ -169,8 +181,8 @@ export const POST = withRouteHandler(
169181

170182
const duplicatedFolder = await db
171183
.select()
172-
.from(workflowFolder)
173-
.where(eq(workflowFolder.id, newFolderId))
184+
.from(folderTable)
185+
.where(and(eq(folderTable.id, newFolderId), eq(folderTable.resourceType, 'workflow')))
174186
.then((rows) => rows[0])
175187

176188
return NextResponse.json({ folder: toFolderApi(duplicatedFolder) }, { status: 201 })
@@ -230,14 +242,14 @@ async function assertTargetParentFolderMutable(
230242
visited.add(currentFolderId)
231243
const [folder] = await tx
232244
.select({
233-
id: workflowFolder.id,
234-
parentId: workflowFolder.parentId,
235-
workspaceId: workflowFolder.workspaceId,
236-
locked: workflowFolder.locked,
237-
archivedAt: workflowFolder.archivedAt,
245+
id: folderTable.id,
246+
parentId: folderTable.parentId,
247+
workspaceId: folderTable.workspaceId,
248+
locked: folderTable.locked,
249+
archivedAt: folderTable.deletedAt,
238250
})
239-
.from(workflowFolder)
240-
.where(eq(workflowFolder.id, currentFolderId))
251+
.from(folderTable)
252+
.where(and(eq(folderTable.id, currentFolderId), eq(folderTable.resourceType, 'workflow')))
241253
.limit(1)
242254

243255
if (!folder || folder.workspaceId !== targetWorkspaceId || folder.archivedAt) {
@@ -254,37 +266,6 @@ async function assertTargetParentFolderMutable(
254266
}
255267
}
256268

257-
async function deduplicateFolderName(
258-
tx: DbOrTx,
259-
workspaceId: string,
260-
parentId: string | null,
261-
requestedName: string
262-
): Promise<string> {
263-
const parentCondition = parentId
264-
? eq(workflowFolder.parentId, parentId)
265-
: isNull(workflowFolder.parentId)
266-
const siblingRows = await tx
267-
.select({ name: workflowFolder.name })
268-
.from(workflowFolder)
269-
.where(
270-
and(
271-
eq(workflowFolder.workspaceId, workspaceId),
272-
parentCondition,
273-
isNull(workflowFolder.archivedAt)
274-
)
275-
)
276-
const siblingNames = new Set(siblingRows.map((row) => row.name))
277-
if (!siblingNames.has(requestedName)) return requestedName
278-
279-
let suffix = 1
280-
let candidate = `${requestedName} (${suffix})`
281-
while (siblingNames.has(candidate)) {
282-
suffix += 1
283-
candidate = `${requestedName} (${suffix})`
284-
}
285-
return candidate
286-
}
287-
288269
async function duplicateFolderStructure(
289270
tx: DbOrTx,
290271
sourceFolderId: string,
@@ -297,28 +278,28 @@ async function duplicateFolderStructure(
297278
): Promise<void> {
298279
const childFolders = await tx
299280
.select()
300-
.from(workflowFolder)
281+
.from(folderTable)
301282
.where(
302283
and(
303-
eq(workflowFolder.parentId, sourceFolderId),
304-
eq(workflowFolder.workspaceId, sourceWorkspaceId),
305-
isNull(workflowFolder.archivedAt)
284+
eq(folderTable.parentId, sourceFolderId),
285+
eq(folderTable.workspaceId, sourceWorkspaceId),
286+
eq(folderTable.resourceType, 'workflow'),
287+
isNull(folderTable.deletedAt)
306288
)
307289
)
308290

309291
for (const childFolder of childFolders) {
310292
const newChildFolderId = generateId()
311293
folderMapping.set(childFolder.id, newChildFolderId)
312294

313-
await tx.insert(workflowFolder).values({
295+
await tx.insert(folderTable).values({
314296
id: newChildFolderId,
297+
resourceType: 'workflow',
315298
userId,
316299
workspaceId: targetWorkspaceId,
317300
name: childFolder.name,
318-
color: childFolder.color,
319301
parentId: newParentFolderId,
320302
sortOrder: childFolder.sortOrder,
321-
isExpanded: false,
322303
locked: false,
323304
createdAt: timestamp,
324305
updatedAt: timestamp,

apps/sim/app/api/folders/[id]/route.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ const mockFolder = {
7373

7474
/** Queues the folder-existence lookup the route runs before authorizing. */
7575
function queueFolderLookup(folder: Record<string, unknown> = mockFolder) {
76-
queueTableRows(schemaMock.workflowFolder, [folder])
76+
queueTableRows(schemaMock.folder, [folder])
7777
}
7878

7979
/** Makes the next folder lookup throw, exercising the route's 500 path. */

apps/sim/app/api/folders/[id]/route.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { db } from '@sim/db'
2-
import { workflowFolder } from '@sim/db/schema'
2+
import { folder as folderTable } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { assertFolderMutable, FolderLockedError } from '@sim/platform-authz/workflow'
5-
import { eq } from 'drizzle-orm'
5+
import { and, eq } from 'drizzle-orm'
66
import { type NextRequest, NextResponse } from 'next/server'
77
import { updateFolderContract } from '@/lib/api/contracts'
88
import { parseRequest } from '@/lib/api/server'
@@ -13,6 +13,14 @@ import { captureServerEvent } from '@/lib/posthog/server'
1313
import { performDeleteFolder, performUpdateFolder } from '@/lib/workflows/orchestration'
1414
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1515

16+
/** Maps an orchestration errorCode to its HTTP status; mirrors the POST /api/folders route. */
17+
function folderMutationStatus(errorCode: string | undefined): number {
18+
if (errorCode === 'validation') return 400
19+
if (errorCode === 'conflict') return 409
20+
if (errorCode === 'not_found') return 404
21+
return 500
22+
}
23+
1624
const logger = createLogger('FoldersIDAPI')
1725

1826
// PUT - Update a folder
@@ -44,8 +52,8 @@ export const PUT = withRouteHandler(
4452
// Verify the folder exists
4553
const existingFolder = await db
4654
.select()
47-
.from(workflowFolder)
48-
.where(eq(workflowFolder.id, id))
55+
.from(folderTable)
56+
.where(and(eq(folderTable.id, id), eq(folderTable.resourceType, 'workflow')))
4957
.then((rows) => rows[0])
5058

5159
if (!existingFolder) {
@@ -92,8 +100,7 @@ export const PUT = withRouteHandler(
92100
})
93101

94102
if (!result.success || !result.folder) {
95-
const status =
96-
result.errorCode === 'not_found' ? 404 : result.errorCode === 'validation' ? 400 : 500
103+
const status = folderMutationStatus(result.errorCode)
97104
return NextResponse.json({ error: result.error }, { status })
98105
}
99106

@@ -125,8 +132,8 @@ export const DELETE = withRouteHandler(
125132
// Verify the folder exists
126133
const existingFolder = await db
127134
.select()
128-
.from(workflowFolder)
129-
.where(eq(workflowFolder.id, id))
135+
.from(folderTable)
136+
.where(and(eq(folderTable.id, id), eq(folderTable.resourceType, 'workflow')))
130137
.then((rows) => rows[0])
131138

132139
if (!existingFolder) {

apps/sim/app/api/folders/reorder/route.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { db } from '@sim/db'
2-
import { workflowFolder } from '@sim/db/schema'
2+
import { folder as folderTable } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { assertFolderMutable, FolderLockedError } from '@sim/platform-authz/workflow'
5-
import { eq, inArray } from 'drizzle-orm'
5+
import { and, eq, inArray } from 'drizzle-orm'
66
import { type NextRequest, NextResponse } from 'next/server'
77
import { reorderFoldersContract } from '@/lib/api/contracts'
88
import { parseRequest } from '@/lib/api/server'
@@ -37,9 +37,9 @@ export const PUT = withRouteHandler(async (req: NextRequest) => {
3737

3838
const folderIds = updates.map((u) => u.id)
3939
const existingFolders = await db
40-
.select({ id: workflowFolder.id, workspaceId: workflowFolder.workspaceId })
41-
.from(workflowFolder)
42-
.where(inArray(workflowFolder.id, folderIds))
40+
.select({ id: folderTable.id, workspaceId: folderTable.workspaceId })
41+
.from(folderTable)
42+
.where(and(inArray(folderTable.id, folderIds), eq(folderTable.resourceType, 'workflow')))
4343

4444
const validIds = new Set(
4545
existingFolders.filter((f) => f.workspaceId === workspaceId).map((f) => f.id)
@@ -58,12 +58,14 @@ export const PUT = withRouteHandler(async (req: NextRequest) => {
5858
if (targetParentIds.length > 0) {
5959
const parentFolders = await db
6060
.select({
61-
id: workflowFolder.id,
62-
workspaceId: workflowFolder.workspaceId,
63-
archivedAt: workflowFolder.archivedAt,
61+
id: folderTable.id,
62+
workspaceId: folderTable.workspaceId,
63+
archivedAt: folderTable.deletedAt,
6464
})
65-
.from(workflowFolder)
66-
.where(inArray(workflowFolder.id, targetParentIds))
65+
.from(folderTable)
66+
.where(
67+
and(inArray(folderTable.id, targetParentIds), eq(folderTable.resourceType, 'workflow'))
68+
)
6769

6870
const validParentIds = new Set(
6971
parentFolders.filter((f) => f.workspaceId === workspaceId && !f.archivedAt).map((f) => f.id)
@@ -81,9 +83,11 @@ export const PUT = withRouteHandler(async (req: NextRequest) => {
8183
}
8284

8385
const workspaceFolders = await db
84-
.select({ id: workflowFolder.id, parentId: workflowFolder.parentId })
85-
.from(workflowFolder)
86-
.where(eq(workflowFolder.workspaceId, workspaceId))
86+
.select({ id: folderTable.id, parentId: folderTable.parentId })
87+
.from(folderTable)
88+
.where(
89+
and(eq(folderTable.workspaceId, workspaceId), eq(folderTable.resourceType, 'workflow'))
90+
)
8791

8892
const parentById = new Map<string, string | null>()
8993
for (const folder of workspaceFolders) {
@@ -126,7 +130,10 @@ export const PUT = withRouteHandler(async (req: NextRequest) => {
126130
if (update.parentId !== undefined) {
127131
updateData.parentId = update.parentId || null
128132
}
129-
await tx.update(workflowFolder).set(updateData).where(eq(workflowFolder.id, update.id))
133+
await tx
134+
.update(folderTable)
135+
.set(updateData)
136+
.where(and(eq(folderTable.id, update.id), eq(folderTable.resourceType, 'workflow')))
130137
}
131138
})
132139

apps/sim/app/api/folders/route.test.ts

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -579,35 +579,5 @@ describe('Folders API Route', () => {
579579
expect(capturedValues).not.toBeNull()
580580
expect(capturedValues!.name).toBe('Test Folder With Spaces')
581581
})
582-
583-
it('should use default color when not provided', async () => {
584-
mockAuthenticatedUser()
585-
586-
let capturedValues: CapturedFolderValues | null = null
587-
588-
mockTransaction.mockImplementationOnce(
589-
createMockTransaction({
590-
selectResults: [[], []],
591-
insertResult: [mockFolders[0]],
592-
onInsertValues: (values) => {
593-
capturedValues = values
594-
},
595-
})
596-
)
597-
mockValues.mockImplementationOnce((values: CapturedFolderValues) => {
598-
capturedValues = values
599-
return { returning: mockReturning }
600-
})
601-
602-
const req = createMockRequest('POST', {
603-
name: 'Test Folder',
604-
workspaceId: 'workspace-123',
605-
})
606-
607-
await POST(req)
608-
609-
expect(capturedValues).not.toBeNull()
610-
expect(capturedValues!.color).toBe('#6B7280')
611-
})
612582
})
613583
})

0 commit comments

Comments
 (0)