Skip to content

Commit b2bb6f2

Browse files
committed
feat(folders): move workflow folders onto the generic folder table
Repoints every workflow-folder read and write from `workflow_folder` to the generic `folder` table backfilled by migration 0272, scoped to `resourceType = 'workflow'`. - 11 write sites and 39 read sites repointed; the 21 reads that select by workspace or parent rather than by id now carry an explicit `resourceType` filter so they cannot return another type's folders - `archivedAt` becomes `deletedAt`; the dropped `color`/`isExpanded` columns are gone from the write paths - v1 admin `AdminFolder.color` is retained as always-null so the public response shape stays stable rather than silently losing a field - `GET /api/folders` still returns empty for non-workflow types: file folders are written by `uploads/contexts/workspace` and their backfilled rows are a frozen snapshot until that writer moves too
1 parent 96c54b4 commit b2bb6f2

28 files changed

Lines changed: 479 additions & 136 deletions

File tree

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

Lines changed: 16 additions & 10 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 { workflow, folder as workflowFolder } 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'
@@ -41,7 +41,7 @@ export const POST = withRouteHandler(
4141
const sourceFolder = await db
4242
.select()
4343
.from(workflowFolder)
44-
.where(and(eq(workflowFolder.id, sourceFolderId), isNull(workflowFolder.archivedAt)))
44+
.where(and(eq(workflowFolder.id, sourceFolderId), isNull(workflowFolder.deletedAt)))
4545
.then((rows) => rows[0])
4646

4747
if (!sourceFolder) {
@@ -80,7 +80,13 @@ export const POST = withRouteHandler(
8080
tx
8181
.select({ minSortOrder: min(workflowFolder.sortOrder) })
8282
.from(workflowFolder)
83-
.where(and(eq(workflowFolder.workspaceId, targetWorkspaceId), folderParentCondition)),
83+
.where(
84+
and(
85+
eq(workflowFolder.workspaceId, targetWorkspaceId),
86+
eq(workflowFolder.resourceType, 'workflow'),
87+
folderParentCondition
88+
)
89+
),
8490
tx
8591
.select({ minSortOrder: min(workflow.sortOrder) })
8692
.from(workflow)
@@ -104,13 +110,12 @@ export const POST = withRouteHandler(
104110

105111
await tx.insert(workflowFolder).values({
106112
id: newFolderId,
113+
resourceType: 'workflow',
107114
userId: session.user.id,
108115
workspaceId: targetWorkspaceId,
109116
name: deduplicatedName,
110-
color: sourceFolder.color,
111117
parentId: targetParentId,
112118
sortOrder,
113-
isExpanded: false,
114119
locked: false,
115120
createdAt: now,
116121
updatedAt: now,
@@ -234,7 +239,7 @@ async function assertTargetParentFolderMutable(
234239
parentId: workflowFolder.parentId,
235240
workspaceId: workflowFolder.workspaceId,
236241
locked: workflowFolder.locked,
237-
archivedAt: workflowFolder.archivedAt,
242+
archivedAt: workflowFolder.deletedAt,
238243
})
239244
.from(workflowFolder)
240245
.where(eq(workflowFolder.id, currentFolderId))
@@ -269,8 +274,9 @@ async function deduplicateFolderName(
269274
.where(
270275
and(
271276
eq(workflowFolder.workspaceId, workspaceId),
277+
eq(workflowFolder.resourceType, 'workflow'),
272278
parentCondition,
273-
isNull(workflowFolder.archivedAt)
279+
isNull(workflowFolder.deletedAt)
274280
)
275281
)
276282
const siblingNames = new Set(siblingRows.map((row) => row.name))
@@ -302,7 +308,8 @@ async function duplicateFolderStructure(
302308
and(
303309
eq(workflowFolder.parentId, sourceFolderId),
304310
eq(workflowFolder.workspaceId, sourceWorkspaceId),
305-
isNull(workflowFolder.archivedAt)
311+
eq(workflowFolder.resourceType, 'workflow'),
312+
isNull(workflowFolder.deletedAt)
306313
)
307314
)
308315

@@ -312,13 +319,12 @@ async function duplicateFolderStructure(
312319

313320
await tx.insert(workflowFolder).values({
314321
id: newChildFolderId,
322+
resourceType: 'workflow',
315323
userId,
316324
workspaceId: targetWorkspaceId,
317325
name: childFolder.name,
318-
color: childFolder.color,
319326
parentId: newParentFolderId,
320327
sortOrder: childFolder.sortOrder,
321-
isExpanded: false,
322328
locked: false,
323329
createdAt: timestamp,
324330
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { workflowFolder } from '@sim/db/schema'
2+
import { folder as workflowFolder } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
44
import { assertFolderMutable, FolderLockedError } from '@sim/platform-authz/workflow'
55
import { eq } from 'drizzle-orm'

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

Lines changed: 10 additions & 4 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 workflowFolder } 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'
@@ -60,7 +60,7 @@ export const PUT = withRouteHandler(async (req: NextRequest) => {
6060
.select({
6161
id: workflowFolder.id,
6262
workspaceId: workflowFolder.workspaceId,
63-
archivedAt: workflowFolder.archivedAt,
63+
archivedAt: workflowFolder.deletedAt,
6464
})
6565
.from(workflowFolder)
6666
.where(inArray(workflowFolder.id, targetParentIds))
@@ -83,7 +83,13 @@ export const PUT = withRouteHandler(async (req: NextRequest) => {
8383
const workspaceFolders = await db
8484
.select({ id: workflowFolder.id, parentId: workflowFolder.parentId })
8585
.from(workflowFolder)
86-
.where(eq(workflowFolder.workspaceId, workspaceId))
86+
.where(
87+
and(
88+
eq(workflowFolder.workspaceId, workspaceId),
89+
eq(workflowFolder.resourceType, 'workflow'),
90+
eq(workflowFolder.resourceType, 'workflow')
91+
)
92+
)
8793

8894
const parentById = new Map<string, string | null>()
8995
for (const folder of workspaceFolders) {

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
})

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

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

1414
import { db } from '@sim/db'
15-
import { workflow, workflowFolder } from '@sim/db/schema'
15+
import { workflow, folder as workflowFolder } from '@sim/db/schema'
1616
import { createLogger } from '@sim/logger'
17-
import { eq } from 'drizzle-orm'
17+
import { and, eq } from 'drizzle-orm'
1818
import { NextResponse } from 'next/server'
1919
import { adminV1ExportFolderContract } from '@/lib/api/contracts/v1/admin'
2020
import { parseRequest } from '@/lib/api/server'
@@ -130,7 +130,13 @@ export const GET = withRouteHandler(
130130
parentId: workflowFolder.parentId,
131131
})
132132
.from(workflowFolder)
133-
.where(eq(workflowFolder.workspaceId, folderData.workspaceId))
133+
.where(
134+
and(
135+
eq(workflowFolder.workspaceId, folderData.workspaceId),
136+
eq(workflowFolder.resourceType, 'workflow'),
137+
eq(workflowFolder.resourceType, 'workflow')
138+
)
139+
)
134140

135141
const workflowsInFolder = collectWorkflowsInFolder(folderId, allWorkflows, allFolders)
136142
const subfolders = collectSubfolders(folderId, allFolders)

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type {
1313
user,
1414
userStats,
1515
workflow,
16-
workflowFolder,
16+
folder as workflowFolder,
1717
workspace,
1818
} from '@sim/db/schema'
1919
import type { InferSelectModel } from 'drizzle-orm'
@@ -156,6 +156,11 @@ export interface AdminFolder {
156156
id: string
157157
name: string
158158
parentId: string | null
159+
/**
160+
* Always `null` since folders moved to the generic `folder` table, which has no `color`
161+
* column (it had no consumer). Retained so the v1 admin response shape stays stable for
162+
* existing API clients rather than silently dropping a documented field.
163+
*/
159164
color: string | null
160165
sortOrder: number
161166
createdAt: string
@@ -167,7 +172,7 @@ export function toAdminFolder(dbFolder: DbWorkflowFolder): AdminFolder {
167172
id: dbFolder.id,
168173
name: dbFolder.name,
169174
parentId: dbFolder.parentId,
170-
color: dbFolder.color,
175+
color: null,
171176
sortOrder: dbFolder.sortOrder,
172177
createdAt: dbFolder.createdAt.toISOString(),
173178
updatedAt: dbFolder.updatedAt.toISOString(),

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

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

1414
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
1515
import { db } from '@sim/db'
16-
import { workflow, workflowFolder, workspace } from '@sim/db/schema'
16+
import { workflow, folder as workflowFolder, workspace } from '@sim/db/schema'
1717
import { createLogger } from '@sim/logger'
18-
import { eq } from 'drizzle-orm'
18+
import { and, eq } from 'drizzle-orm'
1919
import { NextResponse } from 'next/server'
2020
import { adminV1ExportWorkspaceContract } from '@/lib/api/contracts/v1/admin'
2121
import { parseRequest } from '@/lib/api/server'
@@ -69,7 +69,13 @@ export const GET = withRouteHandler(
6969
const folders = await db
7070
.select()
7171
.from(workflowFolder)
72-
.where(eq(workflowFolder.workspaceId, workspaceId))
72+
.where(
73+
and(
74+
eq(workflowFolder.workspaceId, workspaceId),
75+
eq(workflowFolder.resourceType, 'workflow'),
76+
eq(workflowFolder.resourceType, 'workflow')
77+
)
78+
)
7379

7480
const workflowExports: Array<{
7581
workflow: WorkspaceExportPayload['workflows'][number]['workflow']

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
*/
1212

1313
import { db } from '@sim/db'
14-
import { workflowFolder, workspace } from '@sim/db/schema'
14+
import { folder as workflowFolder, workspace } from '@sim/db/schema'
1515
import { createLogger } from '@sim/logger'
16-
import { count, eq } from 'drizzle-orm'
16+
import { and, count, eq } from 'drizzle-orm'
1717
import { adminV1ListWorkspaceFoldersContract } from '@/lib/api/contracts/v1/admin'
1818
import { parseRequest } from '@/lib/api/server'
1919
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
@@ -50,11 +50,23 @@ export const GET = withRouteHandler(
5050
db
5151
.select({ total: count() })
5252
.from(workflowFolder)
53-
.where(eq(workflowFolder.workspaceId, workspaceId)),
53+
.where(
54+
and(
55+
eq(workflowFolder.workspaceId, workspaceId),
56+
eq(workflowFolder.resourceType, 'workflow'),
57+
eq(workflowFolder.resourceType, 'workflow')
58+
)
59+
),
5460
db
5561
.select()
5662
.from(workflowFolder)
57-
.where(eq(workflowFolder.workspaceId, workspaceId))
63+
.where(
64+
and(
65+
eq(workflowFolder.workspaceId, workspaceId),
66+
eq(workflowFolder.resourceType, 'workflow'),
67+
eq(workflowFolder.resourceType, 'workflow')
68+
)
69+
)
5870
.orderBy(workflowFolder.sortOrder, workflowFolder.name)
5971
.limit(limit)
6072
.offset(offset),

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
*/
2525

2626
import { db } from '@sim/db'
27-
import { workflow, workflowFolder } from '@sim/db/schema'
27+
import { workflow, folder as workflowFolder } from '@sim/db/schema'
2828
import { createLogger } from '@sim/logger'
2929
import { getErrorMessage } from '@sim/utils/errors'
3030
import { generateId } from '@sim/utils/id'
@@ -154,6 +154,7 @@ export const POST = withRouteHandler(
154154
rootFolderId = generateId()
155155
await db.insert(workflowFolder).values({
156156
id: rootFolderId,
157+
resourceType: 'workflow',
157158
name: rootFolderName,
158159
userId: workspaceData.ownerId,
159160
workspaceId,
@@ -232,6 +233,7 @@ async function importSingleWorkflow(
232233
const folderId = generateId()
233234
await db.insert(workflowFolder).values({
234235
id: folderId,
236+
resourceType: 'workflow',
235237
name: wf.folderPath[i],
236238
userId: ownerId,
237239
workspaceId,

0 commit comments

Comments
 (0)