(params: CreateForkParams)
| 107 | * all credential references) are cleared; env-var references are preserved. |
| 108 | */ |
| 109 | export async function createFork(params: CreateForkParams): Promise<CreateForkResult> { |
| 110 | const { source, policy, userId, requestId = 'unknown' } = params |
| 111 | const selection = params.selection ?? EMPTY_SELECTION |
| 112 | const childName = params.name?.trim() || `${source.name} (fork)` |
| 113 | |
| 114 | // Read the source's deployed workflows + states BEFORE the transaction so these |
| 115 | // global-pool reads don't check out a second pooled connection from inside the |
| 116 | // fork tx (which can deadlock the pool at saturation). |
| 117 | const { deployedWorkflows, sourceStates } = await loadSourceDeployedStates(source.id) |
| 118 | |
| 119 | // Documents the copied workflows reference (document-selector values + nested documentId |
| 120 | // tool params). Those whose parent KB is being copied get a placeholder + id map inside the |
| 121 | // fork tx so their references remap to the copied document instead of being cleared. |
| 122 | const referencedDocumentIds = collectReferencedDocumentIds( |
| 123 | deployedWorkflows.flatMap((wf) => { |
| 124 | const sourceState = sourceStates.get(wf.id) |
| 125 | return sourceState ? [sourceState] : [] |
| 126 | }) |
| 127 | ) |
| 128 | |
| 129 | const forkedWorkflowNames: string[] = [] |
| 130 | let forkedResourceNames: ForkCopiedResourceNames = { |
| 131 | tables: [], |
| 132 | knowledgeBases: [], |
| 133 | customTools: [], |
| 134 | skills: [], |
| 135 | workflowMcpServers: [], |
| 136 | } |
| 137 | const { result, blobTasks, contentPlan, contentRefMaps } = await db.transaction(async (tx) => { |
| 138 | await setForkLockTimeout(tx) |
| 139 | const now = new Date() |
| 140 | const childWorkspaceId = generateId() |
| 141 | |
| 142 | await tx.insert(workspace).values({ |
| 143 | id: childWorkspaceId, |
| 144 | name: childName, |
| 145 | ownerId: userId, |
| 146 | organizationId: policy.organizationId, |
| 147 | workspaceMode: policy.workspaceMode, |
| 148 | billedAccountUserId: policy.billedAccountUserId, |
| 149 | allowPersonalApiKeys: true, |
| 150 | forkedFromWorkspaceId: source.id, |
| 151 | createdAt: now, |
| 152 | updatedAt: now, |
| 153 | }) |
| 154 | |
| 155 | const sourcePermissions = await tx |
| 156 | .select({ userId: permissions.userId, permissionType: permissions.permissionType }) |
| 157 | .from(permissions) |
| 158 | .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, source.id))) |
| 159 | |
| 160 | const permissionByUser = new Map<string, PermissionType>() |
| 161 | for (const row of sourcePermissions) { |
| 162 | permissionByUser.set(row.userId, row.permissionType) |
| 163 | } |
| 164 | permissionByUser.set(userId, 'admin') |
| 165 | if ( |
| 166 | policy.workspaceMode === WORKSPACE_MODE.ORGANIZATION && |
no test coverage detected