diff --git a/.changeset/five-cherries-hang.md b/.changeset/five-cherries-hang.md new file mode 100644 index 0000000000000..bc33eb7cf309b --- /dev/null +++ b/.changeset/five-cherries-hang.md @@ -0,0 +1,20 @@ +--- +'@rocket.chat/omnichannel-transcript': patch +'@rocket.chat/authorization-service': patch +'@rocket.chat/stream-hub-service': patch +'@rocket.chat/network-broker': patch +'@rocket.chat/presence-service': patch +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/account-service': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/ddp-streamer': patch +'@rocket.chat/queue-worker': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/apps': patch +'@rocket.chat/i18n': patch +'@rocket.chat/meteor': patch +--- + +Fixes behavior of app updates that would save undesired field changes to documents diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts index 824a9d5c15afd..16366626c07c8 100644 --- a/apps/meteor/app/apps/server/bridges/messages.ts +++ b/apps/meteor/app/apps/server/bridges/messages.ts @@ -5,7 +5,7 @@ import type { ITypingDescriptor } from '@rocket.chat/apps-engine/server/bridges/ import { MessageBridge } from '@rocket.chat/apps-engine/server/bridges/MessageBridge'; import { api } from '@rocket.chat/core-services'; import type { IMessage } from '@rocket.chat/core-typings'; -import { Users, Subscriptions, Messages } from '@rocket.chat/models'; +import { Users, Subscriptions } from '@rocket.chat/models'; import { deleteMessage } from '../../../lib/server/functions/deleteMessage'; import { updateMessage } from '../../../lib/server/functions/updateMessage'; @@ -44,12 +44,8 @@ export class AppMessageBridge extends MessageBridge { throw new Error('Invalid editor assigned to the message for the update.'); } - if (!message.id || !(await Messages.findOneById(message.id))) { - throw new Error('A message must exist to update.'); - } - // #TODO: #AppsEngineTypes - Remove explicit types and typecasts once the apps-engine definition/implementation mismatch is fixed. - const msg: IMessage | undefined = await this.orch.getConverters()?.get('messages').convertAppMessage(message); + const msg = await this.orch.getConverters()?.get('messages').convertAppMessage(message, true); const editor = await Users.findOneById(message.editor.id); if (!editor) { diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index d88881330a17d..ea8a7fdf6e7e6 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -163,13 +163,13 @@ export class AppRoomBridge extends RoomBridge { protected async update(room: IRoom, members: Array = [], appId: string): Promise { this.orch.debugLog(`The App ${appId} is updating a room.`); - if (!room.id || !(await Rooms.findOneById(room.id))) { - throw new Error('A room must exist to update.'); - } + const rm = await this.orch.getConverters()?.get('rooms').convertAppRoom(room, true); - const rm = await this.orch.getConverters()?.get('rooms').convertAppRoom(room); + const updateResult = await Rooms.updateOne({ _id: room.id }, { $set: rm }); - await Rooms.updateOne({ _id: rm._id }, { $set: rm as Partial }); + if (!updateResult.matchedCount) { + throw new Error('Room id not found'); + } for await (const username of members) { const member = await Users.findOneByUsername(username, {}); @@ -178,7 +178,7 @@ export class AppRoomBridge extends RoomBridge { continue; } - await addUserToRoom(rm._id, member); + await addUserToRoom(room.id, member); } } diff --git a/apps/meteor/app/apps/server/converters/messages.js b/apps/meteor/app/apps/server/converters/messages.js index 530a8aed6e4a5..8cc6f4ea270f2 100644 --- a/apps/meteor/app/apps/server/converters/messages.js +++ b/apps/meteor/app/apps/server/converters/messages.js @@ -137,19 +137,23 @@ export class AppMessagesConverter { return transformMappedData(msgObj, map); } - async convertAppMessage(message) { - if (!message || !message.room) { + async convertAppMessage(message, isPartial = false) { + if (!message) { return undefined; } - const room = await Rooms.findOneById(message.room.id); + let rid; + if (message.room?.id) { + const room = await Rooms.findOneById(message.room.id, { projection: { _id: 1 } }); + rid = room?._id; + } - if (!room) { + if (!rid && !isPartial) { throw new Error('Invalid room provided on the message.'); } let u; - if (message.sender && message.sender.id) { + if (message.sender?.id) { const user = await Users.findOneById(message.sender.id); if (user) { @@ -178,14 +182,27 @@ export class AppMessagesConverter { const attachments = this._convertAppAttachments(message.attachments); + let _id = message.id; + let ts = message.createdAt; + + if (!isPartial) { + if (!message.id) { + _id = Random.id(); + } + + if (!message.createdAt) { + ts = new Date(); + } + } + const newMessage = { - _id: message.id || Random.id(), + _id, ...('threadId' in message && { tmid: message.threadId }), - rid: room._id, + rid, u, msg: message.text, - ts: message.createdAt || new Date(), - _updatedAt: message.updatedAt || new Date(), + ts, + _updatedAt: message.updatedAt, ...(editedBy && { editedBy }), ...('editedAt' in message && { editedAt: message.editedAt }), ...('emoji' in message && { emoji: message.emoji }), @@ -200,7 +217,17 @@ export class AppMessagesConverter { ...('token' in message && { token: message.token }), }; - return Object.assign(newMessage, message._unmappedProperties_); + if (isPartial) { + Object.entries(newMessage).forEach(([key, value]) => { + if (typeof value === 'undefined') { + delete newMessage[key]; + } + }); + } else { + Object.assign(newMessage, message._unmappedProperties_); + } + + return newMessage; } _convertAppAttachments(attachments) { diff --git a/apps/meteor/app/apps/server/converters/rooms.js b/apps/meteor/app/apps/server/converters/rooms.js index 03cbc2028b7c2..b2bbcda49610d 100644 --- a/apps/meteor/app/apps/server/converters/rooms.js +++ b/apps/meteor/app/apps/server/converters/rooms.js @@ -20,7 +20,7 @@ export class AppRoomsConverter { return this.convertRoom(room); } - async convertAppRoom(room) { + async convertAppRoom(room, isPartial = false) { if (!room) { return undefined; } @@ -88,6 +88,26 @@ export class AppRoomsConverter { contactId = contact._id; } + let _default; + if (typeof room.isDefault !== 'undefined') { + _default = room.isDefault; + } + + let ro; + if (typeof room.isReadOnly !== 'undefined') { + ro = room.isReadOnly; + } + + let sysMes; + if (typeof room.displaySystemMessages !== 'undefined') { + sysMes = room.displaySystemMessages; + } + + let msgs; + if (typeof room.messageCount !== 'undefined') { + msgs = room.messageCount; + } + const newRoom = { ...(room.id && { _id: room.id }), fname: room.displayName, @@ -95,17 +115,17 @@ export class AppRoomsConverter { t: room.type, u, v, + ro, + sysMes, + msgs, departmentId, servedBy, closedBy, members: room.members, uids: room.userIds, - default: typeof room.isDefault === 'undefined' ? false : room.isDefault, - ro: typeof room.isReadOnly === 'undefined' ? false : room.isReadOnly, - sysMes: typeof room.displaySystemMessages === 'undefined' ? true : room.displaySystemMessages, + default: _default, waitingResponse: typeof room.isWaitingResponse === 'undefined' ? undefined : !!room.isWaitingResponse, open: typeof room.isOpen === 'undefined' ? undefined : !!room.isOpen, - msgs: room.messageCount || 0, ts: room.createdAt, _updatedAt: room.updatedAt, closedAt: room.closedAt, @@ -122,7 +142,17 @@ export class AppRoomsConverter { }), }; - return Object.assign(newRoom, room._unmappedProperties_); + if (isPartial) { + Object.entries(newRoom).forEach(([key, value]) => { + if (typeof value === 'undefined') { + delete newRoom[key]; + } + }); + } else { + Object.assign(newRoom, room._unmappedProperties_); + } + + return newRoom; } async convertRoom(originalRoom) { @@ -245,6 +275,7 @@ export class AppRoomsConverter { if (originalRoom.closer === 'user') { return this.orch.getConverters().get('users').convertById(closedBy._id); } + return this.orch.getConverters().get('visitors').convertById(closedBy._id); }, servedBy: async (room) => { diff --git a/apps/meteor/tests/unit/app/apps/server/messages.tests.js b/apps/meteor/tests/unit/app/apps/server/messages.tests.js index 195f02bbe8930..29921ed84a790 100644 --- a/apps/meteor/tests/unit/app/apps/server/messages.tests.js +++ b/apps/meteor/tests/unit/app/apps/server/messages.tests.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import proxyquire from 'proxyquire'; -import { appMessageMock, appMessageInvalidRoomMock } from './mocks/data/messages.data'; +import { appMessageMock, appMessageInvalidRoomMock, appPartialMessageMock } from './mocks/data/messages.data'; import { MessagesMock } from './mocks/models/Messages.mock'; import { RoomsMock } from './mocks/models/Rooms.mock'; import { UsersMock } from './mocks/models/Users.mock'; @@ -134,6 +134,20 @@ describe('The AppMessagesConverter instance', () => { }); }); + it('should return a proper schema when receiving a partial object', async () => { + const rocketchatMessage = await messagesConverter.convertAppMessage(appPartialMessageMock, true); + + expect(rocketchatMessage).to.have.property('_id', 'appPartialMessageMock'); + expect(rocketchatMessage).to.have.property('groupable', false); + expect(rocketchatMessage).to.have.property('emoji', ':smirk:'); + expect(rocketchatMessage).to.have.property('alias', 'rocket.feline'); + + expect(rocketchatMessage).to.not.have.property('ts'); + expect(rocketchatMessage).to.not.have.property('u'); + expect(rocketchatMessage).to.not.have.property('rid'); + expect(rocketchatMessage).to.not.have.property('_updatedAt'); + }); + it('should merge `_unmappedProperties_` into the returned message', async () => { const rocketchatMessage = await messagesConverter.convertAppMessage(appMessageMock); @@ -141,6 +155,18 @@ describe('The AppMessagesConverter instance', () => { expect(rocketchatMessage).to.have.property('t', 'uj'); }); + it('should not merge `_unmappedProperties_` into the returned message when receiving a partial object', async () => { + const invalidPartialMessage = structuredClone(appPartialMessageMock); + invalidPartialMessage._unmappedProperties_ = { + t: 'uj', + }; + + const rocketchatMessage = await messagesConverter.convertAppMessage(invalidPartialMessage, true); + + expect(rocketchatMessage).to.not.have.property('_unmappedProperties_'); + expect(rocketchatMessage).to.not.have.property('t'); + }); + it('should throw if message has an invalid room', async () => { try { await messagesConverter.convertAppMessage(appMessageInvalidRoomMock); diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.js b/apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.js index 70d1505d97fef..50e9089140f5b 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/data/messages.data.js @@ -56,6 +56,14 @@ export const appMessageMock = { }, }; +export const appPartialMessageMock = { + id: 'appPartialMessageMock', + text: 'rocket.cat', + groupable: false, + emoji: ':smirk:', + alias: 'rocket.feline', +}; + export const appMessageInvalidRoomMock = { id: 'appMessageInvalidRoomMock', text: 'rocket.cat', diff --git a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js index 5a0a776f037bf..6cf0370f1754e 100644 --- a/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js +++ b/apps/meteor/tests/unit/app/apps/server/mocks/models/Rooms.mock.js @@ -108,6 +108,13 @@ export class RoomsMock extends BaseModelMock { customFields: {}, }, + GENERALPartial: { + id: 'GENERAL', + slugifiedName: 'general', + displaySystemMessages: true, + updatedAt: new Date('2019-04-10T17:44:34.931Z'), + }, + LivechatRoom: { id: 'LivechatRoom', slugifiedName: undefined, diff --git a/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts new file mode 100644 index 0000000000000..718d79baef361 --- /dev/null +++ b/apps/meteor/tests/unit/app/apps/server/rooms.tests.ts @@ -0,0 +1,122 @@ +import type { IAppRoomsConverter, IAppsRoom } from '@rocket.chat/apps'; +import type { IRoom } from '@rocket.chat/core-typings'; +import { expect } from 'chai'; +import { before, describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; + +import { MessagesMock } from './mocks/models/Messages.mock'; +import { RoomsMock } from './mocks/models/Rooms.mock'; +import { UsersMock } from './mocks/models/Users.mock'; +import { AppServerOrchestratorMock } from './mocks/orchestrator.mock'; + +const { AppRoomsConverter } = proxyquire.noCallThru().load('../../../../../app/apps/server/converters/rooms', { + '@rocket.chat/random': { + Random: { + id: () => 1, + }, + }, + '@rocket.chat/models': { + Rooms: new RoomsMock(), + Messages: new MessagesMock(), + Users: new UsersMock(), + }, +}); + +describe('The AppMessagesConverter instance', () => { + let roomConverter: IAppRoomsConverter; + let roomsMock: RoomsMock; + + before(() => { + const orchestrator = new AppServerOrchestratorMock(); + + const usersConverter = orchestrator.getConverters().get('users'); + + usersConverter.convertById = function convertUserByIdStub(id: string) { + return UsersMock.convertedData[id as 'rocket.cat'] || undefined; + }; + + usersConverter.convertToApp = function convertUserToAppStub(user: UsersMock['data']['rocket.cat']) { + return { + id: user._id, + username: user.username, + name: user.name, + }; + }; + + orchestrator.getConverters().get('messages').convertById = async function convertRoomByIdStub(_id: string) { + return {}; + }; + + roomConverter = new AppRoomsConverter(orchestrator); + roomsMock = new RoomsMock(); + }); + + describe('when converting a room from Rocket.Chat to the Engine schema', () => { + it('should return `undefined` when `originalRoom` is falsy', async () => { + const appRoom = await roomConverter.convertRoom(undefined); + + expect(appRoom).to.be.undefined; + }); + + it('should return a proper schema', async () => { + const mockedRoom = roomsMock.findOneById('GENERAL') as RoomsMock['data']['GENERAL']; + const appRoom = await roomConverter.convertRoom(mockedRoom as unknown as IRoom); + + expect(appRoom).to.have.property('id', mockedRoom._id); + expect(appRoom).to.have.property('type', mockedRoom.t); + expect(appRoom).to.have.property('slugifiedName', mockedRoom.name); + expect(appRoom).to.have.property('createdAt').which.equalTime(mockedRoom.ts); + expect(appRoom).to.have.property('updatedAt').which.equalTime(mockedRoom._updatedAt); + expect(appRoom).to.have.property('messageCount', mockedRoom.msgs); + }); + + it('should not mutate the original room object', async () => { + const rocketchatRoomMock = structuredClone(roomsMock.findOneById('GENERAL')); + + await roomConverter.convertRoom(rocketchatRoomMock); + + expect(rocketchatRoomMock).to.deep.equal(roomsMock.findOneById('GENERAL')); + }); + + it('should add an `_unmappedProperties_` field to the converted room which contains the `lastMessage` property of the room', async () => { + const mockedRoom = roomsMock.findOneById('GENERAL') as RoomsMock['data']['GENERAL']; + const appMessage = await roomConverter.convertRoom(mockedRoom as unknown as IRoom); + + expect(appMessage).to.have.property('_unmappedProperties_').which.has.property('lastMessage').to.deep.equal(mockedRoom.lastMessage); + }); + }); + + describe('when converting a room from the Engine schema back to Rocket.Chat', () => { + it('should return `undefined` when `room` is falsy', async () => { + const rocketchatMessage = await roomConverter.convertAppRoom(undefined); + + expect(rocketchatMessage).to.be.undefined; + }); + + it('should return a proper schema', async () => { + const appRoom = RoomsMock.convertedData.GENERAL as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom); + + expect(rocketchatRoom).to.have.property('_id', appRoom.id); + expect(rocketchatRoom).to.have.property('ts', appRoom.createdAt); + expect(rocketchatRoom).to.have.property('lm', appRoom.lastModifiedAt); + expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); + expect(rocketchatRoom).to.have.property('t', appRoom.type); + expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); + }); + + it('should return a proper schema when receiving a partial object', async () => { + const appRoom = RoomsMock.convertedData.GENERALPartial as unknown as IAppsRoom; + const rocketchatRoom = await roomConverter.convertAppRoom(appRoom, true); + + expect(rocketchatRoom).to.have.property('_id', appRoom.id); + expect(rocketchatRoom).to.have.property('name', appRoom.slugifiedName); + expect(rocketchatRoom).to.have.property('sysMes', appRoom.displaySystemMessages); + expect(rocketchatRoom).to.have.property('_updatedAt', appRoom.updatedAt); + + expect(rocketchatRoom).to.not.have.property('msgs'); + expect(rocketchatRoom).to.not.have.property('ro'); + expect(rocketchatRoom).to.not.have.property('default'); + }); + }); +}); diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts index 98cd919f7b002..d4005e3ec52b3 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/MessageBuilder.ts @@ -20,6 +20,10 @@ export class MessageBuilder implements IMessageBuilder { private msg: IMessage; + private changes: Partial = {}; + private attachmentsChanged = false; + private customFieldsChanged = false; + constructor(message?: IMessage) { this.kind = RocketChatAssociationModel.MESSAGE; this.msg = message || ({} as IMessage); @@ -37,11 +41,14 @@ export class MessageBuilder implements IMessageBuilder { this.msg.editor = editor; this.msg.editedAt = new Date(); + this.changes = structuredClone(this.msg); + return this as IMessageBuilder; } public setThreadId(threadId: string): IMessageBuilder { this.msg.threadId = threadId; + this.changes.threadId = threadId; return this as IMessageBuilder; } @@ -52,6 +59,8 @@ export class MessageBuilder implements IMessageBuilder { public setRoom(room: IRoom): IMessageBuilder { this.msg.room = room; + this.changes.room = room; + return this as IMessageBuilder; } @@ -61,6 +70,8 @@ export class MessageBuilder implements IMessageBuilder { public setSender(sender: IUser): IMessageBuilder { this.msg.sender = sender; + this.changes.sender = sender; + return this as IMessageBuilder; } @@ -70,6 +81,8 @@ export class MessageBuilder implements IMessageBuilder { public setText(text: string): IMessageBuilder { this.msg.text = text; + this.changes.text = text; + return this as IMessageBuilder; } @@ -79,6 +92,8 @@ export class MessageBuilder implements IMessageBuilder { public setEmojiAvatar(emoji: string): IMessageBuilder { this.msg.emoji = emoji; + this.changes.emoji = emoji; + return this as IMessageBuilder; } @@ -88,6 +103,8 @@ export class MessageBuilder implements IMessageBuilder { public setAvatarUrl(avatarUrl: string): IMessageBuilder { this.msg.avatarUrl = avatarUrl; + this.changes.avatarUrl = avatarUrl; + return this as IMessageBuilder; } @@ -97,6 +114,8 @@ export class MessageBuilder implements IMessageBuilder { public setUsernameAlias(alias: string): IMessageBuilder { this.msg.alias = alias; + this.changes.alias = alias; + return this as IMessageBuilder; } @@ -110,11 +129,15 @@ export class MessageBuilder implements IMessageBuilder { } this.msg.attachments.push(attachment); + this.attachmentsChanged = true; + return this as IMessageBuilder; } public setAttachments(attachments: Array): IMessageBuilder { this.msg.attachments = attachments; + this.attachmentsChanged = true; + return this as IMessageBuilder; } @@ -123,34 +146,31 @@ export class MessageBuilder implements IMessageBuilder { } public replaceAttachment(position: number, attachment: IMessageAttachment): IMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { + if (!this.msg.attachments?.[position]) { throw new Error(`No attachment found at the index of "${position}" to replace.`); } this.msg.attachments[position] = attachment; + this.attachmentsChanged = true; + return this as IMessageBuilder; } public removeAttachment(position: number): IMessageBuilder { - if (!this.msg.attachments) { - this.msg.attachments = []; - } - - if (!this.msg.attachments[position]) { + if (!this.msg.attachments?.[position]) { throw new Error(`No attachment found at the index of "${position}" to remove.`); } this.msg.attachments.splice(position, 1); + this.attachmentsChanged = true; return this as IMessageBuilder; } public setEditor(user: IUser): IMessageBuilder { this.msg.editor = user; + this.changes.editor = user; + return this as IMessageBuilder; } @@ -160,6 +180,8 @@ export class MessageBuilder implements IMessageBuilder { public setGroupable(groupable: boolean): IMessageBuilder { this.msg.groupable = groupable; + this.changes.groupable = groupable; + return this as IMessageBuilder; } @@ -169,6 +191,8 @@ export class MessageBuilder implements IMessageBuilder { public setParseUrls(parseUrls: boolean): IMessageBuilder { this.msg.parseUrls = parseUrls; + this.changes.parseUrls = parseUrls; + return this as IMessageBuilder; } @@ -203,6 +227,7 @@ export class MessageBuilder implements IMessageBuilder { this.msg.blocks = blocks.getBlocks(); } else { this.msg.blocks = blocks; + this.changes.blocks = blocks; } return this as IMessageBuilder; @@ -227,6 +252,22 @@ export class MessageBuilder implements IMessageBuilder { this.msg.customFields[key] = value; + this.customFieldsChanged = true; + return this as IMessageBuilder; } + + public getChanges(): Partial { + const changes: typeof this.changes = structuredClone(this.changes); + + if (this.attachmentsChanged) { + changes.attachments = structuredClone(this.msg.attachments); + } + + if (this.customFieldsChanged) { + changes.customFields = structuredClone(this.msg.customFields); + } + + return changes; + } } diff --git a/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts b/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts index 38983475162d1..44119ab67ab41 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/builders/RoomBuilder.ts @@ -18,6 +18,9 @@ export class RoomBuilder implements IRoomBuilder { private members: Array; + private changes: Partial = {}; + private customFieldsChanged = false; + constructor(data?: Partial) { this.kind = RocketChatAssociationModel.ROOM; this.room = (data || { customFields: {} }) as IRoom; @@ -28,11 +31,15 @@ export class RoomBuilder implements IRoomBuilder { delete data.id; this.room = data as IRoom; + this.changes = structuredClone(this.room); + return this; } public setDisplayName(name: string): IRoomBuilder { this.room.displayName = name; + this.changes.displayName = name; + return this; } @@ -42,6 +49,8 @@ export class RoomBuilder implements IRoomBuilder { public setSlugifiedName(name: string): IRoomBuilder { this.room.slugifiedName = name; + this.changes.slugifiedName = name; + return this; } @@ -51,6 +60,8 @@ export class RoomBuilder implements IRoomBuilder { public setType(type: RoomType): IRoomBuilder { this.room.type = type; + this.changes.type = type; + return this; } @@ -60,6 +71,8 @@ export class RoomBuilder implements IRoomBuilder { public setCreator(creator: IUser): IRoomBuilder { this.room.creator = creator; + this.changes.creator = creator; + return this; } @@ -110,6 +123,8 @@ export class RoomBuilder implements IRoomBuilder { public setDefault(isDefault: boolean): IRoomBuilder { this.room.isDefault = isDefault; + this.changes.isDefault = isDefault; + return this; } @@ -119,6 +134,8 @@ export class RoomBuilder implements IRoomBuilder { public setReadOnly(isReadOnly: boolean): IRoomBuilder { this.room.isReadOnly = isReadOnly; + this.changes.isReadOnly = isReadOnly; + return this; } @@ -128,6 +145,8 @@ export class RoomBuilder implements IRoomBuilder { public setDisplayingOfSystemMessages(displaySystemMessages: boolean): IRoomBuilder { this.room.displaySystemMessages = displaySystemMessages; + this.changes.displaySystemMessages = displaySystemMessages; + return this; } @@ -141,11 +160,16 @@ export class RoomBuilder implements IRoomBuilder { } this.room.customFields[key] = value; + + this.customFieldsChanged = true; + return this; } public setCustomFields(fields: { [key: string]: object }): IRoomBuilder { this.room.customFields = fields; + this.customFieldsChanged = true; + return this; } @@ -160,4 +184,14 @@ export class RoomBuilder implements IRoomBuilder { public getRoom(): IRoom { return this.room; } + + public getChanges() { + const changes: Partial = structuredClone(this.changes); + + if (this.customFieldsChanged) { + changes.customFields = structuredClone(this.room.customFields); + } + + return changes; + } } diff --git a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts index 8befe7bfa983e..fbdeee609e9d2 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/modify/ModifyUpdater.ts @@ -70,13 +70,17 @@ export class ModifyUpdater implements IModifyUpdater { ) as IUserUpdater; } - public async message(messageId: string, _updater: IUser): Promise { + public async message(messageId: string, editor: IUser): Promise { const response = await this.senderFn({ method: 'bridges:getMessageBridge:doGetById', params: [messageId, AppObjectRegistry.get('id')], }); - return new MessageBuilder(response.result as IMessage); + const builder = new MessageBuilder(response.result as IMessage); + + builder.setEditor(editor); + + return builder; } public async room(roomId: string, _updater: IUser): Promise { @@ -91,15 +95,15 @@ export class ModifyUpdater implements IModifyUpdater { public finish(builder: IMessageBuilder | IRoomBuilder): Promise { switch (builder.kind) { case RocketChatAssociationModel.MESSAGE: - return this._finishMessage(builder as IMessageBuilder); + return this._finishMessage(builder as MessageBuilder); case RocketChatAssociationModel.ROOM: - return this._finishRoom(builder as IRoomBuilder); + return this._finishRoom(builder as RoomBuilder); default: throw new Error('Invalid builder passed to the ModifyUpdater.finish function.'); } } - private async _finishMessage(builder: IMessageBuilder): Promise { + private async _finishMessage(builder: MessageBuilder): Promise { const result = builder.getMessage(); if (!result.id) { @@ -114,40 +118,44 @@ export class ModifyUpdater implements IModifyUpdater { result.blocks = UIHelper.assignIds(result.blocks, AppObjectRegistry.get('id') || ''); } + const changes = { id: result.id, ...builder.getChanges() }; + await this.senderFn({ method: 'bridges:getMessageBridge:doUpdate', - params: [result, AppObjectRegistry.get('id')], + params: [changes, AppObjectRegistry.get('id')], }); } - private async _finishRoom(builder: IRoomBuilder): Promise { - const result = builder.getRoom(); + private async _finishRoom(builder: RoomBuilder): Promise { + const room = builder.getRoom(); - if (!result.id) { + if (!room.id) { throw new Error("Invalid room, can't update a room without an id."); } - if (!result.type) { + if (!room.type) { throw new Error('Invalid type assigned to the room.'); } - if (result.type !== RoomType.LIVE_CHAT) { - if (!result.creator || !result.creator.id) { + if (room.type !== RoomType.LIVE_CHAT) { + if (!room.creator || !room.creator.id) { throw new Error('Invalid creator assigned to the room.'); } - if (!result.slugifiedName || !result.slugifiedName.trim()) { + if (!room.slugifiedName || !room.slugifiedName.trim()) { throw new Error('Invalid slugifiedName assigned to the room.'); } } - if (!result.displayName || !result.displayName.trim()) { + if (!room.displayName || !room.displayName.trim()) { throw new Error('Invalid displayName assigned to the room.'); } + const changes = { id: room.id, ...builder.getChanges() }; + await this.senderFn({ method: 'bridges:getRoomBridge:doUpdate', - params: [result, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], + params: [changes, builder.getMembersToBeAddedUsernames(), AppObjectRegistry.get('id')], }); } } diff --git a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts index 313275c967cfa..1201179952172 100644 --- a/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts +++ b/packages/apps-engine/deno-runtime/lib/accessors/tests/ModifyUpdater.test.ts @@ -5,6 +5,7 @@ import { assertEquals } from 'https://deno.land/std@0.203.0/assert/mod.ts'; import { AppObjectRegistry } from '../../../AppObjectRegistry.ts'; import { ModifyUpdater } from '../modify/ModifyUpdater.ts'; +import { RoomBuilder } from "../builders/RoomBuilder.ts"; describe('ModifyUpdater', () => { let modifyUpdater: ModifyUpdater; @@ -61,7 +62,7 @@ describe('ModifyUpdater', () => { args: [ { method: 'bridges:getMessageBridge:doUpdate', - params: [messageBuilder.getMessage(), 'deno-test'], + params: [{ id: '123', ...messageBuilder.getChanges() }, 'deno-test'], }, ], }); @@ -72,7 +73,7 @@ describe('ModifyUpdater', () => { it('correctly formats requests for the update room flow', async () => { const _spy = spy(modifyUpdater, 'senderFn' as keyof ModifyUpdater); - const roomBuilder = await modifyUpdater.room('123', { id: '456' } as any); + const roomBuilder = await modifyUpdater.room('123', { id: '456' } as any) as RoomBuilder; assertSpyCall(_spy, 0, { args: [ @@ -102,7 +103,7 @@ describe('ModifyUpdater', () => { args: [ { method: 'bridges:getRoomBridge:doUpdate', - params: [roomBuilder.getRoom(), roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], + params: [{ id: '123', ...roomBuilder.getChanges() }, roomBuilder.getMembersToBeAddedUsernames(), 'deno-test'], }, ], }); diff --git a/packages/apps/src/converters/IAppMessagesConverter.ts b/packages/apps/src/converters/IAppMessagesConverter.ts index 863c10c954777..8c10114304d09 100644 --- a/packages/apps/src/converters/IAppMessagesConverter.ts +++ b/packages/apps/src/converters/IAppMessagesConverter.ts @@ -9,6 +9,7 @@ export interface IAppMessagesConverter { convertMessage(message: IMessage | undefined | null): Promise; convertAppMessage(message: undefined | null): Promise; convertAppMessage(message: IAppsMessage): Promise; + convertAppMessage(message: IAppsMessage, isPartial: boolean): Promise>; convertAppMessage(message: IAppsMessage | undefined | null): Promise; convertMessageRaw(message: IMessage): Promise; convertMessageRaw(message: IMessage | undefined | null): Promise; diff --git a/packages/apps/src/converters/IAppRoomsConverter.ts b/packages/apps/src/converters/IAppRoomsConverter.ts index 9408b3f9b63ca..83b12ae4503ca 100644 --- a/packages/apps/src/converters/IAppRoomsConverter.ts +++ b/packages/apps/src/converters/IAppRoomsConverter.ts @@ -10,5 +10,6 @@ export interface IAppRoomsConverter { convertRoom(room: IRoom | undefined | null): Promise; convertAppRoom(room: undefined | null): Promise; convertAppRoom(room: IAppsRoom): Promise; - convertAppRoom(room: IAppsRoom | undefined | null): Promise; + convertAppRoom(room: IAppsRoom, isPartial: boolean): Promise>; + convertAppRoom(room: IAppsRoom | undefined | null, isPartial?: boolean): Promise | undefined>; }