Skip to content

Commit 702a0a3

Browse files
committed
fix(tables): roll back partial folder restores
1 parent 0a66b67 commit 702a0a3

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

617655
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
@@ -368,21 +368,46 @@ async function archiveTableChildren(context: CascadeChildrenContext): Promise<nu
368368
/**
369369
* Restores the tables this cascade archived, through the canonical table restore so a table
370370
* whose name was taken while it was gone is renamed instead of tripping the active-name
371-
* unique index.
371+
* unique index. If a later member fails, the successful prefix is re-archived under the
372+
* original timestamp so no active referrer can point at a still-archived cohort target.
372373
*/
373374
async function restoreTableChildren(context: CascadeChildrenContext): Promise<number> {
374-
const { restoreTable } = await import('@/lib/table/service')
375+
const { deleteTables, restoreTable } = await import('@/lib/table/service')
375376
const ids = await selectChildIds(FOLDER_RESOURCES.table, context, 'archived')
376377
const restoringFolderIds = new Set(context.folderIds)
377378
const restoringTableIds = new Set(ids)
378-
379-
for (const id of ids) {
380-
// restoreFolder fires one folder-level live-list notify for the whole subtree.
381-
await restoreTable(id, `folder-cascade-${context.folderIds[0]}`, {
382-
restoringFolderIds,
383-
restoringTableIds,
384-
skipNotify: true,
385-
})
379+
const restoredIds: string[] = []
380+
381+
try {
382+
for (const id of ids) {
383+
// restoreFolder fires one folder-level live-list notify for the whole subtree.
384+
await restoreTable(id, `folder-cascade-${context.folderIds[0]}`, {
385+
restoringFolderIds,
386+
restoringTableIds,
387+
skipNotify: true,
388+
})
389+
restoredIds.push(id)
390+
}
391+
} catch (error) {
392+
const rollback = await deleteTables(
393+
restoredIds,
394+
`folder-restore-rollback-${context.folderIds[0]}`,
395+
{
396+
expectedWorkspaceId: context.workspaceId,
397+
archivedAt: context.timestamp,
398+
skipNotify: true,
399+
archiveAsCohort: true,
400+
}
401+
)
402+
if (rollback.terminalError) throw rollback.terminalError
403+
if (
404+
rollback.failed.length > 0 ||
405+
rollback.notFound.length > 0 ||
406+
rollback.archived.length !== restoredIds.length
407+
) {
408+
throw new OrchestrationError('internal', 'Failed to roll back the table restore cohort')
409+
}
410+
throw error
386411
}
387412

388413
return ids.length

0 commit comments

Comments
 (0)