Skip to content

Commit 013b808

Browse files
committed
fix(folders): bound every active folder path index read
`loadActiveFolderPathIndex` only applied a LIMIT when a caller passed `maxRows`, so 15 call sites issued an unbounded SELECT over a workspace's folder rows. `updateWorkflow` did both in one function: the folderId branch read unbounded, then the fallback thirty lines later passed the cap. Defaults `maxRows` to `MAX_FOLDERS_PER_WORKSPACE` — the same ceiling folder creation refuses to cross — so no workspace can legitimately hold more rows than a default call accepts. Exceeding it still throws `FolderCollectionLimitExceededError`; the index is never silently truncated, because a partial path index resolves real paths to undefined and re-roots resources at the workspace root.
1 parent 3c28a88 commit 013b808

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

apps/sim/lib/folders/queries.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
schemaMock,
1010
} from '@sim/testing'
1111
import { beforeEach, describe, expect, it, vi } from 'vitest'
12+
import { MAX_FOLDERS_PER_WORKSPACE } from '@/lib/folders/constants'
1213
import { FolderCollectionLimitExceededError } from '@/lib/folders/errors'
1314
import {
1415
findActiveFolder,
@@ -202,6 +203,35 @@ describe('folder queries', () => {
202203
expect(dbChainMockFns.limit).toHaveBeenCalledWith(3)
203204
})
204205

206+
/**
207+
* Callers that omit `maxRows` — `createWorkflow`/`updateWorkflow` on their
208+
* folderId branch among them — must still get a bounded query rather than an
209+
* unbounded `SELECT` over every folder row in the workspace.
210+
*/
211+
it('bounds the path index at the workspace cap when no maxRows is given', async () => {
212+
queueTableRows(schemaMock.folder, [ROW])
213+
214+
await loadActiveFolderPathIndex('ws-1', 'workflow')
215+
216+
expect(dbChainMockFns.limit).toHaveBeenCalledWith(MAX_FOLDERS_PER_WORKSPACE + 1)
217+
})
218+
219+
it('throws rather than truncating when the default cap is exceeded', async () => {
220+
queueTableRows(
221+
schemaMock.folder,
222+
Array.from({ length: MAX_FOLDERS_PER_WORKSPACE + 1 }, (_, i) => ({
223+
...ROW,
224+
id: `f-${i}`,
225+
name: `Reports ${i}`,
226+
}))
227+
)
228+
229+
await expect(loadActiveFolderPathIndex('ws-1', 'workflow')).rejects.toMatchObject({
230+
code: 'payload_too_large',
231+
message: `Folder path index exceeds the ${MAX_FOLDERS_PER_WORKSPACE} row limit`,
232+
})
233+
})
234+
205235
it('fails before returning an oversized folder list', async () => {
206236
queueTableRows(schemaMock.folder, [ROW, { ...ROW, id: 'f-2' }, { ...ROW, id: 'f-3' }])
207237

apps/sim/lib/folders/queries.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { and, type Column, eq, isNotNull, isNull } from 'drizzle-orm'
44
import type { FolderApi, FolderResourceType } from '@/lib/api/contracts/folders'
55
import { type ListSortOrder, listOrderBy, searchFilter } from '@/lib/api/list-query'
66
import type { DbOrTx } from '@/lib/db/types'
7+
import { MAX_FOLDERS_PER_WORKSPACE } from '@/lib/folders/constants'
78
import { FolderCollectionLimitExceededError } from '@/lib/folders/errors'
89
import { buildFolderPathIndex, type FolderPathIndex, ROOT_FOLDER_PATH } from '@/lib/folders/paths'
910
import type { FolderQueryScope } from '@/hooks/queries/utils/folder-keys'
@@ -169,13 +170,24 @@ interface ListActiveFolderRowsOptions {
169170
maxRows?: number
170171
}
171172

173+
/**
174+
* Materializes the workspace's active folder tree for one resource type.
175+
*
176+
* The read is always bounded: `maxRows` defaults to `MAX_FOLDERS_PER_WORKSPACE`,
177+
* the same ceiling folder creation refuses to cross, so a workspace can never
178+
* legitimately hold more rows than a default call will accept. Exceeding the
179+
* bound THROWS `FolderCollectionLimitExceededError` — the index is never
180+
* silently truncated, because a partial path index resolves real folder paths
181+
* to `undefined` and re-roots resources at the workspace root.
182+
*/
172183
export async function loadActiveFolderPathIndex(
173184
workspaceId: string,
174185
resourceType: FolderResourceType,
175186
tx: DbOrTx = db,
176187
options?: { maxRows?: number }
177188
): Promise<FolderPathIndex<typeof folder.$inferSelect>> {
178-
const query = tx
189+
const maxRows = options?.maxRows ?? MAX_FOLDERS_PER_WORKSPACE
190+
const rows = await tx
179191
.select()
180192
.from(folder)
181193
.where(
@@ -185,9 +197,9 @@ export async function loadActiveFolderPathIndex(
185197
isNull(folder.deletedAt)
186198
)
187199
)
188-
const rows = options?.maxRows === undefined ? await query : await query.limit(options.maxRows + 1)
189-
if (options?.maxRows !== undefined && rows.length > options.maxRows) {
190-
throw new FolderCollectionLimitExceededError('path index', options.maxRows)
200+
.limit(maxRows + 1)
201+
if (rows.length > maxRows) {
202+
throw new FolderCollectionLimitExceededError('path index', maxRows)
191203
}
192204

193205
return buildFolderPathIndex(rows)

0 commit comments

Comments
 (0)