diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index e8f8c815a0..8ef7b30c2a 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -78,7 +78,7 @@ import { buildStorageFromConfig, createStorageFromConfig, storageConfigFromEnv } import { program, type Command } from 'commander' import { addControlledDocumentRank } from './qms' import { clearTelegramHistory } from './telegram' -import { backupRestore, diffWorkspace, updateField } from './workspace' +import { backupRestore, diffWorkspace, restoreRemovedDoc, updateField } from './workspace' import core, { AccountRole, @@ -2296,6 +2296,15 @@ export function devTool ( }) }) + program + .command('restore-removed-doc ') + .requiredOption('--ids ', 'ids') + .action(async (workspace: string, cmd: { ids: string }) => { + const wsid = getWorkspaceId(workspace) + const endpoint = await getTransactorEndpoint(generateToken(systemAccountEmail, wsid), 'external') + await restoreRemovedDoc(toolCtx, wsid, endpoint, cmd.ids) + }) + program .command('add-controlled-doc-rank-mongo') .description('add rank to controlled documents') diff --git a/dev/tool/src/workspace.ts b/dev/tool/src/workspace.ts index 718ba13d08..dd3bf71022 100644 --- a/dev/tool/src/workspace.ts +++ b/dev/tool/src/workspace.ts @@ -18,6 +18,7 @@ import contact from '@hcengineering/contact' import core, { DOMAIN_TX, getWorkspaceId, + TxOperations, type BackupClient, type BaseWorkspaceInfo, type Class, @@ -161,3 +162,66 @@ export async function backupRestore ( await storageAdapter.close() } } + +export async function restoreRemovedDoc ( + ctx: MeasureContext, + workspaceId: WorkspaceId, + transactorUrl: string, + idsVal: string +): Promise { + const ids = idsVal.split(';').map((it) => it.trim()) as Ref[] + const connection = (await connect(transactorUrl, workspaceId, undefined, { + mode: 'backup', + model: 'upgrade', // Required for force all clients reload after operation will be complete. + admin: 'true' + })) as unknown as CoreClient & BackupClient + try { + for (const id of ids) { + try { + ctx.info('start restoring', { id }) + const ops = new TxOperations(connection, core.account.System) + const processed = new Set>() + const txes = await getObjectTxesAndRelatedTxes(ctx, ops, id, processed, true) + txes.filter((p) => p._class !== core.class.TxRemoveDoc).sort((a, b) => a.modifiedOn - b.modifiedOn) + for (const tx of txes) { + tx.space = core.space.DerivedTx + await ops.tx(tx) + } + ctx.info('success restored', { id }) + } catch (err) { + ctx.error('error restoring', { id, err }) + } + } + } finally { + await connection.sendForceClose() + await connection.close() + } +} + +async function getObjectTxesAndRelatedTxes ( + ctx: MeasureContext, + client: TxOperations, + objectId: Ref, + processed: Set>, + filterRemoved = false +): Promise { + ctx.info('Find txes for', { objectId }) + const result: Tx[] = [] + if (processed.has(objectId)) { + return result + } + processed.add(objectId) + let txes = (await client.findAll(core.class.TxCUD, { objectId })) as Tx[] + if (filterRemoved) { + txes = txes.filter((it) => it._class !== core.class.TxRemoveDoc) + } + result.push(...txes) + const relatedTxes = await client.findAll(core.class.TxCUD, { attachedTo: objectId }) + result.push(...relatedTxes) + const relatedIds = new Set(relatedTxes.map((it) => it.objectId)) + for (const relatedId of relatedIds) { + const rel = await getObjectTxesAndRelatedTxes(ctx, client, relatedId, processed) + result.push(...rel) + } + return result +}