Skip to content

Commit

Permalink
Introduce Feedback System (#1387)
Browse files Browse the repository at this point in the history
* Add feedback model and typedefs

* Add field level resolvers

* Add mutation and queries

* Add tests for field level resolvers

* Complete testing for feedback system
  • Loading branch information
EshaanAgg authored Sep 20, 2023
1 parent 3973d69 commit 07aed90
Show file tree
Hide file tree
Showing 27 changed files with 909 additions and 0 deletions.
2 changes: 2 additions & 0 deletions codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ const config: CodegenConfig = {

EventProject: "../models/EventProject#InterfaceEventProject",

Feedback: "../models/Feedback#InterfaceFeedback",

// File: '../models/File#InterfaceFile',

Group: "../models/Group#InterfaceGroup",
Expand Down
18 changes: 18 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type CheckIn {
allotedRoom: String
allotedSeat: String
event: Event!
feedbackSubmitted: Boolean!
time: DateTime!
user: User!
}
Expand Down Expand Up @@ -145,10 +146,12 @@ type Event {
allDay: Boolean!
attendees: [User!]!
attendeesCheckInStatus: [CheckInStatus!]!
averageFeedbackScore: Float
creator: User!
description: String!
endDate: Date!
endTime: Time
feedback: [Feedback!]!
isPublic: Boolean!
isRegisterable: Boolean!
latitude: Latitude
Expand Down Expand Up @@ -257,6 +260,19 @@ type ExtendSession {
refreshToken: String!
}

type Feedback {
_id: ID!
event: Event!
rating: Int!
review: String
}

input FeedbackInput {
eventId: ID!
rating: Int!
review: String
}

interface FieldError {
message: String!
path: [String!]!
Expand Down Expand Up @@ -393,6 +409,7 @@ type Mutation {
acceptAdmin(id: ID!): Boolean!
acceptMembershipRequest(membershipRequestId: ID!): MembershipRequest!
addEventAttendee(data: EventAttendeeInput!): User!
addFeedback(data: FeedbackInput!): Feedback!
addLanguageTranslation(data: LanguageInput!): Language!
addOrganizationImage(file: String!, organizationId: String!): Organization!
addUserImage(file: String!): User!
Expand Down Expand Up @@ -710,6 +727,7 @@ type Query {
getDonationByOrgIdConnection(first: Int, orgId: ID!, skip: Int, where: DonationWhereInput): [Donation!]!
getPlugins: [Plugin]
getlanguage(lang_code: String!): [Translation]
hasSubmittedFeedback(eventId: ID!, userId: ID!): Boolean
joinedOrganizations(id: ID): [Organization]
me: User!
myLanguage: String
Expand Down
13 changes: 13 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ export const EVENT_PROJECT_NOT_FOUND_ERROR = {
MESSAGE: "eventProject.notFound",
PARAM: "eventProject",
};

export const FEEDBACK_ALREADY_SUBMITTED = {
MESSAGE: "The user has already submitted a feedback for this event.",
CODE: "feedback.alreadySubmitted",
PARAM: "feedback.alreadySubmitted",
};

export const INVALID_OTP = "Invalid OTP";

export const IN_PRODUCTION = process.env.NODE_ENV === "production";
Expand Down Expand Up @@ -146,6 +153,12 @@ export const USER_NOT_REGISTERED_FOR_EVENT = {
PARAM: "user.notRegistered",
};

export const USER_NOT_CHECKED_IN = {
MESSAGE: "The user did not check in for the event.",
CODE: "user.notCheckedIn",
PARAM: "user.notCheckedIn",
};

export const USER_NOT_ORGANIZATION_ADMIN = {
MESSAGE: "Error: User must be an ADMIN",
CODE: "role.notValid.admin",
Expand Down
6 changes: 6 additions & 0 deletions src/models/CheckIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface InterfaceCheckIn {
time: Date;
allotedRoom: string;
allotedSeat: string;
feedbackSubmitted: boolean;
}

const checkInSchema = new Schema({
Expand All @@ -36,6 +37,11 @@ const checkInSchema = new Schema({
type: String,
required: false,
},
feedbackSubmitted: {
type: Boolean,
required: true,
default: false,
},
});

// We will also create an index here for faster database querying
Expand Down
41 changes: 41 additions & 0 deletions src/models/Feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Types, PopulatedDoc, Document, Model } from "mongoose";
import { Schema, model, models } from "mongoose";
import type { InterfaceEvent } from "./Event";

export interface InterfaceFeedback {
_id: Types.ObjectId;
eventId: PopulatedDoc<InterfaceEvent & Document>;
rating: number;
review: string | null;
}

const feedbackSchema = new Schema({
eventId: {
type: Schema.Types.ObjectId,
ref: "Event",
required: true,
},
rating: {
type: Number,
required: true,
default: 0,
max: 10,
},
review: {
type: String,
required: false,
},
});

// We will also create an index here for faster database querying
feedbackSchema.index({
eventId: 1,
});

const feedbackModel = (): Model<InterfaceFeedback> =>
model<InterfaceFeedback>("Feedback", feedbackSchema);

// This syntax is needed to prevent Mongoose OverwriteModelError while running tests.
export const Feedback = (models.Feedback || feedbackModel()) as ReturnType<
typeof feedbackModel
>;
1 change: 1 addition & 0 deletions src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from "./Donation";
export * from "./Event";
export * from "./EventAttendee";
export * from "./EventProject";
export * from "./Feedback";
export * from "./File";
export * from "./Group";
export * from "./GroupChat";
Expand Down
1 change: 1 addition & 0 deletions src/resolvers/Event/attendees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const attendees: EventResolvers["attendees"] = async (parent) => {
})
.populate("userId")
.lean();

return eventAttendeeObjects.map(
(eventAttendeeObject) => eventAttendeeObject.userId
);
Expand Down
21 changes: 21 additions & 0 deletions src/resolvers/Event/averageFeedbackScore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { EventResolvers } from "../../types/generatedGraphQLTypes";
import { Feedback } from "../../models";

export const averageFeedbackScore: EventResolvers["averageFeedbackScore"] =
async (parent) => {
const feedbacks = await Feedback.find({
eventId: parent._id,
})
.select("rating")
.lean();

// Return null if no feedback has been submitted
if (feedbacks.length === 0) return null;

// Return the average feedback score
const sum = feedbacks.reduce(
(accumulator, feedback) => accumulator + feedback.rating,
0
);
return sum / feedbacks.length;
};
8 changes: 8 additions & 0 deletions src/resolvers/Event/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { EventResolvers } from "../../types/generatedGraphQLTypes";
import { Feedback } from "../../models";

export const feedback: EventResolvers["feedback"] = async (parent) => {
return Feedback.find({
eventId: parent._id,
}).lean();
};
4 changes: 4 additions & 0 deletions src/resolvers/Event/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { EventResolvers } from "../../types/generatedGraphQLTypes";
import { attendees } from "./attendees";
import { attendeesCheckInStatus } from "./attendeesCheckInStatus";
import { averageFeedbackScore } from "./averageFeedbackScore";
import { feedback } from "./feedback";
import { organization } from "./organization";
import { projects } from "./projects";

export const Event: EventResolvers = {
attendees,
attendeesCheckInStatus,
averageFeedbackScore,
feedback,
organization,
projects,
};
8 changes: 8 additions & 0 deletions src/resolvers/Feedback/event.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { FeedbackResolvers } from "../../types/generatedGraphQLTypes";
import { Event } from "../../models";

export const event: FeedbackResolvers["event"] = async (parent) => {
return await Event.findOne({
_id: parent.eventId,
}).lean();
};
6 changes: 6 additions & 0 deletions src/resolvers/Feedback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { FeedbackResolvers } from "../../types/generatedGraphQLTypes";
import { event } from "./event";

export const Feedback: FeedbackResolvers = {
event,
};
79 changes: 79 additions & 0 deletions src/resolvers/Mutation/addFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {
EVENT_NOT_FOUND_ERROR,
USER_NOT_FOUND_ERROR,
USER_NOT_CHECKED_IN,
USER_NOT_REGISTERED_FOR_EVENT,
FEEDBACK_ALREADY_SUBMITTED,
} from "../../constants";
import type { MutationResolvers } from "../../types/generatedGraphQLTypes";
import { errors, requestContext } from "../../libraries";
import { User, Event, EventAttendee, CheckIn, Feedback } from "../../models";

export const addFeedback: MutationResolvers["addFeedback"] = async (
_parent,
args,
context
) => {
const currentUserExists = await User.exists({
_id: context.userId,
});

if (!currentUserExists) {
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
USER_NOT_FOUND_ERROR.PARAM
);
}

const currentEventExists = await Event.exists({
_id: args.data.eventId,
});

if (!currentEventExists) {
throw new errors.NotFoundError(
requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE),
EVENT_NOT_FOUND_ERROR.CODE,
EVENT_NOT_FOUND_ERROR.PARAM
);
}

const eventAttendeeObject = await EventAttendee.findOne({
eventId: args.data.eventId,
userId: context.userId,
})
.populate("checkInId")
.lean();

if (eventAttendeeObject === null) {
throw new errors.ConflictError(
requestContext.translate(USER_NOT_REGISTERED_FOR_EVENT.MESSAGE),
USER_NOT_REGISTERED_FOR_EVENT.CODE,
USER_NOT_REGISTERED_FOR_EVENT.PARAM
);
}

if (eventAttendeeObject.checkInId === null) {
throw new errors.ConflictError(
requestContext.translate(USER_NOT_CHECKED_IN.MESSAGE),
USER_NOT_CHECKED_IN.CODE,
USER_NOT_CHECKED_IN.PARAM
);
}

if (eventAttendeeObject.checkInId.feedbackSubmitted) {
throw new errors.ConflictError(
requestContext.translate(FEEDBACK_ALREADY_SUBMITTED.MESSAGE),
FEEDBACK_ALREADY_SUBMITTED.CODE,
FEEDBACK_ALREADY_SUBMITTED.PARAM
);
}

await CheckIn.findByIdAndUpdate(eventAttendeeObject.checkInId, {
feedbackSubmitted: true,
});

const feedback = await Feedback.create({ ...args.data });

return feedback;
};
2 changes: 2 additions & 0 deletions src/resolvers/Mutation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { MutationResolvers } from "../../types/generatedGraphQLTypes";
import { acceptAdmin } from "./acceptAdmin";
import { acceptMembershipRequest } from "./acceptMembershipRequest";
import { addEventAttendee } from "./addEventAttendee";
import { addFeedback } from "./addFeedback";
import { addLanguageTranslation } from "./addLanguageTranslation";
import { addOrganizationImage } from "./addOrganizationImage";
import { addUserImage } from "./addUserImage";
Expand Down Expand Up @@ -85,6 +86,7 @@ export const Mutation: MutationResolvers = {
acceptAdmin,
acceptMembershipRequest,
addEventAttendee,
addFeedback,
addLanguageTranslation,
addOrganizationImage,
addUserImage,
Expand Down
60 changes: 60 additions & 0 deletions src/resolvers/Query/hasSubmittedFeedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type { QueryResolvers } from "../../types/generatedGraphQLTypes";
import { User, Event, EventAttendee } from "../../models";
import { errors, requestContext } from "../../libraries";
import {
EVENT_NOT_FOUND_ERROR,
USER_NOT_FOUND_ERROR,
USER_NOT_CHECKED_IN,
USER_NOT_REGISTERED_FOR_EVENT,
} from "../../constants";

export const hasSubmittedFeedback: QueryResolvers["hasSubmittedFeedback"] =
async (_parent, args) => {
const currentUserExists = await User.exists({
_id: args.userId,
});

if (!currentUserExists) {
throw new errors.NotFoundError(
requestContext.translate(USER_NOT_FOUND_ERROR.MESSAGE),
USER_NOT_FOUND_ERROR.CODE,
USER_NOT_FOUND_ERROR.PARAM
);
}

const currentEventExists = await Event.exists({
_id: args.eventId,
});

if (!currentEventExists) {
throw new errors.NotFoundError(
requestContext.translate(EVENT_NOT_FOUND_ERROR.MESSAGE),
EVENT_NOT_FOUND_ERROR.CODE,
EVENT_NOT_FOUND_ERROR.PARAM
);
}

const eventAttendeeObject = await EventAttendee.findOne({
...args,
})
.populate("checkInId")
.lean();

if (eventAttendeeObject === null) {
throw new errors.ConflictError(
requestContext.translate(USER_NOT_REGISTERED_FOR_EVENT.MESSAGE),
USER_NOT_REGISTERED_FOR_EVENT.CODE,
USER_NOT_REGISTERED_FOR_EVENT.PARAM
);
}

if (eventAttendeeObject.checkInId === null) {
throw new errors.ConflictError(
requestContext.translate(USER_NOT_CHECKED_IN.MESSAGE),
USER_NOT_CHECKED_IN.CODE,
USER_NOT_CHECKED_IN.PARAM
);
}

return eventAttendeeObject.checkInId.feedbackSubmitted;
};
Loading

0 comments on commit 07aed90

Please sign in to comment.