diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..67310a6 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: [.] +strictDepBuilds: false diff --git a/src/db/dbQueryUtils.ts b/src/db/dbQueryUtils.ts index 3639aef..3f5645c 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"; @@ -16,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 }; @@ -31,6 +33,22 @@ export class QueryUtils { this.db = db; } + 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; + } + async getSpecials(todayAsSQLString: string) { const data = await this.db .select() diff --git a/src/db/getLocations.ts b/src/db/getLocations.ts index f6d932e..b53875a 100644 --- a/src/db/getLocations.ts +++ b/src/db/getLocations.ts @@ -7,23 +7,32 @@ 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, undefined); + + 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( ([id, data]) => { @@ -48,6 +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, }; }, ); diff --git a/src/endpoints/auth.ts b/src/endpoints/auth.ts index 8c8d7eb..962b183 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( @@ -23,7 +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 + // prompt: "select_account", // force account picker + 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, @@ -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)", }, - } + }, ); diff --git a/src/endpoints/reviews.ts b/src/endpoints/reviews.ts index 4dbb393..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 } from "drizzle-orm" import { addStarReview, deleteStarReview, @@ -10,7 +9,8 @@ import { updateTagReview, } 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 @@ -57,12 +57,12 @@ reviewEndpoints text: t.Nullable(t.String()), createdAt: t.Number(), updatedAt: t.Number(), - }) + }), ), - }) + }), ), }), - } + }, ) .put( "/v2/locations/:locationId/reviews/stars/me", @@ -75,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", @@ -83,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", @@ -107,7 +107,7 @@ reviewEndpoints voteUp: t.Nullable(t.Boolean()), text: t.Nullable(t.String({ maxLength: 1000 })), }), - } + }, ) .get( "/v2/locations/:locationId/reviews/tags", @@ -126,16 +126,16 @@ reviewEndpoints vote: t.Boolean(), createdAt: t.Number(), updatedAt: t.Number(), - }) + }), ), - } + }, ) .get( "/v2/locations/:locationId/reports", async ({ params: { locationId } }) => { - const reports = await db.select().from(reportsTable).where(eq(reportsTable.locationId, locationId)) + const yesterday = DateTime.now().minus({ days: 1 }); - return reports + return await new QueryUtils(db).getReportsAfter(yesterday, locationId); }, { response: t.Array( @@ -144,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", () => {