diff --git a/codegen.ts b/codegen.ts index f2f5827b2e..385ef7a58b 100644 --- a/codegen.ts +++ b/codegen.ts @@ -25,6 +25,11 @@ const config: CodegenConfig = { // functionality is useful because what we retrieve from the database and what we choose to return from a graphql server // could be completely different fields. Address to models here is relative to the location of generated types. mappers: { + ActionItem: "../models/ActionItem#InterfaceActionItem", + + ActionItemCategory: + "../models/ActionItemCategory#InterfaceActionItemCategory", + CheckIn: "../models/CheckIn#InterfaceCheckIn", MessageChat: "../models/MessageChat#InterfaceMessageChat", diff --git a/locales/en.json b/locales/en.json index c9f09c338e..69060fe459 100644 --- a/locales/en.json +++ b/locales/en.json @@ -4,6 +4,9 @@ "user.notFound": "User not found", "user.alreadyMember": "User is already a member", "user.profileImage.notFound": "User profile image not found", + "actionItemCategory.notFound": "Action Item Category not found", + "actionItemCategory.alreadyExists": "Action Item Category already exists", + "actionItem.notFound": "Action Item not found", "advertisement.notFound": "Advertisement not found", "event.notFound": "Event not found", "organization.notFound": "Organization not found", diff --git a/locales/fr.json b/locales/fr.json index 8da45242d9..d3fafdc5cb 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -4,6 +4,9 @@ "user.notFound": "Utilisateur introuvable", "user.alreadyMember": "L'utilisateur est déjà membre", "user.profileImage.notFound": "Image du profil utilisateur introuvable", + "actionItemCategory.notFound": "Catégorie d’élément d’action introuvable", + "actionItemCategory.alreadyExists": "La catégorie d’élément d’action existe déjà", + "actionItem.notFound": "Élément d\\’action non trouvé", "event.notFound": "Événement non trouvé", "organization.notFound": "Organisation introuvable", "organization.profileImage.notFound": "Image du profil de l'organisation introuvable", diff --git a/locales/hi.json b/locales/hi.json index ec8731c162..959f97b093 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -4,6 +4,9 @@ "user.notFound": "उपयोगकर्ता नहीं मिला", "user.alreadyMember": "उपयोगकर्ता पहले से ही एक सदस्य है", "user.profileImage.notFound": "उपयोगकर्ता प्रोफ़ाइल छवि नहीं मिली", + "actionItemCategory.notFound": "श्रेणी नहीं मिली", + "actionItemCategory.alreadyExists": "यह श्रेणी पहले से मौजूद है", + "actionItem.notFound": "कार्रवाई का मद नहीं मिला", "advertisement.notFound": "विज्ञापन नहीं मिला", "event.notFound": "घटना नहीं मिली", "organization.notFound": "संगठन नहीं मिला", diff --git a/locales/sp.json b/locales/sp.json index a2301e9fa5..cfee07ce87 100644 --- a/locales/sp.json +++ b/locales/sp.json @@ -4,6 +4,9 @@ "user.notFound": "Usuario no encontrado", "user.alreadyMember": "El usuario ya es miembro", "user.profileImage.notFound": "No se encontró la imagen de perfil de usuario", + "actionItemCategory.notFound": "No se encontró la categoría de elemento de acción", + "actionItemCategory.alreadyExists": "Ya existe una categoría de elemento de acción", + "actionItem.notFound": "Elemento de acción no encontrado", "event.notFound": "Evento no encontrado", "organization.notFound": "Organización no encontrada", "organization.profileImage.notFound": "No se encontró la imagen del perfil de la organización", diff --git a/locales/zh.json b/locales/zh.json index f8cfd535c4..5ef0656cb6 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -4,6 +4,9 @@ "user.notFound": "找不到用戶", "user.alreadyMember": "用戶已經是會員", "user.profileImage.notFound": "未找到用戶個人資料圖像", + "actionItemCategory.notFound": "未找到措施项类别", + "actionItemCategory.alreadyExists": "措施项类别已存在", + "actionItem.notFound": "找不到操作项", "event.notFound": "未找到事件", "organization.notFound": "未找到組織", "organization.profileImage.notFound": "未找到組織檔案圖像", diff --git a/src/constants.ts b/src/constants.ts index 1614b31b7e..6a2f5d5862 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -7,6 +7,27 @@ if (!issues) { ENV = envSchema.parse(process.env); } +export const ACTION_ITEM_NOT_FOUND_ERROR = { + DESC: "ActionItem not found", + CODE: "actionItem.notFound", + MESSAGE: "actionItem.notFound", + PARAM: "actionItem", +}; + +export const ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR = { + DESC: "ActionItemCategory not found", + CODE: "actionItemCategory.notFound", + MESSAGE: "actionItemCategory.notFound", + PARAM: "actionItemCategory", +}; + +export const ACTION_ITEM_CATEGORY_ALREADY_EXISTS = { + DESC: "Action Item Category already exists", + CODE: "actionItemCategory.alreadyExists", + MESSAGE: "actionItemCategory.alreadyExists", + PARAM: "actionItemCategory", +}; + export const CHAT_NOT_FOUND_ERROR = { DESC: "Chat not found", CODE: "chat.notFound", @@ -486,6 +507,8 @@ export const REDIS_HOST = process.env.REDIS_HOST || ""; export const REDIS_PORT = Number(process.env.REDIS_PORT); export const REDIS_PASSWORD = process.env.REDIS_PASSWORD; +export const MILLISECONDS_IN_A_WEEK = 7 * 24 * 60 * 60 * 1000; + export const key = ENV.ENCRYPTION_KEY as string; export const iv = crypto.randomBytes(16).toString("hex"); diff --git a/src/models/ActionItem.ts b/src/models/ActionItem.ts new file mode 100644 index 0000000000..d8832d579c --- /dev/null +++ b/src/models/ActionItem.ts @@ -0,0 +1,107 @@ +import type { PopulatedDoc, Types, Document, Model } from "mongoose"; +import { Schema, model, models } from "mongoose"; +import type { InterfaceUser } from "./User"; +import type { InterfaceEvent } from "./Event"; +import type { InterfaceActionItemCategory } from "./ActionItemCategory"; +import { MILLISECONDS_IN_A_WEEK } from "../constants"; + +/** + * This is an interface that represents a database(MongoDB) document for ActionItem. + */ + +export interface InterfaceActionItem { + _id: Types.ObjectId; + assigneeId: PopulatedDoc; + assignerId: PopulatedDoc; + actionItemCategoryId: PopulatedDoc; + preCompletionNotes: string; + postCompletionNotes: string; + assignmentDate: Date; + dueDate: Date; + completionDate: Date; + isCompleted: boolean; + eventId: PopulatedDoc; + creatorId: PopulatedDoc; + createdAt: Date; + updatedAt: Date; +} + +/** + * This describes the schema for a `ActionItem` that corresponds to `InterfaceActionItem` document. + * @param assigneeId - User to whom the ActionItem is assigned, refer to `User` model. + * @param assignerId - User who assigned the ActionItem, refer to the `User` model. + * @param actionItemCategoryId - ActionItemCategory to which the ActionItem is related, refer to the `ActionItemCategory` model. + * @param preCompletionNotes - Notes prior to completion. + * @param postCompletionNotes - Notes on completion. + * @param assignmentDate - Date of assignment. + * @param dueDate - Due date. + * @param completionDate - Completion date. + * @param isCompleted - Whether the ActionItem has been completed. + * @param eventId - Event to which the ActionItem is related, refer to the `Event` model. + * @param creatorId - User who created the ActionItem, refer to the `User` model. + * @param createdAt - Timestamp when the ActionItem was created. + * @param updatedAt - Timestamp when the ActionItem was last updated. + */ + +const actionItemSchema = new Schema( + { + assigneeId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + assignerId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + actionItemCategoryId: { + type: Schema.Types.ObjectId, + ref: "ActionItemCategory", + required: true, + }, + preCompletionNotes: { + type: String, + }, + postCompletionNotes: { + type: String, + }, + assignmentDate: { + type: Date, + required: true, + default: Date.now(), + }, + dueDate: { + type: Date, + required: true, + default: Date.now() + MILLISECONDS_IN_A_WEEK, + }, + completionDate: { + type: Date, + required: true, + default: Date.now() + MILLISECONDS_IN_A_WEEK, + }, + isCompleted: { + type: Boolean, + required: true, + default: false, + }, + eventId: { + type: Schema.Types.ObjectId, + ref: "Event", + }, + creatorId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + }, + { timestamps: true } +); + +const actionItemModel = (): Model => + model("ActionItem", actionItemSchema); + +// This syntax is needed to prevent Mongoose OverwriteModelError while running tests. +export const ActionItem = (models.ActionItem || + actionItemModel()) as ReturnType; diff --git a/src/models/ActionItemCategory.ts b/src/models/ActionItemCategory.ts new file mode 100644 index 0000000000..4110dfa88c --- /dev/null +++ b/src/models/ActionItemCategory.ts @@ -0,0 +1,68 @@ +import type { PopulatedDoc, Types, Document, Model } from "mongoose"; +import { Schema, model, models } from "mongoose"; +import type { InterfaceUser } from "./User"; +import type { InterfaceOrganization } from "./Organization"; + +/** + * This is an interface that represents a database(MongoDB) document for ActionItemCategory. + */ + +export interface InterfaceActionItemCategory { + _id: Types.ObjectId; + name: string; + organizationId: PopulatedDoc; + isDisabled: boolean; + creatorId: PopulatedDoc; + createdAt: Date; + updatedAt: Date; +} + +/** + * This describes the schema for a `actionItemCategory` that corresponds to `InterfaceCategory` document. + * @param name - An actionItemCategory to be selected for ActionItems. + * @param organizationId - Organization the actionItemCategory belongs to, refer to the `Organization` model. + * @param isDisabled - Whether actionItemCategory is disabled or not. + * @param creatorId - Task creator, refer to `User` model. + * @param createdAt - Time stamp of data creation. + * @param updatedAt - Time stamp of data updation. + */ + +const actionItemCategorySchema = new Schema( + { + name: { + type: String, + required: true, + }, + organizationId: { + type: Schema.Types.ObjectId, + ref: "Organization", + required: true, + }, + isDisabled: { + type: Boolean, + required: true, + default: false, + }, + creatorId: { + type: Schema.Types.ObjectId, + ref: "User", + required: true, + }, + }, + { timestamps: true } +); + +actionItemCategorySchema.index( + { organizationId: 1, name: 1 }, + { unique: true } +); + +const actionItemCategoryModel = (): Model => + model( + "ActionItemCategory", + actionItemCategorySchema + ); + +// This syntax is needed to prevent Mongoose OverwriteModelError while running tests. +export const ActionItemCategory = (models.ActionItemCategory || + actionItemCategoryModel()) as ReturnType; diff --git a/src/models/index.ts b/src/models/index.ts index f819999268..cd287b1ec9 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,4 +1,6 @@ +export * from "./ActionItem"; export * from "./Advertisement"; +export * from "./ActionItemCategory"; export * from "./CheckIn"; export * from "./MessageChat"; export * from "./Comment"; diff --git a/src/resolvers/ActionItem/actionItemCategory.ts b/src/resolvers/ActionItem/actionItemCategory.ts new file mode 100644 index 0000000000..146d836017 --- /dev/null +++ b/src/resolvers/ActionItem/actionItemCategory.ts @@ -0,0 +1,9 @@ +import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes"; +import { ActionItemCategory } from "../../models"; + +export const actionItemCategory: ActionItemResolvers["actionItemCategory"] = + async (parent) => { + return ActionItemCategory.findOne({ + _id: parent.actionItemCategoryId, + }).lean(); + }; diff --git a/src/resolvers/ActionItem/assignee.ts b/src/resolvers/ActionItem/assignee.ts new file mode 100644 index 0000000000..c3f3d13090 --- /dev/null +++ b/src/resolvers/ActionItem/assignee.ts @@ -0,0 +1,8 @@ +import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; + +export const assignee: ActionItemResolvers["assignee"] = async (parent) => { + return User.findOne({ + _id: parent.assigneeId, + }).lean(); +}; diff --git a/src/resolvers/ActionItem/assigner.ts b/src/resolvers/ActionItem/assigner.ts new file mode 100644 index 0000000000..1668d3b4a3 --- /dev/null +++ b/src/resolvers/ActionItem/assigner.ts @@ -0,0 +1,8 @@ +import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; + +export const assigner: ActionItemResolvers["assigner"] = async (parent) => { + return User.findOne({ + _id: parent.assignerId, + }).lean(); +}; diff --git a/src/resolvers/ActionItem/creator.ts b/src/resolvers/ActionItem/creator.ts new file mode 100644 index 0000000000..70dbf78957 --- /dev/null +++ b/src/resolvers/ActionItem/creator.ts @@ -0,0 +1,8 @@ +import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; + +export const creator: ActionItemResolvers["creator"] = async (parent) => { + return User.findOne({ + _id: parent.creatorId, + }).lean(); +}; diff --git a/src/resolvers/ActionItem/event.ts b/src/resolvers/ActionItem/event.ts new file mode 100644 index 0000000000..e79144e5a9 --- /dev/null +++ b/src/resolvers/ActionItem/event.ts @@ -0,0 +1,8 @@ +import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes"; +import { Event } from "../../models"; + +export const event: ActionItemResolvers["event"] = async (parent) => { + return Event.findOne({ + _id: parent.eventId, + }).lean(); +}; diff --git a/src/resolvers/ActionItem/index.ts b/src/resolvers/ActionItem/index.ts new file mode 100644 index 0000000000..dc5979e0d9 --- /dev/null +++ b/src/resolvers/ActionItem/index.ts @@ -0,0 +1,14 @@ +import type { ActionItemResolvers } from "../../types/generatedGraphQLTypes"; +import { assignee } from "./assignee"; +import { assigner } from "./assigner"; +import { actionItemCategory } from "./actionItemCategory"; +import { event } from "./event"; +import { creator } from "./creator"; + +export const ActionItem: ActionItemResolvers = { + assignee, + assigner, + actionItemCategory, + event, + creator, +}; diff --git a/src/resolvers/ActionItemCategory/creator.ts b/src/resolvers/ActionItemCategory/creator.ts new file mode 100644 index 0000000000..b9d355ed0a --- /dev/null +++ b/src/resolvers/ActionItemCategory/creator.ts @@ -0,0 +1,10 @@ +import type { ActionItemCategoryResolvers } from "../../types/generatedGraphQLTypes"; +import { User } from "../../models"; + +export const creator: ActionItemCategoryResolvers["creator"] = async ( + parent +) => { + return User.findOne({ + _id: parent.creatorId, + }).lean(); +}; diff --git a/src/resolvers/ActionItemCategory/index.ts b/src/resolvers/ActionItemCategory/index.ts new file mode 100644 index 0000000000..94bdba820f --- /dev/null +++ b/src/resolvers/ActionItemCategory/index.ts @@ -0,0 +1,8 @@ +import type { ActionItemCategoryResolvers } from "../../types/generatedGraphQLTypes"; +import { organization } from "./organization"; +import { creator } from "./creator"; + +export const ActionItemCategory: ActionItemCategoryResolvers = { + organization, + creator, +}; diff --git a/src/resolvers/ActionItemCategory/organization.ts b/src/resolvers/ActionItemCategory/organization.ts new file mode 100644 index 0000000000..98fcb42897 --- /dev/null +++ b/src/resolvers/ActionItemCategory/organization.ts @@ -0,0 +1,10 @@ +import type { ActionItemCategoryResolvers } from "../../types/generatedGraphQLTypes"; +import { Organization } from "../../models"; + +export const organization: ActionItemCategoryResolvers["organization"] = async ( + parent +) => { + return Organization.findOne({ + _id: parent.organizationId, + }).lean(); +}; diff --git a/src/resolvers/Event/actionItems.ts b/src/resolvers/Event/actionItems.ts new file mode 100644 index 0000000000..0fcc7b29fb --- /dev/null +++ b/src/resolvers/Event/actionItems.ts @@ -0,0 +1,12 @@ +import { ActionItem } from "../../models"; +import type { EventResolvers } from "../../types/generatedGraphQLTypes"; +/** + * This resolver function will fetch and return the action items related to the event from database. + * @param parent - An object that is the return value of the resolver for this field's parent. + * @returns An object that contains the list of all action items related to the event. + */ +export const actionItems: EventResolvers["actionItems"] = async (parent) => { + return await ActionItem.find({ + eventId: parent._id, + }).lean(); +}; diff --git a/src/resolvers/Event/index.ts b/src/resolvers/Event/index.ts index 1317580a5b..b3d1c1ee17 100644 --- a/src/resolvers/Event/index.ts +++ b/src/resolvers/Event/index.ts @@ -4,9 +4,11 @@ import { attendeesCheckInStatus } from "./attendeesCheckInStatus"; import { averageFeedbackScore } from "./averageFeedbackScore"; import { feedback } from "./feedback"; import { organization } from "./organization"; +import { actionItems } from "./actionItems"; import { creator } from "./creator"; export const Event: EventResolvers = { + actionItems, attendees, attendeesCheckInStatus, averageFeedbackScore, diff --git a/src/resolvers/Mutation/createActionItem.ts b/src/resolvers/Mutation/createActionItem.ts new file mode 100644 index 0000000000..c2ec83ff93 --- /dev/null +++ b/src/resolvers/Mutation/createActionItem.ts @@ -0,0 +1,160 @@ +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import type { InterfaceActionItem, InterfaceEvent } from "../../models"; +import { User, Event, ActionItemCategory, ActionItem } from "../../models"; +import { errors, requestContext } from "../../libraries"; +import { + USER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, + USER_NOT_MEMBER_FOR_ORGANIZATION, +} from "../../constants"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +import { Types } from "mongoose"; + +/** + * This function enables to create an action item. + * @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 user exists + * 3. If the asignee exists + * 4. If the actionItemCategory exists + * 5. If the asignee is a member of the organization + * 6. If the user is a member of the organization + * 7. If the event exists (if action item related to an event) + * 8. If the user is authorized. + * @returns Created action item + */ + +export const createActionItem: MutationResolvers["createActionItem"] = async ( + _parent, + args, + context +): Promise => { + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks whether currentUser with _id === context.userId exists. + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + const assignee = await User.findOne({ + _id: args.data.assigneeId, + }); + + // Checks whether the asignee exists. + if (assignee === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + const actionItemCategory = await ActionItemCategory.findOne({ + _id: args.actionItemCategoryId, + }).lean(); + + // Checks if the actionItemCategory exists + if (!actionItemCategory) { + throw new errors.NotFoundError( + requestContext.translate(ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE), + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.CODE, + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.PARAM + ); + } + + let asigneeIsOrganizationMember = false; + asigneeIsOrganizationMember = assignee.joinedOrganizations.some( + (organizationId) => + organizationId === actionItemCategory.organizationId || + Types.ObjectId(organizationId).equals(actionItemCategory.organizationId) + ); + + // Checks if the asignee is a member of the organization + if (!asigneeIsOrganizationMember) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE), + USER_NOT_MEMBER_FOR_ORGANIZATION.CODE, + USER_NOT_MEMBER_FOR_ORGANIZATION.PARAM + ); + } + + let currentUserIsEventAdmin = false; + + if (args.data.eventId) { + let currEvent: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([args.data.eventId]); + + currEvent = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + currEvent = await Event.findOne({ + _id: args.data.eventId, + }).lean(); + + if (currEvent !== null) { + await cacheEvents([currEvent]); + } + } + + // Checks whether currEvent exists. + if (!currEvent) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM + ); + } + + // Checks if the currUser is an admin of the event + currentUserIsEventAdmin = currEvent.admins.some( + (admin) => + admin === context.userID || Types.ObjectId(admin).equals(context.userId) + ); + } + + // Checks if the currUser is an admin of the organization + const currentUserIsOrgAdmin = currentUser.adminFor.some( + (ogranizationId) => + ogranizationId === actionItemCategory.organizationId || + Types.ObjectId(ogranizationId).equals(actionItemCategory.organizationId) + ); + + // Checks whether currentUser with _id === context.userId is authorized for the operation. + if ( + currentUserIsEventAdmin === false && + currentUserIsOrgAdmin === false && + currentUser.userType !== "SUPERADMIN" + ) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM + ); + } + + // Creates new action item. + const createActionItem = await ActionItem.create({ + assigneeId: args.data.assigneeId, + assignerId: context.userId, + actionItemCategoryId: args.actionItemCategoryId, + preCompletionNotes: args.data.preCompletionNotes, + dueDate: args.data.dueDate, + eventId: args.data.eventId, + creatorId: context.userId, + }); + + // Returns created action item. + return createActionItem.toObject(); +}; diff --git a/src/resolvers/Mutation/createActionItemCategory.ts b/src/resolvers/Mutation/createActionItemCategory.ts new file mode 100644 index 0000000000..2df722f91a --- /dev/null +++ b/src/resolvers/Mutation/createActionItemCategory.ts @@ -0,0 +1,93 @@ +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { User, ActionItemCategory, Organization } from "../../models"; +import { errors, requestContext } from "../../libraries"; +import { + USER_NOT_FOUND_ERROR, + ORGANIZATION_NOT_FOUND_ERROR, + ACTION_ITEM_CATEGORY_ALREADY_EXISTS, +} from "../../constants"; + +import { adminCheck } from "../../utilities"; +import { findOrganizationsInCache } from "../../services/OrganizationCache/findOrganizationsInCache"; +import { cacheOrganizations } from "../../services/OrganizationCache/cacheOrganizations"; + +/** + * This function enables to create an ActionItemCategory. + * @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 User exists + * 2. If the Organization exists + * 3. Is the User is Authorized + * 4. If the action item category already exists + * @returns Created ActionItemCategory + */ + +export const createActionItemCategory: MutationResolvers["createActionItemCategory"] = + async (_parent, args, context) => { + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks whether currentUser with _id == context.userId exists. + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + let organization; + + const organizationFoundInCache = await findOrganizationsInCache([ + args.organizationId, + ]); + + organization = organizationFoundInCache[0]; + + if (organizationFoundInCache[0] == null) { + organization = await Organization.findOne({ + _id: args.organizationId, + }).lean(); + + await cacheOrganizations([organization!]); + } + + // Checks whether the organization with _id === args.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 + ); + } + + // Checks whether the user is authorized to perform the operation + await adminCheck(context.userId, organization); + + // Checks whether an actionItemCategory with given name already exists for the current organization + const existingActionItemCategory = await ActionItemCategory.findOne({ + organizationId: organization?._id, + name: args.name, + }); + + if (existingActionItemCategory) { + throw new errors.ConflictError( + requestContext.translate(ACTION_ITEM_CATEGORY_ALREADY_EXISTS.MESSAGE), + ACTION_ITEM_CATEGORY_ALREADY_EXISTS.CODE, + ACTION_ITEM_CATEGORY_ALREADY_EXISTS.PARAM + ); + } + + // Creates new actionItemCategory. + const createdActionItemCategory = await ActionItemCategory.create({ + name: args.name, + organizationId: args.organizationId, + creatorId: context.userId, + }); + + // Returns created actionItemCategory. + return createdActionItemCategory.toObject(); + }; diff --git a/src/resolvers/Mutation/createOrganization.ts b/src/resolvers/Mutation/createOrganization.ts index f321982f14..46ab307946 100644 --- a/src/resolvers/Mutation/createOrganization.ts +++ b/src/resolvers/Mutation/createOrganization.ts @@ -3,7 +3,7 @@ import type { MutationResolvers, Address, } from "../../types/generatedGraphQLTypes"; -import { User, Organization } from "../../models"; +import { User, Organization, ActionItemCategory } from "../../models"; import { errors, requestContext } from "../../libraries"; import { LENGTH_VALIDATION_ERROR } from "../../constants"; import { superAdminCheck } from "../../utilities"; @@ -85,6 +85,13 @@ export const createOrganization: MutationResolvers["createOrganization"] = members: [context.userId], }); + // Creating a default actionItemCategory + await ActionItemCategory.create({ + name: "Default", + organizationId: createdOrganization._id, + creatorId: context.userId, + }); + await cacheOrganizations([createdOrganization.toObject()]); /* diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 5d3333da9a..68e6c4c916 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -18,6 +18,7 @@ import { cancelMembershipRequest } from "./cancelMembershipRequest"; import { updateUserRoleInOrganization } from "./updateUserRoleInOrganization"; import { checkIn } from "./checkIn"; import { createMember } from "./createMember"; +import { createActionItem } from "./createActionItem"; import { createAdmin } from "./createAdmin"; import { createComment } from "./createComment"; import { createDirectChat } from "./createDirectChat"; @@ -30,6 +31,7 @@ import { createPlugin } from "./createPlugin"; import { createAdvertisement } from "./createAdvertisement"; import { createPost } from "./createPost"; import { createSampleOrganization } from "./createSampleOrganization"; +import { createActionItemCategory } from "./createActionItemCategory"; import { createUserTag } from "./createUserTag"; import { deleteDonationById } from "./deleteDonationById"; import { forgotPassword } from "./forgotPassword"; @@ -46,6 +48,7 @@ import { registerForEvent } from "./registerForEvent"; import { rejectAdmin } from "./rejectAdmin"; import { rejectMembershipRequest } from "./rejectMembershipRequest"; import { removeAdmin } from "./removeAdmin"; +import { removeActionItem } from "./removeActionItem"; import { removeComment } from "./removeComment"; import { removeDirectChat } from "./removeDirectChat"; import { removeEvent } from "./removeEvent"; @@ -74,6 +77,8 @@ import { unblockUser } from "./unblockUser"; import { unlikeComment } from "./unlikeComment"; import { unlikePost } from "./unlikePost"; import { unregisterForEventByUser } from "./unregisterForEventByUser"; +import { updateActionItem } from "./updateActionItem"; +import { updateActionItemCategory } from "./updateActionItemCategory"; import { updateEvent } from "./updateEvent"; import { updateLanguage } from "./updateLanguage"; import { updateOrganization } from "./updateOrganization"; @@ -107,6 +112,7 @@ export const Mutation: MutationResolvers = { checkIn, createMember, createAdmin, + createActionItem, createComment, createAdvertisement, createDirectChat, @@ -118,6 +124,7 @@ export const Mutation: MutationResolvers = { createPlugin, createPost, createSampleOrganization, + createActionItemCategory, createUserTag, deleteDonationById, deleteAdvertisementById, @@ -135,6 +142,7 @@ export const Mutation: MutationResolvers = { rejectAdmin, rejectMembershipRequest, removeAdmin, + removeActionItem, removeComment, removeDirectChat, removeEvent, @@ -163,6 +171,8 @@ export const Mutation: MutationResolvers = { unlikeComment, unlikePost, unregisterForEventByUser, + updateActionItem, + updateActionItemCategory, updateEvent, updateLanguage, updateOrganization, diff --git a/src/resolvers/Mutation/removeActionItem.ts b/src/resolvers/Mutation/removeActionItem.ts new file mode 100644 index 0000000000..ae1c023213 --- /dev/null +++ b/src/resolvers/Mutation/removeActionItem.ts @@ -0,0 +1,120 @@ +import { + ACTION_ITEM_NOT_FOUND_ERROR, + EVENT_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../constants"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; +import { User, ActionItem, Event } from "../../models"; +import { Types } from "mongoose"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +/** + * This function enables to remove an action item. + * @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 user exists. + * 2. If the action item exists. + * 3. If the user is authorized. + * @returns deleted action item. + */ + +export const removeActionItem: MutationResolvers["removeActionItem"] = async ( + _parent, + args, + context +) => { + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks if the user exists + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + const actionItem = await ActionItem.findOne({ + _id: args.id, + }) + .populate("actionItemCategoryId") + .lean(); + + // Checks if the actionItem exists + if (!actionItem) { + throw new errors.NotFoundError( + requestContext.translate(ACTION_ITEM_NOT_FOUND_ERROR.MESSAGE), + ACTION_ITEM_NOT_FOUND_ERROR.CODE, + ACTION_ITEM_NOT_FOUND_ERROR.PARAM + ); + } + + const currentUserIsOrgAdmin = currentUser.adminFor.some( + (ogranizationId) => + ogranizationId === actionItem.actionItemCategoryId.organizationId || + Types.ObjectId(ogranizationId).equals( + actionItem.actionItemCategoryId.organizationId + ) + ); + + let currentUserIsEventAdmin = false; + + if (actionItem.eventId) { + let currEvent: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([actionItem.eventId]); + + currEvent = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + currEvent = await Event.findOne({ + _id: actionItem.eventId, + }).lean(); + + if (currEvent !== null) { + await cacheEvents([currEvent]); + } + } + + // Checks whether currEvent exists. + if (!currEvent) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM + ); + } + + // Checks if the currUser is an admin of the event + currentUserIsEventAdmin = currEvent.admins.some( + (admin) => + admin === context.userID || Types.ObjectId(admin).equals(context.userId) + ); + } + + // Checks if the user is authorized for the operation. + if ( + currentUserIsEventAdmin === false && + currentUserIsOrgAdmin === false && + currentUser.userType !== "SUPERADMIN" + ) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM + ); + } + + await ActionItem.deleteOne({ + _id: args.id, + }); + + return actionItem; +}; diff --git a/src/resolvers/Mutation/removeEvent.ts b/src/resolvers/Mutation/removeEvent.ts index d61f630abe..63322f847c 100644 --- a/src/resolvers/Mutation/removeEvent.ts +++ b/src/resolvers/Mutation/removeEvent.ts @@ -1,7 +1,7 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; import { errors, requestContext } from "../../libraries"; import type { InterfaceEvent } from "../../models"; -import { User, Event } from "../../models"; +import { User, Event, ActionItem } from "../../models"; import { USER_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, @@ -127,5 +127,7 @@ export const removeEvent: MutationResolvers["removeEvent"] = async ( await cacheEvents([updatedEvent]); } + await ActionItem.deleteMany({ eventId: event?._id }); + return event; }; diff --git a/src/resolvers/Mutation/removeOrganization.ts b/src/resolvers/Mutation/removeOrganization.ts index 145638eba3..5396eaaef8 100644 --- a/src/resolvers/Mutation/removeOrganization.ts +++ b/src/resolvers/Mutation/removeOrganization.ts @@ -6,6 +6,8 @@ import { Post, Comment, MembershipRequest, + ActionItemCategory, + ActionItem, } from "../../models"; import { superAdminCheck } from "../../utilities"; import { @@ -125,6 +127,24 @@ export const removeOrganization: MutationResolvers["removeOrganization"] = { $pull: { organizationsBlockedBy: organization._id } } ); + // Get the ids of all ActionItemCategories associated with the organization + const actionItemCategories = await ActionItemCategory.find({ + organizationId: organization?._id, + }); + const actionItemCategoriesIds = actionItemCategories.map( + (category) => category._id + ); + + // Remove all ActionItemCategory documents whose id is in the actionItemCategories array + await ActionItemCategory.deleteMany({ + _id: { $in: actionItemCategoriesIds }, + }); + + // Remove all ActionItem documents whose actionItemCategory is in the actionItemCategories array + await ActionItem.deleteMany({ + actionItemCategoryId: { $in: actionItemCategoriesIds }, + }); + // Deletes the organzation. await Organization.deleteOne({ _id: organization._id, diff --git a/src/resolvers/Mutation/updateActionItem.ts b/src/resolvers/Mutation/updateActionItem.ts new file mode 100644 index 0000000000..7a17387f2b --- /dev/null +++ b/src/resolvers/Mutation/updateActionItem.ts @@ -0,0 +1,190 @@ +import { + ACTION_ITEM_NOT_FOUND_ERROR, + EVENT_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, + USER_NOT_MEMBER_FOR_ORGANIZATION, +} from "../../constants"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import type { InterfaceEvent } from "../../models"; +import { User, ActionItem, Event } from "../../models"; +import { Types } from "mongoose"; +import { findEventsInCache } from "../../services/EventCache/findEventInCache"; +import { cacheEvents } from "../../services/EventCache/cacheEvents"; +/** + * This function enables to update an action item. + * @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 user exists. + * 2. If the new asignee exists. + * 2. If the action item exists. + * 4. If the new asignee is a member of the organization. + * 5. If the user is authorized. + * @returns Updated action item. + */ + +type UpdateActionItemInputType = { + assigneeId: string; + preCompletionNotes: string; + postCompletionNotes: string; + dueDate: Date; + completionDate: Date; + isCompleted: boolean; +}; + +export const updateActionItem: MutationResolvers["updateActionItem"] = async ( + _parent, + args, + context +) => { + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks if the user exists + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + const actionItem = await ActionItem.findOne({ + _id: args.id, + }) + .populate("actionItemCategoryId") + .lean(); + + // Checks if the actionItem exists + if (!actionItem) { + throw new errors.NotFoundError( + requestContext.translate(ACTION_ITEM_NOT_FOUND_ERROR.MESSAGE), + ACTION_ITEM_NOT_FOUND_ERROR.CODE, + ACTION_ITEM_NOT_FOUND_ERROR.PARAM + ); + } + + let sameAssignedUser = false; + + if (args.data.assigneeId) { + sameAssignedUser = Types.ObjectId(actionItem.assigneeId).equals( + args.data.assigneeId + ); + + if (!sameAssignedUser) { + const newAssignedUser = await User.findOne({ + _id: args.data.assigneeId, + }); + + // Checks if the new asignee exists + if (newAssignedUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + let userIsOrganizationMember = false; + const currorganizationId = actionItem.actionItemCategoryId.organizationId; + userIsOrganizationMember = newAssignedUser.joinedOrganizations.some( + (organizationId) => + organizationId === currorganizationId || + Types.ObjectId(organizationId).equals(currorganizationId) + ); + + // Checks if the new asignee is a member of the organization + if (!userIsOrganizationMember) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE), + USER_NOT_MEMBER_FOR_ORGANIZATION.CODE, + USER_NOT_MEMBER_FOR_ORGANIZATION.PARAM + ); + } + } + } + + const currentUserIsOrgAdmin = currentUser.adminFor.some( + (ogranizationId) => + ogranizationId === actionItem.actionItemCategoryId.organizationId || + Types.ObjectId(ogranizationId).equals( + actionItem.actionItemCategoryId.organizationId + ) + ); + + let currentUserIsEventAdmin = false; + + if (actionItem.eventId) { + let currEvent: InterfaceEvent | null; + + const eventFoundInCache = await findEventsInCache([actionItem.eventId]); + + currEvent = eventFoundInCache[0]; + + if (eventFoundInCache[0] === null) { + currEvent = await Event.findOne({ + _id: actionItem.eventId, + }).lean(); + + if (currEvent !== null) { + await cacheEvents([currEvent]); + } + } + + // Checks whether currEvent exists. + if (!currEvent) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM + ); + } + + // Checks if the currUser is an admin of the event + currentUserIsEventAdmin = currEvent.admins.some( + (admin) => + admin === context.userID || Types.ObjectId(admin).equals(context.userId) + ); + } + + // Checks if the user is authorized for the operation. + if ( + currentUserIsEventAdmin === false && + currentUserIsOrgAdmin === false && + currentUser.userType !== "SUPERADMIN" + ) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM + ); + } + + const updatedAssignmentDate = sameAssignedUser + ? actionItem.assignmentDate + : new Date(); + + const updatedAssigner = sameAssignedUser + ? actionItem.assignerId + : context.userId; + + const updatedActionItem = await ActionItem.findOneAndUpdate( + { + _id: args.id, + }, + { + ...(args.data as UpdateActionItemInputType), + assignmentDate: updatedAssignmentDate, + assignerId: updatedAssigner, + }, + { + new: true, + } + ).lean(); + + return updatedActionItem; +}; diff --git a/src/resolvers/Mutation/updateActionItemCategory.ts b/src/resolvers/Mutation/updateActionItemCategory.ts new file mode 100644 index 0000000000..d2deaf31f8 --- /dev/null +++ b/src/resolvers/Mutation/updateActionItemCategory.ts @@ -0,0 +1,71 @@ +import { + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../constants"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { errors, requestContext } from "../../libraries"; +import { User, ActionItemCategory } from "../../models"; +import { adminCheck } from "../../utilities"; +/** + * This function enables to update a actionItemCategory. + * @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 user exists. + * 2. If the actionItemCategory exists. + * 3. If the user is authorized. + * @returns Updated actionItemCategory. + */ + +type UpdateActionItemCategoryInputType = { + name: string; + isDisabled: boolean; +}; + +export const updateActionItemCategory: MutationResolvers["updateActionItemCategory"] = + async (_parent, args, context) => { + const currentUser = await User.findOne({ + _id: context.userId, + }); + + // Checks if the user exists + if (currentUser === null) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM + ); + } + + const actionItemCategory = await ActionItemCategory.findOne({ + _id: args.id, + }) + .populate("organizationId") + .lean(); + + // Checks if the actionItemCategory exists + if (!actionItemCategory) { + throw new errors.NotFoundError( + requestContext.translate(ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE), + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.CODE, + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.PARAM + ); + } + + await adminCheck(context.userId, actionItemCategory.organizationId); + + const updatedCategory = await ActionItemCategory.findOneAndUpdate( + { + _id: args.id, + }, + { + ...(args.data as UpdateActionItemCategoryInputType), + }, + { + new: true, + } + ).lean(); + + return updatedCategory; + }; diff --git a/src/resolvers/Organization/actionItemCategories.ts b/src/resolvers/Organization/actionItemCategories.ts new file mode 100644 index 0000000000..37dbe69f26 --- /dev/null +++ b/src/resolvers/Organization/actionItemCategories.ts @@ -0,0 +1,13 @@ +import { ActionItemCategory } from "../../models"; +import type { OrganizationResolvers } from "../../types/generatedGraphQLTypes"; +/** + * This resolver function will fetch and return the categories of the Organization from database. + * @param parent - An object that is the return value of the resolver for this field's parent. + * @returns An object that contains the list of all categories of the organization. + */ +export const actionItemCategories: OrganizationResolvers["actionItemCategories"] = + async (parent) => { + return await ActionItemCategory.find({ + organizationId: parent._id, + }).lean(); + }; diff --git a/src/resolvers/Organization/index.ts b/src/resolvers/Organization/index.ts index 999c15886a..1851c85ab1 100644 --- a/src/resolvers/Organization/index.ts +++ b/src/resolvers/Organization/index.ts @@ -6,10 +6,12 @@ import { image } from "./image"; import { members } from "./members"; import { pinnedPosts } from "./pinnedPosts"; import { membershipRequests } from "./membershipRequests"; +import { actionItemCategories } from "./actionItemCategories"; // import { userTags } from "./userTags"; export const Organization: OrganizationResolvers = { admins, + actionItemCategories, blockedUsers, creator, image, diff --git a/src/resolvers/Query/actionItem.ts b/src/resolvers/Query/actionItem.ts new file mode 100644 index 0000000000..6d8e2cbfa2 --- /dev/null +++ b/src/resolvers/Query/actionItem.ts @@ -0,0 +1,30 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { ActionItem } from "../../models"; +import { errors } from "../../libraries"; +import { ACTION_ITEM_NOT_FOUND_ERROR } from "../../constants"; +/** + * This query will fetch the action item with given id from the database. + * @param _parent- + * @param args - An object that contains `id` of the action item that need to be fetched. + * @returns An `action item` object. If the `action item` object is null then it throws `NotFoundError` error. + * @remarks You can learn about GraphQL `Resolvers` + * {@link https://www.apollographql.com/docs/apollo-server/data/resolvers/ | here}. + */ +export const actionItem: QueryResolvers["actionItem"] = async ( + _parent, + args +) => { + const actionItem = await ActionItem.findOne({ + _id: args.id, + }).lean(); + + if (!actionItem) { + throw new errors.NotFoundError( + ACTION_ITEM_NOT_FOUND_ERROR.DESC, + ACTION_ITEM_NOT_FOUND_ERROR.CODE, + ACTION_ITEM_NOT_FOUND_ERROR.PARAM + ); + } + + return actionItem; +}; diff --git a/src/resolvers/Query/actionItemCategoriesByOrganization.ts b/src/resolvers/Query/actionItemCategoriesByOrganization.ts new file mode 100644 index 0000000000..ffb8c85e48 --- /dev/null +++ b/src/resolvers/Query/actionItemCategoriesByOrganization.ts @@ -0,0 +1,16 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { ActionItemCategory } from "../../models"; +/** + * This query will fetch all categories for the organization from database. + * @param _parent- + * @param args - An object that contains `organizationId` which is the _id of the Organization. + * @returns A `categories` object that holds all categories for the Organization. + */ +export const actionItemCategoriesByOrganization: QueryResolvers["actionItemCategoriesByOrganization"] = + async (_parent, args) => { + const categories = await ActionItemCategory.find({ + organizationId: args.organizationId, + }).lean(); + + return categories; + }; diff --git a/src/resolvers/Query/actionItemCategory.ts b/src/resolvers/Query/actionItemCategory.ts new file mode 100644 index 0000000000..e0cfb1232e --- /dev/null +++ b/src/resolvers/Query/actionItemCategory.ts @@ -0,0 +1,30 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { ActionItemCategory } from "../../models"; +import { errors } from "../../libraries"; +import { ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR } from "../../constants"; +/** + * This query will fetch the actionItemCategory with given id from the database. + * @param _parent- + * @param args - An object that contains `id` of the actionItemCategory that need to be fetched. + * @returns An `actionItemCategory` object. If the `actionItemCategory` object is null then it throws `NotFoundError` error. + * @remarks You can learn about GraphQL `Resolvers` + * {@link https://www.apollographql.com/docs/apollo-server/data/resolvers/ | here}. + */ +export const actionItemCategory: QueryResolvers["actionItemCategory"] = async ( + _parent, + args +) => { + const actionItemCategory = await ActionItemCategory.findOne({ + _id: args.id, + }).lean(); + + if (!actionItemCategory) { + throw new errors.NotFoundError( + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.DESC, + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.CODE, + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.PARAM + ); + } + + return actionItemCategory; +}; diff --git a/src/resolvers/Query/actionItemsByEvent.ts b/src/resolvers/Query/actionItemsByEvent.ts new file mode 100644 index 0000000000..d031dfdb78 --- /dev/null +++ b/src/resolvers/Query/actionItemsByEvent.ts @@ -0,0 +1,18 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { ActionItem } from "../../models"; +/** + * This query will fetch all action items for an event from database. + * @param _parent- + * @param args - An object that contains `eventId` which is the _id of the Event. + * @returns An `actionItems` object that holds all action items for the Event. + */ +export const actionItemsByEvent: QueryResolvers["actionItemsByEvent"] = async ( + _parent, + args +) => { + const actionItems = await ActionItem.find({ + eventId: args.eventId, + }).lean(); + + return actionItems; +}; diff --git a/src/resolvers/Query/actionItemsByOrganization.ts b/src/resolvers/Query/actionItemsByOrganization.ts new file mode 100644 index 0000000000..4520f02af6 --- /dev/null +++ b/src/resolvers/Query/actionItemsByOrganization.ts @@ -0,0 +1,24 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { ActionItem, ActionItemCategory } from "../../models"; +/** + * This query will fetch all action items for an organization from database. + * @param _parent- + * @param args - An object that contains `organizationId` which is the _id of the Organization. + * @returns An `actionItems` object that holds all action items for the Event. + */ +export const actionItemsByOrganization: QueryResolvers["actionItemsByOrganization"] = + async (_parent, args) => { + // Get the ids of all ActionItemCategories associated with the organization + const actionItemCategories = await ActionItemCategory.find({ + organizationId: args.organizationId, + }); + const actionItemCategoriesIds = actionItemCategories.map( + (category) => category._id + ); + + const actionItems = await ActionItem.find({ + actionItemCategoryId: { $in: actionItemCategoriesIds }, + }).lean(); + + return actionItems; + }; diff --git a/src/resolvers/Query/index.ts b/src/resolvers/Query/index.ts index 6c4bbd2a2c..69e92fe7ce 100644 --- a/src/resolvers/Query/index.ts +++ b/src/resolvers/Query/index.ts @@ -1,4 +1,9 @@ import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import { actionItem } from "./actionItem"; +import { actionItemsByEvent } from "./actionItemsByEvent"; +import { actionItemCategory } from "./actionItemCategory"; +import { actionItemsByOrganization } from "./actionItemsByOrganization"; +import { actionItemCategoriesByOrganization } from "./actionItemCategoriesByOrganization"; import { checkAuth } from "./checkAuth"; import { customDataByOrganization } from "./customDataByOrganization"; import { customFieldsByOrganization } from "./customFieldsByOrganization"; @@ -29,6 +34,11 @@ import { getAdvertisements } from "./getAdvertisements"; import { usersConnection } from "./usersConnection"; export const Query: QueryResolvers = { + actionItem, + actionItemsByEvent, + actionItemCategory, + actionItemsByOrganization, + actionItemCategoriesByOrganization, checkAuth, customFieldsByOrganization, customDataByOrganization, diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index b746c0c75b..7ded81a68f 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -1,4 +1,6 @@ import type { Resolvers } from "../types/generatedGraphQLTypes"; +import { ActionItem } from "./ActionItem"; +import { ActionItemCategory } from "./ActionItemCategory"; import { CheckIn } from "./CheckIn"; import { Comment } from "./Comment"; import { DirectChat } from "./DirectChat"; @@ -31,6 +33,8 @@ import { } from "graphql-scalars"; const resolvers: Resolvers = { + ActionItem, + ActionItemCategory, CheckIn, Comment, DirectChat, diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index 89750c91f8..a72ed4107a 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -35,6 +35,13 @@ export const inputs = gql` organizationId: ID! } + input CreateActionItemInput { + assigneeId: ID! + preCompletionNotes: String + dueDate: Date + eventId: ID + } + input CursorPaginationInput { cursor: String direction: PaginationDirection! @@ -237,6 +244,15 @@ export const inputs = gql` tagId: ID! } + input UpdateActionItemInput { + assigneeId: ID + preCompletionNotes: String + postCompletionNotes: String + dueDate: Date + completionDate: Date + isCompleted: Boolean + } + input UpdateEventInput { title: String description: String @@ -290,6 +306,11 @@ export const inputs = gql` name: String! } + input UpdateActionItemCategoryInput { + name: String + isDisabled: Boolean + } + input AddressInput { city: String countryCode: String diff --git a/src/typeDefs/mutations.ts b/src/typeDefs/mutations.ts index 5038ba09d2..1843e92141 100644 --- a/src/typeDefs/mutations.ts +++ b/src/typeDefs/mutations.ts @@ -56,6 +56,16 @@ export const mutations = gql` @auth @role(requires: SUPERADMIN) + createActionItem( + data: CreateActionItemInput! + actionItemCategoryId: ID! + ): ActionItem! @auth + + createActionItemCategory( + name: String! + organizationId: ID! + ): ActionItemCategory! @auth + createComment(postId: ID!, data: CommentInput!): Comment @auth createDirectChat(data: createChatInput!): DirectChat! @auth @@ -135,6 +145,8 @@ export const mutations = gql` @auth @role(requires: SUPERADMIN) + removeActionItem(id: ID!): ActionItem! @auth + removeOrganizationCustomField( organizationId: ID! customFieldId: ID! @@ -200,6 +212,13 @@ export const mutations = gql` unregisterForEventByUser(id: ID!): Event! @auth + updateActionItem(id: ID!, data: UpdateActionItemInput!): ActionItem @auth + + updateActionItemCategory( + id: ID! + data: UpdateActionItemCategoryInput! + ): ActionItemCategory @auth + updateAdvertisement( input: UpdateAdvertisementInput! ): UpdateAdvertisementPayload @auth diff --git a/src/typeDefs/queries.ts b/src/typeDefs/queries.ts index f890e73e22..975f839ad9 100644 --- a/src/typeDefs/queries.ts +++ b/src/typeDefs/queries.ts @@ -7,6 +7,18 @@ export const queries = gql` type Query { adminPlugin(orgId: ID!): [Plugin] + actionItem(id: ID!): ActionItem + + actionItemsByEvent(eventId: ID!): [ActionItem] + + actionItemsByOrganization(organizationId: ID!): [ActionItem] + + actionItemCategory(id: ID!): ActionItemCategory + + actionItemCategoriesByOrganization( + organizationId: ID! + ): [ActionItemCategory] + checkAuth: User! @auth customFieldsByOrganization(id: ID!): [OrganizationCustomField] diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index ed97691455..b6674de48b 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -16,6 +16,34 @@ export const types = gql` refreshToken: String! } + type ActionItemCategory { + _id: ID! + name: String! + organization: Organization + isDisabled: Boolean! + creator: User + createdAt: Date! + updatedAt: Date! + } + + # Action Item for a ActionItemCategory + type ActionItem { + _id: ID! + assignee: User + assigner: User + actionItemCategory: ActionItemCategory + preCompletionNotes: String + postCompletionNotes: String + assignmentDate: Date! + dueDate: Date! + completionDate: Date! + isCompleted: Boolean! + event: Event + creator: User + createdAt: Date! + updatedAt: Date! + } + # Stores the detail of an check in of an user in an event type CheckIn { _id: ID! @@ -135,6 +163,7 @@ export const types = gql` attendees: [User] # For each attendee, gives information about whether he/she has checked in yet or not attendeesCheckInStatus: [CheckInStatus!]! + actionItems: [ActionItem] admins(adminId: ID): [User!] status: Status! feedback: [Feedback!]! @@ -230,6 +259,7 @@ export const types = gql` createdAt: DateTime! updatedAt: DateTime! members: [User] + actionItemCategories: [ActionItemCategory] admins(adminId: ID): [User!] membershipRequests: [MembershipRequest] userRegistrationRequired: Boolean! diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index cf43d14495..a9a9f0fad8 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -1,4 +1,6 @@ import type { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; +import type { InterfaceActionItem as InterfaceActionItemModel } from '../models/ActionItem'; +import type { InterfaceActionItemCategory as InterfaceActionItemCategoryModel } from '../models/ActionItemCategory'; import type { InterfaceCheckIn as InterfaceCheckInModel } from '../models/CheckIn'; import type { InterfaceMessageChat as InterfaceMessageChatModel } from '../models/MessageChat'; import type { InterfaceComment as InterfaceCommentModel } from '../models/Comment'; @@ -51,6 +53,35 @@ export type Scalars = { Upload: { input: any; output: any; } }; +export type ActionItem = { + __typename?: 'ActionItem'; + _id: Scalars['ID']['output']; + actionItemCategory?: Maybe; + assignee?: Maybe; + assigner?: Maybe; + assignmentDate: Scalars['Date']['output']; + completionDate: Scalars['Date']['output']; + createdAt: Scalars['Date']['output']; + creator?: Maybe; + dueDate: Scalars['Date']['output']; + event?: Maybe; + isCompleted: Scalars['Boolean']['output']; + postCompletionNotes?: Maybe; + preCompletionNotes?: Maybe; + updatedAt: Scalars['Date']['output']; +}; + +export type ActionItemCategory = { + __typename?: 'ActionItemCategory'; + _id: Scalars['ID']['output']; + createdAt: Scalars['Date']['output']; + creator?: Maybe; + isDisabled: Scalars['Boolean']['output']; + name: Scalars['String']['output']; + organization?: Maybe; + updatedAt: Scalars['Date']['output']; +}; + export type Address = { __typename?: 'Address'; city?: Maybe; @@ -163,6 +194,13 @@ export type ConnectionPageInfo = { startCursor?: Maybe; }; +export type CreateActionItemInput = { + assigneeId: Scalars['ID']['input']; + dueDate?: InputMaybe; + eventId?: InputMaybe; + preCompletionNotes?: InputMaybe; +}; + export type CreateUserTagInput = { name: Scalars['String']['input']; organizationId: Scalars['ID']['input']; @@ -260,6 +298,7 @@ export type Error = { export type Event = { __typename?: 'Event'; _id: Scalars['ID']['output']; + actionItems?: Maybe>>; admins?: Maybe>; allDay: Scalars['Boolean']['output']; attendees?: Maybe>>; @@ -555,6 +594,8 @@ export type Mutation = { blockUser: User; cancelMembershipRequest: MembershipRequest; checkIn: CheckIn; + createActionItem: ActionItem; + createActionItemCategory: ActionItemCategory; createAdmin: User; createAdvertisement: Advertisement; createComment?: Maybe; @@ -584,6 +625,7 @@ export type Mutation = { registerForEvent: Event; rejectAdmin: Scalars['Boolean']['output']; rejectMembershipRequest: MembershipRequest; + removeActionItem: ActionItem; removeAdmin: User; removeAdvertisement?: Maybe; removeComment?: Maybe; @@ -613,6 +655,8 @@ export type Mutation = { unlikeComment?: Maybe; unlikePost?: Maybe; unregisterForEventByUser: Event; + updateActionItem?: Maybe; + updateActionItemCategory?: Maybe; updateAdvertisement?: Maybe; updateEvent: Event; updateLanguage: User; @@ -720,6 +764,18 @@ export type MutationCheckInArgs = { }; +export type MutationCreateActionItemArgs = { + actionItemCategoryId: Scalars['ID']['input']; + data: CreateActionItemInput; +}; + + +export type MutationCreateActionItemCategoryArgs = { + name: Scalars['String']['input']; + organizationId: Scalars['ID']['input']; +}; + + export type MutationCreateAdminArgs = { data: UserAndOrganizationInput; }; @@ -871,6 +927,11 @@ export type MutationRejectMembershipRequestArgs = { }; +export type MutationRemoveActionItemArgs = { + id: Scalars['ID']['input']; +}; + + export type MutationRemoveAdminArgs = { data: UserAndOrganizationInput; }; @@ -1009,6 +1070,18 @@ export type MutationUnregisterForEventByUserArgs = { }; +export type MutationUpdateActionItemArgs = { + data: UpdateActionItemInput; + id: Scalars['ID']['input']; +}; + + +export type MutationUpdateActionItemCategoryArgs = { + data: UpdateActionItemCategoryInput; + id: Scalars['ID']['input']; +}; + + export type MutationUpdateAdvertisementArgs = { input: UpdateAdvertisementInput; }; @@ -1078,6 +1151,7 @@ export type OtpInput = { export type Organization = { __typename?: 'Organization'; _id: Scalars['ID']['output']; + actionItemCategories?: Maybe>>; address?: Maybe
; admins?: Maybe>; apiUrl: Scalars['URL']['output']; @@ -1320,6 +1394,11 @@ export type PostWhereInput = { export type Query = { __typename?: 'Query'; + actionItem?: Maybe; + actionItemCategoriesByOrganization?: Maybe>>; + actionItemCategory?: Maybe; + actionItemsByEvent?: Maybe>>; + actionItemsByOrganization?: Maybe>>; adminPlugin?: Maybe>>; checkAuth: User; customDataByOrganization: Array; @@ -1356,6 +1435,31 @@ export type Query = { }; +export type QueryActionItemArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryActionItemCategoriesByOrganizationArgs = { + organizationId: Scalars['ID']['input']; +}; + + +export type QueryActionItemCategoryArgs = { + id: Scalars['ID']['input']; +}; + + +export type QueryActionItemsByEventArgs = { + eventId: Scalars['ID']['input']; +}; + + +export type QueryActionItemsByOrganizationArgs = { + organizationId: Scalars['ID']['input']; +}; + + export type QueryAdminPluginArgs = { orgId: Scalars['ID']['input']; }; @@ -1576,6 +1680,20 @@ export type UnauthorizedError = Error & { message: Scalars['String']['output']; }; +export type UpdateActionItemCategoryInput = { + isDisabled?: InputMaybe; + name?: InputMaybe; +}; + +export type UpdateActionItemInput = { + assigneeId?: InputMaybe; + completionDate?: InputMaybe; + dueDate?: InputMaybe; + isCompleted?: InputMaybe; + postCompletionNotes?: InputMaybe; + preCompletionNotes?: InputMaybe; +}; + export type UpdateAdvertisementInput = { _id: Scalars['ID']['input']; endDate?: InputMaybe; @@ -1976,6 +2094,8 @@ export type ResolversInterfaceTypes> = { /** Mapping between all available schema types and the resolvers types */ export type ResolversTypes = { + ActionItem: ResolverTypeWrapper; + ActionItemCategory: ResolverTypeWrapper; Address: ResolverTypeWrapper
; AddressInput: AddressInput; Advertisement: ResolverTypeWrapper & { creator?: Maybe }>; @@ -1993,6 +2113,7 @@ export type ResolversTypes = { ConnectionError: ResolverTypeWrapper['ConnectionError']>; ConnectionPageInfo: ResolverTypeWrapper; CountryCode: ResolverTypeWrapper; + CreateActionItemInput: CreateActionItemInput; CreateUserTagInput: CreateUserTagInput; CursorPaginationInput: CursorPaginationInput; Date: ResolverTypeWrapper; @@ -2076,6 +2197,8 @@ export type ResolversTypes = { URL: ResolverTypeWrapper; UnauthenticatedError: ResolverTypeWrapper; UnauthorizedError: ResolverTypeWrapper; + UpdateActionItemCategoryInput: UpdateActionItemCategoryInput; + UpdateActionItemInput: UpdateActionItemInput; UpdateAdvertisementInput: UpdateAdvertisementInput; UpdateAdvertisementPayload: ResolverTypeWrapper & { advertisement?: Maybe }>; UpdateEventInput: UpdateEventInput; @@ -2111,6 +2234,8 @@ export type ResolversTypes = { /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = { + ActionItem: InterfaceActionItemModel; + ActionItemCategory: InterfaceActionItemCategoryModel; Address: Address; AddressInput: AddressInput; Advertisement: Omit & { creator?: Maybe }; @@ -2127,6 +2252,7 @@ export type ResolversParentTypes = { ConnectionError: ResolversUnionTypes['ConnectionError']; ConnectionPageInfo: ConnectionPageInfo; CountryCode: Scalars['CountryCode']['output']; + CreateActionItemInput: CreateActionItemInput; CreateUserTagInput: CreateUserTagInput; CursorPaginationInput: CursorPaginationInput; Date: Scalars['Date']['output']; @@ -2199,6 +2325,8 @@ export type ResolversParentTypes = { URL: Scalars['URL']['output']; UnauthenticatedError: UnauthenticatedError; UnauthorizedError: UnauthorizedError; + UpdateActionItemCategoryInput: UpdateActionItemCategoryInput; + UpdateActionItemInput: UpdateActionItemInput; UpdateAdvertisementInput: UpdateAdvertisementInput; UpdateAdvertisementPayload: Omit & { advertisement?: Maybe }; UpdateEventInput: UpdateEventInput; @@ -2240,6 +2368,35 @@ export type RoleDirectiveArgs = { export type RoleDirectiveResolver = DirectiveResolverFn; +export type ActionItemResolvers = { + _id?: Resolver; + actionItemCategory?: Resolver, ParentType, ContextType>; + assignee?: Resolver, ParentType, ContextType>; + assigner?: Resolver, ParentType, ContextType>; + assignmentDate?: Resolver; + completionDate?: Resolver; + createdAt?: Resolver; + creator?: Resolver, ParentType, ContextType>; + dueDate?: Resolver; + event?: Resolver, ParentType, ContextType>; + isCompleted?: Resolver; + postCompletionNotes?: Resolver, ParentType, ContextType>; + preCompletionNotes?: Resolver, ParentType, ContextType>; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type ActionItemCategoryResolvers = { + _id?: Resolver; + createdAt?: Resolver; + creator?: Resolver, ParentType, ContextType>; + isDisabled?: Resolver; + name?: Resolver; + organization?: Resolver, ParentType, ContextType>; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type AddressResolvers = { city?: Resolver, ParentType, ContextType>; countryCode?: Resolver, ParentType, ContextType>; @@ -2394,6 +2551,7 @@ export type ErrorResolvers = { _id?: Resolver; + actionItems?: Resolver>>, ParentType, ContextType>; admins?: Resolver>, ParentType, ContextType, Partial>; allDay?: Resolver; attendees?: Resolver>>, ParentType, ContextType>; @@ -2583,6 +2741,8 @@ export type MutationResolvers>; cancelMembershipRequest?: Resolver>; checkIn?: Resolver>; + createActionItem?: Resolver>; + createActionItemCategory?: Resolver>; createAdmin?: Resolver>; createAdvertisement?: Resolver>; createComment?: Resolver, ParentType, ContextType, RequireFields>; @@ -2612,6 +2772,7 @@ export type MutationResolvers>; rejectAdmin?: Resolver>; rejectMembershipRequest?: Resolver>; + removeActionItem?: Resolver>; removeAdmin?: Resolver>; removeAdvertisement?: Resolver, ParentType, ContextType, RequireFields>; removeComment?: Resolver, ParentType, ContextType, RequireFields>; @@ -2641,6 +2802,8 @@ export type MutationResolvers, ParentType, ContextType, RequireFields>; unlikePost?: Resolver, ParentType, ContextType, RequireFields>; unregisterForEventByUser?: Resolver>; + updateActionItem?: Resolver, ParentType, ContextType, RequireFields>; + updateActionItemCategory?: Resolver, ParentType, ContextType, RequireFields>; updateAdvertisement?: Resolver, ParentType, ContextType, RequireFields>; updateEvent?: Resolver>; updateLanguage?: Resolver>; @@ -2656,6 +2819,7 @@ export type MutationResolvers = { _id?: Resolver; + actionItemCategories?: Resolver>>, ParentType, ContextType>; address?: Resolver, ParentType, ContextType>; admins?: Resolver>, ParentType, ContextType, Partial>; apiUrl?: Resolver; @@ -2762,6 +2926,11 @@ export type PostConnectionResolvers = { + actionItem?: Resolver, ParentType, ContextType, RequireFields>; + actionItemCategoriesByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; + actionItemCategory?: Resolver, ParentType, ContextType, RequireFields>; + actionItemsByEvent?: Resolver>>, ParentType, ContextType, RequireFields>; + actionItemsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; adminPlugin?: Resolver>>, ParentType, ContextType, RequireFields>; checkAuth?: Resolver; customDataByOrganization?: Resolver, ParentType, ContextType, RequireFields>; @@ -2972,6 +3141,8 @@ export type UsersConnectionResultResolvers = { + ActionItem?: ActionItemResolvers; + ActionItemCategory?: ActionItemCategoryResolvers; Address?: AddressResolvers; Advertisement?: AdvertisementResolvers; AggregatePost?: AggregatePostResolvers; diff --git a/tests/helpers/actionItem.ts b/tests/helpers/actionItem.ts new file mode 100644 index 0000000000..1ebb112f69 --- /dev/null +++ b/tests/helpers/actionItem.ts @@ -0,0 +1,119 @@ +import type { InterfaceActionItem } from "../../src/models"; +import { ActionItem, ActionItemCategory, Event } from "../../src/models"; +import type { Document } from "mongoose"; +import { + createTestUser, + createTestUserAndOrganization, + type TestOrganizationType, + type TestUserType, +} from "./userAndOrg"; +import type { TestActionItemCategoryType } from "./actionItemCategory"; +import { createTestCategory } from "./actionItemCategory"; +import { nanoid } from "nanoid"; +import type { TestEventType } from "./events"; + +export type TestActionItemType = InterfaceActionItem & Document; + +export const createTestActionItem = async (): Promise< + [ + TestUserType, + TestOrganizationType, + TestActionItemCategoryType, + TestActionItemType, + TestUserType + ] +> => { + const [testUser, testOrganization] = await createTestUserAndOrganization(); + const randomUser = await createTestUser(); + + const testCategory = await ActionItemCategory.create({ + creatorId: testUser?._id, + organizationId: testOrganization?._id, + name: "Default", + }); + + const testActionItem = await ActionItem.create({ + creatorId: testUser?._id, + assigneeId: randomUser?._id, + assignerId: testUser?._id, + actionItemCategoryId: testCategory?._id, + }); + + return [testUser, testOrganization, testCategory, testActionItem, randomUser]; +}; + +interface InterfaceCreateNewTestAction { + currUserId: string; + assignedUserId: string; + actionItemCategoryId: string; +} + +export const createNewTestActionItem = async ({ + currUserId, + assignedUserId, + actionItemCategoryId, +}: InterfaceCreateNewTestAction): Promise => { + const newTestActionItem = await ActionItem.create({ + creatorId: currUserId, + assigneeId: assignedUserId, + assignerId: currUserId, + actionItemCategoryId: actionItemCategoryId, + }); + + return newTestActionItem; +}; + +export const createTestActionItems = async (): Promise< + [TestUserType, TestEventType, TestOrganizationType] +> => { + const randomUser = await createTestUser(); + const [testUser, testOrganization, testCategory] = await createTestCategory(); + + const testActionItem1 = await ActionItem.create({ + creatorId: testUser?._id, + assigneeId: randomUser?._id, + assignerId: testUser?._id, + actionItemCategoryId: testCategory?._id, + }); + + const testActionItem2 = await ActionItem.create({ + creatorId: testUser?._id, + assigneeId: randomUser?._id, + assignerId: testUser?._id, + actionItemCategoryId: testCategory?._id, + }); + + const testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: true, + isPublic: true, + isRegisterable: true, + creatorId: testUser?._id, + admins: [testUser?._id], + organization: testOrganization?._id, + actionItems: [testActionItem1?._id, testActionItem2?._id], + }); + + await ActionItem.updateOne( + { + _id: testActionItem1?._id, + }, + { + eventId: testEvent?._id, + } + ); + + await ActionItem.updateOne( + { + _id: testActionItem2?._id, + }, + { + eventId: testEvent?._id, + } + ); + + return [testUser, testEvent, testOrganization]; +}; diff --git a/tests/helpers/actionItemCategory.ts b/tests/helpers/actionItemCategory.ts new file mode 100644 index 0000000000..c9c73ac2bd --- /dev/null +++ b/tests/helpers/actionItemCategory.ts @@ -0,0 +1,43 @@ +import type { InterfaceActionItemCategory } from "../../src/models"; +import { ActionItemCategory, Organization } from "../../src/models"; +import type { Document } from "mongoose"; +import { + createTestUserAndOrganization, + type TestOrganizationType, + type TestUserType, +} from "./userAndOrg"; + +export type TestActionItemCategoryType = InterfaceActionItemCategory & Document; + +export const createTestCategory = async (): Promise< + [TestUserType, TestOrganizationType, TestActionItemCategoryType] +> => { + const [testUser, testOrganization] = await createTestUserAndOrganization(); + const testCategory = await ActionItemCategory.create({ + creatorId: testUser?._id, + organizationId: testOrganization?._id, + name: "Default", + }); + + return [testUser, testOrganization, testCategory]; +}; + +export const createTestCategories = async (): Promise< + [TestUserType, TestOrganizationType] +> => { + const [testUser, testOrganization] = await createTestUserAndOrganization(); + + await ActionItemCategory.create({ + creatorId: testUser?._id, + organizationId: testOrganization?._id, + name: "Default", + }); + + await ActionItemCategory.create({ + creatorId: testUser?._id, + organizationId: testOrganization?._id, + name: "Default2", + }); + + return [testUser, testOrganization]; +}; diff --git a/tests/resolvers/ActionItem/assignee.spec.ts b/tests/resolvers/ActionItem/assignee.spec.ts new file mode 100644 index 0000000000..d910b54919 --- /dev/null +++ b/tests/resolvers/ActionItem/assignee.spec.ts @@ -0,0 +1,36 @@ +import "dotenv/config"; +import { assignee as assigneeResolver } from "../../../src/resolvers/ActionItem/assignee"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import { User } from "../../../src/models"; +import type { TestUserType } from "../../helpers/userAndOrg"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { createTestActionItem } from "../../helpers/actionItem"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let randomTestUser: TestUserType; +let testActionItem: TestActionItemType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, , , testActionItem, randomTestUser] = await createTestActionItem(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> ActionItem -> assignee", () => { + it(`returns the assignee for parent action item`, async () => { + const parent = testActionItem?.toObject(); + + const assignedToPayload = await assigneeResolver?.(parent, {}, {}); + + const assignedToObject = await User.findOne({ + _id: randomTestUser?._id, + }).lean(); + + expect(assignedToPayload).toEqual(assignedToObject); + }); +}); diff --git a/tests/resolvers/ActionItem/assigner.spec.ts b/tests/resolvers/ActionItem/assigner.spec.ts new file mode 100644 index 0000000000..e27fee41cb --- /dev/null +++ b/tests/resolvers/ActionItem/assigner.spec.ts @@ -0,0 +1,36 @@ +import "dotenv/config"; +import { assigner as assignerResolver } from "../../../src/resolvers/ActionItem/assigner"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import { User } from "../../../src/models"; +import type { TestUserType } from "../../helpers/userAndOrg"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { createTestActionItem } from "../../helpers/actionItem"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testActionItem: TestActionItemType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [testUser, , , testActionItem] = await createTestActionItem(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> ActionItem -> assigner", () => { + it(`returns the assigner for parent action item`, async () => { + const parent = testActionItem?.toObject(); + + const assignedByPayload = await assignerResolver?.(parent, {}, {}); + + const assignedByObject = await User.findOne({ + _id: testUser?._id, + }).lean(); + + expect(assignedByPayload).toEqual(assignedByObject); + }); +}); diff --git a/tests/resolvers/ActionItem/category.spec.ts b/tests/resolvers/ActionItem/category.spec.ts new file mode 100644 index 0000000000..d034f89468 --- /dev/null +++ b/tests/resolvers/ActionItem/category.spec.ts @@ -0,0 +1,40 @@ +import "dotenv/config"; +import { actionItemCategory as actionItemCategoryResolver } from "../../../src/resolvers/ActionItem/actionItemCategory"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import { ActionItemCategory } from "../../../src/models"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { createTestActionItem } from "../../helpers/actionItem"; +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testActionItem: TestActionItemType; +let testCategory: TestActionItemCategoryType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, , testCategory, testActionItem] = await createTestActionItem(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> ActionItem -> actionItemCategory", () => { + it(`returns the actionItemCategory for parent action item`, async () => { + const parent = testActionItem?.toObject(); + + const actionItemCategoryPayload = await actionItemCategoryResolver?.( + parent, + {}, + {} + ); + + const actionItemCategoryObject = await ActionItemCategory.findOne({ + _id: testCategory?._id, + }).lean(); + + expect(actionItemCategoryPayload).toEqual(actionItemCategoryObject); + }); +}); diff --git a/tests/resolvers/ActionItem/creator.spec.ts b/tests/resolvers/ActionItem/creator.spec.ts new file mode 100644 index 0000000000..91a19580bf --- /dev/null +++ b/tests/resolvers/ActionItem/creator.spec.ts @@ -0,0 +1,36 @@ +import "dotenv/config"; +import { creator as creatorResolver } from "../../../src/resolvers/ActionItem/creator"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import { User } from "../../../src/models"; +import type { TestUserType } from "../../helpers/userAndOrg"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { createTestActionItem } from "../../helpers/actionItem"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testActionItem: TestActionItemType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [testUser, , , testActionItem] = await createTestActionItem(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> ActionItem -> creator", () => { + it(`returns the creator for parent action item`, async () => { + const parent = testActionItem?.toObject(); + + const createdByPayload = await creatorResolver?.(parent, {}, {}); + + const createdByObject = await User.findOne({ + _id: testUser?._id, + }).lean(); + + expect(createdByPayload).toEqual(createdByObject); + }); +}); diff --git a/tests/resolvers/ActionItem/event.spec.ts b/tests/resolvers/ActionItem/event.spec.ts new file mode 100644 index 0000000000..f93397e5a5 --- /dev/null +++ b/tests/resolvers/ActionItem/event.spec.ts @@ -0,0 +1,67 @@ +import "dotenv/config"; +import { event as eventResolver } from "../../../src/resolvers/ActionItem/event"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { InterfaceActionItem } from "../../../src/models"; +import { ActionItem, Event } from "../../../src/models"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { createTestActionItem } from "../../helpers/actionItem"; +import { nanoid } from "nanoid"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testOrganization: TestOrganizationType; +let testActionItem: TestActionItemType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [testUser, testOrganization, , testActionItem] = await createTestActionItem(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> ActionItem -> event", () => { + it(`returns the event for parent action item`, async () => { + const testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: true, + isPublic: true, + isRegisterable: true, + creatorId: testUser?._id, + admins: [testUser?._id], + organization: testOrganization?._id, + }); + + const updatedTestActionItem = await ActionItem.findOneAndUpdate( + { + _id: testActionItem?._id, + }, + { + eventId: testEvent?._id, + }, + { + new: true, + } + ); + + const parent = updatedTestActionItem?.toObject(); + + const eventByPayload = await eventResolver?.( + parent as InterfaceActionItem, + {}, + {} + ); + + expect(eventByPayload?._id).toEqual(updatedTestActionItem?.eventId); + }); +}); diff --git a/tests/resolvers/ActionItemCategory/creator.spec.ts b/tests/resolvers/ActionItemCategory/creator.spec.ts new file mode 100644 index 0000000000..dd6fe80367 --- /dev/null +++ b/tests/resolvers/ActionItemCategory/creator.spec.ts @@ -0,0 +1,36 @@ +import "dotenv/config"; +import { creator as creatorResolver } from "../../../src/resolvers/ActionItemCategory/creator"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import { User } from "../../../src/models"; +import type { TestUserType } from "../../helpers/userAndOrg"; +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; +import { createTestCategory } from "../../helpers/actionItemCategory"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testUser: TestUserType; +let testCategory: TestActionItemCategoryType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [testUser, , testCategory] = await createTestCategory(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> ActionItemCategory -> creator", () => { + it(`returns the creator for parent actionItemCategory`, async () => { + const parent = testCategory?.toObject(); + + const createdByPayload = await creatorResolver?.(parent, {}, {}); + + const createdByObject = await User.findOne({ + _id: testUser?._id, + }).lean(); + + expect(createdByPayload).toEqual(createdByObject); + }); +}); diff --git a/tests/resolvers/ActionItemCategory/organization.spec.ts b/tests/resolvers/ActionItemCategory/organization.spec.ts new file mode 100644 index 0000000000..2c1a06e0d2 --- /dev/null +++ b/tests/resolvers/ActionItemCategory/organization.spec.ts @@ -0,0 +1,36 @@ +import "dotenv/config"; +import { organization as organizationResolver } from "../../../src/resolvers/ActionItemCategory/organization"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import { Organization } from "../../../src/models"; +import { type TestOrganizationType } from "../../helpers/userAndOrg"; +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; +import { createTestCategory } from "../../helpers/actionItemCategory"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; +let testCategory: TestActionItemCategoryType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testOrganization, testCategory] = await createTestCategory(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> ActionItemCategory -> org", () => { + it(`returns the organization object for parent actionItemCategory`, async () => { + const parent = testCategory?.toObject(); + + const orgPayload = await organizationResolver?.(parent, {}, {}); + + const orgObject = await Organization.findOne({ + _id: testOrganization?._id, + }).lean(); + + expect(orgPayload).toEqual(orgObject); + }); +}); diff --git a/tests/resolvers/Event/actionItems.spec.ts b/tests/resolvers/Event/actionItems.spec.ts new file mode 100644 index 0000000000..a984e00f05 --- /dev/null +++ b/tests/resolvers/Event/actionItems.spec.ts @@ -0,0 +1,35 @@ +import "dotenv/config"; +import { actionItems as actionItemsResolver } from "../../../src/resolvers/Event/actionItems"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { ActionItem } from "../../../src/models"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestEventType } from "../../helpers/events"; +import { createTestActionItems } from "../../helpers/actionItem"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testEvent] = await createTestActionItems(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Organization -> actionItems", () => { + it(`returns all actionItems for parent Event`, async () => { + const parent = testEvent?.toObject(); + if (parent) { + const actionItemsPayload = await actionItemsResolver?.(parent, {}, {}); + + const actionItems = await ActionItem.find({ + eventId: testEvent?._id, + }).lean(); + + expect(actionItemsPayload).toEqual(actionItems); + } + }); +}); diff --git a/tests/resolvers/Mutation/createActionItem.spec.ts b/tests/resolvers/Mutation/createActionItem.spec.ts new file mode 100644 index 0000000000..c2b0ec22fc --- /dev/null +++ b/tests/resolvers/Mutation/createActionItem.spec.ts @@ -0,0 +1,280 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import type { MutationCreateActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; +import { createActionItem as createActionItemResolver } from "../../../src/resolvers/Mutation/createActionItem"; +import { connect, disconnect } from "../../helpers/db"; +import { + USER_NOT_FOUND_ERROR, + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, + USER_NOT_MEMBER_FOR_ORGANIZATION, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { createTestUser } from "../../helpers/userAndOrg"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; + +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; +import { createTestCategory } from "../../helpers/actionItemCategory"; +import type { TestEventType } from "../../helpers/events"; +import { Event, User } from "../../../src/models"; +import { nanoid } from "nanoid"; + +let randomUser: TestUserType; +let randomUser2: TestUserType; +let superAdminTestUser: TestUserType; +let testUser: TestUserType; +let testOrganization: TestOrganizationType; +let testCategory: TestActionItemCategoryType; +let testEvent: TestEventType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message + ); + + randomUser = await createTestUser(); + randomUser2 = await createTestUser(); + + superAdminTestUser = await User.findOneAndUpdate( + { + _id: randomUser2?._id, + }, + { + userType: "SUPERADMIN", + }, + { + new: true, + } + ); + + [testUser, testOrganization, testCategory] = await createTestCategory(); + + testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: true, + isPublic: true, + isRegisterable: true, + creatorId: randomUser?._id, + admins: [randomUser?._id], + organization: testOrganization?._id, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> createActionItem", () => { + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + try { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: Types.ObjectId().toString(), + }; + + await createActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no actionItemCategory exists with _id === args.organizationId`, async () => { + try { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + }, + actionItemCategoryId: Types.ObjectId().toString(), + }; + + const context = { + userId: testUser?._id, + }; + + await createActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual( + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE + ); + } + }); + + it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { + try { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: Types.ObjectId().toString(), + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: testUser?._id, + }; + + await createActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError new assignee is not a member of the organization`, async () => { + try { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: testUser?._id, + }; + + await createActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE); + } + }); + + it(`throws NotFoundError if no event exists with _id === args.data.eventId`, async () => { + await User.findOneAndUpdate( + { + _id: randomUser?._id, + }, + { + $push: { joinedOrganizations: testOrganization?._id }, + } + ); + + try { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + eventId: Types.ObjectId().toString(), + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: randomUser?._id, + }; + + await createActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotAuthorizedError if the user is not authorized for performing the operation`, async () => { + try { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: randomUser?._id, + }; + + await createActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_AUTHORIZED_ERROR.MESSAGE); + } + }); + + it(`creates the actionItem when user is authorized as an eventAdmin`, async () => { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + eventId: testEvent?._id, + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: randomUser?._id, + }; + + const createActionItemPayload = await createActionItemResolver?.( + {}, + args, + context + ); + + expect(createActionItemPayload).toEqual( + expect.objectContaining({ + actionItemCategoryId: testCategory?._id, + }) + ); + }); + + it(`creates the actionItem when user is authorized as an orgAdmin`, async () => { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: testUser?._id, + }; + + const createActionItemPayload = await createActionItemResolver?.( + {}, + args, + context + ); + + expect(createActionItemPayload).toEqual( + expect.objectContaining({ + actionItemCategoryId: testCategory?._id, + }) + ); + }); + + it(`creates the actionItem when user is authorized as superadmin`, async () => { + const args: MutationCreateActionItemArgs = { + data: { + assigneeId: randomUser?._id, + }, + actionItemCategoryId: testCategory?._id, + }; + + const context = { + userId: superAdminTestUser?._id, + }; + + const createActionItemPayload = await createActionItemResolver?.( + {}, + args, + context + ); + + expect(createActionItemPayload).toEqual( + expect.objectContaining({ + actionItemCategoryId: testCategory?._id, + }) + ); + }); +}); diff --git a/tests/resolvers/Mutation/createActionItemCategory.spec.ts b/tests/resolvers/Mutation/createActionItemCategory.spec.ts new file mode 100644 index 0000000000..f796341472 --- /dev/null +++ b/tests/resolvers/Mutation/createActionItemCategory.spec.ts @@ -0,0 +1,176 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import type { MutationCreateActionItemCategoryArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; +import { createActionItemCategory as createActionItemCategoryResolver } from "../../../src/resolvers/Mutation/createActionItemCategory"; +import { + ORGANIZATION_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ADMIN, + ACTION_ITEM_CATEGORY_ALREADY_EXISTS, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { + createTestUser, + createTestUserAndOrganization, +} from "../../helpers/userAndOrg"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; + +import { Organization, User } from "../../../src/models"; + +let randomUser: TestUserType; +let testUser: TestUserType; +let testOrganization: TestOrganizationType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message + ); + + randomUser = await createTestUser(); + + [testUser, testOrganization] = await createTestUserAndOrganization(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> createCategory", () => { + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + try { + const args: MutationCreateActionItemCategoryArgs = { + organizationId: testOrganization?._id, + name: "Default", + }; + + const context = { + userId: Types.ObjectId().toString(), + }; + + await createActionItemCategoryResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no organization exists with _id === args.organizationId`, async () => { + try { + const args: MutationCreateActionItemCategoryArgs = { + organizationId: Types.ObjectId().toString(), + name: "Default", + }; + + const context = { + userId: testUser?.id, + }; + + await createActionItemCategoryResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(ORGANIZATION_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotAuthorizedError if the user is not a superadmin or the admin of the organization`, async () => { + try { + const args: MutationCreateActionItemCategoryArgs = { + organizationId: testOrganization?._id, + name: "Default", + }; + + const context = { + userId: randomUser?.id, + }; + + await createActionItemCategoryResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_AUTHORIZED_ADMIN.MESSAGE); + } + }); + + it(`creates the actionItemCategory and returns it as an admin`, async () => { + const args: MutationCreateActionItemCategoryArgs = { + organizationId: testOrganization?._id, + name: "Default", + }; + + const context = { + userId: testUser?._id, + }; + + const createCategoryPayload = await createActionItemCategoryResolver?.( + {}, + args, + context + ); + + expect(createCategoryPayload).toEqual( + expect.objectContaining({ + organizationId: testOrganization?._id, + name: "Default", + }) + ); + }); + + it(`creates the actionItemCategory and returns it as superAdmin`, async () => { + const superAdminTestUser = await User.findOneAndUpdate( + { + _id: randomUser?._id, + }, + { + userType: "SUPERADMIN", + }, + { + new: true, + } + ); + + const args: MutationCreateActionItemCategoryArgs = { + organizationId: testOrganization?._id, + name: "Default2", + }; + + const context = { + userId: superAdminTestUser?._id, + }; + + const createCategoryPayload = await createActionItemCategoryResolver?.( + {}, + args, + context + ); + + expect(createCategoryPayload).toEqual( + expect.objectContaining({ + organizationId: testOrganization?._id, + name: "Default2", + }) + ); + }); + + it(`throws ConflictError when the actionItemCategory with given name already exists for the current organization`, async () => { + try { + const args: MutationCreateActionItemCategoryArgs = { + organizationId: testOrganization?._id, + name: "Default2", + }; + + const context = { + userId: randomUser?._id, + }; + + await createActionItemCategoryResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual( + ACTION_ITEM_CATEGORY_ALREADY_EXISTS.MESSAGE + ); + } + }); +}); diff --git a/tests/resolvers/Mutation/createOrganization.spec.ts b/tests/resolvers/Mutation/createOrganization.spec.ts index abd2dc9725..03e107065d 100644 --- a/tests/resolvers/Mutation/createOrganization.spec.ts +++ b/tests/resolvers/Mutation/createOrganization.spec.ts @@ -1,6 +1,6 @@ import "dotenv/config"; import type mongoose from "mongoose"; -import { User } from "../../../src/models"; +import { ActionItemCategory, User } from "../../../src/models"; import type { MutationCreateOrganizationArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; @@ -178,6 +178,18 @@ describe("resolvers -> Mutation -> createOrganization", () => { adminFor: [createOrganizationPayload?._id], }) ); + + const defaultCategory = await ActionItemCategory.findOne({ + organizationId: createOrganizationPayload?._id, + }).lean(); + + expect(defaultCategory).toEqual( + expect.objectContaining({ + organizationId: createOrganizationPayload?._id, + name: "Default", + isDisabled: false, + }) + ); }); it(`creates the organization without image and returns it`, async () => { vi.spyOn(uploadImage, "uploadImage").mockImplementation( diff --git a/tests/resolvers/Mutation/removeActionItem.spec.ts b/tests/resolvers/Mutation/removeActionItem.spec.ts new file mode 100644 index 0000000000..780980096a --- /dev/null +++ b/tests/resolvers/Mutation/removeActionItem.spec.ts @@ -0,0 +1,259 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import type { MutationRemoveActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; +import { + USER_NOT_FOUND_ERROR, + ACTION_ITEM_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { + createTestUser, + createTestUserAndOrganization, +} from "../../helpers/userAndOrg"; +import { removeActionItem as removeActionItemResolver } from "../../../src/resolvers/Mutation/removeActionItem"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; + +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; +import { ActionItem, Event, User } from "../../../src/models"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { + createNewTestActionItem, + createTestActionItem, +} from "../../helpers/actionItem"; +import type { TestEventType } from "../../helpers/events"; +import { nanoid } from "nanoid"; + +let randomUser: TestUserType; +let assignedTestUser: TestUserType; +let testUser: TestUserType; +let testUser2: TestUserType; +let testOrganization: TestOrganizationType; +let testCategory: TestActionItemCategoryType; +let testActionItem: TestActionItemType; +let testEvent: TestEventType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message + ); + + randomUser = await createTestUser(); + + [testUser2] = await createTestUserAndOrganization(); + [testUser, testOrganization, testCategory, testActionItem, assignedTestUser] = + await createTestActionItem(); + + testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: true, + isPublic: true, + isRegisterable: true, + creatorId: testUser2?._id, + admins: [testUser2?._id], + organization: testOrganization?._id, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> removeActionItem", () => { + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + try { + const args: MutationRemoveActionItemArgs = { + id: Types.ObjectId().toString(), + }; + + const context = { + userId: Types.ObjectId().toString(), + }; + + await removeActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no action item exists with _id === args.id`, async () => { + try { + const args: MutationRemoveActionItemArgs = { + id: Types.ObjectId().toString(), + }; + + const context = { + userId: testUser?._id, + }; + + await removeActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(ACTION_ITEM_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotAuthorizedError if the user is not a superadmin/orgAdmin/eventAdmin`, async () => { + try { + const args: MutationRemoveActionItemArgs = { + id: testActionItem?._id, + }; + + const context = { + userId: testUser2?._id, + }; + + await removeActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_AUTHORIZED_ERROR.MESSAGE); + } + }); + + it(`removes the action item and returns it as an admin`, async () => { + const args: MutationRemoveActionItemArgs = { + id: testActionItem?._id, + }; + + const context = { + userId: testUser?._id, + }; + + const removedActionItemPayload = await removeActionItemResolver?.( + {}, + args, + context + ); + + // console.log(removedActionItemPayload); + expect(removedActionItemPayload).toEqual( + expect.objectContaining({ + assigneeId: assignedTestUser?._id, + }) + ); + }); + + it(`removes the action item and returns it as superadmin`, async () => { + const newTestActionItem = await createNewTestActionItem({ + currUserId: testUser?._id, + assignedUserId: randomUser?._id, + actionItemCategoryId: testCategory?._id, + }); + + const superAdminTestUser = await User.findOneAndUpdate( + { + _id: randomUser?._id, + }, + { + userType: "SUPERADMIN", + }, + { + new: true, + } + ); + + const args: MutationRemoveActionItemArgs = { + id: newTestActionItem?._id, + }; + + const context = { + userId: superAdminTestUser?._id, + }; + + const removedActionItemPayload = await removeActionItemResolver?.( + {}, + args, + context + ); + + expect(removedActionItemPayload).toEqual( + expect.objectContaining({ + assigneeId: randomUser?._id, + }) + ); + }); + + it(`throws NotFoundError if no event exists to which the action item is associated`, async () => { + const newTestActionItem = await createNewTestActionItem({ + currUserId: testUser?._id, + assignedUserId: randomUser?._id, + actionItemCategoryId: testCategory?._id, + }); + + const updatedTestActionItem = await ActionItem.findOneAndUpdate( + { + _id: newTestActionItem?._id, + }, + { + eventId: Types.ObjectId().toString(), + }, + { + new: true, + } + ); + + try { + const args: MutationRemoveActionItemArgs = { + id: updatedTestActionItem?._id, + }; + + const context = { + userId: testUser?._id, + }; + + await removeActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`removes the actionItem when the user is authorized as an eventAdmin`, async () => { + const newTestActionItem = await createNewTestActionItem({ + currUserId: testUser?._id, + assignedUserId: randomUser?._id, + actionItemCategoryId: testCategory?._id, + }); + + const updatedTestActionItem = await ActionItem.findOneAndUpdate( + { + _id: newTestActionItem?._id, + }, + { + eventId: testEvent?._id, + }, + { + new: true, + } + ); + + const args: MutationRemoveActionItemArgs = { + id: updatedTestActionItem?._id, + }; + + const context = { + userId: testUser2?._id, + }; + + const removedActionItemPayload = await removeActionItemResolver?.( + {}, + args, + context + ); + + expect(removedActionItemPayload).toEqual( + expect.objectContaining({ + assigneeId: randomUser?._id, + }) + ); + }); +}); diff --git a/tests/resolvers/Mutation/removeEvent.spec.ts b/tests/resolvers/Mutation/removeEvent.spec.ts index 1eb0ae1f66..ae83acaa73 100644 --- a/tests/resolvers/Mutation/removeEvent.spec.ts +++ b/tests/resolvers/Mutation/removeEvent.spec.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import type mongoose from "mongoose"; import { Types } from "mongoose"; -import { User, Event } from "../../../src/models"; +import { User, Event, ActionItem } from "../../../src/models"; import type { MutationRemoveEventArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; @@ -19,11 +19,14 @@ import type { import type { TestEventType } from "../../helpers/events"; import { createTestEvent } from "../../helpers/events"; import { cacheEvents } from "../../../src/services/EventCache/cacheEvents"; +import { createTestActionItems } from "../../helpers/actionItem"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; +let newTestUser: TestUserType; let testOrganization: TestOrganizationType; let testEvent: TestEventType; +let newTestEvent: TestEventType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); @@ -199,4 +202,26 @@ describe("resolvers -> Mutation -> removeEvent", () => { expect(updatedTestEvent?.status).toEqual("DELETED"); }); + + it(`removes the events and all action items assiciated with it`, async () => { + [newTestUser, newTestEvent] = await createTestActionItems(); + + const args: MutationRemoveEventArgs = { + id: newTestEvent?.id, + }; + + const context = { + userId: newTestUser?.id, + }; + + const removeEventPayload = await removeEventResolver?.({}, args, context); + + expect(removeEventPayload).toEqual(newTestEvent?.toObject()); + + const deletedActionItems = await ActionItem.find({ + eventId: newTestEvent?._id, + }); + + expect(deletedActionItems).toEqual([]); + }); }); diff --git a/tests/resolvers/Mutation/removeOrganization.spec.ts b/tests/resolvers/Mutation/removeOrganization.spec.ts index 267e6a0689..32e27e4cfe 100644 --- a/tests/resolvers/Mutation/removeOrganization.spec.ts +++ b/tests/resolvers/Mutation/removeOrganization.spec.ts @@ -6,6 +6,8 @@ import type { InterfaceOrganization, InterfaceComment, InterfacePost, + InterfaceActionItemCategory, + InterfaceActionItem, } from "../../../src/models"; import { User, @@ -13,6 +15,8 @@ import { Post, Comment, MembershipRequest, + ActionItemCategory, + ActionItem, } from "../../../src/models"; import type { MutationRemoveOrganizationArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; @@ -42,6 +46,8 @@ let testOrganization: InterfaceOrganization & Document; let testPost: InterfacePost & Document; let testComment: InterfaceComment & Document; +let testCategory: InterfaceActionItemCategory & Document; +let testActionItem: InterfaceActionItem & Document; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); @@ -116,6 +122,19 @@ beforeAll(async () => { organization: testOrganization._id, }); + testCategory = await ActionItemCategory.create({ + creatorId: testUsers[0]?._id, + organizationId: testOrganization?._id, + name: "Default", + }); + + testActionItem = await ActionItem.create({ + creatorId: testUsers[0]?._id, + assigneeId: testUsers[1]?._id, + assignerId: testUsers[0]?._id, + actionItemCategoryId: testCategory?._id, + }); + await Organization.updateOne( { _id: testOrganization._id, @@ -331,11 +350,23 @@ describe("resolvers -> Mutation -> removeOrganization", () => { _id: testComment._id, }).lean(); + const deletedTestCategories = await ActionItemCategory.find({ + organizationId: testOrganization?._id, + }).lean(); + + const deteledTestActionItems = await ActionItem.find({ + _id: testActionItem?._id, + }); + expect(deletedMembershipRequests).toEqual([]); expect(deletedTestPosts).toEqual([]); expect(deletedTestComments).toEqual([]); + + expect(deletedTestCategories).toEqual([]); + + expect(deteledTestActionItems).toEqual([]); }); it(`removes the organization with image and returns the updated user's object with _id === context.userId`, async () => { diff --git a/tests/resolvers/Mutation/updateActionItem.spec.ts b/tests/resolvers/Mutation/updateActionItem.spec.ts new file mode 100644 index 0000000000..db74533b50 --- /dev/null +++ b/tests/resolvers/Mutation/updateActionItem.spec.ts @@ -0,0 +1,309 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import type { MutationUpdateActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; +import { + USER_NOT_FOUND_ERROR, + ACTION_ITEM_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, + USER_NOT_MEMBER_FOR_ORGANIZATION, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { + createTestUser, + createTestUserAndOrganization, +} from "../../helpers/userAndOrg"; +import { updateActionItem as updateActionItemResolver } from "../../../src/resolvers/Mutation/updateActionItem"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; + +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; +import { ActionItem, Event, User } from "../../../src/models"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { createTestActionItem } from "../../helpers/actionItem"; +import type { TestEventType } from "../../helpers/events"; +import { nanoid } from "nanoid"; + +let randomUser: TestUserType; +let assignedTestUser: TestUserType; +let testUser: TestUserType; +let testUser2: TestUserType; +let testOrganization: TestOrganizationType; +let testCategory: TestActionItemCategoryType; +let testActionItem: TestActionItemType; +let testEvent: TestEventType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message + ); + + randomUser = await createTestUser(); + + [testUser2] = await createTestUserAndOrganization(); + [testUser, testOrganization, testCategory, testActionItem, assignedTestUser] = + await createTestActionItem(); + + testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: true, + isPublic: true, + isRegisterable: true, + creatorId: testUser2?._id, + admins: [testUser2?._id], + organization: testOrganization?._id, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> updateActionItem", () => { + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + try { + const args: MutationUpdateActionItemArgs = { + id: Types.ObjectId().toString(), + data: { + assigneeId: randomUser?._id, + }, + }; + + const context = { + userId: Types.ObjectId().toString(), + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no action item exists with _id === args.id`, async () => { + try { + const args: MutationUpdateActionItemArgs = { + id: Types.ObjectId().toString(), + data: { + assigneeId: randomUser?._id, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(ACTION_ITEM_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { + try { + const args: MutationUpdateActionItemArgs = { + id: testActionItem?._id, + data: { + assigneeId: Types.ObjectId().toString(), + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if the new asignee is not a member of the organization`, async () => { + try { + const args: MutationUpdateActionItemArgs = { + id: testActionItem?._id, + data: { + assigneeId: randomUser?._id, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE); + } + }); + + it(`throws NotAuthorizedError if the user is not a superadmin/orgAdmin/eventAdmin`, async () => { + try { + const args: MutationUpdateActionItemArgs = { + id: testActionItem?._id, + data: { + assigneeId: testUser?._id, + }, + }; + + const context = { + userId: testUser2?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_AUTHORIZED_ERROR.MESSAGE); + } + }); + + it(`updates the action item and returns it as an admin`, async () => { + const args: MutationUpdateActionItemArgs = { + id: testActionItem?._id, + data: { + assigneeId: assignedTestUser?._id, + }, + }; + + const context = { + userId: testUser?._id, + }; + + const updatedActionItemPayload = await updateActionItemResolver?.( + {}, + args, + context + ); + + expect(updatedActionItemPayload).toEqual( + expect.objectContaining({ + assigneeId: assignedTestUser?._id, + actionItemCategoryId: testCategory?._id, + }) + ); + }); + + it(`updates the action item and returns it as superadmin`, async () => { + const superAdminTestUser = await User.findOneAndUpdate( + { + _id: randomUser?._id, + }, + { + userType: "SUPERADMIN", + }, + { + new: true, + } + ); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem?._id, + data: { + assigneeId: testUser?._id, + }, + }; + + const context = { + userId: superAdminTestUser?._id, + }; + + const updatedActionItemPayload = await updateActionItemResolver?.( + {}, + args, + context + ); + + expect(updatedActionItemPayload).toEqual( + expect.objectContaining({ + assigneeId: testUser?._id, + actionItemCategoryId: testCategory?._id, + }) + ); + }); + + it(`throws NotFoundError if no event exists to which the action item is associated`, async () => { + const updatedTestActionItem = await ActionItem.findOneAndUpdate( + { + _id: testActionItem?._id, + }, + { + eventId: Types.ObjectId().toString(), + }, + { + new: true, + } + ); + + await User.updateOne( + { + _id: randomUser?._id, + }, + { + $push: { joinedOrganizations: testOrganization?._id }, + } + ); + + try { + const args: MutationUpdateActionItemArgs = { + id: updatedTestActionItem?._id, + data: { + assigneeId: randomUser?._id, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`updates the actionItem when the user is authorized as an eventAdmin`, async () => { + const updatedTestActionItem = await ActionItem.findOneAndUpdate( + { + _id: testActionItem?._id, + }, + { + eventId: testEvent?._id, + }, + { + new: true, + } + ); + + const args: MutationUpdateActionItemArgs = { + data: { + assigneeId: testUser?._id, + }, + id: updatedTestActionItem?._id, + }; + + const context = { + userId: testUser2?._id, + }; + + const updatedActionItemPayload = await updateActionItemResolver?.( + {}, + args, + context + ); + + expect(updatedActionItemPayload).toEqual( + expect.objectContaining({ + actionItemCategoryId: testCategory?._id, + assigneeId: testUser?._id, + }) + ); + }); +}); diff --git a/tests/resolvers/Mutation/updateActionItemCategory.spec.ts b/tests/resolvers/Mutation/updateActionItemCategory.spec.ts new file mode 100644 index 0000000000..e2718e69cc --- /dev/null +++ b/tests/resolvers/Mutation/updateActionItemCategory.spec.ts @@ -0,0 +1,175 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import type { MutationUpdateActionItemCategoryArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; +import { + USER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ADMIN, + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import { createTestUser } from "../../helpers/userAndOrg"; +import type { + TestOrganizationType, + TestUserType, +} from "../../helpers/userAndOrg"; + +import { updateActionItemCategory as updateActionItemCategoryResolver } from "../../../src/resolvers/Mutation/updateActionItemCategory"; +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; +import { createTestCategory } from "../../helpers/actionItemCategory"; +import { User } from "../../../src/models"; + +let randomUser: TestUserType; +let testUser: TestUserType; +let testOrganization: TestOrganizationType; +let testCategory: TestActionItemCategoryType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message + ); + + randomUser = await createTestUser(); + + [testUser, testOrganization, testCategory] = await createTestCategory(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> updateActionItemCategoryResolver", () => { + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + try { + const args: MutationUpdateActionItemCategoryArgs = { + id: Types.ObjectId().toString(), + data: { + name: "updatedDefault", + isDisabled: true, + }, + }; + + const context = { + userId: Types.ObjectId().toString(), + }; + + await updateActionItemCategoryResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no actionItemCategory exists with _id === args.id`, async () => { + try { + const args: MutationUpdateActionItemCategoryArgs = { + id: Types.ObjectId().toString(), + data: { + name: "updatedDefault", + isDisabled: true, + }, + }; + + const context = { + userId: testUser?.id, + }; + + await updateActionItemCategoryResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual( + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE + ); + } + }); + + it(`throws NotAuthorizedError if the user is not a superadmin or the admin of the organization`, async () => { + try { + const args: MutationUpdateActionItemCategoryArgs = { + id: testCategory?._id, + data: { + name: "updatedDefault", + isDisabled: true, + }, + }; + + const context = { + userId: randomUser?.id, + }; + + await updateActionItemCategoryResolver?.({}, args, context); + } catch (error: any) { + expect(error.message).toEqual(USER_NOT_AUTHORIZED_ADMIN.MESSAGE); + } + }); + + it(`updates the actionItemCategory and returns it as an admin`, async () => { + const args: MutationUpdateActionItemCategoryArgs = { + id: testCategory?._id, + data: { + name: "updatedDefault", + isDisabled: true, + }, + }; + + const context = { + userId: testUser?._id, + }; + + const updatedCategory = await updateActionItemCategoryResolver?.( + {}, + args, + context + ); + + expect(updatedCategory).toEqual( + expect.objectContaining({ + organizationId: testOrganization?._id, + name: "updatedDefault", + isDisabled: true, + }) + ); + }); + + it(`updates the actionItemCategory and returns it as superadmin`, async () => { + const superAdminTestUser = await User.findOneAndUpdate( + { + _id: randomUser?._id, + }, + { + userType: "SUPERADMIN", + }, + { + new: true, + } + ); + + const args: MutationUpdateActionItemCategoryArgs = { + id: testCategory?._id, + data: { + name: "updatedDefault", + isDisabled: false, + }, + }; + + const context = { + userId: superAdminTestUser?._id, + }; + + const updatedCategory = await updateActionItemCategoryResolver?.( + {}, + args, + context + ); + + expect(updatedCategory).toEqual( + expect.objectContaining({ + organizationId: testOrganization?._id, + name: "updatedDefault", + isDisabled: false, + }) + ); + }); +}); diff --git a/tests/resolvers/Organization/actionItemCategories.spec.ts b/tests/resolvers/Organization/actionItemCategories.spec.ts new file mode 100644 index 0000000000..ac51d984f8 --- /dev/null +++ b/tests/resolvers/Organization/actionItemCategories.spec.ts @@ -0,0 +1,39 @@ +import "dotenv/config"; +import { actionItemCategories as actionItemCategoriesResolver } from "../../../src/resolvers/Organization/actionItemCategories"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { ActionItemCategory } from "../../../src/models"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import { createTestCategories } from "../../helpers/actionItemCategory"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testOrganization] = await createTestCategories(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Organization -> actionItemCategories", () => { + it(`returns all actionItemCategories for parent organization`, async () => { + const parent = testOrganization?.toObject(); + if (parent) { + const actionCategoriesPayload = await actionItemCategoriesResolver?.( + parent, + {}, + {} + ); + + const categories = await ActionItemCategory.find({ + organizationId: testOrganization?._id, + }).lean(); + + expect(actionCategoriesPayload).toEqual(categories); + } + }); +}); diff --git a/tests/resolvers/Query/actionItem.spec.ts b/tests/resolvers/Query/actionItem.spec.ts new file mode 100644 index 0000000000..88dbbc4d42 --- /dev/null +++ b/tests/resolvers/Query/actionItem.spec.ts @@ -0,0 +1,51 @@ +import "dotenv/config"; +import { actionItem as actionItemResolver } from "../../../src/resolvers/Query/actionItem"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { ACTION_ITEM_NOT_FOUND_ERROR } from "../../../src/constants"; +import type { QueryActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { createTestActionItem } from "../../helpers/actionItem"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testActionItem: TestActionItemType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const resultArray = await createTestActionItem(); + testActionItem = resultArray[3]; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> actionItem", () => { + it(`throws NotFoundError if no actionItem exists with _id === args.id`, async () => { + try { + const args: QueryActionItemArgs = { + id: Types.ObjectId().toString(), + }; + + await actionItemResolver?.({}, args, {}); + } catch (error: any) { + expect(error.message).toEqual(ACTION_ITEM_NOT_FOUND_ERROR.DESC); + } + }); + + it(`returns action item with _id === args.id`, async () => { + const args: QueryActionItemArgs = { + id: testActionItem?._id, + }; + + const actionItemPayload = await actionItemResolver?.({}, args, {}); + + expect(actionItemPayload).toEqual( + expect.objectContaining({ + _id: testActionItem?._id, + }) + ); + }); +}); diff --git a/tests/resolvers/Query/actionItemCategoriesByOrganization.spec.ts b/tests/resolvers/Query/actionItemCategoriesByOrganization.spec.ts new file mode 100644 index 0000000000..1889c27e85 --- /dev/null +++ b/tests/resolvers/Query/actionItemCategoriesByOrganization.spec.ts @@ -0,0 +1,40 @@ +import "dotenv/config"; +import { ActionItemCategory } from "../../../src/models"; +import { connect, disconnect } from "../../helpers/db"; +import type { QueryActionItemCategoriesByOrganizationArgs } from "../../../src/types/generatedGraphQLTypes"; +import { actionItemCategoriesByOrganization as categoriesByOrganizationResolver } from "../../../src/resolvers/Query/actionItemCategoriesByOrganization"; +import { createTestCategories } from "../../helpers/actionItemCategory"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import type mongoose from "mongoose"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testOrganization] = await createTestCategories(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> actionItemCategoriesByOrganization", () => { + it(`returns list of all categories belonging to an organization`, async () => { + const args: QueryActionItemCategoriesByOrganizationArgs = { + organizationId: testOrganization?._id, + }; + + const categoriesByOrganizationPayload = + await categoriesByOrganizationResolver?.({}, args, {}); + + const categoriesByOrganizationInfo = await ActionItemCategory.find({ + organizationId: testOrganization?._id, + }).lean(); + + expect(categoriesByOrganizationPayload).toEqual( + categoriesByOrganizationInfo + ); + }); +}); diff --git a/tests/resolvers/Query/actionItemCategory.spec.ts b/tests/resolvers/Query/actionItemCategory.spec.ts new file mode 100644 index 0000000000..dbccdfd506 --- /dev/null +++ b/tests/resolvers/Query/actionItemCategory.spec.ts @@ -0,0 +1,55 @@ +import "dotenv/config"; +import { actionItemCategory as actionItemCategoryResolver } from "../../../src/resolvers/Query/actionItemCategory"; +import { connect, disconnect } from "../../helpers/db"; +import type mongoose from "mongoose"; +import { Types } from "mongoose"; +import { ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR } from "../../../src/constants"; +import type { QueryActionItemCategoryArgs } from "../../../src/types/generatedGraphQLTypes"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; +import { createTestCategory } from "../../helpers/actionItemCategory"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testCategory: TestActionItemCategoryType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const resultArray = await createTestCategory(); + testCategory = resultArray[2]; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> actionItemCategory", () => { + it(`throws NotFoundError if no actionItemCategory exists with _id === args.id`, async () => { + try { + const args: QueryActionItemCategoryArgs = { + id: Types.ObjectId().toString(), + }; + + await actionItemCategoryResolver?.({}, args, {}); + } catch (error: any) { + expect(error.message).toEqual(ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.DESC); + } + }); + + it(`returns actionItemCategory with _id === args.id`, async () => { + const args: QueryActionItemCategoryArgs = { + id: testCategory?._id, + }; + + const actionItemCategoryPayload = await actionItemCategoryResolver?.( + {}, + args, + {} + ); + + expect(actionItemCategoryPayload).toEqual( + expect.objectContaining({ + _id: testCategory?._id, + }) + ); + }); +}); diff --git a/tests/resolvers/Query/actionItemsByEvent.spec.ts b/tests/resolvers/Query/actionItemsByEvent.spec.ts new file mode 100644 index 0000000000..34f161eee7 --- /dev/null +++ b/tests/resolvers/Query/actionItemsByEvent.spec.ts @@ -0,0 +1,41 @@ +import "dotenv/config"; +import { ActionItem } from "../../../src/models"; +import { connect, disconnect } from "../../helpers/db"; +import type { QueryActionItemsByEventArgs } from "../../../src/types/generatedGraphQLTypes"; +import { actionItemsByEvent as actionItemsByEventsResolver } from "../../../src/resolvers/Query/actionItemsByEvent"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type mongoose from "mongoose"; +import { createTestActionItems } from "../../helpers/actionItem"; +import type { TestEventType } from "../../helpers/events"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testEvent] = await createTestActionItems(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> actionItemsByEvent", () => { + it(`returns list of all action items associated with an event`, async () => { + const args: QueryActionItemsByEventArgs = { + eventId: testEvent?._id, + }; + + const actionItemsByEventPayload = await actionItemsByEventsResolver?.( + {}, + args, + {} + ); + + const actionItemsByEventInfo = await ActionItem.find({ + eventId: testEvent?._id, + }).lean(); + + expect(actionItemsByEventPayload).toEqual(actionItemsByEventInfo); + }); +}); diff --git a/tests/resolvers/Query/actionItemsByOrganization.spec.ts b/tests/resolvers/Query/actionItemsByOrganization.spec.ts new file mode 100644 index 0000000000..3c6c3326dc --- /dev/null +++ b/tests/resolvers/Query/actionItemsByOrganization.spec.ts @@ -0,0 +1,49 @@ +import "dotenv/config"; +import { ActionItem, ActionItemCategory } from "../../../src/models"; +import { connect, disconnect } from "../../helpers/db"; +import type { QueryActionItemsByOrganizationArgs } from "../../../src/types/generatedGraphQLTypes"; +import { actionItemsByOrganization as actionItemsByOrganizationResolver } from "../../../src/resolvers/Query/actionItemsByOrganization"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type mongoose from "mongoose"; +import { createTestActionItems } from "../../helpers/actionItem"; +import type { TestEventType } from "../../helpers/events"; +import { TestOrganizationType } from "../../helpers/userAndOrg"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; +let testOrganization: TestOrganizationType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + [, testEvent, testOrganization] = await createTestActionItems(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> actionItemsByOrganization", () => { + it(`returns list of all action items associated with an organization`, async () => { + const args: QueryActionItemsByOrganizationArgs = { + organizationId: testOrganization?._id, + }; + + const actionItemsByOrganizationPayload = + await actionItemsByOrganizationResolver?.({}, args, {}); + + const actionItemCategories = await ActionItemCategory.find({ + organizationId: args.organizationId, + }); + const actionItemCategoriesIds = actionItemCategories.map( + (category) => category._id + ); + + const actionItemsByOrganizationInfo = await ActionItem.find({ + actionItemCategoryId: { $in: actionItemCategoriesIds }, + }).lean(); + + expect(actionItemsByOrganizationPayload).toEqual( + actionItemsByOrganizationInfo + ); + }); +});