(tableId: string, requestId: string)
| 611 | * Restores an archived table. |
| 612 | */ |
| 613 | export async function restoreTable(tableId: string, requestId: string): Promise<void> { |
| 614 | const table = await getTableById(tableId, { includeArchived: true }) |
| 615 | if (!table) { |
| 616 | throw new Error('Table not found') |
| 617 | } |
| 618 | |
| 619 | if (!table.archivedAt) { |
| 620 | throw new Error('Table is not archived') |
| 621 | } |
| 622 | |
| 623 | if (table.workspaceId) { |
| 624 | const { getWorkspaceWithOwner } = await import('@/lib/workspaces/permissions/utils') |
| 625 | const ws = await getWorkspaceWithOwner(table.workspaceId) |
| 626 | if (!ws || ws.archivedAt) { |
| 627 | throw new Error('Cannot restore table into an archived workspace') |
| 628 | } |
| 629 | } |
| 630 | |
| 631 | /** |
| 632 | * A concurrent rename/create can claim the chosen name after `generateRestoreName`'s check (MVCC). |
| 633 | * Retries pick a new random suffix; 23505 maps to {@link TableConflictError} after exhaustion. |
| 634 | */ |
| 635 | const maxUniqueViolationRetries = 8 |
| 636 | let attemptedRestoreName = '' |
| 637 | |
| 638 | for (let attempt = 0; attempt < maxUniqueViolationRetries; attempt++) { |
| 639 | attemptedRestoreName = '' |
| 640 | try { |
| 641 | await db.transaction(async (tx) => { |
| 642 | await setTableTxTimeouts(tx) |
| 643 | await tx.execute(sql`SELECT 1 FROM user_table_definitions WHERE id = ${tableId} FOR UPDATE`) |
| 644 | |
| 645 | attemptedRestoreName = await generateRestoreName(table.name, async (candidate) => { |
| 646 | const [match] = await tx |
| 647 | .select({ id: userTableDefinitions.id }) |
| 648 | .from(userTableDefinitions) |
| 649 | .where( |
| 650 | and( |
| 651 | eq(userTableDefinitions.workspaceId, table.workspaceId), |
| 652 | eq(userTableDefinitions.name, candidate), |
| 653 | isNull(userTableDefinitions.archivedAt) |
| 654 | ) |
| 655 | ) |
| 656 | .limit(1) |
| 657 | return !!match |
| 658 | }) |
| 659 | |
| 660 | const now = new Date() |
| 661 | await tx |
| 662 | .update(userTableDefinitions) |
| 663 | .set({ archivedAt: null, updatedAt: now, name: attemptedRestoreName }) |
| 664 | .where(eq(userTableDefinitions.id, tableId)) |
| 665 | }) |
| 666 | break |
| 667 | } catch (error: unknown) { |
| 668 | if (getPostgresErrorCode(error) !== '23505') { |
| 669 | throw error |
| 670 | } |
no test coverage detected