Skip to content

Commit c913829

Browse files
committed
fix(tables): roll back partial folder restores
1 parent c6b5faf commit c913829

2 files changed

Lines changed: 73 additions & 10 deletions

File tree

‎apps/sim/lib/folders/cascade.test.ts‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@ import { folderMutationStatus } from '@/lib/folders/status'
2626

2727
const tableServiceMocks = vi.hoisted(() => ({
2828
deleteTables: vi.fn(),
29+
restoreTable: vi.fn(),
2930
}))
3031

3132
vi.mock('@/lib/table/service', () => ({
3233
deleteTables: tableServiceMocks.deleteTables,
34+
restoreTable: tableServiceMocks.restoreTable,
3335
}))
3436

3537
interface SelectCall {
@@ -614,6 +616,42 @@ describe('knowledge_base and table folder resources', () => {
614616
})
615617
).rejects.toMatchObject({ code: 'locked', message: 'Table deletion is locked' })
616618
})
619+
620+
it('re-archives a successful restore prefix when a later cohort table fails', async () => {
621+
resetDbChainMock()
622+
queueTableRows(schemaMock.userTableDefinitions, [
623+
{ id: 'tbl_accounts' },
624+
{ id: 'tbl_contacts' },
625+
])
626+
const restoreError = new Error('restore failed')
627+
tableServiceMocks.restoreTable
628+
.mockResolvedValueOnce(undefined)
629+
.mockRejectedValueOnce(restoreError)
630+
tableServiceMocks.deleteTables.mockResolvedValueOnce({
631+
archived: [{ id: 'tbl_accounts', name: 'Accounts', workspaceId: 'ws-1' }],
632+
failed: [],
633+
notFound: [],
634+
})
635+
636+
await expect(
637+
tableConfig.restoreChildren?.({
638+
workspaceId: 'ws-1',
639+
folderIds: ['folder-1'],
640+
timestamp: TIMESTAMP,
641+
})
642+
).rejects.toBe(restoreError)
643+
644+
expect(tableServiceMocks.deleteTables).toHaveBeenCalledWith(
645+
['tbl_accounts'],
646+
'folder-restore-rollback-folder-1',
647+
{
648+
expectedWorkspaceId: 'ws-1',
649+
archivedAt: TIMESTAMP,
650+
skipNotify: true,
651+
archiveAsCohort: true,
652+
}
653+
)
654+
})
617655
})
618656

619657
describe('table folder deletion guard', () => {

‎apps/sim/lib/folders/config.ts‎

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -336,21 +336,46 @@ async function archiveTableChildren(context: CascadeChildrenContext): Promise<nu
336336
/**
337337
* Restores the tables this cascade archived, through the canonical table restore so a table
338338
* whose name was taken while it was gone is renamed instead of tripping the active-name
339-
* unique index.
339+
* unique index. If a later member fails, the successful prefix is re-archived under the
340+
* original timestamp so no active referrer can point at a still-archived cohort target.
340341
*/
341342
async function restoreTableChildren(context: CascadeChildrenContext): Promise<number> {
342-
const { restoreTable } = await import('@/lib/table/service')
343+
const { deleteTables, restoreTable } = await import('@/lib/table/service')
343344
const ids = await selectChildIds(FOLDER_RESOURCES.table, context, 'archived')
344345
const restoringFolderIds = new Set(context.folderIds)
345346
const restoringTableIds = new Set(ids)
346-
347-
for (const id of ids) {
348-
// restoreFolder fires one folder-level live-list notify for the whole subtree.
349-
await restoreTable(id, `folder-cascade-${context.folderIds[0]}`, {
350-
restoringFolderIds,
351-
restoringTableIds,
352-
skipNotify: true,
353-
})
347+
const restoredIds: string[] = []
348+
349+
try {
350+
for (const id of ids) {
351+
// restoreFolder fires one folder-level live-list notify for the whole subtree.
352+
await restoreTable(id, `folder-cascade-${context.folderIds[0]}`, {
353+
restoringFolderIds,
354+
restoringTableIds,
355+
skipNotify: true,
356+
})
357+
restoredIds.push(id)
358+
}
359+
} catch (error) {
360+
const rollback = await deleteTables(
361+
restoredIds,
362+
`folder-restore-rollback-${context.folderIds[0]}`,
363+
{
364+
expectedWorkspaceId: context.workspaceId,
365+
archivedAt: context.timestamp,
366+
skipNotify: true,
367+
archiveAsCohort: true,
368+
}
369+
)
370+
if (rollback.terminalError) throw rollback.terminalError
371+
if (
372+
rollback.failed.length > 0 ||
373+
rollback.notFound.length > 0 ||
374+
rollback.archived.length !== restoredIds.length
375+
) {
376+
throw new OrchestrationError('internal', 'Failed to roll back the table restore cohort')
377+
}
378+
throw error
354379
}
355380

356381
return ids.length

0 commit comments

Comments
 (0)