diff --git a/apps/www/src/pages/api/v0/history/[server]/get-daily-data.ts b/apps/www/src/pages/api/v0/history/[server]/get-daily-data.ts index fff7620..c6d4a8f 100644 --- a/apps/www/src/pages/api/v0/history/[server]/get-daily-data.ts +++ b/apps/www/src/pages/api/v0/history/[server]/get-daily-data.ts @@ -28,35 +28,66 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -import { MongoClient } from "mongodb"; -import { NextApiRequest, NextApiResponse } from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { MongoClient as MongoClientImpl } from "mongodb"; + +interface DailyAverage { + day: string; + result: number; +} + +interface ResponseData { + result: DailyAverage[]; +} export default async function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { - const client = new MongoClient(process.env.MONGO_DB as string); + const client = new MongoClientImpl(process.env.MONGO_DB as string); + + try { const db = client.db("mhsf").collection("history"); const server = req.query.server as string; + const daysOfWeek = [ + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; - const daysOfWeek = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] - const result = await Promise.all([1,2,3,4,5,6,7].map(async (c) => { - const results = await db.find({ - $and: [ - { server }, - { $expr: { $eq: [{ $dayOfWeek: "$date" }, c] } } - ] - }).toArray() + // Use MongoDB aggregation pipeline for better performance + const dailyAverages = (await db + .aggregate([ + { + $match: { server }, + }, + { + $group: { + _id: { $dayOfWeek: "$date" }, + averagePlayerCount: { $avg: "$player_count" }, + }, + }, + { + $project: { + _id: 0, + day: { $arrayElemAt: [daysOfWeek, { $subtract: ["$_id", 1] }] }, + result: { $floor: "$averagePlayerCount" }, + }, + }, + { + $sort: { _id: 1 }, + }, + ]) + .toArray()) as DailyAverage[]; - if (results.length !== 0) { - const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count) - const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length; - - return { day: daysOfWeek[c - 1], result: Math.floor(average) }; - } - return undefined; - })); - - client.close() - res.send({result: result.filter((c) => c !== undefined)}); -} \ No newline at end of file + res.send({ result: dailyAverages }); + } catch (error) { + res.status(500).json({ message: "An error occurred while fetching data" }); + } finally { + await client.close(); + } +} diff --git a/apps/www/src/pages/api/v0/history/[server]/get-historical-data.ts b/apps/www/src/pages/api/v0/history/[server]/get-historical-data.ts index 7d5f5e8..3f4441e 100644 --- a/apps/www/src/pages/api/v0/history/[server]/get-historical-data.ts +++ b/apps/www/src/pages/api/v0/history/[server]/get-historical-data.ts @@ -28,35 +28,64 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -import { MongoClient } from "mongodb"; -import { NextApiRequest, NextApiResponse } from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { MongoClient as MongoClientImpl } from "mongodb"; + +// Define types for our data +interface ServerHistoricalRecord { + server: string; + [key: string]: unknown; +} + +interface ResponseData { + data: Record[]; +} export default async function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { - const client = new MongoClient(process.env.MONGO_DB as string); - const db = client.db("mhsf").collection("historical"); - const server = req.query.server as string; - const scopes: Array = checkForInfoOrLeave(res, req.body.scopes); + const client = new MongoClientImpl(process.env.MONGO_DB as string); - const allData = await db.find({ server }).toArray(); - const data: any[] = []; + try { + const db = client.db("mhsf").collection("historical"); + const server = req.query.server as string; + const scopes: string[] = checkForInfoOrLeave(res, req.body.scopes); - allData.forEach((d) => { - const result: any = {}; - scopes.forEach((b) => { - result[b] = d[b]; + // Only fetch the fields we need using projection + const projection: Record = { server: 1 }; + for (const scope of scopes) { + projection[scope] = 1; + } + + const allData = await db + .find({ server }, { projection }) + .toArray(); + + // Use map instead of forEach for better performance + const data = allData.map((d) => { + const result: Record = {}; + for (const scope of scopes) { + result[scope] = d[scope]; + } + return result; }); - data.push(result); - }); - client.close(); - res.send({ data }); + res.send({ data }); + } catch (error) { + res.status(500).json({ message: "An error occurred while fetching data" }); + } finally { + await client.close(); + } } -function checkForInfoOrLeave(res: NextApiResponse, info: any) { - if (info == undefined) +function checkForInfoOrLeave( + res: NextApiResponse, + info: string[] | undefined +): string[] { + if (info === undefined) { res.status(400).json({ message: "Information wasn't supplied" }); + return []; + } return info; } diff --git a/apps/www/src/pages/api/v0/history/[server]/get-monthly-data.ts b/apps/www/src/pages/api/v0/history/[server]/get-monthly-data.ts index 84a5313..0767663 100644 --- a/apps/www/src/pages/api/v0/history/[server]/get-monthly-data.ts +++ b/apps/www/src/pages/api/v0/history/[server]/get-monthly-data.ts @@ -28,35 +28,78 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -import { MongoClient } from "mongodb"; -import { NextApiRequest, NextApiResponse } from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { MongoClient as MongoClientImpl } from "mongodb"; + +interface MonthlyAverage { + month: string; + result: number; +} + +interface ResponseData { + result: MonthlyAverage[]; +} export default async function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { - const client = new MongoClient(process.env.MONGO_DB as string); + const client = new MongoClientImpl(process.env.MONGO_DB as string); + + try { const db = client.db("mhsf").collection("history"); const server = req.query.server as string; + const months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + const currentYear = new Date().getFullYear(); - const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] - const result = await Promise.all([1,2,3,4,5,6,7,8,9,10,11,12].map(async (c) => { - const results = await db.find({ - $and: [ - { server }, - { date: { $gte: new Date(new Date().getFullYear(), c - 1, 1), $lt: new Date(new Date().getFullYear(), c, 1) } } - ] - }).toArray() + // Use MongoDB aggregation pipeline for better performance + const monthlyAverages = (await db + .aggregate([ + { + $match: { + server, + date: { + $gte: new Date(currentYear, 0, 1), + $lt: new Date(currentYear + 1, 0, 1), + }, + }, + }, + { + $group: { + _id: { $month: "$date" }, + averagePlayerCount: { $avg: "$player_count" }, + }, + }, + { + $project: { + _id: 0, + month: { $arrayElemAt: [months, { $subtract: ["$_id", 1] }] }, + result: { $floor: "$averagePlayerCount" }, + }, + }, + { + $sort: { _id: 1 }, + }, + ]) + .toArray()) as MonthlyAverage[]; - if (results.length !== 0) { - const averageNums = (results as any as {player_count: number}[]).map((x: {player_count: number}) => x.player_count) - const average = averageNums.reduce((sum, val) => sum + val, 0) / averageNums.length; - - return { month: months[c - 1], result: Math.floor(average) }; - } - return undefined; - })); - - client.close() - res.send({result: result.filter((c) => c !== undefined)}); -} \ No newline at end of file + res.send({ result: monthlyAverages }); + } catch (error) { + res.status(500).json({ message: "An error occurred while fetching data" }); + } finally { + await client.close(); + } +} diff --git a/apps/www/src/pages/api/v0/history/[server]/get-relative-data.ts b/apps/www/src/pages/api/v0/history/[server]/get-relative-data.ts index d762bd2..bbfa7dc 100644 --- a/apps/www/src/pages/api/v0/history/[server]/get-relative-data.ts +++ b/apps/www/src/pages/api/v0/history/[server]/get-relative-data.ts @@ -28,42 +28,92 @@ * OTHER DEALINGS IN THE SOFTWARE. */ -import { MongoClient } from "mongodb"; -import { NextApiRequest, NextApiResponse } from "next"; +import type { NextApiRequest, NextApiResponse } from "next"; +import { MongoClient as MongoClientImpl } from "mongodb"; + +interface ServerHistoryRecord { + server: string; + player_count: number; + date: Date; +} + +interface MHRecord { + total_players: number; + date: Date; +} + +interface RelativeData { + relativePrecentage: number; + date: Date; +} + +interface ResponseData { + data: RelativeData[]; +} export default async function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { - const client = new MongoClient(process.env.MONGO_DB as string); - const db = client.db("mhsf").collection("history"); - const mh = client.db("mhsf").collection("mh"); - const server = req.query.server as string; + const client = new MongoClientImpl(process.env.MONGO_DB as string); - const allData = await db.find({ server }).toArray(); - const data: any[] = []; - if (server === "peww") console.log(allData.slice(-30)); + try { + const db = client.db("mhsf").collection("history"); + const mh = client.db("mhsf").collection("mh"); + const server = req.query.server as string; - for (const d of allData.slice(-30)) { - const dateOfEntry = new Date(d.date); - const result = await mh - .find({ - date: { - $gte: new Date(dateOfEntry.getTime() - 1000 * 60 * 60), - $lt: new Date(dateOfEntry.getTime() + 1000 * 60 * 60), - }, - }) + // Get only the last 30 records with needed fields + const recentData = await db + .find( + { server }, + { + projection: { player_count: 1, date: 1 }, + sort: { date: -1 }, + limit: 30, + } + ) .toArray(); - if (result.length > 0) { - const resultedData = result[0]; - data.push({ - relativePrecentage: d.player_count / resultedData.total_players, - date: dateOfEntry, - }); - } - } + const data: RelativeData[] = []; - client.close(); - res.send({ data }); + // Process in batches to reduce the number of database queries + const batchSize = 5; + for (let i = 0; i < recentData.length; i += batchSize) { + const batch = recentData.slice(i, i + batchSize); + const batchQueries = batch.map(async (d) => { + const dateOfEntry = new Date(d.date); + const hourBefore = new Date(dateOfEntry.getTime() - 1000 * 60 * 60); + const hourAfter = new Date(dateOfEntry.getTime() + 1000 * 60 * 60); + + const result = await mh.findOne( + { + date: { + $gte: hourBefore, + $lt: hourAfter, + }, + }, + { projection: { total_players: 1, date: 1 } } + ); + + if (result) { + return { + relativePrecentage: d.player_count / result.total_players, + date: dateOfEntry, + }; + } + return null; + }); + + const batchResults = await Promise.all(batchQueries); + data.push( + ...batchResults.filter((item): item is RelativeData => item !== null) + ); + } + + res.send({ data }); + } catch (error) { + res.status(500).json({ message: "An error occurred while fetching data" }); + } finally { + await client.close(); + } } diff --git a/apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts b/apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts index fbf79e2..e1b4a63 100644 --- a/apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts +++ b/apps/www/src/pages/api/v0/history/[server]/get-short-term-data.ts @@ -1,67 +1,67 @@ -/* - * MHSF, Minehut Server List - * All external content is rather licensed under the ECA Agreement - * located here: https://mhsf.app/docs/legal/external-content-agreement - * - * All code under MHSF is licensed under the MIT License - * by open source contributors - * - * Copyright (c) 2025 dvelo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES - * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND - * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT - * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - * OTHER DEALINGS IN THE SOFTWARE. - */ +import type { NextApiRequest, NextApiResponse } from "next"; +import { MongoClient as MongoClientImpl } from "mongodb"; -import { MongoClient } from "mongodb"; -import { NextApiRequest, NextApiResponse } from "next"; +// Define types for our data +interface ServerHistoryRecord { + server: string; + player_count: number; + date: Date; + [key: string]: unknown; +} + +interface ResponseData { + data: Record[]; + dataMax: number; +} export default async function handler( req: NextApiRequest, - res: NextApiResponse + res: NextApiResponse ) { - const client = new MongoClient(process.env.MONGO_DB as string); - const db = client.db("mhsf").collection("history"); - const server = req.query.server as string; - let dataMax = 0; - const scopes: Array = checkForInfoOrLeave(res, req.body.scopes); + const client = new MongoClientImpl(process.env.MONGO_DB as string); - const allData = await db.find({ server }).toArray(); - const data: any[] = []; + try { + const db = client.db("mhsf").collection("history"); + const server = req.query.server as string; + const scopes: string[] = checkForInfoOrLeave(res, req.body.scopes); - dataMax = ( - await db.find({ server }).sort({ player_count: -1 }).limit(1).toArray() - )[0].player_count; + // Run these queries in parallel for better performance + const [allData, maxPlayerData] = await Promise.all([ + db.find({ server }).toArray(), + db + .find({ server }) + .sort({ player_count: -1 }) + .limit(1) + .toArray(), + ]); - allData.forEach((d) => { - const result: any = {}; - scopes.forEach((b) => { - result[b] = d[b]; + const dataMax = + maxPlayerData.length > 0 ? maxPlayerData[0].player_count : 0; + + // Use map instead of forEach for better performance + const data = allData.map((d) => { + const result: Record = {}; + for (const scope of scopes) { + result[scope] = d[scope]; + } + return result; }); - data.push(result); - }); - client.close(); - res.send({ data, dataMax }); + res.send({ data, dataMax }); + } catch (error) { + res.status(500).json({ message: "An error occurred while fetching data" }); + } finally { + await client.close(); + } } -function checkForInfoOrLeave(res: NextApiResponse, info: any) { - if (info == undefined) +function checkForInfoOrLeave( + res: NextApiResponse, + info: string[] | undefined +): string[] { + if (info === undefined) { res.status(400).json({ message: "Information wasn't supplied" }); + return []; + } return info; }