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: functionality to view unread messages in chat #2611

Open
wants to merge 118 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
a8c02f8
Updated subscriptions and added mutations for directChat and groupChat
disha1202 Jul 15, 2024
fe6907d
Update index.ts
disha1202 Jul 16, 2024
3a9799d
Update messageSentToDirectChat.ts
disha1202 Jul 16, 2024
2fa65b1
Update createDirectChat.spec.ts
disha1202 Jul 16, 2024
f68bb01
Update createDirectChat.spec.ts
disha1202 Jul 16, 2024
f7089b9
fix: linting errors
disha1202 Jul 16, 2024
2fe5353
fix: lint errors
disha1202 Jul 16, 2024
7e31c14
fix: formatting issues
disha1202 Jul 16, 2024
0bf28a2
Merge branch 'develop' into chat-feature
disha1202 Jul 16, 2024
9c44ea4
fix: formatting issues
disha1202 Jul 16, 2024
fa5f6ec
fix: test cases
disha1202 Jul 16, 2024
3db8381
fix test cases
disha1202 Jul 16, 2024
a1379bf
Update messageSentToDirectChat.ts
disha1202 Jul 18, 2024
bd98a6b
Update messageSentToDirectChat.ts
disha1202 Jul 18, 2024
7ec1773
added test cases for query directChatById
Jul 18, 2024
3781ede
Update directChatById.spec.ts
disha1202 Jul 18, 2024
8aaf5f3
Update directChatById.spec.ts
disha1202 Jul 18, 2024
e83657d
Update directChatById.spec.ts
disha1202 Jul 18, 2024
23a9676
Added test cases
disha1202 Jul 18, 2024
9576d4a
fix: test cases
disha1202 Jul 18, 2024
c2af22d
fix: tests
disha1202 Jul 18, 2024
ad48619
fix test cases
disha1202 Jul 18, 2024
77c7fde
Merge branch 'chat-feature' of https://github.com/disha1202/talawa-ap…
disha1202 Jul 18, 2024
d18843a
added support to reply to direct chat and group chat
disha1202 Jul 24, 2024
cf27ca6
Merge branch 'develop' of https://github.com/disha1202/talawa-api int…
disha1202 Jul 24, 2024
8469653
fix: test cases
disha1202 Jul 24, 2024
87ad70f
updated schema for chat
disha1202 Jul 28, 2024
f936adc
removed console logs
disha1202 Jul 28, 2024
babf1c0
Merge branch 'develop' into reply-functionality
disha1202 Jul 28, 2024
c84f33a
Merge branch 'develop' into reply-functionality
disha1202 Jul 30, 2024
33535ac
Added tests for chat
disha1202 Aug 8, 2024
98d06a7
fix format
disha1202 Aug 8, 2024
5a61b43
fix test cases
disha1202 Aug 8, 2024
a84f1c6
fix tests
disha1202 Aug 8, 2024
4d65b03
fix tests
disha1202 Aug 8, 2024
c23008b
fix: test cases
disha1202 Aug 9, 2024
a539e3e
fix: test cases
disha1202 Aug 9, 2024
074b79c
fix: test cases
disha1202 Aug 10, 2024
9926e63
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
disha1202 Aug 10, 2024
1586732
added test cases
disha1202 Aug 15, 2024
8b873ea
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Aug 15, 2024
6cb5dcc
feat: added unread msgs feature
disha1202 Aug 17, 2024
15d6bed
removed unwanted code
disha1202 Aug 18, 2024
46d4f5f
Removed unwanted code
disha1202 Aug 18, 2024
ce20fe0
Merge branch 'develop' into schema-update
disha1202 Aug 18, 2024
dcd6006
fix: lint errors
disha1202 Aug 18, 2024
ea23279
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Aug 18, 2024
ef2d45d
Merge branch 'develop' into reply-functionality
disha1202 Aug 18, 2024
dca6cf3
sort chats by updatedAt field
disha1202 Aug 18, 2024
d33aada
fix: formatting issues
disha1202 Aug 18, 2024
56cd761
fix: chat updation
disha1202 Aug 18, 2024
a2ccfb0
Removed unwanted code
disha1202 Aug 18, 2024
52165f1
Merge branch 'reply-functionality' of https://github.com/disha1202/ta…
disha1202 Aug 18, 2024
ff1987f
Merge branch 'develop' into schema-update
disha1202 Aug 21, 2024
369a81d
Merge branch 'develop' into reply-functionality
disha1202 Aug 21, 2024
7cd8040
Merge branch 'develop' into schema-update
disha1202 Aug 21, 2024
979bf37
Merge branch 'develop' into reply-functionality
disha1202 Aug 21, 2024
f29ac65
Merge branch 'develop' into schema-update
disha1202 Aug 23, 2024
dd9120f
Merge branch 'develop' into reply-functionality
disha1202 Aug 23, 2024
a440bad
fix: chat documentation
disha1202 Aug 23, 2024
615a5ac
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Aug 23, 2024
5ad1d76
Merge branch 'develop' into schema-update
disha1202 Aug 24, 2024
08a6fed
Merge branch 'develop' into reply-functionality
disha1202 Aug 24, 2024
c2d831a
added test cases
disha1202 Aug 24, 2024
bac5283
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Aug 24, 2024
a2e5141
fix: formatting issues
disha1202 Aug 24, 2024
39d9086
fix: failed test cases
disha1202 Aug 24, 2024
b6fa93e
added test cases
disha1202 Aug 24, 2024
0e0b1d3
Merge branch 'reply-functionality' of https://github.com/disha1202/ta…
disha1202 Aug 24, 2024
4f9ea48
removed unwanted comments
disha1202 Aug 24, 2024
69ee5c0
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
disha1202 Aug 24, 2024
cb9bd96
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Aug 24, 2024
d450059
Merge branch 'develop' into schema-update
disha1202 Sep 1, 2024
7baed1d
Merge branch 'develop' into reply-functionality
disha1202 Sep 1, 2024
aad9ea2
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
disha1202 Sep 22, 2024
b5da053
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Sep 22, 2024
50d579e
fix: failling tests
disha1202 Sep 22, 2024
99a24c2
removed unwanted code
disha1202 Sep 22, 2024
23ba0b5
Update tests/resolvers/Query/chatById.spec.ts
disha1202 Sep 22, 2024
c4f6cd6
fix: removed ignore statements
disha1202 Sep 22, 2024
3b0f12c
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Sep 22, 2024
9f2f12d
Update schema.graphql
disha1202 Sep 22, 2024
b3032db
Merge branch 'develop' into reply-functionality
disha1202 Sep 22, 2024
a6b960f
fix: failing tests
disha1202 Sep 22, 2024
8548f25
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Sep 22, 2024
bf38bc6
fix: removed eslint disabled statements
disha1202 Sep 22, 2024
8f42668
updated test cases and helper functions
disha1202 Sep 29, 2024
d42aca6
updated tests and helper functions
disha1202 Sep 29, 2024
4124a3a
Merge branch 'develop' into schema-update
disha1202 Oct 6, 2024
7d5667d
reverted: unrelated changes
disha1202 Oct 6, 2024
9911641
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Oct 6, 2024
3b4e431
reverted: unrelated changes
disha1202 Oct 6, 2024
d0b54f6
Merge branch 'develop' into reply-functionality
disha1202 Oct 6, 2024
670f79e
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
disha1202 Oct 7, 2024
b66d394
Merge branch 'reply-functionality' of https://github.com/disha1202/ta…
disha1202 Oct 7, 2024
03a26d8
fix: linting errors'
disha1202 Oct 7, 2024
87eea3d
fix: failing tests
disha1202 Oct 8, 2024
de1dea1
fix: failing tests
disha1202 Oct 8, 2024
4050877
Merge branch 'develop' into schema-update
disha1202 Oct 13, 2024
badec95
Merge branch 'reply-functionality' of https://github.com/disha1202/ta…
disha1202 Oct 13, 2024
6a4ea4b
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Oct 13, 2024
b625e84
fix: eslint errors
disha1202 Oct 13, 2024
2d49e0a
fix: eslint errors
disha1202 Oct 13, 2024
7d96ccc
fix: eslint errors
disha1202 Oct 13, 2024
7054695
refator: removed parser for graphql files
disha1202 Oct 13, 2024
de2fade
Merge branch 'develop' into schema-update
disha1202 Oct 16, 2024
3f75514
Update eslint.config.mjs
disha1202 Oct 16, 2024
c7fc6e3
fix: formatting issues
disha1202 Oct 16, 2024
8fdf40b
Merge branch 'schema-update' of https://github.com/disha1202/talawa-a…
disha1202 Oct 19, 2024
96189f7
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
disha1202 Oct 19, 2024
70b59fb
fix: failing test cases
disha1202 Oct 19, 2024
0154a9c
fix: failing tests
disha1202 Oct 19, 2024
48ee919
Merge branch 'develop' of https://github.com/PalisadoesFoundation/tal…
disha1202 Oct 22, 2024
497d4de
Merge branch 'develop' into unread-msgs
disha1202 Oct 25, 2024
6ef15db
Merge branch 'develop' into unread-msgs
disha1202 Oct 28, 2024
df50b95
Update src/resolvers/Mutation/markChatMessagesAsRead.ts
disha1202 Oct 29, 2024
e638d96
Update src/resolvers/Mutation/markChatMessagesAsRead.ts
disha1202 Oct 29, 2024
40df8f8
Merge branch 'develop' into unread-msgs
disha1202 Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ type Chat {
messages: [ChatMessage]
name: String
organization: Organization
unseenMessagesByUsers: JSON
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using a more type-safe structure for unseenMessagesByUsers.

The JSON type provides flexibility but lacks type safety. Consider creating a dedicated type to represent unseen messages data.

-  unseenMessagesByUsers: JSON
+  unseenMessagesByUsers: UnseenMessages

Add these type definitions:

type UnseenMessages {
  """Map of user IDs to their last seen message timestamp"""
  userToLastSeen: [UserLastSeen!]
}

type UserLastSeen {
  userId: ID!
  lastSeenAt: DateTime!
}

updatedAt: DateTime!
users: [User!]!
}
Expand Down Expand Up @@ -1114,6 +1115,7 @@ type Mutation {
likePost(id: ID!): Post
login(data: LoginInput!): AuthData!
logout: Boolean!
markChatMessagesAsRead(chatId: ID!, userId: ID!): Chat
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling to markChatMessagesAsRead mutation.

The mutation should handle cases like invalid chat/user IDs and unauthorized access.

-  markChatMessagesAsRead(chatId: ID!, userId: ID!): Chat
+  markChatMessagesAsRead(chatId: ID!, userId: ID!): MarkChatMessagesAsReadPayload!

Add these type definitions:

type MarkChatMessagesAsReadPayload {
  chat: Chat
  userErrors: [MarkChatMessagesAsReadError!]!
}

union MarkChatMessagesAsReadError = ChatNotFoundError | UserNotFoundError | UserNotAuthorizedError

type ChatNotFoundError implements Error {
  message: String!
}

otp(data: OTPInput!): OtpData!
recaptcha(data: RecaptchaVerification!): Boolean!
refreshToken(refreshToken: String!): ExtendSession!
Expand Down
7 changes: 6 additions & 1 deletion src/models/Chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface InterfaceChat {
createdAt: Date;
updatedAt: Date;
lastMessageId: string;
unseenMessagesByUsers: JSON;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using a stricter type instead of JSON.

Using the JSON type reduces type safety. Consider creating a specific TypeScript interface to define the structure of unseenMessagesByUsers.

interface UnseenMessages {
  [userId: string]: number;  // Maps user IDs to their unseen message count
}

// Then use in interface:
unseenMessagesByUsers: UnseenMessages;

Also, please add JSDoc documentation for this new field in the interface.

}

/**
Expand Down Expand Up @@ -98,9 +99,13 @@ const chatSchema = new Schema(
type: String,
required: false,
},
unseenMessagesByUsers: {
type: JSON,
required: true,
},
},
{
timestamps: true,
timestamps: false,
},
);

Expand Down
13 changes: 9 additions & 4 deletions src/resolvers/Mutation/createChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@
}
}

// const userExists = (await User.exists({
// _id: { $in: args.data.userIds },
// })) as unknown as string[];

const userExists = await User.find({
_id: { $in: args.data.userIds },
}).lean();
Expand All @@ -54,6 +50,13 @@

const now = new Date();

const unseenMessagesByUsers = JSON.stringify(
args.data.userIds.reduce((unseenMessages: Record<string, number>, user) => {
unseenMessages[user] = 0;
return unseenMessages;
}, {}),
);
Comment on lines +53 to +58
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type safety and validation for unseenMessagesByUsers.

The current implementation could benefit from:

  1. Type safety for the Record structure
  2. Validation for user ID format
  3. Error handling for JSON.stringify
+interface UnseenMessages {
+  [userId: string]: number;
+}
+
 const unseenMessagesByUsers = JSON.stringify(
-  args.data.userIds.reduce((unseenMessages: Record<string, number>, user) => {
+  args.data.userIds.reduce((unseenMessages: UnseenMessages, user) => {
+    if (!/^[0-9a-fA-F]{24}$/.test(user)) {
+      throw new errors.ValidationError(
+        'Invalid user ID format',
+        'INVALID_USER_ID',
+        'userIds'
+      );
+    }
     unseenMessages[user] = 0;
     return unseenMessages;
   }, {}),
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const unseenMessagesByUsers = JSON.stringify(
args.data.userIds.reduce((unseenMessages: Record<string, number>, user) => {
unseenMessages[user] = 0;
return unseenMessages;
}, {}),
);
interface UnseenMessages {
[userId: string]: number;
}
const unseenMessagesByUsers = JSON.stringify(
args.data.userIds.reduce((unseenMessages: UnseenMessages, user) => {
if (!/^[0-9a-fA-F]{24}$/.test(user)) {
throw new errors.ValidationError(
'Invalid user ID format',
'INVALID_USER_ID',
'userIds'
);
}
unseenMessages[user] = 0;
return unseenMessages;
}, {}),
);


const chatPayload = args.data.isGroup
? {
isGroup: args.data.isGroup,
Expand All @@ -65,13 +68,15 @@
createdAt: now,
updatedAt: now,
image: args.data.image,
unseenMessagesByUsers,

Check warning on line 71 in src/resolvers/Mutation/createChat.ts

View check run for this annotation

Codecov / codecov/patch

src/resolvers/Mutation/createChat.ts#L71

Added line #L71 was not covered by tests
}
: {
creatorId: context.userId,
users: args.data.userIds,
isGroup: args.data.isGroup,
createdAt: now,
updatedAt: now,
unseenMessagesByUsers,
};

const createdChat = await Chat.create(chatPayload);
Expand Down
2 changes: 2 additions & 0 deletions src/resolvers/Mutation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { revokeRefreshTokenForUser } from "./revokeRefreshTokenForUser";
import { saveFcmToken } from "./saveFcmToken";
import { sendMembershipRequest } from "./sendMembershipRequest";
import { sendMessageToChat } from "./sendMessageToChat";
import { markChatMessagesAsRead } from "./markChatMessagesAsRead";
import { signUp } from "./signUp";
import { togglePostPin } from "./togglePostPin";
import { unassignUserTag } from "./unassignUserTag";
Expand Down Expand Up @@ -237,4 +238,5 @@ export const Mutation: MutationResolvers = {
updateFundraisingCampaignPledge,
createFundraisingCampaignPledge,
removeFundraisingCampaignPledge,
markChatMessagesAsRead,
};
62 changes: 62 additions & 0 deletions src/resolvers/Mutation/markChatMessagesAsRead.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { MutationResolvers } from "../../types/generatedGraphQLTypes";
import { errors, requestContext } from "../../libraries";
import { Chat, User } from "../../models";
import { CHAT_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants";
/**
/**
* This function marks all messages as read for the current user in a chat.
* @param _parent - parent of current request
* @param args - payload provided with the request
* @param context - context of entire application
* @remarks The following checks are done:
* 1. If the direct chat exists.
* 2. If the user exists
* @returns Updated chat object.
*/
export const markChatMessagesAsRead: MutationResolvers["markChatMessagesAsRead"] =
async (_parent, args, context) => {
const chat = await Chat.findOne({
_id: args.chatId,
}).lean();

if (!chat) {
throw new errors.NotFoundError(
requestContext.translate(CHAT_NOT_FOUND_ERROR.MESSAGE),
CHAT_NOT_FOUND_ERROR.CODE,
CHAT_NOT_FOUND_ERROR.PARAM,
);
}
Comment on lines +16 to +28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add input validation for chatId.

While MongoDB will handle invalid ObjectId formats, it's better to validate the input early to provide a better user experience.

Add validation before the database query:

 export const markChatMessagesAsRead: MutationResolvers["markChatMessagesAsRead"] =
   async (_parent, args, context) => {
+    if (!args.chatId?.match(/^[0-9a-fA-F]{24}$/)) {
+      throw new errors.ValidationError(
+        requestContext.translate(CHAT_NOT_FOUND_ERROR.MESSAGE),
+        CHAT_NOT_FOUND_ERROR.CODE,
+        CHAT_NOT_FOUND_ERROR.PARAM
+      );
+    }
+
     const chat = await Chat.findOne({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const markChatMessagesAsRead: MutationResolvers["markChatMessagesAsRead"] =
async (_parent, args, context) => {
const chat = await Chat.findOne({
_id: args.chatId,
}).lean();
if (!chat) {
throw new errors.NotFoundError(
requestContext.translate(CHAT_NOT_FOUND_ERROR.MESSAGE),
CHAT_NOT_FOUND_ERROR.CODE,
CHAT_NOT_FOUND_ERROR.PARAM,
);
}
export const markChatMessagesAsRead: MutationResolvers["markChatMessagesAsRead"] =
async (_parent, args, context) => {
if (!args.chatId?.match(/^[0-9a-fA-F]{24}$/)) {
throw new errors.NotFoundError(
context.translate(CHAT_NOT_FOUND_ERROR.MESSAGE),
CHAT_NOT_FOUND_ERROR.CODE,
CHAT_NOT_FOUND_ERROR.PARAM
);
}
const chat = await Chat.findOne({
_id: args.chatId,
}).lean();
if (!chat) {
throw new errors.NotFoundError(
context.translate(CHAT_NOT_FOUND_ERROR.MESSAGE),
CHAT_NOT_FOUND_ERROR.CODE,
CHAT_NOT_FOUND_ERROR.PARAM,
);
}


const currentUserExists = await User.exists({
_id: context.userId,
}).lean();

if (!currentUserExists) {
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
USER_NOT_FOUND_ERROR.PARAM,
);
}
Comment on lines +30 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Verify user's authorization to access the chat.

The code only checks if the user exists but doesn't verify if they are a member of the chat. This could lead to unauthorized access.

Add authorization check:

     if (!currentUserExists) {
       throw new errors.NotFoundError(
         requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
         USER_NOT_FOUND_ERROR.CODE,
         USER_NOT_FOUND_ERROR.PARAM,
       );
     }
+
+    // Verify user is a member of the chat
+    if (!chat.users?.includes(context.userId)) {
+      throw new errors.UnauthorizedError(
+        requestContext.translate("User is not a member of this chat"),
+        "UNAUTHORIZED_CHAT_ACCESS"
+      );
+    }

Committable suggestion was skipped due to low confidence.


const unseenMessagesByUsers = JSON.parse(
chat.unseenMessagesByUsers as unknown as string,
);

Comment on lines +42 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for JSON parsing.

The JSON.parse operation could throw if the data is malformed. Also, the type casting looks suspicious.

Add error handling and proper typing:

-    const unseenMessagesByUsers = JSON.parse(
-      chat.unseenMessagesByUsers as unknown as string,
-    );
+    let unseenMessagesByUsers: Record<string, number>;
+    try {
+      unseenMessagesByUsers = JSON.parse(
+        typeof chat.unseenMessagesByUsers === 'string' 
+          ? chat.unseenMessagesByUsers 
+          : JSON.stringify(chat.unseenMessagesByUsers)
+      );
+    } catch (error) {
+      throw new errors.ValidationError(
+        requestContext.translate("Invalid chat data format"),
+        "INVALID_CHAT_DATA"
+      );
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const unseenMessagesByUsers = JSON.parse(
chat.unseenMessagesByUsers as unknown as string,
);
let unseenMessagesByUsers: Record<string, number>;
try {
unseenMessagesByUsers = JSON.parse(
typeof chat.unseenMessagesByUsers === 'string'
? chat.unseenMessagesByUsers
: JSON.stringify(chat.unseenMessagesByUsers)
);
} catch (error) {
throw new errors.ValidationError(
requestContext.translate("Invalid chat data format"),
"INVALID_CHAT_DATA"
);
}

if (unseenMessagesByUsers[context.userId] !== undefined) {
unseenMessagesByUsers[context.userId] = 0;
}
Comment on lines +46 to +48
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add type validation for unseenMessagesByUsers structure.

The code assumes unseenMessagesByUsers is a valid object with number values, but there's no validation of its structure.

Add type validation:

+    // Validate unseenMessagesByUsers structure
+    if (typeof unseenMessagesByUsers !== 'object' || unseenMessagesByUsers === null) {
+      throw new errors.ValidationError(
+        requestContext.translate('Invalid unseenMessagesByUsers format'),
+        'INVALID_MESSAGE_COUNT_FORMAT'
+      );
+    }
+
+    // Validate the specific user's message count
+    if (unseenMessagesByUsers[context.userId] !== undefined && 
+        typeof unseenMessagesByUsers[context.userId] !== 'number') {
+      throw new errors.ValidationError(
+        requestContext.translate('Invalid message count format'),
+        'INVALID_MESSAGE_COUNT_TYPE'
+      );
+    }
+
     if (unseenMessagesByUsers[context.userId] !== undefined) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (unseenMessagesByUsers[context.userId] !== undefined) {
unseenMessagesByUsers[context.userId] = 0;
}
// Validate unseenMessagesByUsers structure
if (typeof unseenMessagesByUsers !== 'object' || unseenMessagesByUsers === null) {
throw new errors.ValidationError(
requestContext.translate('Invalid unseenMessagesByUsers format'),
'INVALID_MESSAGE_COUNT_FORMAT'
);
}
// Validate the specific user's message count
if (unseenMessagesByUsers[context.userId] !== undefined &&
typeof unseenMessagesByUsers[context.userId] !== 'number') {
throw new errors.ValidationError(
requestContext.translate('Invalid message count format'),
'INVALID_MESSAGE_COUNT_TYPE'
);
}
if (unseenMessagesByUsers[context.userId] !== undefined) {
unseenMessagesByUsers[context.userId] = 0;
}


const updatedChat = await Chat.findByIdAndUpdate(
{
_id: chat._id,
},
{
$set: {
unseenMessagesByUsers: JSON.stringify(unseenMessagesByUsers),
},
},
);
Comment on lines +50 to +59
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add optimistic concurrency control to prevent race conditions.

Multiple concurrent updates to unseenMessagesByUsers could lead to lost updates.

Add version control to prevent race conditions:

     const updatedChat = await Chat.findByIdAndUpdate(
-      {
-        _id: chat._id,
-      },
+      chat._id,
       {
         $set: {
           unseenMessagesByUsers: JSON.stringify(unseenMessagesByUsers),
         },
       },
+      {
+        new: true,
+        // Add version control
+        __v: chat.__v,
+      }
     );

+    if (!updatedChat) {
+      throw new errors.ConflictError(
+        requestContext.translate('Chat was updated by another operation'),
+        'CONCURRENT_MODIFICATION'
+      );
+    }

Committable suggestion was skipped due to low confidence.


return updatedChat;
};
Comment on lines +50 to +62
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix update operation and add result validation.

The update operation needs the { new: true } option to return the updated document, and the result should be validated.

Apply these changes:

     const updatedChat = await Chat.findByIdAndUpdate(
-      {
-        _id: chat._id,
-      },
+      chat._id,
       {
         $set: {
           unseenMessagesByUsers: JSON.stringify(unseenMessagesByUsers),
         },
       },
+      { new: true }
     );
 
+    if (!updatedChat) {
+      throw new errors.NotFoundError(
+        requestContext.translate(CHAT_NOT_FOUND_ERROR.MESSAGE),
+        CHAT_NOT_FOUND_ERROR.CODE,
+        CHAT_NOT_FOUND_ERROR.PARAM,
+      );
+    }
+
     return updatedChat;

Committable suggestion was skipped due to low confidence.

15 changes: 15 additions & 0 deletions src/resolvers/Mutation/sendMessageToChat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ export const sendMessageToChat: MutationResolvers["sendMessageToChat"] = async (
updatedAt: now,
});

const unseenMessagesByUsers = JSON.parse(
chat.unseenMessagesByUsers as unknown as string,
);

Object.keys(unseenMessagesByUsers).map((user: string) => {
if (user !== context.userId) {
console.log("user", user, context.userId);
unseenMessagesByUsers[user] += 1;
}
});
Comment on lines +55 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Multiple improvements needed in unseen messages handling logic.

The current implementation has several issues that need to be addressed:

  1. Missing error handling for JSON.parse
  2. Using type assertions suggests potential type system issues
  3. Debug console.log statement in production code
  4. No validation of unseenMessagesByUsers structure
  5. Using map for side effects

Consider applying these improvements:

- const unseenMessagesByUsers = JSON.parse(
-   chat.unseenMessagesByUsers as unknown as string,
- );
-
- Object.keys(unseenMessagesByUsers).map((user: string) => {
-   if (user !== context.userId) {
-     console.log("user", user, context.userId);
-     unseenMessagesByUsers[user] += 1;
-   }
- });

+ let unseenMessagesByUsers;
+ try {
+   unseenMessagesByUsers = JSON.parse(chat.unseenMessagesByUsers);
+   if (!unseenMessagesByUsers || typeof unseenMessagesByUsers !== 'object') {
+     throw new Error('Invalid unseenMessagesByUsers structure');
+   }
+ } catch (error) {
+   throw new errors.ValidationError(
+     'Invalid unseenMessagesByUsers format',
+     'INVALID_UNSEEN_MESSAGES',
+     'unseenMessagesByUsers'
+   );
+ }
+
+ // Use forEach instead of map for side effects
+ Object.keys(unseenMessagesByUsers).forEach((user: string) => {
+   if (user !== context.userId) {
+     unseenMessagesByUsers[user] += 1;
+   }
+ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const unseenMessagesByUsers = JSON.parse(
chat.unseenMessagesByUsers as unknown as string,
);
Object.keys(unseenMessagesByUsers).map((user: string) => {
if (user !== context.userId) {
console.log("user", user, context.userId);
unseenMessagesByUsers[user] += 1;
}
});
let unseenMessagesByUsers;
try {
unseenMessagesByUsers = JSON.parse(chat.unseenMessagesByUsers);
if (!unseenMessagesByUsers || typeof unseenMessagesByUsers !== 'object') {
throw new Error('Invalid unseenMessagesByUsers structure');
}
} catch (error) {
throw new errors.ValidationError(
'Invalid unseenMessagesByUsers format',
'INVALID_UNSEEN_MESSAGES',
'unseenMessagesByUsers'
);
}
// Use forEach instead of map for side effects
Object.keys(unseenMessagesByUsers).forEach((user: string) => {
if (user !== context.userId) {
unseenMessagesByUsers[user] += 1;
}
});


// add createdDirectChatMessage to directChat
await Chat.updateOne(
{
Expand All @@ -61,6 +72,10 @@ export const sendMessageToChat: MutationResolvers["sendMessageToChat"] = async (
$push: {
messages: createdChatMessage._id,
},
$set: {
unseenMessagesByUsers: JSON.stringify(unseenMessagesByUsers),
updatedAt: now,
},
Comment on lines +75 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using atomic operations for better concurrency handling.

The current implementation might be susceptible to race conditions when multiple users send messages simultaneously. Consider using MongoDB's atomic operations.

Here's a suggested improvement using atomic operations:

- $set: {
-   unseenMessagesByUsers: JSON.stringify(unseenMessagesByUsers),
-   updatedAt: now,
- },
+ $set: {
+   updatedAt: now,
+ },
+ // Use dynamic field updates to atomically increment counters
+ ...Object.keys(unseenMessagesByUsers).reduce((acc, userId) => {
+   if (userId !== context.userId) {
+     acc[`unseenMessagesByUsers.${userId}`] = unseenMessagesByUsers[userId];
+   }
+   return acc;
+ }, {}),

This approach:

  1. Eliminates the need for JSON stringify/parse
  2. Uses atomic updates to prevent race conditions
  3. Maintains data consistency in concurrent scenarios

Committable suggestion was skipped due to low confidence.

},
);

Expand Down
16 changes: 13 additions & 3 deletions src/resolvers/Query/chatsByUserId.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { QueryResolvers } from "../../types/generatedGraphQLTypes";
import { Chat } from "../../models";
import type { SortOrder } from "mongoose";
/**
* This query will fetch all the Chats for the current user from the database.
* @param _parent-
Expand All @@ -13,11 +14,20 @@ export const chatsByUserId: QueryResolvers["chatsByUserId"] = async (
_parent,
args,
) => {
const sort = {
updatedAt: -1,
} as
| string
| { [key: string]: SortOrder | { $meta: unknown } }
| [string, SortOrder][]
| null
| undefined;
Comment on lines +17 to +24
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider simplifying the sort configuration.

While the current implementation works, we can make it more maintainable and readable.

Consider this simplified version:

-  const sort = {
-    updatedAt: -1,
-  } as
-    | string
-    | { [key: string]: SortOrder | { $meta: unknown } }
-    | [string, SortOrder][]
-    | null
-    | undefined;
+  const sort: { [key: string]: SortOrder } = {
+    updatedAt: SortOrder.DESC
+  };

This change:

  1. Uses the SortOrder enum for better readability
  2. Simplifies the type definition to match the actual usage
  3. Maintains the same functionality while being more explicit
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sort = {
updatedAt: -1,
} as
| string
| { [key: string]: SortOrder | { $meta: unknown } }
| [string, SortOrder][]
| null
| undefined;
const sort: { [key: string]: SortOrder } = {
updatedAt: SortOrder.DESC
};


const chats = await Chat.find({
users: args.id,
}).lean();

console.log(chats);
})
.sort(sort)
.lean();

return chats;
};
2 changes: 2 additions & 0 deletions src/typeDefs/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ export const mutations = gql`
replyTo: ID
): ChatMessage! @auth

markChatMessagesAsRead(chatId: ID!, userId: ID!): Chat @auth

signUp(data: UserInput!, file: String): AuthData!

togglePostPin(id: ID!, title: String): Post! @auth
Expand Down
1 change: 1 addition & 0 deletions src/typeDefs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ export const types = gql`
admins: [User]
lastMessageId: String
image: String
unseenMessagesByUsers: JSON
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider using a strict type instead of JSON.

Using the JSON scalar type loses GraphQL's type safety benefits. Consider creating a dedicated type to represent unseen messages:

+ type UnseenMessages {
+   userId: ID!
+   count: Int!
+ }

  type Chat {
    # ... other fields ...
-   unseenMessagesByUsers: JSON
+   unseenMessagesByUsers: [UnseenMessages!]
  }

This provides several benefits:

  • Type safety and validation at the GraphQL schema level
  • Better documentation through the type system
  • Improved developer experience with IDE support

Committable suggestion was skipped due to low confidence.

}

type ChatMessage {
Expand Down
10 changes: 10 additions & 0 deletions src/types/generatedGraphQLTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ export type Chat = {
messages?: Maybe<Array<Maybe<ChatMessage>>>;
name?: Maybe<Scalars['String']['output']>;
organization?: Maybe<Organization>;
unseenMessagesByUsers?: Maybe<Scalars['JSON']['output']>;
updatedAt: Scalars['DateTime']['output'];
users: Array<User>;
};
Expand Down Expand Up @@ -1200,6 +1201,7 @@ export type Mutation = {
likePost?: Maybe<Post>;
login: AuthData;
logout: Scalars['Boolean']['output'];
markChatMessagesAsRead?: Maybe<Chat>;
otp: OtpData;
recaptcha: Scalars['Boolean']['output'];
refreshToken: ExtendSession;
Expand Down Expand Up @@ -1566,6 +1568,12 @@ export type MutationLoginArgs = {
};


export type MutationMarkChatMessagesAsReadArgs = {
chatId: Scalars['ID']['input'];
userId: Scalars['ID']['input'];
};


export type MutationOtpArgs = {
data: OtpInput;
};
Expand Down Expand Up @@ -3818,6 +3826,7 @@ export type ChatResolvers<ContextType = any, ParentType extends ResolversParentT
messages?: Resolver<Maybe<Array<Maybe<ResolversTypes['ChatMessage']>>>, ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
organization?: Resolver<Maybe<ResolversTypes['Organization']>, ParentType, ContextType>;
unseenMessagesByUsers?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
users?: Resolver<Array<ResolversTypes['User']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
Expand Down Expand Up @@ -4278,6 +4287,7 @@ export type MutationResolvers<ContextType = any, ParentType extends ResolversPar
likePost?: Resolver<Maybe<ResolversTypes['Post']>, ParentType, ContextType, RequireFields<MutationLikePostArgs, 'id'>>;
login?: Resolver<ResolversTypes['AuthData'], ParentType, ContextType, RequireFields<MutationLoginArgs, 'data'>>;
logout?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
markChatMessagesAsRead?: Resolver<Maybe<ResolversTypes['Chat']>, ParentType, ContextType, RequireFields<MutationMarkChatMessagesAsReadArgs, 'chatId' | 'userId'>>;
otp?: Resolver<ResolversTypes['OtpData'], ParentType, ContextType, RequireFields<MutationOtpArgs, 'data'>>;
recaptcha?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationRecaptchaArgs, 'data'>>;
refreshToken?: Resolver<ResolversTypes['ExtendSession'], ParentType, ContextType, RequireFields<MutationRefreshTokenArgs, 'refreshToken'>>;
Expand Down
3 changes: 3 additions & 0 deletions tests/helpers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export const createTestChat = async (): Promise<
createdAt: new Date(),
updatedAt: new Date(),
admins: [testUser._id],
unseenMessagesByUsers: JSON.stringify({
[testUser._id]: 0,
}),
Comment on lines +29 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add unseenMessagesByUsers initialization to all chat creation helpers and handle multiple users.

The current implementation has several gaps:

  1. The property is missing in createTestChatwithUsers and createTestMessageForMultipleUser functions
  2. Only initializes unseen messages for the creator, not all chat users
  3. Using raw JSON.stringify without type safety

Consider applying this pattern to all chat creation helpers:

// Define a type for better type safety
type UnseenMessages = { [userId: string]: number };

// Helper function to create unseenMessagesByUsers
const createUnseenMessagesMap = (userIds: string[]): string => {
  const unseenMessages: UnseenMessages = Object.fromEntries(
    userIds.map(userId => [userId, 0])
  );
  return JSON.stringify(unseenMessages);
};

Then use it in chat creation:

 const testChat = await Chat.create({
   creatorId: testUser._id,
   users: [testUser._id],
   organization: testOrganization._id,
   isGroup: true,
   createdAt: new Date(),
   updatedAt: new Date(),
   admins: [testUser._id],
-  unseenMessagesByUsers: JSON.stringify({
-    [testUser._id]: 0,
-  }),
+  unseenMessagesByUsers: createUnseenMessagesMap([testUser._id]),
 });

});

return [testUser, testOrganization, testChat];
Expand Down
Loading
Loading