Files
sim/apps/sim/lib/folders/naming.ts
T
Waleed 5b7cf99126 feat(folders): add the generic resourceType-driven folder engine (#6037)
- Replaces the workflow-hardcoded folder orchestration with a config-driven engine: one implementation parameterized by `FOLDER_RESOURCES`, with `lib/workflows/orchestration/folder-lifecycle.ts` reduced from 620 lines to thin wrappers so no existing caller moved
- Threads `resourceType` through all four folder routes and the React Query keys; contract widened to `workflow | knowledge_base | table`
- Folder locking moved behind a `supportsLocking` capability rather than scattered `resourceType === 'workflow'` checks
- Folder restore now matches dependents by the cascade timestamp, so a schedule or webhook archived independently before a folder delete stays archived (deliberate behavior change, documented in the PR)
- 25 cascade tests including a fixed-statement-count assertion guarding against N+1 regressions
2026-07-28 19:29:07 -07:00

48 lines
1.7 KiB
TypeScript

import type { db } from '@sim/db'
import { folder as folderTable } from '@sim/db/schema'
import { and, eq, isNull } from 'drizzle-orm'
import type { FolderResourceType } from '@/lib/api/contracts/folders'
type DbOrTx = Pick<typeof db, 'select'>
/**
* Returns `requestedName`, or the first `"<name> (N)"` variant not already taken by an
* active sibling under `parentId`.
*
* The generic `folder` table has a partial unique index on active
* `(workspaceId, resourceType, parentId, name)`, so any path that makes a row active with a
* caller-supplied name has to either dedup here or handle a 23505. Use this where the user
* has no opportunity to choose a different name (duplicate, restore); return a conflict
* instead where they do (create, rename).
*
* The `" (N)"` shape deliberately matches both the client-side dedup in
* `useFolderCreateWithDedup` and the backfill in migration 0272, so a deduped name reads the
* same however it was produced.
*/
export async function deduplicateFolderName(
tx: DbOrTx,
workspaceId: string,
parentId: string | null,
requestedName: string,
resourceType: FolderResourceType
): Promise<string> {
const siblingRows = await tx
.select({ name: folderTable.name })
.from(folderTable)
.where(
and(
eq(folderTable.workspaceId, workspaceId),
eq(folderTable.resourceType, resourceType),
parentId ? eq(folderTable.parentId, parentId) : isNull(folderTable.parentId),
isNull(folderTable.deletedAt)
)
)
const siblingNames = new Set(siblingRows.map((row) => row.name))
if (!siblingNames.has(requestedName)) return requestedName
let suffix = 1
while (siblingNames.has(`${requestedName} (${suffix})`)) suffix += 1
return `${requestedName} (${suffix})`
}