From 801d3aca80c6eeb3c147880471d36a5ec6653514 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Sun, 26 May 2024 12:28:06 -0500 Subject: [PATCH 01/10] stats service --- src/app.ts | 2 + src/services/events/events-schema.ts | 5 ++ src/services/stats/stats-router.ts | 127 +++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 src/services/stats/stats-router.ts diff --git a/src/app.ts b/src/app.ts index 6da05cb..8625c6b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -15,6 +15,7 @@ import eventsRouter from "./services/events/events-router"; import notificationsRouter from "./services/notifications/notifications-router"; import registrationRouter from "./services/registration/registration-router"; import s3Router from "./services/s3/s3-router"; +import statsRouter from "./services/stats/stats-router"; import subscriptionRouter from "./services/subscription/subscription-router"; const app = express(); @@ -39,6 +40,7 @@ app.use("/events", eventsRouter); app.use("/notifications", notificationsRouter); app.use("/registration", registrationRouter); app.use("/s3", s3Router); +app.use("/stats", statsRouter); app.use("/subscription", subscriptionRouter); app.get("/status", (_, res) => { diff --git a/src/services/events/events-schema.ts b/src/services/events/events-schema.ts index 7789433..7a9a90a 100644 --- a/src/services/events/events-schema.ts +++ b/src/services/events/events-schema.ts @@ -12,6 +12,7 @@ export const EventValidator = z.object({ isVirtual: z.boolean(), imageUrl: z.string().nullable().optional(), isVisible: z.boolean().default(false), + attendanceCount: z.number().optional(), }); export const EventSchema = new Schema({ @@ -53,4 +54,8 @@ export const EventSchema = new Schema({ type: Boolean, default: false, }, + attendanceCount: { + type: Number, + default: 0, + }, }); diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts new file mode 100644 index 0000000..116957f --- /dev/null +++ b/src/services/stats/stats-router.ts @@ -0,0 +1,127 @@ +import { Router } from "express"; +import { StatusCodes } from "http-status-codes"; +import { Database } from "../../database"; +import RoleChecker from "../../middleware/role-checker"; +import { Role } from "../auth/auth-models"; + +const statsRouter = Router(); + +// Get the number of people checked in (staff only) +statsRouter.get( + "/check-in", + RoleChecker([Role.enum.STAFF], true), + async (req, res, next) => { + try { + const attendees = await Database.ATTENDEES.find({ + events: { $ne: [] }, + }); + + return res.status(StatusCodes.OK).json({ count: attendees.length }); + } catch (error) { + next(error); + } + } +); + +// Get the number of people eligible for merch item (staff only) +statsRouter.get( + "/merch-item/:PRICE", + RoleChecker([Role.enum.STAFF], true), + async (req, res, next) => { + try { + const price = req.params.PRICE; + if (!price) { + return res + .status(StatusCodes.BAD_REQUEST) + .json({ error: "MissingPriceParameter" }); + } + const attendees = await Database.ATTENDEES.find({ + points: { $gte: price }, + }); + + return res.status(StatusCodes.OK).json({ count: attendees.length }); + } catch (error) { + next(error); + } + } +); + +// Get the number of priority attendees (staff only) +statsRouter.get( + "/priority-attendee", + RoleChecker([Role.enum.STAFF], true), + async (req, res, next) => { + try { + const currentTime = new Date(); + const attendees = await Database.ATTENDEES.find({ + priority_expiry: { $gt: currentTime }, + }); + + return res.status(StatusCodes.OK).json({ count: attendees.length }); + } catch (error) { + next(error); + } + } +); + +// Get the attendance of the past n events (staff only) +statsRouter.get( + "/attendance/:N", + RoleChecker([Role.enum.STAFF], true), + async (req, res, next) => { + try { + const n = req.params.N; + if (!n) { + return res + .status(StatusCodes.BAD_REQUEST) + .json({ error: "MissingNParameter" }); + } + const currentTime = new Date(); + const events = await Database.EVENTS.find({ + endTime: { $lt: currentTime }, + }) + .sort({ endTime: -1 }) + .limit(parseInt(n)); + + const attendanceCounts = events.map( + (event) => event.attendanceCount + ); + + return res + .status(StatusCodes.OK) + .json({ attendanceCounts: attendanceCounts }); + } catch (error) { + next(error); + } + } +); + +// Get the dietary restriction breakdown/counts (staff only) +statsRouter.get( + "/dietary-restrictions", + RoleChecker([Role.enum.STAFF], true), + async (req, res, next) => { + try { + const attendees = await Database.ATTENDEES.find({}); + const dietaryRestrictionsMap: { [key: string]: number } = {}; + + attendees.forEach((attendee) => { + const dietary_restriction = + attendee.dietary_restrictions as string; + if (dietaryRestrictionsMap[dietary_restriction]) { + dietaryRestrictionsMap[dietary_restriction]++; + } else { + dietaryRestrictionsMap[dietary_restriction] = 1; + } + }); + + return res + .status(StatusCodes.OK) + .json({ dietaryRestrictions: dietaryRestrictionsMap }); + } catch (error) { + next(error); + } + } +); + +export default statsRouter; From d4c7fa973f3d262d84624aec6c8d1564531f6cb8 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Sun, 26 May 2024 12:29:19 -0500 Subject: [PATCH 02/10] strong JWT verification --- src/services/stats/stats-router.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index 116957f..ee496b6 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -9,7 +9,7 @@ const statsRouter = Router(); // Get the number of people checked in (staff only) statsRouter.get( "/check-in", - RoleChecker([Role.enum.STAFF], true), + RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { const attendees = await Database.ATTENDEES.find({ @@ -26,7 +26,7 @@ statsRouter.get( // Get the number of people eligible for merch item (staff only) statsRouter.get( "/merch-item/:PRICE", - RoleChecker([Role.enum.STAFF], true), + RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { const price = req.params.PRICE; @@ -49,7 +49,7 @@ statsRouter.get( // Get the number of priority attendees (staff only) statsRouter.get( "/priority-attendee", - RoleChecker([Role.enum.STAFF], true), + RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { const currentTime = new Date(); @@ -67,7 +67,7 @@ statsRouter.get( // Get the attendance of the past n events (staff only) statsRouter.get( "/attendance/:N", - RoleChecker([Role.enum.STAFF], true), + RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { const n = req.params.N; @@ -99,7 +99,7 @@ statsRouter.get( // Get the dietary restriction breakdown/counts (staff only) statsRouter.get( "/dietary-restrictions", - RoleChecker([Role.enum.STAFF], true), + RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { const attendees = await Database.ATTENDEES.find({}); From 7fc2df007775707f20be86e84fc9a6ebd0eaca76 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Fri, 31 May 2024 10:42:58 -0400 Subject: [PATCH 03/10] yse --- src/services/attendees/attendee-schema.ts | 8 +++-- .../registration/registration-schema.ts | 4 ++- src/services/stats/stats-router.ts | 29 +++++++++++++------ 3 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/services/attendees/attendee-schema.ts b/src/services/attendees/attendee-schema.ts index 358795d..2499eb1 100644 --- a/src/services/attendees/attendee-schema.ts +++ b/src/services/attendees/attendee-schema.ts @@ -7,9 +7,11 @@ const AttendeeValidator = z.object({ name: z.string(), email: z.string().email(), events: z.array(z.string()), - dietary_restrictions: z.string(), + dietary_restrictions: z.string().array(), + allergies: z.string().array(), priority_expiry: z.date().nullable().optional(), points: z.number().min(0).default(0), + checkin: z.boolean().array().default([false, false, false, false, false]), }); // Mongoose schema for attendee @@ -18,9 +20,11 @@ const AttendeeSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, events: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }], - dietary_restrictions: { type: String, required: true }, + dietary_restrictions: { type: [String], required: true }, + allergies: { type: [String], required: true }, priority_expiry: { type: Date, default: null }, points: { type: Number, default: 0 }, + checkin: { type: [Boolean], deafult: [false, false, false, false, false] }, }); export { AttendeeSchema, AttendeeValidator }; diff --git a/src/services/registration/registration-schema.ts b/src/services/registration/registration-schema.ts index c2ebf44..2963d81 100644 --- a/src/services/registration/registration-schema.ts +++ b/src/services/registration/registration-schema.ts @@ -10,6 +10,7 @@ const RegistrationValidator = z.object({ graduation: z.string().nullable().optional(), major: z.string().nullable().optional(), dietaryRestrictions: z.string().array(), + allergies: z.string().array(), age: z.number().nullable().optional(), gender: z.string().nullable().optional(), race: z.array(z.string()).nullable().optional(), @@ -32,7 +33,8 @@ const RegistrationSchema = new mongoose.Schema({ university: { type: String, required: true }, graduation: { type: String, default: null }, major: { type: String, default: null }, - dietaryRstrictions: { type: String, required: true }, + dietaryRestrictions: { type: [String], required: true }, + allergies: { type: [String], required: true }, age: { type: Number, default: null }, gender: { type: String, default: null }, race: [{ type: String }], diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index ee496b6..788f33a 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -103,21 +103,32 @@ statsRouter.get( async (req, res, next) => { try { const attendees = await Database.ATTENDEES.find({}); - const dietaryRestrictionsMap: { [key: string]: number } = {}; + let none = 0; + let dietary_restrictions = 0; + let allergies = 0; + let both = 0; attendees.forEach((attendee) => { - const dietary_restriction = - attendee.dietary_restrictions as string; - if (dietaryRestrictionsMap[dietary_restriction]) { - dietaryRestrictionsMap[dietary_restriction]++; + if ( + attendee.dietary_restrictions.length > 0 && + attendee.allergies.length > 0 + ) { + both++; + } else if (attendee.dietary_restrictions.length > 0) { + dietary_restrictions++; + } else if (attendee.allergies.length > 0) { + allergies++; } else { - dietaryRestrictionsMap[dietary_restriction] = 1; + none++; } }); - return res - .status(StatusCodes.OK) - .json({ dietaryRestrictions: dietaryRestrictionsMap }); + return res.status(StatusCodes.OK).json({ + none: none, + dietary_restrictions: dietary_restrictions, + allergies: allergies, + both: both, + }); } catch (error) { next(error); } From ed7f068c3c75cd5aba2ee348ffa802804efbb4ad Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Fri, 31 May 2024 12:34:37 -0400 Subject: [PATCH 04/10] allergy counts + dietary restrictio ncoutns --- src/services/stats/stats-router.ts | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index 788f33a..15063a9 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -104,9 +104,11 @@ statsRouter.get( try { const attendees = await Database.ATTENDEES.find({}); let none = 0; - let dietary_restrictions = 0; - let allergies = 0; + let dietary_restrictions = 0; // Gluten-Free, Lactose-Intolerant, No Pork, No Beef, No Fish, Halal, Vegetarian, Vegan, Diabetes + let allergies = 0; // Milk, Eggs, Tree nuts, Peanuts, Shellfish, Fish, Soy, Wheat, Sesame let both = 0; + const dietary_restriction_counts: { [key: string]: number } = {}; + const allergy_counts: { [key: string]: number } = {}; attendees.forEach((attendee) => { if ( @@ -121,6 +123,22 @@ statsRouter.get( } else { none++; } + + attendee.dietary_restrictions.forEach((dietary_restriction) => { + if (dietary_restriction_counts[dietary_restriction]) { + dietary_restriction_counts[dietary_restriction]++; + } else { + dietary_restriction_counts[dietary_restriction] = 1; + } + }); + + attendee.allergies.forEach((allergies) => { + if (allergy_counts[allergies]) { + allergy_counts[allergies]++; + } else { + allergy_counts[allergies] = 1; + } + }); }); return res.status(StatusCodes.OK).json({ @@ -128,6 +146,8 @@ statsRouter.get( dietary_restrictions: dietary_restrictions, allergies: allergies, both: both, + allergy_counts: allergy_counts, + dietary_restriction_counts: dietary_restriction_counts, }); } catch (error) { next(error); From 01d32f24c810131c1a56462d4157cfcce8d68d19 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Fri, 31 May 2024 12:37:42 -0400 Subject: [PATCH 05/10] typescript workarounds: --- src/services/stats/stats-router.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index 15063a9..f7f79de 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -124,7 +124,9 @@ statsRouter.get( none++; } - attendee.dietary_restrictions.forEach((dietary_restriction) => { + const attendee_dietary_restrictions: string[] = + attendee.dietary_restrictions; + attendee_dietary_restrictions.forEach((dietary_restriction) => { if (dietary_restriction_counts[dietary_restriction]) { dietary_restriction_counts[dietary_restriction]++; } else { @@ -132,7 +134,8 @@ statsRouter.get( } }); - attendee.allergies.forEach((allergies) => { + const attendee_allergies: string[] = attendee.allergies; + attendee_allergies.forEach((allergies) => { if (allergy_counts[allergies]) { allergy_counts[allergies]++; } else { From 1d18aa7fce306914cefdb9d5e526015d54ee2df2 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:51:23 -0400 Subject: [PATCH 06/10] changes in progress --- src/services/attendees/attendee-schema.ts | 30 ++++++++-- src/services/stats/stats-router.ts | 71 +++++++++++++++-------- 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/services/attendees/attendee-schema.ts b/src/services/attendees/attendee-schema.ts index 2499eb1..a5dee1c 100644 --- a/src/services/attendees/attendee-schema.ts +++ b/src/services/attendees/attendee-schema.ts @@ -7,11 +7,17 @@ const AttendeeValidator = z.object({ name: z.string(), email: z.string().email(), events: z.array(z.string()), - dietary_restrictions: z.string().array(), + dietaryRestrictions: z.string().array(), allergies: z.string().array(), - priority_expiry: z.date().nullable().optional(), + priorityExpiry: z.date().nullable().optional(), points: z.number().min(0).default(0), - checkin: z.boolean().array().default([false, false, false, false, false]), + hasPriority: z.record(z.boolean()).default({ + dayOne: false, + dayTwo: false, + dayThree: false, + dayFour: false, + dayFive: false, + }), }); // Mongoose schema for attendee @@ -20,11 +26,23 @@ const AttendeeSchema = new mongoose.Schema({ name: { type: String, required: true }, email: { type: String, required: true, unique: true }, events: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }], - dietary_restrictions: { type: [String], required: true }, + dietaryRestrictions: { type: [String], required: true }, allergies: { type: [String], required: true }, - priority_expiry: { type: Date, default: null }, + priorityExpiry: { type: Date, default: null }, points: { type: Number, default: 0 }, - checkin: { type: [Boolean], deafult: [false, false, false, false, false] }, + hasPriority: { + type: new mongoose.Schema( + { + dayOne: { type: Boolean, default: false }, + dayTwo: { type: Boolean, default: false }, + dayThree: { type: Boolean, default: false }, + dayFour: { type: Boolean, default: false }, + dayFive: { type: Boolean, default: false }, + }, + { _id: false } + ), + default: () => ({}), + }, }); export { AttendeeSchema, AttendeeValidator }; diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index f7f79de..9cdb49d 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -3,6 +3,8 @@ import { StatusCodes } from "http-status-codes"; import { Database } from "../../database"; import RoleChecker from "../../middleware/role-checker"; import { Role } from "../auth/auth-models"; +import { AttendeeSchema } from "../attendees/attendee-schema"; +import mongoose from "mongoose"; const statsRouter = Router(); @@ -54,7 +56,7 @@ statsRouter.get( try { const currentTime = new Date(); const attendees = await Database.ATTENDEES.find({ - priority_expiry: { $gt: currentTime }, + priorityExpiry: { $gt: currentTime }, }); return res.status(StatusCodes.OK).json({ count: attendees.length }); @@ -70,8 +72,8 @@ statsRouter.get( RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { - const n = req.params.N; - if (!n) { + const numEvents = req.params.N; + if (!numEvents) { return res .status(StatusCodes.BAD_REQUEST) .json({ error: "MissingNParameter" }); @@ -81,7 +83,7 @@ statsRouter.get( endTime: { $lt: currentTime }, }) .sort({ endTime: -1 }) - .limit(parseInt(n)); + .limit(parseInt(numEvents)); const attendanceCounts = events.map( (event) => event.attendanceCount @@ -104,53 +106,53 @@ statsRouter.get( try { const attendees = await Database.ATTENDEES.find({}); let none = 0; - let dietary_restrictions = 0; // Gluten-Free, Lactose-Intolerant, No Pork, No Beef, No Fish, Halal, Vegetarian, Vegan, Diabetes + let dietaryRestrictions = 0; // Gluten-Free, Lactose-Intolerant, No Pork, No Beef, No Fish, Halal, Vegetarian, Vegan, Diabetes let allergies = 0; // Milk, Eggs, Tree nuts, Peanuts, Shellfish, Fish, Soy, Wheat, Sesame let both = 0; - const dietary_restriction_counts: { [key: string]: number } = {}; - const allergy_counts: { [key: string]: number } = {}; + const dietaryRestrictionCounts: { [key: string]: number } = {}; + const allergyCounts: { [key: string]: number } = {}; attendees.forEach((attendee) => { if ( - attendee.dietary_restrictions.length > 0 && + attendee.dietaryRestrictions.length > 0 && attendee.allergies.length > 0 ) { both++; - } else if (attendee.dietary_restrictions.length > 0) { - dietary_restrictions++; + } else if (attendee.dietaryRestrictions.length > 0) { + dietaryRestrictions++; } else if (attendee.allergies.length > 0) { allergies++; } else { none++; } - const attendee_dietary_restrictions: string[] = - attendee.dietary_restrictions; - attendee_dietary_restrictions.forEach((dietary_restriction) => { - if (dietary_restriction_counts[dietary_restriction]) { - dietary_restriction_counts[dietary_restriction]++; + const attendeeDietaryRestrictions: string[] = + attendee.dietaryRestrictions; + attendeeDietaryRestrictions.forEach((dietaryRestriction) => { + if (dietaryRestrictionCounts[dietaryRestriction]) { + dietaryRestrictionCounts[dietaryRestriction]++; } else { - dietary_restriction_counts[dietary_restriction] = 1; + dietaryRestrictionCounts[dietaryRestriction] = 1; } }); - const attendee_allergies: string[] = attendee.allergies; - attendee_allergies.forEach((allergies) => { - if (allergy_counts[allergies]) { - allergy_counts[allergies]++; + const attendeeAllergies: string[] = attendee.allergies; + attendeeAllergies.forEach((allergies) => { + if (allergyCounts[allergies]) { + allergyCounts[allergies]++; } else { - allergy_counts[allergies] = 1; + allergyCounts[allergies] = 1; } }); }); return res.status(StatusCodes.OK).json({ none: none, - dietary_restrictions: dietary_restrictions, + dietaryRestrictions: dietaryRestrictions, allergies: allergies, both: both, - allergy_counts: allergy_counts, - dietary_restriction_counts: dietary_restriction_counts, + allergyCounts: allergyCounts, + dietaryRestrictionCounts: dietaryRestrictionCounts, }); } catch (error) { next(error); @@ -158,4 +160,25 @@ statsRouter.get( } ); +statsRouter.get( + "/test", + RoleChecker([Role.enum.STAFF], true), + async (req, res, next) => { + try { + const Attendee = mongoose.model("Attendee", AttendeeSchema); + const exampleAttendee = new Attendee({ + userId: "12345", + name: "John Doe", + email: "john.doe@example.com", + dietaryRestrictions: ["Vegan"], + allergies: ["Peanuts"], + }); + console.log(exampleAttendee); + return res.status(StatusCodes.OK); + } catch (error) { + next(error); + } + } +); + export default statsRouter; From 2db9f3a30482e97cc77e214f7587748c881bae24 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:14:42 -0400 Subject: [PATCH 07/10] resolved comments --- src/services/attendees/attendee-schema.ts | 4 +- src/services/events/events-router.ts | 23 ++-- src/services/events/events-schema.ts | 16 ++- src/services/stats/stats-router.ts | 143 ++++++++++++---------- 4 files changed, 107 insertions(+), 79 deletions(-) diff --git a/src/services/attendees/attendee-schema.ts b/src/services/attendees/attendee-schema.ts index a5dee1c..29bf7d5 100644 --- a/src/services/attendees/attendee-schema.ts +++ b/src/services/attendees/attendee-schema.ts @@ -9,7 +9,7 @@ const AttendeeValidator = z.object({ events: z.array(z.string()), dietaryRestrictions: z.string().array(), allergies: z.string().array(), - priorityExpiry: z.date().nullable().optional(), + hasCheckedIn: z.boolean().default(false), points: z.number().min(0).default(0), hasPriority: z.record(z.boolean()).default({ dayOne: false, @@ -28,7 +28,7 @@ const AttendeeSchema = new mongoose.Schema({ events: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }], dietaryRestrictions: { type: [String], required: true }, allergies: { type: [String], required: true }, - priorityExpiry: { type: Date, default: null }, + hasCheckedIn: { type: Boolean, default: false }, points: { type: Number, default: 0 }, hasPriority: { type: new mongoose.Schema( diff --git a/src/services/events/events-router.ts b/src/services/events/events-router.ts index ffbb31f..eaebbc7 100644 --- a/src/services/events/events-router.ts +++ b/src/services/events/events-router.ts @@ -1,13 +1,13 @@ import { Router } from "express"; import { StatusCodes } from "http-status-codes"; -import { EventValidator } from "./events-schema"; +import { publicEventValidator, privateEventValidator } from "./events-schema"; import { Database } from "../../database"; const eventsRouter = Router(); eventsRouter.post("/", async (req, res, next) => { try { - const validatedData = EventValidator.parse(req.body); + const validatedData = publicEventValidator.parse(req.body); const event = new Database.EVENTS(validatedData); await event.save(); return res.sendStatus(StatusCodes.CREATED); @@ -19,15 +19,21 @@ eventsRouter.post("/", async (req, res, next) => { eventsRouter.get("/:EVENTID", async (req, res, next) => { const eventId = req.params.EVENTID; try { - const event = await Database.EVENTS.findOne({ eventId: eventId }); + const unfiltered_event = await Database.EVENTS.findOne({ + eventId: eventId, + }); - if (!event) { + if (!unfiltered_event) { return res .status(StatusCodes.NOT_FOUND) .json({ error: "DoesNotExist" }); } - return res.status(StatusCodes.OK).json(event.toObject()); + const filtered_event = publicEventValidator.parse( + unfiltered_event.toJSON() + ); + + return res.status(StatusCodes.OK).json(filtered_event); } catch (error) { next(error); } @@ -36,8 +42,11 @@ eventsRouter.get("/:EVENTID", async (req, res, next) => { // Get all events eventsRouter.get("/", async (req, res, next) => { try { - const events = await Database.EVENTS.find(); - return res.status(StatusCodes.OK).json(events); + const unfiltered_events = await Database.EVENTS.find(); + const filtered_events = unfiltered_events.map((unfiltered_event) => { + return publicEventValidator.parse(unfiltered_event.toJSON()); + }); + return res.status(StatusCodes.OK).json(filtered_events); } catch (error) { next(error); } diff --git a/src/services/events/events-schema.ts b/src/services/events/events-schema.ts index 7a9a90a..2bf35c1 100644 --- a/src/services/events/events-schema.ts +++ b/src/services/events/events-schema.ts @@ -2,7 +2,7 @@ import { Schema } from "mongoose"; import { z } from "zod"; import { v4 as uuidv4 } from "uuid"; -export const EventValidator = z.object({ +export const publicEventValidator = z.object({ eventId: z.coerce.string().optional(), name: z.string(), startTime: z.coerce.date(), @@ -12,7 +12,19 @@ export const EventValidator = z.object({ isVirtual: z.boolean(), imageUrl: z.string().nullable().optional(), isVisible: z.boolean().default(false), - attendanceCount: z.number().optional(), +}); + +export const privateEventValidator = z.object({ + eventId: z.coerce.string().optional(), + name: z.string(), + startTime: z.coerce.date(), + endTime: z.coerce.date(), + points: z.number().min(0), + description: z.string(), + isVirtual: z.boolean(), + imageUrl: z.string().nullable().optional(), + isVisible: z.boolean().default(false), + attendanceCount: z.number(), }); export const EventSchema = new Schema({ diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index 9cdb49d..2243d1a 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -3,8 +3,6 @@ import { StatusCodes } from "http-status-codes"; import { Database } from "../../database"; import RoleChecker from "../../middleware/role-checker"; import { Role } from "../auth/auth-models"; -import { AttendeeSchema } from "../attendees/attendee-schema"; -import mongoose from "mongoose"; const statsRouter = Router(); @@ -54,9 +52,8 @@ statsRouter.get( RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { - const currentTime = new Date(); const attendees = await Database.ATTENDEES.find({ - priorityExpiry: { $gt: currentTime }, + hasCheckedIn: true, }); return res.status(StatusCodes.OK).json({ count: attendees.length }); @@ -104,53 +101,84 @@ statsRouter.get( RoleChecker([Role.enum.STAFF], false), async (req, res, next) => { try { - const attendees = await Database.ATTENDEES.find({}); - let none = 0; - let dietaryRestrictions = 0; // Gluten-Free, Lactose-Intolerant, No Pork, No Beef, No Fish, Halal, Vegetarian, Vegan, Diabetes - let allergies = 0; // Milk, Eggs, Tree nuts, Peanuts, Shellfish, Fish, Soy, Wheat, Sesame - let both = 0; - const dietaryRestrictionCounts: { [key: string]: number } = {}; - const allergyCounts: { [key: string]: number } = {}; - - attendees.forEach((attendee) => { - if ( - attendee.dietaryRestrictions.length > 0 && - attendee.allergies.length > 0 - ) { - both++; - } else if (attendee.dietaryRestrictions.length > 0) { - dietaryRestrictions++; - } else if (attendee.allergies.length > 0) { - allergies++; - } else { - none++; + const results = await Promise.allSettled([ + Database.ATTENDEES.countDocuments({ + allergies: { $size: 0 }, + dietaryRestrictions: { $size: 0 }, + }), + Database.ATTENDEES.countDocuments({ + allergies: { $size: 0 }, + dietaryRestrictions: { $ne: [] }, + }), + Database.ATTENDEES.countDocuments({ + allergies: { $ne: [] }, + dietaryRestrictions: { $size: 0 }, + }), + Database.ATTENDEES.countDocuments({ + allergies: { $ne: [] }, + dietaryRestrictions: { $ne: [] }, + }), + Database.ATTENDEES.aggregate([ + { + $unwind: "$allergies", + }, + { + $group: { + _id: "$allergies", + count: { $sum: 1 }, + }, + }, + ]), + Database.ATTENDEES.aggregate([ + { + $unwind: "$dietaryRestrictions", + }, + { + $group: { + _id: "$dietaryRestrictions", + count: { $sum: 1 }, + }, + }, + ]), + ]); + + for (let i = 0; i < results.length; i++) { + if (results[i].status === "rejected") { + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .send({ error: "InternalError" }); } + } - const attendeeDietaryRestrictions: string[] = - attendee.dietaryRestrictions; - attendeeDietaryRestrictions.forEach((dietaryRestriction) => { - if (dietaryRestrictionCounts[dietaryRestriction]) { - dietaryRestrictionCounts[dietaryRestriction]++; - } else { - dietaryRestrictionCounts[dietaryRestriction] = 1; - } - }); - - const attendeeAllergies: string[] = attendee.allergies; - attendeeAllergies.forEach((allergies) => { - if (allergyCounts[allergies]) { - allergyCounts[allergies]++; - } else { - allergyCounts[allergies] = 1; - } - }); - }); + const allergyCounts: { [key: string]: number } = {}; + const unprocessedAllergyCounts = ( + results[4] as PromiseFulfilledResult + ).value; + for (let i = 0; i < unprocessedAllergyCounts.length; i++) { + allergyCounts[unprocessedAllergyCounts[i]._id as string] = + unprocessedAllergyCounts[i].count; + } + const dietaryRestrictionCounts: { [key: string]: number } = {}; + const unprocessedDietaryRestrictionCountss = ( + results[5] as PromiseFulfilledResult + ).value; + for ( + let i = 0; + i < unprocessedDietaryRestrictionCountss.length; + i++ + ) { + dietaryRestrictionCounts[ + unprocessedDietaryRestrictionCountss[i]._id as string + ] = unprocessedDietaryRestrictionCountss[i].count; + } return res.status(StatusCodes.OK).json({ - none: none, - dietaryRestrictions: dietaryRestrictions, - allergies: allergies, - both: both, + none: (results[0] as PromiseFulfilledResult).value, + dietaryRestrictions: ( + results[1] as PromiseFulfilledResult + ).value, + allergies: (results[2] as PromiseFulfilledResult).value, + both: (results[3] as PromiseFulfilledResult).value, allergyCounts: allergyCounts, dietaryRestrictionCounts: dietaryRestrictionCounts, }); @@ -160,25 +188,4 @@ statsRouter.get( } ); -statsRouter.get( - "/test", - RoleChecker([Role.enum.STAFF], true), - async (req, res, next) => { - try { - const Attendee = mongoose.model("Attendee", AttendeeSchema); - const exampleAttendee = new Attendee({ - userId: "12345", - name: "John Doe", - email: "john.doe@example.com", - dietaryRestrictions: ["Vegan"], - allergies: ["Peanuts"], - }); - console.log(exampleAttendee); - return res.status(StatusCodes.OK); - } catch (error) { - next(error); - } - } -); - export default statsRouter; From 307a239eec52aca7ef370586bea3db6055a7621f Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:26:34 -0400 Subject: [PATCH 08/10] eslint fixes --- src/database.ts | 7 +++++-- src/services/events/events-router.ts | 2 +- src/services/stats/stats-router.ts | 10 +++++++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/database.ts b/src/database.ts index 42fdb60..efb5041 100644 --- a/src/database.ts +++ b/src/database.ts @@ -4,7 +4,10 @@ import { AttendeeValidator, } from "./services/attendees/attendee-schema"; import { RoleValidator, RoleSchema } from "./services/auth/auth-schema"; -import { EventSchema, EventValidator } from "./services/events/events-schema"; +import { + EventSchema, + privateEventValidator, +} from "./services/events/events-schema"; import { RegistrationSchema, RegistrationValidator, @@ -51,7 +54,7 @@ function initializeModel( // Example usage export const Database = { ROLES: initializeModel("roles", RoleSchema, RoleValidator), - EVENTS: initializeModel("events", EventSchema, EventValidator), + EVENTS: initializeModel("events", EventSchema, privateEventValidator), SUBSCRIPTIONS: initializeModel( "subscriptions", SubscriptionSchema, diff --git a/src/services/events/events-router.ts b/src/services/events/events-router.ts index eaebbc7..baddd91 100644 --- a/src/services/events/events-router.ts +++ b/src/services/events/events-router.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import { StatusCodes } from "http-status-codes"; -import { publicEventValidator, privateEventValidator } from "./events-schema"; +import { publicEventValidator } from "./events-schema"; import { Database } from "../../database"; const eventsRouter = Router(); diff --git a/src/services/stats/stats-router.ts b/src/services/stats/stats-router.ts index 2243d1a..84af89a 100644 --- a/src/services/stats/stats-router.ts +++ b/src/services/stats/stats-router.ts @@ -98,7 +98,7 @@ statsRouter.get( // Get the dietary restriction breakdown/counts (staff only) statsRouter.get( "/dietary-restrictions", - RoleChecker([Role.enum.STAFF], false), + RoleChecker([Role.enum.STAFF], true), async (req, res, next) => { try { const results = await Promise.allSettled([ @@ -150,9 +150,13 @@ statsRouter.get( } } + type mongoQueryType = { + _id: string; + count: number; + }; const allergyCounts: { [key: string]: number } = {}; const unprocessedAllergyCounts = ( - results[4] as PromiseFulfilledResult + results[4] as PromiseFulfilledResult ).value; for (let i = 0; i < unprocessedAllergyCounts.length; i++) { allergyCounts[unprocessedAllergyCounts[i]._id as string] = @@ -160,7 +164,7 @@ statsRouter.get( } const dietaryRestrictionCounts: { [key: string]: number } = {}; const unprocessedDietaryRestrictionCountss = ( - results[5] as PromiseFulfilledResult + results[5] as PromiseFulfilledResult ).value; for ( let i = 0; From 0cdc40daf23136fa265bb6f3b5b8e437c29393a0 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:41:50 -0400 Subject: [PATCH 09/10] u can review now aydan --- src/services/events/events-schema.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/services/events/events-schema.ts b/src/services/events/events-schema.ts index e22d80a..77ec5c8 100644 --- a/src/services/events/events-schema.ts +++ b/src/services/events/events-schema.ts @@ -17,18 +17,8 @@ export const publicEventValidator = z.object({ eventType: EventType, }); -export const privateEventValidator = z.object({ - eventId: z.coerce.string().optional(), - name: z.string(), - startTime: z.coerce.date(), - endTime: z.coerce.date(), - points: z.number().min(0), - description: z.string(), - isVirtual: z.boolean(), - imageUrl: z.string().nullable().optional(), - isVisible: z.boolean().default(false), +export const privateEventValidator = publicEventValidator.extend({ attendanceCount: z.number(), - eventType: EventType, }); export const EventSchema = new Schema({ From 7da8c5ea912fdff1bb543e9d242ff2e3bb751a28 Mon Sep 17 00:00:00 2001 From: Alex Yang <32620988+DatProJack@users.noreply.github.com> Date: Sat, 8 Jun 2024 18:56:43 -0400 Subject: [PATCH 10/10] resolved comments --- src/services/attendees/attendee-schema.ts | 12 ++++++------ src/services/events/events-router.ts | 4 +--- src/services/events/events-schema.ts | 2 +- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/services/attendees/attendee-schema.ts b/src/services/attendees/attendee-schema.ts index 29bf7d5..443572f 100644 --- a/src/services/attendees/attendee-schema.ts +++ b/src/services/attendees/attendee-schema.ts @@ -11,12 +11,12 @@ const AttendeeValidator = z.object({ allergies: z.string().array(), hasCheckedIn: z.boolean().default(false), points: z.number().min(0).default(0), - hasPriority: z.record(z.boolean()).default({ - dayOne: false, - dayTwo: false, - dayThree: false, - dayFour: false, - dayFive: false, + hasPriority: z.object({ + dayOne: z.boolean(), + dayTwo: z.boolean(), + dayThree: z.boolean(), + dayFour: z.boolean(), + dayFive: z.boolean(), }), }); diff --git a/src/services/events/events-router.ts b/src/services/events/events-router.ts index 818a173..6deaeca 100644 --- a/src/services/events/events-router.ts +++ b/src/services/events/events-router.ts @@ -50,9 +50,7 @@ eventsRouter.get("/:EVENTID", async (req, res, next) => { .json({ error: "DoesNotExist" }); } - const filtered_event = publicEventValidator.parse( - unfiltered_event.toJSON() - ); + const filtered_event = publicEventValidator.parse(unfiltered_event); return res.status(StatusCodes.OK).json(filtered_event); } catch (error) { diff --git a/src/services/events/events-schema.ts b/src/services/events/events-schema.ts index 77ec5c8..92ca48f 100644 --- a/src/services/events/events-schema.ts +++ b/src/services/events/events-schema.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from "uuid"; export const EventType = z.enum(["A", "B", "C"]); export const publicEventValidator = z.object({ - eventId: z.coerce.string().optional(), + eventId: z.coerce.string(), name: z.string(), startTime: z.coerce.date(), endTime: z.coerce.date(),