Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Apps Engine Bridge Method for Unread Room Messages #754

Open
wants to merge 12 commits into
base: alpha
Choose a base branch
from
16 changes: 15 additions & 1 deletion src/definition/accessors/IMessageRead.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -12,4 +13,17 @@ export interface IMessageRead {
getSenderUser(messageId: string): Promise<IUser | undefined>;

getRoom(messageId: string): Promise<IRoom | undefined>;

/**
* 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<GetMessagesOptions>): Promise<IMessageRaw[]>;
}
7 changes: 7 additions & 0 deletions src/definition/accessors/IUserRead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,11 @@ export interface IUserRead {
* @param uid user's id
*/
getUserUnreadMessageCount(uid: string): Promise<number | undefined>;

/**
* 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<number | undefined>;
}
33 changes: 32 additions & 1 deletion src/server/accessors/MessageRead.ts
Original file line number Diff line number Diff line change
@@ -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) {}
Expand Down Expand Up @@ -30,4 +31,34 @@ export class MessageRead implements IMessageRead {

return msg.room;
}

public async getUnreadByRoomAndUser(roomId: string, uid: string, options: Partial<GetMessagesOptions> = {}): Promise<IMessageRaw[]> {
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<string, unknown>) {
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}`);
}
});
}
}
4 changes: 4 additions & 0 deletions src/server/accessors/UserRead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,8 @@ export class UserRead implements IUserRead {
public getUserUnreadMessageCount(uid: string): Promise<number> {
return this.userBridge.doGetUserUnreadMessageCount(uid, this.appId);
}

public getUserUnreadMessageCountByRoom(uid: string, rid: string): Promise<number> {
return this.userBridge.doGetUserUnreadMessageCountByRoom(uid, rid, this.appId);
}
}
11 changes: 10 additions & 1 deletion src/server/bridges/MessageBridge.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -54,6 +55,12 @@ export abstract class MessageBridge extends BaseBridge {
}
}

public async doGetUnreadByRoomAndUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise<IMessageRaw[]> {
if (this.hasReadPermission(appId)) {
return this.getUnreadByRoomAndUser(roomId, uid, options, appId);
}
}

public async doAddReaction(messageId: string, userId: string, reaction: Reaction, appId: string): Promise<void> {
if (this.hasWritePermission(appId)) {
return this.addReaction(messageId, userId, reaction);
Expand All @@ -80,6 +87,8 @@ export abstract class MessageBridge extends BaseBridge {

protected abstract delete(message: IMessage, user: IUser, appId: string): Promise<void>;

protected abstract getUnreadByRoomAndUser(roomId: string, uid: string, options: GetMessagesOptions, appId: string): Promise<IMessageRaw[]>;

protected abstract addReaction(messageId: string, userId: string, reaction: Reaction): Promise<void>;

protected abstract removeReaction(messageId: string, userId: string, reaction: Reaction): Promise<void>;
Expand Down
12 changes: 10 additions & 2 deletions src/server/bridges/UserBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,13 @@ export abstract class UserBridge extends BaseBridge {

public async doGetUserUnreadMessageCount(uid: string, appId: string): Promise<number> {
if (this.hasReadPermission(appId)) {
return this.getUserUnreadMessageCount(uid);
return this.getUserUnreadMessageCount(uid, appId);
}
}

public async doGetUserUnreadMessageCountByRoom(uid: string, roomId: string, appId: string): Promise<number> {
if (this.hasReadPermission(appId)) {
return this.getUserUnreadMessageCountByRoom(uid, roomId, appId);
}
}

Expand All @@ -65,7 +71,9 @@ export abstract class UserBridge extends BaseBridge {

protected abstract getActiveUserCount(): Promise<number>;

protected abstract getUserUnreadMessageCount(uid: string): Promise<number>;
protected abstract getUserUnreadMessageCount(uid: string, appId: string): Promise<number>;

protected abstract getUserUnreadMessageCountByRoom(uid: string, roomId: string, appId: string): Promise<number>;

/**
* Creates a user.
Expand Down
30 changes: 29 additions & 1 deletion tests/server/accessors/MessageRead.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
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';

export class MessageReadAccessorTestFixture {
private msg: IMessage;

private unreadMsgs: IMessageRaw[];

private unreadRoomId: string;

private unreadUserId: string;

private mockMsgBridgeWithMsg: MessageBridge;

private mockMsgBridgeNoMsg: MessageBridge;

@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<IMessage> {
return Promise.resolve(theMsg);
},
doGetUnreadByRoomAndUser(roomId, uid, options, appId): Promise<IMessageRaw[]> {
if (roomId === unreadRoomId && uid === unreadUserId) {
return Promise.resolve(theUnreadMsg);
}
return Promise.resolve([]);
},
} as MessageBridge;

this.mockMsgBridgeNoMsg = {
doGetById(id, appId): Promise<IMessage> {
return Promise.resolve(undefined);
},
doGetUnreadByRoomAndUser(roomId, uid, options, appId): Promise<IMessageRaw[]> {
return Promise.resolve(undefined);
},
} as MessageBridge;
}

Expand All @@ -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()
Expand All @@ -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();
}
}
7 changes: 6 additions & 1 deletion tests/test-data/bridges/messageBridge.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
Expand All @@ -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<IMessageRaw[]> {
throw new Error('Method not implemented.');
}

public update(message: IMessage, appId: string): Promise<void> {
throw new Error('Method not implemented.');
}
Expand Down
6 changes: 5 additions & 1 deletion tests/test-data/bridges/userBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export class TestsUserBridge extends UserBridge {
throw new Error('Method not implemented');
}

protected getUserUnreadMessageCount(uid: string): Promise<number> {
protected getUserUnreadMessageCount(uid: string, appId: string): Promise<number> {
throw new Error('Method not implemented.');
}

protected getUserUnreadMessageCountByRoom(uid: string, roomId: string, appId: string): Promise<number> {
throw new Error('Method not implemented.');
}

Expand Down
Loading