diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 46983fee..ce83edc2 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -62,7 +62,7 @@ jobs: CI: false PUBLIC_API_URL: ${{secrets.PUBLIC_API_URL}} PUBLIC_API_GRAPHQL_URL: ${{secrets.PUBLIC_API_GRAPHQL_URL}} - MONGO_URL: ${{secrets.MONGO_URL}} + DATABASE_URL: ${{ github.event_name == 'pull_request' && secrets.DEV_DATABASE_URL || secrets.PROD_DATABASE_URL }} SESSION_SECRET: ${{secrets.SESSION_SECRET}} GOOGLE_CLIENT: ${{secrets.GOOGLE_CLIENT}} GOOGLE_SECRET: ${{secrets.GOOGLE_SECRET}} diff --git a/README.md b/README.md index ecf2eee4..db7cd45f 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ Features include: - [PeterPortal API](https://github.com/icssc/peterportal-api-next) - Express - React +- tRPC - SST and AWS CDK -- MongoDB +- PostgreSQL +- Drizzle ORM - GraphQL - TypeScript - Vite @@ -77,7 +79,7 @@ git clone https://github.com//peterportal-client 5. Rename the `.env.example` file in the api directory to `.env`. This includes the minimum environment variables needed for running the backend. -6. (Optional) Set up your own MongoDB and Google OAuth to be able to test features that require signing in such as leaving reviews or saving roadmaps to your account. Add additional variables/secrets to the .env file from the previous step. +6. (Optional) Set up your own PostgreSQL database and Google OAuth to be able to test features that require signing in such as leaving reviews or saving roadmaps to your account. Add additional variables/secrets to the .env file from the previous step. ## Open Source Contribution Guide diff --git a/api/.env.example b/api/.env.example index a47a2850..9fa84f8d 100644 --- a/api/.env.example +++ b/api/.env.example @@ -3,9 +3,9 @@ PUBLIC_API_URL=https://api-next.peterportal.org/v1/rest/ PUBLIC_API_GRAPHQL_URL=https://api-next.peterportal.org/v1/graphql PORT=8080 # should match the port on the frontend proxy under site/vite.config.ts -# below are stubs of variables/secrets for MongoDB, google oauth, and recaptcha +# below are stubs of variables/secrets for the PostgreSQL database, google oauth, and recaptcha # these are necessary for features that require logging in -# MONGO_URL= +# DATABASE_URL= # SESSION_SECRET= # GOOGLE_CLIENT= # GOOGLE_SECRET= diff --git a/api/drizzle.config.ts b/api/drizzle.config.ts new file mode 100644 index 00000000..0b5c1c92 --- /dev/null +++ b/api/drizzle.config.ts @@ -0,0 +1,11 @@ +import 'dotenv/config'; +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + out: './drizzle', + schema: './src/db/schema.ts', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/api/package.json b/api/package.json index 2c9070ca..94fc91f0 100644 --- a/api/package.json +++ b/api/package.json @@ -12,20 +12,21 @@ "@trpc/server": "^10.45.1", "@vendia/serverless-express": "^4.12.6", "axios": "^1.6.8", - "connect-mongodb-session": "^5.0.0", + "connect-pg-simple": "^10.0.0", "cookie-parser": "^1.4.6", "dotenv-flow": "^4.1.0", + "drizzle-orm": "^0.35.3", "express": "^4.19.2", "express-session": "^1.18.0", - "mongoose": "^8.3.3", "morgan": "^1.10.0", "passport": "^0.7.0", "passport-google-oauth": "^2.0.0", + "pg": "^8.13.0", "zod": "^3.23.8" }, "devDependencies": { "@peterportal/types": "workspace:*", - "@types/connect-mongodb-session": "^2.4.7", + "@types/connect-pg-simple": "^7.0.3", "@types/cookie-parser": "^1.4.7", "@types/dotenv-flow": "^3.3.3", "@types/express": "^4.17.21", @@ -33,8 +34,10 @@ "@types/morgan": "^1.9.9", "@types/passport": "^1.0.16", "@types/passport-google-oauth": "^1.0.45", + "@types/pg": "^8.11.10", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", + "drizzle-kit": "^0.26.2", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "nodemon": "^3.1.7", diff --git a/api/src/app.ts b/api/src/app.ts index d7536273..bc2a99ae 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -3,22 +3,18 @@ * @module */ -import express from 'express'; +import express, { ErrorRequestHandler } from 'express'; import logger from 'morgan'; import cookieParser from 'cookie-parser'; import passport from 'passport'; import session from 'express-session'; -import MongoDBStore from 'connect-mongodb-session'; +import connectPgSimple from 'connect-pg-simple'; import dotenv from 'dotenv-flow'; import serverlessExpress from '@vendia/serverless-express'; import * as trpcExpress from '@trpc/server/adapters/express'; -import mongoose, { Mongoose } from 'mongoose'; // load env dotenv.config(); -// Configs -import { DB_NAME, COLLECTION_NAMES } from './helpers/mongo'; - // Custom Routes import authRouter from './controllers/auth'; @@ -30,26 +26,11 @@ import passportInit from './config/passport'; // instantiate app const app = express(); -// Setup mongo store for sessions -const mongoStore = MongoDBStore(session); +const PGStore = connectPgSimple(session); -let store: undefined | MongoDBStore.MongoDBStore; -if (process.env.MONGO_URL) { - store = new mongoStore({ - uri: process.env.MONGO_URL, - databaseName: DB_NAME, - collection: COLLECTION_NAMES.SESSIONS, - }); -} else { - console.log('MONGO_URL env var is not defined!'); +if (!process.env.DATABASE_URL) { + console.log('DATABASE_URL env var is not defined!'); } -// Catch errors -mongoose.connection.on('error', function (error) { - console.log(error); -}); -store?.on('error', function (error) { - console.log(error); -}); // Setup Passport and Sessions if (!process.env.SESSION_SECRET) { console.log('SESSION_SECRET env var is not defined!'); @@ -60,7 +41,10 @@ app.use( resave: false, saveUninitialized: false, cookie: { maxAge: SESSION_LENGTH }, - store: store, + store: new PGStore({ + conString: process.env.DATABASE_URL, + createTableIfMissing: true, + }), }), ); @@ -113,45 +97,20 @@ app.use('/api', expressRouter); /** * Error Handler */ -app.use(function (req, res) { - console.error(req); - res.status(500).json({ error: `Internal Serverless Error - '${req}'` }); -}); - -export const connect = async () => { - let conn: null | Mongoose = null; - const uri = process.env.MONGO_URL; - - if (conn == null && uri) { - conn = await mongoose.connect(uri!, { - dbName: DB_NAME, - serverSelectionTimeoutMS: 5000, - }); - } - return conn; +const errorHandler: ErrorRequestHandler = (err, req, res) => { + console.error(err); + res.status(500).json({ message: 'Internal Serverless Error', err }); }; +app.use(errorHandler); -let serverlessExpressInstance: ReturnType; -async function setup(event: unknown, context: unknown) { - await connect(); - serverlessExpressInstance = serverlessExpress({ app }); - return serverlessExpressInstance(event, context); -} // run local dev server const NODE_ENV = process.env.NODE_ENV ?? 'development'; if (NODE_ENV === 'development') { const port = process.env.PORT ?? 8080; - connect().then(() => { - app.listen(port, () => { - console.log('Listening on port', port); - }); + app.listen(port, () => { + console.log('Listening on port', port); }); } -export const handler = async (event: unknown, context: unknown) => { - if (serverlessExpressInstance) { - return serverlessExpressInstance(event, context); - } - return setup(event, context); -}; // export for serverless +export const handler = serverlessExpress({ app }); diff --git a/api/src/config/passport.ts b/api/src/config/passport.ts index 30c27a9a..ce350c82 100644 --- a/api/src/config/passport.ts +++ b/api/src/config/passport.ts @@ -2,7 +2,7 @@ @module PassportConfig */ -import { User } from '@peterportal/types'; +import { PassportUser } from '@peterportal/types'; import passport from 'passport'; import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth'; @@ -11,7 +11,7 @@ export default function passportInit() { done(null, user); }); - passport.deserializeUser(function (user: false | User | null | undefined, done) { + passport.deserializeUser(function (user: false | PassportUser | null | undefined, done) { done(null, user); }); diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 0a54992a..9a27f416 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -1,7 +1,9 @@ import express, { Request, Response } from 'express'; import passport from 'passport'; import { SESSION_LENGTH } from '../config/constants'; -import { User } from '@peterportal/types'; +import { PassportUser } from '@peterportal/types'; +import { db } from '../db'; +import { user } from '../db/schema'; const router = express.Router(); @@ -10,11 +12,23 @@ const router = express.Router(); * @param req Express Request Object * @param res Express Response Object */ -function successLogin(req: Request, res: Response) { - // set the user cookie - res.cookie('user', req.user, { +async function successLogin(req: Request, res: Response) { + const { + email, + name, + id: googleId, + picture, + } = req.user as { email: string; id: string; name: string; picture: string }; + // upsert user data in db + const userData = await db + .insert(user) + .values({ googleId, name, email, picture }) + .onConflictDoUpdate({ target: user.googleId, set: { name, email, picture } }) + .returning(); + res.cookie('user', true, { maxAge: SESSION_LENGTH, }); + req.session.userId = userData[0].id; // redirect browser to the page they came from const returnTo = req.session.returnTo ?? '/'; delete req.session.returnTo; @@ -51,7 +65,7 @@ router.get('/google/callback', function (req, res) { 'google', { failureRedirect: '/', session: true }, // provides user information to determine whether or not to authenticate - function (err: Error, user: User | false | null) { + function (err: Error, user: PassportUser | false | null) { if (err) return console.error(err); if (!user) return console.error('Invalid login data'); // manually login @@ -60,7 +74,7 @@ router.get('/google/callback', function (req, res) { // check if user is an admin const allowedUsers = JSON.parse(process.env.ADMIN_EMAILS ?? '[]'); if (allowedUsers.includes(user.email)) { - req.session.passport!.isAdmin = true; + req.session.isAdmin = true; } req.session.returnTo = returnTo; successLogin(req, res); diff --git a/api/src/controllers/index.ts b/api/src/controllers/index.ts index 7ed87028..7e1d776c 100644 --- a/api/src/controllers/index.ts +++ b/api/src/controllers/index.ts @@ -4,6 +4,7 @@ import professorsRouter from './professors'; import reportsRouter from './reports'; import reviewsRouter from './reviews'; import roadmapsRouter from './roadmap'; +import { savedCoursesRouter } from './savedCourses'; import scheduleRouter from './schedule'; import usersRouter from './users'; @@ -13,6 +14,7 @@ export const appRouter = router({ roadmaps: roadmapsRouter, reports: reportsRouter, reviews: reviewsRouter, + savedCourses: savedCoursesRouter, schedule: scheduleRouter, users: usersRouter, }); diff --git a/api/src/controllers/reports.ts b/api/src/controllers/reports.ts index 71cf8f80..8472e485 100644 --- a/api/src/controllers/reports.ts +++ b/api/src/controllers/reports.ts @@ -2,45 +2,35 @@ @module ReportsRoute */ -import Report from '../models/report'; import { adminProcedure, publicProcedure, router } from '../helpers/trpc'; import { z } from 'zod'; import { ReportData, reportSubmission } from '@peterportal/types'; +import { db } from '../db'; +import { report } from '../db/schema'; +import { eq } from 'drizzle-orm'; +import { datesToStrings } from '../helpers/date'; const reportsRouter = router({ /** * Get all reports */ get: adminProcedure.query(async () => { - const reports = await Report.find(); - return reports; + return (await db.select().from(report)).map((report) => datesToStrings(report)) as ReportData[]; }), /** * Add a report */ add: publicProcedure.input(reportSubmission).mutation(async ({ input }) => { - const report = new Report(input); - await report.save(); - + await db.insert(report).values(input); return input; }), /** - * Delete a report + * Delete reports by review id */ - delete: adminProcedure - .input(z.object({ id: z.string().optional(), reviewID: z.string().optional() })) - .mutation(async ({ input }) => { - if (input.id) { - // delete report by report id - return await Report.deleteOne({ _id: input.id }); - } else if (input.reviewID) { - // delete report(s) by review id - return await Report.deleteMany({ reviewID: input.reviewID }); - } else { - // no id or reviewID specified - return false; - } - }), + delete: adminProcedure.input(z.object({ reviewId: z.number() })).mutation(async ({ input }) => { + await db.delete(report).where(eq(report.reviewId, input.reviewId)); + return true; + }), }); export default reportsRouter; diff --git a/api/src/controllers/reviews.ts b/api/src/controllers/reviews.ts index 09033933..58350d9f 100644 --- a/api/src/controllers/reviews.ts +++ b/api/src/controllers/reviews.ts @@ -3,185 +3,160 @@ */ import { verifyCaptcha } from '../helpers/recaptcha'; -import Review from '../models/review'; -import Vote from '../models/vote'; -import Report from '../models/report'; -import mongoose from 'mongoose'; import { adminProcedure, publicProcedure, router, userProcedure } from '../helpers/trpc'; import { z } from 'zod'; -import { editReviewSubmission, featuredQuery, ReviewData, reviewSubmission } from '@peterportal/types'; +import { + anonymousName, + editReviewSubmission, + featuredQuery, + FeaturedReviewData, + ReviewData, + reviewSubmission, +} from '@peterportal/types'; import { TRPCError } from '@trpc/server'; +import { db } from '../db'; +import { review, user, vote } from '../db/schema'; +import { and, count, desc, eq, sql } from 'drizzle-orm'; +import { datesToStrings } from '../helpers/date'; -interface ReviewFilter { - courseID?: string; - professorID?: string; - userID?: string; - _id?: mongoose.Types.ObjectId; - verified?: boolean; -} +async function userWroteReview(userId: number | undefined, reviewId: number) { + if (!userId) { + return false; + } -interface VoteData { - _id?: string; - userID: string; - reviewID: string; - score: number; + return ( + ( + await db + .select({ count: count() }) + .from(review) + .where(and(eq(review.id, reviewId), eq(review.userId, userId))) + )[0].count > 0 + ); } -async function userWroteReview(userID: string | undefined, reviewID: string) { - if (!userID) { - return false; +async function getReviews( + where: { + courseId?: string; + professorId?: string; + userId?: number; + reviewId?: number; + verified?: boolean; + }, + sessUserId?: number, +) { + const { courseId, professorId, userId, reviewId, verified } = where; + const userVoteSubquery = db + .select({ reviewId: vote.reviewId, userVote: vote.vote }) + .from(vote) + .where(eq(vote.userId, sessUserId!)) + .as('user_vote_query'); + const results = await db + .select({ + review: review, + score: sql`COALESCE(SUM(${vote.vote}), 0)`.mapWith(Number), + userDisplay: user.name, + userVote: sql`COALESCE(${userVoteSubquery.userVote}, 0)`.mapWith(Number), + }) + .from(review) + .where( + and( + courseId ? eq(review.courseId, courseId) : undefined, + professorId ? eq(review.professorId, professorId) : undefined, + userId ? eq(review.userId, userId) : undefined, + reviewId ? eq(review.id, reviewId) : undefined, + verified !== undefined ? eq(review.verified, verified) : undefined, + ), + ) + .leftJoin(vote, eq(vote.reviewId, review.id)) + .leftJoin(user, eq(user.id, review.userId)) + .leftJoin(userVoteSubquery, eq(userVoteSubquery.reviewId, review.id)) + .groupBy(review.id, user.name, userVoteSubquery.userVote); + + if (results) { + return results.map(({ review, score, userDisplay, userVote }) => + datesToStrings({ + ...review, + score, + userDisplay: review.anonymous ? anonymousName : userDisplay!, + userVote: userVote, + authored: sessUserId === review.userId, + }), + ) as ReviewData[]; + } else { + return []; } - - return await Review.exists({ _id: reviewID, userID: userID }); } const reviewsRouter = router({ + getUsersReviews: userProcedure.query(async ({ ctx }) => { + return await getReviews({ userId: ctx.session.userId }, ctx.session.userId); + }), /** * Query reviews */ get: publicProcedure .input( z.object({ - courseID: z.string().optional(), - professorID: z.string().optional(), + courseId: z.string().optional(), + professorId: z.string().optional(), verified: z.boolean().optional(), - userID: z.string().optional(), - reviewID: z.string().optional(), + reviewId: z.number().optional(), }), ) .query(async ({ input, ctx }) => { - const { courseID, professorID, userID, reviewID, verified } = input; - - const query: ReviewFilter = { - courseID, - professorID, - userID, - _id: reviewID ? new mongoose.Types.ObjectId(reviewID) : undefined, - verified, - }; - - // remove null params - for (const param in query) { - if (query[param as keyof ReviewFilter] === null || query[param as keyof ReviewFilter] === undefined) { - delete query[param as keyof ReviewFilter]; - } - } - - const pipeline = [ - { - $match: query, - }, - { - $addFields: { - _id: { - $toString: '$_id', - }, - }, - }, - { - $lookup: { - from: 'votes', - let: { - cmpUserID: ctx.session.passport?.user.id, - cmpReviewID: '$_id', - }, - pipeline: [ - { - $match: { - $expr: { - $and: [ - { - $eq: ['$$cmpUserID', '$userID'], - }, - { - $eq: ['$$cmpReviewID', '$reviewID'], - }, - ], - }, - }, - }, - ], - as: 'userVote', - }, - }, - { - $addFields: { - userVote: { - $cond: { - if: { - $ne: ['$userVote', []], - }, - then: { - $getField: { - field: 'score', - input: { - $first: '$userVote', - }, - }, - }, - else: 0, - }, - }, - }, - }, - ]; - - const reviews = await Review.aggregate(pipeline); - if (reviews) { - return reviews; - } else { - return []; - } + return await getReviews({ ...input }, ctx.session.userId); }), /** * Add a review */ add: userProcedure.input(reviewSubmission).mutation(async ({ input, ctx }) => { + const userId = ctx.session.userId!; // check if user is trusted - const verifiedCount = await Review.find({ - userID: ctx.session.passport!.user.id, - verified: true, - }) - .countDocuments() - .exec(); - - const review = { + const { verifiedCount } = ( + await db + .select({ verifiedCount: count() }) + .from(review) + .where(and(eq(review.userId, userId), eq(review.verified, true))) + )[0]; + const reviewToAdd = { ...input, - userDisplay: input.userDisplay === 'Anonymous Peter' ? 'Anonymous Peter' : ctx.session.passport!.user.name, - userID: ctx.session.passport!.user.id, + userId: userId, verified: verifiedCount >= 3, // auto-verify if use has 3+ verified reviews }; - //check if review already exists for same professor, course, and user - const query: ReviewFilter = { - courseID: input.courseID, - professorID: input.professorID, - userID: ctx.session.passport?.user.id, - }; - - const reviews = await Review.find(query); - if (reviews?.length > 0) + /** @todo: do a check for existing review on the frontend, remove this for the sake of speed since constraints will already prevent duplicate reviews on insertion */ + //check if review already exists for same professor, course, and user (do this before verifying captcha) + const existingReview = await db + .select({ count: count() }) + .from(review) + .where( + and(eq(review.userId, userId), eq(review.courseId, input.courseId), eq(review.professorId, input.professorId)), + ); + if (existingReview[0].count > 0) { throw new TRPCError({ code: 'BAD_REQUEST', message: 'You have already reviewed this professor and course!' }); + } // Verify the captcha - const verifyResponse = await verifyCaptcha(review); + const verifyResponse = await verifyCaptcha(reviewToAdd); if (!verifyResponse?.success) throw new TRPCError({ code: 'BAD_REQUEST', message: 'ReCAPTCHA token is invalid' }); - delete review.captchaToken; // so it doesn't get stored in DB - // add review to mongo - return (await new Review(review).save()) as unknown as ReviewData; + const addedReview = (await db.insert(review).values(reviewToAdd).returning())[0]; + return datesToStrings({ + ...addedReview, + userDisplay: input.anonymous ? anonymousName : ctx.session.passport!.user.name, + score: 0, + userVote: 0, + authored: true, + }) as ReviewData; }), /** * Delete a review (user can delete their own or admin can delete any through reports) */ - delete: userProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => { - if (ctx.session.passport!.isAdmin || (await userWroteReview(ctx.session.passport!.user.id, input.id))) { - await Review.deleteOne({ _id: input.id }); - // delete all votes and reports associated with review - await Vote.deleteMany({ reviewID: input.id }); - await Report.deleteMany({ reviewID: input.id }); + delete: userProcedure.input(z.object({ id: z.number() })).mutation(async ({ input, ctx }) => { + if (ctx.session.isAdmin || (await userWroteReview(ctx.session.userId, input.id))) { + await db.delete(review).where(eq(review.id, input.id)); return true; } else { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Must be an admin or review author to delete reviews!' }); @@ -191,77 +166,59 @@ const reviewsRouter = router({ /** * Vote on a review */ - vote: userProcedure.input(z.object({ id: z.string(), upvote: z.boolean() })).mutation(async ({ input, ctx }) => { - //get id and delta score from initial vote - const id = input.id; - let deltaScore = input.upvote ? 1 : -1; - //query to search for a vote matching the same review and user - const currentVotes = { - userID: ctx.session.passport!.user.id, - reviewID: id, - }; - //either length 1 or 0 array(ideally) 0 if no existing vote, 1 if existing vote - const existingVote = (await Vote.find(currentVotes)) as VoteData[]; - //check if there is an existing vote and it has the same vote as the previous vote - if (existingVote.length != 0 && deltaScore == existingVote[0].score) { - //remove the vote - - //delete the existing vote from the votes collection - await Vote.deleteMany(currentVotes); - //update the votes document with a lowered score - await Review.updateOne({ _id: id }, { $inc: { score: -1 * deltaScore } }); - - return { deltaScore: -1 * deltaScore }; - } else if (existingVote.length != 0 && deltaScore != existingVote[0].score) { - //there is an existing vote but the vote was different - deltaScore *= 2; - //*2 to reverse the old vote and implement the new one - await Review.updateOne({ _id: id }, { $inc: { score: deltaScore } }); - //override old vote with new data - await Vote.updateOne({ _id: existingVote[0]._id }, { $set: { score: deltaScore / 2 } }); + vote: userProcedure + .input(z.object({ id: z.number(), vote: z.number().int().min(-1).max(1) })) + .mutation(async ({ input, ctx }) => { + if (input.vote === 0) { + await db.delete(vote).where(and(eq(vote.userId, ctx.session.userId!), eq(vote.reviewId, input.id))); + return true; + } - return { deltaScore: deltaScore }; - } else { - //no old vote, just add in new vote data - await Review.updateOne({ _id: id }, { $inc: { score: deltaScore } }); - //sends in vote - await new Vote({ userID: ctx.session.passport!.user.id, reviewID: id, score: deltaScore }).save(); - return { deltaScore: deltaScore }; - } - }), + await db + .insert(vote) + .values({ userId: ctx.session.userId!, reviewId: input.id, vote: input.vote }) + .onConflictDoUpdate({ target: [vote.userId, vote.reviewId], set: { vote: input.vote } }); + }), - verify: adminProcedure.input(z.object({ id: z.string() })).mutation(async ({ input }) => { - return await Review.updateOne({ _id: input.id }, { verified: true }); + verify: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => { + await db.update(review).set({ verified: true }).where(eq(review.id, input.id)); + return true; }), edit: userProcedure.input(editReviewSubmission).mutation(async ({ input, ctx }) => { - if (!(await userWroteReview(ctx.session.passport!.user.id, input._id))) { + if (!(await userWroteReview(ctx.session.userId, input.id))) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'You are not the author of this review.' }); } - const { _id, ...updateWithoutId } = input; - await Review.updateOne({ _id }, updateWithoutId); - return input; + const { id, ...updateWithoutId } = input; + await db.update(review).set(updateWithoutId).where(eq(review.id, id)); + return true; }), /** * Get featured review for a course or professor */ featured: publicProcedure.input(featuredQuery).query(async ({ input }) => { - // search by professor or course field - let field = ''; - if (input.type == 'course') { - field = 'courseID'; - } else if (input.type == 'professor') { - field = 'professorID'; - } - - // find first review with the highest score - const review = await Review.findOne({ [field]: input.id }) - .sort({ reviewContent: -1, score: -1, verified: -1 }) - .limit(1); - - return review; + const voteSubQuery = db + .select({ reviewId: vote.reviewId, score: sql`sum(${vote.vote})`.mapWith(Number).as('score') }) + .from(vote) + .groupBy(vote.reviewId) + .as('vote_query'); + + const featuredReviewCriteria = [desc(review.content), desc(voteSubQuery.score), desc(review.verified)]; + + const field = input.type === 'course' ? review.courseId : review?.professorId; + const featuredReview = ( + await db + .select() + .from(review) + .where(eq(field, input.id)) + .leftJoin(voteSubQuery, eq(voteSubQuery.reviewId, review.id)) + .orderBy(...featuredReviewCriteria) + .limit(1) + )[0]; + + return datesToStrings(featuredReview.review) as FeaturedReviewData; }), /** @@ -270,29 +227,15 @@ const reviewsRouter = router({ scores: publicProcedure .input(z.object({ type: z.enum(['course', 'professor']), id: z.string() })) .query(async ({ input }) => { - // match filters all reviews with given field - // group aggregates by field - let matchField = ''; - let groupField = ''; - if (input.type == 'professor') { - matchField = 'professorID'; - groupField = '$courseID'; - } else if (input.type == 'course') { - matchField = 'courseID'; - groupField = '$professorID'; - } - - // execute aggregation on the reviews collection - - const array = await Review.aggregate([ - { $match: { [matchField]: input.id } }, - { $group: { _id: groupField, score: { $avg: '$rating' } } }, - ]); - - // rename id to name - const results = array.map((v) => { - return { name: v._id as string, score: v.score as number }; - }); + const field = input.type === 'course' ? review.courseId : review.professorId; + const otherField = input.type === 'course' ? review.professorId : review.courseId; + + const results = await db + .select({ name: otherField, score: sql`COALESCE(SUM(${vote.vote}), 0)`.mapWith(Number) }) + .from(review) + .where(eq(field, input.id)) + .leftJoin(vote, eq(vote.reviewId, review.id)) + .groupBy(otherField); return results; }), diff --git a/api/src/controllers/roadmap.ts b/api/src/controllers/roadmap.ts index b2d07551..eea2b4ac 100644 --- a/api/src/controllers/roadmap.ts +++ b/api/src/controllers/roadmap.ts @@ -1,27 +1,88 @@ -import Roadmap from '../models/roadmap'; import { router, userProcedure } from '../helpers/trpc'; -import { z } from 'zod'; -import { mongoRoadmap, MongoRoadmap } from '@peterportal/types'; +import { SavedPlannerData, savedRoadmap, SavedRoadmap, TransferData } from '@peterportal/types'; +import { db } from '../db'; +import { planner, transferredCourse, user } from '../db/schema'; +import { and, asc, eq, inArray, not } from 'drizzle-orm'; const roadmapsRouter = router({ /** * Get a user's roadmap */ - get: userProcedure.input(z.object({ userID: z.string() })).query(async ({ input }) => { - const roadmap = await Roadmap.findOne({ userID: input.userID }); + get: userProcedure.query(async ({ ctx }) => { + const [planners, transfers, timestamp] = await Promise.all([ + db + .select({ id: planner.id, name: planner.name, content: planner.years }) + .from(planner) + .where(eq(planner.userId, ctx.session.userId!)) + .orderBy(asc(planner.id)), + db + .select({ name: transferredCourse.courseName, units: transferredCourse.units }) + .from(transferredCourse) + .where(eq(transferredCourse.userId, ctx.session.userId!)), + db.select({ timestamp: user.lastRoadmapEditAt }).from(user).where(eq(user.id, ctx.session.userId!)), + ]); + const roadmap: SavedRoadmap = { + planners: planners as SavedPlannerData[], + transfers: transfers as TransferData[], + timestamp: timestamp[0].timestamp?.toISOString(), + }; return roadmap; }), /** * Save a user's roadmap */ - save: userProcedure.input(mongoRoadmap).mutation(async ({ input }) => { - const { userID, roadmap, coursebag } = input; - if (await Roadmap.exists({ userID })) { - return await Roadmap.replaceOne({ userID }, { roadmap, userID, coursebag }); - } else { - // add roadmap to mongo - return await new Roadmap({ roadmap, userID, coursebag }).save(); - } + save: userProcedure.input(savedRoadmap).mutation(async ({ input, ctx }) => { + const { planners, transfers, timestamp } = input; + const userId = ctx.session.userId!; + + const plannerUpdates = planners + .filter((planner) => planner.id !== undefined) + .map((plannerData) => + db + .update(planner) + .set({ name: plannerData.name, years: plannerData.content }) + .where(eq(planner.id, plannerData.id!)), + ); + + const newPlannersToAdd = planners + .filter((planner) => planner.id === undefined) + .map((planner) => ({ userId, name: planner.name, years: planner.content })); + + // Delete any existing planners that are not in the planners array (user removed them), then insert any new planners (to avoid race when deleting new ones) + const plannerInsertionsAndDeletions = db + .delete(planner) + .where( + and( + eq(planner.userId, userId), + not(inArray(planner.id, planners.map((p) => p.id).filter((id) => id !== undefined) as number[])), + ), + ) + .then(() => { + if (newPlannersToAdd.length > 0) { + return db.insert(planner).values(newPlannersToAdd); + } + }); + + const replaceTransferredCourses = db + .delete(transferredCourse) + .where(eq(transferredCourse.userId, userId)) + .then(() => { + return db + .insert(transferredCourse) + .values(transfers.map((transfer) => ({ userId, courseName: transfer.name, units: transfer.units }))); + }); + + const updateLastEditTimestamp = db + .update(user) + .set({ lastRoadmapEditAt: new Date(timestamp!) }) + .where(eq(user.id, userId)); + + await Promise.all([ + ...plannerUpdates, + plannerInsertionsAndDeletions, + replaceTransferredCourses, + updateLastEditTimestamp, + ]); }), }); diff --git a/api/src/controllers/savedCourses.ts b/api/src/controllers/savedCourses.ts new file mode 100644 index 00000000..c8501357 --- /dev/null +++ b/api/src/controllers/savedCourses.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; +import { router, userProcedure } from '../helpers/trpc'; +import { db } from '../db'; +import { savedCourse } from '../db/schema'; +import { and, eq } from 'drizzle-orm'; + +export const savedCoursesRouter = router({ + get: userProcedure.query(async ({ ctx }) => { + return ( + await db + .select({ courseId: savedCourse.courseId }) + .from(savedCourse) + .where(eq(savedCourse.userId, ctx.session.userId!)) + ).map((row) => row.courseId); + }), + add: userProcedure.input(z.object({ courseId: z.string() })).mutation(async ({ input, ctx }) => { + await db.insert(savedCourse).values({ userId: ctx.session.userId!, courseId: input.courseId }); + }), + remove: userProcedure.input(z.object({ courseId: z.string() })).mutation(async ({ input, ctx }) => { + await db + .delete(savedCourse) + .where(and(eq(savedCourse.userId, ctx.session.userId!), eq(savedCourse.courseId, input.courseId))); + }), +}); diff --git a/api/src/controllers/users.ts b/api/src/controllers/users.ts index 3188f605..b2c55cf0 100644 --- a/api/src/controllers/users.ts +++ b/api/src/controllers/users.ts @@ -2,57 +2,33 @@ @module UsersRoute */ -import Preference from '../models/preference'; -import { publicProcedure, router, userProcedure } from '../helpers/trpc'; -import { userPreferences, UserPreferences } from '@peterportal/types'; +import { router, userProcedure } from '../helpers/trpc'; +import { theme, UserData } from '@peterportal/types'; +import { db } from '../db'; +import { user } from '../db/schema'; +import { eq } from 'drizzle-orm'; +import { z } from 'zod'; +import { datesToStrings } from '../helpers/date'; const usersRouter = router({ /** - * Get the user's session data + * Get the user's data */ - get: publicProcedure.query(async ({ ctx }) => { - return ctx.session; - }), - - /** - * Get the user's theme preferences - */ - getPreferences: userProcedure.query(async ({ ctx }): Promise => { - const userID = ctx.session.passport?.user.id; - const preference = await Preference.findOne({ userID: userID }); - - return (preference ? preference : {}) as UserPreferences; + get: userProcedure.query(async ({ ctx }) => { + const userData = (await db.select().from(user).where(eq(user.id, ctx.session.userId!)))[0]; + return datesToStrings({ + ...userData, + isAdmin: ctx.session.isAdmin, + }) as UserData; }), /** * Configure the user's theme preferences */ - setPreferences: userProcedure.input(userPreferences).mutation(async ({ input, ctx }) => { - const userID = ctx.session.passport?.user.id; - - // make user's preference doc if it doesn't exist - if (!(await Preference.exists({ userID }))) { - await Preference.create({ userID, theme: input.theme }); - } - - // set the preferences - await Preference.updateOne({ userID }, input); - - // echo back body + setTheme: userProcedure.input(z.object({ theme })).mutation(async ({ input, ctx }) => { + await db.update(user).set({ theme: input.theme }).where(eq(user.id, ctx.session.userId!)); return input; }), - - /** - * Get whether or not a user is an admin - */ - isAdmin: publicProcedure.query(async ({ ctx }) => { - // not logged in - if (!ctx.session?.passport) { - return { admin: false }; - } else { - return { admin: ctx.session.passport.isAdmin as boolean }; - } - }), }); export default usersRouter; diff --git a/api/src/db/index.ts b/api/src/db/index.ts new file mode 100644 index 00000000..b61b0535 --- /dev/null +++ b/api/src/db/index.ts @@ -0,0 +1,4 @@ +import 'dotenv-flow/config'; +import { drizzle } from 'drizzle-orm/node-postgres'; + +export const db = drizzle(process.env.DATABASE_URL!); diff --git a/api/src/db/schema.ts b/api/src/db/schema.ts new file mode 100644 index 00000000..5e77fb50 --- /dev/null +++ b/api/src/db/schema.ts @@ -0,0 +1,141 @@ +import { SavedPlannerYearData } from '@peterportal/types'; +import { sql } from 'drizzle-orm'; +import { + boolean, + check, + index, + integer, + jsonb, + pgTable, + primaryKey, + real, + text, + timestamp, + unique, +} from 'drizzle-orm/pg-core'; + +export const user = pgTable( + 'user', + { + id: integer('id').primaryKey().generatedAlwaysAsIdentity(), + googleId: text('google_id').notNull(), + name: text('name').notNull(), + email: text('email').notNull(), + picture: text('picture').notNull(), + theme: text('theme'), + lastRoadmapEditAt: timestamp('last_roadmap_edit_at'), + }, + (table) => ({ + uniqueGoogleId: unique('unique_google_id').on(table.googleId), + }), +); + +export const report = pgTable( + 'report', + { + id: integer('id').primaryKey().generatedAlwaysAsIdentity(), + reviewId: integer('review_id') + .notNull() + .references(() => review.id, { onDelete: 'cascade' }), + reason: text('reason').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + }, + (table) => ({ + reviewIdIdx: index('reports_review_id_idx').on(table.reviewId), + }), +); + +export const review = pgTable( + 'review', + { + id: integer('id').primaryKey().generatedAlwaysAsIdentity(), + professorId: text('professor_id').notNull(), + courseId: text('course_id').notNull(), + userId: integer('user_id') + .notNull() + .references(() => user.id), + anonymous: boolean('anonymous').notNull(), + content: text('content'), + rating: integer('rating').notNull(), + difficulty: integer('difficulty').notNull(), + gradeReceived: text('grade_received').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow(), + forCredit: boolean('for_credit').notNull(), + quarter: text('quarter').notNull(), + takeAgain: boolean('take_again').notNull(), + textbook: boolean('textbook').notNull(), + attendance: boolean('attendance').notNull(), + tags: text('tags').array(), + verified: boolean('verified').notNull().default(false), + }, + (table) => ({ + ratingCheck: check('rating_check', sql`${table.rating} >= 1 AND ${table.rating} <= 5`), + difficultyCheck: check('difficulty_check', sql`${table.difficulty} >= 1 AND ${table.difficulty} <= 5`), + unique: unique('unique_review').on(table.userId, table.professorId, table.courseId), + professorIdIdx: index('reviews_professor_id_idx').on(table.professorId), + courseIdIdx: index('reviews_course_id_idx').on(table.courseId), + }), +); + +export const planner = pgTable( + 'planner', + { + id: integer('id').primaryKey().generatedAlwaysAsIdentity(), + userId: integer('user_id').references(() => user.id), + name: text('name').notNull(), + years: jsonb('years').$type().array().notNull(), + }, + (table) => ({ + userIdIdx: index('planners_user_id_idx').on(table.userId), + }), +); + +export const transferredCourse = pgTable( + 'transferred_course', + { + userId: integer('user_id').references(() => user.id), + courseName: text('course_name'), + units: real('units'), + }, + (table) => ({ + userIdIdx: index('transferred_courses_user_id_idx').on(table.userId), + }), +); + +export const vote = pgTable( + 'vote', + { + reviewId: integer('review_id') + .notNull() + .references(() => review.id, { onDelete: 'cascade' }), + userId: integer('user_id') + .notNull() + .references(() => user.id), + vote: integer('vote').notNull(), + }, + (table) => ({ + voteCheck: check('votes_vote_check', sql`${table.vote} = 1 OR ${table.vote} = -1`), + primaryKey: primaryKey({ columns: [table.reviewId, table.userId] }), + userIdIdx: index('votes_user_id_idx').on(table.userId), + }), +); + +export const savedCourse = pgTable( + 'saved_course', + { + userId: integer('user_id') + .references(() => user.id) + .notNull(), + courseId: text('course_id').notNull(), + }, + (table) => ({ + primaryKey: primaryKey({ columns: [table.userId, table.courseId] }), + }), +); + +export const session = pgTable('session', { + sid: text('sid').primaryKey(), + sess: jsonb('sess').notNull(), + expire: timestamp('expire').notNull(), +}); diff --git a/api/src/helpers/date.ts b/api/src/helpers/date.ts new file mode 100644 index 00000000..b2231afd --- /dev/null +++ b/api/src/helpers/date.ts @@ -0,0 +1,17 @@ +type DateConvertible = { + [K in keyof T]: T[K] extends Date ? string : T[K] extends Date | null ? string | null : T[K]; +}; + +/** + * Converts all Date object values to ISO strings + */ +export function datesToStrings(object: T): DateConvertible { + for (const key of Object.keys(object) as Array) { + const value = object[key]; + if (value instanceof Date) { + object[key] = value.toISOString() as T[keyof T]; + } + } + + return object as DateConvertible; +} diff --git a/api/src/helpers/mongo.ts b/api/src/helpers/mongo.ts deleted file mode 100644 index a69a9839..00000000 --- a/api/src/helpers/mongo.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - @module MongoHelper -*/ - -/** - * Database name to use in mongo - */ -const DB_NAME = process.env.NODE_ENV == 'production' ? 'peterPortalDB' : 'peterPortalDevDB'; - -/** - * Collection names to use in mongo - */ -const COLLECTION_NAMES = { - SESSIONS: 'sessions', - REVIEWS: 'reviews', - ROADMAPS: 'roadmaps', - VOTES: 'votes', - REPORTS: 'reports', - PREFERENCES: 'preferences', -}; - -export { DB_NAME, COLLECTION_NAMES }; diff --git a/api/src/helpers/trpc.ts b/api/src/helpers/trpc.ts index 2a4b5d7c..a4edf3aa 100644 --- a/api/src/helpers/trpc.ts +++ b/api/src/helpers/trpc.ts @@ -14,13 +14,13 @@ export const router = trpc.router; export const publicProcedure = trpc.procedure; export const adminProcedure = publicProcedure.use(async (opts) => { - if (!opts.ctx.session.passport?.isAdmin) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not an admin' }); + if (!opts.ctx.session.isAdmin) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not an admin' }); return opts.next(opts); }); export const userProcedure = publicProcedure.use(async (opts) => { - if (!opts.ctx.session.passport) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not logged in' }); + if (!opts.ctx.session.userId) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not logged in' }); return opts.next(opts); }); diff --git a/api/src/models/preference.ts b/api/src/models/preference.ts deleted file mode 100644 index a595be9c..00000000 --- a/api/src/models/preference.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { theme } from '@peterportal/types'; -import mongoose from 'mongoose'; - -const preferenceSchema = new mongoose.Schema({ - theme: { - type: String, - enum: theme.options, - required: true, - }, - userID: { - type: String, - required: true, - }, -}); - -const Preference = mongoose.model('Preference', preferenceSchema); - -export default Preference; diff --git a/api/src/models/report.ts b/api/src/models/report.ts deleted file mode 100644 index b5a1ae1d..00000000 --- a/api/src/models/report.ts +++ /dev/null @@ -1,22 +0,0 @@ -import mongoose from 'mongoose'; - -const reportSchema = new mongoose.Schema({ - reviewID: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Review', - required: true, - }, - reason: { - type: String, - required: true, - }, - timestamp: { - type: Date, - required: true, - default: Date.now, - }, -}); - -const Report = mongoose.model('Report', reportSchema); - -export default Report; diff --git a/api/src/models/review.ts b/api/src/models/review.ts deleted file mode 100644 index 3e9e2de7..00000000 --- a/api/src/models/review.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { grades, tags } from '@peterportal/types'; -import mongoose from 'mongoose'; - -const reviewSchema = new mongoose.Schema({ - professorID: { - type: String, - required: true, - }, - courseID: { - type: String, - required: true, - }, - userID: { - type: String, - ref: 'User', - required: true, - }, - userDisplay: { - type: String, - required: true, - default: 'Anonymous Peter', - }, - reviewContent: { - type: String, - required: false, - default: '', - }, - rating: { - type: Number, - required: true, - default: 3, - min: 0, - max: 5, - }, - difficulty: { - type: Number, - required: true, - min: 0, - default: 3, - max: 5, - }, - score: { - type: Number, - required: false, - min: 0, - default: 0, - }, - quarter: { - type: String, - required: true, - }, - timestamp: { - type: Date, - required: true, - default: Date.now, - }, - gradeReceived: { - type: String, - required: true, - enum: grades, - }, - forCredit: { - type: Boolean, - required: false, - }, - takeAgain: { - type: Boolean, - required: false, - }, - attendance: { - type: Boolean, - required: false, - }, - tags: { - type: [String], - required: false, - enum: tags, - }, - verified: { - type: Boolean, - required: true, - default: false, - }, -}); - -const Review = mongoose.model('Review', reviewSchema); - -export default Review; diff --git a/api/src/models/roadmap.ts b/api/src/models/roadmap.ts deleted file mode 100644 index 075f28e4..00000000 --- a/api/src/models/roadmap.ts +++ /dev/null @@ -1,20 +0,0 @@ -import mongoose from 'mongoose'; - -const roadmapSchema = new mongoose.Schema({ - roadmap: { - type: mongoose.Schema.Types.Mixed, - required: true, - }, - userID: { - type: String, - required: true, - }, - coursebag: { - type: [String], - required: true, - }, -}); - -const Roadmap = mongoose.model('Roadmap', roadmapSchema); - -export default Roadmap; diff --git a/api/src/models/session.ts b/api/src/models/session.ts deleted file mode 100644 index cc7cd79c..00000000 --- a/api/src/models/session.ts +++ /dev/null @@ -1,63 +0,0 @@ -import mongoose from 'mongoose'; - -const sessionSchema = new mongoose.Schema({ - expires: { - type: Date, - required: true, - }, - session: { - cookie: { - originalMaxAge: { - type: Number, - }, - expires: { - type: Date, - }, - secure: { - type: Boolean, - default: null, - }, - httpOnly: { - type: Boolean, - default: true, - }, - domain: { - type: String, - default: null, - }, - path: { - type: String, - default: '/', - }, - sameSite: { - type: Boolean, - default: null, - }, - }, - passport: { - user: { - id: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - }, - email: { - type: String, - required: true, - match: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/, - }, - name: { - type: String, - required: true, - }, - picture: { - type: String, - required: false, - }, - }, - }, - }, -}); - -const Session = mongoose.model('Session', sessionSchema); - -export default Session; diff --git a/api/src/models/vote.ts b/api/src/models/vote.ts deleted file mode 100644 index c44be808..00000000 --- a/api/src/models/vote.ts +++ /dev/null @@ -1,26 +0,0 @@ -import mongoose from 'mongoose'; - -const voteSchema = new mongoose.Schema({ - userID: { - type: String, - required: true, - }, - reviewID: { - type: String, - required: true, - }, - timestamp: { - type: Date, - required: true, - default: Date.now, - }, - score: { - type: Number, - required: true, - enum: [-1, 1], - }, -}); - -const Vote = mongoose.model('Vote', voteSchema); - -export default Vote; diff --git a/api/src/types/session.ts b/api/src/types/session.ts index 0936191d..e949f385 100644 --- a/api/src/types/session.ts +++ b/api/src/types/session.ts @@ -1,20 +1,18 @@ -import { User } from '@peterportal/types'; +import { PassportUser } from '@peterportal/types'; import 'express-session'; declare module 'express-session' { export interface SessionData { - /** - * Store custom data in passport - */ passport: PassportData; /** * URL to return to when finish authentication */ returnTo: string; + userId: number; + isAdmin: boolean; } export interface PassportData { - isAdmin: boolean; - user: User; + user: PassportUser; } } diff --git a/api/tsconfig.json b/api/tsconfig.json index f7253419..171ef9d2 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2020", - "module": "es2020", + "module": "esnext", "outDir": "./dist", "rootDir": "./src", "strict": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d90f275..1209ada3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 4.1.0 sst: specifier: 2.41.5 - version: 2.41.5(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@types/react@18.3.1)(aws-crt@1.21.2) + version: 2.41.5(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@types/react@18.3.1)(aws-crt@1.21.2)(pg@8.13.0) devDependencies: '@typescript-eslint/eslint-plugin': specifier: ^7.8.0 @@ -54,24 +54,24 @@ importers: axios: specifier: ^1.6.8 version: 1.6.8 - connect-mongodb-session: - specifier: ^5.0.0 - version: 5.0.0(@aws-sdk/credential-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(socks@2.8.3) + connect-pg-simple: + specifier: ^10.0.0 + version: 10.0.0 cookie-parser: specifier: ^1.4.6 version: 1.4.6 dotenv-flow: specifier: ^4.1.0 version: 4.1.0 + drizzle-orm: + specifier: ^0.35.3 + version: 0.35.3(@aws-sdk/client-rds-data@3.556.0)(@libsql/client-wasm@0.14.0)(@types/pg@8.11.10)(@types/react@18.3.1)(kysely@0.25.0)(pg@8.13.0)(react@18.3.1) express: specifier: ^4.19.2 version: 4.19.2 express-session: specifier: ^1.18.0 version: 1.18.0 - mongoose: - specifier: ^8.3.3 - version: 8.3.3(@aws-sdk/credential-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(socks@2.8.3) morgan: specifier: ^1.10.0 version: 1.10.0 @@ -81,6 +81,9 @@ importers: passport-google-oauth: specifier: ^2.0.0 version: 2.0.0 + pg: + specifier: ^8.13.0 + version: 8.13.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -88,9 +91,9 @@ importers: '@peterportal/types': specifier: workspace:* version: link:../types - '@types/connect-mongodb-session': - specifier: ^2.4.7 - version: 2.4.7(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) + '@types/connect-pg-simple': + specifier: ^7.0.3 + version: 7.0.3 '@types/cookie-parser': specifier: ^1.4.7 version: 1.4.7 @@ -112,12 +115,18 @@ importers: '@types/passport-google-oauth': specifier: ^1.0.45 version: 1.0.45 + '@types/pg': + specifier: ^8.11.10 + version: 8.11.10 '@typescript-eslint/eslint-plugin': specifier: ^7.8.0 version: 7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.2))(eslint@8.57.0)(typescript@5.4.2) '@typescript-eslint/parser': specifier: ^7.8.0 version: 7.8.0(eslint@8.57.0)(typescript@5.4.2) + drizzle-kit: + specifier: ^0.26.2 + version: 0.26.2 eslint: specifier: ^8.57.0 version: 8.57.0 @@ -907,6 +916,9 @@ packages: resolution: {integrity: sha512-B90/1OmA9mf9bEJnplLj7FGf+N2v2ikB68c/9W9uXmCa4ep/V00ymCiivwGLyeuzQRW33tcj4+KxZ2utfmu39Q==} engines: {node: '>=12'} + '@drizzle-team/brocli@0.10.1': + resolution: {integrity: sha512-AHy0vjc+n/4w/8Mif+w86qpppHuF3AyXbcWW+R/W7GNA3F5/p2nuhlkCJaTXSLZheB4l1rtHzOfr9A7NwoR/Zg==} + '@envelop/core@3.0.6': resolution: {integrity: sha512-06t1xCPXq6QFN7W1JUEf68aCwYN0OUDNAIoJe7bAqhaoa2vn7NCcuX1VHkJ/OWpmElUgCsRO6RiBbIru1in0Ig==} @@ -919,6 +931,20 @@ packages: '@envelop/core': ^3.0.6 graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.20.2': resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} engines: {node: '>=12'} @@ -937,6 +963,18 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.20.2': resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} engines: {node: '>=12'} @@ -955,6 +993,18 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.20.2': resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} engines: {node: '>=12'} @@ -973,6 +1023,18 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.20.2': resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} engines: {node: '>=12'} @@ -991,6 +1053,18 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.20.2': resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} engines: {node: '>=12'} @@ -1009,6 +1083,18 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.20.2': resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} engines: {node: '>=12'} @@ -1027,6 +1113,18 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.20.2': resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} engines: {node: '>=12'} @@ -1045,6 +1143,18 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.20.2': resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} engines: {node: '>=12'} @@ -1063,6 +1173,18 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.20.2': resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} engines: {node: '>=12'} @@ -1081,6 +1203,18 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.20.2': resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} engines: {node: '>=12'} @@ -1099,6 +1233,18 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.20.2': resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} engines: {node: '>=12'} @@ -1117,6 +1263,18 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.20.2': resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} engines: {node: '>=12'} @@ -1135,6 +1293,18 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.20.2': resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} engines: {node: '>=12'} @@ -1153,6 +1323,18 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.20.2': resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} engines: {node: '>=12'} @@ -1171,6 +1353,18 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.20.2': resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} engines: {node: '>=12'} @@ -1189,6 +1383,18 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.20.2': resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} engines: {node: '>=12'} @@ -1207,6 +1413,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.20.2': resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} engines: {node: '>=12'} @@ -1225,6 +1443,18 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.20.2': resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} engines: {node: '>=12'} @@ -1249,6 +1479,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.20.2': resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} engines: {node: '>=12'} @@ -1267,6 +1509,18 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.20.2': resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} engines: {node: '>=12'} @@ -1285,6 +1539,18 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.20.2': resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} engines: {node: '>=12'} @@ -1303,6 +1569,18 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.20.2': resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} engines: {node: '>=12'} @@ -1321,6 +1599,18 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.20.2': resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} engines: {node: '>=12'} @@ -1439,13 +1729,18 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@libsql/client-wasm@0.14.0': + resolution: {integrity: sha512-gB/jtz0xuwrqAHApBv9e9JSew2030Fhj2edyZ83InZ4yPj/Q2LTUlEhaspEYT0T0xsAGqPy38uGrmq/OGS+DdQ==} + bundledDependencies: + - '@libsql/libsql-wasm-experimental' + + '@libsql/core@0.14.0': + resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} + '@lukeed/ms@2.0.2': resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} - '@mongodb-js/saslprep@1.1.5': - resolution: {integrity: sha512-XLNOMH66KhJzUJNwT/qlMnS4WsNDWD5ASdyaSH3EtK+F4r/CFGa3jT4GNi4mfOitGvWXtdLgQJkQjxSVrio+jA==} - '@nivo/annotations@0.84.0': resolution: {integrity: sha512-g3n+WaZgRza7fZVQZrrxq1cLS+6vmjhWGmQqEynFmKM2f11F7gdkHLhGMYosayjZ0Sb/bMUXvBSkUbyKli7NVw==} peerDependencies: @@ -1969,8 +2264,8 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} - '@types/connect-mongodb-session@2.4.7': - resolution: {integrity: sha512-bu9LyhcYjcX3OD7ghPP1d1559uZvwahHClLbpJm56Kqi2xOCpGCr6omYzi0gmrvY9oAv/N2JDc6peT2P/cgr1Q==} + '@types/connect-pg-simple@7.0.3': + resolution: {integrity: sha512-NGCy9WBlW2bw+J/QlLnFZ9WjoGs6tMo3LAut6mY4kK+XHzue//lpNVpAvYRpIwM969vBRAM2Re0izUvV6kt+NA==} '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -2053,6 +2348,9 @@ packages: '@types/passport@1.0.16': resolution: {integrity: sha512-FD0qD5hbPWQzaM0wHUnJ/T0BBCJBxCeemtnCwc/ThhTg3x9jfrAcRUmj5Dopza+MfFS9acTe3wk7rcVnRIp/0A==} + '@types/pg@8.11.10': + resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==} + '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} @@ -2101,15 +2399,6 @@ packages: '@types/warning@3.0.3': resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} - '@types/webidl-conversions@7.0.3': - resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} - - '@types/whatwg-url@11.0.4': - resolution: {integrity: sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==} - - '@types/whatwg-url@8.2.2': - resolution: {integrity: sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==} - '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} @@ -2259,10 +2548,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - archetype@0.13.0: - resolution: {integrity: sha512-ts/rng/A4UQPw1ZuQWWZvR2T0q2S5+zQGBH0RPsSlmyIAsZuIGEm1rgRga2NJnHODBbW/jVWMZIWbtlEyrS7JQ==} - engines: {node: '>= 4.0.0'} - archiver-utils@2.1.0: resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} engines: {node: '>= 6'} @@ -2458,14 +2743,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bson@4.7.2: - resolution: {integrity: sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==} - engines: {node: '>=6.9.0'} - - bson@6.6.0: - resolution: {integrity: sha512-BVINv2SgcMjL4oYbBuCQTpE3/VKOSxrOA8Cj/wQP7izSzlBGVomdm+TcUd0Pzy0ytLSSDweCKQ6X3f5veM5LQA==} - engines: {node: '>=16.20.1'} - buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -2618,8 +2895,9 @@ packages: resolution: {integrity: sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==} engines: {node: '>=12'} - connect-mongodb-session@5.0.0: - resolution: {integrity: sha512-Yxr09hsafOvMgwiqeMi6fk5VudMXm2z5/IaJJp4EO6/tzajqsCUJTX4x9541URNwBL43lB8wSR7q77JpIdddSA==} + connect-pg-simple@10.0.0: + resolution: {integrity: sha512-pBGVazlqiMrackzCr0eKhn4LO5trJXsOX0nQoey9wCOayh80MYtThCbq8eoLsjpiWgiok/h+1/uti9/2/Una8A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=22.0.0} constructs@10.3.0: resolution: {integrity: sha512-vbK8i3rIb/xwZxSpTjz3SagHn1qq9BChLEfy5Hf6fB3/2eFbrwt2n9kHwQcS0CPTRBesreeAcsJfMq2229FnbQ==} @@ -2857,6 +3135,100 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + drizzle-kit@0.26.2: + resolution: {integrity: sha512-cMq8omEKywjIy5KcqUo6LvEFxkl8/zYHsgYjFVXjmPWWtuW4blcz+YW9+oIhoaALgs2ebRjzXwsJgN9i6P49Dw==} + hasBin: true + + drizzle-orm@0.35.3: + resolution: {integrity: sha512-Uv6N+b36x4BaZlxc96e+ag7RnMapBLGhc4SSi2F7RDwqYJipWjaU/P68RUp1FbW9r+mxoDp8nMz2Eece8PJxfA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@electric-sql/pglite': '>=0.1.1' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.1' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=13.2.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + react: '>=18' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dset@3.1.3: resolution: {integrity: sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==} engines: {node: '>=4'} @@ -2934,11 +3306,26 @@ packages: resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} engines: {node: '>= 0.4'} + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + esbuild@0.18.13: resolution: {integrity: sha512-vhg/WR/Oiu4oUIkVhmfcc23G6/zWuEQKFS+yiosSHe4aN6+DQRXIfeloYGibIfVhkr4wyfuVsGNLr+sQU1rWWw==} engines: {node: '>=12'} hasBin: true + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -3395,10 +3782,6 @@ packages: invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ip-address@9.0.5: - resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} - engines: {node: '>= 12'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -3580,6 +3963,9 @@ packages: jquery@3.7.1: resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + js-sdsl@4.3.0: resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} @@ -3590,9 +3976,6 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - jsbn@1.1.0: - resolution: {integrity: sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==} - jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -3631,10 +4014,6 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - kareem@2.6.3: - resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} - engines: {node: '>=12.0.0'} - keyboard-key@1.1.0: resolution: {integrity: sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==} @@ -3710,9 +4089,6 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} @@ -3731,9 +4107,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.set@4.3.2: - resolution: {integrity: sha512-4hNPN5jlm/N/HLMCO43v8BXKq9Z7QdAGc/VGrRD61w8gN9g/6jF9A4L1pbUgBLCffi0w9VsXfTOij5x8iTyFvg==} - lodash.truncate@4.4.2: resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} @@ -3780,9 +4153,6 @@ packages: memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - memory-pager@1.5.0: - resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} - merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} @@ -3862,61 +4232,12 @@ packages: mnemonist@0.39.8: resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} - mongodb-connection-string-url@2.6.0: - resolution: {integrity: sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==} + morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} - mongodb-connection-string-url@3.0.0: - resolution: {integrity: sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==} - - mongodb@4.17.2: - resolution: {integrity: sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==} - engines: {node: '>=12.9.0'} - - mongodb@6.5.0: - resolution: {integrity: sha512-Fozq68InT+JKABGLqctgtb8P56pRrJFkbhW0ux+x1mdHeyinor8oNzJqwLjV/t5X5nJGfTlluxfyMnOXNggIUA==} - engines: {node: '>=16.20.1'} - peerDependencies: - '@aws-sdk/credential-providers': ^3.188.0 - '@mongodb-js/zstd': ^1.1.0 - gcp-metadata: ^5.2.0 - kerberos: ^2.0.1 - mongodb-client-encryption: '>=6.0.0 <7' - snappy: ^7.2.2 - socks: ^2.7.1 - peerDependenciesMeta: - '@aws-sdk/credential-providers': - optional: true - '@mongodb-js/zstd': - optional: true - gcp-metadata: - optional: true - kerberos: - optional: true - mongodb-client-encryption: - optional: true - snappy: - optional: true - socks: - optional: true - - mongoose@8.3.3: - resolution: {integrity: sha512-3kSk0db9DM2tLttCdS6WNRqewPleamFEa4Vz/Qldc0dB4Zow/FiZxb9GExHTJjBZQ9T2xiGleQ3GzRrES3hhsA==} - engines: {node: '>=16.20.1'} - - morgan@1.10.0: - resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} - engines: {node: '>= 0.8.0'} - - mpath@0.8.4: - resolution: {integrity: sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==} - engines: {node: '>=4.0.0'} - - mpath@0.9.0: - resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==} - engines: {node: '>=4.0.0'} - - mqtt-packet@6.10.0: - resolution: {integrity: sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==} + mqtt-packet@6.10.0: + resolution: {integrity: sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==} mqtt@4.2.8: resolution: {integrity: sha512-DJYjlXODVXtSDecN8jnNzi6ItX3+ufGsEs9OB3YV24HtkRrh7kpx8L5M1LuyF0KzaiGtWr2PzDcMGAY60KGOSA==} @@ -3928,10 +4249,6 @@ packages: engines: {node: '>=10.0.0'} hasBin: true - mquery@5.0.0: - resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==} - engines: {node: '>=14.0.0'} - ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -4025,6 +4342,9 @@ packages: obliterator@2.0.4: resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + oidc-token-hash@5.0.3: resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==} engines: {node: ^10.13.0 || >=12.0.0} @@ -4165,6 +4485,48 @@ packages: peterportal-api-next-types@1.0.0-rc.3: resolution: {integrity: sha512-TlylpK4OcfxG91aze6urHv9ei7/gX/GLrR7v8ktPVX31r/xPfYS77ygi6wcawwguka6dk8/JHQukYdy5tlOYgA==} + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.7.0: + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + + pg@8.13.0: + resolution: {integrity: sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -4193,6 +4555,41 @@ packages: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4606,9 +5003,6 @@ packages: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} - sift@16.0.1: - resolution: {integrity: sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==} - signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -4640,26 +5034,23 @@ packages: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - - socks@2.8.3: - resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - source-map-js@1.2.0: resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} engines: {node: '>=0.10.0'} - sparse-bitfield@3.0.3: - resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} split2@3.2.2: resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} - sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} sst-aws-cdk@2.132.1: resolution: {integrity: sha512-zep3sr+XkCmw9rHjgYQc44G+6s4eB/yxgLCCoi1h6R2AN8IarwZ4aHRwxAfigmJTPXYO6kmsxBIOcwjKy+oi3g==} @@ -4796,14 +5187,6 @@ packages: resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true - tr46@3.0.0: - resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} - engines: {node: '>=12'} - - tr46@4.1.1: - resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} - engines: {node: '>=14'} - tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -5015,24 +5398,12 @@ packages: webcrypto-core@1.7.9: resolution: {integrity: sha512-FE+a4PPkOmBbgNDIyRmcHhgXn+2ClRl3JzJdDu/P4+B8y81LqKe6RAsI9b3lAOHe1T1BMkSjsRHTYRikImZnVA==} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} - websoc-fuzzy-search@1.0.1: resolution: {integrity: sha512-1UlDdT2OvMxVIczNSQzI+vSoojfagbORdwtMQiLAnG1zVLG9Po6x5+VWNysi8w5xoxE2NootQH72HzoenLygDg==} websocket-stream@5.5.2: resolution: {integrity: sha512-8z49MKIHbGk3C4HtuHWDtYX8mYej1wWabjthC/RupM9ngeukU4IWoM46dgth1UOS/T4/IqgEdCDJuMe2039OQQ==} - whatwg-url@11.0.0: - resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} - engines: {node: '>=12'} - - whatwg-url@13.0.0: - resolution: {integrity: sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==} - engines: {node: '>=16'} - which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -5251,7 +5622,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-crypto/supports-web-crypto': 3.0.0 '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.535.0 + '@aws-sdk/types': 3.567.0 '@aws-sdk/util-locate-window': 3.535.0 '@aws-sdk/util-utf8-browser': 3.259.0 tslib: 1.14.1 @@ -5259,7 +5630,7 @@ snapshots: '@aws-crypto/sha256-js@3.0.0': dependencies: '@aws-crypto/util': 3.0.0 - '@aws-sdk/types': 3.535.0 + '@aws-sdk/types': 3.567.0 tslib: 1.14.1 '@aws-crypto/sha256-js@5.2.0': @@ -5274,7 +5645,7 @@ snapshots: '@aws-crypto/util@3.0.0': dependencies: - '@aws-sdk/types': 3.535.0 + '@aws-sdk/types': 3.567.0 '@aws-sdk/util-utf8-browser': 3.259.0 tslib: 1.14.1 @@ -5657,6 +6028,52 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-rds-data@3.556.0': + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/core': 3.556.0 + '@aws-sdk/credential-provider-node': 3.556.0 + '@aws-sdk/middleware-host-header': 3.535.0 + '@aws-sdk/middleware-logger': 3.535.0 + '@aws-sdk/middleware-recursion-detection': 3.535.0 + '@aws-sdk/middleware-user-agent': 3.540.0 + '@aws-sdk/region-config-resolver': 3.535.0 + '@aws-sdk/types': 3.535.0 + '@aws-sdk/util-endpoints': 3.540.0 + '@aws-sdk/util-user-agent-browser': 3.535.0 + '@aws-sdk/util-user-agent-node': 3.535.0 + '@smithy/config-resolver': 2.2.0 + '@smithy/core': 1.4.2 + '@smithy/fetch-http-handler': 2.5.0 + '@smithy/hash-node': 2.2.0 + '@smithy/invalid-dependency': 2.2.0 + '@smithy/middleware-content-length': 2.2.0 + '@smithy/middleware-endpoint': 2.5.1 + '@smithy/middleware-retry': 2.3.1 + '@smithy/middleware-serde': 2.3.0 + '@smithy/middleware-stack': 2.2.0 + '@smithy/node-config-provider': 2.3.0 + '@smithy/node-http-handler': 2.5.0 + '@smithy/protocol-http': 3.3.0 + '@smithy/smithy-client': 2.5.1 + '@smithy/types': 2.12.0 + '@smithy/url-parser': 2.2.0 + '@smithy/util-base64': 2.3.0 + '@smithy/util-body-length-browser': 2.2.0 + '@smithy/util-body-length-node': 2.3.0 + '@smithy/util-defaults-mode-browser': 2.2.1 + '@smithy/util-defaults-mode-node': 2.3.1 + '@smithy/util-endpoints': 1.2.0 + '@smithy/util-middleware': 2.2.0 + '@smithy/util-retry': 2.2.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + optional: true + '@aws-sdk/client-rds-data@3.556.0(aws-crt@1.21.2)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 @@ -5856,13 +6273,59 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-sso-oidc@3.556.0(@aws-sdk/credential-provider-node@3.556.0)': + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/core': 3.556.0 + '@aws-sdk/credential-provider-node': 3.556.0 + '@aws-sdk/middleware-host-header': 3.535.0 + '@aws-sdk/middleware-logger': 3.535.0 + '@aws-sdk/middleware-recursion-detection': 3.535.0 + '@aws-sdk/middleware-user-agent': 3.540.0 + '@aws-sdk/region-config-resolver': 3.535.0 + '@aws-sdk/types': 3.535.0 + '@aws-sdk/util-endpoints': 3.540.0 + '@aws-sdk/util-user-agent-browser': 3.535.0 + '@aws-sdk/util-user-agent-node': 3.535.0 + '@smithy/config-resolver': 2.2.0 + '@smithy/core': 1.4.2 + '@smithy/fetch-http-handler': 2.5.0 + '@smithy/hash-node': 2.2.0 + '@smithy/invalid-dependency': 2.2.0 + '@smithy/middleware-content-length': 2.2.0 + '@smithy/middleware-endpoint': 2.5.1 + '@smithy/middleware-retry': 2.3.1 + '@smithy/middleware-serde': 2.3.0 + '@smithy/middleware-stack': 2.2.0 + '@smithy/node-config-provider': 2.3.0 + '@smithy/node-http-handler': 2.5.0 + '@smithy/protocol-http': 3.3.0 + '@smithy/smithy-client': 2.5.1 + '@smithy/types': 2.12.0 + '@smithy/url-parser': 2.2.0 + '@smithy/util-base64': 2.3.0 + '@smithy/util-body-length-browser': 2.2.0 + '@smithy/util-body-length-node': 2.3.0 + '@smithy/util-defaults-mode-browser': 2.2.1 + '@smithy/util-defaults-mode-node': 2.3.1 + '@smithy/util-endpoints': 1.2.0 + '@smithy/util-middleware': 2.2.0 + '@smithy/util-retry': 2.2.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + optional: true + '@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sts': 3.572.0(aws-crt@1.21.2) '@aws-sdk/core': 3.572.0 - '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) + '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) '@aws-sdk/middleware-host-header': 3.567.0 '@aws-sdk/middleware-logger': 3.568.0 '@aws-sdk/middleware-recursion-detection': 3.567.0 @@ -5902,22 +6365,20 @@ snapshots: - '@aws-sdk/client-sts' - aws-crt - '@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2)': + '@aws-sdk/client-sso@3.556.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.572.0(aws-crt@1.21.2) - '@aws-sdk/core': 3.572.0 - '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) - '@aws-sdk/middleware-host-header': 3.567.0 - '@aws-sdk/middleware-logger': 3.568.0 - '@aws-sdk/middleware-recursion-detection': 3.567.0 - '@aws-sdk/middleware-user-agent': 3.572.0 - '@aws-sdk/region-config-resolver': 3.572.0 - '@aws-sdk/types': 3.567.0 - '@aws-sdk/util-endpoints': 3.572.0 - '@aws-sdk/util-user-agent-browser': 3.567.0 - '@aws-sdk/util-user-agent-node': 3.568.0(aws-crt@1.21.2) + '@aws-sdk/core': 3.556.0 + '@aws-sdk/middleware-host-header': 3.535.0 + '@aws-sdk/middleware-logger': 3.535.0 + '@aws-sdk/middleware-recursion-detection': 3.535.0 + '@aws-sdk/middleware-user-agent': 3.540.0 + '@aws-sdk/region-config-resolver': 3.535.0 + '@aws-sdk/types': 3.535.0 + '@aws-sdk/util-endpoints': 3.540.0 + '@aws-sdk/util-user-agent-browser': 3.535.0 + '@aws-sdk/util-user-agent-node': 3.535.0 '@smithy/config-resolver': 2.2.0 '@smithy/core': 1.4.2 '@smithy/fetch-http-handler': 2.5.0 @@ -5946,6 +6407,7 @@ snapshots: tslib: 2.6.2 transitivePeerDependencies: - aws-crt + optional: true '@aws-sdk/client-sso@3.556.0(aws-crt@1.21.2)': dependencies: @@ -6120,6 +6582,51 @@ snapshots: transitivePeerDependencies: - aws-crt + '@aws-sdk/client-sts@3.556.0(@aws-sdk/credential-provider-node@3.556.0)': + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/core': 3.556.0 + '@aws-sdk/credential-provider-node': 3.556.0 + '@aws-sdk/middleware-host-header': 3.535.0 + '@aws-sdk/middleware-logger': 3.535.0 + '@aws-sdk/middleware-recursion-detection': 3.535.0 + '@aws-sdk/middleware-user-agent': 3.540.0 + '@aws-sdk/region-config-resolver': 3.535.0 + '@aws-sdk/types': 3.535.0 + '@aws-sdk/util-endpoints': 3.540.0 + '@aws-sdk/util-user-agent-browser': 3.535.0 + '@aws-sdk/util-user-agent-node': 3.535.0 + '@smithy/config-resolver': 2.2.0 + '@smithy/core': 1.4.2 + '@smithy/fetch-http-handler': 2.5.0 + '@smithy/hash-node': 2.2.0 + '@smithy/invalid-dependency': 2.2.0 + '@smithy/middleware-content-length': 2.2.0 + '@smithy/middleware-endpoint': 2.5.1 + '@smithy/middleware-retry': 2.3.1 + '@smithy/middleware-serde': 2.3.0 + '@smithy/middleware-stack': 2.2.0 + '@smithy/node-config-provider': 2.3.0 + '@smithy/node-http-handler': 2.5.0 + '@smithy/protocol-http': 3.3.0 + '@smithy/smithy-client': 2.5.1 + '@smithy/types': 2.12.0 + '@smithy/url-parser': 2.2.0 + '@smithy/util-base64': 2.3.0 + '@smithy/util-body-length-browser': 2.2.0 + '@smithy/util-body-length-node': 2.3.0 + '@smithy/util-defaults-mode-browser': 2.2.1 + '@smithy/util-defaults-mode-node': 2.3.1 + '@smithy/util-endpoints': 1.2.0 + '@smithy/util-middleware': 2.2.0 + '@smithy/util-retry': 2.2.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + optional: true + '@aws-sdk/client-sts@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 @@ -6171,7 +6678,7 @@ snapshots: '@aws-crypto/sha256-js': 3.0.0 '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) '@aws-sdk/core': 3.572.0 - '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) + '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) '@aws-sdk/middleware-host-header': 3.567.0 '@aws-sdk/middleware-logger': 3.568.0 '@aws-sdk/middleware-recursion-detection': 3.567.0 @@ -6331,6 +6838,24 @@ snapshots: - '@aws-sdk/credential-provider-node' - aws-crt + '@aws-sdk/credential-provider-ini@3.556.0(@aws-sdk/credential-provider-node@3.556.0)': + dependencies: + '@aws-sdk/client-sts': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/credential-provider-env': 3.535.0 + '@aws-sdk/credential-provider-process': 3.535.0 + '@aws-sdk/credential-provider-sso': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/credential-provider-web-identity': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/types': 3.535.0 + '@smithy/credential-provider-imds': 2.3.0 + '@smithy/property-provider': 2.2.0 + '@smithy/shared-ini-file-loader': 2.4.0 + '@smithy/types': 2.12.0 + tslib: 2.6.2 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - aws-crt + optional: true + '@aws-sdk/credential-provider-ini@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@aws-sdk/client-sts@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: '@aws-sdk/client-sts': 3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) @@ -6348,12 +6873,12 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-ini@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': + '@aws-sdk/credential-provider-ini@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: '@aws-sdk/client-sts': 3.572.0(aws-crt@1.21.2) '@aws-sdk/credential-provider-env': 3.568.0 '@aws-sdk/credential-provider-process': 3.572.0 - '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(aws-crt@1.21.2) + '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) '@aws-sdk/credential-provider-web-identity': 3.568.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2)) '@aws-sdk/types': 3.567.0 '@smithy/credential-provider-imds': 2.3.0 @@ -6365,22 +6890,23 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-ini@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': + '@aws-sdk/credential-provider-node@3.556.0': dependencies: - '@aws-sdk/client-sts': 3.572.0(aws-crt@1.21.2) - '@aws-sdk/credential-provider-env': 3.568.0 - '@aws-sdk/credential-provider-process': 3.572.0 - '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) - '@aws-sdk/credential-provider-web-identity': 3.568.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2)) - '@aws-sdk/types': 3.567.0 + '@aws-sdk/credential-provider-env': 3.535.0 + '@aws-sdk/credential-provider-http': 3.552.0 + '@aws-sdk/credential-provider-ini': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/credential-provider-process': 3.535.0 + '@aws-sdk/credential-provider-sso': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/credential-provider-web-identity': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/types': 3.535.0 '@smithy/credential-provider-imds': 2.3.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 '@smithy/types': 2.12.0 tslib: 2.6.2 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt + optional: true '@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2)': dependencies: @@ -6418,25 +6944,6 @@ snapshots: - '@aws-sdk/client-sts' - aws-crt - '@aws-sdk/credential-provider-node@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': - dependencies: - '@aws-sdk/credential-provider-env': 3.568.0 - '@aws-sdk/credential-provider-http': 3.568.0 - '@aws-sdk/credential-provider-ini': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) - '@aws-sdk/credential-provider-process': 3.572.0 - '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(aws-crt@1.21.2) - '@aws-sdk/credential-provider-web-identity': 3.568.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2)) - '@aws-sdk/types': 3.567.0 - '@smithy/credential-provider-imds': 2.3.0 - '@smithy/property-provider': 2.2.0 - '@smithy/shared-ini-file-loader': 2.4.0 - '@smithy/types': 2.12.0 - tslib: 2.6.2 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - '@aws-sdk/client-sts' - - aws-crt - '@aws-sdk/credential-provider-node@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: '@aws-sdk/credential-provider-env': 3.568.0 @@ -6482,8 +6989,21 @@ snapshots: '@aws-sdk/credential-provider-sso@3.556.0(@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: - '@aws-sdk/client-sso': 3.556.0(aws-crt@1.21.2) - '@aws-sdk/token-providers': 3.556.0(@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2))(aws-crt@1.21.2) + '@aws-sdk/client-sso': 3.556.0(aws-crt@1.21.2) + '@aws-sdk/token-providers': 3.556.0(@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2))(aws-crt@1.21.2) + '@aws-sdk/types': 3.535.0 + '@smithy/property-provider': 2.2.0 + '@smithy/shared-ini-file-loader': 2.4.0 + '@smithy/types': 2.12.0 + tslib: 2.6.2 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - aws-crt + + '@aws-sdk/credential-provider-sso@3.556.0(@aws-sdk/credential-provider-node@3.556.0)': + dependencies: + '@aws-sdk/client-sso': 3.556.0 + '@aws-sdk/token-providers': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) '@aws-sdk/types': 3.535.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 @@ -6492,6 +7012,7 @@ snapshots: transitivePeerDependencies: - '@aws-sdk/credential-provider-node' - aws-crt + optional: true '@aws-sdk/credential-provider-sso@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: @@ -6506,10 +7027,10 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-sso@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(aws-crt@1.21.2)': + '@aws-sdk/credential-provider-sso@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: '@aws-sdk/client-sso': 3.572.0(aws-crt@1.21.2) - '@aws-sdk/token-providers': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)) + '@aws-sdk/token-providers': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2)) '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 @@ -6519,22 +7040,20 @@ snapshots: - '@aws-sdk/client-sso-oidc' - aws-crt - '@aws-sdk/credential-provider-sso@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': + '@aws-sdk/credential-provider-web-identity@3.556.0(@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2))(aws-crt@1.21.2)': dependencies: - '@aws-sdk/client-sso': 3.572.0(aws-crt@1.21.2) - '@aws-sdk/token-providers': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2)) - '@aws-sdk/types': 3.567.0 + '@aws-sdk/client-sts': 3.556.0(@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2))(aws-crt@1.21.2) + '@aws-sdk/types': 3.535.0 '@smithy/property-provider': 2.2.0 - '@smithy/shared-ini-file-loader': 2.4.0 '@smithy/types': 2.12.0 tslib: 2.6.2 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/credential-provider-node' - aws-crt - '@aws-sdk/credential-provider-web-identity@3.556.0(@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2))(aws-crt@1.21.2)': + '@aws-sdk/credential-provider-web-identity@3.556.0(@aws-sdk/credential-provider-node@3.556.0)': dependencies: - '@aws-sdk/client-sts': 3.556.0(@aws-sdk/credential-provider-node@3.556.0(aws-crt@1.21.2))(aws-crt@1.21.2) + '@aws-sdk/client-sts': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) '@aws-sdk/types': 3.535.0 '@smithy/property-provider': 2.2.0 '@smithy/types': 2.12.0 @@ -6542,6 +7061,7 @@ snapshots: transitivePeerDependencies: - '@aws-sdk/credential-provider-node' - aws-crt + optional: true '@aws-sdk/credential-provider-web-identity@3.567.0(@aws-sdk/client-sts@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))': dependencies: @@ -6789,16 +7309,20 @@ snapshots: - '@aws-sdk/credential-provider-node' - aws-crt - '@aws-sdk/token-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))': + '@aws-sdk/token-providers@3.556.0(@aws-sdk/credential-provider-node@3.556.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.572.0(aws-crt@1.21.2) - '@aws-sdk/types': 3.567.0 + '@aws-sdk/client-sso-oidc': 3.556.0(@aws-sdk/credential-provider-node@3.556.0) + '@aws-sdk/types': 3.535.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 '@smithy/types': 2.12.0 tslib: 2.6.2 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - aws-crt + optional: true - '@aws-sdk/token-providers@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))': + '@aws-sdk/token-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))': dependencies: '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) '@aws-sdk/types': 3.567.0 @@ -6809,7 +7333,7 @@ snapshots: '@aws-sdk/token-providers@3.572.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))': dependencies: - '@aws-sdk/client-sso-oidc': 3.572.0(aws-crt@1.21.2) + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 @@ -6869,6 +7393,14 @@ snapshots: bowser: 2.11.0 tslib: 2.6.2 + '@aws-sdk/util-user-agent-node@3.535.0': + dependencies: + '@aws-sdk/types': 3.535.0 + '@smithy/node-config-provider': 2.3.0 + '@smithy/types': 2.12.0 + tslib: 2.6.2 + optional: true + '@aws-sdk/util-user-agent-node@3.535.0(aws-crt@1.21.2)': dependencies: '@aws-sdk/types': 3.535.0 @@ -7137,6 +7669,8 @@ snapshots: '@codegenie/serverless-express@4.14.1': {} + '@drizzle-team/brocli@0.10.1': {} + '@envelop/core@3.0.6': dependencies: '@envelop/types': 3.0.2 @@ -7154,6 +7688,19 @@ snapshots: lru-cache: 6.0.0 tslib: 2.6.2 + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.8.1 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + '@esbuild/aix-ppc64@0.20.2': optional: true @@ -7163,6 +7710,12 @@ snapshots: '@esbuild/android-arm64@0.18.13': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + '@esbuild/android-arm64@0.20.2': optional: true @@ -7172,6 +7725,12 @@ snapshots: '@esbuild/android-arm@0.18.13': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + '@esbuild/android-arm@0.20.2': optional: true @@ -7181,6 +7740,12 @@ snapshots: '@esbuild/android-x64@0.18.13': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + '@esbuild/android-x64@0.20.2': optional: true @@ -7190,6 +7755,12 @@ snapshots: '@esbuild/darwin-arm64@0.18.13': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + '@esbuild/darwin-arm64@0.20.2': optional: true @@ -7199,6 +7770,12 @@ snapshots: '@esbuild/darwin-x64@0.18.13': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + '@esbuild/darwin-x64@0.20.2': optional: true @@ -7208,6 +7785,12 @@ snapshots: '@esbuild/freebsd-arm64@0.18.13': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + '@esbuild/freebsd-arm64@0.20.2': optional: true @@ -7217,6 +7800,12 @@ snapshots: '@esbuild/freebsd-x64@0.18.13': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + '@esbuild/freebsd-x64@0.20.2': optional: true @@ -7226,6 +7815,12 @@ snapshots: '@esbuild/linux-arm64@0.18.13': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + '@esbuild/linux-arm64@0.20.2': optional: true @@ -7235,6 +7830,12 @@ snapshots: '@esbuild/linux-arm@0.18.13': optional: true + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + '@esbuild/linux-arm@0.20.2': optional: true @@ -7244,6 +7845,12 @@ snapshots: '@esbuild/linux-ia32@0.18.13': optional: true + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + '@esbuild/linux-ia32@0.20.2': optional: true @@ -7253,6 +7860,12 @@ snapshots: '@esbuild/linux-loong64@0.18.13': optional: true + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + '@esbuild/linux-loong64@0.20.2': optional: true @@ -7262,6 +7875,12 @@ snapshots: '@esbuild/linux-mips64el@0.18.13': optional: true + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + '@esbuild/linux-mips64el@0.20.2': optional: true @@ -7271,6 +7890,12 @@ snapshots: '@esbuild/linux-ppc64@0.18.13': optional: true + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + '@esbuild/linux-ppc64@0.20.2': optional: true @@ -7280,6 +7905,12 @@ snapshots: '@esbuild/linux-riscv64@0.18.13': optional: true + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + '@esbuild/linux-riscv64@0.20.2': optional: true @@ -7289,6 +7920,12 @@ snapshots: '@esbuild/linux-s390x@0.18.13': optional: true + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + '@esbuild/linux-s390x@0.20.2': optional: true @@ -7298,6 +7935,12 @@ snapshots: '@esbuild/linux-x64@0.18.13': optional: true + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + '@esbuild/linux-x64@0.20.2': optional: true @@ -7307,6 +7950,12 @@ snapshots: '@esbuild/netbsd-x64@0.18.13': optional: true + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + '@esbuild/netbsd-x64@0.20.2': optional: true @@ -7319,6 +7968,12 @@ snapshots: '@esbuild/openbsd-x64@0.18.13': optional: true + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + '@esbuild/openbsd-x64@0.20.2': optional: true @@ -7328,6 +7983,12 @@ snapshots: '@esbuild/sunos-x64@0.18.13': optional: true + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + '@esbuild/sunos-x64@0.20.2': optional: true @@ -7337,6 +7998,12 @@ snapshots: '@esbuild/win32-arm64@0.18.13': optional: true + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + '@esbuild/win32-arm64@0.20.2': optional: true @@ -7346,6 +8013,12 @@ snapshots: '@esbuild/win32-ia32@0.18.13': optional: true + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + '@esbuild/win32-ia32@0.20.2': optional: true @@ -7355,6 +8028,12 @@ snapshots: '@esbuild/win32-x64@0.18.13': optional: true + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + '@esbuild/win32-x64@0.20.2': optional: true @@ -7500,11 +8179,16 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 - '@lukeed/ms@2.0.2': {} + '@libsql/client-wasm@0.14.0': + dependencies: + '@libsql/core': 0.14.0 + js-base64: 3.7.7 - '@mongodb-js/saslprep@1.1.5': + '@libsql/core@0.14.0': dependencies: - sparse-bitfield: 3.0.3 + js-base64: 3.7.7 + + '@lukeed/ms@2.0.2': {} '@nivo/annotations@0.84.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -8264,14 +8948,11 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 20.12.8 - '@types/connect-mongodb-session@2.4.7(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2)': + '@types/connect-pg-simple@7.0.3': dependencies: + '@types/express': 4.17.21 '@types/express-session': 1.18.0 - '@types/node': 20.12.8 - mongodb: 4.17.2(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt + '@types/pg': 8.11.10 '@types/connect@3.4.38': dependencies: @@ -8359,6 +9040,12 @@ snapshots: dependencies: '@types/express': 4.17.21 + '@types/pg@8.11.10': + dependencies: + '@types/node': 20.12.8 + pg-protocol: 1.7.0 + pg-types: 4.0.2 + '@types/prop-types@15.7.12': {} '@types/qs@6.9.15': {} @@ -8418,17 +9105,6 @@ snapshots: '@types/warning@3.0.3': {} - '@types/webidl-conversions@7.0.3': {} - - '@types/whatwg-url@11.0.4': - dependencies: - '@types/webidl-conversions': 7.0.3 - - '@types/whatwg-url@8.2.2': - dependencies: - '@types/node': 20.12.8 - '@types/webidl-conversions': 7.0.3 - '@types/ws@8.5.10': dependencies: '@types/node': 20.12.8 @@ -8687,12 +9363,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - archetype@0.13.0: - dependencies: - lodash.clonedeep: 4.5.0 - lodash.set: 4.3.2 - mpath: 0.8.4 - archiver-utils@2.1.0: dependencies: glob: 7.2.3 @@ -8969,12 +9639,6 @@ snapshots: node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) - bson@4.7.2: - dependencies: - buffer: 5.7.1 - - bson@6.6.0: {} - buffer-crc32@0.2.13: {} buffer-from@1.1.2: {} @@ -9156,18 +9820,11 @@ snapshots: pkg-up: 3.1.0 semver: 7.6.0 - connect-mongodb-session@5.0.0(@aws-sdk/credential-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(socks@2.8.3): + connect-pg-simple@10.0.0: dependencies: - archetype: 0.13.0 - mongodb: 6.5.0(@aws-sdk/credential-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(socks@2.8.3) + pg: 8.13.0 transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks + - pg-native constructs@10.3.0: {} @@ -9394,6 +10051,26 @@ snapshots: dotenv@16.4.5: {} + drizzle-kit@0.26.2: + dependencies: + '@drizzle-team/brocli': 0.10.1 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.19.12 + esbuild-register: 3.6.0(esbuild@0.19.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.35.3(@aws-sdk/client-rds-data@3.556.0)(@libsql/client-wasm@0.14.0)(@types/pg@8.11.10)(@types/react@18.3.1)(kysely@0.25.0)(pg@8.13.0)(react@18.3.1): + dependencies: + '@libsql/client-wasm': 0.14.0 + optionalDependencies: + '@aws-sdk/client-rds-data': 3.556.0 + '@types/pg': 8.11.10 + '@types/react': 18.3.1 + kysely: 0.25.0 + pg: 8.13.0 + react: 18.3.1 + dset@3.1.3: {} duplexify@3.7.1: @@ -9528,6 +10205,13 @@ snapshots: is-date-object: 1.0.5 is-symbol: 1.0.4 + esbuild-register@3.6.0(esbuild@0.19.12): + dependencies: + debug: 4.3.4(supports-color@5.5.0) + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + esbuild@0.18.13: optionalDependencies: '@esbuild/android-arm': 0.18.13 @@ -9553,6 +10237,57 @@ snapshots: '@esbuild/win32-ia32': 0.18.13 '@esbuild/win32-x64': 0.18.13 + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 @@ -10172,11 +10907,6 @@ snapshots: dependencies: loose-envify: 1.4.0 - ip-address@9.0.5: - dependencies: - jsbn: 1.1.0 - sprintf-js: 1.1.3 - ipaddr.js@1.9.1: {} is-arguments@1.1.1: @@ -10338,6 +11068,8 @@ snapshots: jquery@3.7.1: {} + js-base64@3.7.7: {} + js-sdsl@4.3.0: {} js-tokens@4.0.0: {} @@ -10346,8 +11078,6 @@ snapshots: dependencies: argparse: 2.0.1 - jsbn@1.1.0: {} - jsesc@2.5.2: {} json-buffer@3.0.1: {} @@ -10385,21 +11115,21 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 - kareem@2.6.3: {} - keyboard-key@1.1.0: {} keyv@4.5.4: dependencies: json-buffer: 3.0.1 - kysely-codegen@0.10.1(kysely@0.25.0): + kysely-codegen@0.10.1(kysely@0.25.0)(pg@8.13.0): dependencies: chalk: 4.1.2 dotenv: 16.4.5 kysely: 0.25.0 micromatch: 4.0.5 minimist: 1.2.8 + optionalDependencies: + pg: 8.13.0 kysely-data-api@0.2.1(@aws-sdk/client-rds-data@3.556.0(aws-crt@1.21.2))(kysely@0.25.0): dependencies: @@ -10462,8 +11192,6 @@ snapshots: lodash-es@4.17.21: {} - lodash.clonedeep@4.5.0: {} - lodash.defaults@4.2.0: {} lodash.difference@4.5.0: {} @@ -10476,8 +11204,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.set@4.3.2: {} - lodash.truncate@4.4.2: {} lodash.union@4.6.0: {} @@ -10521,8 +11247,6 @@ snapshots: memoize-one@5.2.1: {} - memory-pager@1.5.0: {} - merge-descriptors@1.0.1: {} merge-stream@2.0.0: {} @@ -10580,56 +11304,6 @@ snapshots: dependencies: obliterator: 2.0.4 - mongodb-connection-string-url@2.6.0: - dependencies: - '@types/whatwg-url': 8.2.2 - whatwg-url: 11.0.0 - - mongodb-connection-string-url@3.0.0: - dependencies: - '@types/whatwg-url': 11.0.4 - whatwg-url: 13.0.0 - - mongodb@4.17.2(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2): - dependencies: - bson: 4.7.2 - mongodb-connection-string-url: 2.6.0 - socks: 2.8.3 - optionalDependencies: - '@aws-sdk/credential-providers': 3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) - '@mongodb-js/saslprep': 1.1.5 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - - mongodb@6.5.0(@aws-sdk/credential-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(socks@2.8.3): - dependencies: - '@mongodb-js/saslprep': 1.1.5 - bson: 6.6.0 - mongodb-connection-string-url: 3.0.0 - optionalDependencies: - '@aws-sdk/credential-providers': 3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2) - socks: 2.8.3 - - mongoose@8.3.3(@aws-sdk/credential-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(socks@2.8.3): - dependencies: - bson: 6.6.0 - kareem: 2.6.3 - mongodb: 6.5.0(@aws-sdk/credential-providers@3.567.0(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(aws-crt@1.21.2))(socks@2.8.3) - mpath: 0.9.0 - mquery: 5.0.0 - ms: 2.1.3 - sift: 16.0.1 - transitivePeerDependencies: - - '@aws-sdk/credential-providers' - - '@mongodb-js/zstd' - - gcp-metadata - - kerberos - - mongodb-client-encryption - - snappy - - socks - - supports-color - morgan@1.10.0: dependencies: basic-auth: 2.0.1 @@ -10640,10 +11314,6 @@ snapshots: transitivePeerDependencies: - supports-color - mpath@0.8.4: {} - - mpath@0.9.0: {} - mqtt-packet@6.10.0: dependencies: bl: 4.1.0 @@ -10697,12 +11367,6 @@ snapshots: - supports-color - utf-8-validate - mquery@5.0.0: - dependencies: - debug: 4.3.4(supports-color@5.5.0) - transitivePeerDependencies: - - supports-color - ms@2.0.0: {} ms@2.1.2: {} @@ -10800,6 +11464,8 @@ snapshots: obliterator@2.0.4: {} + obuf@1.1.2: {} + oidc-token-hash@5.0.3: {} on-finished@2.3.0: @@ -10940,6 +11606,53 @@ snapshots: peterportal-api-next-types@1.0.0-rc.3: {} + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.7.0: {} + + pg-int8@1.0.1: {} + + pg-numeric@1.0.2: {} + + pg-pool@3.7.0(pg@8.13.0): + dependencies: + pg: 8.13.0 + + pg-protocol@1.7.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.13.0: + dependencies: + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.13.0) + pg-protocol: 1.7.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.0.0: {} picomatch@2.3.1: {} @@ -10960,6 +11673,28 @@ snapshots: picocolors: 1.0.0 source-map-js: 1.2.0 + postgres-array@2.0.0: {} + + postgres-array@3.0.2: {} + + postgres-bytea@1.0.0: {} + + postgres-bytea@3.0.0: + dependencies: + obuf: 1.1.2 + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} + prelude-ls@1.2.1: {} prettier@3.2.5: {} @@ -11450,8 +12185,6 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.1 - sift@16.0.1: {} - signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -11483,24 +12216,20 @@ snapshots: ansi-styles: 6.2.1 is-fullwidth-code-point: 5.0.0 - smart-buffer@4.2.0: {} - - socks@2.8.3: - dependencies: - ip-address: 9.0.5 - smart-buffer: 4.2.0 - source-map-js@1.2.0: {} - sparse-bitfield@3.0.3: + source-map-support@0.5.21: dependencies: - memory-pager: 1.5.0 + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} split2@3.2.2: dependencies: readable-stream: 3.6.2 - sprintf-js@1.1.3: {} + split2@4.2.0: {} sst-aws-cdk@2.132.1: dependencies: @@ -11512,7 +12241,7 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - sst@2.41.5(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@types/react@18.3.1)(aws-crt@1.21.2): + sst@2.41.5(@aws-sdk/client-sso-oidc@3.572.0(aws-crt@1.21.2))(@types/react@18.3.1)(aws-crt@1.21.2)(pg@8.13.0): dependencies: '@aws-cdk/aws-lambda-python-alpha': 2.132.1-alpha.0(aws-cdk-lib@2.132.1(constructs@10.3.0))(constructs@10.3.0) '@aws-cdk/cloud-assembly-schema': 2.132.1 @@ -11567,7 +12296,7 @@ snapshots: ink: 4.4.1(@types/react@18.3.1)(react@18.3.1) ink-spinner: 5.0.0(ink@4.4.1(@types/react@18.3.1)(react@18.3.1))(react@18.3.1) kysely: 0.25.0 - kysely-codegen: 0.10.1(kysely@0.25.0) + kysely-codegen: 0.10.1(kysely@0.25.0)(pg@8.13.0) kysely-data-api: 0.2.1(@aws-sdk/client-rds-data@3.556.0(aws-crt@1.21.2))(kysely@0.25.0) minimatch: 6.2.0 openid-client: 5.6.5 @@ -11728,14 +12457,6 @@ snapshots: touch@3.1.1: {} - tr46@3.0.0: - dependencies: - punycode: 2.3.1 - - tr46@4.1.1: - dependencies: - punycode: 2.3.1 - tree-kill@1.2.2: {} ts-api-utils@1.3.0(typescript@5.4.2): @@ -11934,8 +12655,6 @@ snapshots: pvtsutils: 1.3.5 tslib: 2.6.2 - webidl-conversions@7.0.0: {} - websoc-fuzzy-search@1.0.1: dependencies: base64-arraybuffer: 1.0.2 @@ -11953,16 +12672,6 @@ snapshots: - bufferutil - utf-8-validate - whatwg-url@11.0.0: - dependencies: - tr46: 3.0.0 - webidl-conversions: 7.0.0 - - whatwg-url@13.0.0: - dependencies: - tr46: 4.1.1 - webidl-conversions: 7.0.0 - which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4 diff --git a/site/src/App.tsx b/site/src/App.tsx index 6267352c..a1eb90b3 100644 --- a/site/src/App.tsx +++ b/site/src/App.tsx @@ -18,10 +18,13 @@ import ReviewsPage from './pages/ReviewsPage'; import SideBar from './component/SideBar/SideBar'; import ThemeContext from './style/theme-context'; -import { useCookies } from 'react-cookie'; import trpc from './trpc'; import { Theme } from '@peterportal/types'; +import { useAppDispatch } from './store/hooks'; +import { searchAPIResults } from './helpers/util'; +import { setCoursebag } from './store/slices/coursebagSlice'; +import { useIsLoggedIn } from './hooks/isLoggedIn'; function isSystemDark() { return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; @@ -35,8 +38,9 @@ export default function App() { const [darkMode, setDarkMode] = useState( usingSystemTheme ? isSystemDark() : localStorage.getItem('theme') === 'dark', ); - const [cookies] = useCookies(['user']); + const isLoggedIn = useIsLoggedIn(); const [prevDarkMode, setPrevDarkMode] = useState(false); // light theme is default on page load + const dispatch = useAppDispatch(); /** * we run this check at render-time and compare with previous state because a useEffect @@ -70,23 +74,33 @@ export default function App() { */ const setTheme = (theme: Theme) => { setThemeState(theme); - if (cookies.user) { - trpc.users.setPreferences.mutate({ theme }); + if (isLoggedIn) { + trpc.users.setTheme.mutate({ theme }); } else { localStorage.setItem('theme', theme); } }; + const loadCoursebag = useCallback(async () => { + const courseIds = isLoggedIn + ? await trpc.savedCourses.get.query() + : JSON.parse(localStorage.getItem('coursebag') ?? '[]'); + const coursebag = await searchAPIResults('courses', courseIds); + dispatch(setCoursebag(Object.values(coursebag))); + }, [dispatch, isLoggedIn]); + useEffect(() => { - // if logged in, load user prefs (theme) from mongo - if (cookies.user) { - trpc.users.getPreferences.query().then((res) => { + // if logged in, load user theme from db + if (isLoggedIn) { + trpc.users.get.query().then((res) => { if (res.theme) { setThemeState(res.theme); } }); } - }, [cookies.user, setThemeState]); + + loadCoursebag(); + }, [isLoggedIn, setThemeState, dispatch, loadCoursebag]); return ( diff --git a/site/src/component/AppHeader/Profile.tsx b/site/src/component/AppHeader/Profile.tsx index 05908dc5..d0225bac 100644 --- a/site/src/component/AppHeader/Profile.tsx +++ b/site/src/component/AppHeader/Profile.tsx @@ -1,4 +1,3 @@ -import { useCookies } from 'react-cookie'; import { useContext, useEffect, useState } from 'react'; import ThemeContext from '../../style/theme-context'; import { Button, OverlayTrigger, Popover } from 'react-bootstrap'; @@ -6,17 +5,20 @@ import { Icon } from 'semantic-ui-react'; import './Profile.scss'; import { NavLink } from 'react-router-dom'; import { ArrowLeftCircleFill } from 'react-bootstrap-icons'; +import trpc from '../../trpc'; +import { useIsLoggedIn } from '../../hooks/isLoggedIn'; type ProfileMenuTab = 'default' | 'theme'; const Profile = () => { const { darkMode, setTheme, usingSystemTheme } = useContext(ThemeContext); - const [cookies] = useCookies(['user']); const [show, setShow] = useState(false); const [tab, setTab] = useState('default'); - const { name, email, picture }: { name: string; email: string; picture: string } = cookies?.user ?? {}; - const isLoggedIn: boolean = cookies.user; + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [picture, setPicture] = useState(''); + const isLoggedIn = useIsLoggedIn(); useEffect(() => { if (!show) { @@ -24,6 +26,16 @@ const Profile = () => { } }, [show]); + useEffect(() => { + if (isLoggedIn) { + trpc.users.get.query().then((user) => { + setName(user.name); + setEmail(user.email); + setPicture(user.picture); + }); + } + }); + const DefaultTab = ( <>
diff --git a/site/src/component/Report/ReportGroup.tsx b/site/src/component/Report/ReportGroup.tsx index 89f84d51..157cb9cf 100644 --- a/site/src/component/Report/ReportGroup.tsx +++ b/site/src/component/Report/ReportGroup.tsx @@ -6,7 +6,7 @@ import { ReportData, ReviewData } from '@peterportal/types'; import trpc from '../../trpc'; interface ReportGroupProps { - reviewID: string; + reviewId: number; reports: ReportData[]; onAccept: () => void; onDeny: () => void; @@ -15,14 +15,14 @@ interface ReportGroupProps { const ReportGroup: FC = (props) => { const [review, setReview] = useState(null!); - const getReviewData = async (reviewID: string) => { - const review = (await trpc.reviews.get.query({ reviewID: reviewID }))[0]; + const getReviewData = async (reviewId: number) => { + const review = (await trpc.reviews.get.query({ reviewId }))[0]; setReview(review); }; useEffect(() => { - getReviewData(props.reviewID); - }, [props.reviewID]); + getReviewData(props.reviewId); + }, [props.reviewId]); if (!review) { return <>; @@ -30,27 +30,27 @@ const ReportGroup: FC = (props) => { return (
-
{review.professorID}
-
{review.courseID}
+
{review.professorId}
+
{review.courseId}
Posted by {review.userDisplay} on{' '} - {new Date(review.timestamp).toLocaleString('default', { year: 'numeric', month: 'long', day: 'numeric' })} + {new Date(review.createdAt).toLocaleString('default', { year: 'numeric', month: 'long', day: 'numeric' })}

Review Content:

-

{review.reviewContent}

+

{review.content}

Reports on this review:

{props.reports.map((report, i) => { return ( ); diff --git a/site/src/component/Report/Reports.tsx b/site/src/component/Report/Reports.tsx index a23e2a88..59d91aca 100644 --- a/site/src/component/Report/Reports.tsx +++ b/site/src/component/Report/Reports.tsx @@ -9,7 +9,7 @@ const Reports: FC = () => { const [loaded, setLoaded] = useState(false); interface ReviewDisplay { - reviewID: string; + reviewId: number; reports: ReportData[]; } @@ -20,9 +20,9 @@ const Reports: FC = () => { reports.forEach((report) => { let i; - if ((i = reportsDisplay.findIndex((reviewDisplay) => report.reviewID === reviewDisplay.reviewID)) < 0) { + if ((i = reportsDisplay.findIndex((reviewDisplay) => report.reviewId === reviewDisplay.reviewId)) < 0) { reportsDisplay.push({ - reviewID: report.reviewID, + reviewId: report.reviewId, reports: [report], }); } else { @@ -45,15 +45,15 @@ const Reports: FC = () => { document.title = 'View Reports | PeterPortal'; }, [getData]); - const acceptReports = async (reviewID: string) => { - await trpc.reviews.delete.mutate({ id: reviewID }); + const acceptReports = async (reviewId: number) => { + await trpc.reviews.delete.mutate({ id: reviewId }); // reports are automatically deleted when deleting a review - setData(data.filter((review) => review.reviewID !== reviewID)); + setData(data.filter((review) => review.reviewId !== reviewId)); }; - const denyReports = async (reviewID: string) => { - await trpc.reports.delete.mutate({ reviewID: reviewID }); - setData(data.filter((review) => review.reviewID !== reviewID)); + const denyReports = async (reviewId: number) => { + await trpc.reports.delete.mutate({ reviewId }); + setData(data.filter((review) => review.reviewId !== reviewId)); }; if (!loaded) { @@ -69,11 +69,11 @@ const Reports: FC = () => { {data.map((review) => { return ( acceptReports(review.reviewID)} - onDeny={() => denyReports(review.reviewID)} + onAccept={() => acceptReports(review.reviewId)} + onDeny={() => denyReports(review.reviewId)} /> ); })} diff --git a/site/src/component/Report/SubReport.tsx b/site/src/component/Report/SubReport.tsx index 3cfefc1f..5f5c4a2f 100644 --- a/site/src/component/Report/SubReport.tsx +++ b/site/src/component/Report/SubReport.tsx @@ -3,8 +3,8 @@ import { Divider } from 'semantic-ui-react'; import './SubReport.scss'; interface SubReportProps { - reportID: string; - reviewID: string; + reportId: number; + reviewId: number; reason: string; timestamp: string; isLast: boolean; diff --git a/site/src/component/ReportForm/ReportForm.tsx b/site/src/component/ReportForm/ReportForm.tsx index d4327f10..2eaffd3d 100644 --- a/site/src/component/ReportForm/ReportForm.tsx +++ b/site/src/component/ReportForm/ReportForm.tsx @@ -9,8 +9,8 @@ import { ReportSubmission } from '@peterportal/types'; interface ReportFormProps { showForm: boolean; - reviewID: string | undefined; - reviewContent: string; + reviewId: number; + reviewContent: string | undefined; closeForm: () => void; } @@ -32,7 +32,7 @@ const ReportForm: FC = (props) => { setValidated(true); const report = { - reviewID: props.reviewID!, + reviewId: props.reviewId, reason, }; postReport(report); diff --git a/site/src/component/Review/Review.tsx b/site/src/component/Review/Review.tsx index 635b2a1e..50af4900 100644 --- a/site/src/component/Review/Review.tsx +++ b/site/src/component/Review/Review.tsx @@ -31,12 +31,12 @@ const Review: FC = (props) => { const getReviews = useCallback(async () => { interface paramsProps { - courseID?: string; - professorID?: string; + courseId?: string; + professorId?: string; } const params: paramsProps = {}; - if (props.course) params.courseID = props.course.id; - if (props.professor) params.professorID = props.professor.ucinetid; + if (props.course) params.courseId = props.course.id; + if (props.professor) params.professorId = props.professor.ucinetid; const reviews = await trpc.reviews.get.query(params); dispatch(setReviews(reviews)); }, [dispatch, props.course, props.professor]); @@ -59,25 +59,25 @@ const Review: FC = (props) => { if (filterOption.length > 0) { if (props.course) { // filter course reviews by specific professor - sortedReviews = sortedReviews.filter((review) => review.professorID === filterOption); + sortedReviews = sortedReviews.filter((review) => review.professorId === filterOption); } else if (props.professor) { // filter professor reviews by specific course - sortedReviews = sortedReviews.filter((review) => review.courseID === filterOption); + sortedReviews = sortedReviews.filter((review) => review.courseId === filterOption); } } switch (sortingOption) { case SortingOption.MOST_RECENT: - sortedReviews.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); + sortedReviews.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); break; case SortingOption.TOP_REVIEWS: // the right side of || will fall back to most recent when score is equal sortedReviews.sort( - (a, b) => b.score - a.score || new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + (a, b) => b.score - a.score || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); break; case SortingOption.CONTROVERSIAL: sortedReviews.sort( - (a, b) => a.score - b.score || new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(), + (a, b) => a.score - b.score || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ); break; } @@ -86,12 +86,12 @@ const Review: FC = (props) => { let reviewFreq = new Map(); if (props.course) { reviewFreq = sortedReviews.reduce( - (acc, review) => acc.set(review.professorID, (acc.get(review.professorID) || 0) + 1), + (acc, review) => acc.set(review.professorId, (acc.get(review.professorId) || 0) + 1), reviewFreq, ); } else if (props.professor) { reviewFreq = sortedReviews.reduce( - (acc, review) => acc.set(review.courseID, (acc.get(review.courseID) || 0) + 1), + (acc, review) => acc.set(review.courseId, (acc.get(review.courseId) || 0) + 1), reviewFreq, ); } @@ -185,7 +185,7 @@ const Review: FC = (props) => {
{sortedReviews.map((review) => ( - + ))} - @@ -175,19 +143,19 @@ const SubReview: FC = ({ review, course, professor }) => {

{professor && ( - - {professor.courses[review.courseID]?.department + ' ' + professor.courses[review.courseID]?.courseNumber} + + {professor.courses[review.courseId]?.department + ' ' + professor.courses[review.courseId]?.courseNumber} )} {course && ( - - {course.instructors[review.professorID]?.name} + + {course.instructors[review.professorId]?.name} )} {!course && !professor && (
- {review.courseID}{' '} - {review.professorID} + {review.courseId}{' '} + {review.professorId}
)}

@@ -230,17 +198,17 @@ const SubReview: FC = ({ review, course, professor }) => {

Posted by {review.userDisplay} {review.verified && verifiedBadge} - {cookies.user?.id === review.userID && authorBadge} + {review.authored && authorBadge}

- {new Date(review.timestamp).toLocaleString('default', { + {new Date(review.createdAt).toLocaleString('default', { year: 'numeric', month: 'long', day: 'numeric', })}

-

{review.reviewContent}

+

{review.content}

@@ -251,7 +219,7 @@ const SubReview: FC = ({ review, course, professor }) => { ))} -
+

Helpful?

setReportFormOpen(false)} /> void; @@ -42,13 +43,13 @@ const ReviewForm: FC = ({ course: courseProp, }) => { const dispatch = useAppDispatch(); - const [professor, setProfessor] = useState(professorProp?.ucinetid ?? reviewToEdit?.professorID ?? ''); - const [course, setCourse] = useState(courseProp?.id ?? reviewToEdit?.courseID ?? ''); + const [professor, setProfessor] = useState(professorProp?.ucinetid ?? reviewToEdit?.professorId ?? ''); + const [course, setCourse] = useState(courseProp?.id ?? reviewToEdit?.courseId ?? ''); const [yearTakenDefault, quarterTakenDefault] = reviewToEdit?.quarter.split(' ') ?? ['', '']; const [yearTaken, setYearTaken] = useState(yearTakenDefault); const [quarterTaken, setQuarterTaken] = useState(quarterTakenDefault); const [gradeReceived, setGradeReceived] = useState(reviewToEdit?.gradeReceived); - const [content, setContent] = useState(reviewToEdit?.reviewContent ?? ''); + const [content, setContent] = useState(reviewToEdit?.content ?? ''); const [quality, setQuality] = useState(reviewToEdit?.rating ?? 3); const [difficulty, setDifficulty] = useState(reviewToEdit?.difficulty ?? 3); const [takeAgain, setTakeAgain] = useState(reviewToEdit?.takeAgain ?? false); @@ -57,9 +58,8 @@ const ReviewForm: FC = ({ const [selectedTags, setSelectedTags] = useState(reviewToEdit?.tags ?? []); const [captchaToken, setCaptchaToken] = useState(''); const [submitted, setSubmitted] = useState(false); - const [cookies] = useCookies(['user']); - const userID: string = cookies.user?.id; - const [userName, setUserName] = useState(reviewToEdit?.userDisplay ?? cookies.user?.name); + const isLoggedIn = useIsLoggedIn(); + const [anonymous, setAnonymous] = useState(reviewToEdit?.userDisplay === anonymousName); const [validated, setValidated] = useState(false); const { darkMode } = useContext(ThemeContext); @@ -67,7 +67,7 @@ const ReviewForm: FC = ({ if (show) { // form opened // if not logged in, close the form - if (cookies.user === undefined) { + if (!isLoggedIn) { alert('You must be logged in to add a review!'); closeForm(); } @@ -82,9 +82,9 @@ const ReviewForm: FC = ({ const postReview = async (review: ReviewSubmission | EditReviewSubmission) => { if (editing) { try { - const res = await trpc.reviews.edit.mutate(review as EditReviewSubmission); + await trpc.reviews.edit.mutate(review as EditReviewSubmission); setSubmitted(true); - dispatch(editReview(res)); + dispatch(editReview(review as EditReviewSubmission)); } catch (e) { alert((e as Error).message); } @@ -119,12 +119,11 @@ const ReviewForm: FC = ({ return; } const review = { - _id: reviewToEdit?._id, - professorID: professor, - courseID: course, - userID, - userDisplay: userName, - reviewContent: content, + id: reviewToEdit?.id, + professorId: professor, + courseId: course, + anonymous: anonymous, + content: content, rating: quality, difficulty, gradeReceived: gradeReceived!, @@ -224,7 +223,7 @@ const ReviewForm: FC = ({ function editReviewHeading() { if (!courseProp && !professorProp) { - return `Edit your review for ${reviewToEdit?.courseID} ${reviewToEdit?.professorID}`; + return `Edit your review for ${reviewToEdit?.courseId} ${reviewToEdit?.professorId}`; } else if (courseProp) { return `Edit your review for ${courseProp?.department} ${courseProp?.courseNumber}`; } else { @@ -442,16 +441,9 @@ const ReviewForm: FC = ({ id="anonymous" label="Post as Anonymous" onChange={(e: React.ChangeEvent) => { - // set name as anonymous - if (e.target.checked) { - setUserName('Anonymous Peter'); - } - // use real name - else { - setUserName(cookies.user.name); - } + setAnonymous(e.target.checked); }} - checked={userName === 'Anonymous Peter'} + checked={anonymous} /> diff --git a/site/src/component/SideBar/SideBar.tsx b/site/src/component/SideBar/SideBar.tsx index e63f44d3..f84ea98f 100644 --- a/site/src/component/SideBar/SideBar.tsx +++ b/site/src/component/SideBar/SideBar.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { XCircle } from 'react-bootstrap-icons'; -import { useCookies } from 'react-cookie'; import { NavLink } from 'react-router-dom'; import { CSSTransition } from 'react-transition-group'; import { Icon } from 'semantic-ui-react'; @@ -11,31 +10,24 @@ import { useAppDispatch, useAppSelector } from '../..//store/hooks'; import { setSidebarStatus } from '../../store/slices/uiSlice'; import Footer from '../Footer/Footer'; import trpc from '../../trpc'; +import { useIsLoggedIn } from '../../hooks/isLoggedIn'; const SideBar = () => { const dispatch = useAppDispatch(); const showSidebar = useAppSelector((state) => state.ui.sidebarOpen); - const [cookies] = useCookies(['user']); + const isLoggedIn = useIsLoggedIn(); const [isAdmin, setIsAdmin] = useState(false); - const isLoggedIn = cookies.user !== undefined; - - interface AdminResponse { - admin: boolean; - } - useEffect(() => { - // if the user is logged in if (isLoggedIn) { // useEffect's function is not allowed to be async, create async checkAdmin function within const checkAdmin = async () => { - const res: AdminResponse = await trpc.users.isAdmin.query(); - const isAdmin: boolean = res.admin; + const { isAdmin } = await trpc.users.get.query(); setIsAdmin(isAdmin); }; checkAdmin(); } - }, [cookies.user, isLoggedIn]); + }, [isLoggedIn]); const closeSidebar = () => dispatch(setSidebarStatus(false)); diff --git a/site/src/component/SideInfo/SideInfo.tsx b/site/src/component/SideInfo/SideInfo.tsx index 1de90c9e..e89c2671 100644 --- a/site/src/component/SideInfo/SideInfo.tsx +++ b/site/src/component/SideInfo/SideInfo.tsx @@ -93,9 +93,9 @@ const SideInfo: FC = (props) => { let key = ''; // determine the key to group reviews by if (props.searchType == 'course') { - key = review.professorID; + key = review.professorId; } else if (props.searchType == 'professor') { - key = review.courseID; + key = review.courseId; } // add review entry diff --git a/site/src/component/Verify/Verify.tsx b/site/src/component/Verify/Verify.tsx index 767e8983..7a25ba46 100644 --- a/site/src/component/Verify/Verify.tsx +++ b/site/src/component/Verify/Verify.tsx @@ -21,14 +21,14 @@ const Verify: FC = () => { document.title = 'Verify Reviews | PeterPortal'; }, []); - const verifyReview = async (reviewID: string) => { - await trpc.reviews.verify.mutate({ id: reviewID }); - getUnverifiedReviews(); + const verifyReview = async (reviewId: number) => { + await trpc.reviews.verify.mutate({ id: reviewId }); + setReviews(reviews.filter((review) => review.id !== reviewId)); }; - const deleteReview = async (reviewID: string) => { - await trpc.reviews.delete.mutate({ id: reviewID }); - getUnverifiedReviews(); + const deleteReview = async (reviewId: number) => { + await trpc.reviews.delete.mutate({ id: reviewId }); + setReviews(reviews.filter((review) => review.id !== reviewId)); }; if (!loaded) { @@ -46,10 +46,10 @@ const Verify: FC = () => {
- -
diff --git a/site/src/helpers/planner.ts b/site/src/helpers/planner.ts index 9b95552b..e7613cfc 100644 --- a/site/src/helpers/planner.ts +++ b/site/src/helpers/planner.ts @@ -10,14 +10,7 @@ import { } from '@peterportal/types'; import { searchAPIResults } from './util'; import { RoadmapPlan, defaultPlan } from '../store/slices/roadmapSlice'; -import { - BatchCourseData, - Coursebag, - InvalidCourseData, - PlannerData, - PlannerQuarterData, - PlannerYearData, -} from '../types/types'; +import { BatchCourseData, InvalidCourseData, PlannerData, PlannerQuarterData, PlannerYearData } from '../types/types'; import trpc from '../trpc'; export function defaultYear() { @@ -31,7 +24,6 @@ export function defaultYear() { } as PlannerYearData | SavedPlannerYearData; } -export const quarterNames: QuarterName[] = ['Fall', 'Winter', 'Spring', 'Summer1', 'Summer2', 'Summer10wk']; export const quarterDisplayNames: Record = { Fall: 'Fall', Winter: 'Winter', @@ -41,25 +33,6 @@ export const quarterDisplayNames: Record = { Summer10wk: 'Summer 10 Week', }; -export function normalizeQuarterName(name: string): QuarterName { - if (quarterNames.includes(name as QuarterName)) return name as QuarterName; - const lookup: { [k: string]: QuarterName } = { - fall: 'Fall', - winter: 'Winter', - spring: 'Spring', - // Old Lowercase Display Names - 'summer I': 'Summer1', - 'summer II': 'Summer2', - 'summer 10 Week': 'Summer10wk', - // Transcript Names - 'First Summer': 'Summer1', - 'Second Summer': 'Summer2', - 'Special / 10-Week Summer': 'Summer10wk', - }; - if (!lookup[name]) throw TypeError('Invalid Quarter Name: ' + name); - return lookup[name]; -} - // remove all unecessary data to store into the database export const collapsePlanner = (planner: PlannerData): SavedPlannerYearData[] => { const savedPlanner: SavedPlannerYearData[] = []; @@ -76,7 +49,11 @@ export const collapsePlanner = (planner: PlannerData): SavedPlannerYearData[] => }; export const collapseAllPlanners = (plans: RoadmapPlan[]): SavedPlannerData[] => { - return plans.map((p) => ({ name: p.name, content: collapsePlanner(p.content.yearPlans) })); + return plans.map((p) => ({ + ...(p.id ? { id: p.id } : {}), + name: p.name, + content: collapsePlanner(p.content.yearPlans), + })); }; // query the lost information from collapsing @@ -101,8 +78,7 @@ export const expandPlanner = async (savedPlanner: SavedPlannerYearData[]): Promi savedPlanner.forEach((savedYear) => { const year: PlannerYearData = { startYear: savedYear.startYear, name: savedYear.name, quarters: [] }; savedYear.quarters.forEach((savedQuarter) => { - const transformedName = normalizeQuarterName(savedQuarter.name); - const quarter: PlannerQuarterData = { name: transformedName, courses: [] }; + const quarter: PlannerQuarterData = { name: savedQuarter.name, courses: [] }; quarter.courses = savedQuarter.courses.map((course) => courseLookup[course]); year.quarters.push(quarter); }); @@ -116,32 +92,23 @@ export const expandAllPlanners = async (plans: SavedPlannerData[]): Promise { const content = await expandPlanner(p.content); - return { name: p.name, content: { yearPlans: content, invalidCourses: [] } }; + return { ...(p.id ? { id: p.id } : {}), name: p.name, content: { yearPlans: content, invalidCourses: [] } }; }), ); }; -interface RoadmapCookies { - user?: { id: string }; -} - export const loadRoadmap = async ( - cookies: RoadmapCookies, - loadHandler: (r: RoadmapPlan[], s: SavedRoadmap, coursebag: Coursebag, isLocalNewer: boolean) => void, + isLoggedIn: boolean, + loadHandler: (r: RoadmapPlan[], s: SavedRoadmap, isLocalNewer: boolean) => void, ) => { let roadmap: SavedRoadmap = null!; - let coursebagStrings: string[] = []; const localRoadmap: SavedRoadmap = JSON.parse(localStorage.getItem('roadmap') ?? 'null'); - // if logged in - if (cookies.user !== undefined) { + if (isLoggedIn) { // get data from account - const request = await trpc.roadmaps.get.query({ userID: cookies.user.id }); + const res = await trpc.roadmaps.get.query(); // if a roadmap is found - if (request?.roadmap) { - roadmap = request.roadmap; - } - if (request?.coursebag) { - coursebagStrings = request.coursebag; + if (res) { + roadmap = res; } } @@ -149,7 +116,7 @@ export const loadRoadmap = async ( if (!roadmap && localRoadmap) { roadmap = localRoadmap; - } else if (roadmap && localRoadmap && (localRoadmap.timestamp ?? 0) > (roadmap.timestamp ?? 0)) { + } else if (roadmap && localRoadmap && new Date(localRoadmap.timestamp ?? 0) > new Date(roadmap.timestamp ?? 0)) { isLocalNewer = true; } else if (!roadmap && !localRoadmap) { // no saved planner @@ -163,9 +130,7 @@ export const loadRoadmap = async ( : [{ name: defaultPlan.name, content: (roadmap as { planner: SavedPlannerYearData[] }).planner }]; // expand planner and set the state const planners = await expandAllPlanners(loadedData); - const coursesObj: BatchCourseData = (await searchAPIResults('courses', coursebagStrings)) as BatchCourseData; - const coursebag = coursebagStrings.map((id) => coursesObj[id]); - loadHandler(planners, roadmap, coursebag, isLocalNewer); + loadHandler(planners, roadmap, isLocalNewer); }; type PrerequisiteNode = Prerequisite | PrerequisiteTree; diff --git a/site/src/hooks/coursebag.ts b/site/src/hooks/coursebag.ts new file mode 100644 index 00000000..1499a6c9 --- /dev/null +++ b/site/src/hooks/coursebag.ts @@ -0,0 +1,36 @@ +import { useCallback, useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { addCourseToBagState, removeCourseFromBagState } from '../store/slices/coursebagSlice'; +import trpc from '../trpc'; +import { useIsLoggedIn } from './isLoggedIn'; +import { CourseGQLData } from '../types/types'; + +export function useCoursebag() { + const coursebag = useAppSelector((state) => state.coursebag.coursebag); + const dispatch = useAppDispatch(); + const isLoggedIn = useIsLoggedIn(); + + useEffect(() => { + if (coursebag !== undefined) { + localStorage.setItem('coursebag', JSON.stringify(coursebag.map((course) => course.id))); + } + }, [coursebag]); + + const addCourseToBag = useCallback( + (course: CourseGQLData) => { + dispatch(addCourseToBagState(course)); + if (isLoggedIn) trpc.savedCourses.add.mutate({ courseId: course.id }); + }, + [dispatch, isLoggedIn], + ); + + const removeCourseFromBag = useCallback( + (course: CourseGQLData) => { + dispatch(removeCourseFromBagState(course)); + if (isLoggedIn) trpc.savedCourses.remove.mutate({ courseId: course.id }); + }, + [dispatch, isLoggedIn], + ); + + return { coursebag: coursebag ?? [], addCourseToBag, removeCourseFromBag }; +} diff --git a/site/src/hooks/isLoggedIn.ts b/site/src/hooks/isLoggedIn.ts new file mode 100644 index 00000000..130d3f37 --- /dev/null +++ b/site/src/hooks/isLoggedIn.ts @@ -0,0 +1,7 @@ +import { useCookies } from 'react-cookie'; + +export function useIsLoggedIn() { + const [cookies] = useCookies(['user']); + + return cookies.user !== undefined; +} diff --git a/site/src/pages/AdminPage/index.tsx b/site/src/pages/AdminPage/index.tsx index eb97b67a..7eb3fbe3 100644 --- a/site/src/pages/AdminPage/index.tsx +++ b/site/src/pages/AdminPage/index.tsx @@ -10,15 +10,10 @@ const AdminPage: FC = () => { const [loaded, setLoaded] = useState(false); const [authorized, setAuthorized] = useState(false); - interface AdminResponse { - admin: boolean; - } - // user has to be authenticated as admin to view this page const checkAdmin = useCallback(async () => { - const res: AdminResponse = await trpc.users.isAdmin.query(); - const isAdmin: boolean = res.admin; - setAuthorized(isAdmin); + const res = await trpc.users.get.query(); + setAuthorized(res.isAdmin); setLoaded(true); }, []); diff --git a/site/src/pages/ReviewsPage/UserReviews.tsx b/site/src/pages/ReviewsPage/UserReviews.tsx index b29f1624..f6528868 100644 --- a/site/src/pages/ReviewsPage/UserReviews.tsx +++ b/site/src/pages/ReviewsPage/UserReviews.tsx @@ -2,7 +2,6 @@ import { FC, useCallback, useEffect, useState } from 'react'; import SubReview from '../../component/Review/SubReview'; import { Divider } from 'semantic-ui-react'; import './UserReviews.scss'; -import { useCookies } from 'react-cookie'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { selectReviews, setReviews } from '../../store/slices/reviewSlice'; import trpc from '../../trpc'; @@ -10,14 +9,13 @@ import trpc from '../../trpc'; const UserReviews: FC = () => { const reviews = useAppSelector(selectReviews); const [loaded, setLoaded] = useState(false); - const [cookies] = useCookies(['user']); const dispatch = useAppDispatch(); const getUserReviews = useCallback(async () => { - const response = await trpc.reviews.get.query({ userID: cookies.user.id }); + const response = await trpc.reviews.getUsersReviews.query(); dispatch(setReviews(response)); setLoaded(true); - }, [cookies.user.id, dispatch]); + }, [dispatch]); useEffect(() => { getUserReviews(); @@ -33,7 +31,7 @@ const UserReviews: FC = () => {

Your Reviews

Deleting a review will remove it permanently.

{reviews.map((review) => ( -
+
diff --git a/site/src/pages/ReviewsPage/index.tsx b/site/src/pages/ReviewsPage/index.tsx index 347d45b7..1febd9a7 100644 --- a/site/src/pages/ReviewsPage/index.tsx +++ b/site/src/pages/ReviewsPage/index.tsx @@ -1,37 +1,20 @@ -import { FC, useEffect, useState } from 'react'; +import { FC, useEffect } from 'react'; import UserReviews from './UserReviews'; import Error from '../../component/Error/Error'; -import { useLocation } from 'react-router-dom'; -import { useCookies } from 'react-cookie'; +import { useIsLoggedIn } from '../../hooks/isLoggedIn'; const ReviewsPage: FC = () => { - const location = useLocation(); - const [loaded, setLoaded] = useState(false); - const [cookies] = useCookies(['user']); - const [authorized, setAuthorized] = useState(false); - - // user has to be logged in to view this page - const checkLoggedIn = async () => { - const loggedIn = cookies.user !== undefined; - setAuthorized(loggedIn); - setLoaded(true); - }; + const isLoggedIn = useIsLoggedIn(); useEffect(() => { - checkLoggedIn(); document.title = 'Your Reviews | PeterPortal'; }, []); - if (!loaded) { - return

Loading...

; - } else if (!authorized) { + if (!isLoggedIn) { return ; - } else { - if (location.pathname.includes('reviews')) { - return ; - } } - return ; + + return ; }; export default ReviewsPage; diff --git a/site/src/pages/RoadmapPage/CourseBag.tsx b/site/src/pages/RoadmapPage/CourseBag.tsx index 7209894b..ed5e876b 100644 --- a/site/src/pages/RoadmapPage/CourseBag.tsx +++ b/site/src/pages/RoadmapPage/CourseBag.tsx @@ -1,11 +1,9 @@ -import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { removeCourseFromBag } from '../../store/slices/roadmapSlice'; +import { useCoursebag } from '../../hooks/coursebag'; import Course from './Course'; import './Coursebag.scss'; import { Draggable } from 'react-beautiful-dnd'; const CourseBag = () => { - const { coursebag } = useAppSelector((state) => state.roadmap); - const dispatch = useAppDispatch(); + const { coursebag, removeCourseFromBag } = useCoursebag(); return (
@@ -29,7 +27,7 @@ const CourseBag = () => { { - dispatch(removeCourseFromBag(course)); + removeCourseFromBag(course); }} />
diff --git a/site/src/pages/RoadmapPage/CourseHitItem.tsx b/site/src/pages/RoadmapPage/CourseHitItem.tsx index 1371655e..c79d42ad 100644 --- a/site/src/pages/RoadmapPage/CourseHitItem.tsx +++ b/site/src/pages/RoadmapPage/CourseHitItem.tsx @@ -1,16 +1,12 @@ import { FC } from 'react'; import { Draggable } from 'react-beautiful-dnd'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { - addCourseToBag, - removeCourseFromBag, - setActiveCourse, - setShowAddCourse, -} from '../../store/slices/roadmapSlice'; +import { useAppDispatch } from '../../store/hooks'; +import { setActiveCourse, setShowAddCourse } from '../../store/slices/roadmapSlice'; import Course from './Course'; import { useIsMobile } from '../../helpers/util'; import { CourseGQLData } from '../../types/types'; +import { useCoursebag } from '../../hooks/coursebag'; interface CourseHitItemProps extends CourseGQLData { index: number; @@ -19,7 +15,7 @@ interface CourseHitItemProps extends CourseGQLData { const CourseHitItem: FC = (props: CourseHitItemProps) => { const dispatch = useAppDispatch(); const isMobile = useIsMobile(); - const coursebag = useAppSelector((state) => state.roadmap.coursebag); + const { coursebag, addCourseToBag, removeCourseFromBag } = useCoursebag(); const isInBag = coursebag.some((course) => course.id === props.id); // do not make course draggable on mobile const onMobileMouseDown = () => { @@ -36,10 +32,10 @@ const CourseHitItem: FC = (props: CourseHitItemProps) => { if (!props) return; if (props.id === undefined) return; if (coursebag.some((course) => course.id === props.id)) return; - dispatch(addCourseToBag(props)); + addCourseToBag(props); }; const removeFromBag = () => { - dispatch(removeCourseFromBag(props)); + removeCourseFromBag(props); }; if (isMobile) { diff --git a/site/src/pages/RoadmapPage/ImportTranscriptPopup.tsx b/site/src/pages/RoadmapPage/ImportTranscriptPopup.tsx index 2c1d6ed0..e64dec03 100644 --- a/site/src/pages/RoadmapPage/ImportTranscriptPopup.tsx +++ b/site/src/pages/RoadmapPage/ImportTranscriptPopup.tsx @@ -7,7 +7,7 @@ import { useAppDispatch } from '../../store/hooks'; import { parse as parseHTML, HTMLElement } from 'node-html-parser'; import ThemeContext from '../../style/theme-context'; import { BatchCourseData, PlannerQuarterData, PlannerYearData } from '../../types/types'; -import { normalizeQuarterName, quarterNames } from '../../helpers/planner'; +import { quarterNames } from '@peterportal/types'; import { searchAPIResults } from '../../helpers/util'; import { QuarterName } from '@peterportal/types'; @@ -87,8 +87,7 @@ function toPlannerQuarter( ): { startYear: number; quarterData: PlannerQuarterData } { const year = parseInt(quarter.name.split(' ')[0]); // Removes the year number and "Session/Quarter" at the end - const tName = quarter.name.split(' ').slice(1, -1).join(' ') as QuarterName; - const name = normalizeQuarterName(tName); + const name = quarter.name.split(' ').slice(1, -1).join(' ') as QuarterName; return { startYear: name === 'Fall' ? year : year - 1, diff --git a/site/src/pages/RoadmapPage/Planner.tsx b/site/src/pages/RoadmapPage/Planner.tsx index 9194fe21..c0eae1d2 100644 --- a/site/src/pages/RoadmapPage/Planner.tsx +++ b/site/src/pages/RoadmapPage/Planner.tsx @@ -1,5 +1,4 @@ import { FC, useEffect, useState } from 'react'; -import { useCookies } from 'react-cookie'; import './Planner.scss'; import Header from './Header'; import AddYearPopup from './AddYearPopup'; @@ -13,29 +12,28 @@ import { selectAllPlans, setAllPlans, defaultPlan, - setCoursebag, } from '../../store/slices/roadmapSlice'; import { useFirstRender } from '../../hooks/firstRenderer'; -import { SavedRoadmap, MongoRoadmap } from '@peterportal/types'; +import { SavedRoadmap } from '@peterportal/types'; import { defaultYear, expandAllPlanners } from '../../helpers/planner'; import ImportTranscriptPopup from './ImportTranscriptPopup'; import { collapseAllPlanners, loadRoadmap, validatePlanner } from '../../helpers/planner'; import { Button, Modal } from 'react-bootstrap'; import trpc from '../../trpc'; +import { useIsLoggedIn } from '../../hooks/isLoggedIn'; const Planner: FC = () => { const dispatch = useAppDispatch(); - const [cookies] = useCookies(['user']); + const isLoggedIn = useIsLoggedIn(); const isFirstRenderer = useFirstRender(); const currentPlanData = useAppSelector(selectYearPlans); const allPlanData = useAppSelector(selectAllPlans); const transfers = useAppSelector((state) => state.roadmap.transfers); - const coursebag = useAppSelector((state) => state.roadmap.coursebag); const [showSyncModal, setShowSyncModal] = useState(false); const [missingPrerequisites, setMissingPrerequisites] = useState(new Set()); const roadmapStr = JSON.stringify({ - planners: collapseAllPlanners(allPlanData), + planners: collapseAllPlanners(allPlanData).map((p) => ({ name: p.name, content: p.content })), // map to remove id attribute transfers: transfers, }); @@ -54,11 +52,10 @@ const Planner: FC = () => { const saveRoadmap = () => { const roadmap: SavedRoadmap = { - timestamp: Date.now(), + timestamp: new Date().toISOString(), planners: collapseAllPlanners(allPlanData), transfers: transfers, }; - const coursebagStrings = coursebag.map((course) => course.id); localStorage.setItem('roadmap', JSON.stringify(roadmap)); @@ -66,12 +63,11 @@ const Planner: FC = () => { dispatch(setUnsavedChanges(false)); // if logged in, save data to account - if (cookies.user !== undefined) { - const mongoRoadmap: MongoRoadmap = { userID: cookies.user.id, roadmap: roadmap, coursebag: coursebagStrings }; + if (isLoggedIn) { trpc.roadmaps.save - .mutate(mongoRoadmap) + .mutate(roadmap) .then(() => { - alert(`Roadmap saved under ${cookies.user.email}`); + alert(`Roadmap saved to your account!`); }) .catch(() => { alert('Roadmap saved locally! Login to save it to your account.'); @@ -115,10 +111,9 @@ const Planner: FC = () => { // if first render and current roadmap is empty, load from local storage if (isFirstRenderer && roadmapStr === emptyRoadmap) { - loadRoadmap(cookies, (planners, roadmap, coursebag, isLocalNewer) => { + loadRoadmap(isLoggedIn, (planners, roadmap, isLocalNewer) => { dispatch(setAllPlans(planners)); dispatch(setTransfers(roadmap.transfers)); - dispatch(setCoursebag(coursebag)); if (isLocalNewer) { setShowSyncModal(true); } @@ -133,11 +128,12 @@ const Planner: FC = () => { // check current roadmap against last-saved roadmap in local storage // if they are different, mark changes as unsaved to enable alert on page leave - const localRoadmap = JSON.parse(localStorage.getItem('roadmap') ?? emptyRoadmap); + const localRoadmap: SavedRoadmap = JSON.parse(localStorage.getItem('roadmap') ?? emptyRoadmap); delete localRoadmap.timestamp; + localRoadmap.planners = localRoadmap.planners.map((p) => ({ name: p.name, content: p.content })); // remove id attribute dispatch(setUnsavedChanges(JSON.stringify(localRoadmap) !== roadmapStr)); } - }, [cookies, currentPlanData, dispatch, isFirstRenderer, roadmapStr, transfers]); + }, [isLoggedIn, currentPlanData, dispatch, isFirstRenderer, roadmapStr, transfers]); const { unitCount, courseCount } = calculatePlannerOverviewStats(); diff --git a/site/src/pages/RoadmapPage/Transfer.tsx b/site/src/pages/RoadmapPage/Transfer.tsx index 64d98e7d..4e145770 100644 --- a/site/src/pages/RoadmapPage/Transfer.tsx +++ b/site/src/pages/RoadmapPage/Transfer.tsx @@ -55,7 +55,7 @@ const TransferEntry: FC = (props) => { setUnits(parseInt(e.target.value))} /> diff --git a/site/src/pages/RoadmapPage/YearModal.tsx b/site/src/pages/RoadmapPage/YearModal.tsx index cd5ab7f0..082a45ec 100644 --- a/site/src/pages/RoadmapPage/YearModal.tsx +++ b/site/src/pages/RoadmapPage/YearModal.tsx @@ -1,8 +1,8 @@ import React, { FC, useState } from 'react'; import { Button, Form, Modal } from 'react-bootstrap'; import { PlannerYearData } from '../../types/types'; -import { quarterDisplayNames, quarterNames, normalizeQuarterName } from '../../helpers/planner'; -import { QuarterName } from '@peterportal/types'; +import { quarterDisplayNames } from '../../helpers/planner'; +import { quarterNames, QuarterName } from '@peterportal/types'; interface YearPopupQuarter { id: QuarterName; @@ -22,8 +22,7 @@ interface YearModalProps { const quarterValues: (selectedQuarters: string[]) => YearPopupQuarter[] = (quarterIds: string[]) => { const base: YearPopupQuarter[] = quarterNames.map((n) => ({ id: n })); quarterIds.forEach((id) => { - const translated = normalizeQuarterName(id); - const quarter = base.find((q) => q.id === translated)!; + const quarter = base.find((q) => q.id === id)!; quarter.checked = true; }); return base; diff --git a/site/src/pages/RoadmapPage/index.tsx b/site/src/pages/RoadmapPage/index.tsx index 60532f8b..3ee01d6b 100644 --- a/site/src/pages/RoadmapPage/index.tsx +++ b/site/src/pages/RoadmapPage/index.tsx @@ -4,22 +4,17 @@ import Planner from './Planner'; import SearchSidebar from './SearchSidebar'; import { DragDropContext, DropResult, DragStart } from 'react-beautiful-dnd'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { - moveCourse, - deleteCourse, - setActiveCourse, - addCourseToBag, - removeCourseFromBag, -} from '../../store/slices/roadmapSlice'; +import { moveCourse, deleteCourse, setActiveCourse } from '../../store/slices/roadmapSlice'; import AddCoursePopup from './AddCoursePopup'; import { CourseGQLData } from '../../types/types'; import { useIsMobile } from '../../helpers/util'; +import { useCoursebag } from '../../hooks/coursebag'; const RoadmapPage: FC = () => { const dispatch = useAppDispatch(); const showSearch = useAppSelector((state) => state.roadmap.showSearch); const searchResults = useAppSelector((state) => state.search.courses.results) as CourseGQLData[]; - const courseBag = useAppSelector((state) => state.roadmap.coursebag); + const { coursebag, addCourseToBag, removeCourseFromBag } = useCoursebag(); const isMobile = useIsMobile(); const roadmaps = useAppSelector((state) => state.roadmap.plans); const roadmap = roadmaps[useAppSelector((state) => state.roadmap.currentPlanIndex)].content.yearPlans; @@ -49,7 +44,7 @@ const RoadmapPage: FC = () => { if (result.destination.droppableId === 'coursebag' && result.source.droppableId != 'coursebag') { const [yearIndex, quarterIndex]: string[] = result.source.droppableId.split('-'); const course = roadmap[parseInt(yearIndex)].quarters[parseInt(quarterIndex)].courses[result.source.index]; - dispatch(addCourseToBag(course)); + addCourseToBag(course); dispatch( deleteCourse({ yearIndex: parseInt(yearIndex), @@ -62,9 +57,9 @@ const RoadmapPage: FC = () => { } if (result.source.droppableId === 'coursebag' && result.destination.droppableId != 'coursebag') { - const course = courseBag[result.source.index]; + const course = coursebag[result.source.index]; - dispatch(removeCourseFromBag(course)); + removeCourseFromBag(course); } const movePayload = { @@ -98,7 +93,7 @@ const RoadmapPage: FC = () => { dispatch(moveCourse(movePayload)); } }, - [courseBag, dispatch, roadmap], + [coursebag, dispatch, roadmap], ); const onDragStart = useCallback( @@ -108,11 +103,11 @@ const RoadmapPage: FC = () => { dispatch(setActiveCourse(activeCourse)); } if (start.source.droppableId === 'coursebag') { - const activeCourse = courseBag[start.source.index]; + const activeCourse = coursebag[start.source.index]; dispatch(setActiveCourse(activeCourse)); } }, - [dispatch, searchResults, courseBag], + [dispatch, searchResults, coursebag], ); // do not conditionally renderer because it would remount planner which would discard unsaved changes diff --git a/site/src/pages/SearchPage/CourseHitItem.tsx b/site/src/pages/SearchPage/CourseHitItem.tsx index 12cb0bb1..c993fe1b 100644 --- a/site/src/pages/SearchPage/CourseHitItem.tsx +++ b/site/src/pages/SearchPage/CourseHitItem.tsx @@ -9,7 +9,7 @@ import { setCourse } from '../../store/slices/popupSlice'; import { CourseGQLData } from '../../types/types'; import { getCourseTags, useIsMobile } from '../../helpers/util'; import { BagFill, BagPlus } from 'react-bootstrap-icons'; -import { addCourseToBag, removeCourseFromBag } from '../../store/slices/roadmapSlice'; +import { useCoursebag } from '../../hooks/coursebag'; interface CourseHitItemProps extends CourseGQLData {} const CourseHitItem: FC = (props) => { @@ -17,7 +17,7 @@ const CourseHitItem: FC = (props) => { const navigate = useNavigate(); const activeCourse = useAppSelector((state) => state.popup.course); const isMobile = useIsMobile(); - const coursebag = useAppSelector((state) => state.roadmap.coursebag); + const { coursebag, addCourseToBag, removeCourseFromBag } = useCoursebag(); const isInBag = coursebag.some((course) => course.id === props.id); // data to be displayed in pills @@ -45,12 +45,12 @@ const CourseHitItem: FC = (props) => { if (!props) return; if (props.id === undefined) return; if (coursebag.some((course) => course.id === props.id)) return; - dispatch(addCourseToBag(props)); + addCourseToBag(props); }; const removeFromBag = (e: React.MouseEvent) => { e.stopPropagation(); - dispatch(removeCourseFromBag(props)); + removeCourseFromBag(props); }; return ( diff --git a/site/src/pages/SearchPage/ProfessorPopup.tsx b/site/src/pages/SearchPage/ProfessorPopup.tsx index 02dc2a89..8f8cdf73 100644 --- a/site/src/pages/SearchPage/ProfessorPopup.tsx +++ b/site/src/pages/SearchPage/ProfessorPopup.tsx @@ -56,7 +56,7 @@ const ProfessorPopup: FC = () => { { title: `Featured Review`, content: featured - ? `For ${featured.courseID}: ${featured.reviewContent.length > 0 ? featured.reviewContent : 'Rating of ' + featured.rating + '/5'}` + ? `For ${featured.courseId}: ${(featured.content ?? '').length > 0 ? featured.content : 'Rating of ' + featured.rating + '/5'}` : 'No Reviews Yet!', }, ]; diff --git a/site/src/store/slices/coursebagSlice.ts b/site/src/store/slices/coursebagSlice.ts new file mode 100644 index 00000000..9107ca94 --- /dev/null +++ b/site/src/store/slices/coursebagSlice.ts @@ -0,0 +1,27 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { RootState } from '../store'; +import type { Coursebag, CourseGQLData } from '../../types/types'; + +const initialState: { coursebag?: Coursebag } = {}; + +export const coursebagSlice = createSlice({ + name: 'coursebag', + initialState, + reducers: { + setCoursebag(state, action: PayloadAction) { + state.coursebag = action.payload; + }, + addCourseToBagState: (state, action: PayloadAction) => { + state.coursebag?.push(action.payload); + }, + removeCourseFromBagState: (state, action: PayloadAction) => { + state.coursebag = state.coursebag?.filter((course) => course.id !== action.payload.id); + }, + }, +}); + +export const { setCoursebag, addCourseToBagState, removeCourseFromBagState } = coursebagSlice.actions; + +export const selectCoursebag = (state: RootState) => state.coursebag.coursebag; + +export default coursebagSlice.reducer; diff --git a/site/src/store/slices/reviewSlice.ts b/site/src/store/slices/reviewSlice.ts index 976e79e1..cad00efc 100644 --- a/site/src/store/slices/reviewSlice.ts +++ b/site/src/store/slices/reviewSlice.ts @@ -27,7 +27,7 @@ export const reviewSlice = createSlice({ state.reviews = action.payload; }, editReview: (state, action: PayloadAction) => { - const i = state.reviews.findIndex((review) => review._id === action.payload._id); + const i = state.reviews.findIndex((review) => review.id === action.payload.id); state.reviews[i] = { ...state.reviews[i], ...action.payload }; // overwrite with edited properties }, setFormStatus: (state, action: PayloadAction) => { diff --git a/site/src/store/slices/roadmapSlice.ts b/site/src/store/slices/roadmapSlice.ts index 3ba4705c..652d5fc5 100644 --- a/site/src/store/slices/roadmapSlice.ts +++ b/site/src/store/slices/roadmapSlice.ts @@ -1,7 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { defaultYear, quarterDisplayNames } from '../../helpers/planner'; import { - Coursebag, CourseGQLData, CourseIdentifier, InvalidCourseData, @@ -31,6 +30,7 @@ export const initialPlanState: RoadmapPlanState = { /** added for multiple planner */ // create roadmap plan object export interface RoadmapPlan { + id?: number; name: string; content: RoadmapPlanState; } @@ -58,8 +58,6 @@ interface RoadmapSliceState { unsavedChanges: boolean; // Selected quarter and year for adding a course on mobile currentYearAndQuarter: { year: number; quarter: number } | null; - // Store the course data of the active dragging item - coursebag: Coursebag; showCourseBag: boolean; // Whether or not to show the search bar on mobile showSearch: boolean; @@ -83,7 +81,6 @@ const initialSliceState: RoadmapSliceState = { showAddCourse: false, showTransfer: false, transfers: [], - coursebag: [], showCourseBag: false, }; /** added for multiple planner */ @@ -345,16 +342,6 @@ export const roadmapSlice = createSlice({ const index = action.payload.index; state.plans[index].name = action.payload.name; }, - /** added for multiple plans */ - setCoursebag(state, action: PayloadAction) { - state.coursebag = action.payload; - }, - addCourseToBag: (state, action: PayloadAction) => { - state.coursebag.push(action.payload); - }, - removeCourseFromBag: (state, action: PayloadAction) => { - state.coursebag = state.coursebag.filter((course) => course.id !== action.payload.id); - }, setShowCourseBag: (state, action: PayloadAction) => { state.showCourseBag = action.payload; }, @@ -401,10 +388,7 @@ export const { setPlanIndex, setPlanName, setUnsavedChanges, - addCourseToBag, - removeCourseFromBag, setShowCourseBag, - setCoursebag, } = roadmapSlice.actions; // Other code such as selectors can use the imported `RootState` type diff --git a/site/src/store/store.ts b/site/src/store/store.ts index 6fbde95d..5d39e7f7 100644 --- a/site/src/store/store.ts +++ b/site/src/store/store.ts @@ -1,4 +1,5 @@ import { configureStore } from '@reduxjs/toolkit'; +import coursebagReducer from './slices/coursebagSlice'; import reviewReducer from './slices/reviewSlice'; import uiReducer from './slices/uiSlice'; import popupReducer from './slices/popupSlice'; @@ -7,6 +8,7 @@ import searchReducer from './slices/searchSlice'; export const store = configureStore({ reducer: { + coursebag: coursebagReducer, review: reviewReducer, ui: uiReducer, popup: popupReducer, diff --git a/stacks/backend.ts b/stacks/backend.ts index 370257f6..6fe70160 100644 --- a/stacks/backend.ts +++ b/stacks/backend.ts @@ -8,7 +8,7 @@ export function BackendStack({ stack }: StackContext) { runtime: 'nodejs18.x', logRetention: stack.stage === 'prod' ? 'two_years' : 'one_week', environment: { - MONGO_URL: process.env.MONGO_URL!, + DATABASE_URL: process.env.DATABASE_URL!, SESSION_SECRET: process.env.SESSION_SECRET!, PUBLIC_API_URL: process.env.PUBLIC_API_URL!, PUBLIC_API_GRAPHQL_URL: process.env.PUBLIC_API_GRAPHQL_URL!, diff --git a/types/src/index.ts b/types/src/index.ts index bd9f2604..e899f030 100644 --- a/types/src/index.ts +++ b/types/src/index.ts @@ -1,3 +1,5 @@ +import { quarters } from 'peterportal-api-next-types'; + export * from './course'; export * from './professor'; export * from './roadmap'; @@ -6,3 +8,4 @@ export * from './review'; export * from './user'; export * from 'peterportal-api-next-types'; +export const quarterNames = quarters; diff --git a/types/src/report.ts b/types/src/report.ts index 4b9f0f23..429972db 100644 --- a/types/src/report.ts +++ b/types/src/report.ts @@ -1,13 +1,13 @@ import { z } from 'zod'; export const reportSubmission = z.object({ - reviewID: z.string(), + reviewId: z.number(), reason: z.string().min(1).max(500), }); export type ReportSubmission = z.infer; export const reportData = reportSubmission.extend({ - _id: z.string(), - timestamp: z.string(), + id: z.number(), + createdAt: z.string(), }); export type ReportData = z.infer; diff --git a/types/src/review.ts b/types/src/review.ts index 601bcac4..41541e4c 100644 --- a/types/src/review.ts +++ b/types/src/review.ts @@ -1,5 +1,7 @@ import { z } from 'zod'; +export const anonymousName = 'Anonymous Peter'; + export const grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F', 'P', 'NP'] as const; export const gradesEnum = z.enum(grades); export type ReviewGrade = z.infer; @@ -26,10 +28,10 @@ export const tagsEnum = z.enum(tags); export type ReviewTags = z.infer; export const reviewSubmission = z.object({ - professorID: z.string(), - courseID: z.string(), - userDisplay: z.string(), - reviewContent: z.string().max(500), + professorId: z.string(), + courseId: z.string(), + anonymous: z.boolean(), + content: z.string().max(500).optional(), rating: z.number().min(1).max(5), difficulty: z.number().min(1).max(5), gradeReceived: gradesEnum, @@ -43,26 +45,23 @@ export const reviewSubmission = z.object({ }); export type ReviewSubmission = z.infer; -export const editReviewSubmission = reviewSubmission.extend({ _id: z.string() }); +export const editReviewSubmission = reviewSubmission.extend({ id: z.number() }); export type EditReviewSubmission = z.infer; -export const reviewData = reviewSubmission.omit({ captchaToken: true }).extend({ - _id: z.string(), - userID: z.string(), +export const reviewData = reviewSubmission.omit({ captchaToken: true, anonymous: true }).extend({ + id: z.number(), + userId: z.number(), + userDisplay: z.string(), verified: z.boolean(), score: z.number(), userVote: z.number(), - timestamp: z.string(), + createdAt: z.string(), + updatedAt: z.string().optional(), + authored: z.boolean(), }); export type ReviewData = z.infer; -export type FeaturedReviewData = Omit; - -export const voteRequest = z.object({ - id: z.string(), - upvote: z.boolean(), -}); -export type VoteRequest = z.infer; +export type FeaturedReviewData = Omit; export const featuredQuery = z.object({ type: z.enum(['course', 'professor']), diff --git a/types/src/roadmap.ts b/types/src/roadmap.ts index 9c419172..1df5fbbd 100644 --- a/types/src/roadmap.ts +++ b/types/src/roadmap.ts @@ -5,22 +5,8 @@ export const quarterName = z.enum(quarters); /** @todo duplicate of Quarter from peterportal-api-next-types. probably don't need but also don't know if new api has a types package */ export type QuarterName = z.infer; -// these aren't used anymore but some old roadmaps still have them, included for valid schema -/** @todo migrate legacy quarter names to new ones */ -export const legacyQuarterNames = z.enum([ - 'fall', - 'winter', - 'spring', - 'summer I', - 'summer II', - 'summer 10 Week', - 'First Summer', - 'Second Summer', - 'Special / 10-Week Summer', -]); - export const savedPlannerQuarterData = z.object({ - name: quarterName.or(legacyQuarterNames), + name: quarterName, courses: z.array(z.string()), }); export type SavedPlannerQuarterData = z.infer; @@ -33,6 +19,7 @@ export const savedPlannerYearData = z.object({ export type SavedPlannerYearData = z.infer; export const savedPlannerData = z.object({ + id: z.number().optional(), name: z.string().max(35), content: z.array(savedPlannerYearData), }); @@ -41,13 +28,13 @@ export type SavedPlannerData = z.infer; // Specify name of transfer course and how many units its worth export const transferData = z.object({ name: z.string(), - units: z.number().optional(), + units: z.number().nullish(), }); export type TransferData = z.infer; // Bundle planner and transfer data in one object export const savedRoadmap = z.object({ - timestamp: z.number(), + timestamp: z.string().optional(), planners: z.array(savedPlannerData), transfers: z.array(transferData), }); @@ -56,7 +43,7 @@ export type SavedRoadmap = z.infer; // Structure stored in mongo for accounts export const mongoRoadmap = z.object({ roadmap: savedRoadmap, - userID: z.string(), + userId: z.number(), coursebag: z.array(z.string()), }); export type MongoRoadmap = z.infer; diff --git a/types/src/user.ts b/types/src/user.ts index e6ef9d17..02b9291c 100644 --- a/types/src/user.ts +++ b/types/src/user.ts @@ -3,15 +3,19 @@ import { z } from 'zod'; export const theme = z.enum(['light', 'dark', 'system']); export type Theme = z.infer; -export const userPreferences = z.object({ - theme: theme, -}); -export type UserPreferences = z.infer; - -/** @todo make use of this on frontend when accessing user cookie with useCookies */ -export interface User { +export interface PassportUser { + /** + * google id + */ id: string; email: string; name: string; picture: string; } + +export interface UserData extends Omit { + id: number; + theme: Theme; + isAdmin: boolean; + lastRoadmapEditAt?: string; +}