diff --git a/components/stats/Picklist.tsx b/components/stats/Picklist.tsx index 939a366f..502114c8 100644 --- a/components/stats/Picklist.tsx +++ b/components/stats/Picklist.tsx @@ -20,6 +20,8 @@ import { const SHOW_PICKLISTS_ON_TEAM_CARDS = false; const SHOW_CARD_IDS = false; +const api = new ClientApi(); + function TeamCard(props: { entry: PicklistEntry; draggable: boolean; @@ -297,8 +299,6 @@ export function TeamList(props: { ); } -const api = new ClientApi(); - export default function PicklistScreen(props: { teams: number[]; reports: Report[]; @@ -321,10 +321,16 @@ export default function PicklistScreen(props: { const teams = props.teams.map((team) => ({ number: team })); // Save picklists - useEffect( - () => savePicklistGroup(props.picklist._id, picklists, strikethroughs, api), - [props.picklist._id, picklists, strikethroughs], - ); + useEffect(() => { + if (loadingPicklists !== LoadState.Loaded) return; + savePicklistGroup(props.picklist._id, picklists, strikethroughs, api); + }, [ + props.picklist._id, + picklists, + strikethroughs, + LoadState.Loaded, + loadingPicklists, + ]); const updatePicklist = useCallback( (picklist: Picklist) => { diff --git a/lib/MongoDB.ts b/lib/MongoDB.ts index eada8b7f..eab6f292 100644 --- a/lib/MongoDB.ts +++ b/lib/MongoDB.ts @@ -5,6 +5,7 @@ import DbInterface, { WithStringOrObjectIdId, } from "./client/dbinterfaces/DbInterface"; import { default as BaseMongoDbInterface } from "mongo-anywhere/MongoDbInterface"; +import CachedDbInterface from "./client/dbinterfaces/CachedDbInterface"; if (!process.env.MONGODB_URI) { // Necessary to allow connections from files running outside of Next @@ -28,10 +29,13 @@ clientPromise = global.clientPromise; export { clientPromise }; -export async function getDatabase(): Promise { +export async function getDatabase(): Promise { if (!global.interface) { await clientPromise; - const dbInterface = new MongoDBInterface(clientPromise); + const dbInterface = new CachedDbInterface( + new MongoDBInterface(clientPromise), + { stdTTL: 120, useClones: false }, + ); await dbInterface.init(); global.interface = dbInterface; diff --git a/lib/ResendUtils.ts b/lib/ResendUtils.ts index bac16f7b..106b1f79 100644 --- a/lib/ResendUtils.ts +++ b/lib/ResendUtils.ts @@ -3,6 +3,7 @@ import { Resend } from "resend"; import { getDatabase } from "./MongoDB"; import { User } from "./Types"; import CollectionId from "./client/CollectionId"; +import { ObjectId } from "bson"; const resend = new Resend(process.env.SMTP_PASSWORD); @@ -41,13 +42,16 @@ export class ResendUtils implements ResendInterface { } const db = await getDatabase(); - // Going around our own interface is a red flag, but it's 11 PM and I'm tired -Renato - db.db - ?.collection(CollectionId.Users) - .updateOne( - { email: user.email }, - { $set: { resendContactId: res.data.id } }, - ); + const id = (await db.findObject(CollectionId.Users, { email: user.email })) + ?._id; + if (!id) { + console.error("User not found in database", user.email); + return; + } + + db.updateObjectById(CollectionId.Users, new ObjectId(id), { + resendContactId: res.data.id, + }); } async emailDevelopers(subject: string, message: string) { diff --git a/lib/TheBlueAlliance.ts b/lib/TheBlueAlliance.ts index aadb7527..deb03646 100644 --- a/lib/TheBlueAlliance.ts +++ b/lib/TheBlueAlliance.ts @@ -12,6 +12,7 @@ import { import { NotLinkedToTba } from "./client/ClientUtils"; import { GameId, defaultGameId } from "./client/GameId"; import { games } from "./games"; +import DbInterface from "./client/dbinterfaces/DbInterface"; export namespace TheBlueAlliance { export interface SimpleTeam { @@ -209,7 +210,7 @@ export namespace TheBlueAlliance { export class Interface { req: Request; - db: Promise; + db: Promise; competitionPairings: CompetitonNameIdPair[] = []; diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index 55a1084e..03c2ea5e 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -2303,4 +2303,32 @@ export default class ClientApi extends NextApiTemplate { res.status(200).send(responseObj); }, }); + + getCacheStats = createNextRoute< + [], + object | undefined, + ApiDependencies, + void + >({ + isAuthorized: AccessLevels.IfDeveloper, + handler: async (req, res, {}, authData, args) => { + if (!global.cache) return res.status(200).send(undefined); + const stats = global.cache.getStats(); + return res.status(200).send(stats); + }, + }); + + getCachedValue = createNextRoute< + [string], + object | undefined, + ApiDependencies, + void + >({ + isAuthorized: AccessLevels.IfDeveloper, + handler: async (req, res, {}, authData, [key]) => { + if (!global.cache) return res.status(500).send({ error: "No cache" }); + const val = global.cache.get(key) as object | undefined; + return res.status(200).send(val); + }, + }); } diff --git a/lib/client/dbinterfaces/CachedDbInterface.ts b/lib/client/dbinterfaces/CachedDbInterface.ts new file mode 100644 index 00000000..027bfe54 --- /dev/null +++ b/lib/client/dbinterfaces/CachedDbInterface.ts @@ -0,0 +1,54 @@ +import { ObjectId } from "bson"; +import CollectionId, { CollectionIdToType } from "@/lib/client/CollectionId"; +import DbInterface, { + WithStringOrObjectIdId, +} from "@/lib/client/dbinterfaces/DbInterface"; +import { default as BaseCachedDbInterface } from "mongo-anywhere/CachedDbInterface"; + +export default class CachedDbInterface + extends BaseCachedDbInterface> + implements DbInterface +{ + init(): Promise { + return super.init(Object.values(CollectionId)); + } + addObject>( + collection: TId, + object: WithStringOrObjectIdId, + ): Promise { + return super.addObject(collection, object); + } + deleteObjectById(collection: CollectionId, id: ObjectId): Promise { + return super.deleteObjectById(collection, id); + } + updateObjectById< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, id: ObjectId, newValues: Partial): Promise { + return super.updateObjectById(collection, id, newValues); + } + findObjectById< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, id: ObjectId): Promise { + return super.findObjectById(collection, id); + } + findObject>( + collection: TId, + query: object, + ): Promise { + return super.findObject(collection, query); + } + findObjects>( + collection: TId, + query: object, + ): Promise { + return super.findObjects(collection, query); + } + countObjects( + collection: CollectionId, + query: object, + ): Promise { + return super.countObjects(collection, query); + } +} diff --git a/package-lock.json b/package-lock.json index 2b45e283..962384b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.1.21", + "version": "1.2.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.1.21", + "version": "1.2.2", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", @@ -26,7 +26,7 @@ "jose": "^5.9.6", "levenary": "^1.1.1", "minimongo": "^6.19.0", - "mongo-anywhere": "^1.0.21", + "mongo-anywhere": "^1.1.2", "mongodb": "^5.0.0", "next": "^15.1.6", "next-auth": "^4.24.11", @@ -3588,6 +3588,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7870,13 +7878,14 @@ } }, "node_modules/mongo-anywhere": { - "version": "1.0.23", - "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.0.23.tgz", - "integrity": "sha512-n6N/fLRb2mylontUuxU4C57SFjULCqCXUUyeNbITRU2jZ8SH9brpZn3woEDwQ0DYZ/GXLHYcDqn2esZkAZTnKQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.1.2.tgz", + "integrity": "sha512-poV4+UHC6Zeq6KRqctL+c5p0ioQqjGm0t8nXVXVaHLzerCZQqkDt2LiVbb1nQmKfHrnSXKhxAig9zZaAvQ2vdA==", "dependencies": { "bson": "^5.0.0", "minimongo": "^6.19.0", - "mongodb": "^5.0.0" + "mongodb": "^5.0.0", + "node-cache": "^5.1.2" } }, "node_modules/mongodb": { @@ -8097,6 +8106,17 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", diff --git a/package.json b/package.json index 82f9ae39..fc52e30d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.2.1", + "version": "1.2.2", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", @@ -35,7 +35,7 @@ "jose": "^5.9.6", "levenary": "^1.1.1", "minimongo": "^6.19.0", - "mongo-anywhere": "^1.0.21", + "mongo-anywhere": "^1.1.2", "mongodb": "^5.0.0", "next": "^15.1.6", "next-auth": "^4.24.11", diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx index fa932608..2020e256 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx @@ -330,7 +330,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return { props: { - pitReport: SerializeDatabaseObject(pitreport), + pitReport: makeObjSerializeable(SerializeDatabaseObject(pitreport)), layout: makeObjSerializeable(game.pitReportLayout), teamNumber: resolved.team?.number, compName: resolved.competition?.name, diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx index 78ab9dc0..938e0726 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/scouters.tsx @@ -24,7 +24,7 @@ export default function Scouters(props: { const team = props.team; const comp = props.competition; - const { session, status } = useCurrentSession(); + const { session } = useCurrentSession(); const isManager = session?.user?._id ? team?.owners.includes(session.user?._id) : false; @@ -141,9 +141,9 @@ export default function Scouters(props: { setReports((reports) => { if (!reports) return reports; - const { _id, ...updated } = reports[comment.dbId]; + const { _id, ...old } = reports[comment.dbId]; promise = api.updateReport( - { data: { ...updated.data, comments: "" } }, + { data: { ...old.data, comments: "" } }, comment.dbId, ); @@ -154,7 +154,12 @@ export default function Scouters(props: { } function removePitComment(comment: Comment) { - return api.updatePitreport(comment.dbId, { comments: "" }); + const { _id, ...old } = data.pitReports.find( + (r) => r._id === comment.dbId, + )!; + return api.updatePitreport(comment.dbId, { + data: { ...old.data, comments: "" }, + }); } function removeSubjectiveComment(comment: Comment) { diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index 047de81c..8562de71 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -193,9 +193,13 @@ export const getServerSideProps: GetServerSideProps = async (context) => { submitted: true, }); - const pitReports = await db.findObjects(CollectionId.PitReports, { - _id: { $in: resolved.competition?.pitReports }, - }); + const pitReports = !resolved.competition + ? [] + : await db.findObjects(CollectionId.PitReports, { + _id: { + $in: resolved.competition.pitReports.map((id) => new ObjectId(id)), + }, + }); const subjectiveReports = await db.findObjects( CollectionId.SubjectiveReports, @@ -208,6 +212,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { CollectionId.Picklists, new ObjectId(resolved.competition?.picklist), ); + console.log("Picklists", picklists); return { props: { diff --git a/pages/dev/cache.tsx b/pages/dev/cache.tsx new file mode 100644 index 00000000..a2700c50 --- /dev/null +++ b/pages/dev/cache.tsx @@ -0,0 +1,102 @@ +import Container from "@/components/Container"; +import ClientApi from "@/lib/api/ClientApi"; +import CollectionId from "@/lib/client/CollectionId"; +import useInterval from "@/lib/client/useInterval"; +import { getCacheKey } from "mongo-anywhere/CachedDbInterface"; +import { useCallback, useState } from "react"; + +const api = new ClientApi(); + +export default function Cache() { + const [cacheStats, setCacheStats] = useState(); + const [cachedVals, setCachedVals] = useState< + { key: string; val: object | undefined }[] + >([]); + + const fetchCacheStats = useCallback(async () => { + const stats = await api.getCacheStats(); + setCacheStats(stats); + }, []); + useInterval(fetchCacheStats, 1000); + + async function fetchCachedVal() { + const method = (document.getElementById("method") as HTMLSelectElement) + .value as "findOne" | "findMultiple" | "count"; + const collection = ( + document.getElementById("collection") as HTMLSelectElement + ).value as CollectionId; + const query = (document.getElementById("query") as HTMLInputElement).value; + + const key = getCacheKey(method, collection, query); + + const val = await api.getCachedValue(key); + setCachedVals((old) => [{ key, val }, ...old]); // Keep the most recent at the top + } + + return ( + +

Cache

+ {cacheStats ? ( + Object.entries(cacheStats).map(([key, value]) => ( +
+ {key}: {(+value).toLocaleString()} +
+ )) + ) : ( +
No cache.
+ )} +
+
+ + + + +
+ {cachedVals.map(({ key, val }) => ( +
+

{key}

+

{val ? JSON.stringify(val) : val}

+
+ ))} +
+
+ ); +}