diff --git a/services/web/app/src/Features/Docstore/DocstoreManager.js b/services/web/app/src/Features/Docstore/DocstoreManager.js index b0e799e43d..5fe0f27dc9 100644 --- a/services/web/app/src/Features/Docstore/DocstoreManager.js +++ b/services/web/app/src/Features/Docstore/DocstoreManager.js @@ -40,6 +40,9 @@ function deleteDoc(projectId, docId, name, deletedAt, callback) { }) } +/** + * @param {string} projectId + */ function getAllDocs(projectId, callback) { const url = `${settings.apis.docstore.url}/project/${projectId}/doc` request.get( diff --git a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js index 8b58419ec6..ea7efcf8ee 100644 --- a/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js +++ b/services/web/app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js @@ -10,6 +10,9 @@ const { promisifyMultiResult } = require('@overleaf/promise-utils') const ProjectGetter = require('../Project/ProjectGetter') const FileStoreHandler = require('../FileStore/FileStoreHandler') +/** + * @param {string} projectId + */ function flushProjectToMongo(projectId, callback) { _makeRequest( { @@ -29,6 +32,9 @@ function flushMultipleProjectsToMongo(projectIds, callback) { async.series(jobs, callback) } +/** + * @param {string} projectId + */ function flushProjectToMongoAndDelete(projectId, callback) { _makeRequest( { diff --git a/services/web/scripts/delete_dangling_comments.mjs b/services/web/scripts/delete_dangling_comments.mjs new file mode 100644 index 0000000000..b6033ed8e6 --- /dev/null +++ b/services/web/scripts/delete_dangling_comments.mjs @@ -0,0 +1,85 @@ +// @ts-check + +import minimist from 'minimist' +import ChatApiHandler from '../app/src/Features/Chat/ChatApiHandler.js' +import DocumentUpdaterHandler from '../app/src/Features/DocumentUpdater/DocumentUpdaterHandler.js' +import DocstoreManager from '../app/src/Features/Docstore/DocstoreManager.js' +import HistoryManager from '../app/src/Features/History/HistoryManager.js' +import { db, ObjectId } from '../app/src/infrastructure/mongodb.js' + +const OPTS = parseArgs() + +function usage() { + console.error( + 'Usage: node delete_dangling_comments.mjs [--commit] PROJECT_ID...' + ) +} + +function parseArgs() { + const args = minimist(process.argv.slice(2), { + boolean: ['commit'], + }) + if (args._.length === 0) { + usage() + process.exit(0) + } + return { + projectIds: args._, + commit: args.commit, + } +} + +async function processProject(projectId) { + console.log(`Processing project ${projectId}...`) + await DocumentUpdaterHandler.promises.flushProjectToMongoAndDelete(projectId) + const docRanges = await DocstoreManager.promises.getAllRanges(projectId) + const threads = await ChatApiHandler.promises.getThreads(projectId) + const threadIds = new Set(Object.keys(threads)) + let commentsDeleted = 0 + for (const doc of docRanges) { + const commentsDeletedInDoc = await processDoc(projectId, doc, threadIds) + commentsDeleted += commentsDeletedInDoc + } + if (OPTS.commit) { + console.log(`${commentsDeleted} comments deleted`) + if (commentsDeleted > 0) { + console.log(`Resyncing history for project ${projectId}`) + await HistoryManager.promises.resyncProject(projectId) + } + } +} + +async function processDoc(projectId, doc, threadIds) { + let commentsDeleted = 0 + for (const comment of doc.ranges.comments ?? []) { + const threadId = comment.op.t + if (!threadIds.has(threadId)) { + if (OPTS.commit) { + console.log(`Deleting dangling comment ${comment.op.t}...`) + await deleteComment(doc._id, threadId) + commentsDeleted += 1 + } else { + console.log(`Would delete dangling comment ${comment.op.t}...`) + } + } + } + return commentsDeleted +} + +async function deleteComment(docId, threadId) { + await db.docs.updateOne( + { _id: new ObjectId(docId) }, + { + $pull: { 'ranges.comments': { 'op.t': new ObjectId(threadId) } }, + } + ) +} + +// Main loop +for (const projectId of OPTS.projectIds) { + await processProject(projectId) +} +if (!OPTS.commit) { + console.log('This was a dry run. Rerun with --commit to apply changes') +} +process.exit(0)