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

[chat] Add endpoint for duplicating comment threads

GitOrigin-RevId: 0b3fb1b836150ccb6d213ab2bba6ce7ff6c69b4a
This commit is contained in:
Mathias Jakobsen 2024-06-07 12:06:23 +01:00 committed by Copybot
parent f0eba8e742
commit 304f572f9c
6 changed files with 199 additions and 0 deletions

View file

@ -82,6 +82,10 @@ export async function destroyProject(context) {
return await callMessageHttpController(context, _destroyProject) return await callMessageHttpController(context, _destroyProject)
} }
export async function duplicateCommentThreads(context) {
return await callMessageHttpController(context, _duplicateCommentThreads)
}
export async function getStatus(context) { export async function getStatus(context) {
const message = 'chat is alive' const message = 'chat is alive'
context.res.status(200).setBody(message) context.res.status(200).setBody(message)
@ -254,3 +258,29 @@ async function _getMessages(clientThreadId, req, res) {
logger.debug({ projectId, messages }, 'got messages') logger.debug({ projectId, messages }, 'got messages')
res.status(200).setBody(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 })
}

View file

@ -89,3 +89,16 @@ function _ensureIdsAreObjectIds(query) {
} }
return 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)
}

View file

@ -1,5 +1,7 @@
import { db, ObjectId } from '../../mongodb.js' import { db, ObjectId } from '../../mongodb.js'
export class MissingThreadError extends Error {}
export const GLOBAL_THREAD = 'GLOBAL' export const GLOBAL_THREAD = 'GLOBAL'
export async function findOrCreateThread(projectId, threadId) { export async function findOrCreateThread(projectId, threadId) {
@ -124,3 +126,23 @@ export async function getResolvedThreadIds(projectId) {
.toArray() .toArray()
return resolvedThreadIds 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 }
}

View file

@ -303,6 +303,37 @@ paths:
description: chat is alive description: chat is alive
operationId: getStatus operationId: getStatus
description: Check that the Chat service is alive 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: components:
schemas: schemas:
Message: Message:

View file

@ -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)
})
})
})

View file

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