From 07f4b9ccb9015a187ec6e7eb7d55d01603b721d4 Mon Sep 17 00:00:00 2001 From: meetul Date: Sat, 21 Sep 2024 20:30:23 +0530 Subject: [PATCH] more tag actions --- src/resolvers/Mutation/addToUserTags.ts | 169 +++++++++++++++++++ src/resolvers/Mutation/index.ts | 4 + src/resolvers/Mutation/removeFromUserTags.ts | 166 ++++++++++++++++++ src/typeDefs/inputs.ts | 5 + src/typeDefs/mutations.ts | 4 + src/types/generatedGraphQLTypes.ts | 21 +++ 6 files changed, 369 insertions(+) create mode 100644 src/resolvers/Mutation/addToUserTags.ts create mode 100644 src/resolvers/Mutation/removeFromUserTags.ts diff --git a/src/resolvers/Mutation/addToUserTags.ts b/src/resolvers/Mutation/addToUserTags.ts new file mode 100644 index 0000000000..da788d56fd --- /dev/null +++ b/src/resolvers/Mutation/addToUserTags.ts @@ -0,0 +1,169 @@ +import { Types } from "mongoose"; +import { + TAG_NOT_FOUND, + USER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, +} from "../../constants"; +import { errors, requestContext } from "../../libraries"; +import type { + InterfaceAppUserProfile, + InterfaceOrganizationTagUser, + InterfaceUser, +} from "../../models"; +import { + AppUserProfile, + OrganizationTagUser, + TagUser, + User, +} from "../../models"; +import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; +import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; +import { cacheUsers } from "../../services/UserCache/cacheUser"; +import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; + +/** + * This function enables an admin to assign multiple tags and their ancestors to users with a specified tag. + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire application + * @remarks The following checks are done: + * 1. If the current user exists and has a profile. + * 2. If the current user is an admin for the organization of the tags. + * 3. If the currentTagId exists and the selected tags exist. + * 4. Assign the tags to users who have the currentTagId. + * @returns Array of tags that were assigned to users. + */ +export const addToUserTags: MutationResolvers["addToUserTags"] = async ( + _parent, + args, + context, +) => { + let currentUser: InterfaceUser | null; + const userFoundInCache = await findUserInCache([context.userId]); + currentUser = userFoundInCache[0]; + if (currentUser === null) { + currentUser = await User.findOne({ + _id: context.userId, + }).lean(); + if (currentUser !== null) { + await cacheUsers([currentUser]); + } + } + + // Checks whether the currentUser exists. + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + let currentUserAppProfile: InterfaceAppUserProfile | null; + const appUserProfileFoundInCache = await findAppUserProfileCache([ + currentUser.appUserProfileId.toString(), + ]); + currentUserAppProfile = appUserProfileFoundInCache[0]; + if (currentUserAppProfile === null) { + currentUserAppProfile = await AppUserProfile.findOne({ + userId: currentUser._id, + }).lean(); + if (currentUserAppProfile !== null) { + await cacheAppUserProfile([currentUserAppProfile]); + } + } + if (!currentUserAppProfile) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + // Get the current tag object + const currentTag = await OrganizationTagUser.findOne({ + _id: args.input.currentTagId, + }).lean(); + + if (!currentTag) { + throw new errors.NotFoundError( + requestContext.translate(TAG_NOT_FOUND.MESSAGE), + TAG_NOT_FOUND.CODE, + TAG_NOT_FOUND.PARAM, + ); + } + + // Boolean to determine whether user is an admin of the organization of the current tag. + const currentUserIsOrganizationAdmin = currentUserAppProfile.adminFor.some( + (orgId) => + orgId && + new Types.ObjectId(orgId.toString()).equals(currentTag.organizationId), + ); + if (!(currentUserIsOrganizationAdmin || currentUserAppProfile.isSuperAdmin)) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + // Find all users with the currentTagId + const usersWithCurrentTag = await TagUser.find({ + tagId: currentTag._id, + }).lean(); + + const userIdsWithCurrentTag = usersWithCurrentTag.map( + (userTag) => userTag.userId, + ); + + // Validate selected tags + const selectedTags = await OrganizationTagUser.find({ + _id: { $in: args.input.selectedTagIds }, + }).lean(); + + const selectedTagMap = new Map( + selectedTags.map((tag) => [tag._id.toString(), tag]), + ); + + if (selectedTags.length === 0) { + throw new errors.NotFoundError( + requestContext.translate(TAG_NOT_FOUND.MESSAGE), + TAG_NOT_FOUND.CODE, + TAG_NOT_FOUND.PARAM, + ); + } + + // Find and assign ancestor tags + const allTagsToAssign = new Set(); + for (const tag of selectedTags) { + let currentTagToProcess: InterfaceOrganizationTagUser | null = tag; + while (currentTagToProcess) { + allTagsToAssign.add(currentTagToProcess._id.toString()); + if (currentTagToProcess.parentTagId) { + const parentTag: any = await OrganizationTagUser.findOne({ + _id: currentTagToProcess.parentTagId, + }).lean(); + currentTagToProcess = parentTag || null; + } else { + currentTagToProcess = null; + } + } + } + + const tagUserDocs = userIdsWithCurrentTag.flatMap((userId) => + Array.from(allTagsToAssign).map((tagId) => ({ + updateOne: { + filter: { userId, tagId: new Types.ObjectId(tagId) }, + update: { $setOnInsert: { userId, tagId } }, + upsert: true, + setDefaultsOnInsert: true, + }, + })), + ); + + if (tagUserDocs.length > 0) { + await TagUser.bulkWrite(tagUserDocs); + } + + return currentTag; +}; diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 1d60142b8e..9becc03123 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -10,6 +10,7 @@ import { addUserImage } from "./addUserImage"; import { addUserToGroupChat } from "./addUserToGroupChat"; import { addUserToUserFamily } from "./addUserToUserFamily"; import { addPeopleToUserTag } from "./addPeopleToUserTag"; +import { addToUserTags } from "./addToUserTags"; import { adminRemoveGroup } from "./adminRemoveGroup"; import { assignUserTag } from "./assignUserTag"; import { blockPluginCreationBySuperadmin } from "./blockPluginCreationBySuperadmin"; @@ -87,6 +88,7 @@ import { removeUserFromGroupChat } from "./removeUserFromGroupChat"; import { removeUserFromUserFamily } from "./removeUserFromUserFamily"; import { removeUserImage } from "./removeUserImage"; import { removeUserTag } from "./removeUserTag"; +import { removeFromUserTags } from "./removeFromUserTags"; import { resetCommunity } from "./resetCommunity"; import { revokeRefreshTokenForUser } from "./revokeRefreshTokenForUser"; import { saveFcmToken } from "./saveFcmToken"; @@ -138,6 +140,7 @@ export const Mutation: MutationResolvers = { adminRemoveGroup, addUserToUserFamily, addPeopleToUserTag, + addToUserTags, removeUserFamily, removeUserFromUserFamily, createUserFamily, @@ -214,6 +217,7 @@ export const Mutation: MutationResolvers = { removeUserFromGroupChat, removeUserImage, removeUserTag, + removeFromUserTags, resetCommunity, revokeRefreshTokenForUser, saveFcmToken, diff --git a/src/resolvers/Mutation/removeFromUserTags.ts b/src/resolvers/Mutation/removeFromUserTags.ts new file mode 100644 index 0000000000..5fcedce51f --- /dev/null +++ b/src/resolvers/Mutation/removeFromUserTags.ts @@ -0,0 +1,166 @@ +import { Types } from "mongoose"; +import { + TAG_NOT_FOUND, + USER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, +} from "../../constants"; +import { errors, requestContext } from "../../libraries"; +import type { + InterfaceAppUserProfile, + InterfaceOrganizationTagUser, + InterfaceUser, +} from "../../models"; +import { + AppUserProfile, + OrganizationTagUser, + TagUser, + User, +} from "../../models"; +import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; +import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; +import { cacheUsers } from "../../services/UserCache/cacheUser"; +import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; + +/** + * This function enables an admin to remove multiple tags and their ancestors from users with a specified tag. + * @param _parent - parent of current request + * @param args - payload provided with the request + * @param context - context of entire application + * @remarks The following checks are done: + * 1. If the current user exists and has a profile. + * 2. If the current user is an admin for the organization of the tags. + * 3. If the currentTagId exists and the selected tags exist. + * 4. Remove the tags from users who have the currentTagId. + * @returns Array of tags that were removed from users. + */ +export const removeFromUserTags: MutationResolvers["removeFromUserTags"] = + async (_parent, args, context) => { + let currentUser: InterfaceUser | null; + const userFoundInCache = await findUserInCache([context.userId]); + currentUser = userFoundInCache[0]; + if (currentUser === null) { + currentUser = await User.findOne({ + _id: context.userId, + }).lean(); + if (currentUser !== null) { + await cacheUsers([currentUser]); + } + } + + // Checks whether the currentUser exists. + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + let currentUserAppProfile: InterfaceAppUserProfile | null; + const appUserProfileFoundInCache = await findAppUserProfileCache([ + currentUser.appUserProfileId.toString(), + ]); + currentUserAppProfile = appUserProfileFoundInCache[0]; + if (currentUserAppProfile === null) { + currentUserAppProfile = await AppUserProfile.findOne({ + userId: currentUser._id, + }).lean(); + if (currentUserAppProfile !== null) { + await cacheAppUserProfile([currentUserAppProfile]); + } + } + if (!currentUserAppProfile) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + // Get the current tag object + const currentTag = await OrganizationTagUser.findOne({ + _id: args.input.currentTagId, + }).lean(); + + if (!currentTag) { + throw new errors.NotFoundError( + requestContext.translate(TAG_NOT_FOUND.MESSAGE), + TAG_NOT_FOUND.CODE, + TAG_NOT_FOUND.PARAM, + ); + } + + // Boolean to determine whether user is an admin of the organization of the current tag. + const currentUserIsOrganizationAdmin = currentUserAppProfile.adminFor.some( + (orgId) => + orgId && + new Types.ObjectId(orgId.toString()).equals(currentTag.organizationId), + ); + if ( + !(currentUserIsOrganizationAdmin || currentUserAppProfile.isSuperAdmin) + ) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + + // Find all users with the currentTagId + const usersWithCurrentTag = await TagUser.find({ + tagId: currentTag._id, + }).lean(); + + const userIdsWithCurrentTag = usersWithCurrentTag.map( + (userTag) => userTag.userId, + ); + + // Validate selected tags + const selectedTags = await OrganizationTagUser.find({ + _id: { $in: args.input.selectedTagIds }, + }).lean(); + + console.log("here"); + const selectedTagMap = new Map( + selectedTags.map((tag) => [tag._id.toString(), tag]), + ); + + if (selectedTags.length === 0) { + throw new errors.NotFoundError( + requestContext.translate(TAG_NOT_FOUND.MESSAGE), + TAG_NOT_FOUND.CODE, + TAG_NOT_FOUND.PARAM, + ); + } + + // Find and remove ancestor tags + const allTagsToRemove = new Set(); + for (const tag of selectedTags) { + let currentTagToProcess: InterfaceOrganizationTagUser | null = tag; + while (currentTagToProcess) { + allTagsToRemove.add(currentTagToProcess._id.toString()); + if (currentTagToProcess.parentTagId) { + const parentTag: any = await OrganizationTagUser.findOne({ + _id: currentTagToProcess.parentTagId, + }).lean(); + currentTagToProcess = parentTag || null; + } else { + currentTagToProcess = null; + } + } + } + + const tagUserDocs = userIdsWithCurrentTag.flatMap((userId) => + Array.from(allTagsToRemove).map((tagId) => ({ + deleteOne: { + filter: { userId, tagId: new Types.ObjectId(tagId) }, + }, + })), + ); + + if (tagUserDocs.length > 0) { + await TagUser.bulkWrite(tagUserDocs); + } + + return currentTag; + }; diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index cd7ab37d8f..10dc5498ba 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -417,6 +417,11 @@ export const inputs = gql` tagId: ID! } + input TagActionsInput { + currentTagId: ID! + selectedTagIds: [ID!]! + } + input ToggleUserTagAssignInput { userId: ID! tagId: ID! diff --git a/src/typeDefs/mutations.ts b/src/typeDefs/mutations.ts index cc6a7e0f82..6ad078dd81 100644 --- a/src/typeDefs/mutations.ts +++ b/src/typeDefs/mutations.ts @@ -39,6 +39,8 @@ export const mutations = gql` addPeopleToUserTag(input: AddPeopleToUserTagInput!): UserTag @auth + addToUserTags(input: TagActionsInput!): UserTag @auth + removeUserFromUserFamily(userId: ID!, familyId: ID!): UserFamily! @auth removeUserFamily(familyId: ID!): UserFamily! @auth @@ -235,6 +237,8 @@ export const mutations = gql` removeUserTag(id: ID!): UserTag @auth + removeFromUserTags(input: TagActionsInput!): UserTag @auth + removeSampleOrganization: Boolean! @auth removeUserFromGroupChat(userId: ID!, chatId: ID!): GroupChat! @auth diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index 39c1df4dd6..8f2829e13e 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -1165,6 +1165,7 @@ export type Mutation = { addOrganizationImage: Organization; addPeopleToUserTag?: Maybe; addPledgeToFundraisingCampaign: FundraisingCampaignPledge; + addToUserTags?: Maybe; addUserCustomData: UserCustomData; addUserImage: User; addUserToGroupChat: GroupChat; @@ -1234,6 +1235,7 @@ export type Mutation = { removeEventAttendee: User; removeEventVolunteer: EventVolunteer; removeEventVolunteerGroup: EventVolunteerGroup; + removeFromUserTags?: Maybe; removeFund: Fund; removeFundraisingCampaign: FundraisingCampaign; removeFundraisingCampaignPledge: FundraisingCampaignPledge; @@ -1332,6 +1334,11 @@ export type MutationAddPledgeToFundraisingCampaignArgs = { }; +export type MutationAddToUserTagsArgs = { + input: TagActionsInput; +}; + + export type MutationAddUserCustomDataArgs = { dataName: Scalars['String']['input']; dataValue: Scalars['Any']['input']; @@ -1689,6 +1696,11 @@ export type MutationRemoveEventVolunteerGroupArgs = { }; +export type MutationRemoveFromUserTagsArgs = { + input: TagActionsInput; +}; + + export type MutationRemoveFundArgs = { id: Scalars['ID']['input']; }; @@ -2757,6 +2769,11 @@ export type SubscriptionMessageSentToGroupChatArgs = { userId: Scalars['ID']['input']; }; +export type TagActionsInput = { + currentTagId: Scalars['ID']['input']; + selectedTagIds: Array; +}; + export type ToggleUserTagAssignInput = { tagId: Scalars['ID']['input']; userId: Scalars['ID']['input']; @@ -3477,6 +3494,7 @@ export type ResolversTypes = { Status: Status; String: ResolverTypeWrapper; Subscription: ResolverTypeWrapper<{}>; + TagActionsInput: TagActionsInput; Time: ResolverTypeWrapper; ToggleUserTagAssignInput: ToggleUserTagAssignInput; Translation: ResolverTypeWrapper; @@ -3674,6 +3692,7 @@ export type ResolversParentTypes = { SocialMediaUrlsInput: SocialMediaUrlsInput; String: Scalars['String']['output']; Subscription: {}; + TagActionsInput: TagActionsInput; Time: Scalars['Time']['output']; ToggleUserTagAssignInput: ToggleUserTagAssignInput; Translation: Translation; @@ -4334,6 +4353,7 @@ export type MutationResolvers>; addPeopleToUserTag?: Resolver, ParentType, ContextType, RequireFields>; addPledgeToFundraisingCampaign?: Resolver>; + addToUserTags?: Resolver, ParentType, ContextType, RequireFields>; addUserCustomData?: Resolver>; addUserImage?: Resolver>; addUserToGroupChat?: Resolver>; @@ -4403,6 +4423,7 @@ export type MutationResolvers>; removeEventVolunteer?: Resolver>; removeEventVolunteerGroup?: Resolver>; + removeFromUserTags?: Resolver, ParentType, ContextType, RequireFields>; removeFund?: Resolver>; removeFundraisingCampaign?: Resolver>; removeFundraisingCampaignPledge?: Resolver>;