( knowledgeBaseId: string, requestId: string )
| 543 | * Does NOT revive children that were directly deleted (deletedAt set). |
| 544 | */ |
| 545 | export async function restoreKnowledgeBase( |
| 546 | knowledgeBaseId: string, |
| 547 | requestId: string |
| 548 | ): Promise<void> { |
| 549 | const [kb] = await db |
| 550 | .select({ |
| 551 | id: knowledgeBase.id, |
| 552 | name: knowledgeBase.name, |
| 553 | deletedAt: knowledgeBase.deletedAt, |
| 554 | workspaceId: knowledgeBase.workspaceId, |
| 555 | }) |
| 556 | .from(knowledgeBase) |
| 557 | .where(eq(knowledgeBase.id, knowledgeBaseId)) |
| 558 | .limit(1) |
| 559 | |
| 560 | if (!kb) { |
| 561 | throw new Error('Knowledge base not found') |
| 562 | } |
| 563 | |
| 564 | if (!kb.deletedAt) { |
| 565 | throw new Error('Knowledge base is not archived') |
| 566 | } |
| 567 | |
| 568 | if (kb.workspaceId) { |
| 569 | const { getWorkspaceWithOwner } = await import('@/lib/workspaces/permissions/utils') |
| 570 | const ws = await getWorkspaceWithOwner(kb.workspaceId) |
| 571 | if (!ws || ws.archivedAt) { |
| 572 | throw new Error('Cannot restore knowledge base into an archived workspace') |
| 573 | } |
| 574 | } |
| 575 | |
| 576 | /** |
| 577 | * A concurrent create/rename can commit the same active name after `generateRestoreName`'s check |
| 578 | * (MVCC) and before this transaction commits. Retries pick a new random suffix; 23505 is still |
| 579 | * mapped to {@link KnowledgeBaseConflictError} if exhaustion occurs. |
| 580 | */ |
| 581 | const maxUniqueViolationRetries = 8 |
| 582 | let attemptedRestoreName = '' |
| 583 | |
| 584 | for (let attempt = 0; attempt < maxUniqueViolationRetries; attempt++) { |
| 585 | attemptedRestoreName = '' |
| 586 | try { |
| 587 | await db.transaction(async (tx) => { |
| 588 | await tx.execute(sql`SELECT 1 FROM knowledge_base WHERE id = ${knowledgeBaseId} FOR UPDATE`) |
| 589 | |
| 590 | attemptedRestoreName = await generateRestoreName(kb.name, async (candidate) => { |
| 591 | if (!kb.workspaceId) return false |
| 592 | const [match] = await tx |
| 593 | .select({ id: knowledgeBase.id }) |
| 594 | .from(knowledgeBase) |
| 595 | .where( |
| 596 | and( |
| 597 | eq(knowledgeBase.workspaceId, kb.workspaceId), |
| 598 | eq(knowledgeBase.name, candidate), |
| 599 | isNull(knowledgeBase.deletedAt) |
| 600 | ) |
| 601 | ) |
| 602 | .limit(1) |
no test coverage detected