From 8560c696a40339012545114e5549e8e747542d15 Mon Sep 17 00:00:00 2001 From: stanleymw <107821509+stanleymw@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:51:28 -0400 Subject: [PATCH 01/10] filter reviews to the lastest 24 hours --- src/endpoints/reviews.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/endpoints/reviews.ts b/src/endpoints/reviews.ts index 4dbb393..e473ef8 100644 --- a/src/endpoints/reviews.ts +++ b/src/endpoints/reviews.ts @@ -1,6 +1,6 @@ import Elysia, { status, t } from "elysia"; import { fetchUserDetails } from "./auth"; -import { eq } from "drizzle-orm" +import { eq, and, gt} from "drizzle-orm" import { addStarReview, deleteStarReview, @@ -133,7 +133,14 @@ reviewEndpoints .get( "/v2/locations/:locationId/reports", async ({ params: { locationId } }) => { - const reports = await db.select().from(reportsTable).where(eq(reportsTable.locationId, locationId)) + const MILLIS_IN_DAY = 60 * 60 * 24 * 1000; + + const reports = await db.select().from(reportsTable).where( + and( + gt(reportsTable.createdAt, new Date(Date.now() - MILLIS_IN_DAY)), + eq(reportsTable.locationId, locationId) + ) + ) return reports }, From 2784749014723a50b185fe76149bf76d1deaa6c7 Mon Sep 17 00:00:00 2001 From: stanleymw <107821509+stanleymw@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:20:11 -0400 Subject: [PATCH 02/10] add report count information in /v2/locations --- src/db/dbQueryUtils.ts | 14 +++++++++++++- src/db/getLocations.ts | 14 ++++++++++++++ src/endpoints/reviews.ts | 13 +++++-------- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/db/dbQueryUtils.ts b/src/db/dbQueryUtils.ts index 3639aef..f5c1944 100644 --- a/src/db/dbQueryUtils.ts +++ b/src/db/dbQueryUtils.ts @@ -1,4 +1,4 @@ -import { avg, count, sql } from "drizzle-orm"; +import { avg, count, gt, sql } from "drizzle-orm"; import { externalIdToInternalIdTable, emailTable, @@ -9,6 +9,7 @@ import { timeOverwritesTable, timesTable, weeklyTimeOverwritesTable, + reportsTable, } from "./schema"; import { DBType } from "./db"; @@ -31,6 +32,17 @@ export class QueryUtils { this.db = db; } + async getReportsAfter(start_time: Date, for_location_id?: string) { + const reports = await this.db.select().from(reportsTable).where( + and( + gt(reportsTable.createdAt, start_time), + for_location_id ? eq(reportsTable.locationId, for_location_id) : undefined + ) + ) + + return reports; + } + async getSpecials(todayAsSQLString: string) { const data = await this.db .select() diff --git a/src/db/getLocations.ts b/src/db/getLocations.ts index f6d932e..44c7676 100644 --- a/src/db/getLocations.ts +++ b/src/db/getLocations.ts @@ -24,6 +24,19 @@ export async function getAllLocationsFromDB(db: DBType, today: DateTime) { ); const [ratingsAvgs, ratingsCounts] = await DB.getRatingsAvgsAndCounts(); + const reports = await DB.getReportsAfter(timeSearchCutoff.toJSDate(), undefined); + + let reportCounts = reports.reduce<{ + [locationId: string]: number + }>( + (acc, currentloc) => { + let amt = acc[currentloc.locationId] ?? 0; + acc[currentloc.locationId] = amt + 1; + return acc + }, + {} + ) + // apply overrides, merge all time intervals, and add specials const finalLocationData = Object.entries(locationIdToData).map( ([id, data]) => { @@ -48,6 +61,7 @@ export async function getAllLocationsFromDB(db: DBType, today: DateTime) { ratingsCount: ratingsCounts[id] ?? 0, todaysSoups: specials[id]?.soups ?? [], todaysSpecials: specials[id]?.specials ?? [], + reportCount: reportCounts[id] ?? 0 }; }, ); diff --git a/src/endpoints/reviews.ts b/src/endpoints/reviews.ts index e473ef8..c928393 100644 --- a/src/endpoints/reviews.ts +++ b/src/endpoints/reviews.ts @@ -11,6 +11,8 @@ import { } from "db/reviews"; import { db } from "db/db"; import { reportsTable } from "db/schema"; +import { QueryUtils } from "db/dbQueryUtils"; +import { DateTime } from "luxon"; export const reviewEndpoints = new Elysia(); reviewEndpoints @@ -133,16 +135,11 @@ reviewEndpoints .get( "/v2/locations/:locationId/reports", async ({ params: { locationId } }) => { - const MILLIS_IN_DAY = 60 * 60 * 24 * 1000; + let yesterday = DateTime.now().minus({days: 1}) - const reports = await db.select().from(reportsTable).where( - and( - gt(reportsTable.createdAt, new Date(Date.now() - MILLIS_IN_DAY)), - eq(reportsTable.locationId, locationId) - ) - ) + let ret = await (new QueryUtils(db)).getReportsAfter(yesterday.toJSDate(), locationId) - return reports + return ret; }, { response: t.Array( From 9e3933f7c5361301e14be63689852f0c53bba066 Mon Sep 17 00:00:00 2001 From: topshenyi-web Date: Mon, 18 May 2026 13:34:36 -0400 Subject: [PATCH 03/10] feat: force cmu.edu login --- src/endpoints/auth.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/endpoints/auth.ts b/src/endpoints/auth.ts index 8c8d7eb..ea869b9 100644 --- a/src/endpoints/auth.ts +++ b/src/endpoints/auth.ts @@ -10,7 +10,7 @@ import cookieSigner from "cookie-signature"; const OIDCConfig = await client.discovery( env.OIDC_SERVER, env.OIDC_CLIENT_ID, - env.OIDC_CLIENT_SECRET + env.OIDC_CLIENT_SECRET, ); export const authEndpoints = new Elysia(); authEndpoints.get( @@ -24,6 +24,7 @@ authEndpoints.get( redirect_uri: `${curOrigin.origin}/code-exchange`, scope: "openid email profile", prompt: "select_account", // force account picker + hd: "cmu.edu", }); return new Response(null, { status: 303, @@ -35,7 +36,7 @@ authEndpoints.get( { query: t.Object({ redirectURL: t.Nullable(t.String()) }), detail: { hide: true }, - } + }, ); authEndpoints.get( "/logout", @@ -54,7 +55,7 @@ authEndpoints.get( { query: t.Object({ redirectURL: t.Nullable(t.String()) }), detail: { hide: true }, - } + }, ); authEndpoints.get( "/code-exchange", @@ -68,8 +69,8 @@ authEndpoints.get( console.error(e); notifySlack( ` OIDC code exchange failed with error ${e} ${JSON.stringify( - e.cause - )} CODE: ${e.code}` + e.cause, + )} CODE: ${e.code}`, ); return undefined; }); @@ -86,7 +87,7 @@ authEndpoints.get( if (sessionId !== undefined) { cookie["session_id"]!.value = cookieSigner.sign( sessionId, - env.SESSION_COOKIE_SIGNING_SECRET + env.SESSION_COOKIE_SIGNING_SECRET, ); cookie["session_id"]!.httpOnly = true; cookie["session_id"]!.secure = true; @@ -108,7 +109,7 @@ authEndpoints.get( }, }); }, - { query: t.Object({ code: t.String() }), detail: { hide: true } } + { query: t.Object({ code: t.String() }), detail: { hide: true } }, ); export async function fetchUserDetails(sessionId?: string) { if (env.HARDCODE_SESSION_FOR_DEV_TESTING) @@ -116,7 +117,7 @@ export async function fetchUserDetails(sessionId?: string) { if (sessionId === undefined) return null; const unsignedSessionId = cookieSigner.unsign( sessionId, - env.SESSION_COOKIE_SIGNING_SECRET + env.SESSION_COOKIE_SIGNING_SECRET, ); if (!unsignedSessionId) return null; return await fetchUserSession(db, unsignedSessionId); @@ -153,12 +154,12 @@ authEndpoints.get( firstName: t.Nullable(t.String()), lastName: t.Nullable(t.String()), pictureUrl: t.Nullable(t.String()), - }) + }), ), }), detail: { description: "If you have an active login session with cmueats, this will return your user info. (this is NOT intended to work cross-site)", }, - } + }, ); From 5716f0be2f60f968bb405aa14fcf053564d6885d Mon Sep 17 00:00:00 2001 From: topshenyi-web Date: Mon, 18 May 2026 15:18:59 -0400 Subject: [PATCH 04/10] fix: allow all postinstall scripts to run --- pnpm-workspace.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 pnpm-workspace.yaml diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..8308d18 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1 @@ +strictDepBuilds: false From f907a9e868a30e3ca9b4e55736d270c9d3d2eab6 Mon Sep 17 00:00:00 2001 From: topshenyi-web Date: Mon, 18 May 2026 15:21:02 -0400 Subject: [PATCH 05/10] fix: pnpm-workspace --- pnpm-workspace.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8308d18..67310a6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1 +1,2 @@ +packages: [.] strictDepBuilds: false From 4004f4288a030a2586bdd49ca45de24cd0201784 Mon Sep 17 00:00:00 2001 From: topshenyi-web Date: Mon, 18 May 2026 15:27:10 -0400 Subject: [PATCH 06/10] fix: force cmu login --- src/endpoints/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/auth.ts b/src/endpoints/auth.ts index ea869b9..293da0b 100644 --- a/src/endpoints/auth.ts +++ b/src/endpoints/auth.ts @@ -23,8 +23,8 @@ authEndpoints.get( const redirectURL = client.buildAuthorizationUrl(OIDCConfig, { redirect_uri: `${curOrigin.origin}/code-exchange`, scope: "openid email profile", - prompt: "select_account", // force account picker - hd: "cmu.edu", + // prompt: "select_account", // force account picker + hd: "andrew.cmu.edu", // idk if this excludes cmu.edu emails... }); return new Response(null, { status: 303, From 0c2f894104ed0642635ec42e8bca365681c305c2 Mon Sep 17 00:00:00 2001 From: topshenyi-web Date: Mon, 18 May 2026 15:36:43 -0400 Subject: [PATCH 07/10] chore: change to cmu.edu --- src/endpoints/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/auth.ts b/src/endpoints/auth.ts index 293da0b..388ac3f 100644 --- a/src/endpoints/auth.ts +++ b/src/endpoints/auth.ts @@ -24,7 +24,7 @@ authEndpoints.get( redirect_uri: `${curOrigin.origin}/code-exchange`, scope: "openid email profile", // prompt: "select_account", // force account picker - hd: "andrew.cmu.edu", // idk if this excludes cmu.edu emails... + hd: "cmu.edu", // idk if this excludes cmu.edu emails... }); return new Response(null, { status: 303, From 836839916ca1d7a1e3f518f9f7d52f462e7aade7 Mon Sep 17 00:00:00 2001 From: topshenyi-web Date: Mon, 18 May 2026 15:41:04 -0400 Subject: [PATCH 08/10] chore: revert back to andrew.cmu.edu --- src/endpoints/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/auth.ts b/src/endpoints/auth.ts index 388ac3f..962b183 100644 --- a/src/endpoints/auth.ts +++ b/src/endpoints/auth.ts @@ -24,7 +24,7 @@ authEndpoints.get( redirect_uri: `${curOrigin.origin}/code-exchange`, scope: "openid email profile", // prompt: "select_account", // force account picker - hd: "cmu.edu", // idk if this excludes cmu.edu emails... + hd: "andrew.cmu.edu", // idk if this excludes cmu.edu emails... (most ppl should see the login.cmu.edu screen though) }); return new Response(null, { status: 303, From e13089d80353df9b4f7cf6ff021f2febb2f95740 Mon Sep 17 00:00:00 2001 From: Eric Xu Date: Mon, 18 May 2026 16:12:50 -0400 Subject: [PATCH 09/10] chore: fix variable casing Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/db/dbQueryUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/db/dbQueryUtils.ts b/src/db/dbQueryUtils.ts index f5c1944..62d31a8 100644 --- a/src/db/dbQueryUtils.ts +++ b/src/db/dbQueryUtils.ts @@ -32,11 +32,11 @@ export class QueryUtils { this.db = db; } - async getReportsAfter(start_time: Date, for_location_id?: string) { + async getReportsAfter(startTime: Date, forLocationId?: string) { const reports = await this.db.select().from(reportsTable).where( and( - gt(reportsTable.createdAt, start_time), - for_location_id ? eq(reportsTable.locationId, for_location_id) : undefined + gt(reportsTable.createdAt, startTime), + forLocationId ? eq(reportsTable.locationId, forLocationId) : undefined ) ) From 348f5e9bfab6687044ec1da22de99ff27d51e771 Mon Sep 17 00:00:00 2001 From: topshenyi-web Date: Mon, 18 May 2026 16:24:05 -0400 Subject: [PATCH 10/10] fix: address some of copilot comments --- src/db/dbQueryUtils.ts | 22 ++++++++++++++-------- src/db/getLocations.ts | 28 ++++++++++++---------------- src/endpoints/reviews.ts | 32 ++++++++++++++------------------ tests/database.test.ts | 1 + 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/db/dbQueryUtils.ts b/src/db/dbQueryUtils.ts index 62d31a8..3f5645c 100644 --- a/src/db/dbQueryUtils.ts +++ b/src/db/dbQueryUtils.ts @@ -17,6 +17,7 @@ import { notifySlack } from "utils/slack"; import { and, eq, gte } from "drizzle-orm"; import { parseTimeSlots } from "containers/timeBuilder"; import { ITimeSlot } from "containers/time/parsedTime"; +import { DateTime } from "luxon"; type RequiredProperty = { [P in keyof T]: NonNullable }; @@ -32,15 +33,20 @@ export class QueryUtils { this.db = db; } - async getReportsAfter(startTime: Date, forLocationId?: string) { - const reports = await this.db.select().from(reportsTable).where( - and( - gt(reportsTable.createdAt, startTime), - forLocationId ? eq(reportsTable.locationId, forLocationId) : undefined - ) - ) + async getReportsAfter(startTime: DateTime, locationId?: string) { + const reports = await this.db + .select() + .from(reportsTable) + .where( + and( + gt(reportsTable.createdAt, startTime.toJSDate()), + locationId !== undefined + ? eq(reportsTable.locationId, locationId) + : undefined, + ), + ); - return reports; + return reports; } async getSpecials(todayAsSQLString: string) { diff --git a/src/db/getLocations.ts b/src/db/getLocations.ts index 44c7676..b53875a 100644 --- a/src/db/getLocations.ts +++ b/src/db/getLocations.ts @@ -7,35 +7,31 @@ import { DBType } from "./db"; /** * * @param db - * @param today this parameter is necessary so we can get today's specials and the open hours for the next 7 days, rather than returning everything we've stored in the db + * @param now this parameter is necessary so we can get today's specials and the open hours for the next 7 days, rather than returning everything we've stored in the db * @returns */ -export async function getAllLocationsFromDB(db: DBType, today: DateTime) { - const timeSearchCutoff = today.minus({ days: 1 }); // 1 days worth of data before today +export async function getAllLocationsFromDB(db: DBType, now: DateTime) { + const timeSearchCutoff = now.minus({ days: 1 }); // 1 days worth of data before today const DB = new QueryUtils(db); const locationIdToData = await DB.getLocationIdToDataMap( timeSearchCutoff.toSQLDate(), ); - const specials = await DB.getSpecials(today.toSQLDate()); + const specials = await DB.getSpecials(now.toSQLDate()); const generalOverrides = await DB.getGeneralOverrides(); const { idToPointOverrides, idToWeeklyOverrides } = await DB.getTimeOverrides( timeSearchCutoff.toSQLDate(), ); const [ratingsAvgs, ratingsCounts] = await DB.getRatingsAvgsAndCounts(); - const reports = await DB.getReportsAfter(timeSearchCutoff.toJSDate(), undefined); + const reports = await DB.getReportsAfter(timeSearchCutoff, undefined); - let reportCounts = reports.reduce<{ - [locationId: string]: number - }>( - (acc, currentloc) => { - let amt = acc[currentloc.locationId] ?? 0; - acc[currentloc.locationId] = amt + 1; - return acc - }, - {} - ) + const reportCounts = reports.reduce<{ + [locationId: string]: number; + }>((acc, currentloc) => { + acc[currentloc.locationId] = (acc[currentloc.locationId] ?? 0) + 1; + return acc; + }, {}); // apply overrides, merge all time intervals, and add specials const finalLocationData = Object.entries(locationIdToData).map( @@ -61,7 +57,7 @@ export async function getAllLocationsFromDB(db: DBType, today: DateTime) { ratingsCount: ratingsCounts[id] ?? 0, todaysSoups: specials[id]?.soups ?? [], todaysSpecials: specials[id]?.specials ?? [], - reportCount: reportCounts[id] ?? 0 + reportCount: reportCounts[id] ?? 0, }; }, ); diff --git a/src/endpoints/reviews.ts b/src/endpoints/reviews.ts index c928393..56674fd 100644 --- a/src/endpoints/reviews.ts +++ b/src/endpoints/reviews.ts @@ -1,6 +1,5 @@ import Elysia, { status, t } from "elysia"; import { fetchUserDetails } from "./auth"; -import { eq, and, gt} from "drizzle-orm" import { addStarReview, deleteStarReview, @@ -10,7 +9,6 @@ import { updateTagReview, } from "db/reviews"; import { db } from "db/db"; -import { reportsTable } from "db/schema"; import { QueryUtils } from "db/dbQueryUtils"; import { DateTime } from "luxon"; @@ -59,12 +57,12 @@ reviewEndpoints text: t.Nullable(t.String()), createdAt: t.Number(), updatedAt: t.Number(), - }) + }), ), - }) + }), ), }), - } + }, ) .put( "/v2/locations/:locationId/reviews/stars/me", @@ -77,7 +75,7 @@ reviewEndpoints body: t.Object({ stars: t.Number({ minimum: 0.5, maximum: 5, multipleOf: 0.5 }), }), - } + }, ) .delete( "/v2/locations/:locationId/reviews/stars/me", @@ -85,7 +83,7 @@ reviewEndpoints if (user === null) throw status("Unauthorized"); await deleteStarReview(db, { locationId, userId: user.id }); return new Response("{}", { status: 200 }); - } + }, ) .put( "/v2/locations/:locationId/reviews/tags/:tagId/me", @@ -109,7 +107,7 @@ reviewEndpoints voteUp: t.Nullable(t.Boolean()), text: t.Nullable(t.String({ maxLength: 1000 })), }), - } + }, ) .get( "/v2/locations/:locationId/reviews/tags", @@ -128,18 +126,16 @@ reviewEndpoints vote: t.Boolean(), createdAt: t.Number(), updatedAt: t.Number(), - }) + }), ), - } + }, ) .get( "/v2/locations/:locationId/reports", async ({ params: { locationId } }) => { - let yesterday = DateTime.now().minus({days: 1}) - - let ret = await (new QueryUtils(db)).getReportsAfter(yesterday.toJSDate(), locationId) + const yesterday = DateTime.now().minus({ days: 1 }); - return ret; + return await new QueryUtils(db).getReportsAfter(yesterday, locationId); }, { response: t.Array( @@ -148,8 +144,8 @@ reviewEndpoints userId: t.Nullable(t.Number()), createdAt: t.Date(), locationId: t.String(), - message: t.String() - }) - ) - } + message: t.String(), + }), + ), + }, ); diff --git a/tests/database.test.ts b/tests/database.test.ts index 99e7868..81d615c 100644 --- a/tests/database.test.ts +++ b/tests/database.test.ts @@ -48,6 +48,7 @@ const locationOut = { todaysSoups: [], todaysSpecials: [], conceptId: "1", + reportCount: 0, }; describe("general location insertion tests", () => {