diff --git a/src/definition/accessors/IMessageRead.ts b/src/definition/accessors/IMessageRead.ts index 10c99d263..4502689d2 100644 --- a/src/definition/accessors/IMessageRead.ts +++ b/src/definition/accessors/IMessageRead.ts @@ -1,4 +1,5 @@ -import type { IMessage } from '../messages/index'; +import type { GetMessagesOptions } from '../../server/bridges/RoomBridge'; +import type { IMessage, IMessageRaw } from '../messages/index'; import type { IRoom } from '../rooms/IRoom'; import type { IUser } from '../users/IUser'; @@ -12,4 +13,17 @@ export interface IMessageRead { getSenderUser(messageId: string): Promise; getRoom(messageId: string): Promise; + + /** + * Retrieves an array of unread messages for a specific user in a specific room. + * + * @param roomId The unique identifier of the room from which to retrieve unread messages. + * @param uid The unique identifier of the user for whom to retrieve unread messages. + * @param options Optional parameters for retrieving messages: + * - limit: The maximum number of messages to retrieve. If more than 100 is passed, it defaults to 100. + * - skip: The number of messages to skip (for pagination). + * - sort: An object defining the sorting order of the messages. Each key is a field to sort by, and the value is either 1 for ascending order or -1 for descending order. + * @returns A Promise that resolves to an array of IMessage objects representing the unread messages for the specified user in the specified room. + */ + getUnreadByRoomAndUser(roomId: string, uid: string, options?: Partial): Promise; } diff --git a/src/definition/accessors/IUserRead.ts b/src/definition/accessors/IUserRead.ts index 33c4c6e45..c99c65923 100644 --- a/src/definition/accessors/IUserRead.ts +++ b/src/definition/accessors/IUserRead.ts @@ -19,4 +19,11 @@ export interface IUserRead { * @param uid user's id */ getUserUnreadMessageCount(uid: string): Promise; + + /** + * Gets the user's unread messages count in a room. + * @param uid user's id + * @param roomId room's id + */ + getUserUnreadMessageCountByRoom(uid: string, roomId: string): Promise; } diff --git a/src/server/accessors/MessageRead.ts b/src/server/accessors/MessageRead.ts index fc33eee13..b437ce187 100644 --- a/src/server/accessors/MessageRead.ts +++ b/src/server/accessors/MessageRead.ts @@ -1,8 +1,9 @@ import type { MessageBridge } from '../bridges/MessageBridge'; import type { IMessageRead } from '../../definition/accessors'; -import type { IMessage } from '../../definition/messages'; +import type { IMessage, IMessageRaw } from '../../definition/messages'; import type { IRoom } from '../../definition/rooms'; import type { IUser } from '../../definition/users'; +import { GetMessagesSortableFields, type GetMessagesOptions } from '../bridges/RoomBridge'; export class MessageRead implements IMessageRead { constructor(private messageBridge: MessageBridge, private appId: string) {} @@ -30,4 +31,34 @@ export class MessageRead implements IMessageRead { return msg.room; } + + public async getUnreadByRoomAndUser(roomId: string, uid: string, options: Partial = {}): Promise { + const { limit = 100, sort = { createdAt: 'asc' }, skip = 0 } = options; + + if (typeof roomId !== 'string' || roomId.trim().length === 0) { + throw new Error('Invalid roomId: must be a non-empty string'); + } + + if (!Number.isFinite(limit) || limit <= 0 || limit > 100) { + throw new Error(`Invalid limit provided. Expected number between 1 and 100, got ${limit}`); + } + + this.validateSort(sort); + + const completeOptions: GetMessagesOptions = { limit, sort, skip }; + + return this.messageBridge.doGetUnreadByRoomAndUser(roomId, uid, completeOptions, this.appId); + } + + private validateSort(sort: Record) { + Object.entries(sort).forEach(([key, value]) => { + if (!GetMessagesSortableFields.includes(key as typeof GetMessagesSortableFields[number])) { + throw new Error(`Invalid key "${key}" used in sort. Available keys for sorting are ${GetMessagesSortableFields.join(', ')}`); + } + + if (value !== 'asc' && value !== 'desc') { + throw new Error(`Invalid sort direction for field "${key}". Expected "asc" or "desc", got ${value}`); + } + }); + } } diff --git a/src/server/accessors/UserRead.ts b/src/server/accessors/UserRead.ts index 4eeb38cc7..5112046ea 100644 --- a/src/server/accessors/UserRead.ts +++ b/src/server/accessors/UserRead.ts @@ -20,4 +20,8 @@ export class UserRead implements IUserRead { public getUserUnreadMessageCount(uid: string): Promise { return this.userBridge.doGetUserUnreadMessageCount(uid, this.appId); } + + public getUserUnreadMessageCountByRoom(uid: string, rid: string): Promise { + return this.userBridge.doGetUserUnreadMessageCountByRoom(uid, rid, this.appId); + } } diff --git a/src/server/bridges/MessageBridge.ts b/src/server/bridges/MessageBridge.ts index d37fb13cc..19ae426ca 100644 --- a/src/server/bridges/MessageBridge.ts +++ b/src/server/bridges/MessageBridge.ts @@ -1,11 +1,12 @@ import type { ITypingOptions } from '../../definition/accessors/INotifier'; -import type { IMessage, Reaction } from '../../definition/messages'; +import type { IMessage, IMessageRaw, Reaction } from '../../definition/messages'; import type { IRoom } from '../../definition/rooms'; import type { IUser } from '../../definition/users'; import { PermissionDeniedError } from '../errors/PermissionDeniedError'; import { AppPermissionManager } from '../managers/AppPermissionManager'; import { AppPermissions } from '../permissions/AppPermissions'; import { BaseBridge } from './BaseBridge'; +import type { GetMessagesOptions } from './RoomBridge'; export interface ITypingDescriptor extends ITypingOptions { isTyping: boolean; @@ -54,6 +55,12 @@ export abstract class MessageBridge extends BaseBridge { } } + public async doGetUnreadByRoomAndUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getUnreadByRoomAndUser(roomId, uid, options, appId); + } + } + public async doAddReaction(messageId: string, userId: string, reaction: Reaction, appId: string): Promise { if (this.hasWritePermission(appId)) { return this.addReaction(messageId, userId, reaction); @@ -80,6 +87,8 @@ export abstract class MessageBridge extends BaseBridge { protected abstract delete(message: IMessage, user: IUser, appId: string): Promise; + protected abstract getUnreadByRoomAndUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise; + protected abstract addReaction(messageId: string, userId: string, reaction: Reaction): Promise; protected abstract removeReaction(messageId: string, userId: string, reaction: Reaction): Promise; diff --git a/src/server/bridges/UserBridge.ts b/src/server/bridges/UserBridge.ts index 232f66507..59f4e12d4 100644 --- a/src/server/bridges/UserBridge.ts +++ b/src/server/bridges/UserBridge.ts @@ -41,7 +41,13 @@ export abstract class UserBridge extends BaseBridge { public async doGetUserUnreadMessageCount(uid: string, appId: string): Promise { if (this.hasReadPermission(appId)) { - return this.getUserUnreadMessageCount(uid); + return this.getUserUnreadMessageCount(uid, appId); + } + } + + public async doGetUserUnreadMessageCountByRoom(uid: string, roomId: string, appId: string): Promise { + if (this.hasReadPermission(appId)) { + return this.getUserUnreadMessageCountByRoom(uid, roomId, appId); } } @@ -65,7 +71,9 @@ export abstract class UserBridge extends BaseBridge { protected abstract getActiveUserCount(): Promise; - protected abstract getUserUnreadMessageCount(uid: string): Promise; + protected abstract getUserUnreadMessageCount(uid: string, appId: string): Promise; + + protected abstract getUserUnreadMessageCountByRoom(uid: string, roomId: string, appId: string): Promise; /** * Creates a user. diff --git a/tests/server/accessors/MessageRead.spec.ts b/tests/server/accessors/MessageRead.spec.ts index 144926c71..aeac24a02 100644 --- a/tests/server/accessors/MessageRead.spec.ts +++ b/tests/server/accessors/MessageRead.spec.ts @@ -1,6 +1,6 @@ import { AsyncTest, Expect, SetupFixture } from 'alsatian'; -import type { IMessage } from '../../../src/definition/messages'; +import type { IMessage, IMessageRaw } from '../../../src/definition/messages'; import { MessageRead } from '../../../src/server/accessors'; import type { MessageBridge } from '../../../src/server/bridges'; import { TestData } from '../../test-data/utilities'; @@ -8,6 +8,12 @@ import { TestData } from '../../test-data/utilities'; export class MessageReadAccessorTestFixture { private msg: IMessage; + private unreadMsgs: IMessageRaw[]; + + private unreadRoomId: string; + + private unreadUserId: string; + private mockMsgBridgeWithMsg: MessageBridge; private mockMsgBridgeNoMsg: MessageBridge; @@ -15,18 +21,33 @@ export class MessageReadAccessorTestFixture { @SetupFixture public setupFixture() { this.msg = TestData.getMessage(); + this.unreadMsgs = ['507f1f77bcf86cd799439011', '507f191e810c19729de860ea'].map((id) => TestData.getMessageRaw(id)); + this.unreadRoomId = this.unreadMsgs[0].roomId; + this.unreadUserId = this.unreadMsgs[0].sender._id; const theMsg = this.msg; + const theUnreadMsg = this.unreadMsgs; + const { unreadRoomId } = this; + const { unreadUserId } = this; this.mockMsgBridgeWithMsg = { doGetById(id, appId): Promise { return Promise.resolve(theMsg); }, + doGetUnreadByRoomAndUser(roomId, uid, options, appId): Promise { + if (roomId === unreadRoomId && uid === unreadUserId) { + return Promise.resolve(theUnreadMsg); + } + return Promise.resolve([]); + }, } as MessageBridge; this.mockMsgBridgeNoMsg = { doGetById(id, appId): Promise { return Promise.resolve(undefined); }, + doGetUnreadByRoomAndUser(roomId, uid, options, appId): Promise { + return Promise.resolve(undefined); + }, } as MessageBridge; } @@ -44,6 +65,12 @@ export class MessageReadAccessorTestFixture { Expect(await mr.getRoom('fake')).toBeDefined(); Expect(await mr.getRoom('fake')).toEqual(this.msg.room); + + Expect(await mr.getUnreadByRoomAndUser(this.unreadRoomId, this.unreadUserId)).toBeDefined(); + Expect(await mr.getUnreadByRoomAndUser(this.unreadRoomId, this.unreadUserId)).toEqual(this.unreadMsgs); + + Expect(await mr.getUnreadByRoomAndUser('fake', 'fake')).toBeDefined(); + Expect(await mr.getUnreadByRoomAndUser('fake', 'fake')).toEqual([]); } @AsyncTest() @@ -54,5 +81,6 @@ export class MessageReadAccessorTestFixture { Expect(await nomr.getById('fake')).not.toBeDefined(); Expect(await nomr.getSenderUser('fake')).not.toBeDefined(); Expect(await nomr.getRoom('fake')).not.toBeDefined(); + Expect(await nomr.getUnreadByRoomAndUser('fake', 'fake')).not.toBeDefined(); } } diff --git a/tests/test-data/bridges/messageBridge.ts b/tests/test-data/bridges/messageBridge.ts index 9f21b37e6..e5929bac6 100644 --- a/tests/test-data/bridges/messageBridge.ts +++ b/tests/test-data/bridges/messageBridge.ts @@ -1,8 +1,9 @@ -import type { IMessage, Reaction } from '../../../src/definition/messages'; +import type { IMessage, IMessageRaw, Reaction } from '../../../src/definition/messages'; import type { IRoom } from '../../../src/definition/rooms'; import type { IUser } from '../../../src/definition/users'; import { MessageBridge } from '../../../src/server/bridges'; import type { ITypingDescriptor } from '../../../src/server/bridges/MessageBridge'; +import type { GetMessagesOptions } from '../../../src/server/bridges/RoomBridge'; export class TestsMessageBridge extends MessageBridge { public create(message: IMessage, appId: string): Promise { @@ -13,6 +14,10 @@ export class TestsMessageBridge extends MessageBridge { throw new Error('Method not implemented.'); } + public getUnreadByRoomAndUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise { + throw new Error('Method not implemented.'); + } + public update(message: IMessage, appId: string): Promise { throw new Error('Method not implemented.'); } diff --git a/tests/test-data/bridges/userBridge.ts b/tests/test-data/bridges/userBridge.ts index c9399f5b6..a8cbca41d 100644 --- a/tests/test-data/bridges/userBridge.ts +++ b/tests/test-data/bridges/userBridge.ts @@ -34,7 +34,11 @@ export class TestsUserBridge extends UserBridge { throw new Error('Method not implemented'); } - protected getUserUnreadMessageCount(uid: string): Promise { + protected getUserUnreadMessageCount(uid: string, appId: string): Promise { + throw new Error('Method not implemented.'); + } + + protected getUserUnreadMessageCountByRoom(uid: string, roomId: string, appId: string): Promise { throw new Error('Method not implemented.'); }