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

Updated chat schema #2466

Open
wants to merge 70 commits into
base: develop
Choose a base branch
from

Conversation

disha1202
Copy link

@disha1202 disha1202 commented Aug 18, 2024

What kind of change does this PR introduce?

Issue Number:

Fixes #2465

Did you add tests for your changes?

Snapshots/Videos:

If relevant, did you update the documentation?

Summary

Does this PR introduce a breaking change?

Other information

Have you read the contributing guide?

Summary by CodeRabbit

  • New Features

    • Introduced a new unified Chat and ChatMessage structure for improved chat functionality.
    • Added a createChat mutation to facilitate the creation of both direct and group chats.
    • Implemented a unified sendMessageToChat mutation for sending messages to any chat type.
    • Introduced new queries: chatById and chatsByUserId for retrieving chat information.
    • Added subscription for real-time message updates through messageSentToChat.
    • Enhanced retrieval of chat users and messages through new resolvers.
  • Bug Fixes

    • Enhanced error handling for missing chats and messages, ensuring proper notifications and responses.
  • Documentation

    • Updated GraphQL schema definitions to reflect new naming conventions and functionalities.
  • Tests

    • Expanded test coverage for new chat functionalities and resolver logic to ensure expected behavior.
  • Chores

    • Refactored existing code for improved clarity and maintainability, consolidating chat-related functionalities.

@@ -88,7 +88,7 @@
createComment(postId: ID!, data: CommentInput!): Comment @auth
createDirectChat(data: createChatInput!): DirectChat! @auth
createChat(data: chatInput!): Chat
Copy link
Author

Choose a reason for hiding this comment

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

Removed all the references of DirectChat

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Outside diff range and nitpick comments (3)
src/resolvers/Mutation/sendMessageToChat.ts (1)

1-73: Add tests to cover the various scenarios.

To ensure the reliability and maintainability of the sendMessageToChat function, it is crucial to have a comprehensive test suite. Consider adding tests to cover the following scenarios:

  1. Sending a message to a valid chat as a participant user.
  2. Attempting to send a message to a non-existent chat.
  3. Attempting to send a message as a non-existent user.
  4. Attempting to send a message to a chat where the user is not a participant.
  5. Verifying the subscription event is published with the correct data.

By covering these scenarios, you can have confidence in the function's behavior and catch any potential issues early in the development process.

src/models/ChatMessage.ts (2)

7-9: Update documentation to reflect 'ChatMessage' instead of 'DirectChatMessage'.

The description still refers to "direct chat message". Please update it to "chat message" to reflect the new unified structure.


69-76: Remove redundant 'createdAt' and 'updatedAt' fields from the schema.

Since timestamps: true is enabled, Mongoose automatically adds createdAt and updatedAt fields. Defining them explicitly is unnecessary.

Apply this diff to remove the redundant fields:

     deletedBy: [
       {
         type: Schema.Types.ObjectId,
         ref: "User",
         required: false,
       },
     ],
-    updatedAt: {
-      type: Date,
-      required: true,
-    },
-    createdAt: {
-      type: Date,
-      required: true,
-    },
   },
   {
     timestamps: true, // Adds createdAt and updatedAt automatically
   },
Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

Commits

Files that changed from the base of the PR and between 50d579e and 9f2f12d.

Files selected for processing (17)
  • schema.graphql (6 hunks)
  • src/models/ChatMessage.ts (2 hunks)
  • src/resolvers/Mutation/createMessageChat.ts (0 hunks)
  • src/resolvers/Mutation/index.ts (4 hunks)
  • src/resolvers/Mutation/sendMessageToChat.ts (1 hunks)
  • src/resolvers/Subscription/directMessageChat.ts (0 hunks)
  • src/resolvers/Subscription/index.ts (1 hunks)
  • src/resolvers/Subscription/messageSentToChat.ts (1 hunks)
  • src/typeDefs/mutations.ts (2 hunks)
  • src/typeDefs/queries.ts (1 hunks)
  • src/typeDefs/subscriptions.ts (1 hunks)
  • src/typeDefs/types.ts (1 hunks)
  • src/types/generatedGraphQLTypes.ts (20 hunks)
  • tests/resolvers/Mutation/createMessageChat.spec.ts (0 hunks)
  • tests/resolvers/Query/chatById.spec.ts (1 hunks)
  • tests/resolvers/Query/chatsByuserId.spec.ts (1 hunks)
  • tests/resolvers/Subscription/directMessageChat.spec.ts (0 hunks)
Files not reviewed due to no reviewable changes (4)
  • src/resolvers/Mutation/createMessageChat.ts
  • src/resolvers/Subscription/directMessageChat.ts
  • tests/resolvers/Mutation/createMessageChat.spec.ts
  • tests/resolvers/Subscription/directMessageChat.spec.ts
Files skipped from review as they are similar to previous changes (3)
  • src/resolvers/Mutation/index.ts
  • src/resolvers/Subscription/messageSentToChat.ts
  • tests/resolvers/Query/chatById.spec.ts
Additional context used
Learnings (1)
src/types/generatedGraphQLTypes.ts (1)

<retrieved_learning>
Learnt from: disha1202
PR: #2466
File: src/types/generatedGraphQLTypes.ts:3096-3101
Timestamp: 2024-09-22T09:37:15.069Z
Learning: Direct chats do not need to be associated with any organization, so the organizationId field in ChatInput remains optional.
</retrieved_learning>

Additional comments not posted (22)
src/typeDefs/subscriptions.ts (1)

6-6: Approve the consolidation of subscription fields.

The introduction of the messageSentToChat field in the Subscription type aligns with the objective of merging the DirectChat and GroupChat entities into a unified Chat entity. This change simplifies the message handling process and improves the overall chat architecture.

Please ensure that the corresponding client-side code is updated to use the new subscription field and that existing applications relying on the removed fields (messageSentToDirectChat and messageSentToGroupChat) are migrated accordingly.

Run the following script to verify the usage of the removed subscription fields in the codebase:

src/resolvers/Subscription/index.ts (2)

2-2: LGTM! Ensure thorough testing of the consolidated subscription.

The import statement for the messageSentToChat subscription is correct and aligns with the objective of merging DirectChat and GroupChat entities into a unified Chat entity.

Please ensure that the consolidated messageSentToChat subscription is thoroughly tested to confirm that it handles all chat-related functionality correctly, replacing the previously separate subscriptions.


5-5: LGTM! Update the API documentation and communicate the change to the users.

The export of the messageSentToChat subscription as part of the Subscription object is correct and aligns with the objective of consolidating multiple subscriptions into a single subscription.

Please ensure that the API documentation is updated to reflect the removal of the directMessageChat, messageSentToDirectChat, and messageSentToGroupChat subscriptions and their replacement with the messageSentToChat subscription. Also, communicate this change to the users of the API so that they can update their applications accordingly.

tests/resolvers/Query/chatsByuserId.spec.ts (3)

6-8: LGTM!

The imports have been updated correctly to match the new resolver, model, and argument type names.


26-26: LGTM!

The test suite description has been updated correctly to match the new resolver name.


42-52: LGTM!

The test case has been updated correctly to use the new argument type, resolver, and model. The assertions have also been updated to ensure they are checking the correct payloads.

src/resolvers/Mutation/sendMessageToChat.ts (1)

1-73: Excellent work on implementing the sendMessageToChat function!

The function handles the core functionality of sending a message to a chat effectively. It performs necessary validations, creates a new chat message, updates the chat document, and publishes a subscription event. The error handling and translations ensure a robust and user-friendly experience.

src/typeDefs/queries.ts (2)

47-47: LGTM!

The chatById query is a useful addition that aligns with the objective of consolidating the chat structure. It provides a way to retrieve a specific chat by its ID, replacing the previous groupChatById query.


49-49: LGTM!

The chatsByUserId query is a valuable addition that aligns with the objective of consolidating the chat structure. It provides a way to retrieve all chats associated with a specific user, replacing the previous directChatsByUserID and groupChatsByUserId queries.

src/typeDefs/mutations.ts (1)

235-240: Mutation changes look good! Please provide more details on the new parameters.

The modifications to the sendMessageToChat mutation align with the goal of unifying the chat functionality. The addition of the type and replyTo parameters enhances the flexibility of sending messages.

A few questions to ensure clarity:

  1. What are the allowed values for the type parameter, and how is it being used to determine the chat type (DIRECT or GROUP)? Please ensure that the valid types are documented.
  2. Are there any constraints on the replyTo parameter? For example, should it be validated that the referenced message belongs to the same chat?
src/typeDefs/types.ts (2)

720-733: Excellent work on introducing the unified Chat type!

The new Chat type effectively consolidates the previously separate DirectChat and GroupChat types, simplifying the overall chat structure. The chosen fields provide a comprehensive representation of a chat entity, including metadata, participants, messages, and organizational context.

The isGroup field is a smart addition, as it allows distinguishing between direct and group chats while maintaining a unified structure. The relationships with User, Organization, and ChatMessage types ensure data consistency and enable efficient querying.

This refactoring aligns perfectly with the PR objective and enhances the clarity and maintainability of the chat-related functionality in the schema.


735-744: Great job on defining the ChatMessage type!

The ChatMessage type perfectly complements the Chat type in unifying the chat structure. It encapsulates all the essential information related to a chat message, such as metadata, content, sender, and deletion status.

The mandatory chatMessageBelongsTo field ensures referential integrity by enforcing a relationship with the Chat type. This guarantees that every message belongs to a specific chat, preventing orphaned messages.

The replyTo field is a valuable addition, as it enables the functionality of message replies. This enhances the conversational structure and allows for more organized and contextual communication within chats.

The sender field establishes a clear relationship with the User type, making it easy to identify the author of each message. The deletedBy field provides the flexibility to track message deletions by multiple users, which can be useful for moderation and auditing purposes.

Overall, the ChatMessage type is well-structured and aligns perfectly with the goals of the PR. It contributes to a cleaner and more intuitive chat-related schema.

schema.graphql (8)

193-206: LGTM!

The Chat type looks good with appropriate fields to represent a chat entity. The inclusion of admins field and isGroup flag provides support for both direct and group chats. The messages field establishes the necessary relationship with the ChatMessage type.


208-217: The ChatMessage type structure looks good overall.

The type includes appropriate fields to represent a chat message. The chatMessageBelongsTo field establishes the necessary relationship with the Chat type. The deletedBy field provides a way to track message deletions.


1481-1481: LGTM!

The chatById query looks good. It accepts a required id argument and returns the corresponding Chat type.


1482-1482: LGTM!

The chatsByUserId query looks good. It accepts a required id argument representing the user ID and returns an array of associated Chat types.


1085-1085: LGTM!

The createChat mutation looks good. It accepts a required data argument of type chatInput to capture the necessary details for creating a new chat. It returns the created Chat type.


1618-1618: LGTM!

The messageSentToChat subscription looks good. It allows subscribing to new messages sent to a chat for a specific user. The @auth directive ensures that authentication is required to establish the subscription.


214-214: Avoid recursive type in replyTo field to prevent excessive nesting.

Setting replyTo as ChatMessage can lead to deep recursive nesting when querying messages, which may cause performance issues. Consider using ID as the type for replyTo to reference the parent message without nesting.

Apply this diff to change the replyTo field type:

 type ChatMessage {
   _id: ID!
   chatMessageBelongsTo: Chat!
   createdAt: DateTime!
   deletedBy: [User]
   messageContent: String!
-  replyTo: ChatMessage
+  replyTo: ID
   sender: User!
   updatedAt: DateTime!
 }

Likely invalid or redundant comment.


2035-2038: Include userIds as a required field in chatInput for completeness.

The userIds field in the chatInput type is currently optional. To ensure that participants are always specified when creating a new chat, consider making it a required field.

Apply this diff:

 input chatInput {
   image: String
   isGroup: Boolean!
   name: String
+  organizationId: ID
+  userIds: [ID!]!
 }

Likely invalid or redundant comment.

src/types/generatedGraphQLTypes.ts (2)

13-14: Importing InterfaceChatModel and InterfaceChatMessageModel

The new imports for InterfaceChatModel and InterfaceChatMessageModel are correctly added. This ensures the necessary models are available for the updated types.


3096-3101: Confirmation: Optional organizationId in ChatInput is appropriate

The ChatInput type correctly keeps organizationId as optional. This aligns with the requirement that direct chats do not need to be associated with an organization.

Comment on lines 30 to 34
const args: QueryChatsByUserIdArgs = {
id: new Types.ObjectId().toString(),
};

await directChatsByUserIDResolver?.({}, args, {});
await chatsByUserIdResolver?.({}, args, {});
Copy link

Choose a reason for hiding this comment

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

Update error message for consistency.

The error message in the test case should reflect the updated chat model. Consider updating "DirectChats not found" to a more generic message.

     } catch (error: unknown) {
-      expect((error as Error).message).toEqual("DirectChats not found");
+      expect((error as Error).message).toEqual("Chats not found");
     }
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 args: QueryChatsByUserIdArgs = {
id: new Types.ObjectId().toString(),
};
await directChatsByUserIDResolver?.({}, args, {});
await chatsByUserIdResolver?.({}, args, {});
const args: QueryChatsByUserIdArgs = {
id: new Types.ObjectId().toString(),
};
await chatsByUserIdResolver?.({}, args, {});

Comment on lines +20 to +30
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,
);
}
Copy link

Choose a reason for hiding this comment

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

Consider adding a check to verify if the user is a participant of the chat.

To enhance security and prevent unauthorized users from sending messages to a chat, it would be beneficial to verify if the user sending the message (context.userId) is actually a participant of the specified chat (args.chatId).

You can add this check after the chat existence validation:

// Check if the user is a participant of the chat
const isParticipant = chat.participants.includes(context.userId);

if (!isParticipant) {
  throw new errors.ForbiddenError(
    requestContext.translate(USER_NOT_PARTICIPANT_ERROR.MESSAGE),
    USER_NOT_PARTICIPANT_ERROR.CODE,
    USER_NOT_PARTICIPANT_ERROR.PARAM,
  );
}

This ensures that only users who are participants of the chat can send messages to it.

@@ -88,7 +84,7 @@ export const mutations = gql`
createComment(postId: ID!, data: CommentInput!): Comment @auth
createDirectChat(data: createChatInput!): DirectChat! @auth
createChat(data: chatInput!): Chat
Copy link

Choose a reason for hiding this comment

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

Tip

Codebase Verification

Incomplete removal of createDirectChat and DirectChat found.

The removal of createDirectChat and DirectChat was not fully completed. Please address the following:

  • Resolvers:

    • src/resolvers/index.ts
    • src/resolvers/Mutation/sendMessageToChat.ts
  • Type Definitions:

    • src/typeDefs/errors/index.ts
    • src/typeDefs/errors/createDirectChatError.ts
    • src/types/generatedGraphQLTypes.ts
  • Models:

    • src/models/Chat.ts
    • src/models/ChatMessage.ts
  • Tests:

    • tests/resolvers/ChatMessage/chatMessageBelongsTo.spec.ts
    • tests/resolvers/ChatMessage/sender.spec.ts
    • tests/resolvers/Subscription/messageSentToChat.spec.ts
    • tests/resolvers/Query/chatsByuserId.spec.ts
    • tests/resolvers/ChatMessage/replyTo.spec.ts
    • tests/resolvers/Chat/creator.spec.ts
    • tests/resolvers/Mutation/sendMessageToChat.spec.ts

Ensure that all references to createDirectChat and DirectChat are removed or updated to use the new createChat mutation and Chat type to maintain codebase consistency.

Analysis chain

Mutation looks good! Verify the removal of createDirectChat and DirectChat from the codebase.

The addition of the createChat mutation aligns with the objective of consolidating chat types. Please ensure that:

  • The chatInput input type covers all necessary fields for creating a chat.
  • All occurrences of the createDirectChat mutation and DirectChat type have been removed or updated to use createChat and Chat respectively.

Run the following script to verify the removal of createDirectChat and DirectChat:

Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the removal of the old `createDirectChat` mutation and `DirectChat` type.

# Test 1: Search for the `createDirectChat` mutation. Expect: No results.
rg --type ts 'createDirectChat'

# Test 2: Search for the `DirectChat` type. Expect: No results.
rg --type ts 'DirectChat'

Length of output: 2815

@@ -1165,8 +1151,7 @@ type Mutation {
revokeRefreshTokenForUser: Boolean!
saveFcmToken(token: String): Boolean!
sendMembershipRequest(organizationId: ID!): MembershipRequest!
sendMessageToDirectChat(chatId: ID!, messageContent: String!): DirectChatMessage!
sendMessageToGroupChat(chatId: ID!, messageContent: String!): GroupChatMessage!
sendMessageToChat(chatId: ID!, messageContent: String!, replyTo: ID, type: String!): ChatMessage!
Copy link

Choose a reason for hiding this comment

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

Use MessageType enum for type parameter in sendMessageToChat mutation.

Replacing the String type with the MessageType enum for the type parameter enhances validation and ensures only predefined message types are accepted.

Define the MessageType enum:

enum MessageType {
  TEXT
  IMAGE
  VIDEO
  FILE
}

Apply this diff:

 type Mutation {
-  sendMessageToChat(chatId: ID!, messageContent: String!, replyTo: ID, type: String!): ChatMessage!
+  sendMessageToChat(chatId: ID!, messageContent: String!, replyTo: ID, type: MessageType!): ChatMessage!
   ...
 }

Comment on lines 24 to 33
* @param chatMessageBelongsTo - Reference to the chat session to which the message belongs.
* @param sender - Reference to the user who sent the message.
* @param receiver - Reference to the user who received the message.
* @param messageContent - Content of the direct chat message.
* @param replyTo - Reference to the message replied to.
* @param type - Type of the message (STRING, MEDIA, FILE).
* @param status - Status of the message (ACTIVE, BLOCKED, DELETED).
* @param deletedBy - List of users who have deleted the message.
* @param createdAt - Date when the direct chat message was created.
* @param updatedAt - Date when the direct chat message was last updated.
*/
Copy link

Choose a reason for hiding this comment

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

Synchronize JSDoc comments with the current schema.

Some @param tags reference properties that are either renamed or no longer exist (e.g., type, messageContent described as "direct chat message"). Please update the documentation to match the updated property names and descriptions.

sender: PopulatedDoc<InterfaceUser & Document>;
receiver: PopulatedDoc<InterfaceUser & Document>;
replyTo: PopulatedDoc<InterfaceChatMessage & Document>;
messageContent: string;
status: string;
Copy link

Choose a reason for hiding this comment

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

Define 'status' as a TypeScript enum for better type safety.

Currently, status is typed as a string. Defining it using a TypeScript enum enhances type safety and ensures only valid status values are assigned.

Example:

export enum ChatMessageStatus {
  ACTIVE = "ACTIVE",
  BLOCKED = "BLOCKED",
  DELETED = "DELETED",
}

export interface InterfaceChatMessage {
  // ...
  status: ChatMessageStatus;
  // ...
}

Update the schema to reflect the enum:

   status: {
     type: String,
     required: true,
-    enum: ["ACTIVE", "BLOCKED", "DELETED"],
+    enum: Object.values(ChatMessageStatus),
     default: ChatMessageStatus.ACTIVE,
   },

Comment on lines +265 to +279
export type Chat = {
__typename?: 'Chat';
_id: Scalars['ID']['output'];
admins?: Maybe<Array<Maybe<User>>>;
createdAt: Scalars['DateTime']['output'];
creator?: Maybe<User>;
image?: Maybe<Scalars['String']['output']>;
isGroup: Scalars['Boolean']['output'];
lastMessageId?: Maybe<Scalars['String']['output']>;
messages?: Maybe<Array<Maybe<ChatMessage>>>;
name?: Maybe<Scalars['String']['output']>;
organization?: Maybe<Organization>;
updatedAt: Scalars['DateTime']['output'];
users: Array<User>;
};
Copy link

Choose a reason for hiding this comment

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

Refine the type of lastMessageId in Chat

The Chat type is well-defined. However, the lastMessageId field is currently of type Scalars['String']['output'], which may not accurately represent an ID. For consistency and clarity, consider changing its type to Scalars['ID']['output'], as it likely references a ChatMessage ID.

Apply this diff to update the type of lastMessageId:

 export type Chat = {
   __typename?: 'Chat';
   _id: Scalars['ID']['output'];
   admins?: Maybe<Array<Maybe<User>>>;
   createdAt: Scalars['DateTime']['output'];
   creator?: Maybe<User>;
   image?: Maybe<Scalars['String']['output']>;
   isGroup: Scalars['Boolean']['output'];
-  lastMessageId?: Maybe<Scalars['String']['output']>;
+  lastMessageId?: Maybe<Scalars['ID']['output']>;
   messages?: Maybe<Array<Maybe<ChatMessage>>>;
   name?: Maybe<Scalars['String']['output']>;
   organization?: Maybe<Organization>;
   updatedAt: Scalars['DateTime']['output'];
   users: Array<User>;
 };
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 type Chat = {
__typename?: 'Chat';
_id: Scalars['ID']['output'];
admins?: Maybe<Array<Maybe<User>>>;
createdAt: Scalars['DateTime']['output'];
creator?: Maybe<User>;
image?: Maybe<Scalars['String']['output']>;
isGroup: Scalars['Boolean']['output'];
lastMessageId?: Maybe<Scalars['String']['output']>;
messages?: Maybe<Array<Maybe<ChatMessage>>>;
name?: Maybe<Scalars['String']['output']>;
organization?: Maybe<Organization>;
updatedAt: Scalars['DateTime']['output'];
users: Array<User>;
};
export type Chat = {
__typename?: 'Chat';
_id: Scalars['ID']['output'];
admins?: Maybe<Array<Maybe<User>>>;
createdAt: Scalars['DateTime']['output'];
creator?: Maybe<User>;
image?: Maybe<Scalars['String']['output']>;
isGroup: Scalars['Boolean']['output'];
lastMessageId?: Maybe<Scalars['ID']['output']>;
messages?: Maybe<Array<Maybe<ChatMessage>>>;
name?: Maybe<Scalars['String']['output']>;
organization?: Maybe<Organization>;
updatedAt: Scalars['DateTime']['output'];
users: Array<User>;
};

Comment on lines +281 to +291
export type ChatMessage = {
__typename?: 'ChatMessage';
_id: Scalars['ID']['output'];
chatMessageBelongsTo: Chat;
createdAt: Scalars['DateTime']['output'];
deletedBy?: Maybe<Array<Maybe<User>>>;
messageContent: Scalars['String']['output'];
replyTo?: Maybe<ChatMessage>;
sender: User;
updatedAt: Scalars['DateTime']['output'];
};
Copy link

Choose a reason for hiding this comment

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

Rename chatMessageBelongsTo to chat for clarity

In the ChatMessage type, the field chatMessageBelongsTo could be renamed to chat to improve readability and align with common naming conventions. This makes it immediately clear that the message is associated with a Chat.

Apply this diff to rename the field:

 export type ChatMessage = {
   __typename?: 'ChatMessage';
   _id: Scalars['ID']['output'];
-  chatMessageBelongsTo: Chat;
+  chat: Chat;
   createdAt: Scalars['DateTime']['output'];
   deletedBy?: Maybe<Array<Maybe<User>>>;
   messageContent: Scalars['String']['output'];
   replyTo?: Maybe<ChatMessage>;
   sender: User;
   updatedAt: Scalars['DateTime']['output'];
 };
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 type ChatMessage = {
__typename?: 'ChatMessage';
_id: Scalars['ID']['output'];
chatMessageBelongsTo: Chat;
createdAt: Scalars['DateTime']['output'];
deletedBy?: Maybe<Array<Maybe<User>>>;
messageContent: Scalars['String']['output'];
replyTo?: Maybe<ChatMessage>;
sender: User;
updatedAt: Scalars['DateTime']['output'];
};
export type ChatMessage = {
__typename?: 'ChatMessage';
_id: Scalars['ID']['output'];
chat: Chat;
createdAt: Scalars['DateTime']['output'];
deletedBy?: Maybe<Array<Maybe<User>>>;
messageContent: Scalars['String']['output'];
replyTo?: Maybe<ChatMessage>;
sender: User;
updatedAt: Scalars['DateTime']['output'];
};

let testChatMessageWithoutReply: TestChatMessageType;
let MONGOOSE_INSTANCE: typeof mongoose;

beforeAll(async () => {
Copy link
Member

Choose a reason for hiding this comment

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

The beforeAll block asynchronously sets up data. However, if one of the await calls (e.g., createTestChatMessage) fails, you won’t know which one because the error handling isn’t granular. Use try-catch around each await call for better error handling and debugging.

Example

  try {
    MONGOOSE_INSTANCE = await connect();
  } catch (error) {
    console.error("Failed to connect to the database", error);
    throw error;
  }

  try {
    const temp = await createTestChatMessageWithoutReply();
    testChatMessageWithoutReply = temp[3];
  } catch (error) {
    console.error("Failed to create test chat message without reply", error);
    throw error;
  }

testChatMessage = temp1[3];
});

afterAll(async () => {
Copy link
Member

Choose a reason for hiding this comment

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

If connect() or data creation fails in beforeAll, afterAll could still try to run disconnect(), which might throw errors since MONGOOSE_INSTANCE could be undefined. Add a conditional check before calling disconnect.

Example

afterAll(async () => {
  if (MONGOOSE_INSTANCE) {
    await disconnect(MONGOOSE_INSTANCE);
  }
});


describe("resolvers -> DirectChatMessage -> replyTo", () => {
it(`returns directChat object for parent.replyTo`, async () => {
const parent = testChatMessage?.toObject();
Copy link
Member

Choose a reason for hiding this comment

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

Using ?. before toObject() could mask problems, especially if testChatMessage or testChatMessageWithoutReply is null. In the case of null objects, you’ll just get undefined, leading to silent failures. Use a check with if (!parent) before calling toObject().

const parent = testChatMessage ? testChatMessage.toObject() : null;

if (!parent) {
  throw new Error("Parent object is undefined.");
}

});

describe("resolvers -> DirectChatMessage -> replyTo", () => {
it(`returns directChat object for parent.replyTo`, async () => {
Copy link
Member

Choose a reason for hiding this comment

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

If replyToResolver or ChatMessage.findOne fail in the first test, it may cause errors that aren’t caught. Add explicit try-catch blocks around async calls within your test cases, especially for the first test.

it(`returns directChat object for parent.replyTo`, async () => {
  const parent = testChatMessage?.toObject();
  if (!parent) throw new Error("Parent object is undefined.");
  if (typeof replyToResolver !== "function") throw new Error("replyToResolver is not a function.");

  try {
    const replyToPayload = await replyToResolver(parent, {}, {});
    const replyTo = await ChatMessage.findOne({ _id: testChatMessage?.replyTo }).lean();
    expect(replyToPayload).toEqual(replyTo);
  } catch (error) {
    console.error("Test failed", error);
    throw error;
  }
});


expect(replyToPayload).toEqual(replyTo);
});
it(`throws NotFoundError if no directChat exists`, async () => {
Copy link
Member

Choose a reason for hiding this comment

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

The spy for requestContext.translate could be mocking more than necessary, potentially causing unexpected side effects. Ensure you restore the original function after the test to avoid interference in subsequent tests.

it(`throws NotFoundError if no directChat exists`, async () => {
  const { requestContext } = await import("../../../src/libraries");
  const spy = vi.spyOn(requestContext, "translate").mockImplementationOnce((message: string) => message);

  try {
    const parent = { ...testChatMessage?.toObject(), replyTo: new Types.ObjectId() };
    await replyToResolver(parent, {}, {});
  } catch (error) {
    expect(spy).toBeCalledWith(MESSAGE_NOT_FOUND_ERROR.MESSAGE);
    expect((error as Error).message).toEqual(MESSAGE_NOT_FOUND_ERROR.MESSAGE);
  } finally {
    spy.mockRestore(); // Restore the original function
  }
});

}

try {
// @ts-expect-error - Testing for error
Copy link
Member

Choose a reason for hiding this comment

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

The @ts-expect-error comment assumes a TypeScript error should happen. Ensure the expected error is related to the types you are actually testing. Add explicit type validation to ensure errors occur for the expected reason.

import { createTestUserAndOrganization } from "./userAndOrg";
import type { Document } from "mongoose";

export type TestChatType =
Copy link
Member

Choose a reason for hiding this comment

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

The TestChatType and TestChatMessageType types use union types that include null. This can introduce potential null checks across your codebase, increasing verbosity and potential runtime errors if not handled properly. Use utility types like Partial or Optional depending on your specific use case, or ensure that null is only used where necessary. You might also want to avoid unions if you can guarantee non-null values at certain points in your logic.

export type TestChatType = InterfaceChat & Document<unknown, unknown, InterfaceChat>;
export type TestChatMessageType = InterfaceChatMessage & Document<unknown, unknown, InterfaceChatMessage>;

If you need to handle null, then you can explicitly check for it at points of use.

const someChat: TestChatType = null; // Ensure you handle null later in the code
if (someChat === null) {
  // handle null case
} else {
  // process chat
}

}
}

for await (const userId of args.data.userIds) {
Copy link
Member

Choose a reason for hiding this comment

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

The loop uses for await to check user existence one by one. This results in sequential database queries, which can be slow. Instead of checking each user individually, perform a bulk query to check all user IDs at once. This will significantly improve performance by reducing multiple database round-trips.

const userExists = await User.exists({
  _id: { $in: args.data.userIds },
});

if (userExists.length !== args.data.userIds.length) {
  // Find which user ID(s) do not exist
  const missingUsers = args.data.userIds.filter(
    (id) => !userExists.includes(id)
  );
  throw new errors.NotFoundError(
    requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
    USER_NOT_FOUND_ERROR.CODE,
    { missingUsers }
  );
}


organization = organizationFoundInCache[0];

if (organizationFoundInCache.includes(null)) {
Copy link
Member

Choose a reason for hiding this comment

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

organizationFoundInCache.includes(null) is redundant because we’re only interested in the first organization (organizationFoundInCache[0]). It could lead to confusion if you were checking multiple organizations, but in this case, it's unnecessary. Directly check the first result from the cache.


// Checks whether user with _id === userId exists.
if (userExists === false) {
throw new errors.NotFoundError(
Copy link
Member

Choose a reason for hiding this comment

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

The error handling for the organization and user not being found could be more informative. Currently, it throws a generic error using requestContext.translate but lacks detail about which organization or user was not found. Include more detailed information in the error, such as the organizationId or userId that was not found. This helps in debugging and error tracking.

isGroup: args.data.isGroup,
creatorId: context.userId,
users: usersInChat,
organization: args.data?.organizationId,
Copy link
Member

Choose a reason for hiding this comment

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

Using args.data?.organizationId may be unnecessary because you already check whether the organizationId exists before reaching this point in the code. Since the existence of organizationId is already validated, the optional chaining can be removed, improving clarity and eliminating ambiguity.


const createdChat = await Chat.create(chatPayload);

return createdChat.toObject();
Copy link
Member

Choose a reason for hiding this comment

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

You’re calling .toObject() at the end to convert the Mongoose document into a plain JavaScript object, which may be unnecessary depending on how you intend to use the created chat. If the client or further logic does not require a plain object, you can return the document directly or use .lean() to avoid creating unnecessary copies.

throw new Error("Parent object is undefined.");
}

const usersPayload = await adminsResolver?.(parent, {}, {});
Copy link
Member

Choose a reason for hiding this comment

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

The adminsResolver?.(parent, {}, {}) uses optional chaining (?.), which is unnecessary if you're confident that the adminsResolver will always be defined. If you want to safeguard against undefined resolvers, it’s better to throw an error early on:

if (!adminsResolver) {
  throw new Error("adminsResolver is not defined.");
}
const usersPayload = await adminsResolver(parent, {}, {});

beforeAll(async () => {
MONGOOSE_INSTANCE = await connect();
const userOrgChat = await createTestChatMessage();
testChat = userOrgChat[2];
Copy link
Member

Choose a reason for hiding this comment

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

You access testChat using userOrgChat[2]. If the structure of userOrgChat changes or the function that generates it is modified, this can lead to bugs. Use destructuring to make your code more robust and readable.

const [, , testChat] = await createTestChatMessage();

});

describe("resolvers -> Chat -> admins", () => {
it(`returns user object for parent.users`, async () => {
Copy link
Member

Choose a reason for hiding this comment

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

The test description (returns user object for parent.users) might not be specific enough to describe the exact behavior under test. Update the description to reflect what exactly you are testing (e.g., that the adminsResolver is correctly resolving admin users).

it(`resolves the correct admin users for the given chat`, async () => {
  const parent = testChat?.toObject();
  if (!parent) {
    throw new Error("Parent object is undefined.");
  }

  const usersPayload = await adminsResolver(parent, {}, {});

  const users = await User.find({
    _id: { $in: testChat?.admins },
  }).lean();

  expect(usersPayload).toEqual(users);
});


const organizationPayload = await organizationResolver?.(parent, {}, {});

const organization = await Organization.findOne({
Copy link
Member

Choose a reason for hiding this comment

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

In the Organization.findOne() query, you're using $in with a single organization ID. However, $in is used for matching multiple items. Since testChat.organization should be a single ID, use direct matching instead of $in.

const organization = await Organization.findOne({
  _id: testChat?.organization,
}).lean();

});

describe("resolvers -> Chat -> organization", () => {
it(`returns user object for parent.users`, async () => {
Copy link
Member

Choose a reason for hiding this comment

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

The test description (returns user object for parent.users) is not accurate. You're testing the organization resolver, not the user resolver. Update the test description to properly reflect the behavior you're testing:

it("resolves the correct organization for the given chat", async () => {
  const parent = testChat?.toObject();
  if (!parent) {
    throw new Error("Parent object is undefined.");
  }

  const organizationPayload = await organizationResolver(parent, {}, {});

  const organization = await Organization.findOne({
    _id: testChat?.organization,
  }).lean();

  expect(organizationPayload).toEqual(organization);
});


expect(organizationPayload).toEqual(organization);
});
it(`returns user objects for parent.organization from cache`, async () => {
Copy link
Member

@DMills27 DMills27 Sep 23, 2024

Choose a reason for hiding this comment

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

The second test seems to be checking whether the organization can be resolved from the cache, but there is no logic in place to actually use or verify a cache mechanism. If you’re testing a cached organization resolution, you should mock or inject a cache service to validate whether the cache is being used. Additionally, the description and logic of the test should match:

it("resolves the organization from cache", async () => {
  const parent = testChat?.toObject();
  if (!parent) {
    throw new Error("Parent object is undefined.");
  }

  // Simulate the cache resolution logic here (if applicable)
  const organizationPayload = await organizationResolver(parent, {}, {});

  const organization = await Organization.findOne({
    _id: testChat?.organization,
  }).lean();

  expect(organizationPayload).toEqual(organization);
});

If you have a specific cache service (e.g., findOrganizationsInCache), make sure to mock its behavior and assert whether it's being called.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Outside diff range and nitpick comments (2)
src/resolvers/Mutation/createChat.ts (2)

10-10: Correct the grammatical error in the function description

The sentence "This function enables to create a chat." is grammatically incorrect. Consider rephrasing it to "This function enables the creation of a chat." or "This function allows creating a chat."


10-18: Update function documentation for accuracy

In the @remarks section, point 1 states: "1. If the user exists". Since the function checks for multiple users, consider updating it to "1. If the users exist" for clarity and accuracy.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between bf38bc6 and d42aca6.

📒 Files selected for processing (7)
  • src/models/ChatMessage.ts (1 hunks)
  • src/resolvers/Mutation/createChat.ts (1 hunks)
  • tests/helpers/chat.ts (1 hunks)
  • tests/resolvers/Chat/admins.spec.ts (1 hunks)
  • tests/resolvers/Chat/organization.spec.ts (1 hunks)
  • tests/resolvers/ChatMessage/replyTo.spec.ts (1 hunks)
  • tests/resolvers/Query/chatsByuserId.spec.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/helpers/chat.ts
  • tests/resolvers/ChatMessage/replyTo.spec.ts
🔇 Additional comments (14)
tests/resolvers/Chat/admins.spec.ts (4)

1-11: LGTM: Imports and variable declarations are well-structured.

The imports cover all necessary modules and types for the test file. The global variables testChat and MONGOOSE_INSTANCE are appropriately declared outside the test scope.


13-20: LGTM: Setup and teardown functions are well-implemented.

The beforeAll and afterAll functions correctly handle database connection and disconnection. The use of array destructuring in line 15 ([, , testChat] = await createTestChatMessage();) is a good improvement, making the code more robust and readable as suggested in a previous review.


22-42: LGTM: Test case is well-structured and addresses previous review comments.

The test case effectively validates the adminsResolver functionality. Notable improvements include:

  1. A more specific test description that clearly states the test's purpose.
  2. Proper null checks for both the parent object and the resolver, replacing the previous optional chaining.
  3. Clear comparison between the resolver's output and the expected user data from the database.

These changes address the concerns raised in previous reviews and enhance the overall quality of the test.


1-42: Great job on implementing this test file!

The admins.spec.ts file is well-structured and effectively tests the admins resolver functionality. It follows testing best practices, including proper setup and teardown procedures, clear test descriptions, and thorough assertions. The improvements made based on previous review comments have significantly enhanced the code quality and readability.

Some key strengths of this implementation:

  1. Proper database connection handling
  2. Effective use of array destructuring for test data setup
  3. Null checks to prevent potential runtime errors
  4. Clear and specific test descriptions
  5. Accurate comparison between resolver output and expected data

Keep up the good work in maintaining high-quality test coverage for the project!

tests/resolvers/Chat/organization.spec.ts (4)

2-2: LGTM: Import changes reflect the new Chat structure.

The updated imports correctly align with the shift from DirectChat to Chat in the resolver and test setup. The addition of TestChatType and createTestChatMessage imports supports the new test structure.

Also applies to: 6-8


10-10: LGTM: Variable declarations updated for new Chat structure.

The changes from 'testDirectChat' to 'testChat' and the use of 'createTestChatMessage' in the test setup are consistent with the new Chat structure. These modifications appropriately reflect the consolidation of chat types in the main codebase.

Also applies to: 15-16


23-25: LGTM: First test case updated correctly.

The changes in the first test case accurately reflect the new Chat structure:

  1. The test description now correctly describes the test's purpose.
  2. The use of 'testChat' instead of 'testDirectChat' is consistent throughout the test.
  3. The organization ID is correctly fetched from 'testChat'.

These changes align well with the consolidation of chat types in the main codebase.

Also applies to: 33-33


Line range hint 1-53: LGTM: File structure and consistency align with new Chat entity.

The overall structure and consistency of the file have been successfully updated to reflect the new Chat entity:

  1. All references to DirectChat have been replaced with Chat.
  2. Test cases have been modified to use the new Chat-related functions and variables.
  3. The flow of the tests remains logical and similar to the previous version.
  4. These changes align well with the PR objectives of consolidating DirectChat and GroupChat into a single Chat entity.

The modifications ensure that the tests now correctly focus on the Chat organization resolver, maintaining the integrity of the test suite while adapting to the new chat structure.

tests/resolvers/Query/chatsByuserId.spec.ts (1)

26-38: LGTM: Error handling test case is well-implemented.

This test case correctly checks for the "Chats not found" error when no chats exist for a given user ID. The error message has been appropriately updated to reflect the new unified chat model.

src/models/ChatMessage.ts (4)

16-16: Define 'status' as a TypeScript enum for better type safety.

Currently, status is typed as a string. Defining it using a TypeScript enum enhances type safety and ensures only valid status values are assigned.


99-99: Update logging middleware schema name.

The middleware should reference "ChatMessage" instead of "DirectChatMessage".


101-109: LGTM: Model creation and export.

The implementation for creating and exporting the ChatMessage model is well-structured. It correctly handles potential OverwriteModelError issues that could occur during testing, which is a good practice.


1-109: Overall assessment: Good implementation with minor improvements needed.

The ChatMessage model is well-structured and implements the necessary functionality. The identified issues are minor and mainly relate to documentation and naming consistency. Once these are addressed, the implementation will be robust and maintainable.

Key points:

  1. Consider using an enum for the 'status' field.
  2. Update JSDoc comments to accurately reflect the current schema.
  3. Update the logging middleware to reference "ChatMessage".

Great job on implementing this unified chat message model!

src/resolvers/Mutation/createChat.ts (1)

33-39: Include organizationId in the error message for better debugging

When throwing the NotFoundError due to a missing organization, including the organizationId that was not found can aid in debugging and error tracking. It provides clear insight into which organization ID caused the error.

Comment on lines +38 to +39
it("resolves the organization from cache", async () => {
const parent = testChat?.toObject();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Implement cache simulation for more robust testing.

The changes in the second test case are mostly good:

  1. The test description accurately reflects the purpose of testing organization resolution from cache.
  2. The use of 'testChat' is consistent with the new Chat structure.

However, there's an opportunity for improvement:

The comment on line 44 mentions simulating cache resolution logic, but no actual cache simulation is implemented. To make this test more robust and meaningful:

  1. Implement a mock cache service or use a testing library to simulate cache behavior.
  2. Verify that the cache is being used by the resolver before falling back to database queries.

Here's a suggested implementation:

it("resolves the organization from cache", async () => {
  const parent = testChat?.toObject();
  if (!parent) {
    throw new Error("Parent object is undefined.");
  }

  // Mock cache service
  const mockCacheService = {
    get: vi.fn().mockResolvedValue(null), // Simulate cache miss
    set: vi.fn(),
  };

  // Pass mock cache service to the resolver
  const organizationPayload = await organizationResolver?.(parent, {}, { cacheService: mockCacheService });

  // Verify cache was checked
  expect(mockCacheService.get).toHaveBeenCalledWith(`organization_${testChat?.organization}`);

  // Verify result
  const organization = await Organization.findOne({
    _id: testChat?.organization,
  }).lean();

  expect(organizationPayload).toEqual(organization);

  // Verify cache was updated
  expect(mockCacheService.set).toHaveBeenCalledWith(`organization_${testChat?.organization}`, organization);
});

This implementation will provide a more thorough test of the caching mechanism in the resolver.

Also applies to: 44-44, 48-48

Comment on lines +40 to +53
it(`returns list of all chats with chat.users containing the user
with _id === args.id`, async () => {
const args: QueryChatsByUserIdArgs = {
id: testUser?._id,
};

const chatsByUserIdPayload = await chatsByUserIdResolver?.({}, args, {});

const chatsByUserId = await Chat.find({
users: testUser?._id,
}).lean();

expect(chatsByUserIdPayload).toEqual(chatsByUserId);
});
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 enhancing the test case with more specific assertions.

While the current test correctly verifies that the resolver returns the expected chats, it could be more robust by checking specific properties of the returned chats. This would ensure that the structure of the returned data matches expectations.

Consider adding assertions to check specific properties of the returned chats. For example:

expect(chatsByUserIdPayload).toEqual(expect.arrayContaining([
  expect.objectContaining({
    _id: expect.any(Types.ObjectId),
    users: expect.arrayContaining([expect.any(Types.ObjectId)]),
    messages: expect.arrayContaining([expect.any(Types.ObjectId)]),
    // Add other expected properties
  })
]));

This would provide a more comprehensive test of the resolver's output structure.

Comment on lines +26 to +54
describe("resolvers -> Query -> chatsByUserId", () => {
it(`throws NotFoundError if no Chats exists with chats.users
containing user with _id === args.id`, async () => {
try {
const args: QueryChatsByUserIdArgs = {
id: new Types.ObjectId().toString(),
};

await chatsByUserIdResolver?.({}, args, {});
} catch (error: unknown) {
expect((error as Error).message).toEqual("Chats not found");
}
});

it(`returns list of all chats with chat.users containing the user
with _id === args.id`, async () => {
const args: QueryChatsByUserIdArgs = {
id: testUser?._id,
};

const chatsByUserIdPayload = await chatsByUserIdResolver?.({}, args, {});

const chatsByUserId = await Chat.find({
users: testUser?._id,
}).lean();

expect(chatsByUserIdPayload).toEqual(chatsByUserId);
});
});
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 adding more test cases for comprehensive coverage.

While the current test file covers the basic error and success scenarios, consider adding more test cases to ensure comprehensive coverage of the chatsByUserId resolver. Some suggestions:

  1. Test with a user who has no chats (different from the "not found" case).
  2. Test with a user who has multiple chats.
  3. Test with invalid user IDs (e.g., malformed ObjectId).
  4. Test pagination if applicable to this resolver.

Here's an example of an additional test case you could add:

it('returns an empty array for a user with no chats', async () => {
  const userWithNoChats = await createTestUser(); // Assume this helper exists
  const args: QueryChatsByUserIdArgs = {
    id: userWithNoChats._id,
  };

  const chatsByUserIdPayload = await chatsByUserIdResolver?.({}, args, {});

  expect(chatsByUserIdPayload).toEqual([]);
});

Adding these cases would provide more confidence in the resolver's behavior across various scenarios.

import type { TestUserType } from "../../helpers/userAndOrg";

let testUser: TestUserType;
let MONGOOSE_INSTANCE: typeof mongoose;
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 specific type for MONGOOSE_INSTANCE.

Instead of using typeof mongoose, which is quite broad, you could use a more specific type like Mongoose from the mongoose package. This would provide better type safety and autocompletion.

-let MONGOOSE_INSTANCE: typeof mongoose;
+import type { Mongoose } from 'mongoose';
+let MONGOOSE_INSTANCE: Mongoose;
📝 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
let MONGOOSE_INSTANCE: typeof mongoose;
import type { Mongoose } from 'mongoose';
let MONGOOSE_INSTANCE: Mongoose;

Comment on lines +22 to +49
/**
* ChatMessage Schema
*
* This schema defines the structure of a chat message document in the database.
*
* Fields:
* - chatMessageBelongsTo: ObjectId, ref: "Chat", required
* - The chat to which this message belongs.
* - sender: ObjectId, ref: "User", required
* - The user who sent the message.
* - replyTo: ObjectId, ref: "ChatMessage", optional
* - The message to which this message is a reply.
* - messageContent: String, required
* - The content of the message.
* - type: String, required, enum: ["STRING", "VIDEO", "IMAGE", "FILE"]
* - The type of the message content.
* - status: String, required, enum: ["ACTIVE", "BLOCKED", "DELETED"], default: "ACTIVE"
* - The status of the message.
* - deletedBy: Array of ObjectId, ref: "User", optional
* - List of users who have deleted the message.
* - updatedAt: Date, required
* - The date when the message was last updated.
* - createdAt: Date, required
* - The date when the message was created.
*
* Options:
* - timestamps: Automatically adds createdAt and updatedAt fields.
*/
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Update JSDoc comments to match the current schema.

The JSDoc comments include a description for a type field that doesn't exist in the actual schema. Additionally, some descriptions (e.g., for messageContent) still refer to "direct chat message". Please update the documentation to accurately reflect the current schema structure and field descriptions.

Comment on lines +42 to +55
const userExists = (await User.exists({
_id: { $in: args.data.userIds },
})) as unknown as string[];

if (userExists && userExists.length !== args.data.userIds.length) {
// Find which user ID(s) do not exist
const missingUsers = args.data.userIds.filter(
(id) => !userExists.includes(id),
);
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
JSON.stringify({ missingUsers: missingUsers }),
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correct the usage of User.exists() and fix the logic for user existence check

The User.exists() method returns a boolean value or null, not an array of user IDs. Casting it to string[] and using .length or .includes() is incorrect. Instead, use User.find() to retrieve existing users and compare their IDs with args.data.userIds to identify any missing users.

Apply this diff to fix the logic:

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

- if (userExists && userExists.length !== args.data.userIds.length) {
-   // Find which user ID(s) do not exist
-   const missingUsers = args.data.userIds.filter(
-     (id) => !userExists.includes(id),
-   );
-   throw new errors.NotFoundError(
-     requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
-     USER_NOT_FOUND_ERROR.CODE,
-     JSON.stringify({ missingUsers: missingUsers }),
-   );
- }

+ const existingUsers = await User.find({
+   _id: { $in: args.data.userIds },
+ }).select('_id');

+ const existingUserIds = existingUsers.map((user) => user._id.toString());

+ if (existingUserIds.length !== args.data.userIds.length) {
+   // Find which user ID(s) do not exist
+   const missingUsers = args.data.userIds.filter(
+     (id) => !existingUserIds.includes(id),
+   );
+   throw new errors.NotFoundError(
+     requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
+     USER_NOT_FOUND_ERROR.CODE,
+     JSON.stringify({ missingUsers }),
+   );
+ }
📝 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 userExists = (await User.exists({
_id: { $in: args.data.userIds },
})) as unknown as string[];
if (userExists && userExists.length !== args.data.userIds.length) {
// Find which user ID(s) do not exist
const missingUsers = args.data.userIds.filter(
(id) => !userExists.includes(id),
);
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
JSON.stringify({ missingUsers: missingUsers }),
);
const existingUsers = await User.find({
_id: { $in: args.data.userIds },
}).select('_id');
const existingUserIds = existingUsers.map((user) => user._id.toString());
if (existingUserIds.length !== args.data.userIds.length) {
// Find which user ID(s) do not exist
const missingUsers = args.data.userIds.filter(
(id) => !existingUserIds.includes(id),
);
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
JSON.stringify({ missingUsers }),
);
}

Comment on lines +25 to +40
if (args.data.isGroup && args.data.organizationId) {
const organizationFoundInCache = await findOrganizationsInCache([
args.data.organizationId,
]);

organization = organizationFoundInCache[0];

// Checks whether organization with _id === args.data.organizationId exists.
if (!organization) {
throw new errors.NotFoundError(
requestContext.translate(ORGANIZATION_NOT_FOUND_ERROR.MESSAGE),
ORGANIZATION_NOT_FOUND_ERROR.CODE,
ORGANIZATION_NOT_FOUND_ERROR.PARAM,
);
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Ensure organizationId is provided when creating a group chat

When args.data.isGroup is true, but organizationId is not provided, the code does not handle this scenario and proceeds without checking the organization's existence. This could lead to creating a group chat without an associated organization, which may not be intended. Consider adding a validation to ensure that organizationId is provided when isGroup is true, and throw a validation error if it's missing.

Apply this diff to enforce the validation:

 if (args.data.isGroup && args.data.organizationId) {
+  // Ensure organizationId is provided for group chats
+} else if (args.data.isGroup && !args.data.organizationId) {
+  throw new errors.ValidationError(
+    'Organization ID must be provided for group chats.',
+    'ORGANIZATION_ID_REQUIRED',
+  );
 }
📝 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 (args.data.isGroup && args.data.organizationId) {
const organizationFoundInCache = await findOrganizationsInCache([
args.data.organizationId,
]);
organization = organizationFoundInCache[0];
// Checks whether organization with _id === args.data.organizationId exists.
if (!organization) {
throw new errors.NotFoundError(
requestContext.translate(ORGANIZATION_NOT_FOUND_ERROR.MESSAGE),
ORGANIZATION_NOT_FOUND_ERROR.CODE,
ORGANIZATION_NOT_FOUND_ERROR.PARAM,
);
}
}
if (args.data.isGroup && args.data.organizationId) {
const organizationFoundInCache = await findOrganizationsInCache([
args.data.organizationId,
]);
organization = organizationFoundInCache[0];
// Checks whether organization with _id === args.data.organizationId exists.
if (!organization) {
throw new errors.NotFoundError(
requestContext.translate(ORGANIZATION_NOT_FOUND_ERROR.MESSAGE),
ORGANIZATION_NOT_FOUND_ERROR.CODE,
ORGANIZATION_NOT_FOUND_ERROR.PARAM,
);
}
// Ensure organizationId is provided for group chats
} else if (args.data.isGroup && !args.data.organizationId) {
throw new errors.ValidationError(
'Organization ID must be provided for group chats.',
'ORGANIZATION_ID_REQUIRED',
);
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (5)
setup.ts (5)

Line range hint 125-176: Refactor repeated environment file handling to improve maintainability

In the accessAndRefreshTokens function, the code handling environment variables for .env and .env_test is duplicated. This pattern repeats in multiple functions. Consider refactoring this logic into a helper function to avoid code duplication and enhance maintainability.


Line range hint 194-213: Ensure MongoDB client is closed properly by adding a finally block

In the wipeExistingData function, the MongoDB client is closed after the try/catch blocks. However, if an error occurs before the client.close() call, the client may remain open. To ensure the client is always closed, move the client.close() call into a finally block.

Apply this diff to fix the issue:

 export async function wipeExistingData(url: string): Promise<void> {
   const client = new MongoClient(`${url}`);
   try {
     await client.connect();
     const db = client.db();
     const collections = await db.listCollections().toArray();
 
     if (collections.length > 0) {
       for (const collection of collections) {
         await db.collection(collection.name).deleteMany({});
       }
       console.log("All existing data has been deleted.");
     }
   } catch {
     console.error("Could not connect to database to check for data");
+  } finally {
+    client.close();
   }
 }

Line range hint 218-234: Ensure MongoDB client is closed properly by adding a finally block

In the checkDb function, the MongoDB client should be closed in a finally block to ensure it is always closed, even if an error occurs during the database operation.

Apply this diff to fix the issue:

 export async function checkDb(url: string): Promise<boolean> {
   let dbEmpty = false;
   const client = new MongoClient(`${url}`);
   try {
     await client.connect();
     const db = client.db();
     const collections = await db.listCollections().toArray();
     if (collections.length > 0) {
       console.log("Existing data found in the database");
       dbEmpty = false;
     } else {
       dbEmpty = true;
     }
   } catch {
     console.error("Could not connect to database to check for data");
+  } finally {
+    client.close();
   }
   return dbEmpty;
 }

Line range hint 717-726: Add validation before deleting the MinIO data directory to prevent accidental data loss

When removing the existing MinIO data directory, the code does not validate currentDataDir. This could lead to accidental deletion of unintended directories if currentDataDir is incorrectly set. Please add validation to ensure that currentDataDir is within the expected directory.

Apply this diff to add validation:

 if (changeDataDir && currentDataDir) {
+  // Validate that currentDataDir is within the project directory
+  const absoluteDataDir = path.resolve(currentDataDir);
+  const projectRoot = process.cwd();
+  if (!absoluteDataDir.startsWith(projectRoot)) {
+    console.error(`[MINIO] Invalid data directory path: ${currentDataDir}`);
+    return;
+  }
   try {
     fs.rmSync(currentDataDir, { recursive: true, force: true });
     console.log(
       `[MINIO] Removed existing data directory: ${currentDataDir}`,
     );
   } catch (err) {
     console.error(`[MINIO] Error removing existing data directory: ${err}`);
   }
 }

Line range hint 125-176: Consolidate environment configuration logic to eliminate duplication

The environment variable handling for .env and .env_test is duplicated across multiple functions (accessAndRefreshTokens, setNodeEnvironment, redisConfiguration, superAdmin, twoFactorAuth). Consider creating a utility function to read and update the correct environment file based on process.env.NODE_ENV to improve maintainability.

Also applies to: 364-393, 439-466, 494-521

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Files that changed from the base of the PR and between d42aca6 and 4124a3a.

📒 Files selected for processing (1)
  • setup.ts (1 hunks)
🧰 Additional context used

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants