Forráskód Böngészése

Merge pull request #18637 from overleaf/mj-chat-duplicate-threads

[chat] Add endpoint for duplicating comment threads

GitOrigin-RevId: 0b3fb1b836150ccb6d213ab2bba6ce7ff6c69b4a
Mathias Jakobsen 2 éve
szülő
commit
304f572f9c

+ 30 - 0
services/chat/app/js/Features/Messages/MessageHttpController.js

@@ -82,6 +82,10 @@ export async function destroyProject(context) {
   return await callMessageHttpController(context, _destroyProject)
 }
 
+export async function duplicateCommentThreads(context) {
+  return await callMessageHttpController(context, _duplicateCommentThreads)
+}
+
 export async function getStatus(context) {
   const message = 'chat is alive'
   context.res.status(200).setBody(message)
@@ -254,3 +258,29 @@ async function _getMessages(clientThreadId, req, res) {
   logger.debug({ projectId, messages }, 'got messages')
   res.status(200).setBody(messages)
 }
+
+async function _duplicateCommentThreads(req, res) {
+  const { projectId } = req.params
+  const { threads } = req.body
+  const result = {}
+  for (const id of threads) {
+    logger.debug({ projectId, thread: id }, 'duplicating thread')
+    try {
+      const { oldRoom, newRoom } = await ThreadManager.duplicateThread(
+        projectId,
+        id
+      )
+      await MessageManager.duplicateRoomToOtherRoom(oldRoom._id, newRoom._id)
+      result[id] = { duplicateId: newRoom.thread_id }
+    } catch (error) {
+      if (error instanceof ThreadManager.MissingThreadError) {
+        // Expected error when the comment has been deleted prior to duplication
+        result[id] = { error: 'not found' }
+      } else {
+        logger.err({ error }, 'error duplicating thread')
+        result[id] = { error: 'unknown' }
+      }
+    }
+  }
+  res.json({ newThreads: result })
+}

+ 13 - 0
services/chat/app/js/Features/Messages/MessageManager.js

@@ -89,3 +89,16 @@ function _ensureIdsAreObjectIds(query) {
   }
   return query
 }
+
+export async function duplicateRoomToOtherRoom(sourceRoomId, targetRoomId) {
+  const sourceMessages = await findAllMessagesInRooms([sourceRoomId])
+  const targetMessages = sourceMessages.map(comment => {
+    return _ensureIdsAreObjectIds({
+      room_id: targetRoomId,
+      content: comment.content,
+      timestamp: comment.timestamp,
+      user_id: comment.user_id,
+    })
+  })
+  await db.messages.insertMany(targetMessages)
+}

+ 22 - 0
services/chat/app/js/Features/Threads/ThreadManager.js

@@ -1,5 +1,7 @@
 import { db, ObjectId } from '../../mongodb.js'
 
+export class MissingThreadError extends Error {}
+
 export const GLOBAL_THREAD = 'GLOBAL'
 
 export async function findOrCreateThread(projectId, threadId) {
@@ -124,3 +126,23 @@ export async function getResolvedThreadIds(projectId) {
     .toArray()
   return resolvedThreadIds
 }
+
+export async function duplicateThread(projectId, threadId) {
+  const room = await db.rooms.findOne({
+    project_id: new ObjectId(projectId),
+    thread_id: new ObjectId(threadId),
+  })
+  if (!room) {
+    throw new MissingThreadError('Trying to duplicate a non-existent thread')
+  }
+  const newRoom = {
+    project_id: room.project_id,
+    thread_id: new ObjectId(),
+  }
+  if (room.resolved) {
+    newRoom.resolved = room.resolved
+  }
+  const confirmation = await db.rooms.insertOne(newRoom)
+  newRoom._id = confirmation.insertedId
+  return { oldRoom: room, newRoom }
+}

+ 31 - 0
services/chat/chat.yaml

@@ -303,6 +303,37 @@ paths:
                 description: chat is alive
       operationId: getStatus
       description: Check that the Chat service is alive
+  '/project/{projectId}/duplicate-comment-threads':
+    parameters:
+      - schema:
+          type: string
+        name: projectId
+        in: path
+        required: true
+    post:
+      summary: Duplicate comment threads
+      operationId: duplicateCommentThreads
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                threads:
+                  type: array
+                  items:
+                    type: string
+      responses:
+        '200':
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  newThreads:
+                    type: object
+                description: Mapping of old thread ids to their duplicated thread ids
+      description: Duplicate a list of comment threads
 components:
   schemas:
     Message:

+ 93 - 0
services/chat/test/acceptance/js/CloningCommentThreadsTests.js

@@ -0,0 +1,93 @@
+import { ObjectId } from '../../../app/js/mongodb.js'
+import { expect } from 'chai'
+
+import * as ChatClient from './helpers/ChatClient.js'
+import * as ChatApp from './helpers/ChatApp.js'
+
+const user1Id = new ObjectId().toString()
+const user2Id = new ObjectId().toString()
+
+async function createCommentThread(projectId, threadId = new ObjectId()) {
+  const { response: response1 } = await ChatClient.sendMessage(
+    projectId,
+    threadId.toString(),
+    user1Id,
+    'message 1'
+  )
+  expect(response1.statusCode).to.equal(201)
+  const { response: response2 } = await ChatClient.sendMessage(
+    projectId,
+    threadId,
+    user2Id,
+    'message 2'
+  )
+  expect(response2.statusCode).to.equal(201)
+  return threadId.toString()
+}
+
+describe('Cloning comment threads', async function () {
+  const projectId = new ObjectId().toString()
+
+  before(async function () {
+    await ChatApp.ensureRunning()
+    this.thread1Id = await createCommentThread(projectId)
+    this.thread2Id = await createCommentThread(projectId)
+    this.thread3Id = await createCommentThread(projectId)
+  })
+
+  describe('with non-orphaned threads', async function () {
+    before(async function () {
+      const {
+        response: { body: result, statusCode },
+      } = await ChatClient.duplicateCommentThreads(projectId, [this.thread3Id])
+      this.result = result
+      expect(statusCode).to.equal(200)
+      expect(this.result).to.have.property('newThreads')
+      this.newThreadId = this.result.newThreads[this.thread3Id].duplicateId
+    })
+
+    it('should duplicate threads', function () {
+      expect(this.result.newThreads).to.have.property(this.thread3Id)
+      expect(this.result.newThreads[this.thread3Id]).to.have.property(
+        'duplicateId'
+      )
+      expect(this.result.newThreads[this.thread3Id].duplicateId).to.not.equal(
+        this.thread3Id
+      )
+    })
+
+    it('should not duplicate other threads threads', function () {
+      expect(this.result.newThreads).to.not.have.property(this.thread1Id)
+      expect(this.result.newThreads).to.not.have.property(this.thread2Id)
+    })
+
+    it('should duplicate the messages in the thread', async function () {
+      const {
+        response: { body: threads },
+      } = await ChatClient.getThreads(projectId)
+      function ignoreId(comment) {
+        return {
+          ...comment,
+          id: undefined,
+        }
+      }
+      expect(threads[this.thread3Id].messages.map(ignoreId)).to.deep.equal(
+        threads[this.newThreadId].messages.map(ignoreId)
+      )
+    })
+
+    it('should have two separate unlinked threads', async function () {
+      await ChatClient.sendMessage(
+        projectId,
+        this.newThreadId,
+        user1Id,
+        'third message'
+      )
+      const {
+        response: { body: threads },
+      } = await ChatClient.getThreads(projectId)
+      expect(threads[this.thread3Id].messages.length).to.equal(2)
+      expect(threads[this.newThreadId].messages.length).to.equal(3)
+    })
+  })
+})

+ 10 - 0
services/chat/test/acceptance/js/helpers/ChatClient.js

@@ -144,3 +144,13 @@ export async function destroyProject(projectId) {
     url: `/project/${projectId}`,
   })
 }
+
+export async function duplicateCommentThreads(projectId, threads) {
+  return await asyncRequest({
+    method: 'post',
+    url: `/project/${projectId}/duplicate-comment-threads`,
+    json: {
+      threads,
+    },
+  })
+}