From b345cb2c83aee9f30402301b070f249f2e3405a9 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 8 May 2026 16:19:05 -0700 Subject: [PATCH 1/3] fix(logs): include subfolders when filtering logs by folder --- apps/sim/app/api/logs/export/route.ts | 10 +++++- apps/sim/app/api/logs/route.ts | 6 +++- apps/sim/app/api/logs/stats/route.ts | 9 ++++- apps/sim/lib/logs/filters.ts | 52 +++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index b814678caf6..f2aa9e83a20 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -5,7 +5,11 @@ import { and, desc, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters' +import { + buildFilterConditions, + expandFolderIdsWithDescendants, + LogFilterParamsSchema, +} from '@/lib/logs/filters' const logger = createLogger('LogsExportAPI') @@ -45,6 +49,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => { workflowName: sql`COALESCE(${workflow.name}, 'Deleted Workflow')`, } + if (params.folderIds) { + params.folderIds = await expandFolderIdsWithDescendants(params.workspaceId, params.folderIds) + } + const workspaceCondition = eq(workflowExecutionLogs.workspaceId, params.workspaceId) const filterConditions = buildFilterConditions(params) const conditions = filterConditions diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index cb3690441d2..bf323f78f19 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -31,7 +31,7 @@ import { listLogsContract, type WorkflowLogSummary } from '@/lib/api/contracts/l import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { buildFilterConditions } from '@/lib/logs/filters' +import { buildFilterConditions, expandFolderIdsWithDescendants } from '@/lib/logs/filters' const logger = createLogger('LogsAPI') @@ -162,6 +162,10 @@ export const GET = withRouteHandler(async (request: NextRequest) => { } } + if (params.folderIds) { + params.folderIds = await expandFolderIdsWithDescendants(params.workspaceId, params.folderIds) + } + const commonFilters = buildFilterConditions(params, { useSimpleLevelFilter: false }) if (commonFilters) workflowConditions.push(commonFilters) diff --git a/apps/sim/app/api/logs/stats/route.ts b/apps/sim/app/api/logs/stats/route.ts index 17e6a592328..eb66aeaa7b2 100644 --- a/apps/sim/app/api/logs/stats/route.ts +++ b/apps/sim/app/api/logs/stats/route.ts @@ -13,7 +13,7 @@ import { isZodError } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { buildFilterConditions } from '@/lib/logs/filters' +import { buildFilterConditions, expandFolderIdsWithDescendants } from '@/lib/logs/filters' const logger = createLogger('LogsStatsAPI') @@ -37,6 +37,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const workspaceFilter = eq(workflowExecutionLogs.workspaceId, params.workspaceId) + if (params.folderIds) { + params.folderIds = await expandFolderIdsWithDescendants( + params.workspaceId, + params.folderIds + ) + } + const commonFilters = buildFilterConditions(params, { useSimpleLevelFilter: true }) const whereCondition = commonFilters ? and(workspaceFilter, commonFilters) : workspaceFilter diff --git a/apps/sim/lib/logs/filters.ts b/apps/sim/lib/logs/filters.ts index e8947b2aeaf..ebd2aa18019 100644 --- a/apps/sim/lib/logs/filters.ts +++ b/apps/sim/lib/logs/filters.ts @@ -1,5 +1,6 @@ -import { workflow, workflowExecutionLogs } from '@sim/db/schema' -import { and, eq, gt, gte, inArray, lt, lte, ne, type SQL, sql } from 'drizzle-orm' +import { db } from '@sim/db' +import { workflow, workflowExecutionLogs, workflowFolder } from '@sim/db/schema' +import { and, eq, gt, gte, inArray, isNull, lt, lte, ne, type SQL, sql } from 'drizzle-orm' import { z } from 'zod' import type { TimeRange } from '@/stores/logs/filters/types' @@ -128,6 +129,53 @@ function buildWorkflowIdsCondition(workflowIds: string): SQL | undefined { return undefined } +/** + * Expands a CSV of selected folder IDs to include every descendant folder in the + * workspace, so that filtering by a parent folder also matches workflows that + * live in nested subfolders. + * + * Returns the original CSV when there are no descendants (or when the input is + * empty / undefined). Unknown IDs are preserved so the caller's `inArray` check + * behaves the same as today (matches nothing). + */ +export async function expandFolderIdsWithDescendants( + workspaceId: string, + folderIdsCsv: string | undefined +): Promise { + if (!folderIdsCsv) return folderIdsCsv + const seedIds = folderIdsCsv.split(',').filter(Boolean) + if (seedIds.length === 0) return folderIdsCsv + + const rows = await db + .select({ id: workflowFolder.id, parentId: workflowFolder.parentId }) + .from(workflowFolder) + .where(and(eq(workflowFolder.workspaceId, workspaceId), isNull(workflowFolder.archivedAt))) + + const childrenByParent = new Map() + for (const row of rows) { + if (!row.parentId) continue + const list = childrenByParent.get(row.parentId) + if (list) list.push(row.id) + else childrenByParent.set(row.parentId, [row.id]) + } + + const expanded = new Set(seedIds) + const queue = [...seedIds] + while (queue.length > 0) { + const current = queue.shift() as string + const children = childrenByParent.get(current) + if (!children) continue + for (const childId of children) { + if (!expanded.has(childId)) { + expanded.add(childId) + queue.push(childId) + } + } + } + + return Array.from(expanded).join(',') +} + function buildFolderIdsCondition(folderIds: string): SQL | undefined { const ids = folderIds.split(',').filter(Boolean) if (ids.length > 0) { From d13d6623046d183d992c9986d6ada3f11d89b39d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 8 May 2026 16:30:22 -0700 Subject: [PATCH 2/3] fix(logs): use pop() for O(1) dequeue in folder BFS --- apps/sim/lib/logs/filters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/sim/lib/logs/filters.ts b/apps/sim/lib/logs/filters.ts index ebd2aa18019..4c8898d3612 100644 --- a/apps/sim/lib/logs/filters.ts +++ b/apps/sim/lib/logs/filters.ts @@ -162,7 +162,7 @@ export async function expandFolderIdsWithDescendants( const expanded = new Set(seedIds) const queue = [...seedIds] while (queue.length > 0) { - const current = queue.shift() as string + const current = queue.pop() as string const children = childrenByParent.get(current) if (!children) continue for (const childId of children) { From 970299f5b4171cc045c33d741f03f94d2e655084 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 8 May 2026 16:48:04 -0700 Subject: [PATCH 3/3] fix(logs): move folder expansion to server-only module to fix client bundle build --- apps/sim/app/api/logs/export/route.ts | 7 +--- apps/sim/app/api/logs/route.ts | 3 +- apps/sim/app/api/logs/stats/route.ts | 3 +- apps/sim/lib/logs/filters.ts | 52 +------------------------- apps/sim/lib/logs/folder-expansion.ts | 53 +++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 57 deletions(-) create mode 100644 apps/sim/lib/logs/folder-expansion.ts diff --git a/apps/sim/app/api/logs/export/route.ts b/apps/sim/app/api/logs/export/route.ts index f2aa9e83a20..2c817411b68 100644 --- a/apps/sim/app/api/logs/export/route.ts +++ b/apps/sim/app/api/logs/export/route.ts @@ -5,11 +5,8 @@ import { and, desc, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { - buildFilterConditions, - expandFolderIdsWithDescendants, - LogFilterParamsSchema, -} from '@/lib/logs/filters' +import { buildFilterConditions, LogFilterParamsSchema } from '@/lib/logs/filters' +import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' const logger = createLogger('LogsExportAPI') diff --git a/apps/sim/app/api/logs/route.ts b/apps/sim/app/api/logs/route.ts index bf323f78f19..89f52048b72 100644 --- a/apps/sim/app/api/logs/route.ts +++ b/apps/sim/app/api/logs/route.ts @@ -31,7 +31,8 @@ import { listLogsContract, type WorkflowLogSummary } from '@/lib/api/contracts/l import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { buildFilterConditions, expandFolderIdsWithDescendants } from '@/lib/logs/filters' +import { buildFilterConditions } from '@/lib/logs/filters' +import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' const logger = createLogger('LogsAPI') diff --git a/apps/sim/app/api/logs/stats/route.ts b/apps/sim/app/api/logs/stats/route.ts index eb66aeaa7b2..930e2e36d39 100644 --- a/apps/sim/app/api/logs/stats/route.ts +++ b/apps/sim/app/api/logs/stats/route.ts @@ -13,7 +13,8 @@ import { isZodError } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { buildFilterConditions, expandFolderIdsWithDescendants } from '@/lib/logs/filters' +import { buildFilterConditions } from '@/lib/logs/filters' +import { expandFolderIdsWithDescendants } from '@/lib/logs/folder-expansion' const logger = createLogger('LogsStatsAPI') diff --git a/apps/sim/lib/logs/filters.ts b/apps/sim/lib/logs/filters.ts index 4c8898d3612..e8947b2aeaf 100644 --- a/apps/sim/lib/logs/filters.ts +++ b/apps/sim/lib/logs/filters.ts @@ -1,6 +1,5 @@ -import { db } from '@sim/db' -import { workflow, workflowExecutionLogs, workflowFolder } from '@sim/db/schema' -import { and, eq, gt, gte, inArray, isNull, lt, lte, ne, type SQL, sql } from 'drizzle-orm' +import { workflow, workflowExecutionLogs } from '@sim/db/schema' +import { and, eq, gt, gte, inArray, lt, lte, ne, type SQL, sql } from 'drizzle-orm' import { z } from 'zod' import type { TimeRange } from '@/stores/logs/filters/types' @@ -129,53 +128,6 @@ function buildWorkflowIdsCondition(workflowIds: string): SQL | undefined { return undefined } -/** - * Expands a CSV of selected folder IDs to include every descendant folder in the - * workspace, so that filtering by a parent folder also matches workflows that - * live in nested subfolders. - * - * Returns the original CSV when there are no descendants (or when the input is - * empty / undefined). Unknown IDs are preserved so the caller's `inArray` check - * behaves the same as today (matches nothing). - */ -export async function expandFolderIdsWithDescendants( - workspaceId: string, - folderIdsCsv: string | undefined -): Promise { - if (!folderIdsCsv) return folderIdsCsv - const seedIds = folderIdsCsv.split(',').filter(Boolean) - if (seedIds.length === 0) return folderIdsCsv - - const rows = await db - .select({ id: workflowFolder.id, parentId: workflowFolder.parentId }) - .from(workflowFolder) - .where(and(eq(workflowFolder.workspaceId, workspaceId), isNull(workflowFolder.archivedAt))) - - const childrenByParent = new Map() - for (const row of rows) { - if (!row.parentId) continue - const list = childrenByParent.get(row.parentId) - if (list) list.push(row.id) - else childrenByParent.set(row.parentId, [row.id]) - } - - const expanded = new Set(seedIds) - const queue = [...seedIds] - while (queue.length > 0) { - const current = queue.pop() as string - const children = childrenByParent.get(current) - if (!children) continue - for (const childId of children) { - if (!expanded.has(childId)) { - expanded.add(childId) - queue.push(childId) - } - } - } - - return Array.from(expanded).join(',') -} - function buildFolderIdsCondition(folderIds: string): SQL | undefined { const ids = folderIds.split(',').filter(Boolean) if (ids.length > 0) { diff --git a/apps/sim/lib/logs/folder-expansion.ts b/apps/sim/lib/logs/folder-expansion.ts new file mode 100644 index 00000000000..1ac5c599a70 --- /dev/null +++ b/apps/sim/lib/logs/folder-expansion.ts @@ -0,0 +1,53 @@ +import { db } from '@sim/db' +import { workflowFolder } from '@sim/db/schema' +import { and, eq, isNull } from 'drizzle-orm' + +/** + * Expands a CSV of selected folder IDs to include every descendant folder in the + * workspace, so that filtering by a parent folder also matches workflows that + * live in nested subfolders. + * + * Returns the original CSV when there are no descendants (or when the input is + * empty / undefined). Unknown IDs are preserved so the caller's `inArray` check + * behaves the same as today (matches nothing). + * + * Server-only: pulls in the database client. Keep separate from `filters.ts` + * (imported by client hooks) to avoid leaking postgres into the browser bundle. + */ +export async function expandFolderIdsWithDescendants( + workspaceId: string, + folderIdsCsv: string | undefined +): Promise { + if (!folderIdsCsv) return folderIdsCsv + const seedIds = folderIdsCsv.split(',').filter(Boolean) + if (seedIds.length === 0) return folderIdsCsv + + const rows = await db + .select({ id: workflowFolder.id, parentId: workflowFolder.parentId }) + .from(workflowFolder) + .where(and(eq(workflowFolder.workspaceId, workspaceId), isNull(workflowFolder.archivedAt))) + + const childrenByParent = new Map() + for (const row of rows) { + if (!row.parentId) continue + const list = childrenByParent.get(row.parentId) + if (list) list.push(row.id) + else childrenByParent.set(row.parentId, [row.id]) + } + + const expanded = new Set(seedIds) + const queue = [...seedIds] + while (queue.length > 0) { + const current = queue.pop() as string + const children = childrenByParent.get(current) + if (!children) continue + for (const childId of children) { + if (!expanded.has(childId)) { + expanded.add(childId) + queue.push(childId) + } + } + } + + return Array.from(expanded).join(',') +}