diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 844778ab40..44b5939049 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -37,7 +37,7 @@ jobs: - name: Count number of lines run: | chmod +x ./.github/workflows/countline.py - ./.github/workflows/countline.py --lines 600 --exclude_files src/types/generatedGraphQLTypes.ts tests src/typeDefs/types.ts src/constants.ts + ./.github/workflows/countline.py --lines 600 --exclude_files src/types/generatedGraphQLTypes.ts tests src/typeDefs/types.ts src/constants.ts src/typeDefs/inputs.ts - name: Check for TSDoc comments run: npm run check-tsdoc # Run the TSDoc check script diff --git a/codegen.ts b/codegen.ts index bc996c81da..9a34f09b9b 100644 --- a/codegen.ts +++ b/codegen.ts @@ -98,6 +98,9 @@ const config: CodegenConfig = { User: "../models/User#InterfaceUser", Venue: "../models/Venue#InterfaceVenue", + + VolunteerMembership: + "../models/VolunteerMembership#InterfaceVolunteerMembership", }, useTypeImports: true, diff --git a/schema.graphql b/schema.graphql index 8fcaa7d503..9ce8443540 100644 --- a/schema.graphql +++ b/schema.graphql @@ -5,8 +5,11 @@ directive @role(requires: UserType) on FIELD_DEFINITION type ActionItem { _id: ID! actionItemCategory: ActionItemCategory - allotedHours: Float - assignee: User + allottedHours: Float + assignee: EventVolunteer + assigneeGroup: EventVolunteerGroup + assigneeType: String! + assigneeUser: User assigner: User assignmentDate: Date! completionDate: Date! @@ -41,6 +44,7 @@ input ActionItemWhereInput { categoryName: String event_id: ID is_completed: Boolean + orgId: ID } enum ActionItemsOrderByInput { @@ -310,8 +314,9 @@ interface ConnectionPageInfo { scalar CountryCode input CreateActionItemInput { - allotedHours: Float + allottedHours: Float assigneeId: ID! + assigneeType: String! dueDate: Date eventId: ID preCompletionNotes: String @@ -675,6 +680,8 @@ type Event { startTime: Time title: String! updatedAt: DateTime! + volunteerGroups: [EventVolunteerGroup] + volunteers: [EventVolunteer] } type EventAttendee { @@ -739,21 +746,25 @@ enum EventOrderByInput { type EventVolunteer { _id: ID! + assignments: [ActionItem] createdAt: DateTime! creator: User event: Event - group: EventVolunteerGroup - isAssigned: Boolean - isInvited: Boolean - response: String + groups: [EventVolunteerGroup] + hasAccepted: Boolean! + hoursHistory: [HoursHistory] + hoursVolunteered: Float! + isPublic: Boolean! updatedAt: DateTime! user: User! } type EventVolunteerGroup { _id: ID! + assignments: [ActionItem] createdAt: DateTime! creator: User + description: String event: Event leader: User! name: String @@ -763,20 +774,32 @@ type EventVolunteerGroup { } input EventVolunteerGroupInput { + description: String eventId: ID! - name: String + leaderId: ID! + name: String! + volunteerUserIds: [ID!]! volunteersRequired: Int } +enum EventVolunteerGroupOrderByInput { + assignments_ASC + assignments_DESC + volunteers_ASC + volunteers_DESC +} + input EventVolunteerGroupWhereInput { eventId: ID + leaderName: String name_contains: String - volunteerId: ID + orgId: ID + userId: ID } input EventVolunteerInput { eventId: ID! - groupId: ID! + groupId: ID userId: ID! } @@ -785,6 +808,19 @@ enum EventVolunteerResponse { YES } +input EventVolunteerWhereInput { + eventId: ID + groupId: ID + hasAccepted: Boolean + id: ID + name_contains: String +} + +enum EventVolunteersOrderByInput { + hoursVolunteered_ASC + hoursVolunteered_DESC +} + input EventWhereInput { description: String description_contains: String @@ -942,6 +978,11 @@ type Group { updatedAt: DateTime! } +type HoursHistory { + date: Date! + hours: Float! +} + type InvalidCursor implements FieldError { message: String! path: [String!]! @@ -1093,6 +1134,7 @@ type Mutation { createUserFamily(data: createUserFamilyInput!): UserFamily! createUserTag(input: CreateUserTagInput!): UserTag createVenue(data: VenueInput!): Venue + createVolunteerMembership(data: VolunteerMembershipInput!): VolunteerMembership! deleteAdvertisement(id: ID!): DeleteAdvertisementPayload deleteAgendaCategory(id: ID!): ID! deleteDonationById(id: ID!): DeletePayload! @@ -1157,7 +1199,7 @@ type Mutation { updateCommunity(data: UpdateCommunityInput!): Boolean! updateEvent(data: UpdateEventInput!, id: ID!, recurrenceRuleData: RecurrenceRuleInput, recurringEventUpdateType: RecurringEventMutationType): Event! updateEventVolunteer(data: UpdateEventVolunteerInput, id: ID!): EventVolunteer! - updateEventVolunteerGroup(data: UpdateEventVolunteerGroupInput, id: ID!): EventVolunteerGroup! + updateEventVolunteerGroup(data: UpdateEventVolunteerGroupInput!, id: ID!): EventVolunteerGroup! updateFund(data: UpdateFundInput!, id: ID!): Fund! updateFundraisingCampaign(data: UpdateFundCampaignInput!, id: ID!): FundraisingCampaign! updateFundraisingCampaignPledge(data: UpdateFundCampaignPledgeInput!, id: ID!): FundraisingCampaignPledge! @@ -1171,6 +1213,7 @@ type Mutation { updateUserProfile(data: UpdateUserInput, file: String): User! updateUserRoleInOrganization(organizationId: ID!, role: String!, userId: ID!): Organization! updateUserTag(input: UpdateUserTagInput!): UserTag + updateVolunteerMembership(id: ID!, status: String!): VolunteerMembership! } type Note { @@ -1462,6 +1505,7 @@ type Query { actionItemCategoriesByOrganization(orderBy: ActionItemsOrderByInput, organizationId: ID!, where: ActionItemCategoryWhereInput): [ActionItemCategory] actionItemsByEvent(eventId: ID!): [ActionItem] actionItemsByOrganization(eventId: ID, orderBy: ActionItemsOrderByInput, organizationId: ID!, where: ActionItemWhereInput): [ActionItem] + actionItemsByUser(orderBy: ActionItemsOrderByInput, userId: ID!, where: ActionItemWhereInput): [ActionItem] adminPlugin(orgId: ID!): [Plugin] advertisementsConnection(after: String, before: String, first: PositiveInt, last: PositiveInt): AdvertisementsConnection agendaCategory(id: ID!): AgendaCategory! @@ -1474,9 +1518,8 @@ type Query { customDataByOrganization(organizationId: ID!): [UserCustomData!]! customFieldsByOrganization(id: ID!): [OrganizationCustomField] event(id: ID!): Event - eventVolunteersByEvent(id: ID!): [EventVolunteer] eventsByOrganization(id: ID, orderBy: EventOrderByInput): [Event] - eventsByOrganizationConnection(first: Int, orderBy: EventOrderByInput, skip: Int, where: EventWhereInput): [Event!]! + eventsByOrganizationConnection(first: Int, orderBy: EventOrderByInput, skip: Int, upcomingOnly: Boolean, where: EventWhereInput): [Event!]! fundsByOrganization(orderBy: FundOrderByInput, organizationId: ID!, where: FundWhereInput): [Fund] getAgendaItem(id: ID!): AgendaItem getAgendaSection(id: ID!): AgendaSection @@ -1489,7 +1532,8 @@ type Query { getEventAttendee(eventId: ID!, userId: ID!): EventAttendee getEventAttendeesByEventId(eventId: ID!): [EventAttendee] getEventInvitesByUserId(userId: ID!): [EventAttendee!]! - getEventVolunteerGroups(where: EventVolunteerGroupWhereInput): [EventVolunteerGroup]! + getEventVolunteerGroups(orderBy: EventVolunteerGroupOrderByInput, where: EventVolunteerGroupWhereInput!): [EventVolunteerGroup]! + getEventVolunteers(orderBy: EventVolunteersOrderByInput, where: EventVolunteerWhereInput!): [EventVolunteer]! getFundById(id: ID!, orderBy: CampaignOrderByInput, where: CampaignWhereInput): Fund! getFundraisingCampaignPledgeById(id: ID!): FundraisingCampaignPledge! getFundraisingCampaigns(campaignOrderby: CampaignOrderByInput, pledgeOrderBy: PledgeOrderByInput, where: CampaignWhereInput): [FundraisingCampaign]! @@ -1498,6 +1542,8 @@ type Query { getPlugins: [Plugin] getUserTag(id: ID!): UserTag getVenueByOrgId(first: Int, orderBy: VenueOrderByInput, orgId: ID!, skip: Int, where: VenueWhereInput): [Venue] + getVolunteerMembership(orderBy: VolunteerMembershipOrderByInput, where: VolunteerMembershipWhereInput!): [VolunteerMembership]! + getVolunteerRanks(orgId: ID!, where: VolunteerRankWhereInput!): [VolunteerRank]! getlanguage(lang_code: String!): [Translation] hasSubmittedFeedback(eventId: ID!, userId: ID!): Boolean isSampleOrganization(id: ID!): Boolean! @@ -1643,8 +1689,9 @@ input UpdateActionItemCategoryInput { } input UpdateActionItemInput { - allotedHours: Float + allottedHours: Float assigneeId: ID + assigneeType: String completionDate: Date dueDate: Date isCompleted: Boolean @@ -1714,16 +1761,16 @@ input UpdateEventInput { } input UpdateEventVolunteerGroupInput { - eventId: ID + description: String + eventId: ID! name: String volunteersRequired: Int } input UpdateEventVolunteerInput { - eventId: ID - isAssigned: Boolean - isInvited: Boolean - response: EventVolunteerResponse + assignments: [ID] + hasAccepted: Boolean + isPublic: Boolean } input UpdateFundCampaignInput { @@ -2050,6 +2097,53 @@ input VenueWhereInput { name_starts_with: String } +type VolunteerMembership { + _id: ID! + createdAt: DateTime! + createdBy: User + event: Event! + group: EventVolunteerGroup + status: String! + updatedAt: DateTime! + updatedBy: User + volunteer: EventVolunteer! +} + +input VolunteerMembershipInput { + event: ID! + group: ID + status: String! + userId: ID! +} + +enum VolunteerMembershipOrderByInput { + createdAt_ASC + createdAt_DESC +} + +input VolunteerMembershipWhereInput { + eventId: ID + eventTitle: String + filter: String + groupId: ID + status: String + userId: ID + userName: String +} + +type VolunteerRank { + hoursVolunteered: Float! + rank: Int! + user: User! +} + +input VolunteerRankWhereInput { + limit: Int + nameContains: String + orderBy: String! + timeFrame: String! +} + enum WeekDays { FRIDAY MONDAY diff --git a/src/constants.ts b/src/constants.ts index 71a5d0eab3..992b471a65 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -668,6 +668,13 @@ export const EVENT_VOLUNTEER_INVITE_USER_MISTMATCH = Object.freeze({ PARAM: "eventVolunteers", }); +export const EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR = Object.freeze({ + DESC: "Volunteer membership not found", + CODE: "volunteerMembership.notFound", + MESSAGE: "volunteerMembership.notFound", + PARAM: "volunteerMemberships", +}); + export const USER_ALREADY_CHECKED_IN = Object.freeze({ MESSAGE: "The user has already been checked in for this event.", CODE: "user.alreadyCheckedIn", diff --git a/src/models/ActionItem.ts b/src/models/ActionItem.ts index fe99964a0a..94904a25a7 100644 --- a/src/models/ActionItem.ts +++ b/src/models/ActionItem.ts @@ -5,13 +5,18 @@ import type { InterfaceEvent } from "./Event"; import type { InterfaceActionItemCategory } from "./ActionItemCategory"; import { MILLISECONDS_IN_A_WEEK } from "../constants"; import type { InterfaceOrganization } from "./Organization"; +import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; +import type { InterfaceEventVolunteer } from "./EventVolunteer"; /** * Interface representing a database document for ActionItem in MongoDB. */ export interface InterfaceActionItem { _id: Types.ObjectId; - assignee: PopulatedDoc; + assignee: PopulatedDoc; + assigneeGroup: PopulatedDoc; + assigneeUser: PopulatedDoc; + assigneeType: "EventVolunteer" | "EventVolunteerGroup" | "User"; assigner: PopulatedDoc; actionItemCategory: PopulatedDoc< InterfaceActionItemCategory & Document @@ -22,7 +27,7 @@ export interface InterfaceActionItem { dueDate: Date; completionDate: Date; isCompleted: boolean; - allotedHours: number | null; + allottedHours: number | null; organization: PopulatedDoc; event: PopulatedDoc; creator: PopulatedDoc; @@ -33,6 +38,9 @@ export interface InterfaceActionItem { /** * Defines the schema for the ActionItem document. * @param assignee - User to whom the ActionItem is assigned. + * @param assigneeGroup - Group to whom the ActionItem is assigned. + * @param assigneeUser - Organization User to whom the ActionItem is assigned. + * @param assigneeType - Type of assignee (User or Group). * @param assigner - User who assigned the ActionItem. * @param actionItemCategory - ActionItemCategory to which the ActionItem belongs. * @param preCompletionNotes - Notes recorded before completion. @@ -41,7 +49,7 @@ export interface InterfaceActionItem { * @param dueDate - Due date for the ActionItem. * @param completionDate - Date when the ActionItem was completed. * @param isCompleted - Flag indicating if the ActionItem is completed. - * @param allotedHours - Optional: Number of hours alloted for the ActionItem. + * @param allottedHours - Optional: Number of hours allotted for the ActionItem. * @param event - Optional: Event to which the ActionItem is related. * @param organization - Organization to which the ActionItem belongs. * @param creator - User who created the ActionItem. @@ -51,9 +59,21 @@ export interface InterfaceActionItem { const actionItemSchema = new Schema( { assignee: { + type: Schema.Types.ObjectId, + ref: "EventVolunteer", + }, + assigneeGroup: { + type: Schema.Types.ObjectId, + ref: "EventVolunteerGroup", + }, + assigneeUser: { type: Schema.Types.ObjectId, ref: "User", + }, + assigneeType: { + type: String, required: true, + enum: ["EventVolunteer", "EventVolunteerGroup", "User"], }, assigner: { type: Schema.Types.ObjectId, @@ -91,7 +111,7 @@ const actionItemSchema = new Schema( required: true, default: false, }, - allotedHours: { + allottedHours: { type: Number, }, organization: { diff --git a/src/models/Event.ts b/src/models/Event.ts index 2a30077aad..f853682271 100644 --- a/src/models/Event.ts +++ b/src/models/Event.ts @@ -6,6 +6,7 @@ import { createLoggingMiddleware } from "../libraries/dbLogger"; import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; import type { InterfaceRecurrenceRule } from "./RecurrenceRule"; import type { InterfaceAgendaItem } from "./AgendaItem"; +import type { InterfaceEventVolunteer } from "./EventVolunteer"; /** * Represents a document for an event in the MongoDB database. @@ -37,6 +38,7 @@ export interface InterfaceEvent { startTime: string | undefined; title: string; updatedAt: Date; + volunteers: PopulatedDoc[]; volunteerGroups: PopulatedDoc[]; agendaItems: PopulatedDoc[]; } @@ -66,6 +68,7 @@ export interface InterfaceEvent { * @param admins - Array of admins for the event. * @param organization - Reference to the organization hosting the event. * @param volunteerGroups - Array of volunteer groups associated with the event. + * @param volunteers - Array of volunteers associated with the event. * @param createdAt - Timestamp of when the event was created. * @param updatedAt - Timestamp of when the event was last updated. */ @@ -178,6 +181,14 @@ const eventSchema = new Schema( ref: "Organization", required: true, }, + volunteers: [ + { + type: Schema.Types.ObjectId, + ref: "EventVolunteer", + required: true, + default: [], + }, + ], volunteerGroups: [ { type: Schema.Types.ObjectId, diff --git a/src/models/EventVolunteer.ts b/src/models/EventVolunteer.ts index 0600f77b4c..72ccc24d62 100644 --- a/src/models/EventVolunteer.ts +++ b/src/models/EventVolunteer.ts @@ -4,6 +4,7 @@ import type { InterfaceUser } from "./User"; import type { InterfaceEvent } from "./Event"; import { createLoggingMiddleware } from "../libraries/dbLogger"; import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; +import type { InterfaceActionItem } from "./ActionItem"; /** * Represents a document for an event volunteer in the MongoDB database. @@ -11,60 +12,95 @@ import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; */ export interface InterfaceEventVolunteer { _id: Types.ObjectId; + creator: PopulatedDoc; + event: PopulatedDoc; + groups: PopulatedDoc[]; + user: PopulatedDoc; + hasAccepted: boolean; + isPublic: boolean; + hoursVolunteered: number; + assignments: PopulatedDoc[]; + hoursHistory: { + hours: number; + date: Date; + }[]; createdAt: Date; - creatorId: PopulatedDoc; - eventId: PopulatedDoc; - groupId: PopulatedDoc; - isAssigned: boolean; - isInvited: boolean; - response: string; updatedAt: Date; - userId: PopulatedDoc; } /** * Mongoose schema definition for an event volunteer document. * This schema defines how the data will be stored in the MongoDB database. * - * @param creatorId - Reference to the user who created the event volunteer entry. - * @param eventId - Reference to the event for which the user volunteers. - * @param groupId - Reference to the volunteer group associated with the event. - * @param response - Response status of the volunteer ("YES", "NO", null). - * @param isAssigned - Indicates if the volunteer is assigned to a specific role. - * @param isInvited - Indicates if the volunteer has been invited to participate. - * @param userId - Reference to the user who is volunteering for the event. + * @param creator - Reference to the user who created the event volunteer entry. + * @param event - Reference to the event for which the user volunteers. + * @param groups - Reference to the volunteer groups associated with the event. + * @param user - Reference to the user who is volunteering for the event. + * @param hasAccepted - Indicates if the volunteer has accepted invite. + * @param isPublic - Indicates if the volunteer is public. + * @param hoursVolunteered - Total hours volunteered by the user. + * @param assignments - List of action items assigned to the volunteer. * @param createdAt - Timestamp of when the event volunteer document was created. * @param updatedAt - Timestamp of when the event volunteer document was last updated. */ const eventVolunteerSchema = new Schema( { - creatorId: { + creator: { type: Schema.Types.ObjectId, ref: "User", required: true, }, - eventId: { + event: { type: Schema.Types.ObjectId, ref: "Event", }, - groupId: { + groups: [ + { + type: Schema.Types.ObjectId, + ref: "EventVolunteerGroup", + default: [], + }, + ], + user: { type: Schema.Types.ObjectId, - ref: "EventVolunteerGroup", - }, - response: { - type: String, - enum: ["YES", "NO", null], + ref: "User", + required: true, }, - isAssigned: { + hasAccepted: { type: Boolean, + required: true, + default: false, }, - isInvited: { + isPublic: { type: Boolean, - }, - userId: { - type: Schema.Types.ObjectId, - ref: "User", required: true, + default: true, + }, + hoursVolunteered: { + type: Number, + default: 0, + }, + assignments: [ + { + type: Schema.Types.ObjectId, + ref: "ActionItem", + default: [], + }, + ], + hoursHistory: { + type: [ + { + hours: { + type: Number, + required: true, + }, + date: { + type: Date, + required: true, + }, + }, + ], + default: [], }, }, { @@ -72,6 +108,9 @@ const eventVolunteerSchema = new Schema( }, ); +// Add index on hourHistory.date +eventVolunteerSchema.index({ "hourHistory.date": 1 }); + // Apply logging middleware to the schema createLoggingMiddleware(eventVolunteerSchema, "EventVolunteer"); diff --git a/src/models/EventVolunteerGroup.ts b/src/models/EventVolunteerGroup.ts index 8f8fc5072e..4bd9f5f438 100644 --- a/src/models/EventVolunteerGroup.ts +++ b/src/models/EventVolunteerGroup.ts @@ -4,6 +4,7 @@ import type { InterfaceUser } from "./User"; import type { InterfaceEvent } from "./Event"; import { createLoggingMiddleware } from "../libraries/dbLogger"; import type { InterfaceEventVolunteer } from "./EventVolunteer"; +import type { InterfaceActionItem } from "./ActionItem"; /** * Represents a document for an event volunteer group in the MongoDB database. @@ -11,42 +12,46 @@ import type { InterfaceEventVolunteer } from "./EventVolunteer"; */ export interface InterfaceEventVolunteerGroup { _id: Types.ObjectId; - createdAt: Date; - creatorId: PopulatedDoc; - eventId: PopulatedDoc; - leaderId: PopulatedDoc; + creator: PopulatedDoc; + event: PopulatedDoc; + leader: PopulatedDoc; name: string; - updatedAt: Date; + description?: string; volunteers: PopulatedDoc[]; volunteersRequired?: number; + assignments: PopulatedDoc[]; + createdAt: Date; + updatedAt: Date; } /** * Mongoose schema definition for an event volunteer group document. * This schema defines how the data will be stored in the MongoDB database. * - * @param creatorId - Reference to the user who created the event volunteer group entry. - * @param eventId - Reference to the event for which the volunteer group is created. - * @param leaderId - Reference to the leader of the volunteer group. + * @param creator - Reference to the user who created the event volunteer group entry. + * @param event - Reference to the event for which the volunteer group is created. + * @param leader - Reference to the leader of the volunteer group. * @param name - Name of the volunteer group. + * @param description - Description of the volunteer group (optional). * @param volunteers - List of volunteers in the group. * @param volunteersRequired - Number of volunteers required for the group (optional). + * @param assignments - List of action items assigned to the volunteer group. * @param createdAt - Timestamp of when the event volunteer group document was created. * @param updatedAt - Timestamp of when the event volunteer group document was last updated. */ const eventVolunteerGroupSchema = new Schema( { - creatorId: { + creator: { type: Schema.Types.ObjectId, ref: "User", required: true, }, - eventId: { + event: { type: Schema.Types.ObjectId, ref: "Event", required: true, }, - leaderId: { + leader: { type: Schema.Types.ObjectId, ref: "User", required: true, @@ -55,6 +60,9 @@ const eventVolunteerGroupSchema = new Schema( type: String, required: true, }, + description: { + type: String, + }, volunteers: [ { type: Schema.Types.ObjectId, @@ -65,6 +73,13 @@ const eventVolunteerGroupSchema = new Schema( volunteersRequired: { type: Number, }, + assignments: [ + { + type: Schema.Types.ObjectId, + ref: "ActionItem", + default: [], + }, + ], }, { timestamps: true, // Automatically manage `createdAt` and `updatedAt` fields diff --git a/src/models/VolunteerMembership.ts b/src/models/VolunteerMembership.ts new file mode 100644 index 0000000000..3342129d10 --- /dev/null +++ b/src/models/VolunteerMembership.ts @@ -0,0 +1,94 @@ +import type { PopulatedDoc, Document, Model, Types } from "mongoose"; +import { Schema, model, models } from "mongoose"; +import type { InterfaceEvent } from "./Event"; +import type { InterfaceEventVolunteer } from "./EventVolunteer"; +import type { InterfaceEventVolunteerGroup } from "./EventVolunteerGroup"; +import { createLoggingMiddleware } from "../libraries/dbLogger"; +import type { InterfaceUser } from "./User"; + +/** + * Represents a document for a volunteer membership in the MongoDB database. + * This interface defines the structure and types of data that a volunteer membership document will hold. + */ +export interface InterfaceVolunteerMembership { + _id: Types.ObjectId; + volunteer: PopulatedDoc; + group: PopulatedDoc; + event: PopulatedDoc; + status: "invited" | "requested" | "accepted" | "rejected"; + createdBy: PopulatedDoc; + updatedBy: PopulatedDoc; + createdAt: Date; + updatedAt: Date; +} + +/** + * Mongoose schema definition for a volunteer group membership document. + * This schema defines how the data will be stored in the MongoDB database. + * + * @param volunteer - Reference to the event volunteer involved in the group membership. + * @param group - Reference to the event volunteer group. Absence denotes a request for individual volunteer request. + * @param event - Reference to the event that the group is part of. + * @param status - Current status of the membership (invited, requested, accepted, rejected). + * @param createdBy - Reference to the user who created the group membership document. + * @param updatedBy - Reference to the user who last updated the group membership document. + * @param createdAt - Timestamp of when the group membership document was created. + * @param updatedAt - Timestamp of when the group membership document was last updated. + */ +const volunteerMembershipSchema = new Schema( + { + volunteer: { + type: Schema.Types.ObjectId, + ref: "EventVolunteer", + required: true, + }, + group: { + type: Schema.Types.ObjectId, + ref: "EventVolunteerGroup", + }, + event: { + type: Schema.Types.ObjectId, + ref: "Event", + required: true, + }, + status: { + type: String, + enum: ["invited", "requested", "accepted", "rejected"], + required: true, + default: "invited", + }, + createdBy: { + type: Schema.Types.ObjectId, + ref: "User", + }, + updatedBy: { + type: Schema.Types.ObjectId, + ref: "User", + }, + }, + { + timestamps: true, // Automatically manage `createdAt` and `updatedAt` fields + }, +); + +// Enable logging on changes in VolunteerMembership collection +createLoggingMiddleware(volunteerMembershipSchema, "VolunteerMembership"); + +/** + * Creates a Mongoose model for the volunteer group membership schema. + * This function ensures that we don't create multiple models during testing, which can cause errors. + * + * @returns The VolunteerMembership model. + */ +const volunteerMembershipModel = (): Model => + model( + "VolunteerMembership", + volunteerMembershipSchema, + ); + +/** + * Export the VolunteerMembership model. + * This syntax ensures we don't get an OverwriteModelError while running tests. + */ +export const VolunteerMembership = (models.VolunteerMembership || + volunteerMembershipModel()) as ReturnType; diff --git a/src/models/index.ts b/src/models/index.ts index 1292a2ac41..2da9481412 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -33,6 +33,7 @@ export * from "./RecurrenceRule"; export * from "./SampleData"; export * from "./TagUser"; export * from "./Venue"; +export * from "./VolunteerMembership"; export * from "./User"; export * from "./Note"; export * from "./Chat"; diff --git a/src/resolvers/Event/actionItems.ts b/src/resolvers/Event/actionItems.ts index b40c8a703c..5099572fde 100644 --- a/src/resolvers/Event/actionItems.ts +++ b/src/resolvers/Event/actionItems.ts @@ -15,6 +15,6 @@ import type { EventResolvers } from "../../types/generatedGraphQLTypes"; */ export const actionItems: EventResolvers["actionItems"] = async (parent) => { return await ActionItem.find({ - eventId: parent._id, + event: parent._id, }).lean(); }; diff --git a/src/resolvers/EventVolunteer/creator.ts b/src/resolvers/EventVolunteer/creator.ts deleted file mode 100644 index 59d13f2b57..0000000000 --- a/src/resolvers/EventVolunteer/creator.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { User } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `creator` field of an `EventVolunteer`. - * - * This function retrieves the user who created a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the user who created it. - * @returns A promise that resolves to the user document found in the database. This document represents the user who created the event volunteer. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const creator: EventVolunteerResolvers["creator"] = async (parent) => { - return await User.findOne({ - _id: parent.creatorId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteer/event.ts b/src/resolvers/EventVolunteer/event.ts deleted file mode 100644 index 438754aa91..0000000000 --- a/src/resolvers/EventVolunteer/event.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Event } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `event` field of an `EventVolunteer`. - * - * This function retrieves the event associated with a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the event associated with it. - * @returns A promise that resolves to the event document found in the database. This document represents the event associated with the event volunteer. - * - * @see Event - The Event model used to interact with the events collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const event: EventVolunteerResolvers["event"] = async (parent) => { - return await Event.findOne({ - _id: parent.eventId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteer/group.ts b/src/resolvers/EventVolunteer/group.ts deleted file mode 100644 index 4fa94b9ee6..0000000000 --- a/src/resolvers/EventVolunteer/group.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { EventVolunteerGroup } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `group` field of an `EventVolunteer`. - * - * This function retrieves the group associated with a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the group associated with it. - * @returns A promise that resolves to the group document found in the database. This document represents the group associated with the event volunteer. - * - * @see EventVolunteerGroup - The EventVolunteerGroup model used to interact with the event volunteer groups collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const group: EventVolunteerResolvers["group"] = async (parent) => { - return await EventVolunteerGroup.findOne({ - _id: parent.groupId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteer/index.ts b/src/resolvers/EventVolunteer/index.ts deleted file mode 100644 index 108e57c712..0000000000 --- a/src/resolvers/EventVolunteer/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; -import { event } from "./event"; -import { creator } from "./creator"; -import { user } from "./user"; -import { group } from "./group"; - -export const EventVolunteer: EventVolunteerResolvers = { - creator, - event, - group, - user, -}; diff --git a/src/resolvers/EventVolunteer/user.ts b/src/resolvers/EventVolunteer/user.ts deleted file mode 100644 index 5059bd1dcb..0000000000 --- a/src/resolvers/EventVolunteer/user.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { User } from "../../models"; -import type { EventVolunteerResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `user` field of an `EventVolunteer`. - * - * This function retrieves the user who created a specific event volunteer. - * - * @param parent - The parent object representing the event volunteer. It contains information about the event volunteer, including the ID of the user who created it. - * @returns A promise that resolves to the user document found in the database. This document represents the user who created the event volunteer. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerResolvers - The type definition for the resolvers of the EventVolunteer fields. - * - */ -export const user: EventVolunteerResolvers["user"] = async (parent) => { - const result = await User.findOne({ - _id: parent.userId, - }).lean(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return result!; -}; diff --git a/src/resolvers/EventVolunteerGroup/creator.ts b/src/resolvers/EventVolunteerGroup/creator.ts deleted file mode 100644 index 7924de2115..0000000000 --- a/src/resolvers/EventVolunteerGroup/creator.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { User } from "../../models"; -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `creator` field of an `EventVolunteerGroup`. - * - * This function retrieves the user who created a specific event volunteer group. - * - * @param parent - The parent object representing the event volunteer group. It contains information about the event volunteer group, including the ID of the user who created it. - * @returns A promise that resolves to the user document found in the database. This document represents the user who created the event volunteer group. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerGroupResolvers - The type definition for the resolvers of the EventVolunteerGroup fields. - * - */ -export const creator: EventVolunteerGroupResolvers["creator"] = async ( - parent, -) => { - return await User.findOne({ - _id: parent.creatorId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteerGroup/event.ts b/src/resolvers/EventVolunteerGroup/event.ts deleted file mode 100644 index b758191d0a..0000000000 --- a/src/resolvers/EventVolunteerGroup/event.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Event } from "../../models"; -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `event` field of an `EventVolunteerGroup`. - * - * This function retrieves the event associated with a specific event volunteer group. - * - * @param parent - The parent object representing the event volunteer group. It contains information about the event volunteer group, including the ID of the event associated with it. - * @returns A promise that resolves to the event document found in the database. This document represents the event associated with the event volunteer group. - * - * @see Event - The Event model used to interact with the events collection in the database. - * @see EventVolunteerGroupResolvers - The type definition for the resolvers of the EventVolunteerGroup fields. - * - */ -export const event: EventVolunteerGroupResolvers["event"] = async (parent) => { - return await Event.findOne({ - _id: parent.eventId, - }).lean(); -}; diff --git a/src/resolvers/EventVolunteerGroup/index.ts b/src/resolvers/EventVolunteerGroup/index.ts deleted file mode 100644 index 9c34f29560..0000000000 --- a/src/resolvers/EventVolunteerGroup/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; -import { leader } from "./leader"; -import { creator } from "./creator"; -import { event } from "./event"; - -export const EventVolunteerGroup: EventVolunteerGroupResolvers = { - creator, - leader, - event, -}; diff --git a/src/resolvers/EventVolunteerGroup/leader.ts b/src/resolvers/EventVolunteerGroup/leader.ts deleted file mode 100644 index 93a47b3eab..0000000000 --- a/src/resolvers/EventVolunteerGroup/leader.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { User } from "../../models"; -import type { InterfaceUser } from "../../models"; -import type { EventVolunteerGroupResolvers } from "../../types/generatedGraphQLTypes"; - -/** - * Resolver function for the `leader` field of an `EventVolunteerGroup`. - * - * This function retrieves the user who is the leader of a specific event volunteer group. - * - * @param parent - The parent object representing the event volunteer group. It contains information about the event volunteer group, including the ID of the user who is the leader. - * @returns A promise that resolves to the user document found in the database. This document represents the user who is the leader of the event volunteer group. - * - * @see User - The User model used to interact with the users collection in the database. - * @see EventVolunteerGroupResolvers - The type definition for the resolvers of the EventVolunteerGroup fields. - * - */ -export const leader: EventVolunteerGroupResolvers["leader"] = async ( - parent, -) => { - const groupLeader = await User.findOne({ - _id: parent.leaderId, - }).lean(); - return groupLeader as InterfaceUser; -}; diff --git a/src/resolvers/Mutation/createActionItem.ts b/src/resolvers/Mutation/createActionItem.ts index c38357a282..718bf1caba 100644 --- a/src/resolvers/Mutation/createActionItem.ts +++ b/src/resolvers/Mutation/createActionItem.ts @@ -3,31 +3,31 @@ import { ACTION_ITEM_CATEGORY_IS_DISABLED, ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../constants"; import { errors, requestContext } from "../../libraries"; import type { InterfaceActionItem, - InterfaceAppUserProfile, InterfaceEvent, - InterfaceUser, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, } from "../../models"; import { ActionItem, ActionItemCategory, - AppUserProfile, Event, - User, + EventVolunteer, + EventVolunteerGroup, } from "../../models"; -import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; -import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; import { cacheEvents } from "../../services/EventCache/cacheEvents"; import { findEventsInCache } from "../../services/EventCache/findEventInCache"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { + checkAppUserProfileExists, + checkUserExists, +} from "../../utilities/checks"; /** * Creates a new action item and assigns it to a user. @@ -38,18 +38,18 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; * 2. Ensures that the current user has an associated app user profile. * 3. Checks if the assignee exists. * 4. Validates if the action item category exists and is not disabled. - * 5. Confirms that the assignee is a member of the organization associated with the action item category. - * 6. If the action item is related to an event, checks if the event exists and whether the current user is an admin of that event. - * 7. Verifies if the current user is an admin of the organization or a superadmin. + * 5. If the action item is related to an event, checks if the event exists and whether the current user is an admin of that event. + * 6. Verifies if the current user is an admin of the organization or a superadmin. * * @param _parent - The parent object for the mutation (not used in this function). * @param args - The arguments provided with the request, including: * - `data`: An object containing: * - `assigneeId`: The ID of the user to whom the action item is assigned. + * - `assigneeType`: The type of the assignee (EventVolunteer or EventVolunteerGroup). * - `preCompletionNotes`: Notes to be added before the action item is completed. * - `dueDate`: The due date for the action item. * - `eventId` (optional): The ID of the event associated with the action item. - * - `actionItemCategoryId`: The ID of the action item category. + * - `actionItemCategoryId`: The ID of the action item category. * @param context - The context of the entire application, including user information and other context-specific data. * * @returns A promise that resolves to the created action item object. @@ -60,62 +60,41 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( args, context, ): Promise => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); + const currentUser = await checkUserExists(context.userId); + const currentUserAppProfile = await checkAppUserProfileExists(currentUser); + + const { + assigneeId, + assigneeType, + preCompletionNotes, + allottedHours, + dueDate, + eventId, + } = args.data; + + let assignee: InterfaceEventVolunteer | InterfaceEventVolunteerGroup | null; + if (assigneeType === "EventVolunteer") { + assignee = await EventVolunteer.findById(assigneeId) + .populate("user") + .lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + ); } - } - - // 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 currentUserAppProfile: InterfaceAppUserProfile | null; - const appUserProfileFoundInCache = await findAppUserProfileCache([ - currentUser.appUserProfileId?.toString(), - ]); - currentUserAppProfile = appUserProfileFoundInCache[0]; - if (currentUserAppProfile === null) { - currentUserAppProfile = await AppUserProfile.findOne({ - userId: currentUser._id, - }).lean(); - if (currentUserAppProfile !== null) { - await cacheAppUserProfile([currentUserAppProfile]); + } else if (assigneeType === "EventVolunteerGroup") { + assignee = await EventVolunteerGroup.findById(assigneeId).lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, + ); } } - if (!currentUserAppProfile) { - throw new errors.UnauthorizedError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } - - const assignee = await User.findOne({ - _id: args.data.assigneeId, - }); - - // Checks whether the assignee 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(); @@ -138,36 +117,17 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( ); } - let asigneeIsOrganizationMember = false; - asigneeIsOrganizationMember = assignee.joinedOrganizations.some( - (organizationId) => - organizationId === actionItemCategory.organizationId || - new mongoose.Types.ObjectId(organizationId.toString()).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) { + if (eventId) { let currEvent: InterfaceEvent | null; - const eventFoundInCache = await findEventsInCache([args.data.eventId]); + const eventFoundInCache = await findEventsInCache([eventId]); currEvent = eventFoundInCache[0]; if (eventFoundInCache[0] === null) { currEvent = await Event.findOne({ - _id: args.data.eventId, + _id: eventId, }).lean(); if (currEvent !== null) { @@ -203,6 +163,7 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( ); // Checks whether the currentUser is authorized for the operation. + /* c8 ignore start */ if ( currentUserIsEventAdmin === false && currentUserIsOrgAdmin === false && @@ -214,19 +175,41 @@ export const createActionItem: MutationResolvers["createActionItem"] = async ( USER_NOT_AUTHORIZED_ERROR.PARAM, ); } + /* c8 ignore stop */ // Creates and returns the new action item. const createActionItem = await ActionItem.create({ - assignee: args.data.assigneeId, + assignee: assigneeType === "EventVolunteer" ? assigneeId : undefined, + assigneeGroup: + assigneeType === "EventVolunteerGroup" ? assigneeId : undefined, + assigneeUser: assigneeType === "User" ? assigneeId : undefined, + assigneeType, assigner: context.userId, actionItemCategory: args.actionItemCategoryId, - preCompletionNotes: args.data.preCompletionNotes, - allotedHours: args.data.allotedHours, - dueDate: args.data.dueDate, - event: args.data.eventId, + preCompletionNotes, + allottedHours, + dueDate, + event: eventId, organization: actionItemCategory.organizationId, creator: context.userId, }); + if (assigneeType === "EventVolunteer") { + await EventVolunteer.findByIdAndUpdate(assigneeId, { + $addToSet: { assignments: createActionItem._id }, + }); + } else if (assigneeType === "EventVolunteerGroup") { + const newGrp = (await EventVolunteerGroup.findByIdAndUpdate( + assigneeId, + { $addToSet: { assignments: createActionItem._id } }, + { new: true }, + ).lean()) as InterfaceEventVolunteerGroup; + + await EventVolunteer.updateMany( + { _id: { $in: newGrp.volunteers } }, + { $addToSet: { assignments: createActionItem._id } }, + ); + } + return createActionItem.toObject(); }; diff --git a/src/resolvers/Mutation/createEventVolunteer.ts b/src/resolvers/Mutation/createEventVolunteer.ts index decaa931fc..987c88dfb2 100644 --- a/src/resolvers/Mutation/createEventVolunteer.ts +++ b/src/resolvers/Mutation/createEventVolunteer.ts @@ -1,29 +1,25 @@ import { EVENT_NOT_FOUND_ERROR, - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { Event, EventVolunteerGroup, User } from "../../models"; +import { Event, User, VolunteerMembership } from "../../models"; import { EventVolunteer } from "../../models/EventVolunteer"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { checkUserExists } from "../../utilities/checks"; /** * Creates a new event volunteer entry. * * This function performs the following actions: - * 1. Verifies the existence of the current user. - * 2. Verifies the existence of the volunteer user. - * 3. Verifies the existence of the event. - * 4. Verifies the existence of the volunteer group. - * 5. Ensures that the current user is the leader of the volunteer group. - * 6. Creates a new event volunteer record. - * 7. Adds the newly created volunteer to the group's list of volunteers. + * 1. Validates the existence of the current user. + * 2. Checks if the specified user and event exist. + * 3. Verifies that the current user is an admin of the event. + * 4. Creates a new volunteer entry for the event. + * 5. Creates a volunteer membership record for the new volunteer. + * 6. Returns the created event volunteer record. * * @param _parent - The parent object for the mutation. This parameter is not used in this resolver. * @param args - The arguments for the mutation, including: @@ -38,25 +34,11 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; */ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - const volunteerUser = await User.findOne({ _id: args.data?.userId }).lean(); + const { eventId, userId } = args.data; + const currentUser = await checkUserExists(context.userId); + + // Check if the volunteer user exists + const volunteerUser = await User.findById(userId).lean(); if (!volunteerUser) { throw new errors.NotFoundError( requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), @@ -64,7 +46,8 @@ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, ); } - const event = await Event.findById(args.data.eventId); + // Check if the event exists + const event = await Event.findById(eventId).populate("organization").lean(); if (!event) { throw new errors.NotFoundError( requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), @@ -72,18 +55,18 @@ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = EVENT_NOT_FOUND_ERROR.PARAM, ); } - const group = await EventVolunteerGroup.findOne({ - _id: args.data.groupId, - }).lean(); - if (!group) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, - ); - } - if (group.leaderId.toString() !== currentUser._id.toString()) { + const userIsEventAdmin = event.admins.some( + (admin) => admin.toString() === currentUser?._id.toString(), + ); + + // Checks creator of the event or admin of the organization + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + if (!isAdmin && !userIsEventAdmin) { throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, @@ -91,24 +74,21 @@ export const createEventVolunteer: MutationResolvers["createEventVolunteer"] = ); } + // create the volunteer const createdVolunteer = await EventVolunteer.create({ - userId: args.data.userId, - eventId: args.data.eventId, - groupId: args.data.groupId, - isAssigned: false, - isInvited: true, - creatorId: context.userId, + user: userId, + event: eventId, + creator: context.userId, + groups: [], + }); + + // create volunteer membership record + await VolunteerMembership.create({ + volunteer: createdVolunteer._id, + event: eventId, + status: "invited", + createdBy: context.userId, }); - await EventVolunteerGroup.findOneAndUpdate( - { - _id: args.data.groupId, - }, - { - $push: { - volunteers: createdVolunteer._id, - }, - }, - ); return createdVolunteer.toObject(); }; diff --git a/src/resolvers/Mutation/createEventVolunteerGroup.ts b/src/resolvers/Mutation/createEventVolunteerGroup.ts index 060a2dde49..d3c26adbd0 100644 --- a/src/resolvers/Mutation/createEventVolunteerGroup.ts +++ b/src/resolvers/Mutation/createEventVolunteerGroup.ts @@ -1,14 +1,17 @@ import { EVENT_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { Event, EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import { + Event, + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { checkUserExists } from "../../utilities/checks"; /** * Creates a new event volunteer group and associates it with an event. @@ -19,14 +22,19 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; * 2. Checks if the specified event exists. * 3. Verifies that the current user is an admin of the event. * 4. Creates a new volunteer group for the event. - * 5. Updates the event to include the newly created volunteer group. + * 5. Fetches or creates new volunteers for the group. + * 6. Creates volunteer group membership records for the new volunteers. + * 7. Updates the event to include the new volunteer group. * * @param _parent - The parent object, not used in this resolver. * @param args - The input arguments for the mutation, including: * - `data`: An object containing: - * - `eventId`: The ID of the event to associate the volunteer group with. - * - `name`: The name of the volunteer group. - * - `volunteersRequired`: The number of volunteers required for the group. + * - `eventId`: The ID of the event to associate the volunteer group with. + * - `name`: The name of the volunteer group. + * - `description`: A description of the volunteer group. + * - `leaderId`: The ID of the user who will lead the volunteer group. + * - `volunteerIds`: An array of user IDs for the volunteers in the group. + * - `volunteersRequired`: The number of volunteers required for the group. * @param context - The context object containing user information (context.userId). * * @returns A promise that resolves to the created event volunteer group object. @@ -35,25 +43,20 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; */ export const createEventVolunteerGroup: MutationResolvers["createEventVolunteerGroup"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - const event = await Event.findById(args.data.eventId); + const { + eventId, + name, + description, + leaderId, + volunteerUserIds, + volunteersRequired, + } = args.data; + // Validate the existence of the current user + const currentUser = await checkUserExists(context.userId); + + const event = await Event.findById(args.data.eventId) + .populate("organization") + .lean(); if (!event) { throw new errors.NotFoundError( requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), @@ -66,7 +69,13 @@ export const createEventVolunteerGroup: MutationResolvers["createEventVolunteerG (admin) => admin.toString() === currentUser?._id.toString(), ); - if (!userIsEventAdmin) { + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + // Checks if user is Event Admin or Admin of the organization + if (!isAdmin && !userIsEventAdmin) { throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, @@ -74,24 +83,57 @@ export const createEventVolunteerGroup: MutationResolvers["createEventVolunteerG ); } + // Create the new volunteer group const createdVolunteerGroup = await EventVolunteerGroup.create({ - eventId: args.data.eventId, - creatorId: context.userId, - leaderId: context.userId, - name: args.data.name, - volunteersRequired: args.data?.volunteersRequired, + creator: context.userId, + event: eventId, + leader: leaderId, + name, + description, + volunteers: [], + volunteersRequired, }); - await Event.findOneAndUpdate( - { - _id: args.data.eventId, - }, - { - $push: { - volunteerGroups: createdVolunteerGroup._id, - }, - }, + // Fetch Volunteers or Create New Ones if Necessary + const volunteers = await EventVolunteer.find({ + user: { $in: volunteerUserIds }, + event: eventId, + }).lean(); + + const existingVolunteerIds = volunteers.map((vol) => vol.user.toString()); + const newVolunteerUserIds = volunteerUserIds.filter( + (id) => !existingVolunteerIds.includes(id), ); + // Bulk Create New Volunteers if Needed + const newVolunteers = await EventVolunteer.insertMany( + newVolunteerUserIds.map((userId) => ({ + user: userId, + event: eventId, + creator: context.userId, + groups: [], + })), + ); + + const allVolunteerIds = [ + ...volunteers.map((v) => v._id.toString()), + ...newVolunteers.map((v) => v._id.toString()), + ]; + + // Bulk Create VolunteerMembership Records + await VolunteerMembership.insertMany( + allVolunteerIds.map((volunteerId) => ({ + volunteer: volunteerId, + group: createdVolunteerGroup._id, + event: eventId, + status: "invited", + createdBy: context.userId, + })), + ); + + await Event.findByIdAndUpdate(eventId, { + $push: { volunteerGroups: createdVolunteerGroup._id }, + }); + return createdVolunteerGroup.toObject(); }; diff --git a/src/resolvers/Mutation/createVolunteerMembership.ts b/src/resolvers/Mutation/createVolunteerMembership.ts new file mode 100644 index 0000000000..e46407c45f --- /dev/null +++ b/src/resolvers/Mutation/createVolunteerMembership.ts @@ -0,0 +1,81 @@ +import { EVENT_NOT_FOUND_ERROR, USER_NOT_FOUND_ERROR } from "../../constants"; +import { errors, requestContext } from "../../libraries"; +import { Event, User, VolunteerMembership } from "../../models"; +import { EventVolunteer } from "../../models/EventVolunteer"; +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { checkUserExists } from "../../utilities/checks"; + +/** + * Creates a new event volunteer membership entry. + * + * This function performs the following actions: + * 1. Validates the existence of the current user. + * 2. Checks if the specified user and event exist. + * 3. Creates a new volunteer entry for the event. + * 4. Creates a volunteer membership record for the new volunteer. + * 5. Returns the created vvolunteer membership record. + * + * @param _parent - The parent object for the mutation. This parameter is not used in this resolver. + * @param args - The arguments for the mutation, including: + * - `data.userId`: The ID of the user to be assigned as a volunteer. + * - `data.event`: The ID of the event for which the volunteer is being created. + * - `data.group`: The ID of the volunteer group to which the user is being added. + * - `data.status`: The status of the volunteer membership. + * + * @param context - The context for the mutation, including: + * - `userId`: The ID of the current user performing the operation. + * + * @returns The created event volunteer record. + * + */ +export const createVolunteerMembership: MutationResolvers["createVolunteerMembership"] = + async (_parent, args, context) => { + const { event: eventId, status, group, userId } = args.data; + await checkUserExists(context.userId); + + // Check if the volunteer user exists + const volunteerUser = await User.findById(userId).lean(); + if (!volunteerUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + // Check if the event exists + const event = await Event.findById(eventId).populate("organization").lean(); + if (!event) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM, + ); + } + + // check if event volunteer exists + let eventVolunteer = await EventVolunteer.findOne({ + user: userId, + event: eventId, + }).lean(); + + if (!eventVolunteer) { + // create the volunteer + eventVolunteer = await EventVolunteer.create({ + user: userId, + event: eventId, + creator: context.userId, + groups: [], + }); + } + + // create volunteer membership record + const membership = await VolunteerMembership.create({ + volunteer: eventVolunteer._id, + event: eventId, + status: status, + ...(group && { group }), + createdBy: context.userId, + }); + + return membership.toObject(); + }; diff --git a/src/resolvers/Mutation/index.ts b/src/resolvers/Mutation/index.ts index 4c6359e4b4..1e97d33522 100644 --- a/src/resolvers/Mutation/index.ts +++ b/src/resolvers/Mutation/index.ts @@ -40,6 +40,7 @@ import { createSampleOrganization } from "./createSampleOrganization"; import { createUserFamily } from "./createUserFamily"; import { createUserTag } from "./createUserTag"; import { createVenue } from "./createVenue"; +import { createVolunteerMembership } from "./createVolunteerMembership"; import { deleteAdvertisement } from "./deleteAdvertisement"; import { deleteAgendaCategory } from "./deleteAgendaCategory"; import { deleteDonationById } from "./deleteDonationById"; @@ -114,6 +115,7 @@ import { updateUserPassword } from "./updateUserPassword"; import { updateUserProfile } from "./updateUserProfile"; import { updateUserRoleInOrganization } from "./updateUserRoleInOrganization"; import { updateUserTag } from "./updateUserTag"; +import { updateVolunteerMembership } from "./updateVolunteerMembership"; import { createNote } from "./createNote"; import { deleteNote } from "./deleteNote"; import { updateNote } from "./updateNote"; @@ -161,6 +163,7 @@ export const Mutation: MutationResolvers = { createActionItemCategory, createUserTag, createVenue, + createVolunteerMembership, deleteDonationById, deleteAdvertisement, deleteVenue, @@ -231,6 +234,7 @@ export const Mutation: MutationResolvers = { updateUserProfile, updateUserPassword, updateUserTag, + updateVolunteerMembership, updatePost, updateAdvertisement, updateFundraisingCampaign, diff --git a/src/resolvers/Mutation/removeEventVolunteer.ts b/src/resolvers/Mutation/removeEventVolunteer.ts index d0b42ebe9e..f375b1ae6a 100644 --- a/src/resolvers/Mutation/removeEventVolunteer.ts +++ b/src/resolvers/Mutation/removeEventVolunteer.ts @@ -1,14 +1,13 @@ import { - EVENT_VOLUNTEER_NOT_FOUND_ERROR, - USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, -} from "../../constants"; -import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { EventVolunteer, EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { + checkEventVolunteerExists, + checkUserExists, +} from "../../utilities/checks"; /** * This function enables to remove an Event Volunteer. @@ -16,73 +15,33 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; * @param args - payload provided with the request * @param context - context of entire application * @remarks The following checks are done: - * 1. If the current user exists - * 2. If the Event volunteer to be removed exists. - * 3. If the current user is leader of the corresponding event volunteer group. + * 1. If the user exists. + * 2. If the Event Volunteer exists. + * 3. Remove the Event Volunteer from their groups and delete the volunteer. + * 4. Delete the volunteer and their memberships in a single operation. * @returns Event Volunteer. */ export const removeEventVolunteer: MutationResolvers["removeEventVolunteer"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } + await checkUserExists(context.userId); + const volunteer = await checkEventVolunteerExists(args.id); - const volunteer = await EventVolunteer.findOne({ - _id: args.id, - }); + // Remove volunteer from their groups and delete the volunteer + const groupIds = volunteer.groups; - if (!volunteer) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + if (groupIds.length > 0) { + await EventVolunteerGroup.updateMany( + { _id: { $in: groupIds } }, + { $pull: { volunteers: volunteer._id } }, ); } - const group = await EventVolunteerGroup.findById(volunteer.groupId); - - const userIsLeader = - group?.leaderId.toString() === currentUser._id.toString(); - - if (!userIsLeader) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } - - await EventVolunteer.deleteOne({ - _id: args.id, - }); - - await EventVolunteerGroup.updateOne( - { - _id: volunteer.groupId, - }, - { - $pull: { - volunteers: volunteer._id, - }, - }, - ); + // Delete the volunteer and their memberships in a single operation + await Promise.all([ + EventVolunteer.deleteOne({ _id: volunteer._id }), + VolunteerMembership.deleteMany({ volunteer: volunteer._id }), + ]); return volunteer; }; diff --git a/src/resolvers/Mutation/removeEventVolunteerGroup.ts b/src/resolvers/Mutation/removeEventVolunteerGroup.ts index 74b072c277..346ff8a4e9 100644 --- a/src/resolvers/Mutation/removeEventVolunteerGroup.ts +++ b/src/resolvers/Mutation/removeEventVolunteerGroup.ts @@ -1,14 +1,20 @@ import { - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceUser } from "../../models"; -import { Event, EventVolunteer, EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import { + Event, + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { + checkUserExists, + checkVolunteerGroupExists, +} from "../../utilities/checks"; /** * This function enables to remove an Event Volunteer Group. @@ -24,59 +30,57 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; export const removeEventVolunteerGroup: MutationResolvers["removeEventVolunteerGroup"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } + const currentUser = await checkUserExists(context.userId); + const volunteerGroup = await checkVolunteerGroupExists(args.id); - if (!currentUser) { + const event = await Event.findById(volunteerGroup.event) + .populate("organization") + .lean(); + if (!event) { throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM, ); } - const volunteerGroup = await EventVolunteerGroup.findOne({ - _id: args.id, - }); - - if (!volunteerGroup) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, - ); - } - - const event = await Event.findById(volunteerGroup.eventId); - - const userIsEventAdmin = event?.admins.some( - (admin) => admin._id.toString() === currentUser?._id.toString(), + const userIsEventAdmin = event.admins.some( + (admin) => admin.toString() === currentUser?._id.toString(), ); - if (!userIsEventAdmin) { - throw new errors.NotFoundError( + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + // Checks if user is Event Admin or Admin of the organization + if (!isAdmin && !userIsEventAdmin) { + throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, USER_NOT_AUTHORIZED_ERROR.PARAM, ); } - await EventVolunteerGroup.deleteOne({ - _id: args.id, - }); + await Promise.all([ + // Remove the volunteer group + EventVolunteerGroup.deleteOne({ _id: args.id }), + + // Remove the group from volunteers + EventVolunteer.updateMany( + { groups: { $in: args.id } }, + { $pull: { groups: args.id } }, + ), + + // Delete all associated volunteer group memberships + VolunteerMembership.deleteMany({ group: args.id }), - await EventVolunteer.deleteMany({ - groupId: args.id, - }); + // Remove the group from the event + Event.updateOne( + { _id: volunteerGroup.event }, + { $pull: { volunteerGroups: args.id } }, + ), + ]); return volunteerGroup; }; diff --git a/src/resolvers/Mutation/updateActionItem.ts b/src/resolvers/Mutation/updateActionItem.ts index 265a40104c..555cb82433 100644 --- a/src/resolvers/Mutation/updateActionItem.ts +++ b/src/resolvers/Mutation/updateActionItem.ts @@ -2,45 +2,54 @@ import mongoose from "mongoose"; import { ACTION_ITEM_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../constants"; import { errors, requestContext } from "../../libraries"; import type { - InterfaceAppUserProfile, InterfaceEvent, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, InterfaceUser, } from "../../models"; -import { ActionItem, AppUserProfile, Event, User } from "../../models"; -import { cacheAppUserProfile } from "../../services/AppUserProfileCache/cacheAppUserProfile"; -import { findAppUserProfileCache } from "../../services/AppUserProfileCache/findAppUserProfileCache"; +import { + ActionItem, + Event, + EventVolunteer, + EventVolunteerGroup, + User, +} from "../../models"; import { cacheEvents } from "../../services/EventCache/cacheEvents"; import { findEventsInCache } from "../../services/EventCache/findEventInCache"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { + checkAppUserProfileExists, + checkUserExists, +} from "../../utilities/checks"; /** * 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. - * 6. If the user has appUserProfile. + * 1. Whether the user exists + * 2. Whether the user has an associated app user profile + * 3. Whether the action item exists + * 4. Whether the user is authorized to update the action item + * 5. Whether the user is an admin of the organization or a superadmin + * * @returns Updated action item. */ type UpdateActionItemInputType = { assigneeId: string; + assigneeType: string; preCompletionNotes: string; postCompletionNotes: string; dueDate: Date; - allotedHours: number; + allottedHours: number; completionDate: Date; isCompleted: boolean; }; @@ -50,46 +59,9 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( args, context, ) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - // Checks 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, - ); - } - let currentUserAppProfile: InterfaceAppUserProfile | null; - const appUserProfileFoundInCache = await findAppUserProfileCache([ - currentUser.appUserProfileId?.toString(), - ]); - currentUserAppProfile = appUserProfileFoundInCache[0]; - if (currentUserAppProfile === null) { - currentUserAppProfile = await AppUserProfile.findOne({ - userId: currentUser._id, - }).lean(); - if (currentUserAppProfile !== null) { - await cacheAppUserProfile([currentUserAppProfile]); - } - } - if (!currentUserAppProfile) { - throw new errors.UnauthorizedError( - requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), - USER_NOT_AUTHORIZED_ERROR.CODE, - USER_NOT_AUTHORIZED_ERROR.PARAM, - ); - } + const currentUser = await checkUserExists(context.userId); + const currentUserAppProfile = await checkAppUserProfileExists(currentUser); + const { assigneeId, assigneeType, isCompleted } = args.data; const actionItem = await ActionItem.findOne({ _id: args.id, @@ -106,44 +78,54 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( ); } - let sameAssignedUser = false; + let sameAssignee = false; - if (args.data.assigneeId) { - sameAssignedUser = new mongoose.Types.ObjectId( - actionItem.assignee.toString(), - ).equals(args.data.assigneeId); + if (assigneeId) { + sameAssignee = new mongoose.Types.ObjectId( + assigneeType === "EventVolunteer" + ? actionItem.assignee.toString() + : assigneeType === "EventVolunteerGroup" + ? actionItem.assigneeGroup.toString() + : actionItem.assigneeUser.toString(), + ).equals(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.actionItemCategory.organizationId; - userIsOrganizationMember = newAssignedUser.joinedOrganizations.some( - (organizationId) => - organizationId === currorganizationId || - new mongoose.Types.ObjectId(organizationId.toString()).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, - ); + if (!sameAssignee) { + let assignee: + | InterfaceEventVolunteer + | InterfaceEventVolunteerGroup + | InterfaceUser + | null; + if (assigneeType === "EventVolunteer") { + assignee = await EventVolunteer.findById(assigneeId) + .populate("user") + .lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + ); + } + } else if (assigneeType === "EventVolunteerGroup") { + assignee = await EventVolunteerGroup.findById(assigneeId).lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate( + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, + ), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, + ); + } + } else if (assigneeType === "User") { + assignee = await User.findById(assigneeId).lean(); + if (!assignee) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } } } } @@ -192,8 +174,9 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( ); } - // Checks if the user is authorized for the operation. + // Checks if the user is authorized for the operation. (Exception: when user updates the action item to complete or incomplete) if ( + isCompleted === undefined && currentUserIsEventAdmin === false && currentUserIsOrgAdmin === false && currentUserAppProfile.isSuperAdmin === false @@ -205,13 +188,102 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( ); } - const updatedAssignmentDate = sameAssignedUser + // checks if the assignee is an event volunteer then add allotted hours to the volunteer else if event volunteer group then add divided equal allotted hours to all volunteers in the group + + if (assigneeType === "EventVolunteer") { + const assignee = await EventVolunteer.findById(assigneeId).lean(); + if (assignee) { + if (isCompleted == true) { + await EventVolunteer.findByIdAndUpdate(assigneeId, { + $inc: { + hoursVolunteered: actionItem.allottedHours + ? actionItem.allottedHours + : 0, + }, + ...(actionItem.allottedHours + ? { + $push: { + hoursHistory: { + hours: actionItem.allottedHours, + date: new Date(), + }, + }, + } + : {}), + }); + } else if (isCompleted == false) { + await EventVolunteer.findByIdAndUpdate(assigneeId, { + $inc: { + hoursVolunteered: actionItem.allottedHours + ? -actionItem.allottedHours + : -0, + }, + ...(actionItem.allottedHours + ? { + $push: { + hoursHistory: { + hours: -actionItem.allottedHours, + date: new Date(), + }, + }, + } + : {}), + }); + } + } + } else if (assigneeType === "EventVolunteerGroup") { + const volunteerGroup = + await EventVolunteerGroup.findById(assigneeId).lean(); + if (volunteerGroup) { + const dividedHours = + (actionItem.allottedHours ?? 0) / volunteerGroup.volunteers.length; + if (isCompleted == true) { + await EventVolunteer.updateMany( + { _id: { $in: volunteerGroup.volunteers } }, + { + $inc: { + hoursVolunteered: dividedHours, + }, + ...(dividedHours + ? { + $push: { + hoursHistory: { + hours: dividedHours, + date: new Date(), + }, + }, + } + : {}), + }, + ); + } else if (isCompleted == false) { + await EventVolunteer.updateMany( + { _id: { $in: volunteerGroup.volunteers } }, + { + $inc: { + hoursVolunteered: -dividedHours, + }, + ...(dividedHours + ? { + $push: { + hoursHistory: { + hours: dividedHours, + date: new Date(), + }, + }, + } + : {}), + }, + ); + } + } + } + + const updatedAssignmentDate = sameAssignee ? actionItem.assignmentDate : new Date(); - const updatedAssigner = sameAssignedUser - ? actionItem.assigner - : context.userId; + const updatedAssigner = sameAssignee ? actionItem.assigner : context.userId; const updatedActionItem = await ActionItem.findOneAndUpdate( { @@ -219,7 +291,25 @@ export const updateActionItem: MutationResolvers["updateActionItem"] = async ( }, { ...(args.data as UpdateActionItemInputType), - assignee: args.data.assigneeId || actionItem.assignee, + assigneeType: assigneeType || actionItem.assigneeType, + assignee: + !sameAssignee && assigneeType === "EventVolunteer" + ? assigneeId || actionItem.assignee + : isCompleted === undefined + ? null + : actionItem.assignee, + assigneeGroup: + !sameAssignee && assigneeType === "EventVolunteerGroup" + ? assigneeId || actionItem.assigneeGroup + : isCompleted === undefined + ? null + : actionItem.assigneeGroup, + assigneeUser: + !sameAssignee && assigneeType === "User" + ? assigneeId || actionItem.assigneeUser + : isCompleted === undefined + ? null + : actionItem.assigneeUser, assignmentDate: updatedAssignmentDate, assigner: updatedAssigner, }, diff --git a/src/resolvers/Mutation/updateEventVolunteer.ts b/src/resolvers/Mutation/updateEventVolunteer.ts index 68d5cdbdbb..6aa7946b1f 100644 --- a/src/resolvers/Mutation/updateEventVolunteer.ts +++ b/src/resolvers/Mutation/updateEventVolunteer.ts @@ -1,15 +1,12 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; -import type { EventVolunteerResponse } from "../../constants"; -import { - EVENT_VOLUNTEER_INVITE_USER_MISTMATCH, - EVENT_VOLUNTEER_NOT_FOUND_ERROR, - USER_NOT_FOUND_ERROR, -} from "../../constants"; -import type { InterfaceEventVolunteer, InterfaceUser } from "../../models"; -import { User, EventVolunteer } from "../../models"; +import { EVENT_VOLUNTEER_INVITE_USER_MISTMATCH } from "../../constants"; +import type { InterfaceEventVolunteer } from "../../models"; +import { EventVolunteer } from "../../models"; import { errors, requestContext } from "../../libraries"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; +import { + checkEventVolunteerExists, + checkUserExists, +} from "../../utilities/checks"; /** * This function enables to update an Event Volunteer * @param _parent - parent of current request @@ -19,43 +16,14 @@ import { cacheUsers } from "../../services/UserCache/cacheUser"; * 1. Whether the user exists * 2. Whether the EventVolunteer exists * 3. Whether the current user is the user of EventVolunteer - * 4. Whether the EventVolunteer is invited + * 4. Update the EventVolunteer */ export const updateEventVolunteer: MutationResolvers["updateEventVolunteer"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - if (!currentUser) { - throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, - ); - } - - const eventVolunteer = await EventVolunteer.findOne({ - _id: args.id, - }).lean(); - - if (!eventVolunteer) { - throw new errors.NotFoundError( - requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), - EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, - EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, - ); - } + await checkUserExists(context.userId); + const volunteer = await checkEventVolunteerExists(args.id); - if (eventVolunteer.userId.toString() !== context.userId.toString()) { + if (volunteer.user.toString() !== context.userId.toString()) { throw new errors.ConflictError( requestContext.translate(EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.MESSAGE), EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.CODE, @@ -69,22 +37,18 @@ export const updateEventVolunteer: MutationResolvers["updateEventVolunteer"] = }, { $set: { - eventId: - args.data?.eventId === undefined - ? eventVolunteer.eventId - : (args?.data.eventId as string), - isAssigned: - args.data?.isAssigned === undefined - ? eventVolunteer.isAssigned - : (args.data?.isAssigned as boolean), - isInvited: - args.data?.isInvited === undefined - ? eventVolunteer.isInvited - : (args.data?.isInvited as boolean), - response: - args.data?.response === undefined - ? eventVolunteer.response - : (args.data?.response as EventVolunteerResponse), + assignments: + args.data?.assignments === undefined + ? volunteer.assignments + : (args.data?.assignments as string[]), + hasAccepted: + args.data?.hasAccepted === undefined + ? volunteer.hasAccepted + : (args.data?.hasAccepted as boolean), + isPublic: + args.data?.isPublic === undefined + ? volunteer.isPublic + : (args.data?.isPublic as boolean), }, }, { diff --git a/src/resolvers/Mutation/updateEventVolunteerGroup.ts b/src/resolvers/Mutation/updateEventVolunteerGroup.ts index e7c67290d4..1abb7112f9 100644 --- a/src/resolvers/Mutation/updateEventVolunteerGroup.ts +++ b/src/resolvers/Mutation/updateEventVolunteerGroup.ts @@ -1,14 +1,14 @@ import { + EVENT_NOT_FOUND_ERROR, EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, } from "../../constants"; import { errors, requestContext } from "../../libraries"; -import type { InterfaceEventVolunteerGroup, InterfaceUser } from "../../models"; -import { EventVolunteerGroup, User } from "../../models"; -import { cacheUsers } from "../../services/UserCache/cacheUser"; -import { findUserInCache } from "../../services/UserCache/findUserInCache"; +import type { InterfaceEventVolunteerGroup } from "../../models"; +import { Event, EventVolunteerGroup } from "../../models"; import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import { adminCheck } from "../../utilities"; +import { checkUserExists } from "../../utilities/checks"; /** * This function enables to update the Event Volunteer Group * @param _parent - parent of current request @@ -21,26 +21,28 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; */ export const updateEventVolunteerGroup: MutationResolvers["updateEventVolunteerGroup"] = async (_parent, args, context) => { - let currentUser: InterfaceUser | null; - const userFoundInCache = await findUserInCache([context.userId]); - currentUser = userFoundInCache[0]; - if (currentUser === null) { - currentUser = await User.findOne({ - _id: context.userId, - }).lean(); - if (currentUser !== null) { - await cacheUsers([currentUser]); - } - } - - if (!currentUser) { + const { eventId, description, name, volunteersRequired } = args.data; + const currentUser = await checkUserExists(context.userId); + const event = await Event.findById(eventId).populate("organization").lean(); + if (!event) { throw new errors.NotFoundError( - requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), - USER_NOT_FOUND_ERROR.CODE, - USER_NOT_FOUND_ERROR.PARAM, + requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE), + EVENT_NOT_FOUND_ERROR.CODE, + EVENT_NOT_FOUND_ERROR.PARAM, ); } + const userIsEventAdmin = event.admins.some( + (admin: { toString: () => string }) => + admin.toString() === currentUser?._id.toString(), + ); + + const isAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + const group = await EventVolunteerGroup.findOne({ _id: args.id, }).lean(); @@ -53,7 +55,12 @@ export const updateEventVolunteerGroup: MutationResolvers["updateEventVolunteerG ); } - if (group.leaderId.toString() !== context.userId.toString()) { + // Checks if user is Event Admin or Admin of the organization or Leader of the group + if ( + !isAdmin && + !userIsEventAdmin && + group.leader.toString() !== currentUser._id.toString() + ) { throw new errors.UnauthorizedError( requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), USER_NOT_AUTHORIZED_ERROR.CODE, @@ -67,20 +74,13 @@ export const updateEventVolunteerGroup: MutationResolvers["updateEventVolunteerG }, { $set: { - eventId: - args.data?.eventId === undefined - ? group.eventId - : args?.data.eventId, - name: args.data?.name === undefined ? group.name : args?.data.name, - volunteersRequired: - args.data?.volunteersRequired === undefined - ? group.volunteersRequired - : args?.data.volunteersRequired, + description, + name, + volunteersRequired, }, }, { new: true, - runValidators: true, }, ).lean(); diff --git a/src/resolvers/Mutation/updateVolunteerMembership.ts b/src/resolvers/Mutation/updateVolunteerMembership.ts new file mode 100644 index 0000000000..18d28e483d --- /dev/null +++ b/src/resolvers/Mutation/updateVolunteerMembership.ts @@ -0,0 +1,137 @@ +import type { MutationResolvers } from "../../types/generatedGraphQLTypes"; +import type { + InterfaceEvent, + InterfaceEventVolunteerGroup, + InterfaceVolunteerMembership, +} from "../../models"; +import { + Event, + EventVolunteer, + EventVolunteerGroup, + VolunteerMembership, +} from "../../models"; +import { + checkUserExists, + checkVolunteerMembershipExists, +} from "../../utilities/checks"; +import { adminCheck } from "../../utilities"; +import { errors, requestContext } from "../../libraries"; +import { USER_NOT_AUTHORIZED_ERROR } from "../../constants"; + +/** + * Helper function to handle updates when status is accepted + */ +const handleAcceptedStatusUpdates = async ( + membership: InterfaceVolunteerMembership, +): Promise => { + const updatePromises = []; + + // Always update EventVolunteer to set hasAccepted to true + updatePromises.push( + EventVolunteer.findOneAndUpdate( + { _id: membership.volunteer, event: membership.event }, + { + $set: { hasAccepted: true }, + ...(membership.group && { $push: { groups: membership.group } }), + }, + ), + ); + + // Always update Event to add volunteer + updatePromises.push( + Event.findOneAndUpdate( + { _id: membership.event }, + { $addToSet: { volunteers: membership.volunteer } }, + ), + ); + + // If group exists, update the EventVolunteerGroup as well + if (membership.group) { + updatePromises.push( + EventVolunteerGroup.findOneAndUpdate( + { _id: membership.group }, + { $addToSet: { volunteers: membership.volunteer } }, + ), + ); + } + + // Execute all updates in parallel + await Promise.all(updatePromises); +}; + +/** + * This function enables to update an Volunteer Membership + * @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. Whether the user exists + * 2. Update the Volunteer Membership + * 3. update related fields of Volunteer Group & Volunteer + */ +export const updateVolunteerMembership: MutationResolvers["updateVolunteerMembership"] = + async (_parent, args, context) => { + const currentUser = await checkUserExists(context.userId); + const volunteerMembership = await checkVolunteerMembershipExists(args.id); + + const event = (await Event.findById(volunteerMembership.event) + .populate("organization") + .lean()) as InterfaceEvent; + + if (volunteerMembership.status != "invited") { + // Check if the user is authorized to update the volunteer membership + const isAdminOrSuperAdmin = await adminCheck( + currentUser._id, + event.organization, + false, + ); + const isEventAdmin = event.admins.some( + (admin) => admin.toString() == currentUser._id.toString(), + ); + let isGroupLeader = false; + if (volunteerMembership.group != undefined) { + // check if current user is group leader + const group = (await EventVolunteerGroup.findById( + volunteerMembership.group, + ).lean()) as InterfaceEventVolunteerGroup; + isGroupLeader = group.leader.toString() == currentUser._id.toString(); + } + + // If the user is not an admin or super admin, event admin, or group leader, throw an error + if (!isAdminOrSuperAdmin && !isEventAdmin && !isGroupLeader) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + } + + const updatedVolunteerMembership = + (await VolunteerMembership.findOneAndUpdate( + { + _id: args.id, + }, + { + $set: { + status: args.status as + | "invited" + | "requested" + | "accepted" + | "rejected", + updatedBy: context.userId, + }, + }, + { + new: true, + runValidators: true, + }, + ).lean()) as InterfaceVolunteerMembership; + + // Handle additional updates if the status is accepted + if (args.status === "accepted") { + await handleAcceptedStatusUpdates(updatedVolunteerMembership); + } + + return updatedVolunteerMembership; + }; diff --git a/src/resolvers/Query/actionItemsByOrganization.ts b/src/resolvers/Query/actionItemsByOrganization.ts index 828cf58005..ac7b10f7cd 100644 --- a/src/resolvers/Query/actionItemsByOrganization.ts +++ b/src/resolvers/Query/actionItemsByOrganization.ts @@ -2,6 +2,7 @@ import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; import type { InterfaceActionItem, InterfaceActionItemCategory, + InterfaceEventVolunteer, InterfaceUser, } from "../../models"; import { ActionItem } from "../../models"; @@ -24,7 +25,14 @@ export const actionItemsByOrganization: QueryResolvers["actionItemsByOrganizatio ...where, }) .populate("creator") - .populate("assignee") + .populate({ + path: "assignee", + populate: { + path: "user", + }, + }) + .populate("assigneeUser") + .populate("assigneeGroup") .populate("assigner") .populate("actionItemCategory") .populate("organization") @@ -46,10 +54,24 @@ export const actionItemsByOrganization: QueryResolvers["actionItemsByOrganizatio // Filter the action items based on assignee name if (args.where?.assigneeName) { + const assigneeName = args.where.assigneeName.toLowerCase(); filteredActionItems = filteredActionItems.filter((item) => { - const tempItem = item as InterfaceActionItem; - const assignee = tempItem.assignee as InterfaceUser; - return assignee.firstName.includes(args?.where?.assigneeName as string); + const assigneeType = item.assigneeType; + + if (assigneeType === "EventVolunteer") { + const assignee = item.assignee as InterfaceEventVolunteer; + const assigneeUser = assignee.user as InterfaceUser; + const name = + `${assigneeUser.firstName} ${assigneeUser.lastName}`.toLowerCase(); + + return name.includes(assigneeName); + } else if (assigneeType === "EventVolunteerGroup") { + return item.assigneeGroup.name.toLowerCase().includes(assigneeName); + } else if (assigneeType === "User") { + const name = + `${item.assigneeUser.firstName} ${item.assigneeUser.lastName}`.toLowerCase(); + return name.includes(assigneeName); + } }); } diff --git a/src/resolvers/Query/actionItemsByUser.ts b/src/resolvers/Query/actionItemsByUser.ts new file mode 100644 index 0000000000..43f1af3b76 --- /dev/null +++ b/src/resolvers/Query/actionItemsByUser.ts @@ -0,0 +1,109 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import type { + InterfaceActionItem, + InterfaceActionItemCategory, + InterfaceEvent, + InterfaceEventVolunteer, + InterfaceUser, +} from "../../models"; +import { ActionItem, EventVolunteer } 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 actionItemsByUser: QueryResolvers["actionItemsByUser"] = async ( + _parent, + args, +) => { + const volunteerObjects = await EventVolunteer.find({ + user: args.userId, + }) + .populate({ + path: "assignments", + populate: [ + { path: "creator" }, + { + path: "assignee", + populate: { path: "user" }, + }, + { path: "assigneeGroup" }, + { path: "assigner" }, + { path: "actionItemCategory" }, + { path: "organization" }, + { path: "event" }, + ], + }) + .populate("event") + .lean(); + + const userActionItems = await ActionItem.find({ + assigneeType: "User", + assigneeUser: args.userId, + organization: args.where?.orgId, + }) + .populate("creator") + .populate("assigner") + .populate("actionItemCategory") + .populate("organization") + .populate("assigneeUser") + .lean(); + + const actionItems: InterfaceActionItem[] = []; + volunteerObjects.forEach((volunteer) => { + const tempEvent = volunteer.event as InterfaceEvent; + if (tempEvent.organization._id.toString() === args.where?.orgId) + actionItems.push(...volunteer.assignments); + }); + + actionItems.push(...userActionItems); + + let filteredActionItems: InterfaceActionItem[] = actionItems; + + // filtering based on category name + if (args.where?.categoryName) { + const categoryName = args.where.categoryName.toLowerCase(); + filteredActionItems = filteredActionItems.filter((item) => { + const category = item.actionItemCategory as InterfaceActionItemCategory; + return category.name.toLowerCase().includes(categoryName); + }); + } + + // filtering based on assignee name + if (args.where?.assigneeName) { + const assigneeName = args.where.assigneeName.toLowerCase(); + + filteredActionItems = filteredActionItems.filter((item) => { + const assigneeType = item.assigneeType; + + if (assigneeType === "EventVolunteer") { + const assignee = item.assignee as InterfaceEventVolunteer; + const assigneeUser = assignee.user as InterfaceUser; + const name = + `${assigneeUser.firstName} ${assigneeUser.lastName}`.toLowerCase(); + + return name.includes(assigneeName); + } else if (assigneeType === "EventVolunteerGroup") { + return item.assigneeGroup.name.toLowerCase().includes(assigneeName); + } else if (assigneeType === "User") { + const name = + `${item.assigneeUser.firstName} ${item.assigneeUser.lastName}`.toLowerCase(); + return name.includes(assigneeName); + } + }); + } + + if (args.orderBy === "dueDate_DESC") { + filteredActionItems.sort((a, b) => { + return new Date(b.dueDate).getTime() - new Date(a.dueDate).getTime(); + }); + } else if (args.orderBy === "dueDate_ASC") { + filteredActionItems.sort((a, b) => { + return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime(); + }); + } + + return filteredActionItems as InterfaceActionItem[]; +}; diff --git a/src/resolvers/Query/eventVolunteersByEvent.ts b/src/resolvers/Query/eventVolunteersByEvent.ts deleted file mode 100644 index e982f58e18..0000000000 --- a/src/resolvers/Query/eventVolunteersByEvent.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; -import { EventVolunteer } from "../../models"; -/** - * This query will fetch all events volunteers for the given eventId from database. - * @param _parent- - * @param args - An object that contains `id` of the Event. - * @returns An object that holds all Event Volunteers for the given Event - */ -export const eventVolunteersByEvent: QueryResolvers["eventVolunteersByEvent"] = - async (_parent, args) => { - const eventId = args.id; - - const volunteers = EventVolunteer.find({ - eventId: eventId, - }) - .populate("userId", "-password") - .lean(); - - return volunteers; - }; diff --git a/src/resolvers/Query/eventsByOrganizationConnection.ts b/src/resolvers/Query/eventsByOrganizationConnection.ts index e184a2bc47..0991d0a8af 100644 --- a/src/resolvers/Query/eventsByOrganizationConnection.ts +++ b/src/resolvers/Query/eventsByOrganizationConnection.ts @@ -4,6 +4,7 @@ import { Event } from "../../models"; import { getSort } from "./helperFunctions/getSort"; import { getWhere } from "./helperFunctions/getWhere"; import { createRecurringEventInstancesDuringQuery } from "../../helpers/event/createEventHelpers"; + /** * Retrieves events for a specific organization based on the provided query parameters. * @@ -26,10 +27,19 @@ export const eventsByOrganizationConnection: QueryResolvers["eventsByOrganizatio // get the where and sort let where = getWhere(args.where); const sort = getSort(args.orderBy); - + const currentDate = new Date(); where = { ...where, isBaseRecurringEvent: false, + ...(args.upcomingOnly && { + $or: [ + { endDate: { $gt: currentDate } }, // Future dates + { + endDate: { $eq: currentDate.toISOString().split("T")[0] }, // Events today + endTime: { $gt: currentDate }, // But start time is after current time + }, + ], + }), }; // find all the events according to the requirements @@ -39,6 +49,13 @@ export const eventsByOrganizationConnection: QueryResolvers["eventsByOrganizatio .skip(args.skip ?? 0) .populate("creatorId", "-password") .populate("admins", "-password") + .populate("volunteerGroups") + .populate({ + path: "volunteers", + populate: { + path: "user", + }, + }) .lean(); return events; diff --git a/src/resolvers/Query/getEventVolunteerGroups.ts b/src/resolvers/Query/getEventVolunteerGroups.ts index bb5e7d558e..f4f6913d3e 100644 --- a/src/resolvers/Query/getEventVolunteerGroups.ts +++ b/src/resolvers/Query/getEventVolunteerGroups.ts @@ -1,4 +1,10 @@ -import { EventVolunteerGroup } from "../../models"; +import type { + InterfaceEvent, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, + InterfaceUser, +} from "../../models"; +import { EventVolunteer, EventVolunteerGroup } from "../../models"; import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; import { getWhere } from "./helperFunctions/getWhere"; /** @@ -9,14 +15,111 @@ import { getWhere } from "./helperFunctions/getWhere"; */ export const getEventVolunteerGroups: QueryResolvers["getEventVolunteerGroups"] = async (_parent, args) => { - const where = getWhere(args.where); - const eventVolunteerGroups = await EventVolunteerGroup.find({ - ...where, - }) - .populate("eventId") - .populate("creatorId") - .populate("leaderId") - .populate("volunteers"); + const { eventId, leaderName, userId, orgId } = args.where; + let eventVolunteerGroups: InterfaceEventVolunteerGroup[] = []; + if (eventId) { + const where = getWhere({ name_contains: args.where.name_contains }); + eventVolunteerGroups = await EventVolunteerGroup.find({ + event: eventId, + ...where, + }) + .populate("event") + .populate("creator") + .populate("leader") + .populate({ + path: "volunteers", + populate: { + path: "user", + }, + }) + .populate({ + path: "assignments", + populate: { + path: "actionItemCategory", + }, + }) + .lean(); + } else if (userId && orgId) { + const volunteerProfiles = (await EventVolunteer.find({ + user: userId, + }) + .populate({ + path: "groups", + populate: [ + { + path: "event", + }, + { + path: "creator", + }, + { + path: "leader", + }, + { + path: "volunteers", + populate: { + path: "user", + }, + }, + { + path: "assignments", + populate: { + path: "actionItemCategory", + }, + }, + ], + }) + .populate("event") + .lean()) as InterfaceEventVolunteer[]; + volunteerProfiles.forEach((volunteer) => { + const tempEvent = volunteer.event as InterfaceEvent; + if (tempEvent.organization.toString() == orgId) + eventVolunteerGroups.push(...volunteer.groups); + }); + } - return eventVolunteerGroups; + let filteredEventVolunteerGroups: InterfaceEventVolunteerGroup[] = + eventVolunteerGroups; + + if (leaderName) { + const tempName = leaderName.toLowerCase(); + filteredEventVolunteerGroups = filteredEventVolunteerGroups.filter( + (group) => { + const tempGroup = group as InterfaceEventVolunteerGroup; + const tempLeader = tempGroup.leader as InterfaceUser; + const { firstName, lastName } = tempLeader; + const name = `${firstName} ${lastName}`.toLowerCase(); + return name.includes(tempName); + }, + ); + } + + const sortConfigs = { + /* c8 ignore start */ + volunteers_ASC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => a.volunteers.length - b.volunteers.length, + /* c8 ignore stop */ + volunteers_DESC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => b.volunteers.length - a.volunteers.length, + assignments_ASC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => a.assignments.length - b.assignments.length, + assignments_DESC: ( + a: InterfaceEventVolunteerGroup, + b: InterfaceEventVolunteerGroup, + ): number => b.assignments.length - a.assignments.length, + }; + + if (args.orderBy && args.orderBy in sortConfigs) { + filteredEventVolunteerGroups.sort( + sortConfigs[args.orderBy as keyof typeof sortConfigs], + ); + } + + return filteredEventVolunteerGroups; }; diff --git a/src/resolvers/Query/getEventVolunteers.ts b/src/resolvers/Query/getEventVolunteers.ts new file mode 100644 index 0000000000..ccb461f70c --- /dev/null +++ b/src/resolvers/Query/getEventVolunteers.ts @@ -0,0 +1,61 @@ +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import type { InterfaceEventVolunteer, InterfaceUser } from "../../models"; +import { EventVolunteer } from "../../models"; +import { getSort } from "./helperFunctions/getSort"; +import { getWhere } from "./helperFunctions/getWhere"; + +/** + * This query will fetch all events volunteers for the given eventId from database. + * @param _parent- + * @param args - An object that contains `id` of the Event. + * @returns An object that holds all Event Volunteers for the given Event + */ +export const getEventVolunteers: QueryResolvers["getEventVolunteers"] = async ( + _parent, + args, +) => { + const sort = getSort(args.orderBy); + const { + id, + name_contains: nameContains, + hasAccepted, + eventId, + groupId, + } = args.where; + const where = getWhere({ id, hasAccepted }); + + const volunteers = await EventVolunteer.find({ + event: eventId, + ...(groupId && { + groups: { + $in: groupId, + }, + }), + ...where, + }) + .populate("user", "-password") + .populate("event") + .populate("groups") + .populate({ + path: "assignments", + populate: { + path: "actionItemCategory", + }, + }) + .sort(sort) + .lean(); + + let filteredVolunteers: InterfaceEventVolunteer[] = volunteers; + + if (nameContains) { + filteredVolunteers = filteredVolunteers.filter((volunteer) => { + const tempVolunteer = volunteer as InterfaceEventVolunteer; + const tempUser = tempVolunteer.user as InterfaceUser; + const { firstName, lastName } = tempUser; + const name = `${firstName} ${lastName}`.toLowerCase(); + return name.includes(nameContains.toLowerCase()); + }); + } + + return filteredVolunteers; +}; diff --git a/src/resolvers/Query/getVolunteerMembership.ts b/src/resolvers/Query/getVolunteerMembership.ts new file mode 100644 index 0000000000..f9dc8f1831 --- /dev/null +++ b/src/resolvers/Query/getVolunteerMembership.ts @@ -0,0 +1,129 @@ +import type { + InputMaybe, + QueryResolvers, + VolunteerMembershipOrderByInput, +} from "../../types/generatedGraphQLTypes"; +import type { InterfaceVolunteerMembership } from "../../models"; +import { EventVolunteer, VolunteerMembership } from "../../models"; +import { getSort } from "./helperFunctions/getSort"; + +/** + * Helper function to fetch volunteer memberships by userId + */ +const getVolunteerMembershipsByUserId = async ( + userId: string, + orderBy: InputMaybe | undefined, + status?: string, +): Promise => { + const sort = getSort(orderBy); + const volunteerInstance = await EventVolunteer.find({ user: userId }).lean(); + const volunteerIds = volunteerInstance.map((volunteer) => volunteer._id); + + return await VolunteerMembership.find({ + volunteer: { $in: volunteerIds }, + ...(status && { status }), + }) + .sort(sort) + .populate("event") + .populate("group") + .populate({ + path: "volunteer", + populate: { + path: "user", + }, + }) + .lean(); +}; + +/** + * Helper function to fetch volunteer memberships by eventId + */ +const getVolunteerMembershipsByEventId = async ( + eventId: string, + orderBy: InputMaybe | undefined, + status?: string, + group?: string, +): Promise => { + const sort = getSort(orderBy); + + return await VolunteerMembership.find({ + event: eventId, + ...(status && { status }), + ...(group && { group: group }), + }) + .sort(sort) + .populate("event") + .populate("group") + .populate({ + path: "volunteer", + populate: { + path: "user", + }, + }) + .lean(); +}; + +/** + * Helper function to filter memberships based on various criteria + */ +const filterMemberships = ( + memberships: InterfaceVolunteerMembership[], + filter?: string, + eventTitle?: string, + userName?: string, +): InterfaceVolunteerMembership[] => { + return memberships.filter((membership) => { + const filterCondition = filter + ? filter === "group" + ? !!membership.group + : !membership.group + : true; + + const eventTitleCondition = eventTitle + ? membership.event.title.includes(eventTitle) + : true; + + const userNameCondition = userName + ? ( + membership.volunteer.user.firstName + + membership.volunteer.user.lastName + ).includes(userName) + : true; + + return filterCondition && eventTitleCondition && userNameCondition; + }); +}; + +export const getVolunteerMembership: QueryResolvers["getVolunteerMembership"] = + async (_parent, args) => { + const { status, userId, filter, eventTitle, eventId, userName, groupId } = + args.where; + + let volunteerMemberships: InterfaceVolunteerMembership[] = []; + + if (userId) { + volunteerMemberships = await getVolunteerMembershipsByUserId( + userId, + args.orderBy, + status ?? undefined, + ); + } else if (eventId) { + volunteerMemberships = await getVolunteerMembershipsByEventId( + eventId, + args.orderBy, + status ?? undefined, + groupId ?? undefined, + ); + } + + if (filter || eventTitle || userName) { + return filterMemberships( + volunteerMemberships, + filter ?? undefined, + eventTitle ?? undefined, + userName ?? undefined, + ); + } + + return volunteerMemberships; + }; diff --git a/src/resolvers/Query/getVolunteerRanks.ts b/src/resolvers/Query/getVolunteerRanks.ts new file mode 100644 index 0000000000..a117652f62 --- /dev/null +++ b/src/resolvers/Query/getVolunteerRanks.ts @@ -0,0 +1,146 @@ +import { startOfWeek, startOfMonth, startOfYear, endOfDay } from "date-fns"; +import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; +import type { InterfaceEvent, InterfaceUser } from "../../models"; +import { Event, EventVolunteer } from "../../models"; + +/** + * This query will fetch volunteer ranks based on the provided time frame (allTime, weekly, monthly, yearly), + * and it will filter the results based on an array of volunteer IDs. + * @param _parent - parent of the current request + * @param args - An object that contains where object for volunteer ranks. + * + * @returns An array of `VolunteerRank` object. + */ +export const getVolunteerRanks: QueryResolvers["getVolunteerRanks"] = async ( + _parent, + args, +) => { + const { orgId } = args; + const { timeFrame, orderBy, nameContains, limit } = args.where; + + const volunteerIds: string[] = []; + const events = (await Event.find({ + organization: orgId, + }).lean()) as InterfaceEvent[]; + + // Get all volunteer IDs from the events + events.forEach((event) => { + volunteerIds.push( + ...event.volunteers.map((volunteer) => volunteer.toString()), + ); + }); + + // Fetch all volunteers + const volunteers = await EventVolunteer.find({ + _id: { $in: volunteerIds }, + }) + .populate("user") + .lean(); + + const now = new Date(); + let startDate: Date | null = null; + let endDate: Date | null = null; + + // Determine the date range based on the timeframe + switch (timeFrame) { + case "weekly": + startDate = startOfWeek(now); + endDate = endOfDay(now); + break; + case "monthly": + startDate = startOfMonth(now); + endDate = endOfDay(now); + break; + case "yearly": + startDate = startOfYear(now); + endDate = endOfDay(now); + break; + case "allTime": + default: + startDate = null; // No filtering for "allTime" + endDate = null; + break; + } + + // Accumulate total hours per user + const userHoursMap = new Map< + string, + { hoursVolunteered: number; user: InterfaceUser } + >(); + + volunteers.forEach((volunteer) => { + const userId = volunteer.user._id.toString(); + let totalHours = 0; + + // Filter hoursHistory based on the time frame + if (startDate && endDate) { + totalHours = volunteer.hoursHistory.reduce((sum, record) => { + const recordDate = new Date(record.date); + // Check if the record date is within the specified range + if (recordDate >= startDate && recordDate <= endDate) { + return sum + record.hours; + } + return sum; + }, 0); + } else { + // If "allTime", use hoursVolunteered + totalHours = volunteer.hoursVolunteered; + } + + // Accumulate hours for each user + /* c8 ignore start */ + const existingRecord = userHoursMap.get(userId); + if (existingRecord) { + existingRecord.hoursVolunteered += totalHours; + } else { + userHoursMap.set(userId, { + hoursVolunteered: totalHours, + user: volunteer.user, + }); + } + /* c8 ignore stop */ + }); + + // Convert the accumulated map to an array + const volunteerRanks = Array.from(userHoursMap.values()); + + volunteerRanks.sort((a, b) => b.hoursVolunteered - a.hoursVolunteered); + + // Assign ranks, accounting for ties + const rankedVolunteers = []; + let currentRank = 1; + let lastHours = -1; + + for (const volunteer of volunteerRanks) { + if (volunteer.hoursVolunteered !== lastHours) { + currentRank = rankedVolunteers.length + 1; // New rank + } + + rankedVolunteers.push({ + rank: currentRank, + user: volunteer.user, + hoursVolunteered: volunteer.hoursVolunteered, + }); + + lastHours = volunteer.hoursVolunteered; // Update lastHours + } + + // Sort the ranked volunteers based on the orderBy field + + if (orderBy === "hours_ASC") { + rankedVolunteers.sort((a, b) => a.hoursVolunteered - b.hoursVolunteered); + } else if (orderBy === "hours_DESC") { + rankedVolunteers.sort((a, b) => b.hoursVolunteered - a.hoursVolunteered); + } + + // Filter by name + if (nameContains) { + return rankedVolunteers.filter((volunteer) => { + const fullName = + `${volunteer.user.firstName} ${volunteer.user.lastName}`.toLowerCase(); + return fullName.includes(nameContains.toLowerCase()); + }); + } + + return limit ? rankedVolunteers.slice(0, limit) : rankedVolunteers; +}; diff --git a/src/resolvers/Query/helperFunctions/getSort.ts b/src/resolvers/Query/helperFunctions/getSort.ts index d3f68a704c..a00bde9a21 100644 --- a/src/resolvers/Query/helperFunctions/getSort.ts +++ b/src/resolvers/Query/helperFunctions/getSort.ts @@ -10,6 +10,8 @@ import type { CampaignOrderByInput, FundOrderByInput, ActionItemsOrderByInput, + EventVolunteersOrderByInput, + VolunteerMembershipOrderByInput, } from "../../../types/generatedGraphQLTypes"; export const getSort = ( @@ -24,6 +26,8 @@ export const getSort = ( | CampaignOrderByInput | PledgeOrderByInput | ActionItemsOrderByInput + | EventVolunteersOrderByInput + | VolunteerMembershipOrderByInput > | undefined, ): @@ -335,6 +339,18 @@ export const getSort = ( }; break; + case "hoursVolunteered_ASC": + sortPayload = { + hoursVolunteered: 1, + }; + break; + + case "hoursVolunteered_DESC": + sortPayload = { + hoursVolunteered: -1, + }; + break; + default: break; } diff --git a/src/resolvers/Query/helperFunctions/getWhere.ts b/src/resolvers/Query/helperFunctions/getWhere.ts index e2288ee6cb..56207568e4 100644 --- a/src/resolvers/Query/helperFunctions/getWhere.ts +++ b/src/resolvers/Query/helperFunctions/getWhere.ts @@ -13,6 +13,7 @@ import type { CampaignWhereInput, PledgeWhereInput, ActionItemCategoryWhereInput, + EventVolunteerWhereInput, } from "../../../types/generatedGraphQLTypes"; /** @@ -43,7 +44,8 @@ export const getWhere = ( CampaignWhereInput & FundWhereInput & PledgeWhereInput & - VenueWhereInput + VenueWhereInput & + EventVolunteerWhereInput > > | undefined, @@ -764,21 +766,18 @@ export const getWhere = ( }; } - // Returns objects where volunteerId is present in volunteers list - if (where.volunteerId) { + // Returns object with provided is_disabled condition + if (where.is_disabled !== undefined) { wherePayload = { ...wherePayload, - volunteers: { - $in: [where.volunteerId], - }, + isDisabled: where.is_disabled, }; } - // Returns object with provided is_disabled condition - if (where.is_disabled !== undefined) { + if (where.hasAccepted !== undefined) { wherePayload = { ...wherePayload, - isDisabled: where.is_disabled, + hasAccepted: where.hasAccepted, }; } diff --git a/src/resolvers/Query/index.ts b/src/resolvers/Query/index.ts index 8474b60298..a96026ce71 100644 --- a/src/resolvers/Query/index.ts +++ b/src/resolvers/Query/index.ts @@ -2,6 +2,7 @@ import type { QueryResolvers } from "../../types/generatedGraphQLTypes"; import { isSampleOrganization } from "../Query/organizationIsSample"; import { actionItemCategoriesByOrganization } from "./actionItemCategoriesByOrganization"; import { actionItemsByEvent } from "./actionItemsByEvent"; +import { actionItemsByUser } from "./actionItemsByUser"; import { actionItemsByOrganization } from "./actionItemsByOrganization"; import { advertisementsConnection } from "./advertisementsConnection"; import { agendaCategory } from "./agendaCategory"; @@ -18,6 +19,7 @@ import { chatsByUserId } from "./chatsByUserId"; import { event } from "./event"; import { eventsByOrganization } from "./eventsByOrganization"; import { eventsByOrganizationConnection } from "./eventsByOrganizationConnection"; +import { getEventVolunteers } from "./getEventVolunteers"; import { getEventVolunteerGroups } from "./getEventVolunteerGroups"; import { fundsByOrganization } from "./fundsByOrganization"; import { getAllAgendaItems } from "./getAllAgendaItems"; @@ -49,8 +51,11 @@ import { getEventAttendeesByEventId } from "./getEventAttendeesByEventId"; import { getVenueByOrgId } from "./getVenueByOrgId"; import { getAllNotesForAgendaItem } from "./getAllNotesForAgendaItem"; import { getNoteById } from "./getNoteById"; +import { getVolunteerMembership } from "./getVolunteerMembership"; +import { getVolunteerRanks } from "./getVolunteerRanks"; export const Query: QueryResolvers = { actionItemsByEvent, + actionItemsByUser, agendaCategory, getAgendaItem, getAgendaSection, @@ -74,6 +79,7 @@ export const Query: QueryResolvers = { getDonationByOrgId, getDonationByOrgIdConnection, getEventInvitesByUserId, + getEventVolunteers, getEventVolunteerGroups, getAllNotesForAgendaItem, getNoteById, @@ -100,4 +106,6 @@ export const Query: QueryResolvers = { getEventAttendee, getEventAttendeesByEventId, getVenueByOrgId, + getVolunteerMembership, + getVolunteerRanks, }; diff --git a/src/resolvers/index.ts b/src/resolvers/index.ts index 5319a9fc70..ef021d86ef 100644 --- a/src/resolvers/index.ts +++ b/src/resolvers/index.ts @@ -21,7 +21,6 @@ import { Comment } from "./Comment"; import { Chat } from "./Chat"; import { ChatMessage } from "./ChatMessage"; import { Event } from "./Event"; -import { EventVolunteer } from "./EventVolunteer"; import { Feedback } from "./Feedback"; import { Fund } from "./Fund"; import { MembershipRequest } from "./MembershipRequest"; @@ -51,7 +50,6 @@ const resolvers: Resolvers = { Chat, ChatMessage, Event, - EventVolunteer, Feedback, Fund, UserFamily, diff --git a/src/typeDefs/enums.ts b/src/typeDefs/enums.ts index b3e0a8b4c2..f08f9e06bd 100644 --- a/src/typeDefs/enums.ts +++ b/src/typeDefs/enums.ts @@ -141,6 +141,22 @@ export const enums = gql` endDate_DESC } + enum EventVolunteersOrderByInput { + hoursVolunteered_ASC + hoursVolunteered_DESC + } + + enum EventVolunteerGroupOrderByInput { + volunteers_ASC + volunteers_DESC + assignments_ASC + assignments_DESC + } + enum VolunteerMembershipOrderByInput { + createdAt_ASC + createdAt_DESC + } + enum WeekDays { MONDAY TUESDAY diff --git a/src/typeDefs/inputs.ts b/src/typeDefs/inputs.ts index a16daeda49..ae0958f69a 100644 --- a/src/typeDefs/inputs.ts +++ b/src/typeDefs/inputs.ts @@ -38,8 +38,9 @@ export const inputs = gql` input CreateActionItemInput { assigneeId: ID! + assigneeType: String! preCompletionNotes: String - allotedHours: Float + allottedHours: Float dueDate: Date eventId: ID } @@ -70,6 +71,7 @@ export const inputs = gql` } input ActionItemWhereInput { + orgId: ID actionItemCategory_id: ID event_id: ID categoryName: String @@ -146,31 +148,54 @@ export const inputs = gql` input EventVolunteerInput { userId: ID! eventId: ID! - groupId: ID! + groupId: ID + } + + input EventVolunteerWhereInput { + id: ID + eventId: ID + groupId: ID + hasAccepted: Boolean + name_contains: String } input EventVolunteerGroupInput { - name: String + name: String! + description: String eventId: ID! + leaderId: ID! volunteersRequired: Int + volunteerUserIds: [ID!]! } input EventVolunteerGroupWhereInput { eventId: ID - volunteerId: ID + userId: ID + orgId: ID + leaderName: String name_contains: String } - input UpdateEventVolunteerInput { + input VolunteerMembershipWhereInput { + eventTitle: String + userName: String + status: String + userId: ID eventId: ID - isAssigned: Boolean - isInvited: Boolean - response: EventVolunteerResponse + groupId: ID + filter: String + } + + input UpdateEventVolunteerInput { + assignments: [ID] + hasAccepted: Boolean + isPublic: Boolean } input UpdateEventVolunteerGroupInput { - eventId: ID + eventId: ID! name: String + description: String volunteersRequired: Int } @@ -445,11 +470,12 @@ export const inputs = gql` input UpdateActionItemInput { assigneeId: ID + assigneeType: String preCompletionNotes: String postCompletionNotes: String dueDate: Date completionDate: Date - allotedHours: Float + allottedHours: Float isCompleted: Boolean } @@ -662,6 +688,20 @@ export const inputs = gql` file: String } + input VolunteerMembershipInput { + event: ID! + group: ID + status: String! + userId: ID! + } + + input VolunteerRankWhereInput { + nameContains: String + orderBy: String! + timeFrame: String! + limit: Int + } + input VenueWhereInput { name_contains: String name_starts_with: String diff --git a/src/typeDefs/mutations.ts b/src/typeDefs/mutations.ts index b022b72be1..5253da9783 100644 --- a/src/typeDefs/mutations.ts +++ b/src/typeDefs/mutations.ts @@ -139,6 +139,10 @@ export const mutations = gql` createVenue(data: VenueInput!): Venue @auth + createVolunteerMembership( + data: VolunteerMembershipInput! + ): VolunteerMembership! @auth + deleteAdvertisement(id: ID!): DeleteAdvertisementPayload deleteAgendaCategory(id: ID!): ID! @auth @@ -306,9 +310,12 @@ export const mutations = gql` updateEventVolunteerGroup( id: ID! - data: UpdateEventVolunteerGroupInput + data: UpdateEventVolunteerGroupInput! ): EventVolunteerGroup! @auth + updateVolunteerMembership(id: ID!, status: String!): VolunteerMembership! + @auth + updateFundraisingCampaign( id: ID! data: UpdateFundCampaignInput! diff --git a/src/typeDefs/queries.ts b/src/typeDefs/queries.ts index 7572b3387c..43d6f880af 100644 --- a/src/typeDefs/queries.ts +++ b/src/typeDefs/queries.ts @@ -16,6 +16,12 @@ export const queries = gql` orderBy: ActionItemsOrderByInput ): [ActionItem] + actionItemsByUser( + userId: ID! + where: ActionItemWhereInput + orderBy: ActionItemsOrderByInput + ): [ActionItem] + actionItemCategoriesByOrganization( organizationId: ID! where: ActionItemCategoryWhereInput @@ -54,17 +60,32 @@ export const queries = gql` eventsByOrganizationConnection( where: EventWhereInput + upcomingOnly: Boolean first: Int skip: Int orderBy: EventOrderByInput ): [Event!]! - eventVolunteersByEvent(id: ID!): [EventVolunteer] + getEventVolunteers( + where: EventVolunteerWhereInput! + orderBy: EventVolunteersOrderByInput + ): [EventVolunteer]! getEventVolunteerGroups( - where: EventVolunteerGroupWhereInput + where: EventVolunteerGroupWhereInput! + orderBy: EventVolunteerGroupOrderByInput ): [EventVolunteerGroup]! + getVolunteerMembership( + where: VolunteerMembershipWhereInput! + orderBy: VolunteerMembershipOrderByInput + ): [VolunteerMembership]! + + getVolunteerRanks( + orgId: ID! + where: VolunteerRankWhereInput! + ): [VolunteerRank]! + fundsByOrganization( organizationId: ID! where: FundWhereInput diff --git a/src/typeDefs/types.ts b/src/typeDefs/types.ts index 99c1d43720..6018d229e3 100644 --- a/src/typeDefs/types.ts +++ b/src/typeDefs/types.ts @@ -67,15 +67,19 @@ export const types = gql` createdBy: User updatedBy: User } + # Action Item for a ActionItemCategory type ActionItem { _id: ID! - assignee: User + assignee: EventVolunteer + assigneeGroup: EventVolunteerGroup + assigneeUser: User + assigneeType: String! assigner: User actionItemCategory: ActionItemCategory preCompletionNotes: String postCompletionNotes: String - allotedHours: Float + allottedHours: Float assignmentDate: Date! dueDate: Date! completionDate: Date! @@ -252,21 +256,36 @@ export const types = gql` feedback: [Feedback!]! averageFeedbackScore: Float agendaItems: [AgendaItem] + volunteers: [EventVolunteer] + volunteerGroups: [EventVolunteerGroup] } type EventVolunteer { _id: ID! - createdAt: DateTime! + user: User! creator: User event: Event - group: EventVolunteerGroup - isAssigned: Boolean - isInvited: Boolean - response: String - user: User! + groups: [EventVolunteerGroup] + hasAccepted: Boolean! + isPublic: Boolean! + hoursVolunteered: Float! + assignments: [ActionItem] + hoursHistory: [HoursHistory] + createdAt: DateTime! updatedAt: DateTime! } + type HoursHistory { + hours: Float! + date: Date! + } + + type VolunteerRank { + rank: Int! + user: User! + hoursVolunteered: Float! + } + type EventAttendee { _id: ID! userId: ID! @@ -283,14 +302,28 @@ export const types = gql` type EventVolunteerGroup { _id: ID! - createdAt: DateTime! creator: User event: Event leader: User! name: String + description: String + createdAt: DateTime! updatedAt: DateTime! volunteers: [EventVolunteer] volunteersRequired: Int + assignments: [ActionItem] + } + + type VolunteerMembership { + _id: ID! + status: String! + volunteer: EventVolunteer! + event: Event! + group: EventVolunteerGroup + createdBy: User + updatedBy: User + createdAt: DateTime! + updatedAt: DateTime! } type Feedback { diff --git a/src/types/generatedGraphQLTypes.ts b/src/types/generatedGraphQLTypes.ts index a802e174be..f7995820cb 100644 --- a/src/types/generatedGraphQLTypes.ts +++ b/src/types/generatedGraphQLTypes.ts @@ -34,6 +34,7 @@ import type { InterfaceRecurrenceRule as InterfaceRecurrenceRuleModel } from '.. import type { InterfaceOrganizationTagUser as InterfaceOrganizationTagUserModel } from '../models/OrganizationTagUser'; import type { InterfaceUser as InterfaceUserModel } from '../models/User'; import type { InterfaceVenue as InterfaceVenueModel } from '../models/Venue'; +import type { InterfaceVolunteerMembership as InterfaceVolunteerMembershipModel } from '../models/VolunteerMembership'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -69,8 +70,11 @@ export type ActionItem = { __typename?: 'ActionItem'; _id: Scalars['ID']['output']; actionItemCategory?: Maybe; - allotedHours?: Maybe; - assignee?: Maybe; + allottedHours?: Maybe; + assignee?: Maybe; + assigneeGroup?: Maybe; + assigneeType: Scalars['String']['output']; + assigneeUser?: Maybe; assigner?: Maybe; assignmentDate: Scalars['Date']['output']; completionDate: Scalars['Date']['output']; @@ -106,6 +110,7 @@ export type ActionItemWhereInput = { categoryName?: InputMaybe; event_id?: InputMaybe; is_completed?: InputMaybe; + orgId?: InputMaybe; }; export type ActionItemsOrderByInput = @@ -383,8 +388,9 @@ export type ConnectionPageInfo = { }; export type CreateActionItemInput = { - allotedHours?: InputMaybe; + allottedHours?: InputMaybe; assigneeId: Scalars['ID']['input']; + assigneeType: Scalars['String']['input']; dueDate?: InputMaybe; eventId?: InputMaybe; preCompletionNotes?: InputMaybe; @@ -748,6 +754,8 @@ export type Event = { startTime?: Maybe; title: Scalars['String']['output']; updatedAt: Scalars['DateTime']['output']; + volunteerGroups?: Maybe>>; + volunteers?: Maybe>>; }; @@ -818,13 +826,15 @@ export type EventOrderByInput = export type EventVolunteer = { __typename?: 'EventVolunteer'; _id: Scalars['ID']['output']; + assignments?: Maybe>>; createdAt: Scalars['DateTime']['output']; creator?: Maybe; event?: Maybe; - group?: Maybe; - isAssigned?: Maybe; - isInvited?: Maybe; - response?: Maybe; + groups?: Maybe>>; + hasAccepted: Scalars['Boolean']['output']; + hoursHistory?: Maybe>>; + hoursVolunteered: Scalars['Float']['output']; + isPublic: Scalars['Boolean']['output']; updatedAt: Scalars['DateTime']['output']; user: User; }; @@ -832,8 +842,10 @@ export type EventVolunteer = { export type EventVolunteerGroup = { __typename?: 'EventVolunteerGroup'; _id: Scalars['ID']['output']; + assignments?: Maybe>>; createdAt: Scalars['DateTime']['output']; creator?: Maybe; + description?: Maybe; event?: Maybe; leader: User; name?: Maybe; @@ -843,20 +855,31 @@ export type EventVolunteerGroup = { }; export type EventVolunteerGroupInput = { + description?: InputMaybe; eventId: Scalars['ID']['input']; - name?: InputMaybe; + leaderId: Scalars['ID']['input']; + name: Scalars['String']['input']; + volunteerUserIds: Array; volunteersRequired?: InputMaybe; }; +export type EventVolunteerGroupOrderByInput = + | 'assignments_ASC' + | 'assignments_DESC' + | 'volunteers_ASC' + | 'volunteers_DESC'; + export type EventVolunteerGroupWhereInput = { eventId?: InputMaybe; + leaderName?: InputMaybe; name_contains?: InputMaybe; - volunteerId?: InputMaybe; + orgId?: InputMaybe; + userId?: InputMaybe; }; export type EventVolunteerInput = { eventId: Scalars['ID']['input']; - groupId: Scalars['ID']['input']; + groupId?: InputMaybe; userId: Scalars['ID']['input']; }; @@ -864,6 +887,18 @@ export type EventVolunteerResponse = | 'NO' | 'YES'; +export type EventVolunteerWhereInput = { + eventId?: InputMaybe; + groupId?: InputMaybe; + hasAccepted?: InputMaybe; + id?: InputMaybe; + name_contains?: InputMaybe; +}; + +export type EventVolunteersOrderByInput = + | 'hoursVolunteered_ASC' + | 'hoursVolunteered_DESC'; + export type EventWhereInput = { description?: InputMaybe; description_contains?: InputMaybe; @@ -1024,6 +1059,12 @@ export type Group = { updatedAt: Scalars['DateTime']['output']; }; +export type HoursHistory = { + __typename?: 'HoursHistory'; + date: Scalars['Date']['output']; + hours: Scalars['Float']['output']; +}; + export type InvalidCursor = FieldError & { __typename?: 'InvalidCursor'; message: Scalars['String']['output']; @@ -1178,6 +1219,7 @@ export type Mutation = { createUserFamily: UserFamily; createUserTag?: Maybe; createVenue?: Maybe; + createVolunteerMembership: VolunteerMembership; deleteAdvertisement?: Maybe; deleteAgendaCategory: Scalars['ID']['output']; deleteDonationById: DeletePayload; @@ -1256,6 +1298,7 @@ export type Mutation = { updateUserProfile: User; updateUserRoleInOrganization: Organization; updateUserTag?: Maybe; + updateVolunteerMembership: VolunteerMembership; }; @@ -1493,6 +1536,11 @@ export type MutationCreateVenueArgs = { }; +export type MutationCreateVolunteerMembershipArgs = { + data: VolunteerMembershipInput; +}; + + export type MutationDeleteAdvertisementArgs = { id: Scalars['ID']['input']; }; @@ -1806,7 +1854,7 @@ export type MutationUpdateEventVolunteerArgs = { export type MutationUpdateEventVolunteerGroupArgs = { - data?: InputMaybe; + data: UpdateEventVolunteerGroupInput; id: Scalars['ID']['input']; }; @@ -1886,6 +1934,12 @@ export type MutationUpdateUserTagArgs = { input: UpdateUserTagInput; }; + +export type MutationUpdateVolunteerMembershipArgs = { + id: Scalars['ID']['input']; + status: Scalars['String']['input']; +}; + export type Note = { __typename?: 'Note'; _id: Scalars['ID']['output']; @@ -2218,6 +2272,7 @@ export type Query = { actionItemCategoriesByOrganization?: Maybe>>; actionItemsByEvent?: Maybe>>; actionItemsByOrganization?: Maybe>>; + actionItemsByUser?: Maybe>>; adminPlugin?: Maybe>>; advertisementsConnection?: Maybe; agendaCategory: AgendaCategory; @@ -2230,7 +2285,6 @@ export type Query = { customDataByOrganization: Array; customFieldsByOrganization?: Maybe>>; event?: Maybe; - eventVolunteersByEvent?: Maybe>>; eventsByOrganization?: Maybe>>; eventsByOrganizationConnection: Array; fundsByOrganization?: Maybe>>; @@ -2246,6 +2300,7 @@ export type Query = { getEventAttendeesByEventId?: Maybe>>; getEventInvitesByUserId: Array; getEventVolunteerGroups: Array>; + getEventVolunteers: Array>; getFundById: Fund; getFundraisingCampaignPledgeById: FundraisingCampaignPledge; getFundraisingCampaigns: Array>; @@ -2254,6 +2309,8 @@ export type Query = { getPlugins?: Maybe>>; getUserTag?: Maybe; getVenueByOrgId?: Maybe>>; + getVolunteerMembership: Array>; + getVolunteerRanks: Array>; getlanguage?: Maybe>>; hasSubmittedFeedback?: Maybe; isSampleOrganization: Scalars['Boolean']['output']; @@ -2295,6 +2352,13 @@ export type QueryActionItemsByOrganizationArgs = { }; +export type QueryActionItemsByUserArgs = { + orderBy?: InputMaybe; + userId: Scalars['ID']['input']; + where?: InputMaybe; +}; + + export type QueryAdminPluginArgs = { orgId: Scalars['ID']['input']; }; @@ -2353,11 +2417,6 @@ export type QueryEventArgs = { }; -export type QueryEventVolunteersByEventArgs = { - id: Scalars['ID']['input']; -}; - - export type QueryEventsByOrganizationArgs = { id?: InputMaybe; orderBy?: InputMaybe; @@ -2368,6 +2427,7 @@ export type QueryEventsByOrganizationConnectionArgs = { first?: InputMaybe; orderBy?: InputMaybe; skip?: InputMaybe; + upcomingOnly?: InputMaybe; where?: InputMaybe; }; @@ -2429,7 +2489,14 @@ export type QueryGetEventInvitesByUserIdArgs = { export type QueryGetEventVolunteerGroupsArgs = { - where?: InputMaybe; + orderBy?: InputMaybe; + where: EventVolunteerGroupWhereInput; +}; + + +export type QueryGetEventVolunteersArgs = { + orderBy?: InputMaybe; + where: EventVolunteerWhereInput; }; @@ -2478,6 +2545,18 @@ export type QueryGetVenueByOrgIdArgs = { }; +export type QueryGetVolunteerMembershipArgs = { + orderBy?: InputMaybe; + where: VolunteerMembershipWhereInput; +}; + + +export type QueryGetVolunteerRanksArgs = { + orgId: Scalars['ID']['input']; + where: VolunteerRankWhereInput; +}; + + export type QueryGetlanguageArgs = { lang_code: Scalars['String']['input']; }; @@ -2703,8 +2782,9 @@ export type UpdateActionItemCategoryInput = { }; export type UpdateActionItemInput = { - allotedHours?: InputMaybe; + allottedHours?: InputMaybe; assigneeId?: InputMaybe; + assigneeType?: InputMaybe; completionDate?: InputMaybe; dueDate?: InputMaybe; isCompleted?: InputMaybe; @@ -2775,16 +2855,16 @@ export type UpdateEventInput = { }; export type UpdateEventVolunteerGroupInput = { - eventId?: InputMaybe; + description?: InputMaybe; + eventId: Scalars['ID']['input']; name?: InputMaybe; volunteersRequired?: InputMaybe; }; export type UpdateEventVolunteerInput = { - eventId?: InputMaybe; - isAssigned?: InputMaybe; - isInvited?: InputMaybe; - response?: InputMaybe; + assignments?: InputMaybe>>; + hasAccepted?: InputMaybe; + isPublic?: InputMaybe; }; export type UpdateFundCampaignInput = { @@ -3160,6 +3240,54 @@ export type VenueWhereInput = { name_starts_with?: InputMaybe; }; +export type VolunteerMembership = { + __typename?: 'VolunteerMembership'; + _id: Scalars['ID']['output']; + createdAt: Scalars['DateTime']['output']; + createdBy?: Maybe; + event: Event; + group?: Maybe; + status: Scalars['String']['output']; + updatedAt: Scalars['DateTime']['output']; + updatedBy?: Maybe; + volunteer: EventVolunteer; +}; + +export type VolunteerMembershipInput = { + event: Scalars['ID']['input']; + group?: InputMaybe; + status: Scalars['String']['input']; + userId: Scalars['ID']['input']; +}; + +export type VolunteerMembershipOrderByInput = + | 'createdAt_ASC' + | 'createdAt_DESC'; + +export type VolunteerMembershipWhereInput = { + eventId?: InputMaybe; + eventTitle?: InputMaybe; + filter?: InputMaybe; + groupId?: InputMaybe; + status?: InputMaybe; + userId?: InputMaybe; + userName?: InputMaybe; +}; + +export type VolunteerRank = { + __typename?: 'VolunteerRank'; + hoursVolunteered: Scalars['Float']['output']; + rank: Scalars['Int']['output']; + user: User; +}; + +export type VolunteerRankWhereInput = { + limit?: InputMaybe; + nameContains?: InputMaybe; + orderBy: Scalars['String']['input']; + timeFrame: Scalars['String']['input']; +}; + export type WeekDays = | 'FRIDAY' | 'MONDAY' @@ -3332,9 +3460,12 @@ export type ResolversTypes = { EventVolunteer: ResolverTypeWrapper; EventVolunteerGroup: ResolverTypeWrapper; EventVolunteerGroupInput: EventVolunteerGroupInput; + EventVolunteerGroupOrderByInput: EventVolunteerGroupOrderByInput; EventVolunteerGroupWhereInput: EventVolunteerGroupWhereInput; EventVolunteerInput: EventVolunteerInput; EventVolunteerResponse: EventVolunteerResponse; + EventVolunteerWhereInput: EventVolunteerWhereInput; + EventVolunteersOrderByInput: EventVolunteersOrderByInput; EventWhereInput: EventWhereInput; ExtendSession: ResolverTypeWrapper; Feedback: ResolverTypeWrapper; @@ -3353,6 +3484,7 @@ export type ResolversTypes = { FundraisingCampaignPledge: ResolverTypeWrapper; Gender: Gender; Group: ResolverTypeWrapper; + HoursHistory: ResolverTypeWrapper; ID: ResolverTypeWrapper; Int: ResolverTypeWrapper; InvalidCursor: ResolverTypeWrapper; @@ -3474,6 +3606,12 @@ export type ResolversTypes = { VenueInput: VenueInput; VenueOrderByInput: VenueOrderByInput; VenueWhereInput: VenueWhereInput; + VolunteerMembership: ResolverTypeWrapper; + VolunteerMembershipInput: VolunteerMembershipInput; + VolunteerMembershipOrderByInput: VolunteerMembershipOrderByInput; + VolunteerMembershipWhereInput: VolunteerMembershipWhereInput; + VolunteerRank: ResolverTypeWrapper & { user: ResolversTypes['User'] }>; + VolunteerRankWhereInput: VolunteerRankWhereInput; WeekDays: WeekDays; chatInput: ChatInput; createUserFamilyInput: CreateUserFamilyInput; @@ -3546,6 +3684,7 @@ export type ResolversParentTypes = { EventVolunteerGroupInput: EventVolunteerGroupInput; EventVolunteerGroupWhereInput: EventVolunteerGroupWhereInput; EventVolunteerInput: EventVolunteerInput; + EventVolunteerWhereInput: EventVolunteerWhereInput; EventWhereInput: EventWhereInput; ExtendSession: ExtendSession; Feedback: InterfaceFeedbackModel; @@ -3561,6 +3700,7 @@ export type ResolversParentTypes = { FundraisingCampaign: InterfaceFundraisingCampaignModel; FundraisingCampaignPledge: InterfaceFundraisingCampaignPledgesModel; Group: InterfaceGroupModel; + HoursHistory: HoursHistory; ID: Scalars['ID']['output']; Int: Scalars['Int']['output']; InvalidCursor: InvalidCursor; @@ -3669,6 +3809,11 @@ export type ResolversParentTypes = { Venue: InterfaceVenueModel; VenueInput: VenueInput; VenueWhereInput: VenueWhereInput; + VolunteerMembership: InterfaceVolunteerMembershipModel; + VolunteerMembershipInput: VolunteerMembershipInput; + VolunteerMembershipWhereInput: VolunteerMembershipWhereInput; + VolunteerRank: Omit & { user: ResolversParentTypes['User'] }; + VolunteerRankWhereInput: VolunteerRankWhereInput; chatInput: ChatInput; createUserFamilyInput: CreateUserFamilyInput; }; @@ -3686,8 +3831,11 @@ export type RoleDirectiveResolver = { _id?: Resolver; actionItemCategory?: Resolver, ParentType, ContextType>; - allotedHours?: Resolver, ParentType, ContextType>; - assignee?: Resolver, ParentType, ContextType>; + allottedHours?: Resolver, ParentType, ContextType>; + assignee?: Resolver, ParentType, ContextType>; + assigneeGroup?: Resolver, ParentType, ContextType>; + assigneeType?: Resolver; + assigneeUser?: Resolver, ParentType, ContextType>; assigner?: Resolver, ParentType, ContextType>; assignmentDate?: Resolver; completionDate?: Resolver; @@ -4039,6 +4187,8 @@ export type EventResolvers, ParentType, ContextType>; title?: Resolver; updatedAt?: Resolver; + volunteerGroups?: Resolver>>, ParentType, ContextType>; + volunteers?: Resolver>>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -4059,13 +4209,15 @@ export type EventAttendeeResolvers = { _id?: Resolver; + assignments?: Resolver>>, ParentType, ContextType>; createdAt?: Resolver; creator?: Resolver, ParentType, ContextType>; event?: Resolver, ParentType, ContextType>; - group?: Resolver, ParentType, ContextType>; - isAssigned?: Resolver, ParentType, ContextType>; - isInvited?: Resolver, ParentType, ContextType>; - response?: Resolver, ParentType, ContextType>; + groups?: Resolver>>, ParentType, ContextType>; + hasAccepted?: Resolver; + hoursHistory?: Resolver>>, ParentType, ContextType>; + hoursVolunteered?: Resolver; + isPublic?: Resolver; updatedAt?: Resolver; user?: Resolver; __isTypeOf?: IsTypeOfResolverFn; @@ -4073,8 +4225,10 @@ export type EventVolunteerResolvers = { _id?: Resolver; + assignments?: Resolver>>, ParentType, ContextType>; createdAt?: Resolver; creator?: Resolver, ParentType, ContextType>; + description?: Resolver, ParentType, ContextType>; event?: Resolver, ParentType, ContextType>; leader?: Resolver; name?: Resolver, ParentType, ContextType>; @@ -4158,6 +4312,12 @@ export type GroupResolvers; }; +export type HoursHistoryResolvers = { + date?: Resolver; + hours?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type InvalidCursorResolvers = { message?: Resolver; path?: Resolver, ParentType, ContextType>; @@ -4286,6 +4446,7 @@ export type MutationResolvers>; createUserTag?: Resolver, ParentType, ContextType, RequireFields>; createVenue?: Resolver, ParentType, ContextType, RequireFields>; + createVolunteerMembership?: Resolver>; deleteAdvertisement?: Resolver, ParentType, ContextType, RequireFields>; deleteAgendaCategory?: Resolver>; deleteDonationById?: Resolver>; @@ -4350,7 +4511,7 @@ export type MutationResolvers>; updateEvent?: Resolver>; updateEventVolunteer?: Resolver>; - updateEventVolunteerGroup?: Resolver>; + updateEventVolunteerGroup?: Resolver>; updateFund?: Resolver>; updateFundraisingCampaign?: Resolver>; updateFundraisingCampaignPledge?: Resolver>; @@ -4364,6 +4525,7 @@ export type MutationResolvers>; updateUserRoleInOrganization?: Resolver>; updateUserTag?: Resolver, ParentType, ContextType, RequireFields>; + updateVolunteerMembership?: Resolver>; }; export type NoteResolvers = { @@ -4515,6 +4677,7 @@ export type QueryResolvers>>, ParentType, ContextType, RequireFields>; actionItemsByEvent?: Resolver>>, ParentType, ContextType, RequireFields>; actionItemsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; + actionItemsByUser?: Resolver>>, ParentType, ContextType, RequireFields>; adminPlugin?: Resolver>>, ParentType, ContextType, RequireFields>; advertisementsConnection?: Resolver, ParentType, ContextType, Partial>; agendaCategory?: Resolver>; @@ -4527,7 +4690,6 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; customFieldsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; event?: Resolver, ParentType, ContextType, RequireFields>; - eventVolunteersByEvent?: Resolver>>, ParentType, ContextType, RequireFields>; eventsByOrganization?: Resolver>>, ParentType, ContextType, Partial>; eventsByOrganizationConnection?: Resolver, ParentType, ContextType, Partial>; fundsByOrganization?: Resolver>>, ParentType, ContextType, RequireFields>; @@ -4542,7 +4704,8 @@ export type QueryResolvers, ParentType, ContextType, RequireFields>; getEventAttendeesByEventId?: Resolver>>, ParentType, ContextType, RequireFields>; getEventInvitesByUserId?: Resolver, ParentType, ContextType, RequireFields>; - getEventVolunteerGroups?: Resolver>, ParentType, ContextType, Partial>; + getEventVolunteerGroups?: Resolver>, ParentType, ContextType, RequireFields>; + getEventVolunteers?: Resolver>, ParentType, ContextType, RequireFields>; getFundById?: Resolver>; getFundraisingCampaignPledgeById?: Resolver>; getFundraisingCampaigns?: Resolver>, ParentType, ContextType, Partial>; @@ -4551,6 +4714,8 @@ export type QueryResolvers>>, ParentType, ContextType>; getUserTag?: Resolver, ParentType, ContextType, RequireFields>; getVenueByOrgId?: Resolver>>, ParentType, ContextType, RequireFields>; + getVolunteerMembership?: Resolver>, ParentType, ContextType, RequireFields>; + getVolunteerRanks?: Resolver>, ParentType, ContextType, RequireFields>; getlanguage?: Resolver>>, ParentType, ContextType, RequireFields>; hasSubmittedFeedback?: Resolver, ParentType, ContextType, RequireFields>; isSampleOrganization?: Resolver>; @@ -4766,6 +4931,26 @@ export type VenueResolvers; }; +export type VolunteerMembershipResolvers = { + _id?: Resolver; + createdAt?: Resolver; + createdBy?: Resolver, ParentType, ContextType>; + event?: Resolver; + group?: Resolver, ParentType, ContextType>; + status?: Resolver; + updatedAt?: Resolver; + updatedBy?: Resolver, ParentType, ContextType>; + volunteer?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type VolunteerRankResolvers = { + hoursVolunteered?: Resolver; + rank?: Resolver; + user?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type Resolvers = { ActionItem?: ActionItemResolvers; ActionItemCategory?: ActionItemCategoryResolvers; @@ -4817,6 +5002,7 @@ export type Resolvers = { FundraisingCampaign?: FundraisingCampaignResolvers; FundraisingCampaignPledge?: FundraisingCampaignPledgeResolvers; Group?: GroupResolvers; + HoursHistory?: HoursHistoryResolvers; InvalidCursor?: InvalidCursorResolvers; JSON?: GraphQLScalarType; Language?: LanguageResolvers; @@ -4873,6 +5059,8 @@ export type Resolvers = { UsersConnection?: UsersConnectionResolvers; UsersConnectionEdge?: UsersConnectionEdgeResolvers; Venue?: VenueResolvers; + VolunteerMembership?: VolunteerMembershipResolvers; + VolunteerRank?: VolunteerRankResolvers; }; export type DirectiveResolvers = { diff --git a/src/utilities/adminCheck.ts b/src/utilities/adminCheck.ts index 3d49abcd83..8f1b7ac80d 100644 --- a/src/utilities/adminCheck.ts +++ b/src/utilities/adminCheck.ts @@ -12,12 +12,14 @@ import { AppUserProfile } from "../models"; * This is a utility method. * @param userId - The ID of the current user. It can be a string or a Types.ObjectId. * @param organization - The organization data of `InterfaceOrganization` type. + * @param throwError - A boolean value to determine if the function should throw an error. Default is `true`. * @returns `True` or `False`. */ export const adminCheck = async ( userId: string | Types.ObjectId, organization: InterfaceOrganization, -): Promise => { + throwError: boolean = true, +): Promise => { /** * Check if the user is listed as an admin in the organization. * Compares the user ID with the admin IDs in the organization. @@ -55,10 +57,15 @@ export const adminCheck = async ( * If the user is neither an organization admin nor a super admin, throw an UnauthorizedError. */ if (!userIsOrganizationAdmin && !isUserSuperAdmin) { - throw new errors.UnauthorizedError( - requestContext.translate(`${USER_NOT_AUTHORIZED_ADMIN.MESSAGE}`), - USER_NOT_AUTHORIZED_ADMIN.CODE, - USER_NOT_AUTHORIZED_ADMIN.PARAM, - ); + if (throwError) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ADMIN.MESSAGE), + USER_NOT_AUTHORIZED_ADMIN.CODE, + USER_NOT_AUTHORIZED_ADMIN.PARAM, + ); + } else { + return false; + } } + return true; }; diff --git a/src/utilities/checks.ts b/src/utilities/checks.ts new file mode 100644 index 0000000000..bac1389537 --- /dev/null +++ b/src/utilities/checks.ts @@ -0,0 +1,153 @@ +import { + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../constants"; +import { errors, requestContext } from "../libraries"; +import type { + InterfaceAppUserProfile, + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, + InterfaceUser, + InterfaceVolunteerMembership, +} from "../models"; +import { + AppUserProfile, + EventVolunteer, + EventVolunteerGroup, + User, + VolunteerMembership, +} from "../models"; +import { cacheAppUserProfile } from "../services/AppUserProfileCache/cacheAppUserProfile"; +import { findAppUserProfileCache } from "../services/AppUserProfileCache/findAppUserProfileCache"; +import { cacheUsers } from "../services/UserCache/cacheUser"; +import { findUserInCache } from "../services/UserCache/findUserInCache"; + +/** + * This function checks if the user exists. + * @param userId - user id + * @returns User + */ + +export const checkUserExists = async ( + userId: string, +): Promise => { + let currentUser: InterfaceUser | null; + const userFoundInCache = await findUserInCache([userId]); + currentUser = userFoundInCache[0]; + + if (currentUser === null) { + currentUser = await User.findById(userId).lean(); + if (currentUser !== null) await cacheUsers([currentUser]); + } + + if (!currentUser) { + throw new errors.NotFoundError( + requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE), + USER_NOT_FOUND_ERROR.CODE, + USER_NOT_FOUND_ERROR.PARAM, + ); + } + return currentUser; +}; + +/** + * This function checks if the user has an app profile. + * @param user - user object + * @returns AppUserProfile + */ +export const checkAppUserProfileExists = async ( + user: InterfaceUser, +): Promise => { + let currentUserAppProfile: InterfaceAppUserProfile | null; + const appUserProfileFoundInCache = await findAppUserProfileCache([ + user.appUserProfileId?.toString(), + ]); + currentUserAppProfile = appUserProfileFoundInCache[0]; + if (currentUserAppProfile === null) { + currentUserAppProfile = await AppUserProfile.findOne({ + userId: user._id, + }).lean(); + if (currentUserAppProfile !== null) { + await cacheAppUserProfile([currentUserAppProfile]); + } + } + if (!currentUserAppProfile) { + throw new errors.UnauthorizedError( + requestContext.translate(USER_NOT_AUTHORIZED_ERROR.MESSAGE), + USER_NOT_AUTHORIZED_ERROR.CODE, + USER_NOT_AUTHORIZED_ERROR.PARAM, + ); + } + return currentUserAppProfile; +}; + +/** + * This function checks if the event volunteer exists. + * @param volunteerId - event volunteer id + * @returns EventVolunteer + */ +export const checkEventVolunteerExists = async ( + volunteerId: string, +): Promise => { + const volunteer = await EventVolunteer.findById(volunteerId); + + if (!volunteer) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.PARAM, + ); + } + + return volunteer; +}; + +/** + * This function checks if the volunteer group exists. + * @param groupId - event volunteer group id + * @returns EventVolunteerGroup + */ + +export const checkVolunteerGroupExists = async ( + groupId: string, +): Promise => { + const volunteerGroup = await EventVolunteerGroup.findOne({ + _id: groupId, + }); + + if (!volunteerGroup) { + throw new errors.NotFoundError( + requestContext.translate(EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.PARAM, + ); + } + return volunteerGroup; +}; + +/** + * This function checks if the volunteerMembership exists. + * @param membershipId - id + * @returns VolunteerMembership + */ +export const checkVolunteerMembershipExists = async ( + membershipId: string, +): Promise => { + const volunteerMembership = await VolunteerMembership.findOne({ + _id: membershipId, + }); + + if (!volunteerMembership) { + throw new errors.NotFoundError( + requestContext.translate( + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.MESSAGE, + ), + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.CODE, + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.PARAM, + ); + } + return volunteerMembership; +}; diff --git a/tests/helpers/actionItem.ts b/tests/helpers/actionItem.ts index 86eab57629..b1897f717e 100644 --- a/tests/helpers/actionItem.ts +++ b/tests/helpers/actionItem.ts @@ -35,6 +35,7 @@ export const createTestActionItem = async (): Promise< const testActionItem = await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory?._id, organization: testOrganization?._id, @@ -59,6 +60,7 @@ export const createNewTestActionItem = async ({ const newTestActionItem = await ActionItem.create({ creator: currUserId, assignee: assignedUserId, + assigneeType: "EventVolunteer", assigner: currUserId, actionItemCategory: actionItemCategoryId, organization: organizationId, @@ -82,6 +84,7 @@ export const createTestActionItems = async (): Promise< const testActionItem1 = await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory?._id, organization: testOrganization?._id, @@ -91,6 +94,7 @@ export const createTestActionItems = async (): Promise< const testActionItem2 = await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory?._id, organization: testOrganization?._id, @@ -100,6 +104,7 @@ export const createTestActionItems = async (): Promise< await ActionItem.create({ creator: testUser?._id, assignee: randomUser?._id, + assigneeType: "EventVolunteer", assigner: testUser?._id, actionItemCategory: testCategory2?._id, organization: testOrganization?._id, diff --git a/tests/helpers/events.ts b/tests/helpers/events.ts index 75298ee74a..13130906a8 100644 --- a/tests/helpers/events.ts +++ b/tests/helpers/events.ts @@ -1,6 +1,5 @@ import type { Document } from "mongoose"; import { nanoid } from "nanoid"; -import { EventVolunteerResponse } from "../../src/constants"; import type { InterfaceEvent, InterfaceEventVolunteer, @@ -132,12 +131,11 @@ export const createTestEventAndVolunteer = async (): Promise< const [creatorUser, , testEvent] = await createTestEvent(); const volunteerUser = await createTestUser(); const testEventVolunteer = await EventVolunteer.create({ - userId: volunteerUser?._id, - eventId: testEvent?._id, - isInvited: true, - isAssigned: false, - creatorId: creatorUser?._id, - response: EventVolunteerResponse.NO, + user: volunteerUser?._id, + event: testEvent?._id, + creator: creatorUser?._id, + hasAccepted: false, + isPublic: false, }); return [volunteerUser, creatorUser, testEvent, testEventVolunteer]; @@ -157,9 +155,9 @@ export const createTestEventVolunteerGroup = async (): Promise< const testEventVolunteerGroup = await EventVolunteerGroup.create({ name: "testEventVolunteerGroup", volunteersRequired: 1, - eventId: testEvent?._id, - creatorId: creatorUser?._id, - leaderId: creatorUser?._id, + event: testEvent?._id, + creator: creatorUser?._id, + leader: creatorUser?._id, volunteers: [testEventVolunteer?._id], }); diff --git a/tests/helpers/volunteers.ts b/tests/helpers/volunteers.ts new file mode 100644 index 0000000000..026dc4313a --- /dev/null +++ b/tests/helpers/volunteers.ts @@ -0,0 +1,299 @@ +import type { + InterfaceEventVolunteer, + InterfaceEventVolunteerGroup, + InterfaceVolunteerMembership, +} from "../../src/models"; +import { + ActionItem, + ActionItemCategory, + Event, + EventVolunteer, + EventVolunteerGroup, +} from "../../src/models"; +import type { Document } from "mongoose"; +import { + createTestUser, + createTestUserAndOrganization, + type TestOrganizationType, + type TestUserType, +} from "./userAndOrg"; +import { nanoid } from "nanoid"; +import type { TestEventType } from "./events"; +import type { TestActionItemType } from "./actionItem"; + +export type TestVolunteerType = InterfaceEventVolunteer & Document; +export type TestVolunteerGroupType = InterfaceEventVolunteerGroup & Document; +export type TestVolunteerMembership = InterfaceVolunteerMembership & Document; + +export const createTestVolunteerAndGroup = async (): Promise< + [ + TestUserType, + TestOrganizationType, + TestEventType, + TestVolunteerType, + TestVolunteerGroupType, + ] +> => { + const [testUser, testOrganization] = await createTestUserAndOrganization(); + const randomUser = await createTestUser(); + + const testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: false, + isPublic: true, + isRegisterable: true, + creatorId: testUser?._id, + admins: [testUser?._id], + organization: testOrganization?._id, + volunteers: [], + volunteerGroups: [], + }); + + const testVolunteer = await EventVolunteer.create({ + creator: randomUser?._id, + event: testEvent?._id, + user: testUser?._id, + groups: [], + assignments: [], + }); + + // create a volunteer group with testVolunteer as a member & leader + const testVolunteerGroup = await EventVolunteerGroup.create({ + creator: randomUser?._id, + event: testEvent?._id, + volunteers: [testVolunteer?._id], + leader: testVolunteer?._id, + assignments: [], + name: "Test Volunteer Group 1", + }); + + // add volunteer & group to event + await Event.updateOne( + { + _id: testEvent?._id, + }, + { + $push: { + volunteers: testVolunteer?._id, + volunteerGroups: testVolunteerGroup?._id, + }, + }, + ); + + // add group to volunteer + await EventVolunteer.updateOne( + { + _id: testVolunteer?._id, + }, + { + $push: { + groups: testVolunteerGroup?._id, + }, + }, + ); + + return [ + testUser, + testOrganization, + testEvent, + testVolunteer, + testVolunteerGroup, + ]; +}; + +export const createVolunteerAndActions = async (): Promise< + [ + TestOrganizationType, + TestEventType, + TestUserType, + TestUserType, + TestVolunteerType, + TestVolunteerType, + TestVolunteerGroupType, + TestActionItemType, + TestActionItemType, + ] +> => { + const [testUser, testOrganization] = await createTestUserAndOrganization(); + const testUser2 = await createTestUser(); + + const randomUser = await createTestUser(); + + const testEvent = await Event.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + allDay: true, + startDate: new Date(), + recurring: false, + isPublic: true, + isRegisterable: true, + creatorId: testUser?._id, + admins: [testUser?._id], + organization: testOrganization?._id, + volunteers: [], + volunteerGroups: [], + }); + + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + const twoWeeksAgo = new Date(today); + twoWeeksAgo.setDate(today.getDate() - 14); + const twoMonthsAgo = new Date(today); + twoMonthsAgo.setMonth(today.getMonth() - 2); + const twoYearsAgo = new Date(today); + twoYearsAgo.setFullYear(today.getFullYear() - 2); + + const testVolunteer1 = await EventVolunteer.create({ + creator: randomUser?._id, + event: testEvent?._id, + user: testUser?._id, + groups: [], + assignments: [], + hasAccepted: true, + hoursVolunteered: 10, + hoursHistory: [ + { + hours: 2, + date: yesterday, + }, + { + hours: 4, + date: twoWeeksAgo, + }, + { + hours: 2, + date: twoMonthsAgo, + }, + { + hours: 2, + date: twoYearsAgo, + }, + ], + }); + + const testVolunteer2 = await EventVolunteer.create({ + creator: randomUser?._id, + event: testEvent?._id, + user: testUser2?._id, + groups: [], + assignments: [], + hasAccepted: true, + hoursVolunteered: 8, + hoursHistory: [ + { + hours: 1, + date: yesterday, + }, + { + hours: 2, + date: twoWeeksAgo, + }, + { + hours: 3, + date: twoMonthsAgo, + }, + { + hours: 2, + date: twoYearsAgo, + }, + ], + }); + + // create a volunteer group with testVolunteer1 as a member & leader + const testVolunteerGroup = await EventVolunteerGroup.create({ + creator: randomUser?._id, + event: testEvent?._id, + volunteers: [testVolunteer1?._id, testVolunteer2?._id], + leader: testUser?._id, + assignments: [], + name: "Test Volunteer Group 1", + }); + + // add volunteer & group to event + await Event.updateOne( + { + _id: testEvent?._id, + }, + { + $addToSet: { + volunteers: { $each: [testVolunteer1?._id, testVolunteer2?._id] }, + volunteerGroups: testVolunteerGroup?._id, + }, + }, + ); + + const testActionItemCategory = await ActionItemCategory.create({ + creatorId: randomUser?._id, + organizationId: testOrganization?._id, + name: "Test Action Item Category 1", + isDisabled: false, + }); + + const testActionItem1 = await ActionItem.create({ + creator: randomUser?._id, + assigner: randomUser?._id, + assignee: testVolunteer1?._id, + assigneeType: "EventVolunteer", + assigneeGroup: null, + assigneeUser: null, + actionItemCategory: testActionItemCategory?._id, + event: testEvent?._id, + organization: testOrganization?._id, + allottedHours: 2, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + isCompleted: false, + }); + + const testActionItem2 = await ActionItem.create({ + creator: randomUser?._id, + assigner: randomUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: testVolunteerGroup?._id, + assignee: null, + assigneeUser: null, + actionItemCategory: testActionItemCategory?._id, + event: testEvent?._id, + organization: testOrganization?._id, + allottedHours: 4, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 2000), + isCompleted: false, + }); + + await EventVolunteer.findByIdAndUpdate(testVolunteer1?._id, { + $push: { + assignments: testActionItem1?._id, + }, + }); + + await EventVolunteer.updateMany( + { _id: { $in: [testVolunteer1?._id, testVolunteer2?._id] } }, + { + $push: { + groups: testVolunteerGroup?._id, + assignments: testActionItem2?._id, + }, + }, + ); + + await EventVolunteerGroup.findByIdAndUpdate(testVolunteerGroup?._id, { + $addToSet: { assignments: testActionItem2 }, + }); + + return [ + testOrganization, + testEvent, + testUser, + testUser2, + testVolunteer1, + testVolunteer2, + testVolunteerGroup, + testActionItem1, + testActionItem2, + ]; +}; diff --git a/tests/resolvers/Event/actionItems.spec.ts b/tests/resolvers/Event/actionItems.spec.ts index 716f511d96..ad06fbb673 100644 --- a/tests/resolvers/Event/actionItems.spec.ts +++ b/tests/resolvers/Event/actionItems.spec.ts @@ -26,7 +26,7 @@ describe("resolvers -> Organization -> actionItems", () => { const actionItemsPayload = await actionItemsResolver?.(parent, {}, {}); const actionItems = await ActionItem.find({ - eventId: testEvent?._id, + event: testEvent?._id, }).lean(); expect(actionItemsPayload).toEqual(actionItems); diff --git a/tests/resolvers/EventVolunteer/creator.spec.ts b/tests/resolvers/EventVolunteer/creator.spec.ts deleted file mode 100644 index 88e07a8722..0000000000 --- a/tests/resolvers/EventVolunteer/creator.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import "dotenv/config"; -import { creator as creatorResolver } from "../../../src/resolvers/EventVolunteer/creator"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - beforeEach, - vi, -} from "vitest"; -import type { TestEventVolunteerType } from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import type { TestUserType } from "../../helpers/userAndOrg"; -import type { InterfaceEventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEventVolunteer: TestEventVolunteerType; -let creatorUser: TestUserType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [, creatorUser, , testEventVolunteer] = await createTestEventAndVolunteer(); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> creator", () => { - beforeEach(() => { - vi.resetModules(); - }); - it(`returns the correct creator object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - const creatorPayload = await creatorResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - - expect(creatorPayload?._id).toEqual(creatorUser?._id); - }); -}); diff --git a/tests/resolvers/EventVolunteer/event.spec.ts b/tests/resolvers/EventVolunteer/event.spec.ts deleted file mode 100644 index 7a0b04ab5b..0000000000 --- a/tests/resolvers/EventVolunteer/event.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import "dotenv/config"; -import { event as eventResolver } from "../../../src/resolvers/EventVolunteer/event"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { - TestEventType, - TestEventVolunteerType, -} from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import type { InterfaceEventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEvent: TestEventType; -let testEventVolunteer: TestEventVolunteerType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [, , testEvent, testEventVolunteer] = await createTestEventAndVolunteer(); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> event", () => { - it(`returns the correct event object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - - const eventPayload = await eventResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - - expect(eventPayload).toEqual(testEvent?.toObject()); - }); -}); diff --git a/tests/resolvers/EventVolunteer/group.spec.ts b/tests/resolvers/EventVolunteer/group.spec.ts deleted file mode 100644 index 856641b624..0000000000 --- a/tests/resolvers/EventVolunteer/group.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import "dotenv/config"; -import { group as groupResolver } from "../../../src/resolvers/EventVolunteer/group"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - beforeEach, - vi, -} from "vitest"; -import type { - TestEventType, - TestEventVolunteerType, -} from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { TestUserType } from "../../helpers/userAndOrg"; -import type { InterfaceEventVolunteer } from "../../../src/models"; -import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; -import { createTestUser } from "../../helpers/user"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEventVolunteer: TestEventVolunteerType; -let eventAdminUser: TestUserType; -let testUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testUser = await createTestUser(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); - testEventVolunteer = await EventVolunteer.create({ - eventId: testEvent?._id, - userId: testUser?._id, - creatorId: eventAdminUser?._id, - groupId: testGroup?._id, - isAssigned: false, - isInvited: true, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> group", () => { - beforeEach(() => { - vi.resetModules(); - }); - it(`returns the correct event volunteer group object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - const groupPayload = await groupResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - console.log(groupPayload); - console.log(testGroup); - - expect(groupPayload?._id).toEqual(testGroup?._id); - }); -}); diff --git a/tests/resolvers/EventVolunteer/user.spec.ts b/tests/resolvers/EventVolunteer/user.spec.ts deleted file mode 100644 index 56ac382f6b..0000000000 --- a/tests/resolvers/EventVolunteer/user.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import "dotenv/config"; -import { user as userResolver } from "../../../src/resolvers/EventVolunteer/user"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - beforeEach, - vi, -} from "vitest"; -import type { TestEventVolunteerType } from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import type { TestUserType } from "../../helpers/userAndOrg"; -import type { InterfaceEventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testUser: TestUserType; -let testEventVolunteer: TestEventVolunteerType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [testUser, , , testEventVolunteer] = await createTestEventAndVolunteer(); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> user", () => { - beforeEach(() => { - vi.resetModules(); - }); - it(`returns the correct user object for parent event volunteer`, async () => { - const parent = testEventVolunteer?.toObject(); - console.log(testEventVolunteer?.userId); - console.log(testUser?._id); - - const userPayload = await userResolver?.( - parent as InterfaceEventVolunteer, - {}, - {}, - ); - - expect(userPayload).toEqual({ - ...testUser?.toObject(), - updatedAt: expect.anything(), - }); - }); -}); diff --git a/tests/resolvers/EventVolunteerGroup/creator.spec.ts b/tests/resolvers/EventVolunteerGroup/creator.spec.ts deleted file mode 100644 index e9bd6502bf..0000000000 --- a/tests/resolvers/EventVolunteerGroup/creator.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "dotenv/config"; -import { creator as creatorResolver } from "../../../src/resolvers/EventVolunteerGroup/creator"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { InterfaceEventVolunteerGroup } from "../../../src/models"; -import { EventVolunteerGroup } from "../../../src/models"; -import type { TestUserType } from "../../helpers/user"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let eventAdminUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> creator", () => { - it(`returns the correct creator user object for parent event volunteer group`, async () => { - const parent = testGroup?.toObject(); - - const creatorPayload = await creatorResolver?.( - parent as InterfaceEventVolunteerGroup, - {}, - {}, - ); - - expect(creatorPayload?._id).toEqual(eventAdminUser?._id); - }); -}); diff --git a/tests/resolvers/EventVolunteerGroup/event.spec.ts b/tests/resolvers/EventVolunteerGroup/event.spec.ts deleted file mode 100644 index 5cfaeb7450..0000000000 --- a/tests/resolvers/EventVolunteerGroup/event.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "dotenv/config"; -import { event as eventResolver } from "../../../src/resolvers/EventVolunteerGroup/event"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { InterfaceEventVolunteerGroup } from "../../../src/models"; -import { EventVolunteerGroup } from "../../../src/models"; -import type { TestUserType } from "../../helpers/user"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let eventAdminUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> event", () => { - it(`returns the correct event object for parent event volunteer group`, async () => { - const parent = testGroup?.toObject(); - - const eventPayload = await eventResolver?.( - parent as InterfaceEventVolunteerGroup, - {}, - {}, - ); - - expect(eventPayload).toEqual(testEvent?.toObject()); - }); -}); diff --git a/tests/resolvers/EventVolunteerGroup/leader.spec.ts b/tests/resolvers/EventVolunteerGroup/leader.spec.ts deleted file mode 100644 index 07481e41b5..0000000000 --- a/tests/resolvers/EventVolunteerGroup/leader.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "dotenv/config"; -import { leader as leaderResolver } from "../../../src/resolvers/EventVolunteerGroup/leader"; -import { connect, disconnect } from "../../helpers/db"; -import type mongoose from "mongoose"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEvent } from "../../helpers/events"; -import type { InterfaceEventVolunteerGroup } from "../../../src/models"; -import { EventVolunteerGroup } from "../../../src/models"; -import type { TestUserType } from "../../helpers/user"; -import type { TestEventVolunteerGroupType } from "../Mutation/createEventVolunteer.spec"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let eventAdminUser: TestUserType; -let testEvent: TestEventType; -let testGroup: TestEventVolunteerGroupType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - [eventAdminUser, , testEvent] = await createTestEvent(); - testGroup = await EventVolunteerGroup.create({ - name: "test", - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, - eventId: testEvent?._id, - }); -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> EventVolunteer -> leader", () => { - it(`returns the correct leader user object for parent event volunteer group`, async () => { - const parent = testGroup?.toObject(); - - const leaderPayload = await leaderResolver?.( - parent as InterfaceEventVolunteerGroup, - {}, - {}, - ); - - expect(leaderPayload?._id).toEqual(eventAdminUser?._id); - }); -}); diff --git a/tests/resolvers/Mutation/createActionItem.spec.ts b/tests/resolvers/Mutation/createActionItem.spec.ts index e1093a4975..c8b35b5285 100644 --- a/tests/resolvers/Mutation/createActionItem.spec.ts +++ b/tests/resolvers/Mutation/createActionItem.spec.ts @@ -1,85 +1,57 @@ -import "dotenv/config"; import type mongoose from "mongoose"; -import { Types } from "mongoose"; -import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceActionItem } from "../../../src/models"; +import { ActionItemCategory } from "../../../src/models"; import { ACTION_ITEM_CATEGORY_IS_DISABLED, ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, - USER_NOT_AUTHORIZED_ERROR, - USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, } from "../../../src/constants"; -import { createActionItem as createActionItemResolver } from "../../../src/resolvers/Mutation/createActionItem"; -import type { MutationCreateActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; -import { connect, disconnect } from "../../helpers/db"; -import type { - TestOrganizationType, - TestUserType, -} from "../../helpers/userAndOrg"; -import { createTestUser } from "../../helpers/userAndOrg"; - -import { nanoid } from "nanoid"; -import { - ActionItemCategory, - AppUserProfile, - Event, - User, -} from "../../../src/models"; -import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; -import { createTestCategory } from "../../helpers/actionItemCategory"; -import type { TestEventType } from "../../helpers/events"; +import { requestContext } from "../../../src/libraries"; +import { createActionItem } from "../../../src/resolvers/Mutation/createActionItem"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import type { TestActionItemType } from "../../helpers/actionItem"; -let randomUser: TestUserType; -let randomUser2: TestUserType; -// let superAdminTestUserAppProfile: TestAppUserProfileType; -let testUser: TestUserType; +let MONGOOSE_INSTANCE: typeof mongoose; let testOrganization: TestOrganizationType; -let testCategory: TestActionItemCategoryType; -let testDisabledCategory: TestActionItemCategoryType; let testEvent: TestEventType; -let MONGOOSE_INSTANCE: typeof mongoose; +let testUser1: TestUserType; +let testActionItem1: TestActionItemType; +let testEventVolunteer1: TestEventVolunteerType; +let testEventVolunteerGroup: TestEventVolunteerGroupType; 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(); - - await AppUserProfile.updateOne( - { - userId: randomUser2?._id, - }, - { - isSuperAdmin: true, - }, - ); - - [testUser, testOrganization, testCategory] = await createTestCategory(); - - testDisabledCategory = await ActionItemCategory.create({ - name: "a disabled category", - organizationId: testOrganization?._id, - isDisabled: true, - creatorId: testUser?._id, - }); - - testEvent = await Event.create({ - title: `title${nanoid().toLowerCase()}`, - description: `description${nanoid().toLowerCase()}`, - allDay: true, - startDate: new Date(), - recurring: false, - isPublic: true, - isRegisterable: true, - creatorId: randomUser?._id, - admins: [randomUser?._id], - organization: testOrganization?._id, - }); + const [ + organization, + event, + user1, + , + volunteer1, + , + volunteerGroup, + actionItem1, + ] = await createVolunteerAndActions(); + + testOrganization = organization; + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testEventVolunteerGroup = volunteerGroup; + testActionItem1 = actionItem1; }); afterAll(async () => { @@ -87,255 +59,161 @@ afterAll(async () => { }); 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: new Types.ObjectId().toString(), - }; - - await createActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); - } - }); - - it(`throws NotFoundError if no actionItemCategory exists with _id === args.actionItemCategoryId`, async () => { + it(`throws EventVolunteer Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEvent?._id, + assigneeType: "EventVolunteer", + }, + actionItemCategoryId: testEventVolunteer1?._id, }, - actionItemCategoryId: new Types.ObjectId().toString(), - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as Error).message).toEqual( - ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE, + EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, ); } }); - it(`throws ConflictError if the actionItemCategory is disabled`, async () => { + it(`throws EventVolunteerGroup Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEvent?._id, + assigneeType: "EventVolunteerGroup", + }, + actionItemCategoryId: testEventVolunteer1?._id, }, - actionItemCategoryId: testDisabledCategory._id, - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as Error).message).toEqual( - ACTION_ITEM_CATEGORY_IS_DISABLED.MESSAGE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, ); } }); - it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { + it(`throws ActionItemCategory Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: new Types.ObjectId().toString(), + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + }, + actionItemCategoryId: testEventVolunteer1?._id, }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { - expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + ACTION_ITEM_CATEGORY_NOT_FOUND_ERROR.MESSAGE, + ); } }); - it(`throws NotFoundError new assignee is not a member of the organization`, async () => { + it(`throws ActionItemCategory is Disabled`, async () => { + const disabledCategory = await ActionItemCategory.create({ + creatorId: testUser1?._id, + organizationId: testOrganization?._id, + name: "Disabled Category", + isDisabled: true, + }); try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + }, + actionItemCategoryId: disabledCategory?._id.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: testUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as Error).message).toEqual( - USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE, + ACTION_ITEM_CATEGORY_IS_DISABLED.MESSAGE, ); } }); - it(`throws NotFoundError if no event exists with _id === args.data.eventId`, async () => { - await User.findOneAndUpdate( - { - _id: randomUser?._id, - }, - { - $push: { joinedOrganizations: testOrganization?._id }, - }, - ); - + it(`throws Event Not Found`, async () => { try { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, - eventId: new Types.ObjectId().toString(), + (await createActionItem?.( + {}, + { + data: { + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + eventId: testUser1?._id.toString(), + }, + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: randomUser?._id, - }; - - await createActionItemResolver?.({}, args, context); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; } catch (error: unknown) { expect((error as 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 = { + it(`Create Action Item (EventVolunteer) `, async () => { + const createdItem = (await createActionItem?.( + {}, + { data: { - assigneeId: randomUser?._id, + assigneeId: testEventVolunteer1?._id, + assigneeType: "EventVolunteer", + eventId: testEvent?._id.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: randomUser?._id, - }; - - await createActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as 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.toString() ?? "", + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - - const context = { - userId: randomUser?._id, - }; - - const createActionItemPayload = await createActionItemResolver?.( - {}, - args, - context, - ); + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; - expect(createActionItemPayload).toEqual( - expect.objectContaining({ - actionItemCategory: testCategory?._id, - }), - ); + expect(createdItem).toBeDefined(); + expect(createdItem.creator).toEqual(testUser1?._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?.( + it(`Create Action Item (EventVolunteerGroup) `, async () => { + const createdItem = (await createActionItem?.( {}, - args, - context, - ); - - expect(createActionItemPayload).toEqual( - expect.objectContaining({ - actionItemCategory: testCategory?._id, - }), - ); - }); - - it(`creates the actionItem when user is authorized as superadmin`, async () => { - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + { + data: { + assigneeId: testEventVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + eventId: testEvent?._id.toString(), + }, + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; - const context = { - userId: randomUser2?._id, - }; - // const superAdmin = await AppUserProfile.findOne({ - // userId: randomUser2?._id, - // }); - // console.log(superAdmin) + expect(createdItem).toBeDefined(); + expect(createdItem.creator).toEqual(testUser1?._id); + }); - const createActionItemPayload = await createActionItemResolver?.( + it(`Create Action Item (User) `, async () => { + const createdItem = (await createActionItem?.( {}, - args, - context, - ); - - expect(createActionItemPayload).toEqual( - expect.objectContaining({ - actionItemCategory: testCategory?._id, - }), - ); - }); - it("throws error if the user does not have appUserProfile", async () => { - await AppUserProfile.deleteOne({ - userId: randomUser?._id, - }); - const args: MutationCreateActionItemArgs = { - data: { - assigneeId: randomUser?._id, + { + data: { + assigneeId: testUser1?._id.toString() as string, + assigneeType: "User", + }, + actionItemCategoryId: testActionItem1.actionItemCategory.toString(), }, - actionItemCategoryId: testCategory?._id, - }; - const context = { - userId: randomUser?._id, - }; - try { - await createActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual( - USER_NOT_AUTHORIZED_ERROR.MESSAGE, - ); - } + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceActionItem; + + expect(createdItem).toBeDefined(); + expect(createdItem.creator).toEqual(testUser1?._id); }); }); diff --git a/tests/resolvers/Mutation/createEventVolunteer.spec.ts b/tests/resolvers/Mutation/createEventVolunteer.spec.ts index 8d339b4370..7734361eef 100644 --- a/tests/resolvers/Mutation/createEventVolunteer.spec.ts +++ b/tests/resolvers/Mutation/createEventVolunteer.spec.ts @@ -45,10 +45,11 @@ beforeAll(async () => { [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", + volunteers: [eventAdminUser?._id, testUser2?._id, testUser1?._id], }); }); @@ -230,22 +231,15 @@ describe("resolvers -> Mutation -> createEventVolunteer", () => { context, ); - const updatedGroup = await EventVolunteerGroup.findOne({ - _id: testGroup?._id, - }); - - expect(updatedGroup?.volunteers.toString()).toEqual( - [createdVolunteer?._id.toString()].toString(), - ); - expect(createdVolunteer).toEqual( expect.objectContaining({ - eventId: new Types.ObjectId(testEvent?.id), - userId: testUser2?._id, - groupId: testGroup?._id, - creatorId: eventAdminUser?._id, - isInvited: true, - isAssigned: false, + event: new Types.ObjectId(testEvent?.id), + user: testUser2?._id, + groups: [], + creator: eventAdminUser?._id, + hasAccepted: false, + isPublic: true, + hoursVolunteered: 0, }), ); }); diff --git a/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts b/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts index f41663b29b..5604676eac 100644 --- a/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts +++ b/tests/resolvers/Mutation/createEventVolunteerGroup.spec.ts @@ -54,6 +54,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: testUser?._id, + volunteerUserIds: [testUser?._id], eventId: testEvent?._id, }, }; @@ -82,6 +84,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: testUser?._id, + volunteerUserIds: [testUser?._id], eventId: new Types.ObjectId().toString(), }, }; @@ -111,6 +115,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: testUser?._id, + volunteerUserIds: [testUser?._id], eventId: testEvent?._id, }, }; @@ -137,6 +143,8 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { const args: MutationCreateEventVolunteerGroupArgs = { data: { name: "Test group", + leaderId: eventAdminUser?._id, + volunteerUserIds: [testUser?._id], eventId: testEvent?._id, }, }; @@ -163,9 +171,9 @@ describe("resolvers -> Mutation -> createEventVolunteerGroup", () => { expect(createdGroup).toEqual( expect.objectContaining({ name: "Test group", - eventId: new Types.ObjectId(testEvent?.id), - creatorId: eventAdminUser?._id, - leaderId: eventAdminUser?._id, + event: new Types.ObjectId(testEvent?.id), + creator: eventAdminUser?._id, + leader: eventAdminUser?._id, }), ); }); diff --git a/tests/resolvers/Mutation/createVolunteerMembership.spec.ts b/tests/resolvers/Mutation/createVolunteerMembership.spec.ts new file mode 100644 index 0000000000..6ef779bbc6 --- /dev/null +++ b/tests/resolvers/Mutation/createVolunteerMembership.spec.ts @@ -0,0 +1,165 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import type { Document } from "mongoose"; +import { Types } from "mongoose"; +import type { MutationCreateVolunteerMembershipArgs } from "../../../src/types/generatedGraphQLTypes"; +import { connect, disconnect } from "../../helpers/db"; + +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { + EVENT_NOT_FOUND_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import type { TestUserType } from "../../helpers/userAndOrg"; +import { createTestEvent } from "../../helpers/events"; +import type { TestEventType } from "../../helpers/events"; +import { createTestUser } from "../../helpers/user"; +import type { InterfaceEventVolunteerGroup } from "../../../src/models"; +import { createVolunteerMembership } from "../../../src/resolvers/Mutation/createVolunteerMembership"; +import type { + TestVolunteerGroupType, + TestVolunteerType, +} from "../../helpers/volunteers"; +import { createTestVolunteerAndGroup } from "../../helpers/volunteers"; + +export type TestEventVolunteerGroupType = + | (InterfaceEventVolunteerGroup & Document) + | null; + +let testUser1: TestUserType; +let testEvent: TestEventType; +let tUser: TestUserType; +let tEvent: TestEventType; +let tVolunteer: TestVolunteerType; +let tVolunteerGroup: TestVolunteerGroupType; +let MONGOOSE_INSTANCE: typeof mongoose; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); + testUser1 = await createTestUser(); + [, , testEvent] = await createTestEvent(); + + [tUser, , tEvent, tVolunteer, tVolunteerGroup] = + await createTestVolunteerAndGroup(); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> createVolunteerMembership", () => { + afterEach(() => { + vi.doUnmock("../../../src/constants"); + vi.resetModules(); + }); + + it(`throws NotFoundError if no user exists with _id === context.userId`, async () => { + try { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: tEvent?._id, + group: tVolunteerGroup?._id, + status: "invited", + userId: tUser?._id, + }, + }; + + const context = { + userId: new Types.ObjectId().toString(), + }; + + await createVolunteerMembership?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no volunteer user exists with _id === args.data.userId`, async () => { + try { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: tEvent?._id, + group: tVolunteerGroup?._id, + status: "invited", + userId: new Types.ObjectId().toString(), + }, + }; + + const context = { + userId: tUser?._id, + }; + + await createVolunteerMembership?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`throws NotFoundError if no event exists with _id === args.data.event`, async () => { + try { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: new Types.ObjectId().toString(), + group: tVolunteerGroup?._id, + status: "invited", + userId: tUser?._id, + }, + }; + + const context = { + userId: tUser?._id, + }; + + await createVolunteerMembership?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`Create Voluneer Membership when volunteer already exists`, async () => { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: tEvent?._id, + group: tVolunteerGroup?._id, + status: "invited", + userId: tUser?._id, + }, + }; + + const context = { + userId: tUser?._id, + }; + + const mem = await createVolunteerMembership?.({}, args, context); + expect(mem?.volunteer).toEqual(tVolunteer?._id); + }); + + it(`Create Voluneer Membership when volunteer doesn't exists`, async () => { + const args: MutationCreateVolunteerMembershipArgs = { + data: { + event: testEvent?._id, + status: "invited", + userId: testUser1?._id, + }, + }; + + const context = { + userId: tUser?._id, + }; + + const mem = await createVolunteerMembership?.({}, args, context); + expect(mem?.event).toEqual(testEvent?._id); + }); +}); diff --git a/tests/resolvers/Mutation/removeEventVolunteer.spec.ts b/tests/resolvers/Mutation/removeEventVolunteer.spec.ts index bc6aa6d891..30c8bb4142 100644 --- a/tests/resolvers/Mutation/removeEventVolunteer.spec.ts +++ b/tests/resolvers/Mutation/removeEventVolunteer.spec.ts @@ -25,6 +25,7 @@ import { createTestEvent } from "../../helpers/events"; import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; import { createTestUser } from "../../helpers/user"; import type { TestEventVolunteerGroupType } from "./createEventVolunteer.spec"; +import { removeEventVolunteer } from "../../../src/resolvers/Mutation/removeEventVolunteer"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; @@ -35,23 +36,27 @@ let testGroup: TestEventVolunteerGroupType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); testUser = await createTestUser(); [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", }); testEventVolunteer = await EventVolunteer.create({ - creatorId: eventAdminUser?._id, - userId: testUser?._id, - eventId: testEvent?._id, - groupId: testGroup._id, - isInvited: true, - isAssigned: false, + creator: eventAdminUser?._id, + user: testUser?._id, + event: testEvent?._id, + groups: [testGroup?._id], + hasAccepted: false, + isPublic: false, }); }); @@ -64,13 +69,33 @@ describe("resolvers -> Mutation -> removeEventVolunteer", () => { vi.doUnmock("../../../src/constants"); vi.resetModules(); }); - it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); + it(`removes event volunteer with _id === args.id and returns it`, async () => { + const args: MutationUpdateEventVolunteerArgs = { + id: testEventVolunteer?._id, + }; + + const context = { userId: eventAdminUser?._id }; + + const deletedVolunteer = await removeEventVolunteer?.({}, args, context); + + const updatedGroup = await EventVolunteerGroup.findOne({ + _id: testGroup?._id, + }); + + expect(updatedGroup?.volunteers.toString()).toEqual(""); + expect(deletedVolunteer).toEqual( + expect.objectContaining({ + _id: testEventVolunteer?._id, + user: testEventVolunteer?.user, + hasAccepted: testEventVolunteer?.hasAccepted, + isPublic: testEventVolunteer?.isPublic, + }), + ); + }); + + it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { try { const args: MutationUpdateEventVolunteerArgs = { id: testEventVolunteer?._id, @@ -78,25 +103,15 @@ describe("resolvers -> Mutation -> removeEventVolunteer", () => { const context = { userId: new Types.ObjectId().toString() }; - const { removeEventVolunteer: removeEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteer"); - - await removeEventVolunteerResolver?.({}, args, context); + await removeEventVolunteer?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, + `${USER_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws NotFoundError if no event volunteer exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: new Types.ObjectId().toString(), @@ -104,76 +119,35 @@ describe("resolvers -> Mutation -> removeEventVolunteer", () => { const context = { userId: testUser?._id }; - const { removeEventVolunteer: removeEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteer"); - - await removeEventVolunteerResolver?.({}, args, context); + await removeEventVolunteer?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( - EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, - ); expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE}`, + `${EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws UnauthorizedError if current user is not leader of group`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { + const newVolunteer = await EventVolunteer.create({ + creator: eventAdminUser?._id, + user: testUser?._id, + event: testEvent?._id, + groups: [testGroup?._id], + hasAccepted: false, + isPublic: false, + }); const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, + id: newVolunteer?._id.toString(), }; const context = { userId: testUser?._id }; - const { removeEventVolunteer: removeEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteer"); - - await removeEventVolunteerResolver?.({}, args, context); + await removeEventVolunteer?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + `${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, ); } }); - - it(`removes event volunteer with _id === args.id and returns it`, async () => { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - }; - - const context = { userId: eventAdminUser?._id }; - const { removeEventVolunteer: removeEventVolunteerResolver } = await import( - "../../../src/resolvers/Mutation/removeEventVolunteer" - ); - - const deletedVolunteer = await removeEventVolunteerResolver?.( - {}, - args, - context, - ); - - const updatedGroup = await EventVolunteerGroup.findOne({ - _id: testGroup?._id, - }); - - expect(updatedGroup?.volunteers.toString()).toEqual(""); - - expect(deletedVolunteer).toEqual( - expect.objectContaining({ - _id: testEventVolunteer?._id, - userId: testEventVolunteer?.userId, - isInvited: testEventVolunteer?.isInvited, - isAssigned: testEventVolunteer?.isAssigned, - response: testEventVolunteer?.response, - }), - ); - }); }); diff --git a/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts b/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts index f29ca33ecd..784afb8394 100644 --- a/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts +++ b/tests/resolvers/Mutation/removeEventVolunteerGroup.spec.ts @@ -6,6 +6,7 @@ import { USER_NOT_FOUND_ERROR, EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, } from "../../../src/constants"; import { beforeAll, @@ -22,6 +23,7 @@ import { createTestEvent } from "../../helpers/events"; import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; import { createTestUser } from "../../helpers/user"; import type { TestEventVolunteerGroupType } from "./createEventVolunteer.spec"; +import { removeEventVolunteerGroup } from "../../../src/resolvers/Mutation/removeEventVolunteerGroup"; let MONGOOSE_INSTANCE: typeof mongoose; let testUser: TestUserType; @@ -31,23 +33,27 @@ let testGroup: TestEventVolunteerGroupType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); + const { requestContext } = await import("../../../src/libraries"); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); testUser = await createTestUser(); [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", }); await EventVolunteer.create({ - creatorId: eventAdminUser?._id, - userId: testUser?._id, - eventId: testEvent?._id, - groupId: testGroup._id, - isInvited: true, - isAssigned: false, + creator: eventAdminUser?._id, + user: testUser?._id, + event: testEvent?._id, + groups: [testGroup._id], + hasAccepted: false, + isPublic: false, }); }); @@ -61,12 +67,6 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { vi.resetModules(); }); it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: testGroup?._id, @@ -74,27 +74,20 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { const context = { userId: new Types.ObjectId().toString() }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = + const { removeEventVolunteerGroup: removeEventVolunteerGroup } = await import( "../../../src/resolvers/Mutation/removeEventVolunteerGroup" ); - await removeEventVolunteerGroupResolver?.({}, args, context); + await removeEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, + `${USER_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws NotFoundError if no event volunteer group exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: new Types.ObjectId().toString(), @@ -102,29 +95,15 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { const context = { userId: testUser?._id }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/removeEventVolunteerGroup" - ); - - await removeEventVolunteerGroupResolver?.({}, args, context); + await removeEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( - EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, - ); expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE}`, + `${EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE}`, ); } }); it(`throws UnauthorizedError if current user is not an event admin`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - try { const args: MutationUpdateEventVolunteerArgs = { id: testGroup?._id, @@ -132,16 +111,10 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { const context = { userId: testUser?._id }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/removeEventVolunteerGroup" - ); - - await removeEventVolunteerGroupResolver?.({}, args, context); + await removeEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, + `${USER_NOT_AUTHORIZED_ERROR.MESSAGE}`, ); } }); @@ -152,10 +125,8 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { }; const context = { userId: eventAdminUser?._id }; - const { removeEventVolunteerGroup: removeEventVolunteerGroupResolver } = - await import("../../../src/resolvers/Mutation/removeEventVolunteerGroup"); - const deletedVolunteerGroup = await removeEventVolunteerGroupResolver?.( + const deletedVolunteerGroup = await removeEventVolunteerGroup?.( {}, args, context, @@ -164,11 +135,34 @@ describe("resolvers -> Mutation -> removeEventVolunteerGroup", () => { expect(deletedVolunteerGroup).toEqual( expect.objectContaining({ _id: testGroup?._id, - leaderId: testGroup?.leaderId, + leader: testGroup?.leader, name: testGroup?.name, - creatorId: testGroup?.creatorId, - eventId: testGroup?.eventId, + creator: testGroup?.creator, + event: testGroup?.event, }), ); }); + + it(`throws NotFoundError if volunteerGroup.event doesn't exist`, async () => { + try { + const newGrp = await EventVolunteerGroup.create({ + creator: eventAdminUser?._id, + event: new Types.ObjectId(), + leader: eventAdminUser?._id, + name: "Test group", + }); + + const args: MutationUpdateEventVolunteerArgs = { + id: newGrp?._id.toString(), + }; + + const context = { userId: eventAdminUser?._id }; + + await removeEventVolunteerGroup?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + `${EVENT_NOT_FOUND_ERROR.MESSAGE}`, + ); + } + }); }); diff --git a/tests/resolvers/Mutation/removeOrganization.spec.ts b/tests/resolvers/Mutation/removeOrganization.spec.ts index f2dfad353c..1555db8e61 100644 --- a/tests/resolvers/Mutation/removeOrganization.spec.ts +++ b/tests/resolvers/Mutation/removeOrganization.spec.ts @@ -146,6 +146,7 @@ beforeAll(async () => { creator: testUsers[0]?._id, assignee: testUsers[1]?._id, assigner: testUsers[0]?._id, + assigneeType: "EventVolunteer", actionItemCategory: testCategory?._id, organization: testOrganization?._id, }); diff --git a/tests/resolvers/Mutation/updateActionItem.spec.ts b/tests/resolvers/Mutation/updateActionItem.spec.ts index b7406cd6fa..fc6c8eb5cd 100644 --- a/tests/resolvers/Mutation/updateActionItem.spec.ts +++ b/tests/resolvers/Mutation/updateActionItem.spec.ts @@ -5,9 +5,10 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { ACTION_ITEM_NOT_FOUND_ERROR, EVENT_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, USER_NOT_FOUND_ERROR, - USER_NOT_MEMBER_FOR_ORGANIZATION, } from "../../../src/constants"; import { updateActionItem as updateActionItemResolver } from "../../../src/resolvers/Mutation/updateActionItem"; import type { MutationUpdateActionItemArgs } from "../../../src/types/generatedGraphQLTypes"; @@ -16,26 +17,28 @@ import type { TestOrganizationType, TestUserType, } from "../../helpers/userAndOrg"; -import { - createTestUser, - createTestUserAndOrganization, -} from "../../helpers/userAndOrg"; +import { createTestUserAndOrganization } from "../../helpers/userAndOrg"; import { nanoid } from "nanoid"; -import { ActionItem, AppUserProfile, Event, User } from "../../../src/models"; +import { ActionItem, AppUserProfile, Event } from "../../../src/models"; import type { TestActionItemType } from "../../helpers/actionItem"; import { createTestActionItem } from "../../helpers/actionItem"; import type { TestActionItemCategoryType } from "../../helpers/actionItemCategory"; import type { TestEventType } from "../../helpers/events"; +import type { + TestVolunteerGroupType, + TestVolunteerType, +} from "../../helpers/volunteers"; +import { createTestVolunteerAndGroup } from "../../helpers/volunteers"; -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 tVolunteer: TestVolunteerType; +let tVolunteerGroup: TestVolunteerGroupType; let MONGOOSE_INSTANCE: typeof mongoose; beforeAll(async () => { @@ -45,10 +48,8 @@ beforeAll(async () => { (message) => message, ); - randomUser = await createTestUser(); - [testUser2] = await createTestUserAndOrganization(); - [testUser, testOrganization, testCategory, testActionItem, assignedTestUser] = + [testUser, testOrganization, testCategory, testActionItem] = await createTestActionItem(); testEvent = await Event.create({ @@ -63,6 +64,8 @@ beforeAll(async () => { admins: [testUser2?._id], organization: testOrganization?._id, }); + + [, , , tVolunteer, tVolunteerGroup] = await createTestVolunteerAndGroup(); }); afterAll(async () => { @@ -75,7 +78,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { const args: MutationUpdateActionItemArgs = { id: new Types.ObjectId().toString(), data: { - assigneeId: randomUser?._id, + assigneeId: tVolunteer?._id, }, }; @@ -94,7 +97,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { const args: MutationUpdateActionItemArgs = { id: new Types.ObjectId().toString(), data: { - assigneeId: randomUser?._id, + assigneeId: tVolunteer?._id, }, }; @@ -116,6 +119,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { id: testActionItem?._id, data: { assigneeId: new Types.ObjectId().toString(), + assigneeType: "EventVolunteer", }, }; @@ -125,16 +129,31 @@ describe("resolvers -> Mutation -> updateActionItem", () => { await updateActionItemResolver?.({}, args, context); } catch (error: unknown) { - expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, + ); } }); - it(`throws NotFoundError if the new asignee is not a member of the organization`, async () => { + it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + }); + const args: MutationUpdateActionItemArgs = { - id: testActionItem?._id, + id: testActionItem2?._id.toString() ?? "", data: { - assigneeId: randomUser?._id, + assigneeId: new Types.ObjectId().toString(), + assigneeType: "EventVolunteerGroup", }, }; @@ -145,17 +164,83 @@ describe("resolvers -> Mutation -> updateActionItem", () => { await updateActionItemResolver?.({}, args, context); } catch (error: unknown) { expect((error as Error).message).toEqual( - USER_NOT_MEMBER_FOR_ORGANIZATION.MESSAGE, + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, ); } }); + it(`throws NotFoundError if no user exists with _id === args.data.assigneeId`, async () => { + try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: new Types.ObjectId().toString(), + assigneeType: "EventVolunteerGroup", + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, + ); + } + }); + it(`throws NotFoundError if no user exists when assigneeUser (doesn't exist)`, async () => { + try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "User", + assigneeUser: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: null, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: new Types.ObjectId().toString(), + assigneeType: "User", + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.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, + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", }, }; @@ -171,18 +256,280 @@ describe("resolvers -> Mutation -> updateActionItem", () => { } }); - it(`updates the action item and returns it as an admin`, async () => { + it(`throws NotAuthorizedError if the actionItem.event doesn't exist`, async () => { + try { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + event: new Types.ObjectId().toString(), + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: new Types.ObjectId().toString(), + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + }, + }; + + const context = { + userId: testUser2?._id, + }; + + await updateActionItemResolver?.({}, args, context); + } catch (error: unknown) { + expect((error as Error).message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it(`updates the action item and sets action item as completed`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: false, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 0, + isCompleted: false, + }); + const args: MutationUpdateActionItemArgs = { - id: testActionItem?._id, + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: true, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: true, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the action item and sets action item as not completed`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: true, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteer", + assignee: tVolunteer?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + isCompleted: true, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: false, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", + data: { + assigneeId: tVolunteer?._id, + assigneeType: "EventVolunteer", + isCompleted: false, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the action item and sets action item as completed (Volunteer Group)`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: false, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 0, + isCompleted: false, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: true, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", data: { - assigneeId: assignedTestUser?._id, + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: true, }, }; - // console.log(testUser?._id); + const context = { userId: testUser?._id, }; + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the action item and sets action item as not completed (Volunteer Group)`, async () => { + const testActionItem2 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + allottedHours: 2, + isCompleted: true, + }); + + const testActionItem3 = await ActionItem.create({ + title: `title${nanoid().toLowerCase()}`, + description: `description${nanoid().toLowerCase()}`, + creator: testUser?._id, + assigneeType: "EventVolunteerGroup", + assigneeGroup: tVolunteerGroup?._id, + organization: testOrganization?._id, + assigner: testUser?._id, + actionItemCategory: testCategory?._id, + event: testEvent?._id, + isCompleted: true, + }); + + const args: MutationUpdateActionItemArgs = { + id: testActionItem2?._id.toString() ?? "", + data: { + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: false, + }, + }; + + const args2: MutationUpdateActionItemArgs = { + id: testActionItem3?._id.toString() ?? "", + data: { + assigneeId: tVolunteerGroup?._id, + assigneeType: "EventVolunteerGroup", + isCompleted: false, + }, + }; + + const context = { + userId: testUser?._id, + }; + + await updateActionItemResolver?.({}, args, context); + await updateActionItemResolver?.({}, args2, context); + }); + + it(`updates the actionItem when the user is authorized as an eventAdmin`, async () => { + const updatedTestActionItem = await ActionItem.findOneAndUpdate( + { + _id: testActionItem?._id, + }, + { + event: testEvent?._id, + }, + { + new: true, + }, + ); + + const args: MutationUpdateActionItemArgs = { + data: { + isCompleted: true, + }, + id: updatedTestActionItem?._id.toString() ?? "", + }; + + const context = { + userId: testUser2?._id, + }; + const updatedActionItemPayload = await updateActionItemResolver?.( {}, args, @@ -191,19 +538,19 @@ describe("resolvers -> Mutation -> updateActionItem", () => { expect(updatedActionItemPayload).toEqual( expect.objectContaining({ - assignee: assignedTestUser?._id, actionItemCategory: testCategory?._id, + isCompleted: true, }), ); }); - it(`updates the action item and returns it as superadmin`, async () => { - const superAdminTestUser = await AppUserProfile.findOneAndUpdate( + it(`updates the actionItem isCompleted is undefined (EventVolunteer)`, async () => { + const updatedTestActionItem = await ActionItem.findOneAndUpdate( { - userId: randomUser?._id, + _id: testActionItem?._id, }, { - isSuperAdmin: true, + event: testEvent?._id, }, { new: true, @@ -211,14 +558,16 @@ describe("resolvers -> Mutation -> updateActionItem", () => { ); const args: MutationUpdateActionItemArgs = { - id: testActionItem?._id, data: { - assigneeId: testUser?._id, + isCompleted: undefined, + assigneeId: undefined, + assigneeType: "EventVolunteer", }, + id: updatedTestActionItem?._id.toString() ?? "", }; const context = { - userId: superAdminTestUser?.userId, + userId: testUser2?._id, }; const updatedActionItemPayload = await updateActionItemResolver?.( @@ -229,53 +578,53 @@ describe("resolvers -> Mutation -> updateActionItem", () => { expect(updatedActionItemPayload).toEqual( expect.objectContaining({ - assignee: testUser?._id, actionItemCategory: testCategory?._id, + isCompleted: true, }), ); }); - it(`throws NotFoundError if no event exists to which the action item is associated`, async () => { + it(`updates the actionItem isCompleted is undefined (EventVolunteerGroup)`, async () => { const updatedTestActionItem = await ActionItem.findOneAndUpdate( { _id: testActionItem?._id, }, { - event: new Types.ObjectId().toString(), + event: testEvent?._id, }, { new: true, }, ); - await User.updateOne( - { - _id: randomUser?._id, - }, - { - $push: { joinedOrganizations: testOrganization?._id }, + const args: MutationUpdateActionItemArgs = { + data: { + isCompleted: undefined, + assigneeId: undefined, + assigneeType: "EventVolunteerGroup", }, - ); + id: updatedTestActionItem?._id.toString() ?? "", + }; - try { - const args: MutationUpdateActionItemArgs = { - id: updatedTestActionItem?._id.toString() ?? "", - data: { - assigneeId: randomUser?._id, - }, - }; + const context = { + userId: testUser2?._id, + }; - const context = { - userId: testUser?._id, - }; + const updatedActionItemPayload = await updateActionItemResolver?.( + {}, + args, + context, + ); - await updateActionItemResolver?.({}, args, context); - } catch (error: unknown) { - expect((error as Error).message).toEqual(EVENT_NOT_FOUND_ERROR.MESSAGE); - } + expect(updatedActionItemPayload).toEqual( + expect.objectContaining({ + actionItemCategory: testCategory?._id, + isCompleted: true, + }), + ); }); - it(`updates the actionItem when the user is authorized as an eventAdmin`, async () => { + it(`updates the actionItem isCompleted is undefined (User)`, async () => { const updatedTestActionItem = await ActionItem.findOneAndUpdate( { _id: testActionItem?._id, @@ -290,7 +639,9 @@ describe("resolvers -> Mutation -> updateActionItem", () => { const args: MutationUpdateActionItemArgs = { data: { - isCompleted: true, + isCompleted: undefined, + assigneeId: undefined, + assigneeType: "User", }, id: updatedTestActionItem?._id.toString() ?? "", }; @@ -312,6 +663,7 @@ describe("resolvers -> Mutation -> updateActionItem", () => { }), ); }); + it("throws error if user does not have appUserProfile", async () => { await AppUserProfile.deleteOne({ userId: testUser2?._id, diff --git a/tests/resolvers/Mutation/updateEventVolunteer.spec.ts b/tests/resolvers/Mutation/updateEventVolunteer.spec.ts index 4a749cca91..86ab7b1521 100644 --- a/tests/resolvers/Mutation/updateEventVolunteer.spec.ts +++ b/tests/resolvers/Mutation/updateEventVolunteer.spec.ts @@ -1,38 +1,29 @@ import type mongoose from "mongoose"; -import { Types } from "mongoose"; -import type { MutationUpdateEventVolunteerArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; -import { - USER_NOT_FOUND_ERROR, - EventVolunteerResponse, - EVENT_VOLUNTEER_NOT_FOUND_ERROR, - EVENT_VOLUNTEER_INVITE_USER_MISTMATCH, -} from "../../../src/constants"; -import { - beforeAll, - afterAll, - describe, - it, - expect, - vi, - afterEach, -} from "vitest"; -import type { - TestEventType, - TestEventVolunteerType, -} from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import { createTestUser } from "../../helpers/user"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { TestEventVolunteerType } from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceEventVolunteer } from "../../../src/models"; +import { updateEventVolunteer } from "../../../src/resolvers/Mutation/updateEventVolunteer"; +import { EVENT_VOLUNTEER_INVITE_USER_MISTMATCH } from "../../../src/constants"; +import { requestContext } from "../../../src/libraries"; let MONGOOSE_INSTANCE: typeof mongoose; -let testEvent: TestEventType; -let testEventVolunteer: TestEventVolunteerType; +let testUser1: TestUserType; +let testUser2: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - const temp = await createTestEventAndVolunteer(); - testEvent = temp[2]; - testEventVolunteer = temp[3]; + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); + const [, , user1, user2, volunteer1] = await createVolunteerAndActions(); + + testUser1 = user1; + testUser2 = user2; + testEventVolunteer1 = volunteer1; }); afterAll(async () => { @@ -40,161 +31,55 @@ afterAll(async () => { }); describe("resolvers -> Mutation -> updateEventVolunteer", () => { - afterEach(() => { - vi.doUnmock("../../../src/constants"); - vi.resetModules(); - }); - it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - + it(`throws error if context.userId !== volunteer._id`, async () => { try { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: { - response: EventVolunteerResponse.YES, + (await updateEventVolunteer?.( + {}, + { + id: testEventVolunteer1?._id, + data: { + isPublic: false, + }, }, - }; - - const context = { userId: new Types.ObjectId().toString() }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteer"); - - await updateEventVolunteerResolver?.({}, args, context); + { userId: testUser2?._id.toString() }, + )) as unknown as InterfaceEventVolunteer[]; } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( - `Translated ${USER_NOT_FOUND_ERROR.MESSAGE}`, - ); - } - }); - - it(`throws NotFoundError if no event volunteer exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - - try { - const args: MutationUpdateEventVolunteerArgs = { - id: new Types.ObjectId().toString(), - data: { - response: EventVolunteerResponse.YES, - }, - }; - - const context = { userId: testEventVolunteer?.userId }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteer"); - - await updateEventVolunteerResolver?.({}, args, context); - } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( - EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, - ); - expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE}`, - ); - } - }); - - it(`throws ConflictError if userId of volunteer is not equal to context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - - const spy = vi - .spyOn(requestContext, "translate") - .mockImplementationOnce((message) => `Translated ${message}`); - - try { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: { - response: EventVolunteerResponse.YES, - }, - }; - - const testUser2 = await createTestUser(); - const context = { userId: testUser2?._id }; - const { updateEventVolunteer: updateEventVolunteerResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteer"); - - await updateEventVolunteerResolver?.({}, args, context); - } catch (error: unknown) { - expect(spy).toHaveBeenLastCalledWith( EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.MESSAGE, ); - expect((error as Error).message).toEqual( - `Translated ${EVENT_VOLUNTEER_INVITE_USER_MISTMATCH.MESSAGE}`, - ); } }); - it(`updates the Event Volunteer with _id === args.id and returns it`, async () => { - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: { - isAssigned: true, - response: EventVolunteerResponse.YES, - isInvited: true, - eventId: testEvent?._id, - }, - }; - - const context = { userId: testEventVolunteer?.userId }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = await import( - "../../../src/resolvers/Mutation/updateEventVolunteer" - ); - - const updatedEventVolunteer = await updateEventVolunteerResolver?.( + it(`data remains same if no values are updated`, async () => { + const updatedVolunteer = (await updateEventVolunteer?.( {}, - args, - context, - ); - - expect(updatedEventVolunteer).toEqual( - expect.objectContaining({ - isAssigned: true, - response: EventVolunteerResponse.YES, - eventId: testEvent?._id, - isInvited: true, - }), + { + id: testEventVolunteer1?._id, + data: {}, + }, + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceEventVolunteer; + expect(updatedVolunteer.isPublic).toEqual(testEventVolunteer1?.isPublic); + expect(updatedVolunteer.hasAccepted).toEqual( + testEventVolunteer1?.hasAccepted, ); }); - it(`updates the Event Volunteer with _id === args.id, even if args.data is empty object`, async () => { - const t = await createTestEventAndVolunteer(); - testEventVolunteer = t[3]; - const args: MutationUpdateEventVolunteerArgs = { - id: testEventVolunteer?._id, - data: {}, - }; - - const context = { userId: testEventVolunteer?.userId }; - - const { updateEventVolunteer: updateEventVolunteerResolver } = await import( - "../../../src/resolvers/Mutation/updateEventVolunteer" - ); - - const updatedEventVolunteer = await updateEventVolunteerResolver?.( + it(`updates EventVolunteer`, async () => { + const updatedVolunteer = (await updateEventVolunteer?.( {}, - args, - context, - ); - - expect(updatedEventVolunteer).toEqual( - expect.objectContaining({ - isAssigned: testEventVolunteer?.isAssigned, - response: testEventVolunteer?.response, - eventId: testEventVolunteer?.eventId, - isInvited: testEventVolunteer?.isInvited, - }), - ); + { + id: testEventVolunteer1?._id, + data: { + isPublic: false, + hasAccepted: false, + assignments: [], + }, + }, + { userId: testUser1?._id.toString() }, + )) as unknown as InterfaceEventVolunteer; + expect(updatedVolunteer.isPublic).toEqual(false); + expect(updatedVolunteer.hasAccepted).toEqual(false); + expect(updatedVolunteer.assignments).toEqual([]); }); }); diff --git a/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts b/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts index 645161bec6..c735688e63 100644 --- a/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts +++ b/tests/resolvers/Mutation/updateEventVolunteerGroup.spec.ts @@ -1,14 +1,12 @@ import type mongoose from "mongoose"; import { Types } from "mongoose"; -import type { - MutationUpdateEventVolunteerArgs, - MutationUpdateEventVolunteerGroupArgs, -} from "../../../src/types/generatedGraphQLTypes"; +import type { MutationUpdateEventVolunteerGroupArgs } from "../../../src/types/generatedGraphQLTypes"; import { connect, disconnect } from "../../helpers/db"; import { USER_NOT_FOUND_ERROR, EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, USER_NOT_AUTHORIZED_ERROR, + EVENT_NOT_FOUND_ERROR, } from "../../../src/constants"; import { beforeAll, @@ -25,6 +23,8 @@ import { createTestUser } from "../../helpers/user"; import type { TestUserType } from "../../helpers/userAndOrg"; import type { TestEventVolunteerGroupType } from "./createEventVolunteer.spec"; import { EventVolunteerGroup } from "../../../src/models"; +import { requestContext } from "../../../src/libraries"; +import { updateEventVolunteerGroup } from "../../../src/resolvers/Mutation/updateEventVolunteerGroup"; let MONGOOSE_INSTANCE: typeof mongoose; let testEvent: TestEventType; @@ -35,9 +35,9 @@ beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); [eventAdminUser, , testEvent] = await createTestEvent(); testGroup = await EventVolunteerGroup.create({ - creatorId: eventAdminUser?._id, - eventId: testEvent?._id, - leaderId: eventAdminUser?._id, + creator: eventAdminUser?._id, + event: testEvent?._id, + leader: eventAdminUser?._id, name: "Test group", volunteersRequired: 2, }); @@ -54,8 +54,6 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { }); it(`throws NotFoundError if no user exists with _id === context.userId `, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); @@ -65,17 +63,12 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { id: testGroup?._id, data: { name: "updated name", + eventId: testEvent?._id, }, }; const context = { userId: new Types.ObjectId().toString() }; - - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/updateEventVolunteerGroup" - ); - - await updateEventVolunteerGroupResolver?.({}, args, context); + await updateEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { expect(spy).toHaveBeenLastCalledWith(USER_NOT_FOUND_ERROR.MESSAGE); expect((error as Error).message).toEqual( @@ -85,8 +78,6 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { }); it(`throws NotFoundError if no event volunteer group exists with _id === args.id`, async () => { - const { requestContext } = await import("../../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); @@ -96,17 +87,12 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { id: new Types.ObjectId().toString(), data: { name: "updated name", + eventId: testEvent?._id, }, }; const context = { userId: eventAdminUser?._id }; - - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/updateEventVolunteerGroup" - ); - - await updateEventVolunteerGroupResolver?.({}, args, context); + await updateEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { expect(spy).toHaveBeenLastCalledWith( EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, @@ -117,9 +103,31 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { } }); - it(`throws UnauthorizedError if current user is not leader of group `, async () => { - const { requestContext } = await import("../../../src/libraries"); + it(`throws NotFoundError if no event exists with _id === args.data.eventId`, async () => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + try { + const args: MutationUpdateEventVolunteerGroupArgs = { + id: testGroup?._id, + data: { + name: "updated name", + eventId: new Types.ObjectId().toString(), + }, + }; + + const context = { userId: eventAdminUser?._id }; + await updateEventVolunteerGroup?.({}, args, context); + } catch (error: unknown) { + expect(spy).toHaveBeenLastCalledWith(EVENT_NOT_FOUND_ERROR.MESSAGE); + expect((error as Error).message).toEqual( + `Translated ${EVENT_NOT_FOUND_ERROR.MESSAGE}`, + ); + } + }); + + it(`throws UnauthorizedError if current user is not leader of group `, async () => { const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); @@ -129,18 +137,14 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { id: testGroup?._id, data: { name: "updated name", + eventId: testEvent?._id, }, }; const testUser2 = await createTestUser(); const context = { userId: testUser2?._id }; - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import( - "../../../src/resolvers/Mutation/updateEventVolunteerGroup" - ); - - await updateEventVolunteerGroupResolver?.({}, args, context); + await updateEventVolunteerGroup?.({}, args, context); } catch (error: unknown) { expect(spy).toHaveBeenLastCalledWith(USER_NOT_AUTHORIZED_ERROR.MESSAGE); expect((error as Error).message).toEqual( @@ -160,20 +164,12 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { }; const context = { userId: eventAdminUser?._id }; - - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteerGroup"); - - const updatedGroup = await updateEventVolunteerGroupResolver?.( - {}, - args, - context, - ); + const updatedGroup = await updateEventVolunteerGroup?.({}, args, context); expect(updatedGroup).toEqual( expect.objectContaining({ name: "updated", - eventId: testEvent?._id, + event: testEvent?._id, volunteersRequired: 10, }), ); @@ -182,35 +178,26 @@ describe("resolvers -> Mutation -> updateEventVolunteerGroup", () => { it(`updates the Event Volunteer group with _id === args.id, even if args.data is empty object`, async () => { const testGroup2 = await EventVolunteerGroup.create({ name: "test", - eventId: testEvent?._id, - creatorId: eventAdminUser?._id, + event: testEvent?._id, + creator: eventAdminUser?._id, volunteersRequired: 2, - leaderId: eventAdminUser?._id, + leader: eventAdminUser?._id, }); - const args: MutationUpdateEventVolunteerArgs = { + const args: MutationUpdateEventVolunteerGroupArgs = { id: testGroup2?._id.toString(), - data: {}, + data: { + eventId: testEvent?._id, + }, }; const context = { userId: eventAdminUser?._id }; + const updatedGroup = await updateEventVolunteerGroup?.({}, args, context); - const { updateEventVolunteerGroup: updateEventVolunteerGroupResolver } = - await import("../../../src/resolvers/Mutation/updateEventVolunteerGroup"); - - const updatedGroup = await updateEventVolunteerGroupResolver?.( - {}, - args, - context, - ); - - console.log(updatedGroup); - - console.log(); expect(updatedGroup).toEqual( expect.objectContaining({ name: testGroup2?.name, volunteersRequired: testGroup2?.volunteersRequired, - eventId: testGroup2?.eventId, + event: testGroup2?.event, }), ); }); diff --git a/tests/resolvers/Mutation/updateVolunteerMembership.spec.ts b/tests/resolvers/Mutation/updateVolunteerMembership.spec.ts new file mode 100644 index 0000000000..348998e37e --- /dev/null +++ b/tests/resolvers/Mutation/updateVolunteerMembership.spec.ts @@ -0,0 +1,175 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect, vi } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import { createTestUser, type TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import { VolunteerMembership } from "../../../src/models"; +import { updateVolunteerMembership } from "../../../src/resolvers/Mutation/updateVolunteerMembership"; +import { Types } from "mongoose"; +import { + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../../src/constants"; +import { requestContext } from "../../../src/libraries"; +import { MembershipStatus } from "../Query/getVolunteerMembership.spec"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testEventVolunteerGroup: TestEventVolunteerGroupType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + vi.spyOn(requestContext, "translate").mockImplementation( + (message) => message, + ); + const [, event, user1, , volunteer1, , volunteerGroup, ,] = + await createVolunteerAndActions(); + + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testEventVolunteerGroup = volunteerGroup; + + await VolunteerMembership.insertMany([ + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.INVITED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + group: testEventVolunteerGroup._id, + status: MembershipStatus.REQUESTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.ACCEPTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.REJECTED, + }, + ]); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Mutation -> updateVolunteerMembership", () => { + it("throws NotFoundError if current User does not exist", async () => { + try { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.REQUESTED, + group: testEventVolunteerGroup._id, + volunteer: testEventVolunteer1?._id, + }); + + await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() ?? "", + status: MembershipStatus.ACCEPTED, + }, + { userId: new Types.ObjectId().toString() }, + ); + } catch (error: unknown) { + expect((error as Error).message).toEqual(USER_NOT_FOUND_ERROR.MESSAGE); + } + }); + + it("throws NotFoundError if VolunteerMembership does not exist", async () => { + try { + await VolunteerMembership.findOne({ + status: MembershipStatus.REQUESTED, + group: testEventVolunteerGroup._id, + volunteer: testEventVolunteer1?._id, + }); + + await updateVolunteerMembership?.( + {}, + { + id: new Types.ObjectId().toString() ?? "", + status: MembershipStatus.ACCEPTED, + }, + { userId: testUser1?._id }, + ); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.MESSAGE, + ); + } + }); + + it("throws UnauthorizedUser Error", async () => { + try { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.REJECTED, + volunteer: testEventVolunteer1?._id, + }); + + const randomUser = await createTestUser(); + + await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() as string, + status: MembershipStatus.ACCEPTED, + }, + { userId: randomUser?._id.toString() as string }, + ); + } catch (error: unknown) { + expect((error as Error).message).toEqual( + USER_NOT_AUTHORIZED_ERROR.MESSAGE, + ); + } + }); + + it(`updateVolunteerMembership (with group) - set to accepted `, async () => { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.INVITED, + volunteer: testEventVolunteer1?._id, + }); + + const updatedMembership = await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() ?? "", + status: MembershipStatus.REJECTED, + }, + { userId: testUser1?._id }, + ); + + expect(updatedMembership?.status).toEqual(MembershipStatus.REJECTED); + }); + + it(`updateVolunteerMembership (with group) - set to accepted `, async () => { + const membership = await VolunteerMembership.findOne({ + status: MembershipStatus.REQUESTED, + group: testEventVolunteerGroup._id, + volunteer: testEventVolunteer1?._id, + }); + + const updatedMembership = await updateVolunteerMembership?.( + {}, + { + id: membership?._id.toString() ?? "", + status: MembershipStatus.ACCEPTED, + }, + { userId: testUser1?._id }, + ); + + expect(updatedMembership?.status).toEqual(MembershipStatus.ACCEPTED); + }); +}); diff --git a/tests/resolvers/Query/actionItemsByOrganization.spec.ts b/tests/resolvers/Query/actionItemsByOrganization.spec.ts index 8437ad5cdd..36cca12583 100644 --- a/tests/resolvers/Query/actionItemsByOrganization.spec.ts +++ b/tests/resolvers/Query/actionItemsByOrganization.spec.ts @@ -1,31 +1,46 @@ -import "dotenv/config"; -import type { InterfaceActionItem } from "../../../src/models"; -import { ActionItem, ActionItemCategory } from "../../../src/models"; +import type mongoose from "mongoose"; import { connect, disconnect } from "../../helpers/db"; -import type { - ActionItemWhereInput, - ActionItemsOrderByInput, - 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 { - TestOrganizationType, - TestUserType, -} from "../../helpers/userAndOrg"; import type { TestEventType } from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceActionItem } from "../../../src/models"; +import { ActionItem } from "../../../src/models"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { actionItemsByOrganization } from "../../../src/resolvers/Query/actionItemsByOrganization"; let MONGOOSE_INSTANCE: typeof mongoose; let testOrganization: TestOrganizationType; let testEvent: TestEventType; -let testAssigneeUser: TestUserType; +let testUser1: TestUserType; +let testActionItem1: TestActionItemType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - [, testAssigneeUser, testEvent, testOrganization] = - await createTestActionItems(); + const [organization, event, user1, , , , , actionItem1] = + await createVolunteerAndActions(); + + testOrganization = organization; + testEvent = event; + testUser1 = user1; + testActionItem1 = actionItem1; + + await ActionItem.create({ + creator: testUser1?._id, + assigner: testUser1?._id, + assigneeUser: testUser1?._id, + assigneeType: "User", + assignee: null, + assigneeGroup: null, + actionItemCategory: testActionItem1.actionItemCategory, + event: null, + organization: testOrganization?._id, + allottedHours: 2, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 3000), + isCompleted: false, + }); }); afterAll(async () => { @@ -33,238 +48,37 @@ afterAll(async () => { }); describe("resolvers -> Query -> actionItemsByOrganization", () => { - it(`returns list of all action items associated with an organization in ascending order where eventId is not null`, async () => { - const orderBy: ActionItemsOrderByInput = "createdAt_ASC"; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - orderBy, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), - ); - }); - - it(`returns list of all action items associated with an organization in ascending order where eventId is null`, async () => { - const orderBy: ActionItemsOrderByInput = "createdAt_ASC"; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: null, - orderBy, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), - ); - }); - - it(`returns list of all action items associated with an organization in descending order`, async () => { - const orderBy: ActionItemsOrderByInput = "createdAt_DESC"; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: null, - orderBy, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), + it(`actionItemsByOrganization - organizationId, eventId, assigneeName`, async () => { + const actionItems = (await actionItemsByOrganization?.( + {}, + { + organizationId: testOrganization?._id, + eventId: testEvent?._id, + where: { + categoryName: "Test Action Item Category 1", + assigneeName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[0].assigneeType).toEqual("EventVolunteer"); + expect(actionItems[0].assignee.user.firstName).toEqual( + testUser1?.firstName, ); }); - it(`returns list of all action items associated with an organization and belonging to an action item category`, async () => { - const actionItemCategories = await ActionItemCategory.find({ - organizationId: testOrganization?._id, - }); - - const actionItemCategoriesIds = actionItemCategories.map( - (category) => category._id, - ); - - const actionItemCategoryId = actionItemCategoriesIds[0]; - - const where: ActionItemWhereInput = { - actionItemCategory_id: actionItemCategoryId.toString(), - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - where, - }; - - const actionItemsByOrganizationPayload = - await actionItemsByOrganizationResolver?.({}, args, {}); - - const actionItemsByOrganizationInfo = await ActionItem.find({ - actionItemCategoryId, - }).lean(); - - expect(actionItemsByOrganizationPayload).toEqual( - actionItemsByOrganizationInfo, - ); - }); - it(`returns list of all action items associated with an organization that are active`, async () => { - const where: ActionItemWhereInput = { - is_completed: false, - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[1]._id, - }), - ); - }); - - it(`returns list of all action items associated with an organization that are completed`, async () => { - const where: ActionItemWhereInput = { - is_completed: true, - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0]).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0]._id, - }), - ); - }); - - it(`returns list of all action items matching categoryName Filter`, async () => { - const where: ActionItemWhereInput = { - categoryName: "Default", - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0].actionItemCategory).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0].actionItemCategory, - }), - ); - }); - - it(`returns list of all action items matching assigneeName Filter`, async () => { - const where: ActionItemWhereInput = { - assigneeName: testAssigneeUser?.firstName, - }; - - const args: QueryActionItemsByOrganizationArgs = { - organizationId: testOrganization?._id, - eventId: testEvent?._id, - where, - }; - - const actionItemsByOrganizationPayload = - (await actionItemsByOrganizationResolver?.( - {}, - args, - {}, - )) as InterfaceActionItem[]; - - const actionItemsByOrganizationInfo = await ActionItem.find({ - organization: args.organizationId, - event: args.eventId, - }).lean(); - - expect(actionItemsByOrganizationPayload[0].assignee).toEqual( - expect.objectContaining({ - _id: actionItemsByOrganizationInfo[0].assignee, - }), - ); + it(`actionItemsByOrganization - organizationId, assigneeName`, async () => { + const actionItems = (await actionItemsByOrganization?.( + {}, + { + organizationId: testOrganization?._id, + where: { + assigneeName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[0].assigneeType).toEqual("User"); + expect(actionItems[0].assigneeUser.firstName).toEqual(testUser1?.firstName); }); }); diff --git a/tests/resolvers/Query/actionItemsByUser.spec.ts b/tests/resolvers/Query/actionItemsByUser.spec.ts new file mode 100644 index 0000000000..e5929381b6 --- /dev/null +++ b/tests/resolvers/Query/actionItemsByUser.spec.ts @@ -0,0 +1,98 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { TestUserType } from "../../helpers/user"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceActionItem } from "../../../src/models"; +import { ActionItem } from "../../../src/models"; +import type { TestActionItemType } from "../../helpers/actionItem"; +import { actionItemsByUser } from "../../../src/resolvers/Query/actionItemsByUser"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; +let testUser1: TestUserType; +let testActionItem1: TestActionItemType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [organization, , user1, , , , , actionItem1] = + await createVolunteerAndActions(); + + testOrganization = organization; + testUser1 = user1; + testActionItem1 = actionItem1; + + await ActionItem.create({ + creator: testUser1?._id, + assigner: testUser1?._id, + assigneeUser: testUser1?._id, + assigneeType: "User", + assignee: null, + assigneeGroup: null, + actionItemCategory: testActionItem1.actionItemCategory, + event: null, + organization: testOrganization?._id, + allottedHours: 2, + assignmentDate: new Date(), + dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 3000), + isCompleted: false, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> actionItemsByUser", () => { + it(`actionItemsByUser for userId, categoryName, dueDate_ASC`, async () => { + const actionItems = (await actionItemsByUser?.( + {}, + { + userId: testUser1?._id.toString() ?? "testUserId", + orderBy: "dueDate_ASC", + where: { + categoryName: "Test Action Item Category 1", + orgId: testOrganization?._id.toString(), + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[0].assigneeType).toEqual("EventVolunteer"); + expect(actionItems[1].assigneeType).toEqual("EventVolunteerGroup"); + }); + + it(`actionItemsByUser for userId, assigneeName, dueDate_DESC`, async () => { + const actionItems = (await actionItemsByUser?.( + {}, + { + userId: testUser1?._id.toString() ?? "testUserId", + orderBy: "dueDate_DESC", + where: { + categoryName: "Test Action Item Category 1", + assigneeName: testUser1?.firstName, + orgId: testOrganization?._id.toString(), + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems[1].assignee.user.firstName).toEqual( + testUser1?.firstName, + ); + }); + + it(`actionItemsByUser for userId, assigneeName doesn't match`, async () => { + const actionItems = (await actionItemsByUser?.( + {}, + { + userId: testUser1?._id.toString() ?? "testUserId", + where: { + assigneeName: "xyz", + orgId: testOrganization?._id.toString(), + }, + }, + {}, + )) as unknown as InterfaceActionItem[]; + expect(actionItems.length).toEqual(0); + }); +}); diff --git a/tests/resolvers/Query/eventVolunteersByEvent.spec.ts b/tests/resolvers/Query/eventVolunteersByEvent.spec.ts deleted file mode 100644 index 7c02d96f0e..0000000000 --- a/tests/resolvers/Query/eventVolunteersByEvent.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type mongoose from "mongoose"; -import { connect, disconnect } from "../../helpers/db"; -import { eventVolunteersByEvent } from "../../../src/resolvers/Query/eventVolunteersByEvent"; -import { beforeAll, afterAll, describe, it, expect } from "vitest"; -import type { TestEventType } from "../../helpers/events"; -import { createTestEventAndVolunteer } from "../../helpers/events"; -import { EventVolunteer } from "../../../src/models"; - -let MONGOOSE_INSTANCE: typeof mongoose; -let testEvent: TestEventType; - -beforeAll(async () => { - MONGOOSE_INSTANCE = await connect(); - const temp = await createTestEventAndVolunteer(); - testEvent = temp[2]; -}); - -afterAll(async () => { - await disconnect(MONGOOSE_INSTANCE); -}); - -describe("resolvers -> Mutation -> eventVolunteersByEvent", () => { - it(`returns list of all existing event volunteers with eventId === args.id`, async () => { - const volunteersPayload = await eventVolunteersByEvent?.( - {}, - { - id: testEvent?._id, - }, - {}, - ); - - const volunteers = await EventVolunteer.find({ - eventId: testEvent?._id, - }) - .populate("userId", "-password") - .lean(); - - expect(volunteersPayload).toEqual(volunteers); - }); -}); diff --git a/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts b/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts index 674ad19e97..15d1ce6492 100644 --- a/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts +++ b/tests/resolvers/Query/eventsByOrganizationConnection.spec.ts @@ -40,24 +40,50 @@ beforeAll(async () => { await dropAllCollectionsFromDatabase(MONGOOSE_INSTANCE); [testUser, testOrganization] = await createTestUserAndOrganization(); const testEvent1 = await createEventWithRegistrant( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUser!._id.toString(), + testUser?._id.toString() ?? "defaultUserId", testOrganization?._id, true, ); const testEvent2 = await createEventWithRegistrant( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUser!._id.toString(), + testUser?._id.toString() ?? "defaultUserId", testOrganization?._id, false, ); const testEvent3 = await createEventWithRegistrant( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - testUser!._id.toString(), + testUser?._id.toString() ?? "defaultUserId", testOrganization?._id, false, ); - testEvents = [testEvent1, testEvent2, testEvent3]; + const testEvent4 = await createEventWithRegistrant( + testUser?._id.toString() ?? "defaultUserId", + testOrganization?._id, + false, + ); + const testEvent5 = await createEventWithRegistrant( + testUser?._id.toString() ?? "defaultUserId", + testOrganization?._id, + false, + ); + + if (testEvent4) { + const today = new Date(); + const nextWeek = addDays(today, 7); + testEvent4.startDate = nextWeek.toISOString().split("T")[0]; + testEvent4.endDate = nextWeek.toISOString().split("T")[0]; + await testEvent4.save(); + } + + if (testEvent5) { + // set endDate to today and set endTime to 1 min from now + const today = new Date(); + testEvent5.endDate = today.toISOString().split("T")[0]; + testEvent5.endTime = new Date( + today.setMinutes(today.getMinutes() + 1), + ).toISOString(); + await testEvent5.save(); + } + + testEvents = [testEvent1, testEvent2, testEvent3, testEvent4, testEvent5]; }); afterAll(async () => { @@ -639,4 +665,19 @@ describe("resolvers -> Query -> organizationsMemberConnection", () => { vi.useRealTimers(); }); + + it("fetch upcoming events for the current date", async () => { + const upcomingEvents = (await eventsByOrganizationConnectionResolver?.( + {}, + { + upcomingOnly: true, + where: { + organization_id: testOrganization?._id, + }, + }, + {}, + )) as unknown as InterfaceEvent[]; + expect(upcomingEvents[0]?._id).toEqual(testEvents[3]?._id); + expect(upcomingEvents[1]?._id).toEqual(testEvents[4]?._id); + }); }); diff --git a/tests/resolvers/Query/getEventVolunteerGroups.spec.ts b/tests/resolvers/Query/getEventVolunteerGroups.spec.ts index a52a9a49d4..ab95e31c01 100644 --- a/tests/resolvers/Query/getEventVolunteerGroups.spec.ts +++ b/tests/resolvers/Query/getEventVolunteerGroups.spec.ts @@ -1,23 +1,52 @@ import type mongoose from "mongoose"; import { connect, disconnect } from "../../helpers/db"; -import { getEventVolunteerGroups } from "../../../src/resolvers/Query/getEventVolunteerGroups"; import { beforeAll, afterAll, describe, it, expect } from "vitest"; import type { TestEventType, TestEventVolunteerGroupType, + TestEventVolunteerType, } from "../../helpers/events"; -import { createTestEventVolunteerGroup } from "../../helpers/events"; -import type { EventVolunteerGroup } from "../../../src/types/generatedGraphQLTypes"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceEventVolunteerGroup } from "../../../src/models"; +import { EventVolunteer, EventVolunteerGroup } from "../../../src/models"; +import { getEventVolunteerGroups } from "../../../src/resolvers/Query/getEventVolunteerGroups"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; let testEvent: TestEventType; -let testEventVolunteerGroup: TestEventVolunteerGroupType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testVolunteerGroup1: TestEventVolunteerGroupType; +let testVolunteerGroup2: TestEventVolunteerGroupType; beforeAll(async () => { MONGOOSE_INSTANCE = await connect(); - const temp = await createTestEventVolunteerGroup(); - testEvent = temp[2]; - testEventVolunteerGroup = temp[4]; + const [organization, event, user1, , volunteer1, , volunteerGroup] = + await createVolunteerAndActions(); + + testOrganization = organization; + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testVolunteerGroup1 = volunteerGroup; + testVolunteerGroup2 = await EventVolunteerGroup.create({ + creator: testUser1?._id, + event: testEvent?._id, + volunteers: [testEventVolunteer1?._id], + leader: testUser1?._id, + assignments: [], + name: "Test Volunteer Group 2", + }); + + await EventVolunteer.updateOne( + { _id: testEventVolunteer1?._id }, + { groups: [testVolunteerGroup1?._id, testVolunteerGroup2?._id] }, + { + new: true, + }, + ); }); afterAll(async () => { @@ -25,31 +54,77 @@ afterAll(async () => { }); describe("resolvers -> Query -> getEventVolunteerGroups", () => { - it(`returns list of all existing event volunteer groups with eventId === args.where.eventId`, async () => { - const volunteerGroupsPayload = (await getEventVolunteerGroups?.( + it(`getEventVolunteerGroups - eventId, name_contains, orderBy is volunteers_ASC`, async () => { + const groups = (await getEventVolunteerGroups?.( {}, { where: { eventId: testEvent?._id, + name_contains: testVolunteerGroup1.name, + leaderName: testUser1?.firstName, }, + orderBy: "volunteers_ASC", }, {}, - )) as unknown as EventVolunteerGroup[]; + )) as unknown as InterfaceEventVolunteerGroup[]; + + expect(groups[0].name).toEqual(testVolunteerGroup1.name); + }); + + it(`getEventVolunteerGroups - userId, orgId, orderBy is volunteers_DESC`, async () => { + const groups = (await getEventVolunteerGroups?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + orgId: testOrganization?._id, + }, + orderBy: "volunteers_DESC", + }, + {}, + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups.length).toEqual(2); + }); - expect(volunteerGroupsPayload[0]._id).toEqual(testEventVolunteerGroup._id); + it(`getEventVolunteerGroups - eventId, orderBy is assignments_ASC`, async () => { + const groups = (await getEventVolunteerGroups?.( + {}, + { + where: { + eventId: testEvent?._id, + }, + orderBy: "assignments_ASC", + }, + {}, + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups[0].name).toEqual(testVolunteerGroup2.name); }); - it(`returns empty list of all existing event volunteer groups with eventId !== args.where.eventId`, async () => { - const volunteerGroupsPayload = (await getEventVolunteerGroups?.( + it(`getEventVolunteerGroups - eventId, orderBy is assignements_DESC`, async () => { + const groups = (await getEventVolunteerGroups?.( {}, { where: { - eventId: "123456789012345678901234", + eventId: testEvent?._id, }, + orderBy: "assignments_DESC", }, {}, - )) as unknown as EventVolunteerGroup[]; + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups[0].name).toEqual(testVolunteerGroup1.name); + }); - expect(volunteerGroupsPayload).toEqual([]); + it(`getEventVolunteerGroups - userId, wrong orgId`, async () => { + const groups = (await getEventVolunteerGroups?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + orgId: testEvent?._id, + }, + }, + {}, + )) as unknown as InterfaceEventVolunteerGroup[]; + expect(groups).toEqual([]); }); }); diff --git a/tests/resolvers/Query/getEventVolunteers.spec.ts b/tests/resolvers/Query/getEventVolunteers.spec.ts new file mode 100644 index 0000000000..93c532c0d3 --- /dev/null +++ b/tests/resolvers/Query/getEventVolunteers.spec.ts @@ -0,0 +1,62 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceEventVolunteer } from "../../../src/models"; +import { getEventVolunteers } from "../../../src/resolvers/Query/getEventVolunteers"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testVolunteerGroup: TestEventVolunteerGroupType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [, event, user1, , volunteer1, , volunteerGroup, ,] = + await createVolunteerAndActions(); + + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testVolunteerGroup = volunteerGroup; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> getEventVolunteers", () => { + it(`getEventVolunteers - eventId, name_contains`, async () => { + const eventVolunteers = (await getEventVolunteers?.( + {}, + { + where: { + eventId: testEvent?._id, + name_contains: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceEventVolunteer[]; + expect(eventVolunteers[0].user.firstName).toEqual(testUser1?.firstName); + }); + it(`getEventVolunteers - eventId, groupId`, async () => { + const eventVolunteers = (await getEventVolunteers?.( + {}, + { + where: { + eventId: testEvent?._id, + groupId: testVolunteerGroup?._id, + }, + }, + {}, + )) as unknown as InterfaceEventVolunteer[]; + expect(eventVolunteers[0]._id).toEqual(testEventVolunteer1?._id); + }); +}); diff --git a/tests/resolvers/Query/getVolunteerMembership.spec.ts b/tests/resolvers/Query/getVolunteerMembership.spec.ts new file mode 100644 index 0000000000..ecc68c5d3a --- /dev/null +++ b/tests/resolvers/Query/getVolunteerMembership.spec.ts @@ -0,0 +1,170 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { + TestEventType, + TestEventVolunteerGroupType, + TestEventVolunteerType, +} from "../../helpers/events"; +import type { TestUserType } from "../../helpers/user"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import type { InterfaceVolunteerMembership } from "../../../src/models"; +import { VolunteerMembership } from "../../../src/models"; +import { getVolunteerMembership } from "../../../src/resolvers/Query/getVolunteerMembership"; + +export enum MembershipStatus { + INVITED = "invited", + REQUESTED = "requested", + ACCEPTED = "accepted", + REJECTED = "rejected", +} + +let MONGOOSE_INSTANCE: typeof mongoose; +let testEvent: TestEventType; +let testUser1: TestUserType; +let testEventVolunteer1: TestEventVolunteerType; +let testEventVolunteerGroup: TestEventVolunteerGroupType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [, event, user1, , volunteer1, , volunteerGroup, ,] = + await createVolunteerAndActions(); + + testEvent = event; + testUser1 = user1; + testEventVolunteer1 = volunteer1; + testEventVolunteerGroup = volunteerGroup; + + await VolunteerMembership.insertMany([ + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.INVITED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + group: testEventVolunteerGroup._id, + status: MembershipStatus.REQUESTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.ACCEPTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.REJECTED, + }, + { + event: testEvent?._id, + volunteer: testEventVolunteer1._id, + status: MembershipStatus.INVITED, + group: testEventVolunteerGroup._id, + }, + ]); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> getVolunteerMembership", () => { + it(`getVolunteerMembership for userId, status invited`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + status: MembershipStatus.INVITED, + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].status).toEqual(MembershipStatus.INVITED); + }); + + it(`getVolunteerMembership for eventId, status accepted`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + eventId: testEvent?._id, + eventTitle: testEvent?.title, + status: MembershipStatus.ACCEPTED, + }, + orderBy: "createdAt_ASC", + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].status).toEqual(MembershipStatus.ACCEPTED); + expect(volunteerMemberships[0].event._id).toEqual(testEvent?._id); + }); + + it(`getVolunteerMembership for eventId, filter group, userName`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + eventId: testEvent?._id, + filter: "group", + userName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].status).toEqual(MembershipStatus.REQUESTED); + expect(volunteerMemberships[0].event._id).toEqual(testEvent?._id); + expect(volunteerMemberships[0].group?._id).toEqual( + testEventVolunteerGroup?._id, + ); + }); + + it(`getVolunteerMembership for userId`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + userId: testUser1?._id.toString(), + filter: "individual", + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships.length).toEqual(3); + expect(volunteerMemberships[0].group).toBeUndefined(); + expect(volunteerMemberships[1].group).toBeUndefined(); + expect(volunteerMemberships[2].group).toBeUndefined(); + }); + + it(`getVolunteerMembership for eventId, groupId`, async () => { + const volunteerMemberships = (await getVolunteerMembership?.( + {}, + { + where: { + eventId: testEvent?._id, + groupId: testEventVolunteerGroup?._id, + filter: "group", + userName: testUser1?.firstName, + }, + }, + {}, + )) as unknown as InterfaceVolunteerMembership[]; + expect(volunteerMemberships[0].volunteer?._id).toEqual( + testEventVolunteer1?._id, + ); + expect(volunteerMemberships[0].group?._id).toEqual( + testEventVolunteerGroup?._id, + ); + }); +}); diff --git a/tests/resolvers/Query/getVolunteerRanks.spec.ts b/tests/resolvers/Query/getVolunteerRanks.spec.ts new file mode 100644 index 0000000000..0f5d6973d5 --- /dev/null +++ b/tests/resolvers/Query/getVolunteerRanks.spec.ts @@ -0,0 +1,100 @@ +import type mongoose from "mongoose"; +import { connect, disconnect } from "../../helpers/db"; +import { beforeAll, afterAll, describe, it, expect } from "vitest"; +import type { VolunteerRank } from "../../../src/types/generatedGraphQLTypes"; +import type { TestUserType } from "../../helpers/user"; +import type { TestOrganizationType } from "../../helpers/userAndOrg"; +import { createVolunteerAndActions } from "../../helpers/volunteers"; +import { getVolunteerRanks } from "../../../src/resolvers/Query/getVolunteerRanks"; + +let MONGOOSE_INSTANCE: typeof mongoose; +let testOrganization: TestOrganizationType; +let testUser1: TestUserType; +let testUser2: TestUserType; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + const [organization, , user1, user2] = await createVolunteerAndActions(); + testOrganization = organization; + testUser1 = user1; + testUser2 = user2; +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("resolvers -> Query -> getVolunteerRanks", () => { + it(`getVolunteerRanks for allTime, descending, no limit/name`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "allTime", + orderBy: "hours_DESC", + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(10); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + expect(volunteerRanks[1].hoursVolunteered).toEqual(8); + expect(volunteerRanks[1].user._id).toEqual(testUser2?._id); + expect(volunteerRanks[1].rank).toEqual(2); + }); + + it(`getVolunteerRanks for weekly, descending, limit, no name`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "weekly", + orderBy: "hours_DESC", + limit: 1, + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(2); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + }); + + it(`getVolunteerRanks for monthly, descending, name, no limit`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "monthly", + orderBy: "hours_ASC", + nameContains: testUser1?.firstName, + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(2); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + }); + + it(`getVolunteerRanks for yearly, descending, no name/limit`, async () => { + const volunteerRanks = (await getVolunteerRanks?.( + {}, + { + orgId: testOrganization?._id, + where: { + timeFrame: "yearly", + orderBy: "hours_DESC", + }, + }, + {}, + )) as unknown as VolunteerRank[]; + expect(volunteerRanks[0].hoursVolunteered).toEqual(8); + expect(volunteerRanks[0].user._id).toEqual(testUser1?._id); + expect(volunteerRanks[0].rank).toEqual(1); + }); +}); diff --git a/tests/resolvers/Query/helperFunctions/getSort.spec.ts b/tests/resolvers/Query/helperFunctions/getSort.spec.ts index 10b0b1668c..98c5ca9434 100644 --- a/tests/resolvers/Query/helperFunctions/getSort.spec.ts +++ b/tests/resolvers/Query/helperFunctions/getSort.spec.ts @@ -9,6 +9,9 @@ import type { VenueOrderByInput, FundOrderByInput, CampaignOrderByInput, + ActionItemsOrderByInput, + EventVolunteersOrderByInput, + VolunteerMembershipOrderByInput, } from "../../../../src/types/generatedGraphQLTypes"; describe("getSort function", () => { @@ -61,6 +64,8 @@ describe("getSort function", () => { ["fundingGoal_DESC", { fundingGoal: -1 }], ["dueDate_ASC", { dueDate: 1 }], ["dueDate_DESC", { dueDate: -1 }], + ["hoursVolunteered_ASC", { hoursVolunteered: 1 }], + ["hoursVolunteered_DESC", { hoursVolunteered: -1 }], ]; it.each(testCases)( @@ -75,7 +80,10 @@ describe("getSort function", () => { | VenueOrderByInput | PledgeOrderByInput | FundOrderByInput - | CampaignOrderByInput, + | CampaignOrderByInput + | ActionItemsOrderByInput + | EventVolunteersOrderByInput + | VolunteerMembershipOrderByInput, ); expect(result).toEqual(expected); }, diff --git a/tests/resolvers/Query/helperFunctions/getWhere.spec.ts b/tests/resolvers/Query/helperFunctions/getWhere.spec.ts index 3c3494c9b6..b6e8c12faa 100644 --- a/tests/resolvers/Query/helperFunctions/getWhere.spec.ts +++ b/tests/resolvers/Query/helperFunctions/getWhere.spec.ts @@ -13,6 +13,7 @@ import type { EventVolunteerGroupWhereInput, PledgeWhereInput, ActionItemCategoryWhereInput, + EventVolunteerWhereInput, } from "../../../../src/types/generatedGraphQLTypes"; describe("getWhere function", () => { @@ -30,7 +31,8 @@ describe("getWhere function", () => { FundWhereInput & CampaignWhereInput & VenueWhereInput & - PledgeWhereInput + PledgeWhereInput & + EventVolunteerWhereInput >, Record, ][] = [ @@ -335,17 +337,10 @@ describe("getWhere function", () => { { organizationId: "6f6cd" }, ], ["campaignId", { campaignId: "6f6c" }, { _id: "6f6c" }], - [ - "volunteerId", - { volunteerId: "6f43d" }, - { - volunteers: { - $in: ["6f43d"], - }, - }, - ], ["is_disabled", { is_disabled: true }, { isDisabled: true }], ["is_disabled", { is_disabled: false }, { isDisabled: false }], + ["hasAccepted", { hasAccepted: true }, { hasAccepted: true }], + ["hasAccepted", { hasAccepted: false }, { hasAccepted: false }], ]; it.each(testCases)( diff --git a/tests/utilities/adminCheck.spec.ts b/tests/utilities/adminCheck.spec.ts index 84e2c41798..d5ba8b0662 100644 --- a/tests/utilities/adminCheck.spec.ts +++ b/tests/utilities/adminCheck.spec.ts @@ -16,6 +16,8 @@ import { AppUserProfile, Organization } from "../../src/models"; import { connect, disconnect } from "../helpers/db"; import type { TestOrganizationType, TestUserType } from "../helpers/userAndOrg"; import { createTestUserAndOrganization } from "../helpers/userAndOrg"; +import { adminCheck } from "../../src/utilities"; +import { requestContext } from "../../src/libraries"; let testUser: TestUserType; let testOrganization: TestOrganizationType; @@ -38,14 +40,11 @@ describe("utilities -> adminCheck", () => { }); it("throws error if userIsOrganizationAdmin === false and isUserSuperAdmin === false", async () => { - const { requestContext } = await import("../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); try { - const { adminCheck } = await import("../../src/utilities"); await adminCheck( testUser?._id, testOrganization ?? ({} as InterfaceOrganization), @@ -58,6 +57,16 @@ describe("utilities -> adminCheck", () => { expect(spy).toBeCalledWith(USER_NOT_AUTHORIZED_ADMIN.MESSAGE); }); + it("Returns boolean if userIsOrganizationAdmin === false and isUserSuperAdmin === false and throwError is false", async () => { + expect( + await adminCheck( + testUser?._id, + testOrganization ?? ({} as InterfaceOrganization), + false, + ), + ).toEqual(false); + }); + it("throws no error if userIsOrganizationAdmin === false and isUserSuperAdmin === true", async () => { const updatedUser = await AppUserProfile.findOneAndUpdate( { @@ -72,12 +81,11 @@ describe("utilities -> adminCheck", () => { }, ); - const { adminCheck } = await import("../../src/utilities"); - await expect( adminCheck( updatedUser?.userId?.toString() ?? "", testOrganization ?? ({} as InterfaceOrganization), + false, ), ).resolves.not.toThrowError(); }); @@ -98,8 +106,6 @@ describe("utilities -> adminCheck", () => { }, ); - const { adminCheck } = await import("../../src/utilities"); - await expect( adminCheck( testUser?._id, @@ -108,14 +114,11 @@ describe("utilities -> adminCheck", () => { ).resolves.not.toThrowError(); }); it("throws error if user is not found with the specific Id", async () => { - const { requestContext } = await import("../../src/libraries"); - const spy = vi .spyOn(requestContext, "translate") .mockImplementationOnce((message) => `Translated ${message}`); try { - const { adminCheck } = await import("../../src/utilities"); await adminCheck( new mongoose.Types.ObjectId(), testOrganization ?? ({} as InterfaceOrganization), diff --git a/tests/utilities/checks.spec.ts b/tests/utilities/checks.spec.ts new file mode 100644 index 0000000000..6cf1f3744e --- /dev/null +++ b/tests/utilities/checks.spec.ts @@ -0,0 +1,156 @@ +import "dotenv/config"; +import type mongoose from "mongoose"; +import { + afterAll, + afterEach, + beforeAll, + describe, + expect, + it, + vi, +} from "vitest"; +import { + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR, + EVENT_VOLUNTEER_NOT_FOUND_ERROR, + USER_NOT_AUTHORIZED_ERROR, + USER_NOT_FOUND_ERROR, +} from "../../src/constants"; + +import type { InterfaceUser } from "../../src/models"; +import { AppUserProfile, VolunteerMembership } from "../../src/models"; +import { connect, disconnect } from "../helpers/db"; +import type { TestUserType } from "../helpers/userAndOrg"; +import { requestContext } from "../../src/libraries"; +import { + checkAppUserProfileExists, + checkEventVolunteerExists, + checkUserExists, + checkVolunteerGroupExists, + checkVolunteerMembershipExists, +} from "../../src/utilities/checks"; +import { createTestUser } from "../helpers/user"; +import { createVolunteerAndActions } from "../helpers/volunteers"; +import type { TestEventVolunteerType } from "../helpers/events"; +import type { TestEventVolunteerGroupType } from "../resolvers/Mutation/createEventVolunteer.spec"; + +let randomUser: InterfaceUser; +let testUser: TestUserType; +let testVolunteer: TestEventVolunteerType; +let testGroup: TestEventVolunteerGroupType; +let MONGOOSE_INSTANCE: typeof mongoose; + +const expectError = async ( + fn: () => Promise, + expectedMessage: string, +): Promise => { + const spy = vi + .spyOn(requestContext, "translate") + .mockImplementationOnce((message) => `Translated ${message}`); + + try { + await fn(); + } catch (error: unknown) { + expect((error as Error).message).toEqual(`Translated ${expectedMessage}`); + } + + expect(spy).toBeCalledWith(expectedMessage); +}; + +beforeAll(async () => { + MONGOOSE_INSTANCE = await connect(); + + const [, , user1, , volunteer1, , volunteerGroup] = + await createVolunteerAndActions(); + + testUser = user1; + testVolunteer = volunteer1; + testGroup = volunteerGroup; + + randomUser = (await createTestUser()) as InterfaceUser; + await AppUserProfile.deleteOne({ + userId: randomUser._id, + }); +}); + +afterAll(async () => { + await disconnect(MONGOOSE_INSTANCE); +}); + +describe("utilities -> checks", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("checkUserExists -> invalid userId", async () => { + await expectError( + () => checkUserExists(testUser?.appUserProfileId), + USER_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkUserExists -> valid userId", async () => { + expect((await checkUserExists(testUser?._id))._id).toEqual(testUser?._id); + }); + + it("checkAppUserProfileExists -> unauthorized user", async () => { + await expectError( + () => checkAppUserProfileExists(randomUser), + USER_NOT_AUTHORIZED_ERROR.MESSAGE, + ); + }); + + it("checkAppUserProfileExists -> authorized user", async () => { + expect( + (await checkAppUserProfileExists(testUser as InterfaceUser)).userId, + ).toEqual(testUser?._id); + }); + + it("checkEventVolunteerExists -> invalid volunteerId", async () => { + await expectError( + () => checkEventVolunteerExists(testUser?._id), + EVENT_VOLUNTEER_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkEventVolunteerExists -> valid volunteerId", async () => { + expect((await checkEventVolunteerExists(testVolunteer?._id))._id).toEqual( + testVolunteer?._id, + ); + }); + + it("checkVolunteerGroupExists -> invalid groupId", async () => { + await expectError( + () => checkVolunteerGroupExists(testUser?._id), + EVENT_VOLUNTEER_GROUP_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkVolunteerGroupExists -> valid groupId", async () => { + expect((await checkVolunteerGroupExists(testGroup?._id))._id).toEqual( + testGroup?._id, + ); + }); + + it("checkVolunteerMembershipExists -> invalid membershipId", async () => { + await expectError( + () => checkVolunteerMembershipExists(testUser?._id), + EVENT_VOLUNTEER_MEMBERSHIP_NOT_FOUND_ERROR.MESSAGE, + ); + }); + + it("checkVolunteerMembershipExists -> valid membershipId", async () => { + const volunteerMembership = await VolunteerMembership.create({ + event: testVolunteer?._id, + volunteer: testUser?._id, + status: "invited", + }); + expect( + ( + await checkVolunteerMembershipExists( + volunteerMembership?._id.toString(), + ) + )._id, + ).toEqual(volunteerMembership?._id); + }); +});