From c538091fa810c3824c16fedf3cc80edf6179dfd1 Mon Sep 17 00:00:00 2001 From: Domagoj Kriskovic Date: Wed, 2 Jul 2025 11:32:48 +0200 Subject: [PATCH] Add script to check and fix duplicate collaborators in projects (#26572) * [web] Add script to check and fix duplicate collaborators in projects * use batchedUpdate * project-id param and BATCH_RANGE_START, GitOrigin-RevId: 451549eaff255dfae3e55515786d7a68184d557f --- .../scripts/check_duplicate_collaborators.mjs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 services/web/scripts/check_duplicate_collaborators.mjs diff --git a/services/web/scripts/check_duplicate_collaborators.mjs b/services/web/scripts/check_duplicate_collaborators.mjs new file mode 100644 index 0000000000..cf4ba7eafd --- /dev/null +++ b/services/web/scripts/check_duplicate_collaborators.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +/** + * Script to check for and optionally fix duplicate collaborators in projects + * + * A duplicate collaborator is when the same user id appears in multiple collaborator + * arrays for the same project (collaberator_refs, readOnly_refs, reviewer_refs, etc.) + * + * If "--fix" is used, this script will remove users from higher privilege roles and keeps them in lower privilege roles + * + * Usage: + * node scripts/check_duplicate_collaborators.mjs [--fix] [--project-id=] + */ + +import { + batchedUpdate, + READ_PREFERENCE_SECONDARY, +} from '@overleaf/mongo-utils/batchedUpdate.js' +import { db, ObjectId } from '../app/src/infrastructure/mongodb.js' +import minimist from 'minimist' +import { scriptRunner } from './lib/ScriptRunner.mjs' + +const args = minimist(process.argv.slice(2), { + boolean: ['fix'], + string: ['project-id', 'start-date', 'end-date'], + default: { + fix: false, + }, +}) + +async function fixDuplicateCollaborators(project, trackProgress) { + const dryRun = !args.fix + const removeCollaboratorRefs = [] + const removeReviewerRefs = [] + + for (const reviewerRef of project.reviewer_refs || []) { + if (includesId(project.readOnly_refs, reviewerRef)) { + removeReviewerRefs.push(reviewerRef) // remove from reviewer_refs (keep read-only) + } + if (includesId(project.collaberator_refs, reviewerRef)) { + removeCollaboratorRefs.push(reviewerRef) // remove from collaberator_refs (keep reviewer) + } + } + + if ( + !dryRun && + (removeCollaboratorRefs.length > 0 || removeReviewerRefs.length > 0) + ) { + await db.projects.updateOne( + { _id: project._id }, + { + $pull: { + collaberator_refs: { $in: removeCollaboratorRefs }, + reviewer_refs: { $in: removeReviewerRefs }, + }, + } + ) + } + + const action = args.fix ? 'Removed' : 'Found duplicates in' + + if (removeCollaboratorRefs.length > 0) { + trackProgress( + `${action} collaborators from project ${project._id}:`, + removeCollaboratorRefs + ) + } + if (removeReviewerRefs.length > 0) { + trackProgress( + `${action} reviewers from project ${project._id}:`, + removeReviewerRefs + ) + } +} + +async function main(trackProgress) { + if (!args['start-date'] && !args['project-id']) { + console.error( + 'Please provide either --start-date or --project-id argument.' + ) + process.exit(1) + } + + if (args['project-id']) { + const projectId = new ObjectId(args['project-id']) + const project = await db.projects.findOne( + { _id: projectId }, + { + readPreference: READ_PREFERENCE_SECONDARY, + projection: { + _id: 1, + collaberator_refs: 1, + readOnly_refs: 1, + reviewer_refs: 1, + }, + } + ) + + if (!project) { + console.error(`Project with id ${projectId} not found`) + process.exit(1) + } + + await fixDuplicateCollaborators(project, trackProgress) + + return + } + + let projectsProcessed = 0 + await batchedUpdate( + db.projects, + { + reviewer_refs: { $ne: [] }, + $or: [{ readOnly_refs: { $ne: [] } }, { collaberator_refs: { $ne: [] } }], + }, + /** + * @param {Array} projects + * @return {Promise} + */ + async function projects(projects) { + for (const project of projects) { + projectsProcessed += 1 + if (projectsProcessed % 10000 === 0) { + console.log(projectsProcessed, 'projects processed') + } + + await fixDuplicateCollaborators(project, trackProgress) + } + }, + { + _id: 1, + collaberator_refs: 1, + readOnly_refs: 1, + reviewer_refs: 1, + }, + undefined, + { + trackProgress, + BATCH_RANGE_START: new Date(args['start-date']).toISOString(), + BATCH_RANGE_END: args['end-date'] + ? new Date(args['end-date']).toISOString() + : new Date().toISOString(), + } + ) +} + +function includesId(array, id) { + return array?.some(item => item.toString() === id.toString()) +} + +try { + await scriptRunner(main) + process.exit() +} catch (error) { + console.error(error) + process.exit(1) +}