Skip to content

Commit

Permalink
Merge pull request #71 from ReflectionsProjections/dev/alex/stats-ser…
Browse files Browse the repository at this point in the history
…vice

Stats Service
  • Loading branch information
aletya authored Jun 8, 2024
2 parents cb367b5 + 7da8c5e commit a4e70a5
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 12 deletions.
2 changes: 2 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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) => {
Expand Down
7 changes: 5 additions & 2 deletions src/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
28 changes: 27 additions & 1 deletion src/services/attendees/attendee-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,43 @@ const AttendeeValidator = z.object({
userId: z.string(),
name: z.string(),
email: z.string().email(),
events: z.array(z.string()),
dietaryRestrictions: z.string().array(),
allergies: z.string().array(),
hasCheckedIn: z.boolean().default(false),
points: z.number().min(0).default(0),
hasPriority: z.object({
dayOne: z.boolean(),
dayTwo: z.boolean(),
dayThree: z.boolean(),
dayFour: z.boolean(),
dayFive: z.boolean(),
}),
});

// Mongoose schema for attendee
const AttendeeSchema = new mongoose.Schema({
userId: { type: String, required: true, unique: true },
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
dietaryRestrictions: [{ type: String, required: true }],
events: [{ type: mongoose.Schema.Types.ObjectId, ref: "Event" }],
dietaryRestrictions: { type: [String], required: true },
allergies: { type: [String], required: true },
hasCheckedIn: { type: Boolean, default: false },
points: { type: Number, default: 0 },
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 };
21 changes: 14 additions & 7 deletions src/services/events/events-router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import { EventValidator } from "./events-schema";
import { publicEventValidator } from "./events-schema";
import { Database } from "../../database";

const eventsRouter = Router();
Expand Down Expand Up @@ -28,7 +28,7 @@ eventsRouter.get("/currentOrNext", async (req, res, next) => {

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);
Expand All @@ -40,15 +40,19 @@ 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);

return res.status(StatusCodes.OK).json(filtered_event);
} catch (error) {
next(error);
}
Expand All @@ -57,8 +61,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);
}
Expand Down
12 changes: 10 additions & 2 deletions src/services/events/events-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { v4 as uuidv4 } from "uuid";

export const EventType = z.enum(["A", "B", "C"]);

export const EventValidator = z.object({
eventId: z.coerce.string().optional(),
export const publicEventValidator = z.object({
eventId: z.coerce.string(),
name: z.string(),
startTime: z.coerce.date(),
endTime: z.coerce.date(),
Expand All @@ -17,6 +17,10 @@ export const EventValidator = z.object({
eventType: EventType,
});

export const privateEventValidator = publicEventValidator.extend({
attendanceCount: z.number(),
});

export const EventSchema = new Schema({
eventId: {
type: String,
Expand Down Expand Up @@ -56,6 +60,10 @@ export const EventSchema = new Schema({
type: Boolean,
default: false,
},
attendanceCount: {
type: Number,
default: 0,
},
eventType: {
type: String,
required: true,
Expand Down
2 changes: 2 additions & 0 deletions src/services/registration/registration-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -33,6 +34,7 @@ const RegistrationSchema = new mongoose.Schema({
graduation: { type: String, default: null },
major: { type: String, default: null },
dietaryRestrictions: [{ type: String, required: true }],
allergies: [{ type: String, required: true }],
age: { type: Number, default: null },
gender: { type: String, default: null },
race: [{ type: String }],
Expand Down
195 changes: 195 additions & 0 deletions src/services/stats/stats-router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
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], false),
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], false),
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], false),
async (req, res, next) => {
try {
const attendees = await Database.ATTENDEES.find({
hasCheckedIn: true,
});

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], false),
async (req, res, next) => {
try {
const numEvents = req.params.N;
if (!numEvents) {
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(numEvents));

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 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" });
}
}

type mongoQueryType = {
_id: string;
count: number;
};
const allergyCounts: { [key: string]: number } = {};
const unprocessedAllergyCounts = (
results[4] as PromiseFulfilledResult<mongoQueryType[]>
).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<mongoQueryType[]>
).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: (results[0] as PromiseFulfilledResult<number>).value,
dietaryRestrictions: (
results[1] as PromiseFulfilledResult<number>
).value,
allergies: (results[2] as PromiseFulfilledResult<number>).value,
both: (results[3] as PromiseFulfilledResult<number>).value,
allergyCounts: allergyCounts,
dietaryRestrictionCounts: dietaryRestrictionCounts,
});
} catch (error) {
next(error);
}
}
);

export default statsRouter;

0 comments on commit a4e70a5

Please sign in to comment.