diff --git a/src/resolvers/Mutation/addEventAttendee.ts b/src/resolvers/Mutation/addEventAttendee.ts index 7cf5a632e6..955eb3da69 100644 --- a/src/resolvers/Mutation/addEventAttendee.ts +++ b/src/resolvers/Mutation/addEventAttendee.ts @@ -6,7 +6,11 @@ import { } from "../../constants"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; import { User, Event, EventAttendee } from "../../models"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +import { Types } from "mongoose"; export const addEventAttendee: MutationResolvers["addEventAttendee"] = async ( _parent, @@ -25,11 +29,23 @@ export const addEventAttendee: MutationResolvers["addEventAttendee"] = async ( ); } - const currentEvent = await Event.findOne({ - _id: args.data.eventId, - }).lean(); + let event: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.data.eventId]); + + event = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + event = await Event.findOne({ + _id: args.data.eventId, + }).lean(); + + if (event !== null) { + await cacheEvents([event]); + } + } - if (currentEvent === null) { + if (event === null) { throw new errors.NotFoundError( requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), EVENT_NOT_FOUND_ERROR.CODE, @@ -37,8 +53,9 @@ export const addEventAttendee: MutationResolvers["addEventAttendee"] = async ( ); } - const isUserEventAdmin = currentEvent.admins.some( - (admin) => admin.toString() === context.userId.toString() + const isUserEventAdmin = event.admins.some( + (admin) => + admin === context.userID || Types.ObjectId(admin).equals(context.userId) ); if (!isUserEventAdmin && currentUser.userType !== "SUPERADMIN") { diff --git a/src/resolvers/Mutation/adminRemoveEvent.ts b/src/resolvers/Mutation/adminRemoveEvent.ts index ca7e2471cf..a2b17f472f 100644 --- a/src/resolvers/Mutation/adminRemoveEvent.ts +++ b/src/resolvers/Mutation/adminRemoveEvent.ts @@ -9,6 +9,9 @@ import { } from "../../constants"; import { findOrganizationsInCache } from "../../services/OrganizationCache/findOrganizationsInCache"; import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +import { deleteEventFromCache } from "../../services/EventCache/deleteEventFromCache"; /** * This function enables an admin to remove a event * @param _parent - parent of current request @@ -26,9 +29,21 @@ export const adminRemoveEvent: MutationResolvers["adminRemoveEvent"] = async ( args, context ) => { - const event = await Event.findOne({ - _id: args.eventId, - }).lean(); + let event; + + const eventFoundInCache = await findEventsInCache([args.eventId]); + + event = eventFoundInCache[0]; + + if (event === null) { + event = await Event.findOne({ + _id: args.eventId, + }).lean(); + + if (event !== null) { + await cacheEvents([event]); + } + } // Checks whether event exists. if (!event) { @@ -101,6 +116,8 @@ export const adminRemoveEvent: MutationResolvers["adminRemoveEvent"] = async ( _id: event._id, }); + await deleteEventFromCache(event._id); + // Returns the deleted event. return event; }; diff --git a/src/resolvers/Mutation/checkIn.ts b/src/resolvers/Mutation/checkIn.ts index ba9ec8f47b..662d8cb704 100644 --- a/src/resolvers/Mutation/checkIn.ts +++ b/src/resolvers/Mutation/checkIn.ts @@ -7,7 +7,11 @@ import { } from "../../constants"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; import { User, Event, EventAttendee, CheckIn } from "../../models"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +import { Types } from "mongoose"; export const checkIn: MutationResolvers["checkIn"] = async ( _parent, @@ -26,9 +30,21 @@ export const checkIn: MutationResolvers["checkIn"] = async ( ); } - const currentEvent = await Event.findOne({ - _id: args.data.eventId, - }).lean(); + let currentEvent: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.data.eventId]); + + currentEvent = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + currentEvent = await Event.findOne({ + _id: args.data.eventId, + }).lean(); + + if (currentEvent !== null) { + await cacheEvents([currentEvent]); + } + } if (currentEvent === null) { throw new errors.NotFoundError( @@ -39,7 +55,8 @@ export const checkIn: MutationResolvers["checkIn"] = async ( } const isUserEventAdmin = currentEvent.admins.some( - (admin) => admin.toString() === context.userId.toString() + (admin) => + admin === context.userID || Types.ObjectId(admin).equals(context.userId) ); if (!isUserEventAdmin && currentUser.userType !== "SUPERADMIN") { diff --git a/src/resolvers/Mutation/createComment.ts b/src/resolvers/Mutation/createComment.ts index 23986099dd..eb8837ee96 100644 --- a/src/resolvers/Mutation/createComment.ts +++ b/src/resolvers/Mutation/createComment.ts @@ -2,6 +2,8 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { User, Post, Comment } from "../../models"; import { errors, requestContext } from "../../libraries"; import { POST_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants"; +import { cacheComments } from "../../services/CommentCache/cacheComments"; +import { cachePosts } from "../../services/PostCache/cachePosts"; /** * This function enables to create comment. @@ -50,8 +52,10 @@ export const createComment: MutationResolvers["createComment"] = async ( postId: args.postId, }); + await cacheComments([createdComment]); + // Increase commentCount by 1 on post's document with _id === args.postId. - await Post.updateOne( + const updatedPost = await Post.findOneAndUpdate( { _id: args.postId, }, @@ -59,9 +63,16 @@ export const createComment: MutationResolvers["createComment"] = async ( $inc: { commentCount: 1, }, + }, + { + new: true, } ); + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } + // Returns the createdComment. return createdComment.toObject(); }; diff --git a/src/resolvers/Mutation/createEvent.ts b/src/resolvers/Mutation/createEvent.ts index 30fdb2b3f6..e7ad3dd813 100644 --- a/src/resolvers/Mutation/createEvent.ts +++ b/src/resolvers/Mutation/createEvent.ts @@ -12,6 +12,7 @@ import { getApps } from "firebase-admin/app"; import { isValidString } from "../../libraries/validators/validateString"; import { compareDates } from "../../libraries/validators/compareDates"; import { EventAttendee } from "../../models/EventAttendee"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; const applicationDefault = credential.applicationDefault; @@ -133,6 +134,10 @@ export const createEvent: MutationResolvers["createEvent"] = async ( organization: organization._id, }); + if (createdEvent !== null) { + await cacheEvents([createdEvent]); + } + await EventAttendee.create({ userId: currentUser._id.toString(), eventId: createdEvent._id, diff --git a/src/resolvers/Mutation/createEventProject.ts b/src/resolvers/Mutation/createEventProject.ts index a3ed4e1351..3277654ce9 100644 --- a/src/resolvers/Mutation/createEventProject.ts +++ b/src/resolvers/Mutation/createEventProject.ts @@ -1,5 +1,5 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; -import type { InterfaceEventProject } from "../../models"; +import type { InterfaceEvent, InterfaceEventProject } from "../../models"; import { User, EventProject, Event } from "../../models"; import { errors, requestContext } from "../../libraries"; import { @@ -7,6 +7,9 @@ import { USER_NOT_AUTHORIZED_ERROR, EVENT_NOT_FOUND_ERROR, } from "../../constants"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +import { Types } from "mongoose"; /** * This function enables to create an event project. @@ -35,9 +38,21 @@ export const createEventProject: MutationResolvers["createEventProject"] = ); } - const event = await Event.findOne({ - _id: args.data.eventId, - }).lean(); + let event: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.data.eventId]); + + event = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + event = await Event.findOne({ + _id: args.data.eventId, + }).lean(); + + if (event !== null) { + await cacheEvents([event]); + } + } // Checks whether event exists. if (!event) { @@ -48,8 +63,9 @@ export const createEventProject: MutationResolvers["createEventProject"] = ); } - const currentUserIsEventAdmin = event.admins.some((admin) => - admin.equals(context.userId) + const currentUserIsEventAdmin = event.admins.some( + (admin) => + admin === context.userID || Types.ObjectId(admin).equals(context.userId) ); // Checks whether currentUser with _id === context.userId is an admin of event. diff --git a/src/resolvers/Mutation/createPost.ts b/src/resolvers/Mutation/createPost.ts index fee87dcc96..9a56958aa7 100644 --- a/src/resolvers/Mutation/createPost.ts +++ b/src/resolvers/Mutation/createPost.ts @@ -12,7 +12,7 @@ import { uploadEncodedImage } from "../../utilities/encodedImageStorage/uploadEn import { uploadEncodedVideo } from "../../utilities/encodedVideoStorage/uploadEncodedVideo"; import { findOrganizationsInCache } from "../../services/OrganizationCache/findOrganizationsInCache"; import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations"; - +import { cachePosts } from "../../services/PostCache/cachePosts"; /** * This function enables to create a post. * @param _parent - parent of current request @@ -132,6 +132,10 @@ export const createPost: MutationResolvers["createPost"] = async ( videoUrl: uploadVideoFileName, }); + if (createdPost !== null) { + await cachePosts([createdPost]); + } + if (args.data.pinned) { // Add the post to pinnedPosts of the organization const updatedOrganizaiton = await Organization.findOneAndUpdate( diff --git a/src/resolvers/Mutation/leaveOrganization.ts b/src/resolvers/Mutation/leaveOrganization.ts index 879a766f1d..d8b72ba98e 100644 --- a/src/resolvers/Mutation/leaveOrganization.ts +++ b/src/resolvers/Mutation/leaveOrganization.ts @@ -64,12 +64,9 @@ export const leaveOrganization: MutationResolvers["leaveOrganization"] = async ( ); } - const currentUserIsOrganizationMember = - organization.members.length !== 0 - ? organization.members.some((member) => - Types.ObjectId(member).equals(currentUser?._id) - ) - : false; + const currentUserIsOrganizationMember = organization.members.some((member) => + Types.ObjectId(member).equals(currentUser?._id) + ); // Checks whether currentUser is not a member of organzation. if (!currentUserIsOrganizationMember) { diff --git a/src/resolvers/Mutation/likeComment.ts b/src/resolvers/Mutation/likeComment.ts index a8eb1a1b0b..8a4513bc76 100644 --- a/src/resolvers/Mutation/likeComment.ts +++ b/src/resolvers/Mutation/likeComment.ts @@ -2,6 +2,8 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { User, Comment } from "../../models"; import { errors, requestContext } from "../../libraries"; import { COMMENT_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants"; +import { findCommentsInCache } from "../../services/CommentCache/findCommentsInCache"; +import { cacheComments } from "../../services/CommentCache/cacheComments"; /** * This function enables to like a post. * @param _parent - parent of current request @@ -31,9 +33,21 @@ export const likeComment: MutationResolvers["likeComment"] = async ( ); } - const comment = await Comment.findOne({ - _id: args.id, - }).lean(); + let comment; + + const commentsFoundInCache = await findCommentsInCache([args.id]); + + comment = commentsFoundInCache[0]; + + if (commentsFoundInCache.includes(null)) { + comment = await Comment.findOne({ + _id: args.id, + }).lean(); + + if (comment !== null) { + await cacheComments([comment]); + } + } // Checks whether comment exists. if (!comment) { @@ -54,7 +68,7 @@ export const likeComment: MutationResolvers["likeComment"] = async ( Adds context.userId to likedBy list and increases likeCount field by 1 of comment's document and returns the updated comment. */ - return await Comment.findOneAndUpdate( + const updatedComment = await Comment.findOneAndUpdate( { _id: comment._id, }, @@ -70,6 +84,12 @@ export const likeComment: MutationResolvers["likeComment"] = async ( new: true, } ).lean(); + + if (updatedComment !== null) { + await cacheComments([updatedComment]); + } + + return updatedComment; } // Returns the comment without liking. diff --git a/src/resolvers/Mutation/likePost.ts b/src/resolvers/Mutation/likePost.ts index dc9aa1a2ea..70f9338d78 100644 --- a/src/resolvers/Mutation/likePost.ts +++ b/src/resolvers/Mutation/likePost.ts @@ -2,6 +2,8 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { User, Post } from "../../models"; import { errors, requestContext } from "../../libraries"; import { POST_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants"; +import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; +import { cachePosts } from "../../services/PostCache/cachePosts"; /** * This function enables to like a post. * @param _parent - parent of current request @@ -31,9 +33,21 @@ export const likePost: MutationResolvers["likePost"] = async ( ); } - const post = await Post.findOne({ - _id: args.id, - }).lean(); + let post; + + const postFoundInCache = await findPostsInCache([args.id]); + + post = postFoundInCache[0]; + + if (postFoundInCache.includes(null)) { + post = await Post.findOne({ + _id: args.id, + }).lean(); + + if (post !== null) { + await cachePosts([post]); + } + } // Checks whether post exists. if (!post) { @@ -54,7 +68,7 @@ export const likePost: MutationResolvers["likePost"] = async ( Adds context.userId to likedBy list and increases likeCount field by 1 of post's document and returns the updated post. */ - return await Post.findOneAndUpdate( + const updatedPost = await Post.findOneAndUpdate( { _id: args.id, }, @@ -70,6 +84,12 @@ export const likePost: MutationResolvers["likePost"] = async ( new: true, } ).lean(); + + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } + + return updatedPost; } // Returns the post without liking. diff --git a/src/resolvers/Mutation/registerForEvent.ts b/src/resolvers/Mutation/registerForEvent.ts index fecfcd3a92..4efc08a08c 100644 --- a/src/resolvers/Mutation/registerForEvent.ts +++ b/src/resolvers/Mutation/registerForEvent.ts @@ -1,11 +1,14 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; import { User, Event, EventAttendee } from "../../models"; import { USER_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, REGISTRANT_ALREADY_EXIST_ERROR, } from "../../constants"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; /** * This function enables to register for event. @@ -37,9 +40,21 @@ export const registerForEvent: MutationResolvers["registerForEvent"] = async ( ); } - const event = await Event.findOne({ - _id: args.id, - }).lean(); + let event: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.id]); + + event = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + event = await Event.findOne({ + _id: args.id, + }).lean(); + + if (event !== null) { + await cacheEvents([event]); + } + } // Checks whether event exists. if (!event) { diff --git a/src/resolvers/Mutation/removeComment.ts b/src/resolvers/Mutation/removeComment.ts index f703db3032..38ef0f8b10 100644 --- a/src/resolvers/Mutation/removeComment.ts +++ b/src/resolvers/Mutation/removeComment.ts @@ -1,4 +1,5 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import type { InterfaceComment } from "../../models"; import { User, Post, Comment } from "../../models"; import { errors, requestContext } from "../../libraries"; import { @@ -6,6 +7,9 @@ import { COMMENT_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, } from "../../constants"; +import { findCommentsInCache } from "../../services/CommentCache/findCommentsInCache"; +import { deleteCommentFromCache } from "../../services/CommentCache/deleteCommentFromCache"; +import { cachePosts } from "../../services/PostCache/cachePosts"; /** * This function enables to remove a comment. @@ -37,11 +41,19 @@ export const removeComment: MutationResolvers["removeComment"] = async ( ); } - const comment = await Comment.findOne({ - _id: args.id, - }) - .populate("postId") - .lean(); + let comment: InterfaceComment; + + const commentsFoundInCache = await findCommentsInCache([args.id]); + + if (commentsFoundInCache[0] == null) { + comment = await Comment.findOne({ + _id: args.id, + }) + .populate("postId") + .lean(); + } else { + comment = commentsFoundInCache[0]; + } // Checks whether comment exists. if (!comment) { @@ -70,7 +82,8 @@ export const removeComment: MutationResolvers["removeComment"] = async ( } // Reduce the commentCount by 1 of the post with _id === commentPost.postId - await Post.updateOne( + + const updatedPost = await Post.findOneAndUpdate( { _id: comment!.postId._id, }, @@ -78,14 +91,23 @@ export const removeComment: MutationResolvers["removeComment"] = async ( $inc: { commentCount: -1, }, + }, + { + new: true, } - ); + ).lean(); + + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } // Deletes the comment await Comment.deleteOne({ _id: comment._id, }); + await deleteCommentFromCache(comment); + // Replace the populated postId in comment object with just the id comment.postId = comment.postId._id; diff --git a/src/resolvers/Mutation/removeEvent.ts b/src/resolvers/Mutation/removeEvent.ts index 26d4d2c732..0d97473d19 100644 --- a/src/resolvers/Mutation/removeEvent.ts +++ b/src/resolvers/Mutation/removeEvent.ts @@ -1,11 +1,14 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; import { User, Event, EventProject, Task, TaskVolunteer } from "../../models"; import { USER_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, } from "../../constants"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; /** * This function enables to remove an event. * @param _parent - parent of current request @@ -36,9 +39,21 @@ export const removeEvent: MutationResolvers["removeEvent"] = async ( ); } - const event = await Event.findOne({ - _id: args.id, - }).lean(); + let event: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.id]); + + event = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + event = await Event.findOne({ + _id: args.id, + }).lean(); + + if (event !== null) { + await cacheEvents([event]); + } + } // Checks whether event exists. if (!event) { @@ -51,7 +66,7 @@ export const removeEvent: MutationResolvers["removeEvent"] = async ( // Boolean to determine whether user is an admin of organization. const currentUserIsOrganizationAdmin = currentUser.adminFor.some( - (organization) => organization.equals(event.organization) + (organization) => organization.equals(event?.organization) ); // Boolean to determine whether user is an admin of event. @@ -96,15 +111,22 @@ export const removeEvent: MutationResolvers["removeEvent"] = async ( } ); - await Event.updateOne( + const updatedEvent = await Event.findOneAndUpdate( { _id: event._id, }, { status: "DELETED", + }, + { + new: true, } ); + if (updatedEvent !== null) { + await cacheEvents([updatedEvent]); + } + // Fetch and delete all the event projects under the particular event const eventProjects = await EventProject.find( { diff --git a/src/resolvers/Mutation/removeEventAttendee.ts b/src/resolvers/Mutation/removeEventAttendee.ts index 4fc2b726c5..05e67ad2ab 100644 --- a/src/resolvers/Mutation/removeEventAttendee.ts +++ b/src/resolvers/Mutation/removeEventAttendee.ts @@ -6,7 +6,10 @@ import { } from "../../constants"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; import { User, Event, EventAttendee } from "../../models"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; export const removeEventAttendee: MutationResolvers["removeEventAttendee"] = async (_parent, args, context) => { @@ -22,9 +25,21 @@ export const removeEventAttendee: MutationResolvers["removeEventAttendee"] = ); } - const currentEvent = await Event.findOne({ - _id: args.data.eventId, - }).lean(); + let currentEvent: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.data.eventId]); + + currentEvent = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + currentEvent = await Event.findOne({ + _id: args.data.eventId, + }).lean(); + + if (currentEvent !== null) { + await cacheEvents([currentEvent]); + } + } if (currentEvent === null) { throw new errors.NotFoundError( diff --git a/src/resolvers/Mutation/removePost.ts b/src/resolvers/Mutation/removePost.ts index 8cb6619c67..7970d4c0c0 100644 --- a/src/resolvers/Mutation/removePost.ts +++ b/src/resolvers/Mutation/removePost.ts @@ -1,5 +1,6 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfacePost } from "../../models"; import { User, Post, Organization } from "../../models"; import { USER_NOT_FOUND_ERROR, @@ -7,6 +8,9 @@ import { USER_NOT_AUTHORIZED_ERROR, } from "../../constants"; import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations"; +import { deletePostFromCache } from "../../services/PostCache/deletePostFromCache"; +import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; +import { cachePosts } from "../../services/PostCache/cachePosts"; /** * This function enables to remove a post. * @param _parent - parent of current request @@ -38,9 +42,20 @@ export const removePost: MutationResolvers["removePost"] = async ( ); } - const post = await Post.findOne({ - _id: args.id, - }).lean(); + let post: InterfacePost | null; + + const postFoundInCache = await findPostsInCache([args.id]); + + post = postFoundInCache[0]; + + if (postFoundInCache[0] === null) { + post = await Post.findOne({ + _id: args.id, + }).lean(); + if (post !== null) { + await cachePosts([post]); + } + } // Checks whether post exists. if (!post) { @@ -55,7 +70,7 @@ export const removePost: MutationResolvers["removePost"] = async ( const isCreator = post.creator.equals(context.userId); const isSuperAdmin = currentUser?.userType === "SUPERADMIN"; const isAdminOfPostOrganization = currentUser?.adminFor.some((orgID) => - orgID.equals(post.organization) + orgID.equals(post?.organization) ); if (!isCreator && !isSuperAdmin && !isAdminOfPostOrganization) { @@ -71,6 +86,8 @@ export const removePost: MutationResolvers["removePost"] = async ( _id: args.id, }); + await deletePostFromCache(args.id); + // Removes the post from the organization, doesn't fail if the post wasn't pinned const updatedOrganization = await Organization.findOneAndUpdate( { diff --git a/src/resolvers/Mutation/togglePostPin.ts b/src/resolvers/Mutation/togglePostPin.ts index fd31cce485..7c132dfef1 100644 --- a/src/resolvers/Mutation/togglePostPin.ts +++ b/src/resolvers/Mutation/togglePostPin.ts @@ -5,10 +5,13 @@ import { } from "../../constants"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfacePost } from "../../models"; import { User, Post, Organization } from "../../models"; import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations"; import { findOrganizationsInCache } from "../../services/OrganizationCache/findOrganizationsInCache"; import { Types } from "mongoose"; +import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; +import { cachePosts } from "../../services/PostCache/cachePosts"; export const togglePostPin: MutationResolvers["togglePostPin"] = async ( _parent, @@ -30,9 +33,20 @@ export const togglePostPin: MutationResolvers["togglePostPin"] = async ( } // Check if the post object exists - const post = await Post.findOne({ - _id: args.id, - }).lean(); + let post: InterfacePost | null; + + const postFoundInCache = await findPostsInCache([args.id]); + + post = postFoundInCache[0]; + + if (postFoundInCache[0] === null) { + post = await Post.findOne({ + _id: args.id, + }).lean(); + if (post !== null) { + await cachePosts([post]); + } + } if (!post) { throw new errors.NotFoundError( @@ -97,7 +111,7 @@ export const togglePostPin: MutationResolvers["togglePostPin"] = async ( await cacheOrganizations([updatedOrganization]); } - return await Post.findOneAndUpdate( + const updatedPost = await Post.findOneAndUpdate( { _id: args.id, }, @@ -107,6 +121,12 @@ export const togglePostPin: MutationResolvers["togglePostPin"] = async ( }, } ).lean(); + + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } + + return updatedPost!; } else { const updatedOrganization = await Organization.findOneAndUpdate( { @@ -125,7 +145,7 @@ export const togglePostPin: MutationResolvers["togglePostPin"] = async ( if (updatedOrganization !== null) { await cacheOrganizations([updatedOrganization]); } - return await Post.findOneAndUpdate( + const updatedPost = await Post.findOneAndUpdate( { _id: args.id, }, @@ -135,5 +155,11 @@ export const togglePostPin: MutationResolvers["togglePostPin"] = async ( }, } ).lean(); + + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } + + return updatedPost!; } }; diff --git a/src/resolvers/Mutation/unlikeComment.ts b/src/resolvers/Mutation/unlikeComment.ts index 34a3667c3d..20d7b4ca59 100644 --- a/src/resolvers/Mutation/unlikeComment.ts +++ b/src/resolvers/Mutation/unlikeComment.ts @@ -2,6 +2,8 @@ import { COMMENT_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; import { User, Comment } from "../../models"; +import { findCommentsInCache } from "../../services/CommentCache/findCommentsInCache"; +import { cacheComments } from "../../services/CommentCache/cacheComments"; /** * This function enables to unlike a comment. * @param _parent - parent of current request @@ -29,9 +31,21 @@ export const unlikeComment: MutationResolvers["unlikeComment"] = async ( ); } - const comment = await Comment.findOne({ - _id: args.id, - }).lean(); + let comment; + + const commentsFoundInCache = await findCommentsInCache([args.id]); + + comment = commentsFoundInCache[0]; + + if (commentsFoundInCache.includes(null)) { + comment = await Comment.findOne({ + _id: args.id, + }).lean(); + + if (comment !== null) { + await cacheComments([comment]); + } + } if (!comment) { throw new errors.NotFoundError( @@ -46,7 +60,7 @@ export const unlikeComment: MutationResolvers["unlikeComment"] = async ( ); if (currentUserHasLikedComment === true) { - return await Comment.findOneAndUpdate( + const updatedComment = await Comment.findOneAndUpdate( { _id: args.id, }, @@ -62,6 +76,12 @@ export const unlikeComment: MutationResolvers["unlikeComment"] = async ( new: true, } ).lean(); + + if (updatedComment !== null) { + await cacheComments([updatedComment]); + } + + return updatedComment; } return comment; diff --git a/src/resolvers/Mutation/unlikePost.ts b/src/resolvers/Mutation/unlikePost.ts index 5d738d01b4..3dd7791023 100644 --- a/src/resolvers/Mutation/unlikePost.ts +++ b/src/resolvers/Mutation/unlikePost.ts @@ -1,7 +1,10 @@ import { POST_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfacePost } from "../../models"; import { User, Post } from "../../models"; +import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; +import { cachePosts } from "../../services/PostCache/cachePosts"; /** * This function enables to unlike a post. * @param _parent - parent of current request @@ -29,9 +32,20 @@ export const unlikePost: MutationResolvers["unlikePost"] = async ( ); } - const post = await Post.findOne({ - _id: args.id, - }).lean(); + let post: InterfacePost | null; + + const postFoundInCache = await findPostsInCache([args.id]); + + post = postFoundInCache[0]; + + if (postFoundInCache[0] === null) { + post = await Post.findOne({ + _id: args.id, + }).lean(); + if (post !== null) { + await cachePosts([post]); + } + } if (!post) { throw new errors.NotFoundError( @@ -46,7 +60,7 @@ export const unlikePost: MutationResolvers["unlikePost"] = async ( ); if (currentUserHasLikedPost === true) { - return await Post.findOneAndUpdate( + const updatedPost = await Post.findOneAndUpdate( { _id: post._id, }, @@ -62,6 +76,12 @@ export const unlikePost: MutationResolvers["unlikePost"] = async ( new: true, } ).lean(); + + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } + + return updatedPost!; } return post; diff --git a/src/resolvers/Mutation/unregisterForEventByUser.ts b/src/resolvers/Mutation/unregisterForEventByUser.ts index 007fdaaf13..22c3cd5aab 100644 --- a/src/resolvers/Mutation/unregisterForEventByUser.ts +++ b/src/resolvers/Mutation/unregisterForEventByUser.ts @@ -1,11 +1,14 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; import { User, Event, EventAttendee } from "../../models"; import { USER_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, USER_ALREADY_UNREGISTERED_ERROR, } from "../../constants"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; /** * This function enables a user to unregister from an event. @@ -34,10 +37,21 @@ export const unregisterForEventByUser: MutationResolvers["unregisterForEventByUs ); } - const event = await Event.findOne({ - _id: args.id, - }).lean(); + let event: InterfaceEvent | null; + const eventFoundInCache = await findEventsInCache([args.id]); + + event = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + event = await Event.findOne({ + _id: args.id, + }).lean(); + + if (event !== null) { + await cacheEvents([event]); + } + } // checks if there exists an event with _id === args.id if (!event) { throw new errors.NotFoundError( diff --git a/src/resolvers/Mutation/updateEvent.ts b/src/resolvers/Mutation/updateEvent.ts index 31233ccce0..a33a38a740 100644 --- a/src/resolvers/Mutation/updateEvent.ts +++ b/src/resolvers/Mutation/updateEvent.ts @@ -1,5 +1,6 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; import { User, Event } from "../../models"; import { USER_NOT_FOUND_ERROR, @@ -8,6 +9,9 @@ import { LENGTH_VALIDATION_ERROR, } from "../../constants"; import { isValidString } from "../../libraries/validators/validateString"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +import { Types } from "mongoose"; /** * This function enables to update an event. * @param _parent - parent of current request @@ -37,9 +41,21 @@ export const updateEvent: MutationResolvers["updateEvent"] = async ( ); } - const event = await Event.findOne({ - _id: args.id, - }).lean(); + let event: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.id]); + + event = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + event = await Event.findOne({ + _id: args.id, + }).lean(); + + if (event !== null) { + await cacheEvents([event]); + } + } // checks if there exists an event with _id === args.id if (!event) { @@ -50,8 +66,9 @@ export const updateEvent: MutationResolvers["updateEvent"] = async ( ); } - const currentUserIsEventAdmin = event.admins.some((admin) => - admin.equals(context.userId) + const currentUserIsEventAdmin = event.admins.some( + (admin) => + admin === context.userID || Types.ObjectId(admin).equals(context.userId) ); // checks if current user is an admin of the event with _id === args.id @@ -98,7 +115,7 @@ export const updateEvent: MutationResolvers["updateEvent"] = async ( ); } - return await Event.findOneAndUpdate( + const updatedEvent = await Event.findOneAndUpdate( { _id: args.id, }, @@ -109,4 +126,10 @@ export const updateEvent: MutationResolvers["updateEvent"] = async ( new: true, } ).lean(); + + if (updatedEvent !== null) { + await cacheEvents([updatedEvent]); + } + + return updatedEvent!; }; diff --git a/src/resolvers/Mutation/updatePost.ts b/src/resolvers/Mutation/updatePost.ts index 0585a228e8..a35c463f4f 100644 --- a/src/resolvers/Mutation/updatePost.ts +++ b/src/resolvers/Mutation/updatePost.ts @@ -1,5 +1,6 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; +import type { InterfacePost } from "../../models"; import { User, Post } from "../../models"; import { USER_NOT_FOUND_ERROR, @@ -8,6 +9,8 @@ import { LENGTH_VALIDATION_ERROR, } from "../../constants"; import { isValidString } from "../../libraries/validators/validateString"; +import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; +import { cachePosts } from "../../services/PostCache/cachePosts"; import { uploadEncodedImage } from "../../utilities/encodedImageStorage/uploadEncodedImage"; import { uploadEncodedVideo } from "../../utilities/encodedVideoStorage/uploadEncodedVideo"; @@ -29,9 +32,20 @@ export const updatePost: MutationResolvers["updatePost"] = async ( ); } - const post = await Post.findOne({ - _id: args.id, - }).lean(); + let post: InterfacePost | null; + + const postFoundInCache = await findPostsInCache([args.id]); + + post = postFoundInCache[0]; + + if (postFoundInCache[0] === null) { + post = await Post.findOne({ + _id: args.id, + }).lean(); + if (post !== null) { + await cachePosts([post]); + } + } // checks if there exists a post with _id === args.id if (!post) { @@ -87,7 +101,7 @@ export const updatePost: MutationResolvers["updatePost"] = async ( ); } - return await Post.findOneAndUpdate( + const updatedPost = await Post.findOneAndUpdate( { _id: args.id, }, @@ -98,4 +112,10 @@ export const updatePost: MutationResolvers["updatePost"] = async ( new: true, } ).lean(); + + if (updatedPost !== null) { + await cachePosts([updatedPost]); + } + + return updatedPost!; }; diff --git a/src/resolvers/Organization/pinnedPosts.ts b/src/resolvers/Organization/pinnedPosts.ts index 7492214fda..a26548f2fd 100644 --- a/src/resolvers/Organization/pinnedPosts.ts +++ b/src/resolvers/Organization/pinnedPosts.ts @@ -1,12 +1,26 @@ import { Post } from "../../models"; +import { cachePosts } from "../../services/PostCache/cachePosts"; +import { findPostsInCache } from "../../services/PostCache/findPostsInCache"; import type { OrganizationResolvers } from "../../types/generatedGraphQLTypes"; export const pinnedPosts: OrganizationResolvers["pinnedPosts"] = async ( parent ) => { - return await Post.find({ + const postsInCache = await findPostsInCache(parent.pinnedPosts); + + if (!postsInCache.includes(null)) { + return postsInCache; + } + + const posts = await Post.find({ _id: { $in: parent.pinnedPosts, }, }).lean(); + + if (posts !== null) { + await cachePosts(posts); + } + + return posts; }; diff --git a/src/resolvers/Post/comments.ts b/src/resolvers/Post/comments.ts index 4e0af9f52c..2fa0abd431 100644 --- a/src/resolvers/Post/comments.ts +++ b/src/resolvers/Post/comments.ts @@ -1,8 +1,23 @@ import type { PostResolvers } from "../../types/generatedGraphQLTypes"; import { Comment } from "../../models"; +import { cacheComments } from "../../services/CommentCache/cacheComments"; +import { findCommentsByPostIdInCache } from "../../services/CommentCache/findCommentsByPostIdInCache"; export const comments: PostResolvers["comments"] = async (parent) => { - return await Comment.find({ + const commentsInCache = await findCommentsByPostIdInCache(parent._id); + + if ( + !commentsInCache.includes(null) && + commentsInCache.length === parent.commentCount + ) { + return commentsInCache; + } + + const comment = await Comment.find({ postId: parent._id, }).lean(); + + cacheComments(comment); + + return comment; }; diff --git a/src/services/CommentCache/cacheComments.ts b/src/services/CommentCache/cacheComments.ts new file mode 100644 index 0000000000..36021dffdc --- /dev/null +++ b/src/services/CommentCache/cacheComments.ts @@ -0,0 +1,31 @@ +import { logger } from "../../libraries"; +import type { InterfaceComment } from "../../models"; +import CommentCache from "../redisCache"; + +// Function to store comments in the cache using pipelining +export async function cacheComments( + comments: InterfaceComment[] +): Promise { + try { + const pipeline = CommentCache.pipeline(); + + comments.forEach((comment) => { + if (comment !== null) { + const key = `comment:${comment._id}`; + const postID = `post_comments:${comment.postId}`; + // Set the comment in the cache + pipeline.set(key, JSON.stringify(comment)); + // Index comment on its postId + pipeline.hset(postID, key, "null"); + // SET the time to live for each of the organization in the cache to 300s. + pipeline.expire(key, 300); + pipeline.expire(postID, 300); + } + }); + + // Execute the pipeline + await pipeline.exec(); + } catch (error) { + logger.info(error); + } +} diff --git a/src/services/CommentCache/deleteCommentFromCache.ts b/src/services/CommentCache/deleteCommentFromCache.ts new file mode 100644 index 0000000000..f249ac9ba4 --- /dev/null +++ b/src/services/CommentCache/deleteCommentFromCache.ts @@ -0,0 +1,10 @@ +import CommentCache from "../redisCache"; +import type { InterfaceComment } from "../../models"; + +export async function deleteCommentFromCache( + comment: InterfaceComment +): Promise { + const key = `comment:${comment._id}`; + + await CommentCache.del(key); +} diff --git a/src/services/CommentCache/findCommentsByPostIdInCache.ts b/src/services/CommentCache/findCommentsByPostIdInCache.ts new file mode 100644 index 0000000000..26377fab17 --- /dev/null +++ b/src/services/CommentCache/findCommentsByPostIdInCache.ts @@ -0,0 +1,60 @@ +import CommentCache from "../redisCache"; +import type { InterfaceComment } from "../../models"; +import { Types } from "mongoose"; +import { logger } from "../../libraries"; + +export async function findCommentsByPostIdInCache( + postID: Types.ObjectId +): Promise<(InterfaceComment | null)[]> { + // fetches the comment id for a particular post + const hashKey = `post_comments:${postID}`; + + const commentIDs = await CommentCache.hkeys(hashKey); + + if (commentIDs.length == 0) { + return []; + } + + // fetches the comment json data in the cache. + + const commentsFoundInCache = await CommentCache.mget(commentIDs); + + const comments = commentsFoundInCache.map((comment) => { + if (comment === null) { + return null; + } + + try { + const commentObj = JSON.parse(comment); + + // Note: While JSON parsing successfully restores the fields, including those with + // Mongoose Object IDs, these fields are returned as strings due to the serialization + // process. To ensure accurate data representation, we manually convert these string + // values back to their original Mongoose Object ID types before delivering them to + // the requesting resolver. + + return { + ...commentObj, + + _id: Types.ObjectId(commentObj._id), + + createdAt: new Date(commentObj.createdAt), + + creator: Types.ObjectId(commentObj.creator), + + postId: Types.ObjectId(commentObj.postId), + + likedBy: + commentObj?.likedBy.length !== 0 + ? commentObj?.likedBy?.map((user: string) => { + return Types.ObjectId(user); + }) + : [], + }; + } catch (parseError) { + logger.info(`Error parsing JSON:${parseError}`); + } + }); + + return comments; +} diff --git a/src/services/CommentCache/findCommentsInCache.ts b/src/services/CommentCache/findCommentsInCache.ts new file mode 100644 index 0000000000..9188d03129 --- /dev/null +++ b/src/services/CommentCache/findCommentsInCache.ts @@ -0,0 +1,53 @@ +import CommentCache from "../redisCache"; +import type { InterfaceComment } from "../../models"; +import { Types } from "mongoose"; +import { logger } from "../../libraries"; + +export async function findCommentsInCache( + ids: string[] +): Promise<(InterfaceComment | null)[]> { + const keys: string[] = ids.map((id) => { + return `comment:${id}`; + }); + + const commentsFoundInCache = await CommentCache.mget(keys); + + const comments = commentsFoundInCache.map((comment) => { + if (comment === null) { + return null; + } + + try { + const commentObj = JSON.parse(comment); + + // Note: While JSON parsing successfully restores the fields, including those with + // Mongoose Object IDs, these fields are returned as strings due to the serialization + // process. To ensure accurate data representation, we manually convert these string + // values back to their original Mongoose Object ID types before delivering them to + // the requesting resolver. + + return { + ...commentObj, + + _id: Types.ObjectId(commentObj._id), + + createdAt: new Date(commentObj.createdAt), + + creator: Types.ObjectId(commentObj.creator), + + postId: Types.ObjectId(commentObj.postId), + + likedBy: + commentObj?.likedBy.length !== 0 + ? commentObj?.likedBy?.map((user: string) => { + return Types.ObjectId(user); + }) + : [], + }; + } catch (parseError) { + logger.info(`Error parsing JSON:${parseError}`); + } + }); + + return comments; +} diff --git a/src/services/EventCache/cacheEvents.ts b/src/services/EventCache/cacheEvents.ts new file mode 100644 index 0000000000..a73f300cb1 --- /dev/null +++ b/src/services/EventCache/cacheEvents.ts @@ -0,0 +1,24 @@ +import { logger } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; +import EventCache from "../redisCache"; + +// Function to store events in the cache using pipelining +export async function cacheEvents(events: InterfaceEvent[]): Promise { + try { + const pipeline = EventCache.pipeline(); + + events.forEach((event) => { + if (event !== null) { + const key = `event:${event._id}`; + pipeline.set(key, JSON.stringify(event)); + // SET the time to live for each of the organization in the cache to 300s. + pipeline.expire(key, 300); + } + }); + + // Execute the pipeline + await pipeline.exec(); + } catch (error) { + logger.info(error); + } +} diff --git a/src/services/EventCache/deleteEventFromCache.ts b/src/services/EventCache/deleteEventFromCache.ts new file mode 100644 index 0000000000..8affeee451 --- /dev/null +++ b/src/services/EventCache/deleteEventFromCache.ts @@ -0,0 +1,10 @@ +import EventCache from "../redisCache"; +import type { Types } from "mongoose"; + +export async function deleteEventFromCache( + eventId: Types.ObjectId +): Promise { + const key = `event:${eventId}`; + + await EventCache.del(key); +} diff --git a/src/services/EventCache/findEventInCache.ts b/src/services/EventCache/findEventInCache.ts new file mode 100644 index 0000000000..6d06c3ba80 --- /dev/null +++ b/src/services/EventCache/findEventInCache.ts @@ -0,0 +1,61 @@ +import EventCache from "../redisCache"; +import type { InterfaceEvent } from "../../models"; +import { Types } from "mongoose"; +import { logger } from "../../libraries"; + +export async function findEventsInCache( + ids: string[] +): Promise<(InterfaceEvent | null)[]> { + const keys: string[] = ids.map((id) => { + return `event:${id}`; + }); + + const eventsFoundInCache = await EventCache.mget(keys); + + const events = eventsFoundInCache.map((event) => { + if (event === null) { + return null; + } + + try { + const eventObj = JSON.parse(event); + + // Note: While JSON parsing successfully restores the fields, including those with + // Mongoose Object IDs, these fields are returned as strings due to the serialization + // process. To ensure accurate data representation, we manually convert these string + // values back to their original Mongoose Object ID types before delivering them to + // the requesting resolver. + + return { + ...eventObj, + + _id: Types.ObjectId(eventObj._id), + + admins: + eventObj?.admins?.length !== 0 + ? eventObj?.admins?.map((admin: string) => { + return Types.ObjectId(admin); + }) + : [], + + organization: Types.ObjectId(eventObj.organization), + + startDate: new Date(eventObj.startDate), + + ...(eventObj?.endDate ? { endDate: new Date(eventObj.endDate) } : {}), // Conditional removal of endDate field + + ...(eventObj?.startTime + ? { startTime: new Date(eventObj.startTime) } + : {}), // Conditional removal of startTime field + + ...(eventObj?.endTime ? { endTime: new Date(eventObj.endTime) } : {}), // Conditional removal of endTime field + + creator: Types.ObjectId(eventObj.creator), + }; + } catch (parseError) { + logger.info(`Error parsing JSON:${parseError}`); + } + }); + + return events; +} diff --git a/src/services/OrganizationCache/OrganizationCache.ts b/src/services/OrganizationCache/OrganizationCache.ts deleted file mode 100644 index 5cb86ad9a6..0000000000 --- a/src/services/OrganizationCache/OrganizationCache.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Redis } from "ioredis"; -import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../../constants"; - -const OrganizationCache = new Redis({ - host: REDIS_HOST!, - port: REDIS_PORT || 6379, - password: REDIS_PASSWORD, -}); - -// Setting the limit of the max memory of the cache to 100MB -if (!REDIS_PASSWORD) { - OrganizationCache.config("SET", "maxmemory", 100 * 1024 * 1024); - // Setting the eviction policy to Least Frequently Used ,evicted first algorithm - OrganizationCache.config("SET", "maxmemory-policy", "allkeys-lfu"); -} - -export default OrganizationCache; diff --git a/src/services/OrganizationCache/cacheOrganizations.ts b/src/services/OrganizationCache/cacheOrganizations.ts index b08dc51f15..6a0fb4720d 100644 --- a/src/services/OrganizationCache/cacheOrganizations.ts +++ b/src/services/OrganizationCache/cacheOrganizations.ts @@ -1,5 +1,6 @@ +import { logger } from "../../libraries"; import type { InterfaceOrganization } from "../../models"; -import OrganizationCache from "./OrganizationCache"; +import OrganizationCache from "../redisCache"; // Function to store organizations in the cache using pipelining export async function cacheOrganizations( @@ -20,6 +21,6 @@ export async function cacheOrganizations( // Execute the pipeline await pipeline.exec(); } catch (error) { - console.log(error); + logger.info(error); } } diff --git a/src/services/OrganizationCache/deleteOrganizationFromCache.ts b/src/services/OrganizationCache/deleteOrganizationFromCache.ts index 6fb0c7f170..6359664ea1 100644 --- a/src/services/OrganizationCache/deleteOrganizationFromCache.ts +++ b/src/services/OrganizationCache/deleteOrganizationFromCache.ts @@ -1,4 +1,4 @@ -import OrganizationCache from "./OrganizationCache"; +import OrganizationCache from "../redisCache"; import type { InterfaceOrganization } from "../../models"; export async function deleteOrganizationFromCache( @@ -7,6 +7,4 @@ export async function deleteOrganizationFromCache( const key = `organization:${organization._id}`; await OrganizationCache.del(key); - - console.log("Organization deleted from cache"); } diff --git a/src/services/OrganizationCache/findOrganizationsInCache.ts b/src/services/OrganizationCache/findOrganizationsInCache.ts index d1b70c20da..bdf0e3b411 100644 --- a/src/services/OrganizationCache/findOrganizationsInCache.ts +++ b/src/services/OrganizationCache/findOrganizationsInCache.ts @@ -1,6 +1,7 @@ -import OrganizationCache from "./OrganizationCache"; +import OrganizationCache from "../redisCache"; import type { InterfaceOrganization } from "../../models"; import { Types } from "mongoose"; +import { logger } from "../../libraries"; export async function findOrganizationsInCache( ids: string[] @@ -83,7 +84,7 @@ export async function findOrganizationsInCache( : [], }; } catch (parseError) { - console.error("Error parsing JSON:", parseError); + logger.info(`Error parsing JSON:${parseError}`); } }); diff --git a/src/services/PostCache/cachePosts.ts b/src/services/PostCache/cachePosts.ts new file mode 100644 index 0000000000..91a5d123bd --- /dev/null +++ b/src/services/PostCache/cachePosts.ts @@ -0,0 +1,24 @@ +import { logger } from "../../libraries"; +import type { InterfacePost } from "../../models"; +import PostCache from "../redisCache"; + +// Function to store posts in the cache using pipelining +export async function cachePosts(posts: InterfacePost[]): Promise { + try { + const pipeline = PostCache.pipeline(); + + posts.forEach((post) => { + if (post !== null) { + const key = `post:${post._id}`; + pipeline.set(key, JSON.stringify(post)); + // SET the time to live for each of the organization in the cache to 300s. + pipeline.expire(key, 300); + } + }); + + // Execute the pipeline + await pipeline.exec(); + } catch (error) { + logger.info(error); + } +} diff --git a/src/services/PostCache/deletePostFromCache.ts b/src/services/PostCache/deletePostFromCache.ts new file mode 100644 index 0000000000..1a3ffa2dd6 --- /dev/null +++ b/src/services/PostCache/deletePostFromCache.ts @@ -0,0 +1,7 @@ +import PostCache from "../redisCache"; + +export async function deletePostFromCache(postId: string): Promise { + const key = `post:${postId}`; + + await PostCache.del(key); +} diff --git a/src/services/PostCache/findPostsInCache.ts b/src/services/PostCache/findPostsInCache.ts new file mode 100644 index 0000000000..8b895e15ce --- /dev/null +++ b/src/services/PostCache/findPostsInCache.ts @@ -0,0 +1,57 @@ +import PostCache from "../redisCache"; +import type { InterfacePost } from "../../models"; +import { Types } from "mongoose"; +import { logger } from "../../libraries"; + +export async function findPostsInCache( + ids: string[] +): Promise<(InterfacePost | null)[]> { + const keys: string[] = ids.map((id) => { + return `post:${id}`; + }); + + const postsFoundInCache = await PostCache.mget(keys); + + const posts = postsFoundInCache.map((post) => { + if (post === null) { + return null; + } + + try { + const postObj = JSON.parse(post); + + // Note: While JSON parsing successfully restores the fields, including those with + // Mongoose Object IDs, these fields are returned as strings due to the serialization + // process. To ensure accurate data representation, we manually convert these string + // values back to their original Mongoose Object ID types before delivering them to + // the requesting resolver. + + return { + ...postObj, + + _id: Types.ObjectId(postObj._id), + + createdAt: new Date(postObj.createdAt), + + organization: Types.ObjectId(postObj.organization), + + likeCount: Number(postObj.likeCount), + + commentCount: Number(postObj.commentCount), + + likedBy: + postObj?.likedBy.length !== 0 + ? postObj?.likedBy?.map((user: string) => { + return Types.ObjectId(user); + }) + : [], + + creator: Types.ObjectId(postObj.creator), + }; + } catch (parseError) { + logger.info(`Error parsing JSON:${parseError}`); + } + }); + + return posts; +} diff --git a/src/services/redisCache.ts b/src/services/redisCache.ts new file mode 100644 index 0000000000..b87469b859 --- /dev/null +++ b/src/services/redisCache.ts @@ -0,0 +1,17 @@ +import { Redis } from "ioredis"; +import { REDIS_HOST, REDIS_PASSWORD, REDIS_PORT } from "../constants"; + +const RedisCache = new Redis({ + host: REDIS_HOST, + port: REDIS_PORT || 6379, + password: REDIS_PASSWORD, +}); + +// Setting the limit of the max memory of the cache to 100MB +if (!REDIS_PASSWORD) { + RedisCache.config("SET", "maxmemory", 100 * 1024 * 1024); + // Setting the eviction policy to Least Frequently Used ,evicted first algorithm + RedisCache.config("SET", "maxmemory-policy", "allkeys-lfu"); +} + +export default RedisCache; diff --git a/tests/resolvers/Mutation/adminRemoveEvent.spec.ts b/tests/resolvers/Mutation/adminRemoveEvent.spec.ts index 645ea79695..dfd1928d7f 100644 --- a/tests/resolvers/Mutation/adminRemoveEvent.spec.ts +++ b/tests/resolvers/Mutation/adminRemoveEvent.spec.ts @@ -20,6 +20,7 @@ import type { import type { TestEventType } from "../../helpers/events"; import { createTestEvent } from "../../helpers/events"; import { cacheOrganizations } from "../../../src/services/OrganizationCache/cacheOrganizations"; +import { cacheEvents } from "../../../src/services/EventCache/cacheEvents"; let testUser: TestUserType; let testOrganization: TestOrganizationType; @@ -67,7 +68,7 @@ describe("resolvers -> Mutation -> adminRemoveEvent", () => { it(`throws NotFoundError if no organization exists with _id === event.organization for event with _id === args.eventId`, async () => { try { - await Event.updateOne( + await Event.findOneAndUpdate( { _id: testEvent?._id, }, @@ -75,6 +76,9 @@ describe("resolvers -> Mutation -> adminRemoveEvent", () => { $set: { organization: Types.ObjectId().toString(), }, + }, + { + new: true, } ); @@ -94,7 +98,7 @@ describe("resolvers -> Mutation -> adminRemoveEvent", () => { it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { try { - await Event.updateOne( + const updatedEvent = await Event.findOneAndUpdate( { _id: testEvent?._id, }, @@ -102,9 +106,15 @@ describe("resolvers -> Mutation -> adminRemoveEvent", () => { $set: { organization: testOrganization?._id, }, + }, + { + new: true, } ); + if (updatedEvent !== null) { + await cacheEvents([updatedEvent]); + } const args: MutationAdminRemoveEventArgs = { eventId: testEvent?.id, }; diff --git a/tests/resolvers/Mutation/removeComment.spec.ts b/tests/resolvers/Mutation/removeComment.spec.ts index 0106b01ffe..45d36991ff 100644 --- a/tests/resolvers/Mutation/removeComment.spec.ts +++ b/tests/resolvers/Mutation/removeComment.spec.ts @@ -24,6 +24,7 @@ import { import type { TestUserType } from "../../helpers/userAndOrg"; import type { TestPostType } from "../../helpers/posts"; import { createTestPost } from "../../helpers/posts"; +import { cacheComments } from "../../../src/services/CommentCache/cacheComments"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; @@ -126,7 +127,7 @@ describe("resolvers -> Mutation -> removeComment", () => { .mockImplementationOnce((message) => message); try { // Remove the user as the creator of the comment - await Comment.updateOne( + const updatedComment = await Comment.findOneAndUpdate( { _id: testComment?._id, }, @@ -134,9 +135,16 @@ describe("resolvers -> Mutation -> removeComment", () => { $set: { creator: Types.ObjectId().toString(), }, + }, + { + new: true, } ); + if (updatedComment !== null) { + await cacheComments([updatedComment]); + } + // Remove the user as the admin of the organization of the post of the comment await User.updateOne( { @@ -170,17 +178,24 @@ describe("resolvers -> Mutation -> removeComment", () => { it(`deletes the comment with _id === args.id`, async () => { // Make the user creator of the comment again - await Comment.updateOne( + const updatedComment = await Comment.findOneAndUpdate( { _id: testComment?._id, }, { $set: { - creator: testUser?._id, + creator: testUser!._id, }, + }, + { + new: true, } ); + if (updatedComment !== null) { + await cacheComments([updatedComment]); + } + // Set the user as the admin of the organization of the post of the comment await User.updateOne( { diff --git a/tests/resolvers/Mutation/removeEvent.spec.ts b/tests/resolvers/Mutation/removeEvent.spec.ts index bb4bb25fa3..120492713f 100644 --- a/tests/resolvers/Mutation/removeEvent.spec.ts +++ b/tests/resolvers/Mutation/removeEvent.spec.ts @@ -18,6 +18,7 @@ import type { } from "../../helpers/userAndOrg"; import type { TestEventType } from "../../helpers/events"; import { createTestEvent } from "../../helpers/events"; +import { cacheEvents } from "../../../src/services/EventCache/cacheEvents"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; @@ -148,7 +149,7 @@ describe("resolvers -> Mutation -> removeEvent", () => { } ); - await Event.updateOne( + const updatedEvent = await Event.findOneAndUpdate( { _id: testEvent?._id, }, @@ -156,9 +157,16 @@ describe("resolvers -> Mutation -> removeEvent", () => { $push: { admins: testUser?._id, }, + }, + { + new: true, } ); + if (updatedEvent !== null) { + await cacheEvents([updatedEvent]); + } + const args: MutationRemoveEventArgs = { id: testEvent?.id, }; diff --git a/tests/resolvers/Mutation/updateEvent.spec.ts b/tests/resolvers/Mutation/updateEvent.spec.ts index a9a2eab558..3b359fa832 100644 --- a/tests/resolvers/Mutation/updateEvent.spec.ts +++ b/tests/resolvers/Mutation/updateEvent.spec.ts @@ -23,6 +23,7 @@ import { import type { TestUserType } from "../../helpers/userAndOrg"; import { createTestUserAndOrganization } from "../../helpers/userAndOrg"; import type { TestEventType } from "../../helpers/events"; +import { cacheEvents } from "../../../src/services/EventCache/cacheEvents"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; @@ -154,7 +155,7 @@ describe("resolvers -> Mutation -> updateEvent", () => { }); it(`updates the event with _id === args.id and returns the updated event`, async () => { - await Event.updateOne( + const updatedEvent = await Event.findOneAndUpdate( { _id: testEvent?._id, }, @@ -162,8 +163,15 @@ describe("resolvers -> Mutation -> updateEvent", () => { $push: { admins: testUser?._id, }, + }, + { + new: true, } - ); + ).lean(); + + if (updatedEvent !== null) { + await cacheEvents([updatedEvent]); + } await User.updateOne( { diff --git a/tests/resolvers/Organization/pinnedPosts.spec.ts b/tests/resolvers/Organization/pinnedPosts.spec.ts index d648b5c468..6ead610623 100644 --- a/tests/resolvers/Organization/pinnedPosts.spec.ts +++ b/tests/resolvers/Organization/pinnedPosts.spec.ts @@ -2,17 +2,30 @@ import "dotenv/config"; import { pinnedPosts as pinnedPostsResolver } from "../../../src/resolvers/Organization/pinnedPosts"; import { connect, disconnect } from "../../helpers/db"; import type mongoose from "mongoose"; -import { Post } from "../../../src/models"; +import { Organization, Post } from "../../../src/models"; import { beforeAll, afterAll, describe, it, expect } from "vitest"; import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import type { TestPostType } from "../../helpers/posts"; import { createTestPost } from "../../helpers/posts"; let MONGOOSE_INSTANCE: typeof mongoose; let testOrganization: TestOrganizationType; +let testPost: TestPostType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [, testOrganization] = await createTestPost(true); + [, testOrganization, testPost] = await createTestPost(true); + + await Organization.findOneAndUpdate( + { + _id: testOrganization?.id, + }, + { + $push: { + pinnedPosts: testPost?.id, + }, + } + ); }); afterAll(async () => { @@ -21,12 +34,36 @@ afterAll(async () => { describe("resolvers -> Organization -> pinnedPosts", () => { it(`returns all post objects for parent.pinnedPosts`, async () => { - const parent = testOrganization?.toObject(); + const testOrganization2 = await Organization.findOne({ + _id: testOrganization?.id, + }); + + const parent = testOrganization2?.toObject(); + + if (parent) { + const pinnedPostsPayload = await pinnedPostsResolver?.(parent, {}, {}); + const pinnedPosts = await Post.find({ + _id: { + $in: testOrganization2?.pinnedPosts, + }, + }).lean(); + + expect(pinnedPostsPayload).toEqual(pinnedPosts); + } + }); + + it(`returns all post objects for parent.pinnedPosts from the cache`, async () => { + const testOrganization2 = await Organization.findOne({ + _id: testOrganization?.id, + }); + + const parent = testOrganization2?.toObject(); + if (parent) { const pinnedPostsPayload = await pinnedPostsResolver?.(parent, {}, {}); const pinnedPosts = await Post.find({ _id: { - $in: testOrganization?.pinnedPosts, + $in: testOrganization2?.pinnedPosts, }, }).lean(); diff --git a/tests/resolvers/Post/comments.spec.ts b/tests/resolvers/Post/comments.spec.ts index 1e9f7eee64..f2fd613ed9 100644 --- a/tests/resolvers/Post/comments.spec.ts +++ b/tests/resolvers/Post/comments.spec.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import { comments as commentsResolver } from "../../../src/resolvers/Post/comments"; import { connect, disconnect } from "../../helpers/db"; import type mongoose from "mongoose"; -import { Comment } from "../../../src/models"; +import { Comment, Post } from "../../../src/models"; import { beforeAll, afterAll, describe, it, expect } from "vitest"; import type { TestPostType } from "../../helpers/posts"; import { createTestPost } from "../../helpers/posts"; @@ -20,6 +20,17 @@ beforeAll(async () => { creator: testUser!._id, postId: testPost!._id, }); + + await Post.findOneAndUpdate( + { + _id: testPost?._id, + }, + { + $inc: { + commentCount: 1, + }, + } + ); }); afterAll(async () => { @@ -28,9 +39,28 @@ afterAll(async () => { describe("resolvers -> Post -> comments", () => { it(`returns the comment object for parent post`, async () => { - const parent = testPost!.toObject(); + const parent = await Post.findById(testPost?._id); + + const commentsPayload = await commentsResolver?.( + parent!.toObject(), + {}, + {} + ); + + const comments = await Comment.find({ + postId: testPost!._id, + }).lean(); + + expect(commentsPayload).toEqual(comments); + }); + it(`returns the comment object for parent post from cache`, async () => { + const parent = await Post.findById(testPost?._id); - const commentsPayload = await commentsResolver?.(parent, {}, {}); + const commentsPayload = await commentsResolver?.( + parent!.toObject(), + {}, + {} + ); const comments = await Comment.find({ postId: testPost!._id,