Skip to content

Commit 24e85e8

Browse files
committed
fix(folders): scope every folder query to its resource type
Review + cleanup pass over the cutover. - id-keyed lookups were left unfiltered on the theory that UUIDs cannot collide across resource types. That rules out accidental collision but not a caller passing a file/kb/table folder id as a workflow folderId — isFolderInWorkspace and the folder PUT/DELETE/parent checks would have accepted one. Every folder query now carries the resourceType filter - the soft-delete cleanup job pointed at the shared folder table with no resource scope, so it would have hard-deleted file/kb/table rows the workflow cleanup does not own; batchDeleteByWorkspaceAndTimestamp gains an additionalPredicate and the folder target uses it - drop lib/folders/cascade.ts and config.ts: unreferenced, and a second recursive cascade engine over the same table that duplicates folder-lifecycle's. They belong with their first consumer - narrow listFoldersQuerySchema.resourceType to the servable set instead of accepting the full enum and silently answering "you have none", matching the create and reorder bodies - de-alias `folder as workflowFolder` to `folderTable`; collapse six doubled resourceType predicates; drop the orphaned CreateFolderInput and restore TSDoc lost to the dead-code delete
1 parent ecb11ac commit 24e85e8

26 files changed

Lines changed: 281 additions & 533 deletions

File tree

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

Lines changed: 38 additions & 32 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, folder as 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'
@@ -40,8 +40,14 @@ export const POST = withRouteHandler(
4040

4141
const sourceFolder = await db
4242
.select()
43-
.from(workflowFolder)
44-
.where(and(eq(workflowFolder.id, sourceFolderId), isNull(workflowFolder.deletedAt)))
43+
.from(folderTable)
44+
.where(
45+
and(
46+
eq(folderTable.id, sourceFolderId),
47+
isNull(folderTable.deletedAt),
48+
eq(folderTable.resourceType, 'workflow')
49+
)
50+
)
4551
.then((rows) => rows[0])
4652

4753
if (!sourceFolder) {
@@ -70,20 +76,20 @@ export const POST = withRouteHandler(
7076
await assertTargetParentFolderMutable(tx, targetParentId, targetWorkspaceId, sourceFolderId)
7177

7278
const folderParentCondition = targetParentId
73-
? eq(workflowFolder.parentId, targetParentId)
74-
: isNull(workflowFolder.parentId)
79+
? eq(folderTable.parentId, targetParentId)
80+
: isNull(folderTable.parentId)
7581
const workflowParentCondition = targetParentId
7682
? eq(workflow.folderId, targetParentId)
7783
: isNull(workflow.folderId)
7884

7985
const [[folderResult], [workflowResult]] = await Promise.all([
8086
tx
81-
.select({ minSortOrder: min(workflowFolder.sortOrder) })
82-
.from(workflowFolder)
87+
.select({ minSortOrder: min(folderTable.sortOrder) })
88+
.from(folderTable)
8389
.where(
8490
and(
85-
eq(workflowFolder.workspaceId, targetWorkspaceId),
86-
eq(workflowFolder.resourceType, 'workflow'),
91+
eq(folderTable.workspaceId, targetWorkspaceId),
92+
eq(folderTable.resourceType, 'workflow'),
8793
folderParentCondition
8894
)
8995
),
@@ -108,7 +114,7 @@ export const POST = withRouteHandler(
108114
name
109115
)
110116

111-
await tx.insert(workflowFolder).values({
117+
await tx.insert(folderTable).values({
112118
id: newFolderId,
113119
resourceType: 'workflow',
114120
userId: session.user.id,
@@ -174,8 +180,8 @@ export const POST = withRouteHandler(
174180

175181
const duplicatedFolder = await db
176182
.select()
177-
.from(workflowFolder)
178-
.where(eq(workflowFolder.id, newFolderId))
183+
.from(folderTable)
184+
.where(and(eq(folderTable.id, newFolderId), eq(folderTable.resourceType, 'workflow')))
179185
.then((rows) => rows[0])
180186

181187
return NextResponse.json({ folder: toFolderApi(duplicatedFolder) }, { status: 201 })
@@ -235,14 +241,14 @@ async function assertTargetParentFolderMutable(
235241
visited.add(currentFolderId)
236242
const [folder] = await tx
237243
.select({
238-
id: workflowFolder.id,
239-
parentId: workflowFolder.parentId,
240-
workspaceId: workflowFolder.workspaceId,
241-
locked: workflowFolder.locked,
242-
archivedAt: workflowFolder.deletedAt,
244+
id: folderTable.id,
245+
parentId: folderTable.parentId,
246+
workspaceId: folderTable.workspaceId,
247+
locked: folderTable.locked,
248+
archivedAt: folderTable.deletedAt,
243249
})
244-
.from(workflowFolder)
245-
.where(eq(workflowFolder.id, currentFolderId))
250+
.from(folderTable)
251+
.where(and(eq(folderTable.id, currentFolderId), eq(folderTable.resourceType, 'workflow')))
246252
.limit(1)
247253

248254
if (!folder || folder.workspaceId !== targetWorkspaceId || folder.archivedAt) {
@@ -266,17 +272,17 @@ async function deduplicateFolderName(
266272
requestedName: string
267273
): Promise<string> {
268274
const parentCondition = parentId
269-
? eq(workflowFolder.parentId, parentId)
270-
: isNull(workflowFolder.parentId)
275+
? eq(folderTable.parentId, parentId)
276+
: isNull(folderTable.parentId)
271277
const siblingRows = await tx
272-
.select({ name: workflowFolder.name })
273-
.from(workflowFolder)
278+
.select({ name: folderTable.name })
279+
.from(folderTable)
274280
.where(
275281
and(
276-
eq(workflowFolder.workspaceId, workspaceId),
277-
eq(workflowFolder.resourceType, 'workflow'),
282+
eq(folderTable.workspaceId, workspaceId),
283+
eq(folderTable.resourceType, 'workflow'),
278284
parentCondition,
279-
isNull(workflowFolder.deletedAt)
285+
isNull(folderTable.deletedAt)
280286
)
281287
)
282288
const siblingNames = new Set(siblingRows.map((row) => row.name))
@@ -303,21 +309,21 @@ async function duplicateFolderStructure(
303309
): Promise<void> {
304310
const childFolders = await tx
305311
.select()
306-
.from(workflowFolder)
312+
.from(folderTable)
307313
.where(
308314
and(
309-
eq(workflowFolder.parentId, sourceFolderId),
310-
eq(workflowFolder.workspaceId, sourceWorkspaceId),
311-
eq(workflowFolder.resourceType, 'workflow'),
312-
isNull(workflowFolder.deletedAt)
315+
eq(folderTable.parentId, sourceFolderId),
316+
eq(folderTable.workspaceId, sourceWorkspaceId),
317+
eq(folderTable.resourceType, 'workflow'),
318+
isNull(folderTable.deletedAt)
313319
)
314320
)
315321

316322
for (const childFolder of childFolders) {
317323
const newChildFolderId = generateId()
318324
folderMapping.set(childFolder.id, newChildFolderId)
319325

320-
await tx.insert(workflowFolder).values({
326+
await tx.insert(folderTable).values({
321327
id: newChildFolderId,
322328
resourceType: 'workflow',
323329
userId,

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { db } from '@sim/db'
2-
import { folder as 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'
@@ -44,8 +44,8 @@ export const PUT = withRouteHandler(
4444
// Verify the folder exists
4545
const existingFolder = await db
4646
.select()
47-
.from(workflowFolder)
48-
.where(eq(workflowFolder.id, id))
47+
.from(folderTable)
48+
.where(and(eq(folderTable.id, id), eq(folderTable.resourceType, 'workflow')))
4949
.then((rows) => rows[0])
5050

5151
if (!existingFolder) {
@@ -125,8 +125,8 @@ export const DELETE = withRouteHandler(
125125
// Verify the folder exists
126126
const existingFolder = await db
127127
.select()
128-
.from(workflowFolder)
129-
.where(eq(workflowFolder.id, id))
128+
.from(folderTable)
129+
.where(and(eq(folderTable.id, id), eq(folderTable.resourceType, 'workflow')))
130130
.then((rows) => rows[0])
131131

132132
if (!existingFolder) {

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

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { folder as 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'
55
import { and, eq, inArray } from 'drizzle-orm'
@@ -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.deletedAt,
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,14 +83,10 @@ 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+
.select({ id: folderTable.id, parentId: folderTable.parentId })
87+
.from(folderTable)
8688
.where(
87-
and(
88-
eq(workflowFolder.workspaceId, workspaceId),
89-
eq(workflowFolder.resourceType, 'workflow'),
90-
eq(workflowFolder.resourceType, 'workflow')
91-
)
89+
and(eq(folderTable.workspaceId, workspaceId), eq(folderTable.resourceType, 'workflow'))
9290
)
9391

9492
const parentById = new Map<string, string | null>()
@@ -132,7 +130,10 @@ export const PUT = withRouteHandler(async (req: NextRequest) => {
132130
if (update.parentId !== undefined) {
133131
updateData.parentId = update.parentId || null
134132
}
135-
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')))
136137
}
137138
})
138139

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313

1414
import { db } from '@sim/db'
15-
import { workflow, folder as workflowFolder } from '@sim/db/schema'
15+
import { folder as folderTable, workflow } from '@sim/db/schema'
1616
import { createLogger } from '@sim/logger'
1717
import { and, eq } from 'drizzle-orm'
1818
import { NextResponse } from 'next/server'
@@ -106,12 +106,12 @@ export const GET = withRouteHandler(
106106
try {
107107
const [folderData] = await db
108108
.select({
109-
id: workflowFolder.id,
110-
name: workflowFolder.name,
111-
workspaceId: workflowFolder.workspaceId,
109+
id: folderTable.id,
110+
name: folderTable.name,
111+
workspaceId: folderTable.workspaceId,
112112
})
113-
.from(workflowFolder)
114-
.where(eq(workflowFolder.id, folderId))
113+
.from(folderTable)
114+
.where(and(eq(folderTable.id, folderId), eq(folderTable.resourceType, 'workflow')))
115115
.limit(1)
116116

117117
if (!folderData) {
@@ -125,16 +125,15 @@ export const GET = withRouteHandler(
125125

126126
const allFolders = await db
127127
.select({
128-
id: workflowFolder.id,
129-
name: workflowFolder.name,
130-
parentId: workflowFolder.parentId,
128+
id: folderTable.id,
129+
name: folderTable.name,
130+
parentId: folderTable.parentId,
131131
})
132-
.from(workflowFolder)
132+
.from(folderTable)
133133
.where(
134134
and(
135-
eq(workflowFolder.workspaceId, folderData.workspaceId),
136-
eq(workflowFolder.resourceType, 'workflow'),
137-
eq(workflowFolder.resourceType, 'workflow')
135+
eq(folderTable.workspaceId, folderData.workspaceId),
136+
eq(folderTable.resourceType, 'workflow')
138137
)
139138
)
140139

apps/sim/app/api/v1/admin/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77

88
import type {
99
auditLog,
10+
folder as folderTable,
1011
member,
1112
organization,
1213
subscription,
1314
user,
1415
userStats,
1516
workflow,
16-
folder as workflowFolder,
1717
workspace,
1818
} from '@sim/db/schema'
1919
import type { InferSelectModel } from 'drizzle-orm'
@@ -27,7 +27,7 @@ import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/typ
2727
export type DbUser = InferSelectModel<typeof user>
2828
export type DbWorkspace = InferSelectModel<typeof workspace>
2929
export type DbWorkflow = InferSelectModel<typeof workflow>
30-
export type DbWorkflowFolder = InferSelectModel<typeof workflowFolder>
30+
export type DbWorkflowFolder = InferSelectModel<typeof folderTable>
3131
export type DbOrganization = InferSelectModel<typeof organization>
3232
export type DbSubscription = InferSelectModel<typeof subscription>
3333
export type DbMember = InferSelectModel<typeof member>

apps/sim/app/api/v1/admin/workspaces/[id]/export/route.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
1515
import { db } from '@sim/db'
16-
import { workflow, folder as workflowFolder, workspace } from '@sim/db/schema'
16+
import { folder as folderTable, workflow, workspace } from '@sim/db/schema'
1717
import { createLogger } from '@sim/logger'
1818
import { and, eq } from 'drizzle-orm'
1919
import { NextResponse } from 'next/server'
@@ -68,13 +68,9 @@ export const GET = withRouteHandler(
6868

6969
const folders = await db
7070
.select()
71-
.from(workflowFolder)
71+
.from(folderTable)
7272
.where(
73-
and(
74-
eq(workflowFolder.workspaceId, workspaceId),
75-
eq(workflowFolder.resourceType, 'workflow'),
76-
eq(workflowFolder.resourceType, 'workflow')
77-
)
73+
and(eq(folderTable.workspaceId, workspaceId), eq(folderTable.resourceType, 'workflow'))
7874
)
7975

8076
const workflowExports: Array<{

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

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import { db } from '@sim/db'
14-
import { folder as workflowFolder, workspace } from '@sim/db/schema'
14+
import { folder as folderTable, workspace } from '@sim/db/schema'
1515
import { createLogger } from '@sim/logger'
1616
import { and, count, eq } from 'drizzle-orm'
1717
import { adminV1ListWorkspaceFoldersContract } from '@/lib/api/contracts/v1/admin'
@@ -49,25 +49,17 @@ export const GET = withRouteHandler(
4949
const [countResult, folders] = await Promise.all([
5050
db
5151
.select({ total: count() })
52-
.from(workflowFolder)
52+
.from(folderTable)
5353
.where(
54-
and(
55-
eq(workflowFolder.workspaceId, workspaceId),
56-
eq(workflowFolder.resourceType, 'workflow'),
57-
eq(workflowFolder.resourceType, 'workflow')
58-
)
54+
and(eq(folderTable.workspaceId, workspaceId), eq(folderTable.resourceType, 'workflow'))
5955
),
6056
db
6157
.select()
62-
.from(workflowFolder)
58+
.from(folderTable)
6359
.where(
64-
and(
65-
eq(workflowFolder.workspaceId, workspaceId),
66-
eq(workflowFolder.resourceType, 'workflow'),
67-
eq(workflowFolder.resourceType, 'workflow')
68-
)
60+
and(eq(folderTable.workspaceId, workspaceId), eq(folderTable.resourceType, 'workflow'))
6961
)
70-
.orderBy(workflowFolder.sortOrder, workflowFolder.name)
62+
.orderBy(folderTable.sortOrder, folderTable.name)
7163
.limit(limit)
7264
.offset(offset),
7365
])

0 commit comments

Comments
 (0)