From d99d4f304f7aaa0c2c21cbe15f2eaa549450e854 Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Wed, 19 Nov 2025 22:12:59 -0500 Subject: [PATCH 01/18] add infra/auth changes --- package.json | 17 ++- prisma/schema.prisma | 31 ++-- src/server.ts | 2 + src/user/user.schema.ts | 19 +++ src/user/userController.ts | 288 +++++++++++++++++++++++++++++++++++++ src/user/userRouter.ts | 61 ++++++++ 6 files changed, 392 insertions(+), 26 deletions(-) create mode 100644 src/user/user.schema.ts create mode 100644 src/user/userController.ts create mode 100644 src/user/userRouter.ts diff --git a/package.json b/package.json index 5fe5bec..44a7d0f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "format": "prettier --write '{src,test}/**/*.ts'", "scrape": "tsx prisma/scraper.ts", "scrape:scheduled": "SCHEDULED_MODE=true node dist/prisma/scraper.js", - "studio": "prisma studio" + "studio": "prisma studio", + "send-notifications": "tsx prisma/send-notifications.ts", + "test-noti-setup": "tsx src/utils/test-notification-setup.ts" }, "repository": { "type": "git", @@ -29,10 +31,13 @@ "homepage": "https://github.com/cuappdev/eatery-blue-backend#readme", "dependencies": { "@prisma/client": "^6.16.2", + "cookie-parser": "^1.4.7", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "dotenv": "^17.2.3", "express": "^5.1.0", "express-rate-limit": "^8.1.0", - "firebase-admin": "^13.5.0", + "firebase-admin": "^13.6.0", "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "node-cache": "^5.1.2", @@ -42,6 +47,7 @@ "devDependencies": { "@eslint/js": "^9.36.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.3", "@types/node": "^24.5.2", "@types/node-cron": "^3.0.11", @@ -52,8 +58,9 @@ "globals": "^15.0.0", "nodemon": "^3.1.10", "prettier": "^3.6.2", - "prisma": "^6.16.2", - "tsx": "^4.20.5", - "typescript": "^5.9.2" + "prisma": "^6.19.0", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", + "typescript": "^5.9.3" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1390f5c..cf6e6ba 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,43 +8,33 @@ datasource db { } enum CampusArea { - EAST WEST NORTH CENTRAL COLLEGETOWN - SOUTH NONE } -enum PaymentMethod { +enum PaymentMethods { MEAL_SWIPE CASH CARD BRB - FREE } enum EateryType { DINING_ROOM CAFE COFFEE_SHOP - FOOD_COURT - CONVENIENCE_STORE - CART - GENERAL } enum EventType { - AVAILABLE_ALL_DAY BREAKFAST BRUNCH - DINNER - EMPTY - LATE_LUNCH LUNCH - OPEN - GENERAL // Used for dummy event for Cafe eateries (ones with no menus) + DINNER + GENERAL + CAFE // Used for dummy event for Cafe eateries (ones with no menus) PANTS // Pants } @@ -98,13 +88,12 @@ model Report { } model Eatery { - id Int @id @default(autoincrement()) - cornellId Int? @unique + id Int @id @default(autoincrement()) + cornellId Int? @unique announcements String[] name String shortName String about String - shortAbout String cornellDining Boolean menuSummary String imageUrl String @@ -115,9 +104,9 @@ model Eatery { latitude Float longitude Float location String - paymentMethods PaymentMethod[] - eateryTypes EateryType[] - createdAt DateTime @default(now()) + paymentMethods PaymentMethods[] + eateryType EateryType + createdAt DateTime @default(now()) events Event[] favoritedEateries FavoritedEatery[] @@ -189,4 +178,4 @@ model DietaryPreference { model Allergen { name String @id @unique items Item[] -} +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 5521469..3b19a19 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,6 +14,7 @@ import { prisma } from './prisma.js'; import userRouter from './users/userRouter.js'; import { cacheRouter } from './utils/cache.js'; import { refreshCacheFromDB } from './utils/cache.js'; +import userRouter from './user/userRouter.js'; const app = express(); @@ -53,6 +54,7 @@ router.get('/health', async (_: Request, res: Response) => { router.use('/auth', authRouter); router.use('/internal/cache', cacheRouter); router.use('/eateries', eateryRouter); +router.use('/user', userRouter); // Protected routes router.use(requireAuth); diff --git a/src/user/user.schema.ts b/src/user/user.schema.ts new file mode 100644 index 0000000..aae185d --- /dev/null +++ b/src/user/user.schema.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +export const fcmTokenSchema = z.object({ + body: z.object({ + token: z.string().nonempty('FCM token is required'), + }), +}); + +export const favoriteItemSchema = z.object({ + body: z.object({ + name: z.string().nonempty('Item name is required'), + }), +}); + +export const favoriteEaterySchema = z.object({ + body: z.object({ + eateryId: z.number().int().positive('eateryId must be a positive integer'), + }), +}); diff --git a/src/user/userController.ts b/src/user/userController.ts new file mode 100644 index 0000000..22719b4 --- /dev/null +++ b/src/user/userController.ts @@ -0,0 +1,288 @@ +import type { EventType, User } from '@prisma/client'; + +import type { NextFunction, Request, Response } from 'express'; + +import { prisma } from '../prisma.js'; +import { BadRequestError, NotFoundError } from '../utils/AppError.js'; +import { getTodayTimeWindow } from '../utils/time.js'; + +/** + * Middleware to extract deviceId from header and attach user to request. + * This is used for all routes that act on behalf of a user. + */ +export const requireUser = async ( + req: Request, + res: Response, + next: NextFunction, +): Promise => { + const deviceId = req.headers['x-device-id'] as string; + + if (!deviceId) { + return next( + new BadRequestError('Device ID header (X-Device-ID) is required.'), + ); + } + + const user = await prisma.user.findUnique({ + where: { deviceId }, + }); + + if (!user) { + return next( + new NotFoundError( + `User with deviceId "${deviceId}" not found. Please register first.`, + ), + ); + } + + res.locals.user = user; + next(); +}; + +/** + * Add or update an FCM token for the user. + * The token itself is unique, so this will update the existing + * record if it's already in the DB, linking it to the current user. + */ +export const addFcmToken = async ( + req: Request, + res: Response, +): Promise => { + const { token } = req.body; + const { user } = res.locals; + + await prisma.fCMToken.upsert({ + where: { token }, + update: { + userId: user.id, + }, + create: { + token, + userId: user.id, + }, + }); + + res.status(200).json({ message: 'Token registered successfully.' }); +}; + +export const removeFcmToken = async ( + req: Request, + res: Response, +): Promise => { + const { token } = req.body; + const { user } = res.locals; + + try { + await prisma.fCMToken.delete({ + where: { + token, + userId: user.id, + }, + }); + res.status(200).json({ message: 'Token removed successfully.' }); + } catch (e) { + res.status(200).json({ message: 'Token removal processed. Error: ' + e }); + } +}; + +export const addFavoriteItem = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { name } = req.body; + const { user } = res.locals as { user: User }; + + await prisma.user.update({ + where: { id: user.id }, + data: { + // Add to the array if it doesn't already exist + favoritedItemNames: { + push: name, + }, + }, + }); + + res.status(200).json({ message: 'Favorite item added.' }); + } catch (error) { + next(error); + } +}; + +export const removeFavoriteItem = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { name } = req.body; + const { user } = res.locals as { user: User }; + + // Filter the name out of the array + const updatedItems = user.favoritedItemNames.filter( + (item) => item !== name, + ); + + await prisma.user.update({ + where: { id: user.id }, + data: { + favoritedItemNames: updatedItems, + }, + }); + + res.status(200).json({ message: 'Favorite item removed.' }); + } catch (error) { + next(error); + } +}; + +export const addFavoriteEatery = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { eateryId } = req.body; + const { user } = res.locals as { user: User }; + + // Use upsert to avoid crashing if the relation already exists + await prisma.favoritedEatery.upsert({ + where: { + userId_eateryId: { + userId: user.id, + eateryId: eateryId, + }, + }, + create: { + userId: user.id, + eateryId: eateryId, + }, + update: {}, + }); + + res.status(200).json({ message: 'Favorite eatery added.' }); + } catch (error) { + next(error); + } +}; + +export const removeFavoriteEatery = async ( + req: Request, + res: Response, + _next: NextFunction, +) => { + try { + const { eateryId } = req.body; + const { user } = res.locals as { user: User }; + + await prisma.favoritedEatery.delete({ + where: { + userId_eateryId: { + userId: user.id, + eateryId: eateryId, + }, + }, + }); + + res.status(200).json({ message: 'Favorite eatery removed.' }); + } catch { + // Don't fail if they try to delete something that's not there + res.status(200).json({ message: 'Favorite eatery removal processed.' }); + } +}; + +/** + * Gets all of a user's favorite items that are being served today + * and the eateries serving them. + */ +export const getFavoriteMatches = async ( + _: Request, + res: Response, + next: NextFunction, +) => { + try { + const { user } = res.locals as { user: User }; + const { favoritedItemNames } = user; + + if (favoritedItemNames.length === 0) { + return res.json({}); + } + + const { start, end } = getTodayTimeWindow(); + + const eateries = await prisma.eatery.findMany({ + where: { + events: { + some: { + startTimestamp: { lte: end }, + endTimestamp: { gte: start }, + menu: { + some: { + items: { + some: { + name: { in: favoritedItemNames }, + }, + }, + }, + }, + }, + }, + }, + include: { + events: { + where: { + startTimestamp: { lte: end }, + endTimestamp: { gte: start }, + }, + include: { + menu: { + include: { + items: { + where: { + name: { in: favoritedItemNames }, + }, + }, + }, + }, + }, + }, + }, + }); + + // Format the response + const matches: Record = {}; + const userFavoritesSet = new Set(favoritedItemNames); + + for (const eatery of eateries) { + // Use a Map to collect all unique events for each item + const itemToEventsMap = new Map>(); + + for (const event of eatery.events) { + for (const category of event.menu) { + for (const item of category.items) { + if (userFavoritesSet.has(item.name)) { + if (!itemToEventsMap.has(item.name)) { + itemToEventsMap.set(item.name, new Set()); + } + itemToEventsMap.get(item.name)!.add(event.type); + } + } + } + } + + if (itemToEventsMap.size > 0) { + matches[eatery.name] = Array.from(itemToEventsMap.entries()).map( + ([itemName, eventSet]) => ({ + name: itemName, + events: Array.from(eventSet).sort(), + }), + ); + } + } + + res.json(matches); + } catch (error) { + return next(error); + } +}; diff --git a/src/user/userRouter.ts b/src/user/userRouter.ts new file mode 100644 index 0000000..afe92f9 --- /dev/null +++ b/src/user/userRouter.ts @@ -0,0 +1,61 @@ +import { Router } from 'express'; + +import { validateRequest } from '../middleware/validateRequest.js'; +import { + favoriteEaterySchema, + favoriteItemSchema, + fcmTokenSchema, +} from './user.schema.js'; +import { + addFavoriteEatery, + addFavoriteItem, + addFcmToken, + getFavoriteMatches, + removeFavoriteEatery, + removeFavoriteItem, + removeFcmToken, + requireUser, +} from './userController.js'; + +const router = Router(); + +router.use(requireUser); + +/** + * @route POST /user/fcm-token + * @desc Add an FCM token for push notifications + * @access Private (requires X-Device-ID) + */ +router.post('/fcm-token', validateRequest(fcmTokenSchema), addFcmToken); + +/** + * @route DELETE /user/fcm-token + * @desc Remove an FCM token to opt-out + * @access Private (requires X-Device-ID) + */ +router.delete('/fcm-token', validateRequest(fcmTokenSchema), removeFcmToken); + +router.post( + '/favorites/items', + validateRequest(favoriteItemSchema), + addFavoriteItem, +); +router.delete( + '/favorites/items', + validateRequest(favoriteItemSchema), + removeFavoriteItem, +); + +router.post( + '/favorites/eateries', + validateRequest(favoriteEaterySchema), + addFavoriteEatery, +); +router.delete( + '/favorites/eateries', + validateRequest(favoriteEaterySchema), + removeFavoriteEatery, +); +router.get('/favorites/matches', getFavoriteMatches); + +export default router; From 7c0ba6734c52cea889deaa816b1a0252d23af91b Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Wed, 19 Nov 2025 22:24:20 -0500 Subject: [PATCH 02/18] add missing files --- package-lock.json | 640 +++++++++++++++------------------------------- prisma/seed.ts | 130 ++++++++++ 2 files changed, 338 insertions(+), 432 deletions(-) create mode 100644 prisma/seed.ts diff --git a/package-lock.json b/package-lock.json index b1508fb..80f81ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,13 @@ "license": "ISC", "dependencies": { "@prisma/client": "^6.16.2", + "cookie-parser": "^1.4.7", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "dotenv": "^17.2.3", "express": "^5.1.0", "express-rate-limit": "^8.1.0", - "firebase-admin": "^13.5.0", + "firebase-admin": "^13.6.0", "helmet": "^8.1.0", "jsonwebtoken": "^9.0.2", "node-cache": "^5.1.2", @@ -23,6 +26,7 @@ "devDependencies": { "@eslint/js": "^9.36.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.3", "@types/node": "^24.5.2", "@types/node-cron": "^3.0.11", @@ -33,9 +37,10 @@ "globals": "^15.0.0", "nodemon": "^3.1.10", "prettier": "^3.6.2", - "prisma": "^6.16.2", - "tsx": "^4.20.5", - "typescript": "^5.9.2" + "prisma": "^6.19.0", + "ts-node": "^10.9.2", + "tsx": "^4.20.6", + "typescript": "^5.9.3" } }, "node_modules/@babel/code-frame": { @@ -164,72 +169,28 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, "node_modules/@esbuild/darwin-arm64": { @@ -249,363 +210,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", @@ -1416,6 +1020,34 @@ } } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1442,6 +1074,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1878,6 +1520,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1944,6 +1599,13 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2276,15 +1938,6 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2372,6 +2025,32 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2387,6 +2066,25 @@ "node": ">= 8" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2454,6 +2152,16 @@ "devOptional": true, "license": "MIT" }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -3924,9 +3632,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -4227,6 +3935,13 @@ "lru-cache": "6.0.0" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5490,6 +5205,50 @@ "typescript": ">=4.8.4" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5609,6 +5368,13 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5752,6 +5518,16 @@ "node": ">=12" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..e70d2fb --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,130 @@ +import { PrismaClient } from '@prisma/client'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const prisma = new PrismaClient(); + +async function main() { + console.log('Starting seed...'); + + // Get admin emails from environment variable + const adminEmailsEnv = process.env.ADMIN_EMAILS || ''; + const adminEmails = adminEmailsEnv + .split(',') + .map((email) => email.trim()) + .filter((email) => email.length > 0); + + if (adminEmails.length === 0) { + console.log('No admin emails found in ADMIN_EMAILS environment variable.'); + console.log('Skipping admin user seeding.'); + } + + console.log(`Found ${adminEmails.length} admin email(s) to seed.`); + + // Seed admin users + for (const email of adminEmails) { + try { + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + + if (existingUser) { + // Make sure the existing user is an admin + if (existingUser.isAdmin !== true) { + await prisma.user.update({ + where: { email }, + data: { isAdmin: true }, + }); + console.log(`✓ Updated user to admin: ${email} (ID: ${existingUser.id})`); + } else { + console.log(`✓ Admin user already exists: ${email} (ID: ${existingUser.id})`); + } + continue; + } + + const user = await prisma.user.create({ + data: { + email, + isAdmin: true, + // firebaseId and teamId are null until the user logs in + }, + }); + + console.log(`✓ Created admin user: ${email} (ID: ${user.id})`); + } catch (error) { + console.error(`✗ Failed to create admin user ${email}:`, error); + } + } + + try { + const testEatery = await prisma.eatery.upsert({ + // Use a unique cornellId to find the eatery + where: { cornellId: 9999 }, + // If it exists, do nothing + update: {}, + // If it doesn't exist, create it with all required fields + create: { + cornellId: 9999, + name: 'Test Eatery', + shortName: 'Test', + about: 'A simple eatery for testing.', + cornellDining: false, + menuSummary: 'Test items', + imageUrl: 'https://placehold.co/600x400/ccc/fff?text=Test+Eatery', + campusArea: 'CENTRAL', + location: 'Ho Plaza', + latitude: 42.4475, + longitude: -76.483, + paymentMethods: ['CASH', 'CARD'], + eateryType: 'CAFE', + events: { + create: [ + { + type: 'GENERAL', + startTimestamp: new Date('2025-11-12T09:00:00-05:00'), // Nov 12, 2025 9:00 AM + endTimestamp: new Date('2025-12-12T17:00:00-05:00'), // Dec 12, 2025 5:00 PM + menu: { + create: [ + { + name: 'Main Menu', + items: { + create: [ + { + name: 'Pizza', + basePrice: 10.99, + }, + { + name: 'Matcha', + basePrice: 4.5, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + }); + console.log( + `Created or found test eatery: ${testEatery.name} (ID: ${testEatery.id})`, + ); + } catch (error) { + console.error('Failed to create test eatery:', error); + } + + console.log('Seed completed!'); +} + + + +main() + .catch((e) => { + console.error('Error during seeding:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); From 116611635ec26b08aa5e64b371fdca6beb9f7406 Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Wed, 19 Nov 2025 22:46:00 -0500 Subject: [PATCH 03/18] fix merge issues --- package.json | 14 +++-- prisma/schema.prisma | 29 +++++++--- prisma/seed.ts | 130 ------------------------------------------- 3 files changed, 30 insertions(+), 143 deletions(-) delete mode 100644 prisma/seed.ts diff --git a/package.json b/package.json index 44a7d0f..dd6f15d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,11 @@ "lint": "eslint '{src,test}/**/*.ts' --fix", "format": "prettier --write '{src,test}/**/*.ts'", "scrape": "tsx prisma/scraper.ts", +<<<<<<< HEAD "scrape:scheduled": "SCHEDULED_MODE=true node dist/prisma/scraper.js", +======= + "scrape:scheduled": "SCHEDULED_MODE=true tsx prisma/scraper.ts", +>>>>>>> 5487c7c (fix merge issues) "studio": "prisma studio", "send-notifications": "tsx prisma/send-notifications.ts", "test-noti-setup": "tsx src/utils/test-notification-setup.ts" @@ -35,6 +39,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^17.2.3", + "escape-html": "^1.0.3", "express": "^5.1.0", "express-rate-limit": "^8.1.0", "firebase-admin": "^13.6.0", @@ -42,12 +47,14 @@ "jsonwebtoken": "^9.0.2", "node-cache": "^5.1.2", "node-cron": "^4.2.1", + "node-cache": "^5.1.2", "zod": "^4.1.12" }, "devDependencies": { "@eslint/js": "^9.36.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/cookie-parser": "^1.4.9", + "@types/escape-html": "^1.0.4", "@types/express": "^5.0.3", "@types/node": "^24.5.2", "@types/node-cron": "^3.0.11", @@ -58,9 +65,8 @@ "globals": "^15.0.0", "nodemon": "^3.1.10", "prettier": "^3.6.2", - "prisma": "^6.19.0", - "ts-node": "^10.9.2", - "tsx": "^4.20.6", - "typescript": "^5.9.3" + "prisma": "^6.16.2", + "tsx": "^4.20.5", + "typescript": "^5.9.2" } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cf6e6ba..d0c9f55 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,33 +8,43 @@ datasource db { } enum CampusArea { + EAST WEST NORTH CENTRAL COLLEGETOWN + SOUTH NONE } -enum PaymentMethods { +enum PaymentMethod { MEAL_SWIPE CASH CARD BRB + FREE } enum EateryType { DINING_ROOM CAFE COFFEE_SHOP + FOOD_COURT + CONVENIENCE_STORE + CART + GENERAL } enum EventType { + AVAILABLE_ALL_DAY BREAKFAST BRUNCH - LUNCH DINNER - GENERAL - CAFE // Used for dummy event for Cafe eateries (ones with no menus) + EMPTY + LATE_LUNCH + LUNCH + OPEN + GENERAL // Used for dummy event for Cafe eateries (ones with no menus) PANTS // Pants } @@ -88,12 +98,13 @@ model Report { } model Eatery { - id Int @id @default(autoincrement()) - cornellId Int? @unique + id Int @id @default(autoincrement()) + cornellId Int? @unique announcements String[] name String shortName String about String + shortAbout String cornellDining Boolean menuSummary String imageUrl String @@ -104,9 +115,9 @@ model Eatery { latitude Float longitude Float location String - paymentMethods PaymentMethods[] - eateryType EateryType - createdAt DateTime @default(now()) + paymentMethods PaymentMethod[] + eateryTypes EateryType[] + createdAt DateTime @default(now()) events Event[] favoritedEateries FavoritedEatery[] diff --git a/prisma/seed.ts b/prisma/seed.ts deleted file mode 100644 index e70d2fb..0000000 --- a/prisma/seed.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import dotenv from 'dotenv'; - -dotenv.config(); - -const prisma = new PrismaClient(); - -async function main() { - console.log('Starting seed...'); - - // Get admin emails from environment variable - const adminEmailsEnv = process.env.ADMIN_EMAILS || ''; - const adminEmails = adminEmailsEnv - .split(',') - .map((email) => email.trim()) - .filter((email) => email.length > 0); - - if (adminEmails.length === 0) { - console.log('No admin emails found in ADMIN_EMAILS environment variable.'); - console.log('Skipping admin user seeding.'); - } - - console.log(`Found ${adminEmails.length} admin email(s) to seed.`); - - // Seed admin users - for (const email of adminEmails) { - try { - const existingUser = await prisma.user.findUnique({ - where: { email }, - }); - - if (existingUser) { - // Make sure the existing user is an admin - if (existingUser.isAdmin !== true) { - await prisma.user.update({ - where: { email }, - data: { isAdmin: true }, - }); - console.log(`✓ Updated user to admin: ${email} (ID: ${existingUser.id})`); - } else { - console.log(`✓ Admin user already exists: ${email} (ID: ${existingUser.id})`); - } - continue; - } - - const user = await prisma.user.create({ - data: { - email, - isAdmin: true, - // firebaseId and teamId are null until the user logs in - }, - }); - - console.log(`✓ Created admin user: ${email} (ID: ${user.id})`); - } catch (error) { - console.error(`✗ Failed to create admin user ${email}:`, error); - } - } - - try { - const testEatery = await prisma.eatery.upsert({ - // Use a unique cornellId to find the eatery - where: { cornellId: 9999 }, - // If it exists, do nothing - update: {}, - // If it doesn't exist, create it with all required fields - create: { - cornellId: 9999, - name: 'Test Eatery', - shortName: 'Test', - about: 'A simple eatery for testing.', - cornellDining: false, - menuSummary: 'Test items', - imageUrl: 'https://placehold.co/600x400/ccc/fff?text=Test+Eatery', - campusArea: 'CENTRAL', - location: 'Ho Plaza', - latitude: 42.4475, - longitude: -76.483, - paymentMethods: ['CASH', 'CARD'], - eateryType: 'CAFE', - events: { - create: [ - { - type: 'GENERAL', - startTimestamp: new Date('2025-11-12T09:00:00-05:00'), // Nov 12, 2025 9:00 AM - endTimestamp: new Date('2025-12-12T17:00:00-05:00'), // Dec 12, 2025 5:00 PM - menu: { - create: [ - { - name: 'Main Menu', - items: { - create: [ - { - name: 'Pizza', - basePrice: 10.99, - }, - { - name: 'Matcha', - basePrice: 4.5, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - }); - console.log( - `Created or found test eatery: ${testEatery.name} (ID: ${testEatery.id})`, - ); - } catch (error) { - console.error('Failed to create test eatery:', error); - } - - console.log('Seed completed!'); -} - - - -main() - .catch((e) => { - console.error('Error during seeding:', e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); From c88af6e5310dbaf17c049be763ed338f4d2c81e4 Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Wed, 19 Nov 2025 23:20:09 -0500 Subject: [PATCH 04/18] add time helper --- src/utils/time.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/utils/time.ts diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..5d22c89 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,32 @@ +import { addHours, endOfDay, getUnixTime, startOfDay } from 'date-fns'; + +/** + * Gets the start and end of the current day in a specific timezone. + */ +export function getTodayTimeWindow(timeZone: string = 'America/New_York') { + const zonedNow = new Date(new Date().toLocaleString('en-US', { timeZone })); + + const start = startOfDay(zonedNow); + const end = endOfDay(zonedNow); + + return { start, end }; +} + +export function getQueryTimeWindow(): { + windowStartUnix: number; + windowEndUnix: number; +} { + const timeZone = 'America/New_York'; + + // How many hours ahead to look for events + const LOOKAHEAD_HOURS = 7; + + const zonedNow = new Date(new Date().toLocaleString('en-US', { timeZone })); + const start = zonedNow; + const end = addHours(start, LOOKAHEAD_HOURS); + + return { + windowStartUnix: getUnixTime(start), + windowEndUnix: getUnixTime(end), + }; +} From 8f1c0395dcdbbe7231e520df8272184136b94c9a Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Wed, 19 Nov 2025 23:54:48 -0500 Subject: [PATCH 05/18] switch to /users --- package-lock.json | 182 ++--------- src/server.ts | 2 - src/user/userController.ts | 288 ------------------ src/user/userRouter.ts | 61 ---- src/users/userController.ts | 253 ++++++++++++++- src/users/userRouter.ts | 40 +++ .../user.schema.ts => users/users.schema.ts} | 0 7 files changed, 313 insertions(+), 513 deletions(-) delete mode 100644 src/user/userController.ts delete mode 100644 src/user/userRouter.ts rename src/{user/user.schema.ts => users/users.schema.ts} (100%) diff --git a/package-lock.json b/package-lock.json index 80f81ee..ac9d074 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^17.2.3", + "escape-html": "^1.0.3", "express": "^5.1.0", "express-rate-limit": "^8.1.0", "firebase-admin": "^13.6.0", @@ -27,6 +28,7 @@ "@eslint/js": "^9.36.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", "@types/cookie-parser": "^1.4.9", + "@types/escape-html": "^1.0.4", "@types/express": "^5.0.3", "@types/node": "^24.5.2", "@types/node-cron": "^3.0.11", @@ -37,10 +39,9 @@ "globals": "^15.0.0", "nodemon": "^3.1.10", "prettier": "^3.6.2", - "prisma": "^6.19.0", - "ts-node": "^10.9.2", - "tsx": "^4.20.6", - "typescript": "^5.9.3" + "prisma": "^6.16.2", + "tsx": "^4.20.5", + "typescript": "^5.9.2" } }, "node_modules/@babel/code-frame": { @@ -169,30 +170,6 @@ "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", @@ -1020,34 +997,6 @@ } } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1084,6 +1033,13 @@ "@types/express": "*" } }, + "node_modules/@types/escape-html": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", + "integrity": "sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1520,19 +1476,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -1599,13 +1542,6 @@ "node": ">= 8" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1938,6 +1874,15 @@ "node": ">=12" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2044,13 +1989,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2152,16 +2090,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -3935,13 +3863,6 @@ "lru-cache": "6.0.0" } }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true, - "license": "ISC" - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5205,50 +5126,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5368,13 +5245,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5518,16 +5388,6 @@ "node": ">=12" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/src/server.ts b/src/server.ts index 3b19a19..5521469 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,7 +14,6 @@ import { prisma } from './prisma.js'; import userRouter from './users/userRouter.js'; import { cacheRouter } from './utils/cache.js'; import { refreshCacheFromDB } from './utils/cache.js'; -import userRouter from './user/userRouter.js'; const app = express(); @@ -54,7 +53,6 @@ router.get('/health', async (_: Request, res: Response) => { router.use('/auth', authRouter); router.use('/internal/cache', cacheRouter); router.use('/eateries', eateryRouter); -router.use('/user', userRouter); // Protected routes router.use(requireAuth); diff --git a/src/user/userController.ts b/src/user/userController.ts deleted file mode 100644 index 22719b4..0000000 --- a/src/user/userController.ts +++ /dev/null @@ -1,288 +0,0 @@ -import type { EventType, User } from '@prisma/client'; - -import type { NextFunction, Request, Response } from 'express'; - -import { prisma } from '../prisma.js'; -import { BadRequestError, NotFoundError } from '../utils/AppError.js'; -import { getTodayTimeWindow } from '../utils/time.js'; - -/** - * Middleware to extract deviceId from header and attach user to request. - * This is used for all routes that act on behalf of a user. - */ -export const requireUser = async ( - req: Request, - res: Response, - next: NextFunction, -): Promise => { - const deviceId = req.headers['x-device-id'] as string; - - if (!deviceId) { - return next( - new BadRequestError('Device ID header (X-Device-ID) is required.'), - ); - } - - const user = await prisma.user.findUnique({ - where: { deviceId }, - }); - - if (!user) { - return next( - new NotFoundError( - `User with deviceId "${deviceId}" not found. Please register first.`, - ), - ); - } - - res.locals.user = user; - next(); -}; - -/** - * Add or update an FCM token for the user. - * The token itself is unique, so this will update the existing - * record if it's already in the DB, linking it to the current user. - */ -export const addFcmToken = async ( - req: Request, - res: Response, -): Promise => { - const { token } = req.body; - const { user } = res.locals; - - await prisma.fCMToken.upsert({ - where: { token }, - update: { - userId: user.id, - }, - create: { - token, - userId: user.id, - }, - }); - - res.status(200).json({ message: 'Token registered successfully.' }); -}; - -export const removeFcmToken = async ( - req: Request, - res: Response, -): Promise => { - const { token } = req.body; - const { user } = res.locals; - - try { - await prisma.fCMToken.delete({ - where: { - token, - userId: user.id, - }, - }); - res.status(200).json({ message: 'Token removed successfully.' }); - } catch (e) { - res.status(200).json({ message: 'Token removal processed. Error: ' + e }); - } -}; - -export const addFavoriteItem = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { name } = req.body; - const { user } = res.locals as { user: User }; - - await prisma.user.update({ - where: { id: user.id }, - data: { - // Add to the array if it doesn't already exist - favoritedItemNames: { - push: name, - }, - }, - }); - - res.status(200).json({ message: 'Favorite item added.' }); - } catch (error) { - next(error); - } -}; - -export const removeFavoriteItem = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { name } = req.body; - const { user } = res.locals as { user: User }; - - // Filter the name out of the array - const updatedItems = user.favoritedItemNames.filter( - (item) => item !== name, - ); - - await prisma.user.update({ - where: { id: user.id }, - data: { - favoritedItemNames: updatedItems, - }, - }); - - res.status(200).json({ message: 'Favorite item removed.' }); - } catch (error) { - next(error); - } -}; - -export const addFavoriteEatery = async ( - req: Request, - res: Response, - next: NextFunction, -) => { - try { - const { eateryId } = req.body; - const { user } = res.locals as { user: User }; - - // Use upsert to avoid crashing if the relation already exists - await prisma.favoritedEatery.upsert({ - where: { - userId_eateryId: { - userId: user.id, - eateryId: eateryId, - }, - }, - create: { - userId: user.id, - eateryId: eateryId, - }, - update: {}, - }); - - res.status(200).json({ message: 'Favorite eatery added.' }); - } catch (error) { - next(error); - } -}; - -export const removeFavoriteEatery = async ( - req: Request, - res: Response, - _next: NextFunction, -) => { - try { - const { eateryId } = req.body; - const { user } = res.locals as { user: User }; - - await prisma.favoritedEatery.delete({ - where: { - userId_eateryId: { - userId: user.id, - eateryId: eateryId, - }, - }, - }); - - res.status(200).json({ message: 'Favorite eatery removed.' }); - } catch { - // Don't fail if they try to delete something that's not there - res.status(200).json({ message: 'Favorite eatery removal processed.' }); - } -}; - -/** - * Gets all of a user's favorite items that are being served today - * and the eateries serving them. - */ -export const getFavoriteMatches = async ( - _: Request, - res: Response, - next: NextFunction, -) => { - try { - const { user } = res.locals as { user: User }; - const { favoritedItemNames } = user; - - if (favoritedItemNames.length === 0) { - return res.json({}); - } - - const { start, end } = getTodayTimeWindow(); - - const eateries = await prisma.eatery.findMany({ - where: { - events: { - some: { - startTimestamp: { lte: end }, - endTimestamp: { gte: start }, - menu: { - some: { - items: { - some: { - name: { in: favoritedItemNames }, - }, - }, - }, - }, - }, - }, - }, - include: { - events: { - where: { - startTimestamp: { lte: end }, - endTimestamp: { gte: start }, - }, - include: { - menu: { - include: { - items: { - where: { - name: { in: favoritedItemNames }, - }, - }, - }, - }, - }, - }, - }, - }); - - // Format the response - const matches: Record = {}; - const userFavoritesSet = new Set(favoritedItemNames); - - for (const eatery of eateries) { - // Use a Map to collect all unique events for each item - const itemToEventsMap = new Map>(); - - for (const event of eatery.events) { - for (const category of event.menu) { - for (const item of category.items) { - if (userFavoritesSet.has(item.name)) { - if (!itemToEventsMap.has(item.name)) { - itemToEventsMap.set(item.name, new Set()); - } - itemToEventsMap.get(item.name)!.add(event.type); - } - } - } - } - - if (itemToEventsMap.size > 0) { - matches[eatery.name] = Array.from(itemToEventsMap.entries()).map( - ([itemName, eventSet]) => ({ - name: itemName, - events: Array.from(eventSet).sort(), - }), - ); - } - } - - res.json(matches); - } catch (error) { - return next(error); - } -}; diff --git a/src/user/userRouter.ts b/src/user/userRouter.ts deleted file mode 100644 index afe92f9..0000000 --- a/src/user/userRouter.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Router } from 'express'; - -import { validateRequest } from '../middleware/validateRequest.js'; -import { - favoriteEaterySchema, - favoriteItemSchema, - fcmTokenSchema, -} from './user.schema.js'; -import { - addFavoriteEatery, - addFavoriteItem, - addFcmToken, - getFavoriteMatches, - removeFavoriteEatery, - removeFavoriteItem, - removeFcmToken, - requireUser, -} from './userController.js'; - -const router = Router(); - -router.use(requireUser); - -/** - * @route POST /user/fcm-token - * @desc Add an FCM token for push notifications - * @access Private (requires X-Device-ID) - */ -router.post('/fcm-token', validateRequest(fcmTokenSchema), addFcmToken); - -/** - * @route DELETE /user/fcm-token - * @desc Remove an FCM token to opt-out - * @access Private (requires X-Device-ID) - */ -router.delete('/fcm-token', validateRequest(fcmTokenSchema), removeFcmToken); - -router.post( - '/favorites/items', - validateRequest(favoriteItemSchema), - addFavoriteItem, -); -router.delete( - '/favorites/items', - validateRequest(favoriteItemSchema), - removeFavoriteItem, -); - -router.post( - '/favorites/eateries', - validateRequest(favoriteEaterySchema), - addFavoriteEatery, -); -router.delete( - '/favorites/eateries', - validateRequest(favoriteEaterySchema), - removeFavoriteEatery, -); -router.get('/favorites/matches', getFavoriteMatches); - -export default router; diff --git a/src/users/userController.ts b/src/users/userController.ts index e5c39b7..a93323f 100644 --- a/src/users/userController.ts +++ b/src/users/userController.ts @@ -1,6 +1,9 @@ -import type { Request, Response } from 'express'; +import type { EventType, User } from '@prisma/client'; + +import type { NextFunction, Request, Response } from 'express'; import { prisma } from '../prisma.js'; +import { getTodayTimeWindow } from '../utils/time.js'; export const getMe = async (req: Request, res: Response) => { const { userId } = req.user!; @@ -20,3 +23,251 @@ export const getMe = async (req: Request, res: Response) => { return res.json({ user }); }; + +/** + * Add or update an FCM token for the user. + * The token itself is unique, so this will update the existing + * record if it's already in the DB, linking it to the current user. + */ +export const addFcmToken = async ( + req: Request, + res: Response, +): Promise => { + const { token } = req.body; + const { user } = res.locals; + + await prisma.fCMToken.upsert({ + where: { token }, + update: { + userId: user.id, + }, + create: { + token, + userId: user.id, + }, + }); + + res.status(200).json({ message: 'Token registered successfully.' }); +}; + +export const removeFcmToken = async ( + req: Request, + res: Response, +): Promise => { + const { token } = req.body; + const { user } = res.locals; + + try { + await prisma.fCMToken.delete({ + where: { + token, + userId: user.id, + }, + }); + res.status(200).json({ message: 'Token removed successfully.' }); + } catch (e) { + res.status(200).json({ message: 'Token removal processed. Error: ' + e }); + } +}; + +export const addFavoriteItem = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { name } = req.body; + const { user } = res.locals as { user: User }; + + await prisma.user.update({ + where: { id: user.id }, + data: { + // Add to the array if it doesn't already exist + favoritedItemNames: { + push: name, + }, + }, + }); + + res.status(200).json({ message: 'Favorite item added.' }); + } catch (error) { + next(error); + } +}; + +export const removeFavoriteItem = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { name } = req.body; + const { user } = res.locals as { user: User }; + + // Filter the name out of the array + const updatedItems = user.favoritedItemNames.filter( + (item) => item !== name, + ); + + await prisma.user.update({ + where: { id: user.id }, + data: { + favoritedItemNames: updatedItems, + }, + }); + + res.status(200).json({ message: 'Favorite item removed.' }); + } catch (error) { + next(error); + } +}; + +export const addFavoriteEatery = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const { eateryId } = req.body; + const { user } = res.locals as { user: User }; + + // Use upsert to avoid crashing if the relation already exists + await prisma.favoritedEatery.upsert({ + where: { + userId_eateryId: { + userId: user.id, + eateryId: eateryId, + }, + }, + create: { + userId: user.id, + eateryId: eateryId, + }, + update: {}, + }); + + res.status(200).json({ message: 'Favorite eatery added.' }); + } catch (error) { + next(error); + } +}; + +export const removeFavoriteEatery = async ( + req: Request, + res: Response, + _next: NextFunction, +) => { + try { + const { eateryId } = req.body; + const { user } = res.locals as { user: User }; + + await prisma.favoritedEatery.delete({ + where: { + userId_eateryId: { + userId: user.id, + eateryId: eateryId, + }, + }, + }); + + res.status(200).json({ message: 'Favorite eatery removed.' }); + } catch { + // Don't fail if they try to delete something that's not there + res.status(200).json({ message: 'Favorite eatery removal processed.' }); + } +}; + +/** + * Gets all of a user's favorite items that are being served today + * and the eateries serving them. + */ +export const getFavoriteMatches = async ( + _: Request, + res: Response, + next: NextFunction, +) => { + try { + const { user } = res.locals as { user: User }; + const { favoritedItemNames } = user; + + if (favoritedItemNames.length === 0) { + return res.json({}); + } + + const { start, end } = getTodayTimeWindow(); + + const eateries = await prisma.eatery.findMany({ + where: { + events: { + some: { + startTimestamp: { lte: end }, + endTimestamp: { gte: start }, + menu: { + some: { + items: { + some: { + name: { in: favoritedItemNames }, + }, + }, + }, + }, + }, + }, + }, + include: { + events: { + where: { + startTimestamp: { lte: end }, + endTimestamp: { gte: start }, + }, + include: { + menu: { + include: { + items: { + where: { + name: { in: favoritedItemNames }, + }, + }, + }, + }, + }, + }, + }, + }); + + // Format the response + const matches: Record = {}; + const userFavoritesSet = new Set(favoritedItemNames); + + for (const eatery of eateries) { + // Use a Map to collect all unique events for each item + const itemToEventsMap = new Map>(); + + for (const event of eatery.events) { + for (const category of event.menu) { + for (const item of category.items) { + if (userFavoritesSet.has(item.name)) { + if (!itemToEventsMap.has(item.name)) { + itemToEventsMap.set(item.name, new Set()); + } + itemToEventsMap.get(item.name)!.add(event.type); + } + } + } + } + + if (itemToEventsMap.size > 0) { + matches[eatery.name] = Array.from(itemToEventsMap.entries()).map( + ([itemName, eventSet]) => ({ + name: itemName, + events: Array.from(eventSet).sort(), + }), + ); + } + } + + res.json(matches); + } catch (error) { + return next(error); + } +}; diff --git a/src/users/userRouter.ts b/src/users/userRouter.ts index 2e58075..b249227 100644 --- a/src/users/userRouter.ts +++ b/src/users/userRouter.ts @@ -1,9 +1,49 @@ import { Router } from 'express'; +import { validateRequest } from '../middleware/validateRequest.js'; +import { + addFavoriteEatery, + addFavoriteItem, + addFcmToken, + getFavoriteMatches, + removeFavoriteEatery, + removeFavoriteItem, + removeFcmToken, +} from './userController.js'; import { getMe } from './userController.js'; +import { + favoriteEaterySchema, + favoriteItemSchema, + fcmTokenSchema, +} from './users.schema.js'; const router = Router(); router.get('/me', getMe); +router.post('/fcm-token', validateRequest(fcmTokenSchema), addFcmToken); +router.delete('/fcm-token', validateRequest(fcmTokenSchema), removeFcmToken); + +router.post( + '/favorites/items', + validateRequest(favoriteItemSchema), + addFavoriteItem, +); +router.delete( + '/favorites/items', + validateRequest(favoriteItemSchema), + removeFavoriteItem, +); + +router.post( + '/favorites/eateries', + validateRequest(favoriteEaterySchema), + addFavoriteEatery, +); +router.delete( + '/favorites/eateries', + validateRequest(favoriteEaterySchema), + removeFavoriteEatery, +); +router.get('/favorites/matches', getFavoriteMatches); export default router; diff --git a/src/user/user.schema.ts b/src/users/users.schema.ts similarity index 100% rename from src/user/user.schema.ts rename to src/users/users.schema.ts From f6cf91ccda91400762157cf2f2023d956bd359fd Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Thu, 20 Nov 2025 02:19:35 -0500 Subject: [PATCH 06/18] update to new jwt auth --- prisma/send-notifications.ts | 187 +++++++++++++++++ src/auth/auth.schema.ts | 13 ++ src/auth/authController.ts | 51 ++++- src/auth/authRouter.ts | 22 +- src/financials/financialsController.ts | 32 +++ src/financials/financialsRouter.ts | 9 + src/firebase.ts | 132 ++++++++++-- src/server.ts | 3 + src/services/cbord.service.ts | 265 +++++++++++++++++++++++++ src/users/userController.ts | 79 ++++++-- src/utils/test-notification-setup.ts | 86 ++++++++ 11 files changed, 837 insertions(+), 42 deletions(-) create mode 100644 prisma/send-notifications.ts create mode 100644 src/financials/financialsController.ts create mode 100644 src/financials/financialsRouter.ts create mode 100644 src/services/cbord.service.ts create mode 100644 src/utils/test-notification-setup.ts diff --git a/prisma/send-notifications.ts b/prisma/send-notifications.ts new file mode 100644 index 0000000..e534942 --- /dev/null +++ b/prisma/send-notifications.ts @@ -0,0 +1,187 @@ +import { firebaseService, prisma } from '../src/firebase.js'; +import { getQueryTimeWindow } from '../src/utils/time.js'; +import cron from 'node-cron'; + +function buildMessage( + matchesByEatery: Map, +): { title: string; body: string } { + const title = 'Some of your favorites are being served today!'; + const eateryNames = Array.from(matchesByEatery.keys()); + + if (eateryNames.length === 1) { + const eateryName = eateryNames[0]; + const items = matchesByEatery.get(eateryName)!; + if (items.length === 1) { + return { title, body: `${items[0]} is being served at ${eateryName} today.` }; + } else if (items.length === 2) { + return { + title, + body: `${items[0]} and ${items[1]} are at ${eateryName} today.`, + }; + } else { + return { title, body: `Several favorites are at ${eateryName} today.` }; + } + } else { + const eateryListStr = eateryNames.join(', '); + return { + title, + body: `Favorites found at ${eateryListStr} today. Check the app for details!`, + }; + } +} + +export async function main() { + const { windowStartUnix, windowEndUnix } = getQueryTimeWindow(); + + // build a map of { eateryName: Set } + const eateryMenuMap = new Map>(); + const allItemNamesToday = new Set(); + + const eateries = await prisma.eatery.findMany({ + include: { + events: { + where: { + startTimestamp: { lte: new Date(windowEndUnix * 1000) }, + endTimestamp: { gte: new Date(windowStartUnix * 1000) }, + }, + include: { + menu: { + include: { + items: true, + }, + }, + }, + }, + }, + }); + + for (const eatery of eateries) { + if (!eateryMenuMap.has(eatery.name)) { + eateryMenuMap.set(eatery.name, new Set()); + } + const itemSet = eateryMenuMap.get(eatery.name)!; + for (const event of eatery.events) { + for (const category of event.menu) { + for (const item of category.items) { + itemSet.add(item.name); + allItemNamesToday.add(item.name); + } + } + } + } + + if (allItemNamesToday.size === 0) { + console.log('No items found'); + return; + } + + // Get all users with at least one favorite + // item being served today (using the GIN index). + const usersToNotify = await prisma.user.findMany({ + where: { + favoritedItemNames: { + hasSome: Array.from(allItemNamesToday), + }, + fcmTokens: { + some: {}, + }, + }, + include: { + fcmTokens: true, + }, + }); + + if (usersToNotify.length === 0) { + console.log('No users to notify'); + return; + } + + // Loop through filtered users and build their aggregated notification + for (const user of usersToNotify) { + const userFavorites = new Set(user.favoritedItemNames); + const userMatchesByEatery = new Map(); + + for (const [eateryName, itemSet] of eateryMenuMap.entries()) { + const matches = Array.from(itemSet).filter((itemName) => + userFavorites.has(itemName), + ); + if (matches.length > 0) { + userMatchesByEatery.set(eateryName, matches.sort()); + } + } + + if (userMatchesByEatery.size > 0) { + const { title, body } = buildMessage(userMatchesByEatery); + const tokens = user.fcmTokens.map((t) => t.token); + + const dataPayload = { + matches: JSON.stringify(Object.fromEntries(userMatchesByEatery)), + }; + + try { + await firebaseService.sendToTokens(tokens, title, body, dataPayload); + } catch (e) { + console.error(`Failed to send notification for user ${user.id}:`, e); + } + } + } +} + +let isRunning = false; + +async function runNotificationsSafely() { + if (isRunning) { + console.log('[Notifications] Job is already running, skipping.'); + return; + } + + isRunning = true; + console.log(`[Notifications] Starting run at ${new Date().toISOString()}`); + + try { + await main(); + console.log('[Notifications] Completed successfully.'); + } catch (error) { + console.error('[Notifications] Failed:', error); + } finally { + isRunning = false; + } +} + +export function startNotificationScheduler() { + const cronExpression = process.env.NOTI_CRON_SCHEDULE || '0 8,17 * * *'; + + console.log('[Notifications] Initializing scheduler...'); + console.log(`[Notifications] Schedule: ${cronExpression}`); + + const task = cron.schedule(cronExpression, runNotificationsSafely, { + timezone: 'America/New_York', + }); + + console.log('[Notifications] Scheduler started.'); + + return task; +} + + +if (process.env.SCHEDULED_MODE === 'true') { + startNotificationScheduler(); + console.log('[Notifications] Notification scheduler is running. Press Ctrl+C to stop.'); + const gracefulShutdown = async () => { + console.log('[Notifications] Shutting down gracefully...'); + await prisma.$disconnect(); + process.exit(0); + }; + + process.on('SIGTERM', gracefulShutdown); + process.on('SIGINT', gracefulShutdown); +} else { + main() + .catch((e) => { + console.error('Error during scraping:', e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); +} \ No newline at end of file diff --git a/src/auth/auth.schema.ts b/src/auth/auth.schema.ts index 4518a5a..ebf5aee 100644 --- a/src/auth/auth.schema.ts +++ b/src/auth/auth.schema.ts @@ -11,3 +11,16 @@ export const refreshAccessTokenSchema = z.object({ refreshToken: z.string().nonempty('Refresh token is required'), }), }); + +export const getAuthorizeSchema = z.object({ + body: z.object({ + pin: z.string().nonempty('PIN is required'), + sessionId: z.string().nonempty('Session ID is required'), + }), +}); + +export const getRefreshSchema = z.object({ + body: z.object({ + pin: z.string().nonempty('PIN is required'), + }), +}); diff --git a/src/auth/authController.ts b/src/auth/authController.ts index a671136..241801a 100644 --- a/src/auth/authController.ts +++ b/src/auth/authController.ts @@ -1,5 +1,6 @@ -import type { Request, Response } from 'express'; +import type { NextFunction, Request, Response } from 'express'; +import { cbordService } from '../services/cbord.service.js'; import * as authService from './authService.js'; export const verifyDeviceUuid = async (req: Request, res: Response) => { @@ -13,3 +14,51 @@ export const refreshAccessToken = async (req: Request, res: Response) => { const tokens = await authService.refreshAccessToken(refreshToken); return res.json(tokens); }; + +/** + * @desc Links a GET/CBORD account to a deviceId via a PIN. + * This is the one-time setup for the finance feature. + */ +export const getAuthorize = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const { userId } = req.user!; + const { pin, sessionId } = req.body; + + try { + await cbordService.createPin(String(userId), pin, sessionId); + + // This route does NOT create the user, just links the PIN. + return res + .status(200) + .json({ message: 'GET account linked successfully.' }); + } catch (error) { + return next(error); + } +}; + +/** + * @desc Exchanges a deviceId and PIN for a new GET/CBORD session_id. + * This is the "persistent login" flow. + */ +export const getRefresh = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + const { userId } = req.user!; + const { pin } = req.body; + + try { + const newSessionId = await cbordService.authenticatePin( + String(userId), + pin, + ); + + return res.status(200).json({ sessionId: newSessionId }); + } catch (error) { + return next(error); + } +}; diff --git a/src/auth/authRouter.ts b/src/auth/authRouter.ts index 65bc615..ef1582a 100644 --- a/src/auth/authRouter.ts +++ b/src/auth/authRouter.ts @@ -1,11 +1,19 @@ import { Router } from 'express'; +import { requireAuth } from '../middleware/authentication.js'; import { validateRequest } from '../middleware/validateRequest.js'; import { + getAuthorizeSchema, + getRefreshSchema, refreshAccessTokenSchema, verifyDeviceUuidSchema, } from './auth.schema.js'; -import { refreshAccessToken, verifyDeviceUuid } from './authController.js'; +import { + getAuthorize, + getRefresh, + refreshAccessToken, + verifyDeviceUuid, +} from './authController.js'; const router = Router(); @@ -19,5 +27,17 @@ router.post( validateRequest(refreshAccessTokenSchema), refreshAccessToken, ); +router.post( + '/get/authorize', + requireAuth, + validateRequest(getAuthorizeSchema), + getAuthorize, +); +router.post( + '/get/refresh', + requireAuth, + validateRequest(getRefreshSchema), + getRefresh, +); export default router; diff --git a/src/financials/financialsController.ts b/src/financials/financialsController.ts new file mode 100644 index 0000000..f6d53ce --- /dev/null +++ b/src/financials/financialsController.ts @@ -0,0 +1,32 @@ +import type { NextFunction, Request, Response } from 'express'; + +import { cbordService } from '../services/cbord.service.js'; + +export const getFinancials = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const userId = req.user?.userId; + if (!userId) { + throw new Error('Authenticated user not found'); + } + + // sessionId is now provided in the request body + const { sessionId } = req.body as { sessionId?: string }; + if (!sessionId) { + throw new Error('sessionId is required in request body'); + } + + // Fetch accounts and transactions in parallel + const [accounts, transactions] = await Promise.all([ + cbordService.retrieveAccounts(sessionId!), + cbordService.retrieveTransactionHistory(sessionId!), + ]); + + res.json({ accounts, transactions }); + } catch (error) { + next(error); + } +}; diff --git a/src/financials/financialsRouter.ts b/src/financials/financialsRouter.ts new file mode 100644 index 0000000..f5bad3c --- /dev/null +++ b/src/financials/financialsRouter.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; + +import { getFinancials } from './financialsController.js'; + +const router = Router(); + +router.get('/', getFinancials); + +export default router; diff --git a/src/firebase.ts b/src/firebase.ts index c84dce9..efe8339 100644 --- a/src/firebase.ts +++ b/src/firebase.ts @@ -2,24 +2,120 @@ import admin from 'firebase-admin'; import { readFileSync } from 'fs'; import { resolve } from 'path'; -if (!admin.apps.length) { - try { - const serviceAccountPath = resolve( - process.env.FIREBASE_SERVICE_ACCOUNT_PATH || - './firebase-service-account.json', - ); - const serviceAccount = JSON.parse(readFileSync(serviceAccountPath, 'utf8')); - - admin.initializeApp({ - credential: admin.credential.cert(serviceAccount as admin.ServiceAccount), - }); - } catch (error) { - console.error('Failed to initialize Firebase Admin:', error); - throw new Error( - 'Firebase configuration failed. Please check your service account file.', - ); +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); + +class FirebaseService { + public messaging: admin.messaging.Messaging; + + constructor() { + this.initializeFirebase(); + this.messaging = admin.messaging(); + } + + private initializeFirebase() { + if (admin.apps.length === 0) { + try { + const serviceAccountPath = resolve( + process.env.FIREBASE_SERVICE_ACCOUNT_PATH || + './firebase-service-account.json', + ); + const serviceAccount = JSON.parse( + readFileSync(serviceAccountPath, 'utf8'), + ); + + admin.initializeApp({ + credential: admin.credential.cert( + serviceAccount as admin.ServiceAccount, + ), + }); + console.log('Firebase Admin initialized'); + } catch (error) { + console.error('Failed to initialize Firebase Admin:', error); + throw new Error( + 'Firebase configuration failed. Please check your service account file.', + ); + } + } + } + + public async sendToTokens( + tokens: string[], + title: string, + body: string, + data: { [key: string]: string } = {}, + ) { + if (tokens.length === 0) { + return; + } + + const message: admin.messaging.MulticastMessage = { + tokens: tokens, + notification: { + title: title, + body: body, + }, + data: data, + apns: { + payload: { + aps: { + sound: 'default', + }, + }, + }, + android: { + notification: { + sound: 'default', + clickAction: 'FLUTTER_NOTIFICATION_CLICK', + }, + }, + }; + + try { + const response = await this.messaging.sendEachForMulticast(message); + console.log(`Successfully sent ${response.successCount} messages.`); + + if (response.failureCount > 0) { + console.log(`Failed to send ${response.failureCount} messages.`); + const failedTokens: string[] = []; + + response.responses.forEach((resp, idx) => { + if (!resp.success) { + console.error( + `Failure details for token [${tokens[idx]}]:`, + JSON.stringify(resp, null, 2), + ); + failedTokens.push(tokens[idx]); + } + }); + + await this.cleanupFailedTokens(failedTokens); + } + } catch (e) { + console.error('Error sending multicast message:', e); + } + } + + private async cleanupFailedTokens(tokens: string[]) { + if (tokens.length === 0) { + return; + } + + try { + await prisma.fCMToken.deleteMany({ + where: { + token: { + in: tokens, + }, + }, + }); + console.log('Failed tokens cleaned up.'); + } catch (e) { + console.error('Error cleaning up failed tokens:', e); + } } } -const firebaseAdmin = admin; -export default firebaseAdmin; +export const firebaseService = new FirebaseService(); +export const firebaseAdmin = admin; diff --git a/src/server.ts b/src/server.ts index 5521469..411e407 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,6 +6,8 @@ import type { Request, Response } from 'express'; import authRouter from './auth/authRouter.js'; import eateryRouter from './eateries/eateryRouter.js'; +import { eateryRouter } from './eateries/eateryRouter.js'; +import financialRouter from './financials/financialsRouter.js'; import { requireAuth } from './middleware/authentication.js'; import { globalErrorHandler } from './middleware/errorHandler.js'; import { requestLogger } from './middleware/logger.js'; @@ -57,6 +59,7 @@ router.use('/eateries', eateryRouter); // Protected routes router.use(requireAuth); router.use('/users', userRouter); +router.use('/financials', financialRouter); app.use(router); diff --git a/src/services/cbord.service.ts b/src/services/cbord.service.ts new file mode 100644 index 0000000..7f6d0e4 --- /dev/null +++ b/src/services/cbord.service.ts @@ -0,0 +1,265 @@ +import { + AppError, + BadRequestError, + ErrorCodes, + UnauthorizedError, +} from '../utils/AppError.js'; + +const CBORD_BASE_URL = process.env.CBORD_BASE_URL; +if (!CBORD_BASE_URL) { + throw new Error('CBORD_BASE_URL is not defined in environment variables.'); +} + +const CBORD_USER_URL = `${CBORD_BASE_URL}/user`; +const CBORD_AUTH_URL = `${CBORD_BASE_URL}/authentication`; +const CBORD_COMMERCE_URL = `${CBORD_BASE_URL}/commerce`; + +interface CbordResponse { + response: T | null; + exception: { + message: string; + } | null; +} + +type CbordAccount = { + accountDisplayName: string; + balance: number; + [key: string]: unknown; +}; + +type CbordTransaction = { + amount: number; + tenderId: string; + accountName: string; + postedDate: string; + locationName: string; + [key: string]: unknown; +}; + +/** + * Wrapper for making requests to the CBORD API. + * Handles network errors and JSON parsing errors. + */ +async function cbordRequest( + url: string, + payload: object, +): Promise> { + try { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + throw new AppError( + `CBORD API request failed with status ${response.status}`, + response.status, + ErrorCodes.BAD_REQUEST, + ); + } + + // Try to parse the JSON response + try { + const json = (await response.json()) as CbordResponse; + return json; + } catch { + throw new AppError( + 'Failed to parse CBORD API response.', + 500, + ErrorCodes.BAD_REQUEST, + ); + } + } catch (error) { + if (error instanceof AppError) throw error; + + // Handle generic fetch errors + const message = + error instanceof Error ? error.message : 'An unknown error occurred'; + throw new AppError( + `Error communicating with CBORD API: ${message}`, + 502, + ErrorCodes.BAD_REQUEST, + ); + } +} + +/** + * Checks the CBORD JSON response for an "exception" field. + * This is based on your old app's `handle_cbord_exception` function. + */ +function handleCbordException(result: CbordResponse): void { + if (result.exception) { + const msg = result.exception.message; + // "Session not found" or "not validated" are 401 + if (msg.includes('not validated') || msg.includes('Session not found')) { + throw new UnauthorizedError(msg); + } + // Other exceptions are user errors + throw new BadRequestError(msg); + } + + if (result.response === null) { + // Probably shouldn't happen if exception is null but just in case + throw new AppError( + 'CBORD API returned a null response.', + 500, + ErrorCodes.BAD_REQUEST, + ); + } +} + +/** + * Calls the CBORD `createPIN` method. + * This is the one-time setup for linking a device and PIN. + */ +async function createPin( + deviceId: string, + pin: string, + sessionId: string, +): Promise { + const payload = { + method: 'createPIN', + params: { + PIN: pin, + deviceId: deviceId, + sessionId: sessionId, + }, + }; + + const result = await cbordRequest(CBORD_USER_URL, payload); + handleCbordException(result); + + return result.response ?? false; +} + +/** + * Calls the CBORD `authenticatePIN` method. + * Exchanges a deviceId and PIN for a new, valid sessionId. + */ +async function authenticatePin(deviceId: string, pin: string): Promise { + const payload = { + method: 'authenticatePIN', + params: { + systemCredentials: { + domain: '', + userName: 'get_mobile', + password: 'NOTUSED', + }, + deviceId: deviceId, + pin: pin, + }, + }; + + const result = await cbordRequest(CBORD_AUTH_URL, payload); + handleCbordException(result); + + // If the response is null or not a string throw an error + if (typeof result.response !== 'string') { + throw new AppError( + 'Failed to retrieve new sessionId.', + 500, + ErrorCodes.BAD_REQUEST, + ); + } + + return result.response; +} + +/** + * Fetches and parses account balances + */ +async function retrieveAccounts(sessionId: string) { + const payload = { + method: 'retrieveAccounts', + params: { + sessionId: sessionId, + }, + }; + + const result = await cbordRequest<{ accounts: CbordAccount[] }>( + CBORD_COMMERCE_URL, + payload, + ); + handleCbordException(result); + + // Parse accounts (logic from old Django app) + let brbAccount = null; + let cityBucksAccount = null; + let laundryAccount = null; + + for (const account of result.response?.accounts || []) { + const displayName = account.accountDisplayName || ''; + if (displayName.includes('Big Red Bucks') && !brbAccount) { + brbAccount = account; + } + if ( + displayName.includes('City Bucks') && + !displayName.includes('GET') && + !cityBucksAccount + ) { + cityBucksAccount = account; + } + if (displayName.includes('Laundry') && !laundryAccount) { + laundryAccount = account; + } + } + + return { + brb: brbAccount + ? { + name: brbAccount.accountDisplayName, + balance: brbAccount.balance, + } + : null, + city_bucks: cityBucksAccount + ? { + name: cityBucksAccount.accountDisplayName, + balance: cityBucksAccount.balance, + } + : null, + laundry: laundryAccount + ? { + name: laundryAccount.accountDisplayName, + balance: laundryAccount.balance, + } + : null, + }; +} + +/** + * Fetches and parses transaction history. + */ +async function retrieveTransactionHistory(sessionId: string) { + const payload = { + method: 'retrieveTransactionHistoryWithinDateRange', + params: { + paymentSystemType: 0, + queryCriteria: { + maxReturnMostRecent: 100, // Limit to 100 most recent for now + }, + sessionId: sessionId, + }, + }; + + const result = await cbordRequest<{ transactions: CbordTransaction[] }>( + CBORD_COMMERCE_URL, + payload, + ); + handleCbordException(result); + + return (result.response?.transactions || []).map((txn) => ({ + amount: txn.amount, + tenderId: txn.tenderId, + accountName: txn.accountName, + date: txn.postedDate, + location: txn.locationName, + })); +} + +export const cbordService = { + createPin, + authenticatePin, + retrieveAccounts, + retrieveTransactionHistory, +}; diff --git a/src/users/userController.ts b/src/users/userController.ts index a93323f..523a31b 100644 --- a/src/users/userController.ts +++ b/src/users/userController.ts @@ -1,4 +1,4 @@ -import type { EventType, User } from '@prisma/client'; +import type { EventType } from '@prisma/client'; import type { NextFunction, Request, Response } from 'express'; @@ -34,17 +34,12 @@ export const addFcmToken = async ( res: Response, ): Promise => { const { token } = req.body; - const { user } = res.locals; + const { userId } = req.user!; await prisma.fCMToken.upsert({ where: { token }, - update: { - userId: user.id, - }, - create: { - token, - userId: user.id, - }, + update: { userId }, + create: { token, userId }, }); res.status(200).json({ message: 'Token registered successfully.' }); @@ -55,13 +50,13 @@ export const removeFcmToken = async ( res: Response, ): Promise => { const { token } = req.body; - const { user } = res.locals; + const { userId } = req.user!; try { await prisma.fCMToken.delete({ where: { token, - userId: user.id, + userId, }, }); res.status(200).json({ message: 'Token removed successfully.' }); @@ -77,7 +72,15 @@ export const addFavoriteItem = async ( ) => { try { const { name } = req.body; - const { user } = res.locals as { user: User }; + const { userId } = req.user!; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + return res.status(404).json({ message: 'User not found.' }); + } await prisma.user.update({ where: { id: user.id }, @@ -91,7 +94,7 @@ export const addFavoriteItem = async ( res.status(200).json({ message: 'Favorite item added.' }); } catch (error) { - next(error); + return next(error); } }; @@ -102,7 +105,15 @@ export const removeFavoriteItem = async ( ) => { try { const { name } = req.body; - const { user } = res.locals as { user: User }; + const { userId } = req.user!; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + return res.status(404).json({ message: 'User not found.' }); + } // Filter the name out of the array const updatedItems = user.favoritedItemNames.filter( @@ -118,7 +129,7 @@ export const removeFavoriteItem = async ( res.status(200).json({ message: 'Favorite item removed.' }); } catch (error) { - next(error); + return next(error); } }; @@ -129,7 +140,15 @@ export const addFavoriteEatery = async ( ) => { try { const { eateryId } = req.body; - const { user } = res.locals as { user: User }; + const { userId } = req.user!; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + return res.status(404).json({ message: 'User not found.' }); + } // Use upsert to avoid crashing if the relation already exists await prisma.favoritedEatery.upsert({ @@ -148,7 +167,7 @@ export const addFavoriteEatery = async ( res.status(200).json({ message: 'Favorite eatery added.' }); } catch (error) { - next(error); + return next(error); } }; @@ -159,8 +178,14 @@ export const removeFavoriteEatery = async ( ) => { try { const { eateryId } = req.body; - const { user } = res.locals as { user: User }; + const { userId } = req.user!; + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + return res.status(404).json({ message: 'User not found.' }); + } await prisma.favoritedEatery.delete({ where: { userId_eateryId: { @@ -170,10 +195,12 @@ export const removeFavoriteEatery = async ( }, }); - res.status(200).json({ message: 'Favorite eatery removed.' }); + return res.status(200).json({ message: 'Favorite eatery removed.' }); } catch { // Don't fail if they try to delete something that's not there - res.status(200).json({ message: 'Favorite eatery removal processed.' }); + return res + .status(200) + .json({ message: 'Favorite eatery removal processed.' }); } }; @@ -182,12 +209,20 @@ export const removeFavoriteEatery = async ( * and the eateries serving them. */ export const getFavoriteMatches = async ( - _: Request, + req: Request, res: Response, next: NextFunction, ) => { try { - const { user } = res.locals as { user: User }; + const { userId } = req.user!; + + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + + if (!user) { + return res.status(404).json({ message: 'User not found.' }); + } const { favoritedItemNames } = user; if (favoritedItemNames.length === 0) { diff --git a/src/utils/test-notification-setup.ts b/src/utils/test-notification-setup.ts new file mode 100644 index 0000000..ee748c4 --- /dev/null +++ b/src/utils/test-notification-setup.ts @@ -0,0 +1,86 @@ +import { add } from 'date-fns'; + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + // const MY_TEST_FCM_TOKEN = 'fQgyrBIuS0dkt3pv6MoXZ4:APA91bGB0Ckh9z63fOAxuDyq1_j-1vUL8CEg8vOLgJhr0uDa3KRBzJR9pCgBd1-x6lMPilpC3smzatGsj-1azqFelxVVotvW8s9xJw6rOylCt6yvbBaDHy8'; + + const MY_TEST_DEVICE_ID = '1234567890'; + // const MY_TEST_REFRESH_TOKEN = 'test-refresh-token'; + const MY_FAVORITE_ITEM = 'Hot Cocoa'; + + // await prisma.eatery.deleteMany({ where: { name: 'Test Eatery!' } }); + await prisma.user.deleteMany({ where: { deviceUuid: MY_TEST_DEVICE_ID } }); + + // const user = await prisma.user.create({ + // data: { + // deviceUuid: MY_TEST_DEVICE_ID, + // refreshToken: MY_TEST_REFRESH_TOKEN, + // favoritedItemNames: [MY_FAVORITE_ITEM], + // fcmTokens: { + // create: { + // token: MY_TEST_FCM_TOKEN, + // }, + // }, + // }, + // }); + // console.log(`Created user for device: ${user.deviceUuid}`); + + const now = new Date(); + const eventStart = now; + const eventEnd = add(now, { hours: 2 }); + + const eatery = await prisma.eatery.create({ + data: { + name: 'Test Eatery', + shortName: 'Test', + about: 'Test eatery for notifs', + shortAbout: 'Test eatery short description', + cornellDining: true, + menuSummary: 'Pizza', + imageUrl: '', + campusArea: 'CENTRAL', + location: 'Here', + latitude: 42.444, + longitude: -76.501, + paymentMethods: ['BRB'], + eateryTypes: ['DINING_ROOM'], + events: { + create: { + type: 'DINNER', + startTimestamp: eventStart, + endTimestamp: eventEnd, + menu: { + create: { + name: 'Test Menu', + items: { + create: { + name: MY_FAVORITE_ITEM, + basePrice: 5.99, + }, + }, + }, + }, + }, + }, + }, + }); + console.log(`Created test eatery: ${eatery.name}`); + console.log( + `Created test item "${MY_FAVORITE_ITEM}" for event happening now.`, + ); + + console.log('✅ Test setup complete.'); + console.log(`You can now run: npm run send-notifications`); +} + +main() + .catch(async (e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); From d6005c01098d781ae9f5d3b96a38a6a28125210c Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Thu, 20 Nov 2025 17:56:34 -0500 Subject: [PATCH 07/18] rebase --- package.json | 4 ---- src/server.ts | 1 - 2 files changed, 5 deletions(-) diff --git a/package.json b/package.json index dd6f15d..abfec9c 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,7 @@ "lint": "eslint '{src,test}/**/*.ts' --fix", "format": "prettier --write '{src,test}/**/*.ts'", "scrape": "tsx prisma/scraper.ts", -<<<<<<< HEAD - "scrape:scheduled": "SCHEDULED_MODE=true node dist/prisma/scraper.js", -======= "scrape:scheduled": "SCHEDULED_MODE=true tsx prisma/scraper.ts", ->>>>>>> 5487c7c (fix merge issues) "studio": "prisma studio", "send-notifications": "tsx prisma/send-notifications.ts", "test-noti-setup": "tsx src/utils/test-notification-setup.ts" diff --git a/src/server.ts b/src/server.ts index 411e407..31f93ab 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,7 +6,6 @@ import type { Request, Response } from 'express'; import authRouter from './auth/authRouter.js'; import eateryRouter from './eateries/eateryRouter.js'; -import { eateryRouter } from './eateries/eateryRouter.js'; import financialRouter from './financials/financialsRouter.js'; import { requireAuth } from './middleware/authentication.js'; import { globalErrorHandler } from './middleware/errorHandler.js'; From bdd8bcc0f8e9823165e88d57bfea2fcf1b64dd1e Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Wed, 21 Jan 2026 13:18:37 -0500 Subject: [PATCH 08/18] resolve PR comments and adjust announcements to scrape correctly. --- package-lock.json | 40 ------ package.json | 10 +- ...otifications.ts => notificationService.ts} | 8 +- prisma/schema.prisma | 2 +- prisma/scraper.ts | 37 ++--- src/auth/auth.schema.ts | 14 +- src/auth/authController.ts | 37 ++--- src/auth/authRouter.ts | 16 +-- src/constants.ts | 3 + src/financials/financials.schema.ts | 8 ++ src/financials/financialsController.ts | 32 ++--- src/financials/financialsRouter.ts | 5 +- src/firebase.ts | 133 +++--------------- src/services/cbord.service.ts | 4 +- src/utils/notifications.ts | 88 ++++++++++++ src/utils/test-notification-setup.ts | 86 ----------- src/utils/time.ts | 7 +- 17 files changed, 195 insertions(+), 335 deletions(-) rename prisma/{send-notifications.ts => notificationService.ts} (96%) create mode 100644 src/financials/financials.schema.ts create mode 100644 src/utils/notifications.ts delete mode 100644 src/utils/test-notification-setup.ts diff --git a/package-lock.json b/package-lock.json index ac9d074..ddc9c19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,9 @@ "license": "ISC", "dependencies": { "@prisma/client": "^6.16.2", - "cookie-parser": "^1.4.7", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^17.2.3", - "escape-html": "^1.0.3", "express": "^5.1.0", "express-rate-limit": "^8.1.0", "firebase-admin": "^13.6.0", @@ -27,8 +25,6 @@ "devDependencies": { "@eslint/js": "^9.36.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", - "@types/cookie-parser": "^1.4.9", - "@types/escape-html": "^1.0.4", "@types/express": "^5.0.3", "@types/node": "^24.5.2", "@types/node-cron": "^3.0.11", @@ -1023,23 +1019,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie-parser": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", - "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/escape-html": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/escape-html/-/escape-html-1.0.4.tgz", - "integrity": "sha512-qZ72SFTgUAZ5a7Tj6kf2SHLetiH5S6f8G5frB2SPQ3EyF02kxdyBFf4Tz4banE3xCgGnKgWLt//a6VuYHKYJTg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1970,25 +1949,6 @@ "node": ">= 0.6" } }, - "node_modules/cookie-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", - "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.6" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", diff --git a/package.json b/package.json index abfec9c..92e92e7 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,9 @@ "lint": "eslint '{src,test}/**/*.ts' --fix", "format": "prettier --write '{src,test}/**/*.ts'", "scrape": "tsx prisma/scraper.ts", - "scrape:scheduled": "SCHEDULED_MODE=true tsx prisma/scraper.ts", + "scrape:scheduled": "SCHEDULED_MODE=true node dist/prisma/scraper.js", "studio": "prisma studio", - "send-notifications": "tsx prisma/send-notifications.ts", - "test-noti-setup": "tsx src/utils/test-notification-setup.ts" + "send-notifications": "tsx prisma/notificationService.ts" }, "repository": { "type": "git", @@ -31,11 +30,9 @@ "homepage": "https://github.com/cuappdev/eatery-blue-backend#readme", "dependencies": { "@prisma/client": "^6.16.2", - "cookie-parser": "^1.4.7", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "dotenv": "^17.2.3", - "escape-html": "^1.0.3", "express": "^5.1.0", "express-rate-limit": "^8.1.0", "firebase-admin": "^13.6.0", @@ -43,14 +40,11 @@ "jsonwebtoken": "^9.0.2", "node-cache": "^5.1.2", "node-cron": "^4.2.1", - "node-cache": "^5.1.2", "zod": "^4.1.12" }, "devDependencies": { "@eslint/js": "^9.36.0", "@trivago/prettier-plugin-sort-imports": "^5.2.2", - "@types/cookie-parser": "^1.4.9", - "@types/escape-html": "^1.0.4", "@types/express": "^5.0.3", "@types/node": "^24.5.2", "@types/node-cron": "^3.0.11", diff --git a/prisma/send-notifications.ts b/prisma/notificationService.ts similarity index 96% rename from prisma/send-notifications.ts rename to prisma/notificationService.ts index e534942..3794c55 100644 --- a/prisma/send-notifications.ts +++ b/prisma/notificationService.ts @@ -1,7 +1,9 @@ -import { firebaseService, prisma } from '../src/firebase.js'; -import { getQueryTimeWindow } from '../src/utils/time.js'; import cron from 'node-cron'; +import { prisma } from '../src/prisma.js'; +import { sendToTokens } from '../src/utils/notifications.js'; +import { getQueryTimeWindow } from '../src/utils/time.js'; + function buildMessage( matchesByEatery: Map, ): { title: string; body: string } { @@ -119,7 +121,7 @@ export async function main() { }; try { - await firebaseService.sendToTokens(tokens, title, body, dataPayload); + await sendToTokens(tokens, title, body, dataPayload); } catch (e) { console.error(`Failed to send notification for user ${user.id}:`, e); } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d0c9f55..e3ffa78 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -100,7 +100,7 @@ model Report { model Eatery { id Int @id @default(autoincrement()) cornellId Int? @unique - announcements String[] + announcements String[] @default([]) name String shortName String about String diff --git a/prisma/scraper.ts b/prisma/scraper.ts index d74c287..6e751d0 100644 --- a/prisma/scraper.ts +++ b/prisma/scraper.ts @@ -183,15 +183,17 @@ function transformStaticEatery(rawStaticEatery: RawStaticEatery) { menuSummary: menuSummary, imageUrl: imageUrl, campusArea: mapCampusArea(rawStaticEatery.campusArea), - onlineOrderUrl: rawStaticEatery.onlineOrderUrl, - contactPhone: rawStaticEatery.contactPhone, - contactEmail: rawStaticEatery.contactEmail, + onlineOrderUrl: rawStaticEatery.onlineOrderUrl ?? null, + contactPhone: rawStaticEatery.contactPhone ?? null, + contactEmail: rawStaticEatery.contactEmail ?? null, latitude: rawStaticEatery.latitude, longitude: rawStaticEatery.longitude, location: rawStaticEatery.location, paymentMethods: Array.from(new Set(rawStaticEatery.payMethods.map(mapPaymentMethod))), eateryTypes: rawStaticEatery.eateryTypes.map(mapEateryType), - announcements: rawStaticEatery.announcements, + announcements: Array.isArray(rawStaticEatery.announcements) + ? rawStaticEatery.announcements.map((a: unknown) => typeof a === 'string' ? a : (a as { title?: string })?.title ?? '').filter(Boolean) + : [], }, events, }; @@ -209,21 +211,23 @@ function transformEatery(rawEatery: RawEatery) { cornellId: rawEatery.id, name: rawEatery.name, shortName: rawEatery.nameshort, - about: rawEatery.about, - shortAbout: rawEatery.aboutshort, - cornellDining: rawEatery.cornellDining, - menuSummary: rawEatery.opHoursCalcDescr, + about: rawEatery.about || '', + shortAbout: rawEatery.aboutshort || '', + cornellDining: rawEatery.cornellDining ?? false, + menuSummary: rawEatery.opHoursCalcDescr || '', imageUrl: imageUrl, campusArea: mapCampusArea(rawEatery.campusArea), - onlineOrderUrl: rawEatery.onlineOrderUrl, - contactPhone: rawEatery.contactPhone, - contactEmail: rawEatery.contactEmail, + onlineOrderUrl: rawEatery.onlineOrderUrl ?? null, + contactPhone: rawEatery.contactPhone ?? null, + contactEmail: rawEatery.contactEmail ?? null, latitude: rawEatery.latitude, longitude: rawEatery.longitude, - location: rawEatery.location, - paymentMethods: Array.from(new Set(rawEatery.payMethods.map(mapPaymentMethod))), - eateryTypes: rawEatery.eateryTypes.map(mapEateryType), - announcements: rawEatery.announcements, + location: rawEatery.location || '', + paymentMethods: Array.from(new Set((rawEatery.payMethods ?? []).map(mapPaymentMethod))), + eateryTypes: (rawEatery.eateryTypes ?? []).map(mapEateryType), + announcements: Array.isArray(rawEatery.announcements) + ? rawEatery.announcements.map((a: unknown) => typeof a === 'string' ? a : (a as { title?: string })?.title ?? '').filter(Boolean) + : [], }; const events: Array<{ @@ -346,7 +350,8 @@ async function processAllEateries( }> ) { return await prisma.$transaction(async (tx) => { - // Clear existing data + // Clear existing data (order matters due to foreign key constraints) + await tx.favoritedEatery.deleteMany({}); await tx.event.deleteMany({}); await tx.eatery.deleteMany({}); diff --git a/src/auth/auth.schema.ts b/src/auth/auth.schema.ts index ebf5aee..3347f10 100644 --- a/src/auth/auth.schema.ts +++ b/src/auth/auth.schema.ts @@ -12,15 +12,21 @@ export const refreshAccessTokenSchema = z.object({ }), }); -export const getAuthorizeSchema = z.object({ +export const linkCbordAccountSchema = z.object({ body: z.object({ - pin: z.string().nonempty('PIN is required'), + pin: z + .string() + .nonempty('PIN is required') + .regex(/^\d+$/, 'PIN must contain only numeric characters'), sessionId: z.string().nonempty('Session ID is required'), }), }); -export const getRefreshSchema = z.object({ +export const getCbordSessionSchema = z.object({ body: z.object({ - pin: z.string().nonempty('PIN is required'), + pin: z + .string() + .nonempty('PIN is required') + .regex(/^\d+$/, 'PIN must contain only numeric characters'), }), }); diff --git a/src/auth/authController.ts b/src/auth/authController.ts index 241801a..7eff842 100644 --- a/src/auth/authController.ts +++ b/src/auth/authController.ts @@ -1,4 +1,4 @@ -import type { NextFunction, Request, Response } from 'express'; +import type { Request, Response } from 'express'; import { cbordService } from '../services/cbord.service.js'; import * as authService from './authService.js'; @@ -19,46 +19,25 @@ export const refreshAccessToken = async (req: Request, res: Response) => { * @desc Links a GET/CBORD account to a deviceId via a PIN. * This is the one-time setup for the finance feature. */ -export const getAuthorize = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const linkCbordAccount = async (req: Request, res: Response) => { const { userId } = req.user!; const { pin, sessionId } = req.body; - try { - await cbordService.createPin(String(userId), pin, sessionId); + await cbordService.createPin(String(userId), pin, sessionId); - // This route does NOT create the user, just links the PIN. - return res - .status(200) - .json({ message: 'GET account linked successfully.' }); - } catch (error) { - return next(error); - } + // This route does NOT create the user, just links the PIN. + return res.json({ message: 'GET account linked successfully.' }); }; /** * @desc Exchanges a deviceId and PIN for a new GET/CBORD session_id. * This is the "persistent login" flow. */ -export const getRefresh = async ( - req: Request, - res: Response, - next: NextFunction, -) => { +export const getCbordSession = async (req: Request, res: Response) => { const { userId } = req.user!; const { pin } = req.body; - try { - const newSessionId = await cbordService.authenticatePin( - String(userId), - pin, - ); + const newSessionId = await cbordService.authenticatePin(String(userId), pin); - return res.status(200).json({ sessionId: newSessionId }); - } catch (error) { - return next(error); - } + return res.json({ sessionId: newSessionId }); }; diff --git a/src/auth/authRouter.ts b/src/auth/authRouter.ts index ef1582a..212152e 100644 --- a/src/auth/authRouter.ts +++ b/src/auth/authRouter.ts @@ -3,14 +3,14 @@ import { Router } from 'express'; import { requireAuth } from '../middleware/authentication.js'; import { validateRequest } from '../middleware/validateRequest.js'; import { - getAuthorizeSchema, - getRefreshSchema, + getCbordSessionSchema, + linkCbordAccountSchema, refreshAccessTokenSchema, verifyDeviceUuidSchema, } from './auth.schema.js'; import { - getAuthorize, - getRefresh, + getCbordSession, + linkCbordAccount, refreshAccessToken, verifyDeviceUuid, } from './authController.js'; @@ -30,14 +30,14 @@ router.post( router.post( '/get/authorize', requireAuth, - validateRequest(getAuthorizeSchema), - getAuthorize, + validateRequest(linkCbordAccountSchema), + linkCbordAccount, ); router.post( '/get/refresh', requireAuth, - validateRequest(getRefreshSchema), - getRefresh, + validateRequest(getCbordSessionSchema), + getCbordSession, ); export default router; diff --git a/src/constants.ts b/src/constants.ts index 0d01024..43f8a8c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -4,6 +4,9 @@ export const EATERY_IMAGES_BASE_URL = export const DEFAULT_IMAGE_URL = 'https://images-prod.healthline.com/hlcmsresource/images/AN_images/health-benefits-of-apples-1296x728-feature.jpg'; +/** How many hours ahead to look for events when sending notifications */ +export const NOTIFICATION_LOOKAHEAD_HOURS = 7; + export enum Weekday { SUNDAY = 'Sunday', MONDAY = 'Monday', diff --git a/src/financials/financials.schema.ts b/src/financials/financials.schema.ts new file mode 100644 index 0000000..e3d7865 --- /dev/null +++ b/src/financials/financials.schema.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const getFinancialsSchema = z.object({ + body: z.object({ + sessionId: z.string().nonempty('Session ID is required'), + }), +}); + diff --git a/src/financials/financialsController.ts b/src/financials/financialsController.ts index f6d53ce..634b8ff 100644 --- a/src/financials/financialsController.ts +++ b/src/financials/financialsController.ts @@ -1,32 +1,18 @@ -import type { NextFunction, Request, Response } from 'express'; +import type { Request, Response } from 'express'; import { cbordService } from '../services/cbord.service.js'; export const getFinancials = async ( - req: Request, + req: Request, res: Response, - next: NextFunction, ) => { - try { - const userId = req.user?.userId; - if (!userId) { - throw new Error('Authenticated user not found'); - } + const { sessionId } = req.body; - // sessionId is now provided in the request body - const { sessionId } = req.body as { sessionId?: string }; - if (!sessionId) { - throw new Error('sessionId is required in request body'); - } + // Fetch accounts and transactions in parallel + const [accounts, transactions] = await Promise.all([ + cbordService.retrieveAccounts(sessionId), + cbordService.retrieveTransactionHistory(sessionId), + ]); - // Fetch accounts and transactions in parallel - const [accounts, transactions] = await Promise.all([ - cbordService.retrieveAccounts(sessionId!), - cbordService.retrieveTransactionHistory(sessionId!), - ]); - - res.json({ accounts, transactions }); - } catch (error) { - next(error); - } + res.json({ accounts, transactions }); }; diff --git a/src/financials/financialsRouter.ts b/src/financials/financialsRouter.ts index f5bad3c..021fcb3 100644 --- a/src/financials/financialsRouter.ts +++ b/src/financials/financialsRouter.ts @@ -1,9 +1,12 @@ import { Router } from 'express'; +import { requireAuth } from '../middleware/authentication.js'; +import { validateRequest } from '../middleware/validateRequest.js'; +import { getFinancialsSchema } from './financials.schema.js'; import { getFinancials } from './financialsController.js'; const router = Router(); -router.get('/', getFinancials); +router.post('/', requireAuth, validateRequest(getFinancialsSchema), getFinancials); export default router; diff --git a/src/firebase.ts b/src/firebase.ts index efe8339..c4b8db0 100644 --- a/src/firebase.ts +++ b/src/firebase.ts @@ -2,120 +2,33 @@ import admin from 'firebase-admin'; import { readFileSync } from 'fs'; import { resolve } from 'path'; -import { PrismaClient } from '@prisma/client'; - -export const prisma = new PrismaClient(); - -class FirebaseService { - public messaging: admin.messaging.Messaging; - - constructor() { - this.initializeFirebase(); - this.messaging = admin.messaging(); - } - - private initializeFirebase() { - if (admin.apps.length === 0) { - try { - const serviceAccountPath = resolve( - process.env.FIREBASE_SERVICE_ACCOUNT_PATH || - './firebase-service-account.json', - ); - const serviceAccount = JSON.parse( - readFileSync(serviceAccountPath, 'utf8'), - ); - - admin.initializeApp({ - credential: admin.credential.cert( - serviceAccount as admin.ServiceAccount, - ), - }); - console.log('Firebase Admin initialized'); - } catch (error) { - console.error('Failed to initialize Firebase Admin:', error); - throw new Error( - 'Firebase configuration failed. Please check your service account file.', - ); - } - } - } - - public async sendToTokens( - tokens: string[], - title: string, - body: string, - data: { [key: string]: string } = {}, - ) { - if (tokens.length === 0) { - return; - } - - const message: admin.messaging.MulticastMessage = { - tokens: tokens, - notification: { - title: title, - body: body, - }, - data: data, - apns: { - payload: { - aps: { - sound: 'default', - }, - }, - }, - android: { - notification: { - sound: 'default', - clickAction: 'FLUTTER_NOTIFICATION_CLICK', - }, - }, - }; - +function initializeFirebase() { + if (admin.apps.length === 0) { try { - const response = await this.messaging.sendEachForMulticast(message); - console.log(`Successfully sent ${response.successCount} messages.`); - - if (response.failureCount > 0) { - console.log(`Failed to send ${response.failureCount} messages.`); - const failedTokens: string[] = []; - - response.responses.forEach((resp, idx) => { - if (!resp.success) { - console.error( - `Failure details for token [${tokens[idx]}]:`, - JSON.stringify(resp, null, 2), - ); - failedTokens.push(tokens[idx]); - } - }); - - await this.cleanupFailedTokens(failedTokens); - } - } catch (e) { - console.error('Error sending multicast message:', e); - } - } - - private async cleanupFailedTokens(tokens: string[]) { - if (tokens.length === 0) { - return; - } - - try { - await prisma.fCMToken.deleteMany({ - where: { - token: { - in: tokens, - }, - }, + const serviceAccountPath = resolve( + process.env.FIREBASE_SERVICE_ACCOUNT_PATH || + './firebase-service-account.json', + ); + const serviceAccount = JSON.parse( + readFileSync(serviceAccountPath, 'utf8'), + ); + + admin.initializeApp({ + credential: admin.credential.cert( + serviceAccount as admin.ServiceAccount, + ), }); - console.log('Failed tokens cleaned up.'); - } catch (e) { - console.error('Error cleaning up failed tokens:', e); + console.log('Firebase Admin initialized'); + } catch (error) { + console.error('Failed to initialize Firebase Admin:', error); + throw new Error( + 'Firebase configuration failed. Please check your service account file.', + ); } } } -export const firebaseService = new FirebaseService(); +initializeFirebase(); + export const firebaseAdmin = admin; +export const firebaseMessaging: admin.messaging.Messaging = admin.messaging(); diff --git a/src/services/cbord.service.ts b/src/services/cbord.service.ts index 7f6d0e4..a355274 100644 --- a/src/services/cbord.service.ts +++ b/src/services/cbord.service.ts @@ -17,7 +17,7 @@ const CBORD_COMMERCE_URL = `${CBORD_BASE_URL}/commerce`; interface CbordResponse { response: T | null; exception: { - message: string; + message?: string; } | null; } @@ -90,7 +90,7 @@ async function cbordRequest( */ function handleCbordException(result: CbordResponse): void { if (result.exception) { - const msg = result.exception.message; + const msg = result.exception.message ?? 'Unknown CBORD error'; // "Session not found" or "not validated" are 401 if (msg.includes('not validated') || msg.includes('Session not found')) { throw new UnauthorizedError(msg); diff --git a/src/utils/notifications.ts b/src/utils/notifications.ts new file mode 100644 index 0000000..30d1839 --- /dev/null +++ b/src/utils/notifications.ts @@ -0,0 +1,88 @@ +import type admin from 'firebase-admin'; + +import { prisma } from '../prisma.js'; +import { firebaseMessaging } from '../firebase.js'; + +/** + * Send a push notification to multiple FCM tokens. + * Automatically cleans up invalid tokens. + */ +export async function sendToTokens( + tokens: string[], + title: string, + body: string, + data: { [key: string]: string } = {}, +) { + if (tokens.length === 0) { + return; + } + + const message: admin.messaging.MulticastMessage = { + tokens: tokens, + notification: { + title: title, + body: body, + }, + data: data, + apns: { + payload: { + aps: { + sound: 'default', + }, + }, + }, + android: { + notification: { + sound: 'default', + clickAction: 'FLUTTER_NOTIFICATION_CLICK', + }, + }, + }; + + try { + const response = await firebaseMessaging.sendEachForMulticast(message); + console.log(`Successfully sent ${response.successCount} messages.`); + + if (response.failureCount > 0) { + console.log(`Failed to send ${response.failureCount} messages.`); + const failedTokens: string[] = []; + + response.responses.forEach((resp, idx) => { + if (!resp.success) { + console.error( + `Failure details for token [${tokens[idx]}]:`, + JSON.stringify(resp, null, 2), + ); + failedTokens.push(tokens[idx]); + } + }); + + await cleanupFailedTokens(failedTokens); + } + } catch (e) { + console.error('Error sending multicast message:', e); + } +} + +/** + * Remove invalid FCM tokens from the database. + */ +async function cleanupFailedTokens(tokens: string[]) { + if (tokens.length === 0) { + return; + } + + try { + await prisma.fCMToken.deleteMany({ + where: { + token: { + in: tokens, + }, + }, + }); + console.log('Failed tokens cleaned up.'); + } catch (e) { + console.error('Error cleaning up failed tokens:', e); + } +} + diff --git a/src/utils/test-notification-setup.ts b/src/utils/test-notification-setup.ts deleted file mode 100644 index ee748c4..0000000 --- a/src/utils/test-notification-setup.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { add } from 'date-fns'; - -import { PrismaClient } from '@prisma/client'; - -const prisma = new PrismaClient(); - -async function main() { - // const MY_TEST_FCM_TOKEN = 'fQgyrBIuS0dkt3pv6MoXZ4:APA91bGB0Ckh9z63fOAxuDyq1_j-1vUL8CEg8vOLgJhr0uDa3KRBzJR9pCgBd1-x6lMPilpC3smzatGsj-1azqFelxVVotvW8s9xJw6rOylCt6yvbBaDHy8'; - - const MY_TEST_DEVICE_ID = '1234567890'; - // const MY_TEST_REFRESH_TOKEN = 'test-refresh-token'; - const MY_FAVORITE_ITEM = 'Hot Cocoa'; - - // await prisma.eatery.deleteMany({ where: { name: 'Test Eatery!' } }); - await prisma.user.deleteMany({ where: { deviceUuid: MY_TEST_DEVICE_ID } }); - - // const user = await prisma.user.create({ - // data: { - // deviceUuid: MY_TEST_DEVICE_ID, - // refreshToken: MY_TEST_REFRESH_TOKEN, - // favoritedItemNames: [MY_FAVORITE_ITEM], - // fcmTokens: { - // create: { - // token: MY_TEST_FCM_TOKEN, - // }, - // }, - // }, - // }); - // console.log(`Created user for device: ${user.deviceUuid}`); - - const now = new Date(); - const eventStart = now; - const eventEnd = add(now, { hours: 2 }); - - const eatery = await prisma.eatery.create({ - data: { - name: 'Test Eatery', - shortName: 'Test', - about: 'Test eatery for notifs', - shortAbout: 'Test eatery short description', - cornellDining: true, - menuSummary: 'Pizza', - imageUrl: '', - campusArea: 'CENTRAL', - location: 'Here', - latitude: 42.444, - longitude: -76.501, - paymentMethods: ['BRB'], - eateryTypes: ['DINING_ROOM'], - events: { - create: { - type: 'DINNER', - startTimestamp: eventStart, - endTimestamp: eventEnd, - menu: { - create: { - name: 'Test Menu', - items: { - create: { - name: MY_FAVORITE_ITEM, - basePrice: 5.99, - }, - }, - }, - }, - }, - }, - }, - }); - console.log(`Created test eatery: ${eatery.name}`); - console.log( - `Created test item "${MY_FAVORITE_ITEM}" for event happening now.`, - ); - - console.log('✅ Test setup complete.'); - console.log(`You can now run: npm run send-notifications`); -} - -main() - .catch(async (e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/src/utils/time.ts b/src/utils/time.ts index 5d22c89..b09c764 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,5 +1,7 @@ import { addHours, endOfDay, getUnixTime, startOfDay } from 'date-fns'; +import { NOTIFICATION_LOOKAHEAD_HOURS } from '../constants.js'; + /** * Gets the start and end of the current day in a specific timezone. */ @@ -18,12 +20,9 @@ export function getQueryTimeWindow(): { } { const timeZone = 'America/New_York'; - // How many hours ahead to look for events - const LOOKAHEAD_HOURS = 7; - const zonedNow = new Date(new Date().toLocaleString('en-US', { timeZone })); const start = zonedNow; - const end = addHours(start, LOOKAHEAD_HOURS); + const end = addHours(start, NOTIFICATION_LOOKAHEAD_HOURS); return { windowStartUnix: getUnixTime(start), From 235f6fc0f8ae433062c997f5821e0c6865b1463f Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Thu, 19 Feb 2026 14:30:38 -0500 Subject: [PATCH 09/18] Edited docker compose and env example --- .env.example | 1 + docker-compose.yml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 3e14e83..6d1a0ad 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ # Scraper CORNELL_DINING_API_URL= +SCRAPER_CRON_SCHEDULE=0 */2 * * * WORKERS= GOOGLE_SHEETS_API_KEY= FREEDGE_SHEET_ID= diff --git a/docker-compose.yml b/docker-compose.yml index b34d919..4aa33f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: app: - image: cornellappdev/eatery-blue-dev:${IMAGE_TAG} + image: cornellappdev/eatery-prod:${IMAGE_TAG} command: > sh -c "npx prisma db push && npm run start" configs: @@ -17,7 +17,7 @@ services: - scraper scraper: - image: cornellappdev/eatery-blue-dev:${IMAGE_TAG} + image: cornellappdev/eatery-prod:${IMAGE_TAG} env_file: .env networks: - eatery-network From bb2a44b93ea4220e234349e9c8289363fd8e8622 Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Sun, 22 Feb 2026 14:04:34 -0500 Subject: [PATCH 10/18] fix package lock error --- package-lock.json | 425 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 425 insertions(+) diff --git a/package-lock.json b/package-lock.json index ddc9c19..2412cce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -166,6 +166,74 @@ "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", @@ -183,6 +251,363 @@ "node": ">=18" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", From cc77a0a5a9814c20708a044b6d6875966c1e6c77 Mon Sep 17 00:00:00 2001 From: Joshua Dirga Date: Mon, 23 Feb 2026 14:59:57 -0500 Subject: [PATCH 11/18] Add cbord url to env --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 6d1a0ad..c23806e 100644 --- a/.env.example +++ b/.env.example @@ -30,3 +30,5 @@ FIREBASE_SERVICE_ACCOUNT_PATH=./firebase-service-account.json PRISMA_LOG_QUERIES=true PRISMA_LOG_ERRORS=true PRISMA_LOG_WARNINGS=true + +CBORD_BASE_URL= \ No newline at end of file From 7183829108fee756da5c7235b0730aebb9c32a1b Mon Sep 17 00:00:00 2001 From: Fanhao Yu Date: Wed, 25 Feb 2026 15:42:50 -0500 Subject: [PATCH 12/18] Fix empty menus --- package-lock.json | 671 +++++++++++++++++++--------------------------- prisma/scraper.ts | 12 +- 2 files changed, 276 insertions(+), 407 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1508fb..7e7e899 100644 --- a/package-lock.json +++ b/package-lock.json @@ -607,9 +607,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -650,30 +650,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", @@ -701,9 +677,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -713,7 +689,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -724,17 +700,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -758,23 +723,10 @@ "node": ">= 4" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -967,9 +919,9 @@ } }, "node_modules/@google-cloud/storage": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.17.3.tgz", - "integrity": "sha512-gOnCAbFgAYKRozywLsxagdevTF7Gm+2Ncz5u5CQAuOv/2VCa0rdGJWvJFDOftPx1tc+q8TXiC2pEJfFKu+yeMQ==", + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.19.0.tgz", + "integrity": "sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -979,7 +931,7 @@ "abort-controller": "^3.0.0", "async-retry": "^1.3.3", "duplexify": "^4.1.3", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.4", "gaxios": "^6.0.2", "google-auth-library": "^9.6.3", "html-entities": "^2.5.2", @@ -1157,44 +1109,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -1595,21 +1509,20 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", - "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/type-utils": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1619,23 +1532,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.46.3", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", - "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1645,20 +1558,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", - "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.46.3", - "@typescript-eslint/types": "^8.46.3", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1672,14 +1585,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", - "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1690,9 +1603,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", - "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -1707,17 +1620,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", - "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3", - "@typescript-eslint/utils": "8.46.3", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1727,14 +1640,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.3.tgz", - "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -1746,22 +1659,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", - "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.46.3", - "@typescript-eslint/tsconfig-utils": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/visitor-keys": "8.46.3", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1774,17 +1686,56 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.3.tgz", - "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.46.3", - "@typescript-eslint/types": "8.46.3", - "@typescript-eslint/typescript-estree": "8.46.3" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1794,19 +1745,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.46.3", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", - "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.46.3", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1817,13 +1768,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1888,9 +1839,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -2028,33 +1979,38 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -2673,9 +2629,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -2685,7 +2641,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2778,17 +2734,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -2812,19 +2757,6 @@ "node": ">= 4" } }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -3041,36 +2973,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3086,9 +2988,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.7.tgz", + "integrity": "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==", "funding": [ { "type": "github", @@ -3098,22 +3000,12 @@ "license": "MIT", "optional": true, "dependencies": { - "strnum": "^1.1.1" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/faye-websocket": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", @@ -3561,13 +3453,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/gtoken": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", @@ -3730,15 +3615,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ignore": { @@ -4013,12 +3902,12 @@ } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -4075,12 +3964,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -4130,9 +4019,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -4257,30 +4146,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -4316,19 +4181,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/ms": { @@ -4402,25 +4264,25 @@ "license": "MIT" }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", + "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", @@ -4439,15 +4301,27 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/nodemon/node_modules/has-flag": { @@ -4461,16 +4335,19 @@ } }, "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/nodemon/node_modules/supports-color": { @@ -4852,9 +4729,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -4866,27 +4743,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5021,17 +4877,6 @@ "node": ">=14" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -5048,30 +4893,6 @@ "node": ">= 18" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5339,9 +5160,9 @@ } }, "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", "funding": [ { "type": "github", @@ -5439,6 +5260,54 @@ "node": ">=18" } }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5478,9 +5347,9 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { diff --git a/prisma/scraper.ts b/prisma/scraper.ts index e7c5c94..69e89ca 100644 --- a/prisma/scraper.ts +++ b/prisma/scraper.ts @@ -152,7 +152,7 @@ async function fetchFreedgeDiningItems(): Promise { } } -function fetchCafeMenu( +function fetchNonDiningHallMenu( diningItems: RawDiningItem[], ): RawOperatingHourEventMenuCategory[] { const grouped: Record = {}; @@ -161,7 +161,6 @@ function fetchCafeMenu( if (!grouped[item.category]) { grouped[item.category] = []; } - grouped[item.category].push(item); } @@ -209,7 +208,7 @@ function transformStaticEatery(rawStaticEatery: RawStaticEatery) { type: event.descr || 'General', startTimestamp: startDate, endTimestamp: endDate, - menu: fetchCafeMenu(rawStaticEatery.diningItems) || [], + menu: fetchNonDiningHallMenu(rawStaticEatery.diningItems) || [], }); } } @@ -306,12 +305,12 @@ function transformEatery(rawEatery: RawEatery) { }> = []; let cafeDiningItems: RawOperatingHourEventMenuCategory[] = []; - if (eateryData.eateryTypes.includes(EateryType.CAFE)) { - cafeDiningItems = fetchCafeMenu(eateryData.diningItems); + if (!eateryData.eateryTypes.includes(EateryType.DINING_ROOM)) { + cafeDiningItems = fetchNonDiningHallMenu(eateryData.diningItems); } for (const operatingHour of rawEatery.operatingHours) { for (const event of operatingHour.events) { - if (eateryData.eateryTypes.includes(EateryType.CAFE)) { + if (!eateryData.eateryTypes.includes(EateryType.DINING_ROOM)) { events.push({ type: event.descr, startTimestamp: new Date(event.startTimestamp * 1000), @@ -319,6 +318,7 @@ function transformEatery(rawEatery: RawEatery) { menu: cafeDiningItems, }); } else { + if (!event.menu || event.menu.length === 0) continue; events.push({ type: event.descr, startTimestamp: new Date(event.startTimestamp * 1000), From 9c781b778192a33831cbbff8726c1e7525414160 Mon Sep 17 00:00:00 2001 From: Fanhao Yu Date: Thu, 26 Feb 2026 11:14:32 -0500 Subject: [PATCH 13/18] Fix package lock --- package-lock.json | 957 +++++++++++++++++++++++----------------------- 1 file changed, 470 insertions(+), 487 deletions(-) diff --git a/package-lock.json b/package-lock.json index 374f634..0ee3578 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,13 +41,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -56,14 +56,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -103,13 +103,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -119,33 +119,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -153,9 +153,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { @@ -167,9 +167,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", "cpu": [ "ppc64" ], @@ -184,9 +184,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", "cpu": [ "arm" ], @@ -201,9 +201,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", "cpu": [ "arm64" ], @@ -218,9 +218,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", "cpu": [ "x64" ], @@ -235,9 +235,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", "cpu": [ "arm64" ], @@ -252,9 +252,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", "cpu": [ "x64" ], @@ -269,9 +269,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", "cpu": [ "arm64" ], @@ -286,9 +286,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", "cpu": [ "x64" ], @@ -303,9 +303,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", "cpu": [ "arm" ], @@ -320,9 +320,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", "cpu": [ "arm64" ], @@ -337,9 +337,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", "cpu": [ "ia32" ], @@ -354,9 +354,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", "cpu": [ "loong64" ], @@ -371,9 +371,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", "cpu": [ "mips64el" ], @@ -388,9 +388,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", "cpu": [ "ppc64" ], @@ -405,9 +405,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", "cpu": [ "riscv64" ], @@ -422,9 +422,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", "cpu": [ "s390x" ], @@ -439,9 +439,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", "cpu": [ "x64" ], @@ -456,9 +456,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", "cpu": [ "arm64" ], @@ -473,9 +473,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", "cpu": [ "x64" ], @@ -490,9 +490,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", "cpu": [ "arm64" ], @@ -507,9 +507,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", "cpu": [ "x64" ], @@ -524,9 +524,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", "cpu": [ "arm64" ], @@ -541,9 +541,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", "cpu": [ "x64" ], @@ -558,9 +558,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", "cpu": [ "arm64" ], @@ -575,9 +575,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", "cpu": [ "ia32" ], @@ -592,9 +592,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", "cpu": [ "x64" ], @@ -652,6 +652,37 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-array/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/config-helpers": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", @@ -679,20 +710,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -702,6 +733,24 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -725,6 +774,19 @@ "node": ">= 4" } }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@eslint/js": { "version": "9.39.3", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", @@ -958,9 +1020,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.1.tgz", - "integrity": "sha512-sPxgEWtPUR3EnRJCEtbGZG2iX8LQDUls2wUS3o27jg07KqJFMq6YDeWvMo1wfpmy3rqRdS0rivpLwhqQtEyCuQ==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1122,9 +1184,9 @@ } }, "node_modules/@prisma/client": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.0.tgz", - "integrity": "sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.2.tgz", + "integrity": "sha512-gR2EMvfK/aTxsuooaDA32D8v+us/8AAet+C3J1cc04SW35FPdZYgLF+iN4NDLUgAaUGTKdAB0CYenu1TAgGdMg==", "hasInstallScript": true, "license": "Apache-2.0", "engines": { @@ -1144,9 +1206,9 @@ } }, "node_modules/@prisma/config": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.0.tgz", - "integrity": "sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.2.tgz", + "integrity": "sha512-kadBGDl+aUswv/zZMk9Mx0C8UZs1kjao8H9/JpI4Wh4SHZaM7zkTwiKn/iFLfRg+XtOAo/Z/c6pAYhijKl0nzQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -1157,53 +1219,53 @@ } }, "node_modules/@prisma/debug": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.0.tgz", - "integrity": "sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.2.tgz", + "integrity": "sha512-lFnEZsLdFLmEVCVNdskLDCL8Uup41GDfU0LUfquw+ercJC8ODTuL0WNKgOKmYxCJVvFwf0OuZBzW99DuWmoH2A==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.0.tgz", - "integrity": "sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.2.tgz", + "integrity": "sha512-TTkJ8r+uk/uqczX40wb+ODG0E0icVsMgwCTyTHXehaEfb0uo80M9g1aW1tEJrxmFHeOZFXdI2sTA1j1AgcHi4A==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0", - "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "@prisma/fetch-engine": "6.19.0", - "@prisma/get-platform": "6.19.0" + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.2", + "@prisma/get-platform": "6.19.2" } }, "node_modules/@prisma/engines-version": { - "version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773.tgz", - "integrity": "sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==", + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.0.tgz", - "integrity": "sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.2.tgz", + "integrity": "sha512-h4Ff4Pho+SR1S8XerMCC12X//oY2bG3Iug/fUnudfcXEUnIeRiBdXHFdGlGOgQ3HqKgosTEhkZMvGM9tWtYC+Q==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0", - "@prisma/engines-version": "6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773", - "@prisma/get-platform": "6.19.0" + "@prisma/debug": "6.19.2", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.2" } }, "node_modules/@prisma/get-platform": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.0.tgz", - "integrity": "sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.2.tgz", + "integrity": "sha512-PGLr06JUSTqIvztJtAzIxOwtWKtJm5WwOG6xpsgD37Rc84FpfUBGLKz65YpJBGtkRQGXTYEFie7pYALocC3MtA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "6.19.0" + "@prisma/debug": "6.19.2" } }, "node_modules/@protobufjs/aspromise": { @@ -1281,9 +1343,9 @@ "optional": true }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "devOptional": true, "license": "MIT" }, @@ -1336,6 +1398,7 @@ "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, "license": "MIT", "dependencies": { "@types/connect": "*", @@ -1353,6 +1416,7 @@ "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -1366,21 +1430,21 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.5.tgz", - "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "^1" + "@types/serve-static": "^2" } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", "dependencies": { @@ -1394,6 +1458,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1420,12 +1485,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -1433,9 +1492,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "24.10.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.14.tgz", + "integrity": "sha512-OowOUbD1lBCOFIPOZ8xnMIhgqA4sCutMiYOmPHL1PTLt5+y1XA+g2+yC9OOyz8p+deMZqPZLxfMjYIfrKsPeFg==", "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -1452,12 +1511,14 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, "license": "MIT" }, "node_modules/@types/request": { @@ -1477,29 +1538,20 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, @@ -1688,45 +1740,6 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { "version": "8.56.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", @@ -1809,9 +1822,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -1932,11 +1945,14 @@ "optional": true }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -2005,14 +2021,16 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2284,9 +2302,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "devOptional": true, "license": "MIT" }, @@ -2301,15 +2319,16 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -2330,6 +2349,15 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2432,9 +2460,9 @@ "license": "MIT" }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2579,9 +2607,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2592,32 +2620,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" } }, "node_modules/escalade": { @@ -2755,6 +2783,24 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", @@ -2778,6 +2824,19 @@ "node": ">= 4" } }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -2810,9 +2869,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2875,18 +2934,19 @@ } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -2934,19 +2994,10 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } - }, "node_modules/exsolve": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", - "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "devOptional": true, "license": "MIT" }, @@ -3008,10 +3059,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-builder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz", + "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true + }, "node_modules/fast-xml-parser": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.7.tgz", - "integrity": "sha512-JzVLro9NQv92pOM/jTCR6mHlJh2FGwtomH8ZQjhFj/R29P2Fnj38OgPJVtcvYw6SuKClhgYuwUZf5b3rd8u2mA==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz", + "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==", "funding": [ { "type": "github", @@ -3021,6 +3085,7 @@ "license": "MIT", "optional": true, "dependencies": { + "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { @@ -3066,9 +3131,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -3079,7 +3144,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/find-up": { @@ -3100,9 +3169,9 @@ } }, "node_modules/firebase-admin": { - "version": "13.6.0", - "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.0.tgz", - "integrity": "sha512-GdPA/t0+Cq8p1JnjFRBmxRxAGvF/kl2yfdhALl38PrRp325YxyQ5aNaHui0XmaKcKiGRFIJ/EgBNWFoDP0onjw==", + "version": "13.6.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.6.1.tgz", + "integrity": "sha512-Zgc6yPtmPxAZo+FoK6LMG6zpSEsoSK8ifIR+IqF4oWuC3uWZU40OjxgfLTSFcsRlj/k/wD66zNv2UiTRreCNSw==", "license": "Apache-2.0", "dependencies": { "@fastify/busboy": "^3.0.0", @@ -3126,9 +3195,9 @@ } }, "node_modules/firebase-admin/node_modules/@types/node": { - "version": "22.19.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.0.tgz", - "integrity": "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA==", + "version": "22.19.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.12.tgz", + "integrity": "sha512-0QEp0aPJYSyf6RrTjDB7HlKgNMTY+V2C7ESTaVt6G9gQ0rPLzTGz7OF2NXTLR5vcy7HJEtIUsyWLsfX0kTqJBA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -3342,9 +3411,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", "dev": true, "license": "MIT", "dependencies": { @@ -3564,28 +3633,23 @@ "optional": true }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/http-parser-js": { @@ -3834,9 +3898,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -3890,12 +3954,12 @@ "license": "MIT" }, "node_modules/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", "license": "MIT", "dependencies": { - "jws": "^3.2.2", + "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", @@ -3911,27 +3975,6 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/jwa": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", - "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", - "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", - "license": "MIT", - "dependencies": { - "jwa": "^1.4.2", - "safe-buffer": "^5.0.1" - } - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -3944,12 +3987,11 @@ } }, "node_modules/jwks-rsa": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.0.tgz", - "integrity": "sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.2.2.tgz", + "integrity": "sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==", "license": "MIT", "dependencies": { - "@types/express": "^4.17.20", "@types/jsonwebtoken": "^9.0.4", "debug": "^4.3.4", "jose": "^4.15.4", @@ -3960,30 +4002,6 @@ "node": ">=14" } }, - "node_modules/jwks-rsa/node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": { - "version": "4.19.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz", - "integrity": "sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "node_modules/jws": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", @@ -4190,28 +4208,35 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ms": { @@ -4322,29 +4347,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/nodemon/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -4355,22 +4357,6 @@ "node": ">=4" } }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4395,25 +4381,30 @@ } }, "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", "devOptional": true, "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", + "citty": "^0.2.0", "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" + "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" } }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/object-hash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", @@ -4623,9 +4614,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", "bin": { @@ -4639,15 +4630,15 @@ } }, "node_modules/prisma": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.0.tgz", - "integrity": "sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==", + "version": "6.19.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.2.tgz", + "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/config": "6.19.0", - "@prisma/engines": "6.19.0" + "@prisma/config": "6.19.2", + "@prisma/engines": "6.19.2" }, "bin": { "prisma": "build/index.js" @@ -4774,36 +4765,20 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" } }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -4941,9 +4916,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -4953,31 +4928,35 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -4987,6 +4966,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -5387,13 +5370,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.20.6", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", - "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.25.0", + "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "bin": { @@ -5656,9 +5639,9 @@ } }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" From 06e007f0ec176c2cf6aa55231c914eb6ba8e21bc Mon Sep 17 00:00:00 2001 From: Fanhao Yu Date: Thu, 26 Feb 2026 13:36:59 -0500 Subject: [PATCH 14/18] eateryController type fix --- src/eateries/eateryController.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/eateries/eateryController.ts b/src/eateries/eateryController.ts index 557215f..faae82a 100644 --- a/src/eateries/eateryController.ts +++ b/src/eateries/eateryController.ts @@ -11,7 +11,10 @@ export const getAllEateries = async (req: Request, res: Response) => { }; export const getEateryById = async (req: Request, res: Response) => { - const eateryId = parseInt(req.params.eateryId, 10); + const raw = req.params.eateryId; + const idStr = Array.isArray(raw) ? raw[0] : raw; + + const eateryId = parseInt(idStr, 10); const eatery = await eateryService.getEateryById(eateryId); return res.json(eatery); }; From 6d9d4fdb397b984bb90f8f725a59446f92795eec Mon Sep 17 00:00:00 2001 From: Fanhao Yu Date: Thu, 26 Feb 2026 14:02:52 -0500 Subject: [PATCH 15/18] update docker-compose --- docker-compose.yml | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4aa33f1..062c4b1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,14 +5,15 @@ services: image: cornellappdev/eatery-prod:${IMAGE_TAG} command: > sh -c "npx prisma db push && npm run start" - configs: - - source: firebase_service_account - target: /app/firebase-service-account.json + volumes: + - ./firebase-service-account.json:/app/firebase-service-account.json env_file: .env networks: - - eatery-network + eatery-network: + aliases: + - app ports: - - "8000:8000" + - '127.0.0.1:8000:8000' depends_on: - scraper @@ -21,12 +22,8 @@ services: env_file: .env networks: - eatery-network - command: ["sh", "-c", "npx prisma migrate deploy && npm run scrape:scheduled"] + command: + ['sh', '-c', 'npx prisma migrate deploy && npm run scrape:scheduled'] networks: eatery-network: - driver: overlay - -configs: - firebase_service_account: - file: ./firebase-service-account.json From a5cdf68e792583264b493c6de00f2035a20fe1e5 Mon Sep 17 00:00:00 2001 From: skyeslattery Date: Thu, 26 Feb 2026 14:14:14 -0500 Subject: [PATCH 16/18] fix favoriting eateries --- .../migration.sql | 19 +++++++++++++++++++ prisma/schema.prisma | 14 ++++++-------- src/users/userController.ts | 14 +++++++------- src/users/users.schema.ts | 2 +- 4 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 prisma/migrations/20260226190507_use_cornellid_for_favorites/migration.sql diff --git a/prisma/migrations/20260226190507_use_cornellid_for_favorites/migration.sql b/prisma/migrations/20260226190507_use_cornellid_for_favorites/migration.sql new file mode 100644 index 0000000..6e107b3 --- /dev/null +++ b/prisma/migrations/20260226190507_use_cornellid_for_favorites/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + + - The primary key for the `FavoritedEatery` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `eateryId` on the `FavoritedEatery` table. All the data in the column will be lost. + - Added the required column `cornellId` to the `FavoritedEatery` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "FavoritedEatery" DROP CONSTRAINT "FavoritedEatery_eateryId_fkey"; + +-- AlterTable +ALTER TABLE "Eatery" ALTER COLUMN "announcements" SET DEFAULT ARRAY[]::TEXT[]; + +-- AlterTable +ALTER TABLE "FavoritedEatery" DROP CONSTRAINT "FavoritedEatery_pkey", +DROP COLUMN "eateryId", +ADD COLUMN "cornellId" INTEGER NOT NULL, +ADD CONSTRAINT "FavoritedEatery_pkey" PRIMARY KEY ("userId", "cornellId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bdbdc03..8c3605b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -64,12 +64,11 @@ model User { } model FavoritedEatery { - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - userId Int - eatery Eatery @relation(fields: [eateryId], references: [id]) - eateryId Int + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + cornellId Int // Stable eatery identifier that persists across scrapes - @@id([userId, eateryId]) + @@id([userId, cornellId]) } model FCMToken { @@ -120,9 +119,8 @@ model Eatery { eateryTypes EateryType[] createdAt DateTime @default(now()) - events Event[] - favoritedEateries FavoritedEatery[] - reports Report[] + events Event[] + reports Report[] } model Event { diff --git a/src/users/userController.ts b/src/users/userController.ts index 523a31b..8cf733b 100644 --- a/src/users/userController.ts +++ b/src/users/userController.ts @@ -139,7 +139,7 @@ export const addFavoriteEatery = async ( next: NextFunction, ) => { try { - const { eateryId } = req.body; + const { cornellId } = req.body; const { userId } = req.user!; const user = await prisma.user.findUnique({ @@ -153,14 +153,14 @@ export const addFavoriteEatery = async ( // Use upsert to avoid crashing if the relation already exists await prisma.favoritedEatery.upsert({ where: { - userId_eateryId: { + userId_cornellId: { userId: user.id, - eateryId: eateryId, + cornellId: cornellId, }, }, create: { userId: user.id, - eateryId: eateryId, + cornellId: cornellId, }, update: {}, }); @@ -177,7 +177,7 @@ export const removeFavoriteEatery = async ( _next: NextFunction, ) => { try { - const { eateryId } = req.body; + const { cornellId } = req.body; const { userId } = req.user!; const user = await prisma.user.findUnique({ @@ -188,9 +188,9 @@ export const removeFavoriteEatery = async ( } await prisma.favoritedEatery.delete({ where: { - userId_eateryId: { + userId_cornellId: { userId: user.id, - eateryId: eateryId, + cornellId: cornellId, }, }, }); diff --git a/src/users/users.schema.ts b/src/users/users.schema.ts index aae185d..2a7f3a9 100644 --- a/src/users/users.schema.ts +++ b/src/users/users.schema.ts @@ -14,6 +14,6 @@ export const favoriteItemSchema = z.object({ export const favoriteEaterySchema = z.object({ body: z.object({ - eateryId: z.number().int().positive('eateryId must be a positive integer'), + cornellId: z.number().int('cornellId must be an integer'), }), }); From 9fda3a115077c00cdeabfa8092e040dba50afabd Mon Sep 17 00:00:00 2001 From: Fanhao Yu Date: Thu, 26 Feb 2026 22:43:27 -0500 Subject: [PATCH 17/18] Fix days query (filter by EST instead of UTC) --- src/constants.ts | 4 ++++ src/eateries/eateryService.ts | 40 ++++++++++++----------------------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 3311b0b..a32e6cc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -9,6 +9,10 @@ export const NOTIFICATION_LOOKAHEAD_HOURS = 7; export const ITUNES_LOOKUP_URL = 'https://itunes.apple.com/lookup?bundleId=org.cuappdev.eatery'; +export enum TimeZone { + EASTERN = 'America/New_York', +} + export enum Weekday { SUNDAY = 'Sunday', MONDAY = 'Monday', diff --git a/src/eateries/eateryService.ts b/src/eateries/eateryService.ts index 56e5bd6..dbcd90b 100644 --- a/src/eateries/eateryService.ts +++ b/src/eateries/eateryService.ts @@ -1,5 +1,9 @@ +import { endOfDay, startOfDay } from 'date-fns'; +import { fromZonedTime, toZonedTime } from 'date-fns-tz'; + import type { Event } from '@prisma/client'; +import { TimeZone } from '../constants.js'; import { NotFoundError } from '../utils/AppError.js'; import { getAllEateriesData, refreshCacheFromDB } from '../utils/cache.js'; import type { EateryWithEvents } from '../utils/cache.js'; @@ -22,7 +26,7 @@ export const getAllEateries = async (days?: number) => { return cachedEateries; } - // Calculate date range for filtering events using UTC to match event timestamps + // Calculate date range in EST // days=0 means today, days=1 means tomorrow, days=2 means day after tomorrow, etc. const now = new Date(); @@ -31,31 +35,13 @@ export const getAllEateries = async (days?: number) => { const targetTimestamp = now.getTime() + days * msPerDay; const targetDate = new Date(targetTimestamp); - // Set to start of day in UTC - const startOfDay = new Date( - Date.UTC( - targetDate.getUTCFullYear(), - targetDate.getUTCMonth(), - targetDate.getUTCDate(), - 0, - 0, - 0, - 0, - ), - ); + const targetDateEST = toZonedTime(targetDate, TimeZone.EASTERN); + const startOfDayEST = startOfDay(targetDateEST); + const endOfDayEST = endOfDay(targetDateEST); - // Set to end of day in UTC - const endOfDay = new Date( - Date.UTC( - targetDate.getUTCFullYear(), - targetDate.getUTCMonth(), - targetDate.getUTCDate(), - 23, - 59, - 59, - 999, - ), - ); + // Get the EST times in UTC to compare with event timestamps + const startOfDayUTC = fromZonedTime(startOfDayEST, TimeZone.EASTERN); + const endOfDayUTC = fromZonedTime(endOfDayEST, TimeZone.EASTERN); // Filter events to only include those on the specified day const filteredEateries = cachedEateries.map((eatery) => ({ @@ -64,8 +50,8 @@ export const getAllEateries = async (days?: number) => { const eventStart = new Date(event.startTimestamp); const eventEnd = new Date(event.endTimestamp); - // Include event if it overlaps with the target day - return eventStart <= endOfDay && eventEnd >= startOfDay; + // Include event if it overlaps with the target day in EST + return eventStart <= endOfDayUTC && eventEnd >= startOfDayUTC; }), })); From 46b7b07c1ef43ea5fa0d41b0161432adf04dc465 Mon Sep 17 00:00:00 2001 From: Peter Bidoshi <86176234+MrPeterss@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:21:58 -0500 Subject: [PATCH 18/18] Update deploy-prod.yml --- .github/workflows/deploy-prod.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index d263e94..d682527 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -45,7 +45,7 @@ jobs: script: | export IMAGE_TAG=${{ steps.vars.outputs.sha_short }} cd docker-compose - docker stack rm thestack + docker-compose down sleep 20s - docker stack deploy -c docker-compose.yml thestack - yes | docker system prune -a \ No newline at end of file + docker-compose up -d + yes | docker system prune -a