diff --git a/lib/CompetitionHandling.ts b/lib/CompetitionHandling.ts index 5009c3f1..b302212a 100644 --- a/lib/CompetitionHandling.ts +++ b/lib/CompetitionHandling.ts @@ -54,15 +54,12 @@ export async function AssignScoutersToCompetitionMatches( teamId: string, competitionId: string, ) { - const comp = await db.findObjectById( + const comp = await db.findObjectById( CollectionId.Competitions, new ObjectId(competitionId), ); - const team = await db.findObject( - CollectionId.Teams, - new ObjectId(teamId), - ); + const team = await db.findObject(CollectionId.Teams, new ObjectId(teamId)); if (!comp || !team) { throw new Error("Competition or team not found"); @@ -99,7 +96,7 @@ export async function generateReportsForMatch( schedule?: ScheduleMatch, ) { if (typeof match === "string") { - const foundMatch = await db.findObjectById( + const foundMatch = await db.findObjectById( CollectionId.Matches, new ObjectId(match), ); @@ -114,7 +111,7 @@ export async function generateReportsForMatch( match.subjectiveScouter = schedule?.subjectiveScouter; const existingReportPromises = match.reports.map((r) => - db.findObjectById(CollectionId.Reports, new ObjectId(r)), + db.findObjectById(CollectionId.Reports, new ObjectId(r)), ); const existingReports = await Promise.all(existingReportPromises); diff --git a/lib/MongoDB.ts b/lib/MongoDB.ts index 054b1021..eada8b7f 100644 --- a/lib/MongoDB.ts +++ b/lib/MongoDB.ts @@ -4,6 +4,7 @@ import CollectionId, { CollectionIdToType } from "./client/CollectionId"; import DbInterface, { WithStringOrObjectIdId, } from "./client/dbinterfaces/DbInterface"; +import { default as BaseMongoDbInterface } from "mongo-anywhere/MongoDbInterface"; if (!process.env.MONGODB_URI) { // Necessary to allow connections from files running outside of Next @@ -40,97 +41,57 @@ export async function getDatabase(): Promise { return global.interface; } -export class MongoDBInterface implements DbInterface { - promise: Promise | undefined; - client: MongoClient | undefined; - db: Db | undefined; - - constructor(promise: Promise) { - this.promise = promise; - } - +export class MongoDBInterface + extends BaseMongoDbInterface> + implements DbInterface +{ async init() { - this.client = await this.promise; - this.db = this.client?.db(process.env.DB); - //@ts-ignore - - const CollectionId = (await this.db - ?.listCollections() - .toArray()) as CollectionId; - if (CollectionId?.length === 0) { - try { - Object.values(CollectionId).forEach( - async (collectionName) => - await this.db?.createCollection(collectionName), - ); - } catch (e) { - console.log( - "Failed to create CollectionId... (probably exist already)", - ); - } - } + super.init(Object.values(CollectionId)); } async addObject< TId extends CollectionId, TObj extends CollectionIdToType, >(collection: TId, object: WithStringOrObjectIdId): Promise { - if (object._id && typeof object._id === "string") - object._id = new ObjectId(object._id); - - const ack = await this?.db - ?.collection(collection) - .insertOne(object as Document & { _id?: ObjectId }); - object._id = ack?.insertedId; - return object as TObj; + return super.addObject(collection, object); } async deleteObjectById(collection: CollectionId, id: ObjectId) { - var query = { _id: id }; - await this?.db?.collection(collection).deleteOne(query); + await this?.db?.collection(collection).deleteOne({ _id: id }); } updateObjectById< TId extends CollectionId, TObj extends CollectionIdToType, >(collection: TId, id: ObjectId, newValues: Partial): Promise { - var query = { _id: id }; - var updated = { $set: newValues }; - this?.db?.collection(collection).updateOne(query, updated); - - return Promise.resolve(); + return super.updateObjectById(collection, id, newValues); } - async findObjectById( - collection: CollectionId, - id: ObjectId, - ): Promise { - return (await this?.db - ?.collection(collection) - .findOne({ _id: id })) as Type; + async findObjectById< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, id: ObjectId): Promise { + return super.findObjectById(collection, id); } - async findObject( - collection: CollectionId, - query: object, - ): Promise { - return (await this?.db?.collection(collection).findOne(query)) as Type; + async findObject< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, query: object): Promise { + return super.findObject(collection, query); } - async findObjects( - collection: CollectionId, - query: object, - ): Promise { - return (await this?.db - ?.collection(collection) - .find(query) - .toArray()) as Type[]; + async findObjects< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, query: object): Promise { + return super.findObjects(collection, query); } async countObjects( collection: CollectionId, query: object, ): Promise { - return await this?.db?.collection(collection).countDocuments(query); + return super.countObjects(collection, query); } } diff --git a/lib/UrlResolver.ts b/lib/UrlResolver.ts index ab254422..6670398b 100644 --- a/lib/UrlResolver.ts +++ b/lib/UrlResolver.ts @@ -81,17 +81,17 @@ export default async function UrlResolver( try { const promises = [ - db.findObject(CollectionId.Teams, { slug: teamSlug }), + db.findObject(CollectionId.Teams, { slug: teamSlug }), seasonSlug - ? db.findObject(CollectionId.Seasons, { slug: seasonSlug }) + ? db.findObject(CollectionId.Seasons, { slug: seasonSlug }) : null, competitionSlug - ? db.findObject(CollectionId.Competitions, { + ? db.findObject(CollectionId.Competitions, { slug: competitionSlug, }) : null, reportId - ? db.findObject(CollectionId.Reports, { + ? db.findObject(CollectionId.Reports, { _id: new ObjectId(reportId), }) : null, diff --git a/lib/api/AccessLevels.ts b/lib/api/AccessLevels.ts index 9f95c0e3..df6e5039 100644 --- a/lib/api/AccessLevels.ts +++ b/lib/api/AccessLevels.ts @@ -68,7 +68,7 @@ namespace AccessLevels { const team = await ( await db - ).findObjectById(CollectionId.Teams, new ObjectId(teamId)); + ).findObjectById(CollectionId.Teams, new ObjectId(teamId)); if (!team) { return { authorized: false, authData: undefined }; } @@ -92,7 +92,7 @@ namespace AccessLevels { const team = await ( await db - ).findObjectById(CollectionId.Teams, new ObjectId(teamId)); + ).findObjectById(CollectionId.Teams, new ObjectId(teamId)); if (!team) { return { authorized: false, authData: undefined }; } @@ -116,10 +116,7 @@ namespace AccessLevels { const comp = await ( await db - ).findObjectById( - CollectionId.Competitions, - new ObjectId(compId), - ); + ).findObjectById(CollectionId.Competitions, new ObjectId(compId)); if (!comp) { return { authorized: false, authData: undefined }; } @@ -148,7 +145,7 @@ namespace AccessLevels { const season = await ( await db - ).findObjectById(CollectionId.Seasons, new ObjectId(seasonId)); + ).findObjectById(CollectionId.Seasons, new ObjectId(seasonId)); if (!season) { return { authorized: false, authData: undefined }; } @@ -177,7 +174,7 @@ namespace AccessLevels { const match = await ( await db - ).findObjectById(CollectionId.Matches, new ObjectId(matchId)); + ).findObjectById(CollectionId.Matches, new ObjectId(matchId)); if (!match) { return { authorized: false, authData: undefined }; } @@ -211,7 +208,7 @@ namespace AccessLevels { const report = await ( await db - ).findObjectById(CollectionId.Reports, new ObjectId(reportId)); + ).findObjectById(CollectionId.Reports, new ObjectId(reportId)); if (!report) { return { authorized: false, authData: undefined }; } @@ -240,10 +237,7 @@ namespace AccessLevels { const comp = await ( await db - ).findObjectById( - CollectionId.Competitions, - new ObjectId(compId), - ); + ).findObjectById(CollectionId.Competitions, new ObjectId(compId)); if (!comp) { return { authorized: false, authData: undefined }; } @@ -272,7 +266,7 @@ namespace AccessLevels { const match = await ( await db - ).findObjectById(CollectionId.Matches, new ObjectId(matchId)); + ).findObjectById(CollectionId.Matches, new ObjectId(matchId)); if (!match) { return { authorized: false, authData: undefined }; } @@ -306,10 +300,7 @@ namespace AccessLevels { const pitReport = await ( await db - ).findObjectById( - CollectionId.PitReports, - new ObjectId(pitReportId), - ); + ).findObjectById(CollectionId.PitReports, new ObjectId(pitReportId)); if (!pitReport) { return { authorized: false, authData: undefined }; } @@ -343,7 +334,7 @@ namespace AccessLevels { const report = await ( await db - ).findObjectById(CollectionId.Reports, new ObjectId(reportId)); + ).findObjectById(CollectionId.Reports, new ObjectId(reportId)); if (!report) { return { authorized: false, authData: undefined }; } @@ -372,10 +363,7 @@ namespace AccessLevels { const report = await ( await db - ).findObjectById( - CollectionId.SubjectiveReports, - new ObjectId(reportId), - ); + ).findObjectById(CollectionId.SubjectiveReports, new ObjectId(reportId)); if (!report) { return { authorized: false, authData: undefined }; } @@ -404,10 +392,7 @@ namespace AccessLevels { const picklist = await ( await db - ).findObjectById( - CollectionId.Picklists, - new ObjectId(picklistId), - ); + ).findObjectById(CollectionId.Picklists, new ObjectId(picklistId)); if (!picklist) { return { authorized: false, authData: undefined }; } diff --git a/lib/api/ApiUtils.ts b/lib/api/ApiUtils.ts index c2b49a26..4f47703d 100644 --- a/lib/api/ApiUtils.ts +++ b/lib/api/ApiUtils.ts @@ -35,19 +35,19 @@ export function ownsTeam(team?: Team | null, user?: User) { } export function getCompFromReport(db: DbInterface, report: Report) { - return db.findObject(CollectionId.Competitions, { + return db.findObject(CollectionId.Competitions, { matches: report.match?.toString(), }); } export function getCompFromMatch(db: DbInterface, match: Match) { - return db.findObject(CollectionId.Competitions, { + return db.findObject(CollectionId.Competitions, { matches: match._id?.toString(), }); } export function getCompFromPitReport(db: DbInterface, report: Pitreport) { - return db.findObject(CollectionId.Competitions, { + return db.findObject(CollectionId.Competitions, { pitReports: report._id?.toString(), }); } @@ -57,7 +57,7 @@ export function getCompFromSubjectiveReport( report: SubjectiveReport, ) { return db - .findObject(CollectionId.Matches, { + .findObject(CollectionId.Matches, { subjectiveReports: report._id?.toString(), }) .then((match) => { @@ -71,19 +71,19 @@ export function getCompFromPicklist( db: DbInterface, picklist: CompPicklistGroup, ) { - return db.findObject(CollectionId.Competitions, { + return db.findObject(CollectionId.Competitions, { picklist: picklist._id?.toString(), }); } export function getSeasonFromComp(db: DbInterface, comp: Competition) { - return db.findObject(CollectionId.Seasons, { + return db.findObject(CollectionId.Seasons, { competitions: comp?._id?.toString(), // Specifying one value is effectively includes for arrays }); } export function getTeamFromSeason(db: DbInterface, season: Season) { - return db.findObject(CollectionId.Teams, { + return db.findObject(CollectionId.Teams, { seasons: season._id?.toString(), }); } diff --git a/lib/api/ClientApi.ts b/lib/api/ClientApi.ts index 50e6b158..46f61c7c 100644 --- a/lib/api/ClientApi.ts +++ b/lib/api/ClientApi.ts @@ -91,7 +91,7 @@ export default class ClientApi extends NextApiTemplate { handler: async (req, res, { db, userPromise }, authData, [teamId]) => { let team = await ( await db - ).findObjectById(CollectionId.Teams, new ObjectId(teamId)); + ).findObjectById(CollectionId.Teams, new ObjectId(teamId)); if (!team) { return res.error(404, "Team not found"); @@ -130,12 +130,12 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const teamPromise = db.findObjectById( + const teamPromise = db.findObjectById( CollectionId.Teams, new ObjectId(teamId.toString()), ); - const joineePromise = db.findObjectById( + const joineePromise = db.findObjectById( CollectionId.Users, new ObjectId(userId.toString()), ); @@ -239,7 +239,7 @@ export default class ClientApi extends NextApiTemplate { const db = await dbPromise; // Find if team already exists - const existingTeam = await db.findObject(CollectionId.Teams, { + const existingTeam = await db.findObject(CollectionId.Teams, { number, ...(league === League.FRC ? { $or: [{ league: League.FRC }, { league: undefined }] } @@ -593,7 +593,7 @@ export default class ClientApi extends NextApiTemplate { const usedComps = usePublicData && comp.tbaId !== NotLinkedToTba - ? await db.findObjects(CollectionId.Competitions, { + ? await db.findObjects(CollectionId.Competitions, { publicData: true, tbaId: comp.tbaId, gameId: comp.gameId, @@ -603,7 +603,7 @@ export default class ClientApi extends NextApiTemplate { if (usePublicData && !comp.publicData) usedComps.push(comp); const reports = ( - await db.findObjects(CollectionId.Reports, { + await db.findObjects(CollectionId.Reports, { match: { $in: usedComps.flatMap((m) => m.matches) }, submitted: submitted ? true : { $exists: true }, }) @@ -635,7 +635,7 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const matches = await db.findObjects(CollectionId.Matches, { + const matches = await db.findObjects(CollectionId.Matches, { _id: { $in: comp.matches.map((matchId) => new ObjectId(matchId)) }, }); return res.status(200).send(matches); @@ -659,7 +659,7 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const reports = await db.findObjects(CollectionId.Reports, { + const reports = await db.findObjects(CollectionId.Reports, { _id: { $in: match.reports.map((reportId) => new ObjectId(reportId)) }, }); return res.status(200).send(reports); @@ -766,14 +766,14 @@ export default class ClientApi extends NextApiTemplate { [teamId, targetUserId], ) => { const db = await dbPromise; - const targetUserPromise = db.findObjectById( + const targetUserPromise = db.findObjectById( CollectionId.Users, new ObjectId(targetUserId), ); if (!team.slackWebhook) throw new SlackNotLinkedError(res); - const webhookHolder = await db.findObjectById( + const webhookHolder = await db.findObjectById( CollectionId.Webhooks, new ObjectId(team.slackWebhook), ); @@ -996,10 +996,10 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const matches = await db.findObjects(CollectionId.Matches, { + const matches = await db.findObjects(CollectionId.Matches, { _id: { $in: comp.matches.map((matchId) => new ObjectId(matchId)) }, }); - const allReports = await db.findObjects(CollectionId.Reports, { + const allReports = await db.findObjects(CollectionId.Reports, { match: { $in: matches.map((match) => match?._id?.toString()) }, }); const reports = allReports.filter((report) => report.submitted); @@ -1091,12 +1091,9 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const pitReports = await db.findObjects( - CollectionId.PitReports, - { - _id: { $in: comp.pitReports.map((id) => new ObjectId(id)) }, - }, - ); + const pitReports = await db.findObjects(CollectionId.PitReports, { + _id: { $in: comp.pitReports.map((id) => new ObjectId(id)) }, + }); return res.status(200).send(pitReports); }, @@ -1144,7 +1141,7 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const reports = await db.findObjects(CollectionId.Reports, { + const reports = await db.findObjects(CollectionId.Reports, { match: { $in: comp.matches }, }); @@ -1186,7 +1183,7 @@ export default class ClientApi extends NextApiTemplate { for (const scouterId of team?.scouters) { promises.push( db - .findObjectById(CollectionId.Users, new ObjectId(scouterId)) + .findObjectById(CollectionId.Users, new ObjectId(scouterId)) .then((scouter) => scouter && scouters.push(scouter)), ); } @@ -1194,14 +1191,14 @@ export default class ClientApi extends NextApiTemplate { for (const matchId of comp.matches) { promises.push( db - .findObjectById(CollectionId.Matches, new ObjectId(matchId)) + .findObjectById(CollectionId.Matches, new ObjectId(matchId)) .then((match) => match && matches.push(match)), ); } promises.push( db - .findObjects(CollectionId.Reports, { + .findObjects(CollectionId.Reports, { match: { $in: comp.matches }, }) .then((r) => quantitativeReports.push(...r)), @@ -1209,7 +1206,7 @@ export default class ClientApi extends NextApiTemplate { promises.push( db - .findObjects(CollectionId.PitReports, { + .findObjects(CollectionId.PitReports, { _id: { $in: comp.pitReports.map((id) => new ObjectId(id)) }, submitted: true, }) @@ -1218,7 +1215,7 @@ export default class ClientApi extends NextApiTemplate { promises.push( db - .findObjects(CollectionId.SubjectiveReports, { + .findObjects(CollectionId.SubjectiveReports, { match: { $in: comp.matches }, }) .then((r) => subjectiveReports.push(...r)), @@ -1247,7 +1244,7 @@ export default class ClientApi extends NextApiTemplate { handler: async (req, res, { db: dbPromise }, { comp }, [compId]) => { const db = await dbPromise; - const picklist = await db.findObjectById( + const picklist = await db.findObjectById( CollectionId.Picklists, new ObjectId(comp.picklist), ); @@ -1434,12 +1431,9 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const reports = await db.findObjects( - CollectionId.SubjectiveReports, - { - match: { $in: comp.matches }, - }, - ); + const reports = await db.findObjects(CollectionId.SubjectiveReports, { + match: { $in: comp.matches }, + }); return res.status(200).send(reports); }, @@ -1657,12 +1651,9 @@ export default class ClientApi extends NextApiTemplate { } const matchIds = matches.map((match) => match._id?.toString()); - const reports = await db.findObjects( - CollectionId.SubjectiveReports, - { - match: { $in: matchIds }, - }, - ); + const reports = await db.findObjects(CollectionId.SubjectiveReports, { + match: { $in: matchIds }, + }); return res.status(200).send(reports); }, @@ -1685,7 +1676,7 @@ export default class ClientApi extends NextApiTemplate { ) => { const db = await dbPromise; - const removedUserPromise = db.findObjectById( + const removedUserPromise = db.findObjectById( CollectionId.Users, new ObjectId(userId), ); @@ -1736,7 +1727,7 @@ export default class ClientApi extends NextApiTemplate { isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { const db = await dbPromise; - const user = await db.findObjectById( + const user = await db.findObjectById( CollectionId.Users, new ObjectId(id), ); @@ -1753,7 +1744,7 @@ export default class ClientApi extends NextApiTemplate { isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [ids]) => { const db = await dbPromise; - const users = await db.findObjects(CollectionId.Users, { + const users = await db.findObjects(CollectionId.Users, { _id: { $in: ids.map((id) => new ObjectId(id)) }, }); return res.status(200).send(users); @@ -1769,7 +1760,7 @@ export default class ClientApi extends NextApiTemplate { isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { const db = await dbPromise; - const team = await db.findObjectById( + const team = await db.findObjectById( CollectionId.Teams, new ObjectId(id), ); @@ -1801,7 +1792,7 @@ export default class ClientApi extends NextApiTemplate { } : { number: number, league: league }; - const team = await db.findObject(CollectionId.Teams, query); + const team = await db.findObject(CollectionId.Teams, query); return res.status(200).send(team); }, @@ -1816,7 +1807,7 @@ export default class ClientApi extends NextApiTemplate { isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { const db = await dbPromise; - const season = await db.findObjectById( + const season = await db.findObjectById( CollectionId.Seasons, new ObjectId(id), ); @@ -1833,7 +1824,7 @@ export default class ClientApi extends NextApiTemplate { isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { const db = await dbPromise; - const competition = await db.findObjectById( + const competition = await db.findObjectById( CollectionId.Competitions, new ObjectId(id), ); @@ -1850,7 +1841,7 @@ export default class ClientApi extends NextApiTemplate { isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { const db = await dbPromise; - const match = await db.findObjectById( + const match = await db.findObjectById( CollectionId.Matches, new ObjectId(id), ); @@ -1867,7 +1858,7 @@ export default class ClientApi extends NextApiTemplate { isAuthorized: AccessLevels.AlwaysAuthorized, handler: async (req, res, { db: dbPromise }, authData, [id]) => { const db = await dbPromise; - const report = await db.findObjectById( + const report = await db.findObjectById( CollectionId.Reports, new ObjectId(id), ); @@ -2125,7 +2116,7 @@ export default class ClientApi extends NextApiTemplate { handler: async (req, res, { db: dbPromise }, authData, args) => { const db = await dbPromise; - const users = await db.findObjects(CollectionId.Users, { + const users = await db.findObjects(CollectionId.Users, { xp: { $gt: 0 }, email: { $ne: "totallyrealemail@gmail.com" }, }); @@ -2135,7 +2126,7 @@ export default class ClientApi extends NextApiTemplate { users.map((user) => user.teams.map((id) => new ObjectId(id))).flat(), ); - const teams = await db.findObjects(CollectionId.Teams, { + const teams = await db.findObjects(CollectionId.Teams, { _id: { $in: users .map((user) => user.teams.map((id) => new ObjectId(id))) diff --git a/lib/client/dbinterfaces/DbInterface.ts b/lib/client/dbinterfaces/DbInterface.ts index b0d6147c..ade8fb78 100644 --- a/lib/client/dbinterfaces/DbInterface.ts +++ b/lib/client/dbinterfaces/DbInterface.ts @@ -1,17 +1,22 @@ import { ObjectId, Document } from "bson"; import CollectionId, { CollectionIdToType } from "../CollectionId"; +import { default as BaseDbInterface } from "mongo-anywhere/DbInterface"; export type WithStringOrObjectIdId = Omit & { _id?: ObjectId | string; }; -export default interface DbInterface { +export default interface DbInterface + extends BaseDbInterface> { init(): Promise; + addObject>( collection: TId, object: WithStringOrObjectIdId, ): Promise; + deleteObjectById(collection: CollectionId, id: ObjectId): Promise; + updateObjectById< TId extends CollectionId, TObj extends CollectionIdToType, @@ -20,21 +25,28 @@ export default interface DbInterface { id: ObjectId, newValues: Partial, ): Promise; - findObjectById( - collection: CollectionId, + + findObjectById< + TId extends CollectionId, + TObj extends CollectionIdToType, + >( + collection: TId, id: ObjectId, - ): Promise; - findObject( - collection: CollectionId, + ): Promise; + + findObject>( + collection: TId, query: object, - ): Promise; + ): Promise; + /** * Type should not be an array! This function returns an array of Type (Type[]). */ - findObjects( - collection: CollectionId, + findObjects>( + collection: TId, query: object, - ): Promise; + ): Promise; + countObjects( collection: CollectionId, query: object, diff --git a/lib/client/dbinterfaces/InMemoryDbInterface.ts b/lib/client/dbinterfaces/InMemoryDbInterface.ts index b5e55c71..69da8d84 100644 --- a/lib/client/dbinterfaces/InMemoryDbInterface.ts +++ b/lib/client/dbinterfaces/InMemoryDbInterface.ts @@ -1,204 +1,57 @@ -import { Document, EJSON, ObjectId } from "bson"; +import { ObjectId } from "bson"; import CollectionId, { CollectionIdToType } from "@/lib/client/CollectionId"; import DbInterface, { WithStringOrObjectIdId, } from "@/lib/client/dbinterfaces/DbInterface"; -import { MemoryDb } from "minimongo"; - -/** - * Remove undefined values or EJSON will convert them to null - */ -function removeUndefinedValues(obj: { [key: string]: any }): { - [key: string]: any; -} { - const newObj = { ...obj }; - - for (const key in newObj) { - if (newObj[key] === undefined) { - delete newObj[key]; - } else if (Array.isArray(newObj[key])) { - newObj[key] = newObj[key].map((item: any) => { - if (typeof item === "object") { - return removeUndefinedValues(item); - } - return item; - }); - } else if ( - newObj[key] !== undefined && - !(newObj[key] instanceof ObjectId) && - newObj[key] !== null && - typeof newObj[key] === "object" - ) { - newObj[key] = removeUndefinedValues(newObj[key]); - } - } - - return newObj; -} - -function replaceOidOperator( - obj: { [key: string]: any }, - idsToString: boolean, -): { [key: string]: any } { - const newObj = { ...obj }; - - for (const key in newObj) { - if (idsToString && key === "_id") { - newObj["_id"] = newObj._id.$oid; - } else if (!idsToString && key === "_id") { - newObj._id = new ObjectId(newObj._id.toString()); - } else if (Array.isArray(newObj[key])) { - newObj[key] = newObj[key].map((item: any) => { - if (typeof item === "object") { - return replaceOidOperator(item, idsToString); - } - return item; - }); - } else if ( - newObj[key] !== undefined && - !(newObj[key] instanceof ObjectId) && - newObj[key] !== null && - typeof newObj[key] === "object" - ) { - newObj[key] = replaceOidOperator(newObj[key], idsToString); - } - } - - return newObj; -} - -/** - * @param removeUndefined pass false if you're serializing a query where undefined values are important - * (this is most of the time that you're serializing a query) - */ -function serialize(obj: any, removeUndefined: boolean = true): any { - return replaceOidOperator( - EJSON.serialize(removeUndefined ? removeUndefinedValues(obj) : obj), - true, - ); -} - -function deserialize(obj: any): any { - return replaceOidOperator(EJSON.deserialize(obj), false); -} - -/** - * @tested_by tests/lib/client/dbinterfaces/InMemoryDbInterface.test.ts - */ -export default class InMemoryDbInterface implements DbInterface { - backingDb: MemoryDb; - - constructor() { - this.backingDb = new MemoryDb(); - } - +import { default as BaseInMemoryDbInterface } from "mongo-anywhere/InMemoryDbInterface"; + +export default class InMemoryDbInterface + extends BaseInMemoryDbInterface< + CollectionId, + CollectionIdToType + > + implements DbInterface +{ init(): Promise { - const promise = new Promise((resolve) => { - let collectionsCreated = 0; - - function onCollectionCreated() { - collectionsCreated++; - if (collectionsCreated === Object.keys(CollectionId).length) { - resolve(undefined); - } - } - - // Have to use Object.values here or else we'll get the keys as strings - // Be sure to use of, not in! - for (const collectionId of Object.values(CollectionId)) { - this.backingDb.addCollection( - collectionId, - onCollectionCreated, - onCollectionCreated, - ); - } - }); - - return promise as Promise; + return super.init(Object.values(CollectionId)); } - addObject>( collection: TId, object: WithStringOrObjectIdId, ): Promise { - if (!object._id) object._id = new ObjectId(); - - return this.backingDb.collections[collection] - .upsert(serialize(object)) - .then(deserialize); + return super.addObject(collection, object); } - deleteObjectById(collection: CollectionId, id: ObjectId): Promise { - return this.backingDb.collections[collection].remove( - serialize({ _id: id }), - ); + return super.deleteObjectById(collection, id); } - updateObjectById< TId extends CollectionId, TObj extends CollectionIdToType, >(collection: TId, id: ObjectId, newValues: Partial): Promise { - return this.backingDb.collections[collection] - .findOne(serialize({ _id: id })) - .then((existingDoc) => { - if (!existingDoc) { - throw new Error( - `Document with id ${id} not found in collection ${collection}`, - ); - } - - const returnValue = this.backingDb.collections[collection].upsert( - serialize({ ...existingDoc, ...newValues, _id: id }), - ); - return deserialize(returnValue); - }); + return super.updateObjectById(collection, id, newValues); } - - findObjectById( - collection: CollectionId, - id: ObjectId, - ): Promise { - return this.findObject(collection, { _id: id }); + findObjectById< + TId extends CollectionId, + TObj extends CollectionIdToType, + >(collection: TId, id: ObjectId): Promise { + return super.findObjectById(collection, id); } - - findObject( - collection: CollectionId, + findObject>( + collection: TId, query: object, - ): Promise { - return this.backingDb.collections[collection] - .findOne(serialize(query, false)) - .then(deserialize) - .then((obj: Type) => { - if (Object.keys(obj).length === 0) { - return undefined; - } - - return obj; - }); + ): Promise { + return super.findObject(collection, query); } - - findObjects( - collection: CollectionId, + findObjects>( + collection: TId, query: object, - ): Promise { - return this.backingDb.collections[collection] - .find(serialize(query, false)) - .fetch() - .then((res: { [index: string]: object }) => { - return Object.values(res).map(deserialize); - }); + ): Promise { + return super.findObjects(collection, query); } - countObjects( collection: CollectionId, query: object, ): Promise { - return ( - this.backingDb.collections[collection] - .find(serialize(query, false)) - .fetch() as Promise - ).then((objects) => { - return Object.keys(objects as any).length; - }); + return super.countObjects(collection, query); } } diff --git a/lib/dev/FakeData.ts b/lib/dev/FakeData.ts index 73d47902..d84547df 100644 --- a/lib/dev/FakeData.ts +++ b/lib/dev/FakeData.ts @@ -65,7 +65,7 @@ export async function fillTeamWithFakeUsers( users.push((await fakeUser(teamId, db))._id?.toString()); } - const team = await db.findObjectById( + const team = await db.findObjectById( CollectionId.Teams, new ObjectId(teamId?.toString()), ); diff --git a/package-lock.json b/package-lock.json index f174a778..95d48ead 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sj3", - "version": "1.1.5", + "version": "1.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sj3", - "version": "1.1.5", + "version": "1.1.7", "license": "CC BY-NC-SA 4.0", "dependencies": { "dependencies": "^0.0.1", @@ -25,6 +25,7 @@ "jose": "^5.9.6", "levenary": "^1.1.1", "minimongo": "^6.19.0", + "mongo-anywhere": "^1.0.21", "mongodb": "^5.0.0", "next": "^15.1.4", "next-auth": "^4.24.10", @@ -9499,6 +9500,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/mongo-anywhere": { + "version": "1.0.21", + "resolved": "https://registry.npmjs.org/mongo-anywhere/-/mongo-anywhere-1.0.21.tgz", + "integrity": "sha512-nG+uUuVyrgetvkcVd/JgHSvfQgzrAUjIFflfaDirZeRGL/gdluGo49rJxPc7GjjcBb4+NWLIPFXgGwrmFDH4xw==", + "dependencies": { + "bson": "^5.0.0", + "minimongo": "^6.19.0", + "mongodb": "^5.0.0" + } + }, "node_modules/mongodb": { "version": "5.9.2", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", diff --git a/package.json b/package.json index c60e9052..bab397b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sj3", - "version": "1.1.6", + "version": "1.1.7", "private": true, "repository": "https://github.com/Decatur-Robotics/Gearbox", "license": "CC BY-NC-SA 4.0", @@ -34,6 +34,7 @@ "jose": "^5.9.6", "levenary": "^1.1.1", "minimongo": "^6.19.0", + "mongo-anywhere": "^1.0.21", "mongodb": "^5.0.0", "next": "^15.1.4", "next-auth": "^4.24.10", diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx index 9b8f764b..0f58286b 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/pit/[...pitreportId].tsx @@ -308,10 +308,7 @@ export default function PitReportForm(props: { async function getPitreport(id: string) { const db = await getDatabase(); - return await db.findObjectById( - CollectionId.PitReports, - new ObjectId(id), - ); + return await db.findObjectById(CollectionId.PitReports, new ObjectId(id)); } export const getServerSideProps: GetServerSideProps = async (context) => { diff --git a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx index 276ca4fb..047de81c 100644 --- a/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx +++ b/pages/[teamSlug]/[seasonSlug]/[competitonSlug]/stats.tsx @@ -188,23 +188,23 @@ export const getServerSideProps: GetServerSideProps = async (context) => { return resolved; } - const reports = await db.findObjects(CollectionId.Reports, { + const reports = await db.findObjects(CollectionId.Reports, { match: { $in: resolved.competition?.matches }, submitted: true, }); - const pitReports = await db.findObjects(CollectionId.PitReports, { + const pitReports = await db.findObjects(CollectionId.PitReports, { _id: { $in: resolved.competition?.pitReports }, }); - const subjectiveReports = await db.findObjects( + const subjectiveReports = await db.findObjects( CollectionId.SubjectiveReports, { match: { $in: resolved.competition?.matches }, }, ); - const picklists = await db.findObjectById( + const picklists = await db.findObjectById( CollectionId.Picklists, new ObjectId(resolved.competition?.picklist), ); diff --git a/pages/[teamSlug]/index.tsx b/pages/[teamSlug]/index.tsx index b08aa2f3..91b79295 100644 --- a/pages/[teamSlug]/index.tsx +++ b/pages/[teamSlug]/index.tsx @@ -596,11 +596,11 @@ export const getServerSideProps: GetServerSideProps = async (context) => { (seasonId) => new ObjectId(seasonId), ); const userIds = resolved.team?.users.map((userId) => new ObjectId(userId)); - const seasons = await db.findObjects(CollectionId.Seasons, { + const seasons = await db.findObjects(CollectionId.Seasons, { _id: { $in: seasonIds }, }); - var users = await db.findObjects(CollectionId.Users, { + var users = await db.findObjects(CollectionId.Users, { _id: { $in: userIds }, }); @@ -614,7 +614,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => { const currentSeason = seasons[seasons.length - 1]; var comp = undefined; if (currentSeason) { - comp = await db.findObjectById( + comp = await db.findObjectById( CollectionId.Competitions, new ObjectId( currentSeason.competitions[currentSeason.competitions.length - 1], diff --git a/scripts/loadUsersIntoResend.ts b/scripts/loadUsersIntoResend.ts index 0fb7a027..fdc34a3b 100644 --- a/scripts/loadUsersIntoResend.ts +++ b/scripts/loadUsersIntoResend.ts @@ -10,7 +10,7 @@ async function loadUsersIntoResend() { const db = await getDatabase(); console.log("Finding users..."); - const users = await db.findObjects(CollectionId.Users, {}); + const users = await db.findObjects(CollectionId.Users, {}); console.log(`Saving ${users.length} users to Resend...`); diff --git a/tests/lib/api/ClientApi.test.ts b/tests/lib/api/ClientApi.test.ts index c9187a76..aa8e5eec 100644 --- a/tests/lib/api/ClientApi.test.ts +++ b/tests/lib/api/ClientApi.test.ts @@ -47,7 +47,7 @@ describe(`${ClientApi.name}.${api.requestToJoinTeam.name}`, () => { expect(res.status).toHaveBeenCalledWith(200); expect(res.send).toHaveBeenCalledWith({ result: "Already on team" }); - const team = await db.findObjectById(CollectionId.Teams, teamId); + const team = await db.findObjectById(CollectionId.Teams, teamId); expect(team?.requests).toEqual([]); }); @@ -68,7 +68,7 @@ describe(`${ClientApi.name}.${api.requestToJoinTeam.name}`, () => { expect(res.status).toHaveBeenCalledWith(200); expect(res.send).toHaveBeenCalledWith({ result: "Success" }); - const team = await db.findObjectById(CollectionId.Teams, teamId); + const team = await db.findObjectById(CollectionId.Teams, teamId); expect(team?.requests).toEqual([user._id!.toString()]); }); @@ -147,10 +147,10 @@ describe(`${ClientApi.name}.${api.handleTeamJoinRequest.name}`, () => { ])), ); - const team = await db.findObjectById(CollectionId.Teams, teamId); + const team = await db.findObjectById(CollectionId.Teams, teamId); expect(team?.users).toEqual([user._id!.toString()]); - const foundUser = await db.findObjectById( + const foundUser = await db.findObjectById( CollectionId.Users, user._id! as any as ObjectId, ); @@ -177,7 +177,7 @@ describe(`${ClientApi.name}.${api.handleTeamJoinRequest.name}`, () => { ])), ); - const team = await db.findObjectById(CollectionId.Teams, teamId); + const team = await db.findObjectById(CollectionId.Teams, teamId); expect(team?.requests).toEqual([]); expect(team?.users).toEqual([]); }); @@ -225,7 +225,7 @@ describe(`${ClientApi.name}.${api.createTeam.name}`, () => { ...(await getTestApiParams(res, { db, user }, ["", "", 1, League.FRC])), ); - const team = await db.findObject(CollectionId.Teams, { + const team = await db.findObject(CollectionId.Teams, { number: 1, league: League.FRC, }); @@ -242,7 +242,7 @@ describe(`${ClientApi.name}.${api.createTeam.name}`, () => { ); const team = res.send.mock.calls[0][0] as Team; // The handler doesn't return a value, so we have to get the team from res - const foundUser = await db.findObjectById( + const foundUser = await db.findObjectById( CollectionId.Users, new ObjectId(user._id!), ); @@ -566,7 +566,7 @@ describe(`${ClientApi.name}.${api.updateUser.name}`, () => { ...(await getTestApiParams(res, { db, user }, [newValues])), ); - const updatedUser = await db.findObjectById( + const updatedUser = await db.findObjectById( CollectionId.Users, new ObjectId(user._id!), ); @@ -593,7 +593,7 @@ describe(`${ClientApi.name}.${api.updateTeam.name}`, () => { )), ); - const updatedTeam = await db.findObjectById( + const updatedTeam = await db.findObjectById( CollectionId.Teams, new ObjectId(team._id!), ); @@ -850,13 +850,13 @@ describe(`${ClientApi.name}.${api.setSlackWebhook.name}`, () => { webhookUrl, ); - const updatedTeam = await db.findObjectById( + const updatedTeam = await db.findObjectById( CollectionId.Teams, new ObjectId(team._id!), ); expect(updatedTeam?.slackWebhook).not.toBe(undefined); - const webhook = await db.findObjectById( + const webhook = await db.findObjectById( CollectionId.Webhooks, new ObjectId(updatedTeam!.slackWebhook!), ); @@ -892,13 +892,13 @@ describe(`${ClientApi.name}.${api.setSlackWebhook.name}`, () => { webhookUrl, ); - const updatedTeam = await db.findObjectById( + const updatedTeam = await db.findObjectById( CollectionId.Teams, new ObjectId(team._id!), ); expect(updatedTeam?.slackWebhook).toEqual(webhook._id!.toString()); - const updatedWebhook = await db.findObjectById( + const updatedWebhook = await db.findObjectById( CollectionId.Webhooks, new ObjectId(updatedTeam!.slackWebhook!), ); diff --git a/tests/lib/client/dbinterfaces/InMemoryDbInterface.test.ts b/tests/lib/client/dbinterfaces/InMemoryDbInterface.test.ts deleted file mode 100644 index b9d73566..00000000 --- a/tests/lib/client/dbinterfaces/InMemoryDbInterface.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import CollectionId from "@/lib/client/CollectionId"; -import InMemoryDbInterface from "@/lib/client/dbinterfaces/InMemoryDbInterface"; -import { getTestApiUtils } from "@/lib/testutils/TestUtils"; -import { User } from "@/lib/Types"; -import { ObjectId } from "bson"; - -async function getDb() { - const db = new InMemoryDbInterface(); - await db.init(); - return db; -} - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.init.name}: Creates collections`, async () => { - const db = await getDb(); - expect(db.backingDb.collections).toBeTruthy(); - - for (const collectionId of Object.values(CollectionId)) { - expect(db.backingDb.collections[collectionId]).toBeTruthy(); - } -}); - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.addObject.name}: Adds object`, async () => { - const { db, user } = await getTestApiUtils(); - await db.addObject(CollectionId.Users, user); - expect(await db.countObjects(CollectionId.Users, {})).toBe(1); -}); - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.deleteObjectById.name}: Deletes object by id`, async () => { - const { db, user } = await getTestApiUtils(); - await db.addObject(CollectionId.Users, user); - - await db.deleteObjectById(CollectionId.Users, user._id as any as ObjectId); - expect(await db.countObjects(CollectionId.Users, {})).toBe(0); -}); - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.updateObjectById.name}: Updates object`, async () => { - const { db, user } = await getTestApiUtils(); - await db.addObject(CollectionId.Users, user); - - const updated = { name: "Updated User" }; - await db.updateObjectById( - CollectionId.Users, - user._id as any as ObjectId, - updated, - ); - - expect( - await db.findObjectById(CollectionId.Users, user._id as any as ObjectId), - ).toStrictEqual({ ...user, ...updated }); -}); - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.findObjectById.name}: Finds object by id`, async () => { - const { db, user } = await getTestApiUtils(); - await db.addObject(CollectionId.Users, user); - - expect( - await db.findObjectById(CollectionId.Users, user._id as any as ObjectId), - ).toStrictEqual(user); -}); - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.findObject.name}: Finds object by query`, async () => { - const { db, user } = await getTestApiUtils(); - await db.addObject(CollectionId.Users, user); - expect( - await db.findObject(CollectionId.Users, { name: user.name }), - ).toStrictEqual(user); -}); - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.findObjects.name}: Finds multiple objects by query`, async () => { - const db = await getDb(); - - const objects = [ - { _id: new ObjectId(), name: "Test User", group: 1 }, - { _id: new ObjectId(), name: "Test User 2", group: 1 }, - { _id: new ObjectId(), name: "Test User 3", group: 2 }, - ]; - - for (const object of objects) { - await db.addObject(CollectionId.Users, object as any as User); - } - - expect(await db.findObjects(CollectionId.Users, { group: 1 })).toStrictEqual( - objects.filter((o) => o.group === 1), - ); - expect( - await db.findObjects(CollectionId.Users, { name: objects[0].name }), - ).toStrictEqual([objects[0]]); -}); - -test(`${InMemoryDbInterface.name}.${InMemoryDbInterface.prototype.countObjects.name}: Counts objects`, async () => { - const { user } = await getTestApiUtils(); - // User is automatically added to the DB in getTestApiUtils, so we need to create a fresh DB - const db = await getDb(); - expect(await db.countObjects(CollectionId.Users, {})).toBe(0); - - await db.addObject(CollectionId.Users, user); - expect(await db.countObjects(CollectionId.Users, {})).toBe(1); -});