From 386282658929120f10e14bd7e9fc0fd9ec81a9e8 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Thu, 12 Jun 2025 09:11:19 +0200 Subject: [PATCH] [web] let docstore determine user ids of tracked changes (#26333) * [docstore] add endpoint for getting user ids of tracked changes * [web] let docstore determine user ids of tracked changes GitOrigin-RevId: 8d0a131555aa827f7ff80690fedc1aca26cf0817 --- services/docstore/app.js | 4 ++ services/docstore/app/js/DocManager.js | 14 ++++++ services/docstore/app/js/HttpController.js | 7 +++ .../test/acceptance/js/GettingAllDocsTests.js | 48 +++++++++++++++++-- .../acceptance/js/helpers/DocstoreClient.js | 10 ++++ .../src/Features/Docstore/DocstoreManager.js | 13 ++++- 6 files changed, 90 insertions(+), 6 deletions(-) diff --git a/services/docstore/app.js b/services/docstore/app.js index 76659e8411..1adf0ba559 100644 --- a/services/docstore/app.js +++ b/services/docstore/app.js @@ -50,6 +50,10 @@ app.param('doc_id', function (req, res, next, docId) { app.get('/project/:project_id/doc-deleted', HttpController.getAllDeletedDocs) app.get('/project/:project_id/doc', HttpController.getAllDocs) app.get('/project/:project_id/ranges', HttpController.getAllRanges) +app.get( + '/project/:project_id/tracked-changes-user-ids', + HttpController.getTrackedChangesUserIds +) app.get('/project/:project_id/has-ranges', HttpController.projectHasRanges) app.get('/project/:project_id/doc/:doc_id', HttpController.getDoc) app.get('/project/:project_id/doc/:doc_id/deleted', HttpController.isDocDeleted) diff --git a/services/docstore/app/js/DocManager.js b/services/docstore/app/js/DocManager.js index 9b80f83eb9..6ecfe29695 100644 --- a/services/docstore/app/js/DocManager.js +++ b/services/docstore/app/js/DocManager.js @@ -132,6 +132,20 @@ const DocManager = { return docs }, + async getTrackedChangesUserIds(projectId) { + const docs = await DocManager.getAllNonDeletedDocs(projectId, { + ranges: true, + }) + const userIds = new Set() + for (const doc of docs) { + for (const change of doc.ranges?.changes || []) { + if (change.metadata.user_id === 'anonymous-user') continue + userIds.add(change.metadata.user_id) + } + } + return Array.from(userIds) + }, + async projectHasRanges(projectId) { const docs = await MongoManager.getProjectsDocs(projectId, {}, { _id: 1 }) const docIds = docs.map(doc => doc._id) diff --git a/services/docstore/app/js/HttpController.js b/services/docstore/app/js/HttpController.js index 895e8e8e7b..2e6ec2575a 100644 --- a/services/docstore/app/js/HttpController.js +++ b/services/docstore/app/js/HttpController.js @@ -83,6 +83,12 @@ async function getAllRanges(req, res) { res.json(_buildDocsArrayView(projectId, docs)) } +async function getTrackedChangesUserIds(req, res) { + const { project_id: projectId } = req.params + const userIds = await DocManager.getTrackedChangesUserIds(projectId) + res.json(userIds) +} + async function projectHasRanges(req, res) { const { project_id: projectId } = req.params const projectHasRanges = await DocManager.projectHasRanges(projectId) @@ -232,6 +238,7 @@ module.exports = { getAllDocs: expressify(getAllDocs), getAllDeletedDocs: expressify(getAllDeletedDocs), getAllRanges: expressify(getAllRanges), + getTrackedChangesUserIds: expressify(getTrackedChangesUserIds), projectHasRanges: expressify(projectHasRanges), updateDoc: expressify(updateDoc), patchDoc: expressify(patchDoc), diff --git a/services/docstore/test/acceptance/js/GettingAllDocsTests.js b/services/docstore/test/acceptance/js/GettingAllDocsTests.js index 8fe5e7d91b..c5929b78fb 100644 --- a/services/docstore/test/acceptance/js/GettingAllDocsTests.js +++ b/services/docstore/test/acceptance/js/GettingAllDocsTests.js @@ -24,26 +24,51 @@ describe('Getting all docs', function () { { _id: new ObjectId(), lines: ['one', 'two', 'three'], - ranges: { mock: 'one' }, + ranges: { + changes: [ + { + id: new ObjectId().toString(), + metadata: { user_id: 'user-id-1' }, + }, + ], + }, rev: 2, }, { _id: new ObjectId(), lines: ['aaa', 'bbb', 'ccc'], - ranges: { mock: 'two' }, + ranges: { + changes: [ + { + id: new ObjectId().toString(), + metadata: { user_id: 'user-id-2' }, + }, + ], + }, rev: 4, }, { _id: new ObjectId(), lines: ['111', '222', '333'], - ranges: { mock: 'three' }, + ranges: { + changes: [ + { + id: new ObjectId().toString(), + metadata: { user_id: 'anonymous-user' }, + }, + ], + }, rev: 6, }, ] this.deleted_doc = { _id: new ObjectId(), lines: ['deleted'], - ranges: { mock: 'four' }, + ranges: { + changes: [ + { id: new ObjectId().toString(), metadata: { user_id: 'user-id-3' } }, + ], + }, rev: 8, } const version = 42 @@ -96,7 +121,7 @@ describe('Getting all docs', function () { }) }) - return it('getAllRanges should return all the (non-deleted) doc ranges', function (done) { + it('getAllRanges should return all the (non-deleted) doc ranges', function (done) { return DocstoreClient.getAllRanges(this.project_id, (error, res, docs) => { if (error != null) { throw error @@ -109,4 +134,17 @@ describe('Getting all docs', function () { return done() }) }) + + it('getTrackedChangesUserIds should return all the user ids from (non-deleted) ranges', function (done) { + DocstoreClient.getTrackedChangesUserIds( + this.project_id, + (error, res, userIds) => { + if (error != null) { + throw error + } + userIds.should.deep.equal(['user-id-1', 'user-id-2']) + done() + } + ) + }) }) diff --git a/services/docstore/test/acceptance/js/helpers/DocstoreClient.js b/services/docstore/test/acceptance/js/helpers/DocstoreClient.js index d8fe94829b..d1833d3503 100644 --- a/services/docstore/test/acceptance/js/helpers/DocstoreClient.js +++ b/services/docstore/test/acceptance/js/helpers/DocstoreClient.js @@ -100,6 +100,16 @@ module.exports = DocstoreClient = { ) }, + getTrackedChangesUserIds(projectId, callback) { + request.get( + { + url: `http://127.0.0.1:${settings.internal.docstore.port}/project/${projectId}/tracked-changes-user-ids`, + json: true, + }, + callback + ) + }, + updateDoc(projectId, docId, lines, version, ranges, callback) { return request.post( { diff --git a/services/web/app/src/Features/Docstore/DocstoreManager.js b/services/web/app/src/Features/Docstore/DocstoreManager.js index 5fe0f27dc9..af84ede5d2 100644 --- a/services/web/app/src/Features/Docstore/DocstoreManager.js +++ b/services/web/app/src/Features/Docstore/DocstoreManager.js @@ -1,10 +1,11 @@ const { promisify } = require('util') -const { promisifyMultiResult } = require('@overleaf/promise-utils') +const { promisifyMultiResult, callbackify } = require('@overleaf/promise-utils') const request = require('request').defaults({ jar: false }) const OError = require('@overleaf/o-error') const logger = require('@overleaf/logger') const settings = require('@overleaf/settings') const Errors = require('../Errors/Errors') +const { fetchJson } = require('@overleaf/fetch-utils') const TIMEOUT = 30 * 1000 // request timeout @@ -86,6 +87,14 @@ function getAllDeletedDocs(projectId, callback) { }) } +/** + * @param {string} projectId + */ +async function getTrackedChangesUserIds(projectId) { + const url = `${settings.apis.docstore.url}/project/${projectId}/tracked-changes-user-ids` + return fetchJson(url, { signal: AbortSignal.timeout(TIMEOUT) }) +} + /** * @param {string} projectId * @param {Callback} callback @@ -292,6 +301,7 @@ module.exports = { getAllDeletedDocs, getAllRanges, getDoc, + getTrackedChangesUserIds: callbackify(getTrackedChangesUserIds), isDocDeleted, updateDoc, projectHasRanges, @@ -304,6 +314,7 @@ module.exports = { getAllDeletedDocs: promisify(getAllDeletedDocs), getAllRanges: promisify(getAllRanges), getDoc: promisifyMultiResult(getDoc, ['lines', 'rev', 'version', 'ranges']), + getTrackedChangesUserIds, isDocDeleted: promisify(isDocDeleted), updateDoc: promisifyMultiResult(updateDoc, ['modified', 'rev']), projectHasRanges: promisify(projectHasRanges),