From f3db3e1d4a184fd9b6efc99e35d1d3e683cb25fd Mon Sep 17 00:00:00 2001 From: Supermarcel10 Date: Sun, 1 Dec 2024 20:47:12 +0000 Subject: [PATCH 1/6] - Moved constant variables into environment file for easier access. - Updated everything to work with multiple leaderboards. - Changed console.log into console.error. --- aoc-backend/.env.template | 7 ++- aoc-backend/server.js | 113 +++++++++++++++++++++++++++----------- 2 files changed, 86 insertions(+), 34 deletions(-) diff --git a/aoc-backend/.env.template b/aoc-backend/.env.template index 915346c..8835c90 100644 --- a/aoc-backend/.env.template +++ b/aoc-backend/.env.template @@ -1,4 +1,9 @@ # Copy to an .env file. # DO NOT PUSH .env TO VCS! -SESSION_COOKIE= \ No newline at end of file +SESSION_COOKIE= + +PORT=8080 +YEAR=2024 + +CACHE_DURATION=15#mins \ No newline at end of file diff --git a/aoc-backend/server.js b/aoc-backend/server.js index ab35b14..6b3d5c8 100644 --- a/aoc-backend/server.js +++ b/aoc-backend/server.js @@ -3,23 +3,22 @@ const axios = require("axios"); require('dotenv').config(); const cors = require("cors"); const app = express(); -const PORT = 6000; // Configuration +const PORT = process.env.PORT; +const CACHE_DURATION = process.env.CACHE_DURATION * 60 * 1000; // Convert to milliseconds + const config = { sessionCookie: process.env.SESSION_COOKIE, - leaderboardId: "48462", // Your leaderboard ID - year: "2023", + leaderboardIds: [ + "4296175" // CSG + ], + year: process.env.YEAR, }; -// Cache configuration -let cachedLeaderboard = null; -let lastFetchTime = null; -const CACHE_DURATION = 15 * 60 * 1000; // 15 minutes in milliseconds - // CORS Configuration const corsOptions = { - origin: ["http://localhost:6000", "http://127.0.0.1:6000"], + origin: [`http://localhost:${PORT}`, "http://127.0.0.1:${PORT}"], methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", allowedHeaders: ["Content-Type", "Authorization"], credentials: true, @@ -27,33 +26,40 @@ const corsOptions = { optionsSuccessStatus: 204, }; +app.use(cors(corsOptions)); +const leaderboardCache = new Map(); -app.use(cors(corsOptions)); +class LeaderboardCacheEntry { + constructor(data) { + this.data = data; + this.lastFetchTime = Date.now(); + } + + isValid() { + return Date.now() - this.lastFetchTime < CACHE_DURATION; + } +} // Function to fetch the leaderboard -const fetchLeaderboardData = async () => { - const now = Date.now(); +const fetchLeaderboardData = async (leaderboardId) => { + const cachedEntry = leaderboardCache.get(leaderboardId); // Return cached data if it's still valid - if ( - cachedLeaderboard && - lastFetchTime && - now - lastFetchTime < CACHE_DURATION - ) { - return cachedLeaderboard; + if (cachedEntry && cachedEntry.isValid()) { + return cachedEntry.data; } - return hitEndpoint(now); + return hitEndpoint(leaderboardId); }; -const hitEndpoint = async (now) => { +const hitEndpoint = async (leaderboardId) => { try { - let response = await fetchData(); - cachedLeaderboard = response.data; - lastFetchTime = now; - console.log(`Leaderboard data fetched successfully at ${new Date().toISOString()}`); - return cachedLeaderboard; + let response = await fetchData(leaderboardId); + const newCacheEntry = new LeaderboardCacheEntry(response.data); + leaderboardCache.set(leaderboardId, newCacheEntry); + console.log(`Leaderboard ${leaderboardId} data fetched successfully at ${new Date().toISOString()}`); + return newCacheEntry.data; } catch (error) { if (error.message.includes('REDIRECT_ERROR')) { console.error('Session likely expired:', error.message); @@ -61,13 +67,14 @@ const hitEndpoint = async (now) => { console.error("Response status:", error.response.status); console.error("Response headers:", error.response.headers); } else { - console.error("Error fetching leaderboard data:", error.message); + console.error(`Error fetching leaderboard ${leaderboardId} data:`, error.message); } + throw error; } } -const fetchData = async () => { - const url = `https://adventofcode.com/${config.year}/leaderboard/private/view/${config.leaderboardId}.json`; +const fetchData = async (leaderboardId) => { + const url = `https://adventofcode.com/${config.year}/leaderboard/private/view/${leaderboardId}.json`; const response = await axios.get(url, { headers: { @@ -81,10 +88,13 @@ const fetchData = async () => { return response; } -// Leaderboard endpoint -app.get("/leaderboard", cors(corsOptions), async (req, res) => { +// Single leaderboard endpoint +app.get("/leaderboard/:id", cors(corsOptions), async (req, res) => { try { - const data = await fetchLeaderboardData(); + if (!config.leaderboardIds.includes(req.params.id)) { + return res.status(404).json({ error: "Leaderboard not found" }); + } + const data = await fetchLeaderboardData(req.params.id); res.json(data); } catch (error) { res.status(503).json({ @@ -94,6 +104,36 @@ app.get("/leaderboard", cors(corsOptions), async (req, res) => { } }); +// All leaderboards endpoint +app.get("/leaderboards", cors(corsOptions), async (req, res) => { + try { + const results = await Promise.all( + config.leaderboardIds.map(async (leaderboardId) => { + try { + const data = await fetchLeaderboardData(leaderboardId); + return { + id: leaderboardId, + data: data, + error: null + }; + } catch (error) { + return { + id: leaderboardId, + data: null, + error: error.message + }; + } + }) + ); + res.json(results); + } catch (error) { + res.status(503).json({ + error: "Failed to fetch leaderboards data", + message: error.message, + }); + } +}); + // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); @@ -105,6 +145,13 @@ app.use((err, req, res, next) => { app.listen(PORT, async () => { console.log(`Server running on http://localhost:${PORT}`); - console.log(`Fetching leaderboard ID: ${config.leaderboardId} for year ${config.year}`,); - await hitEndpoint(); + + // Initial fetch of all leaderboards + for (const leaderboardId of config.leaderboardIds) { + try { + await hitEndpoint(leaderboardId); + } catch (error) { + console.error(`Failed to fetch initial data for leaderboard ${leaderboardId}`); + } + } }); From ec69900b82f6283edb08e15011af25f535399e99 Mon Sep 17 00:00:00 2001 From: Supermarcel10 Date: Sun, 1 Dec 2024 21:31:06 +0000 Subject: [PATCH 2/6] - Migrated to TS. - Moved into src directory. - Updated express dependency. --- aoc-backend/.env.template | 2 +- aoc-backend/package-lock.json | 1159 +++++++++++++++++++++++++++++---- aoc-backend/package.json | 17 +- aoc-backend/server.js | 157 ----- aoc-backend/src/server.ts | 185 ++++++ aoc-backend/tsconfig.json | 19 + 6 files changed, 1244 insertions(+), 295 deletions(-) delete mode 100644 aoc-backend/server.js create mode 100644 aoc-backend/src/server.ts create mode 100644 aoc-backend/tsconfig.json diff --git a/aoc-backend/.env.template b/aoc-backend/.env.template index 8835c90..aaa6ead 100644 --- a/aoc-backend/.env.template +++ b/aoc-backend/.env.template @@ -3,7 +3,7 @@ SESSION_COOKIE= -PORT=8080 +PORT=3000 YEAR=2024 CACHE_DURATION=15#mins \ No newline at end of file diff --git a/aoc-backend/package-lock.json b/aoc-backend/package-lock.json index a829620..42f1c9b 100644 --- a/aoc-backend/package-lock.json +++ b/aoc-backend/package-lock.json @@ -12,26 +12,289 @@ "axios": "^1.7.8", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.1" + "express": "^5.0.1" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^22.10.1", + "nodemon": "^3.1.7", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" } }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-db": "^1.53.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", + "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", "license": "MIT" }, "node_modules/asynckit": { @@ -51,28 +314,106 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.0.2.tgz", + "integrity": "sha512-SNMk0OONlQ01uk8EPeiBvTW7W4ovpL5b1O3t1sjpPgfxOQ6BqQJ6XjxinDPR79Z6HdcD5zBBwr5ssiTlgdNztQ==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", + "debug": "3.1.0", "destroy": "1.2.0", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.5.2", "on-finished": "2.4.1", "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "raw-body": "^3.0.0", + "type-is": "~1.6.18" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/bytes": { @@ -103,6 +444,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -115,10 +481,17 @@ "node": ">= 0.8" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -146,10 +519,13 @@ } }, "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } }, "node_modules/cors": { "version": "2.8.5", @@ -164,13 +540,28 @@ "node": ">= 0.10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "license": "MIT", "dependencies": { - "ms": "2.0.0" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/define-data-property": { @@ -218,6 +609,16 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -282,55 +683,90 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", + "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", "license": "MIT", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "accepts": "^2.0.0", + "body-parser": "^2.0.1", + "content-disposition": "^1.0.0", "content-type": "~1.0.4", "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", + "cookie-signature": "^1.2.1", + "debug": "4.3.6", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", + "finalhandler": "^2.0.0", + "fresh": "2.0.0", "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", + "merge-descriptors": "^2.0.0", "methods": "~1.1.2", + "mime-types": "^3.0.0", "on-finished": "2.4.1", + "once": "1.4.0", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", + "router": "^2.0.0", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "^1.1.0", + "serve-static": "^2.1.0", "setprototypeof": "1.2.0", "statuses": "2.0.1", - "type-is": "~1.6.18", + "type-is": "^2.0.0", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.53.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.0.0.tgz", + "integrity": "sha512-MX6Zo2adDViYh+GcxxB1dpO43eypOGUOL12rLCOTMQv/DfIbpSJUy4oQIIZhVZkH9e+bZWKMon0XHFEju16tkQ==", "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~2.0.0", + "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -341,6 +777,30 @@ "node": ">= 0.8" } }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -385,12 +845,27 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, "node_modules/function-bind": { @@ -421,18 +896,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.1.0.tgz", + "integrity": "sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.3" + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -446,10 +947,13 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz", + "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==", "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, "engines": { "node": ">= 0.4" }, @@ -498,9 +1002,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -509,6 +1013,13 @@ "node": ">=0.10.0" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -524,20 +1035,82 @@ "node": ">= 0.10" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "license": "MIT", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -551,18 +1124,6 @@ "node": ">= 0.6" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -584,21 +1145,98 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/nodemon": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", + "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -632,6 +1270,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -642,10 +1289,26 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "license": "MIT" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } }, "node_modules/proxy-addr": { "version": "2.0.7", @@ -666,6 +1329,13 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/qs": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -691,20 +1361,63 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.6.3", "unpipe": "1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/router": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.0.0.tgz", + "integrity": "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ==", + "license": "MIT", + "dependencies": { + "array-flatten": "3.0.0", + "is-promise": "4.0.0", + "methods": "~1.1.2", + "parseurl": "~1.3.3", + "path-to-regexp": "^8.0.0", + "setprototypeof": "1.2.0", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -731,37 +1444,49 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", + "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", "license": "MIT", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "http-errors": "^2.0.0", + "mime-types": "^2.1.35", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/send/node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/send/node_modules/ms": { @@ -771,18 +1496,18 @@ "license": "MIT" }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", + "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", "license": "MIT", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, "node_modules/set-function-length": { @@ -826,6 +1551,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -835,6 +1573,32 @@ "node": ">= 0.8" } }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -844,19 +1608,123 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", + "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", + "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", + "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.53.0" }, "engines": { "node": ">= 0.6" } }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -875,6 +1743,13 @@ "node": ">= 0.4.0" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -883,6 +1758,22 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } } } } diff --git a/aoc-backend/package.json b/aoc-backend/package.json index 1c8a508..29ede23 100644 --- a/aoc-backend/package.json +++ b/aoc-backend/package.json @@ -1,9 +1,12 @@ { "name": "aoc-backend", "version": "1.0.0", - "main": "index.js", + "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "start": "node dist/server.js", + "dev": "nodemon", + "build": "tsc", + "watch": "tsc -w" }, "keywords": [], "author": "", @@ -13,6 +16,14 @@ "axios": "^1.7.8", "cors": "^2.8.5", "dotenv": "^16.4.5", - "express": "^4.21.1" + "express": "^5.0.1" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.0", + "@types/node": "^22.10.1", + "nodemon": "^3.1.7", + "ts-node": "^10.9.2", + "typescript": "^5.7.2" } } diff --git a/aoc-backend/server.js b/aoc-backend/server.js deleted file mode 100644 index 6b3d5c8..0000000 --- a/aoc-backend/server.js +++ /dev/null @@ -1,157 +0,0 @@ -const express = require("express"); -const axios = require("axios"); -require('dotenv').config(); -const cors = require("cors"); -const app = express(); - -// Configuration -const PORT = process.env.PORT; -const CACHE_DURATION = process.env.CACHE_DURATION * 60 * 1000; // Convert to milliseconds - -const config = { - sessionCookie: process.env.SESSION_COOKIE, - leaderboardIds: [ - "4296175" // CSG - ], - year: process.env.YEAR, -}; - -// CORS Configuration -const corsOptions = { - origin: [`http://localhost:${PORT}`, "http://127.0.0.1:${PORT}"], - methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", - allowedHeaders: ["Content-Type", "Authorization"], - credentials: true, - preflightContinue: false, - optionsSuccessStatus: 204, -}; - -app.use(cors(corsOptions)); - -const leaderboardCache = new Map(); - -class LeaderboardCacheEntry { - constructor(data) { - this.data = data; - this.lastFetchTime = Date.now(); - } - - isValid() { - return Date.now() - this.lastFetchTime < CACHE_DURATION; - } -} - -// Function to fetch the leaderboard -const fetchLeaderboardData = async (leaderboardId) => { - const cachedEntry = leaderboardCache.get(leaderboardId); - - // Return cached data if it's still valid - if (cachedEntry && cachedEntry.isValid()) { - return cachedEntry.data; - } - - return hitEndpoint(leaderboardId); -}; - -const hitEndpoint = async (leaderboardId) => { - try { - let response = await fetchData(leaderboardId); - const newCacheEntry = new LeaderboardCacheEntry(response.data); - leaderboardCache.set(leaderboardId, newCacheEntry); - console.log(`Leaderboard ${leaderboardId} data fetched successfully at ${new Date().toISOString()}`); - return newCacheEntry.data; - } catch (error) { - if (error.message.includes('REDIRECT_ERROR')) { - console.error('Session likely expired:', error.message); - } else if (error.response) { - console.error("Response status:", error.response.status); - console.error("Response headers:", error.response.headers); - } else { - console.error(`Error fetching leaderboard ${leaderboardId} data:`, error.message); - } - throw error; - } -} - -const fetchData = async (leaderboardId) => { - const url = `https://adventofcode.com/${config.year}/leaderboard/private/view/${leaderboardId}.json`; - - const response = await axios.get(url, { - headers: { - Cookie: `session=${config.sessionCookie}` - }, - }); - - if (typeof response.data === 'string' && response.data.includes('')) - throw new Error('REDIRECT_ERROR: Received HTML instead of JSON - likely redirected to login page'); - - return response; -} - -// Single leaderboard endpoint -app.get("/leaderboard/:id", cors(corsOptions), async (req, res) => { - try { - if (!config.leaderboardIds.includes(req.params.id)) { - return res.status(404).json({ error: "Leaderboard not found" }); - } - const data = await fetchLeaderboardData(req.params.id); - res.json(data); - } catch (error) { - res.status(503).json({ - error: "Failed to fetch leaderboard data", - message: error.message, - }); - } -}); - -// All leaderboards endpoint -app.get("/leaderboards", cors(corsOptions), async (req, res) => { - try { - const results = await Promise.all( - config.leaderboardIds.map(async (leaderboardId) => { - try { - const data = await fetchLeaderboardData(leaderboardId); - return { - id: leaderboardId, - data: data, - error: null - }; - } catch (error) { - return { - id: leaderboardId, - data: null, - error: error.message - }; - } - }) - ); - res.json(results); - } catch (error) { - res.status(503).json({ - error: "Failed to fetch leaderboards data", - message: error.message, - }); - } -}); - -// Error handling middleware -app.use((err, req, res, next) => { - console.error(err.stack); - res.status(500).json({ - error: "Internal Server Error", - message: err.message, - }); -}); - -app.listen(PORT, async () => { - console.log(`Server running on http://localhost:${PORT}`); - - // Initial fetch of all leaderboards - for (const leaderboardId of config.leaderboardIds) { - try { - await hitEndpoint(leaderboardId); - } catch (error) { - console.error(`Failed to fetch initial data for leaderboard ${leaderboardId}`); - } - } -}); diff --git a/aoc-backend/src/server.ts b/aoc-backend/src/server.ts new file mode 100644 index 0000000..bc53616 --- /dev/null +++ b/aoc-backend/src/server.ts @@ -0,0 +1,185 @@ +import express, { Request, Response } from 'express'; +import axios, {AxiosError, AxiosResponse} from "axios"; +import dotenv from 'dotenv'; +import cors, {CorsOptions} from "cors"; + +const envLoadingResult = dotenv.config({ path: '../.env' }); +if (envLoadingResult.error) { + console.error('Error loading .env file:', envLoadingResult.error); + process.exit(1); +} + +interface Config { + sessionCookie: string; + leaderboardIds: string[]; + year: string; +} + +interface LeaderboardData { + // TODO: Define + [key: string]: any; +} + +interface LeaderboardResult { + id: string; + data: LeaderboardData | null; + error: string | null; +} + +class LeaderboardCacheEntry { + constructor( + public data: LeaderboardData, + public lastFetchTime: number = Date.now() + ) {} + + isValid(): boolean { + return Date.now() - this.lastFetchTime < CACHE_DURATION; + } +} + +// Configuration +const PORT = process.env.PORT || 3000; +const CACHE_DURATION = (process.env.CACHE_DURATION as unknown as number || 15) * 60 * 1000; // Convert to milliseconds + +const config: Config = { + sessionCookie: process.env.SESSION_COOKIE || '', + leaderboardIds: [ + "4296175" // CSG + ], + year: process.env.YEAR || '2023', +}; + +// CORS Configuration +const corsOptions: CorsOptions = { + origin: [`http://localhost:${PORT}`, `http://127.0.0.1:${PORT}`], + methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", + allowedHeaders: ["Content-Type", "Authorization"], + credentials: true, + preflightContinue: false, + optionsSuccessStatus: 204, +}; + +const app = express(); +app.use(cors(corsOptions)); + +const leaderboardCache = new Map(); + +// Function to fetch the leaderboard +const fetchLeaderboardData = async (leaderboardId: string): Promise => { + const cachedEntry = leaderboardCache.get(leaderboardId); + + // Return cached data if it's still valid + if (cachedEntry && cachedEntry.isValid()) { + return cachedEntry.data; + } + + return hitEndpoint(leaderboardId); +}; + +const hitEndpoint = async (leaderboardId: string): Promise => { + try { + const response = await fetchData(leaderboardId); + const newCacheEntry = new LeaderboardCacheEntry(response.data); + leaderboardCache.set(leaderboardId, newCacheEntry); + console.log(`Leaderboard ${leaderboardId} data fetched successfully at ${new Date().toISOString()}`); + return newCacheEntry.data; + } catch (error: unknown) { + if (error instanceof Error && error.message?.includes('REDIRECT_ERROR')) { + console.error('Session likely expired:', error.message); + } else if (error instanceof AxiosError && error.response) { + console.error("Response status:", error.response.status); + console.error("Response headers:", error.response.headers); + } else { + console.error(`Error fetching leaderboard ${leaderboardId} data:`, (error as Error).message); + } + throw error; + } +}; + +const fetchData = async (leaderboardId: string): Promise> => { + const url = `https://adventofcode.com/${config.year}/leaderboard/private/view/${leaderboardId}.json`; + + const response = await axios.get(url, { + headers: { + Cookie: `session=${config.sessionCookie}` + }, + }); + + if (response.data != null && response.data.toString().includes('')) { + throw new Error('REDIRECT_ERROR: Received HTML instead of JSON - likely redirected to login page'); + } + return response; +}; + +// Single leaderboard endpoint +app.get("/leaderboard/:id", cors(corsOptions), async (req: Request, res: Response) => { + try { + if (!config.leaderboardIds.includes(req.params.id)) { + res.status(404).json({ error: "Leaderboard not found" }); + return; + } + + const data = await fetchLeaderboardData(req.params.id); + res.json(data); + } catch (error: unknown) { + res.status(503).json( + { + error: "Failed to fetch leaderboard data", + message: (error as Error).message + }); + } +}); + +// All leaderboards endpoint +app.get("/leaderboards", cors(corsOptions), async (_: Request, res: Response) => { + try { + const results: LeaderboardResult[] = await Promise.all( + config.leaderboardIds.map(async (leaderboardId) => { + try { + const data = await fetchLeaderboardData(leaderboardId); + return { + id: leaderboardId, + data: data, + error: null + }; + } catch (error: unknown) { + return { + id: leaderboardId, + data: null, + error: (error as Error).message + }; + } + }) + ); + res.json(results); + } catch (error: unknown) { + res.status(503).json( + { + error: "Failed to fetch leaderboards data", + message: (error as Error).message + }); + } +}); + +// Error handling middleware +app.use((err: Error, _: Request, res: Response) => { + console.error(err.stack); + res.status(500).json( + { + error: "Internal Server Error", + message: err.message + }); +}); + +app.listen(PORT, async () => { + console.log(`Server running on http://localhost:${PORT}`); + + // Initial fetch of all leaderboards + for (const leaderboardId of config.leaderboardIds) { + try { + await hitEndpoint(leaderboardId); + } catch { + console.error(`Failed to fetch initial data for leaderboard ${leaderboardId}`); + } + } +}); diff --git a/aoc-backend/tsconfig.json b/aoc-backend/tsconfig.json new file mode 100644 index 0000000..ccfce3c --- /dev/null +++ b/aoc-backend/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "commonjs", + "lib": ["ES2023"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "sourceMap": true, + "typeRoots": ["./node_modules/@types"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file From c05f38c1a681fb356d0a35221dadf645a7681d0c Mon Sep 17 00:00:00 2001 From: Supermarcel10 Date: Sun, 1 Dec 2024 21:34:25 +0000 Subject: [PATCH 3/6] - Reverted CORS stuff. --- aoc-backend/src/server.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aoc-backend/src/server.ts b/aoc-backend/src/server.ts index bc53616..ab4c6ec 100644 --- a/aoc-backend/src/server.ts +++ b/aoc-backend/src/server.ts @@ -1,4 +1,4 @@ -import express, { Request, Response } from 'express'; +import express, {NextFunction, Request, Response} from "express"; import axios, {AxiosError, AxiosResponse} from "axios"; import dotenv from 'dotenv'; import cors, {CorsOptions} from "cors"; @@ -162,8 +162,9 @@ app.get("/leaderboards", cors(corsOptions), async (_: Request, res: Response) => }); // Error handling middleware -app.use((err: Error, _: Request, res: Response) => { - console.error(err.stack); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +app.use((err: Error, req: Request, res: Response, _: NextFunction): void => { + console.error('Error caught in middleware:', err); res.status(500).json( { error: "Internal Server Error", From d15d7aa7003eea5efa2888bb23445fb1e566252f Mon Sep 17 00:00:00 2001 From: Supermarcel10 Date: Sun, 1 Dec 2024 21:45:14 +0000 Subject: [PATCH 4/6] - Updated type signature based on actual JSON, keeping only items that are relevant. --- aoc-backend/src/server.ts | 65 +++++++++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/aoc-backend/src/server.ts b/aoc-backend/src/server.ts index ab4c6ec..aea65f3 100644 --- a/aoc-backend/src/server.ts +++ b/aoc-backend/src/server.ts @@ -15,14 +15,49 @@ interface Config { year: string; } -interface LeaderboardData { - // TODO: Define - [key: string]: any; +interface Star { + star_index: number; + get_star_ts: number; } +interface DayCompletion { + "1": Star; + "2": Star; +} + +interface CompletionDayLevel { + [day: string]: DayCompletion; +} + +interface Member { + id: string; + name: string; + local_score: number; + completion_day_level: CompletionDayLevel; +} + +interface RawMember { + id: string; + name: string; + local_score: number; + completion_day_level: CompletionDayLevel; + // OTHER FIELDS IGNORED +} + +interface RawLeaderboardData { + members: { [key: string]: RawMember }; + // OTHER FIELDS IGNORED +} + +interface FilteredLeaderboardData { + members: Member[]; +} + +type LeaderboardData = FilteredLeaderboardData + interface LeaderboardResult { id: string; - data: LeaderboardData | null; + data: FilteredLeaderboardData | null; error: string | null; } @@ -64,6 +99,17 @@ app.use(cors(corsOptions)); const leaderboardCache = new Map(); +const filterLeaderboardData = (data: RawLeaderboardData): FilteredLeaderboardData => { + const members = Object.values(data.members).map(member => ({ + id: member.id, + name: member.name, + local_score: member.local_score, + completion_day_level: member.completion_day_level + })); + + return { members }; +}; + // Function to fetch the leaderboard const fetchLeaderboardData = async (leaderboardId: string): Promise => { const cachedEntry = leaderboardCache.get(leaderboardId); @@ -76,13 +122,14 @@ const fetchLeaderboardData = async (leaderboardId: string): Promise => { +const hitEndpoint = async (leaderboardId: string): Promise => { try { const response = await fetchData(leaderboardId); - const newCacheEntry = new LeaderboardCacheEntry(response.data); + const filteredData = filterLeaderboardData(response.data); + const newCacheEntry = new LeaderboardCacheEntry(filteredData); leaderboardCache.set(leaderboardId, newCacheEntry); console.log(`Leaderboard ${leaderboardId} data fetched successfully at ${new Date().toISOString()}`); - return newCacheEntry.data; + return filteredData; } catch (error: unknown) { if (error instanceof Error && error.message?.includes('REDIRECT_ERROR')) { console.error('Session likely expired:', error.message); @@ -96,10 +143,10 @@ const hitEndpoint = async (leaderboardId: string): Promise => { } }; -const fetchData = async (leaderboardId: string): Promise> => { +const fetchData = async (leaderboardId: string): Promise> => { const url = `https://adventofcode.com/${config.year}/leaderboard/private/view/${leaderboardId}.json`; - const response = await axios.get(url, { + const response = await axios.get(url, { headers: { Cookie: `session=${config.sessionCookie}` }, From 35f9b2dd08778c55fb1cb7fc3497b09b10f5dbd7 Mon Sep 17 00:00:00 2001 From: Supermarcel10 Date: Sun, 1 Dec 2024 21:59:10 +0000 Subject: [PATCH 5/6] - Split into separate files to ensure backend is more manageable and everything is split into distinct layers. - Minor fixes here and there. --- aoc-backend/src/config/config.ts | 27 +++ aoc-backend/src/routes/leaderboardRoutes.ts | 49 ++++ aoc-backend/src/server.ts | 215 +----------------- aoc-backend/src/services/leaderboardCache.ts | 24 ++ .../src/services/leaderboardService.ts | 85 +++++++ aoc-backend/src/types/leaderboard.ts | 47 ++++ 6 files changed, 242 insertions(+), 205 deletions(-) create mode 100644 aoc-backend/src/config/config.ts create mode 100644 aoc-backend/src/routes/leaderboardRoutes.ts create mode 100644 aoc-backend/src/services/leaderboardCache.ts create mode 100644 aoc-backend/src/services/leaderboardService.ts create mode 100644 aoc-backend/src/types/leaderboard.ts diff --git a/aoc-backend/src/config/config.ts b/aoc-backend/src/config/config.ts new file mode 100644 index 0000000..3b22255 --- /dev/null +++ b/aoc-backend/src/config/config.ts @@ -0,0 +1,27 @@ +import dotenv from 'dotenv'; +import { Config } from '../types/leaderboard'; + +export function loadConfig(): Config { + const envLoadingResult = dotenv.config({ path: '../.env' }); + if (envLoadingResult.error) { + console.error('Error loading .env file:', envLoadingResult.error); + process.exit(1); + } + + const sessionCookie = process.env.SESSION_COOKIE; + if (!sessionCookie) { + console.error('Session cookie was not provided!') + process.exit(1); + } + + return { + sessionCookie: process.env.SESSION_COOKIE as string, + leaderboardIds: [ + "4296175" // CSG + ], + year: process.env.YEAR || '2023', + }; +} + +export const CACHE_DURATION = (process.env.CACHE_DURATION as unknown as number || 15) * 60 * 1000; +export const PORT = process.env.PORT || 3000; diff --git a/aoc-backend/src/routes/leaderboardRoutes.ts b/aoc-backend/src/routes/leaderboardRoutes.ts new file mode 100644 index 0000000..aed64a8 --- /dev/null +++ b/aoc-backend/src/routes/leaderboardRoutes.ts @@ -0,0 +1,49 @@ +import { Router, Request, Response } from 'express'; +import { Config } from '../types/leaderboard'; +import { LeaderboardService } from '../services/leaderboardService'; + +export function createLeaderboardRouter(leaderboardService: LeaderboardService, config: Config): Router { + const router = Router(); + + router.get("/:id", async (req: Request, res: Response) => { + try { + if (!config.leaderboardIds.includes(req.params.id)) { + res.status(404).json({ error: "Leaderboard not found" }); + return; + } + + const data = await leaderboardService.getLeaderboardData(req.params.id); + res.json(data); + } catch (error: unknown) { + res.status(503).json( + { + error: "Failed to fetch leaderboard data", + message: (error as Error).message + }); + } + }); + + router.get("/", async (_: Request, res: Response) => { + try { + const results = await Promise.all( + config.leaderboardIds.map(async (leaderboardId) => { + try { + const data = await leaderboardService.getLeaderboardData(leaderboardId); + return { id: leaderboardId, data, error: null }; + } catch (error: unknown) { + return { id: leaderboardId, data: null, error: (error as Error).message }; + } + }) + ); + res.json(results); + } catch (error: unknown) { + res.status(503).json( + { + error: "Failed to fetch leaderboards data", + message: (error as Error).message + }); + } + }); + + return router; +} diff --git a/aoc-backend/src/server.ts b/aoc-backend/src/server.ts index aea65f3..52b7685 100644 --- a/aoc-backend/src/server.ts +++ b/aoc-backend/src/server.ts @@ -1,90 +1,11 @@ -import express, {NextFunction, Request, Response} from "express"; -import axios, {AxiosError, AxiosResponse} from "axios"; -import dotenv from 'dotenv'; -import cors, {CorsOptions} from "cors"; +import express, { Request, Response, NextFunction } from 'express'; +import cors, { CorsOptions } from 'cors'; +import { loadConfig, CACHE_DURATION, PORT } from './config/config'; +import { LeaderboardService } from './services/leaderboardService'; +import { createLeaderboardRouter } from './routes/leaderboardRoutes'; -const envLoadingResult = dotenv.config({ path: '../.env' }); -if (envLoadingResult.error) { - console.error('Error loading .env file:', envLoadingResult.error); - process.exit(1); -} +const config = loadConfig(); -interface Config { - sessionCookie: string; - leaderboardIds: string[]; - year: string; -} - -interface Star { - star_index: number; - get_star_ts: number; -} - -interface DayCompletion { - "1": Star; - "2": Star; -} - -interface CompletionDayLevel { - [day: string]: DayCompletion; -} - -interface Member { - id: string; - name: string; - local_score: number; - completion_day_level: CompletionDayLevel; -} - -interface RawMember { - id: string; - name: string; - local_score: number; - completion_day_level: CompletionDayLevel; - // OTHER FIELDS IGNORED -} - -interface RawLeaderboardData { - members: { [key: string]: RawMember }; - // OTHER FIELDS IGNORED -} - -interface FilteredLeaderboardData { - members: Member[]; -} - -type LeaderboardData = FilteredLeaderboardData - -interface LeaderboardResult { - id: string; - data: FilteredLeaderboardData | null; - error: string | null; -} - -class LeaderboardCacheEntry { - constructor( - public data: LeaderboardData, - public lastFetchTime: number = Date.now() - ) {} - - isValid(): boolean { - return Date.now() - this.lastFetchTime < CACHE_DURATION; - } -} - -// Configuration -const PORT = process.env.PORT || 3000; -const CACHE_DURATION = (process.env.CACHE_DURATION as unknown as number || 15) * 60 * 1000; // Convert to milliseconds - -const config: Config = { - sessionCookie: process.env.SESSION_COOKIE || '', - leaderboardIds: [ - "4296175" // CSG - ], - year: process.env.YEAR || '2023', -}; - -// CORS Configuration const corsOptions: CorsOptions = { origin: [`http://localhost:${PORT}`, `http://127.0.0.1:${PORT}`], methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", @@ -97,119 +18,11 @@ const corsOptions: CorsOptions = { const app = express(); app.use(cors(corsOptions)); -const leaderboardCache = new Map(); +const leaderboardService = new LeaderboardService(config, CACHE_DURATION); -const filterLeaderboardData = (data: RawLeaderboardData): FilteredLeaderboardData => { - const members = Object.values(data.members).map(member => ({ - id: member.id, - name: member.name, - local_score: member.local_score, - completion_day_level: member.completion_day_level - })); - - return { members }; -}; - -// Function to fetch the leaderboard -const fetchLeaderboardData = async (leaderboardId: string): Promise => { - const cachedEntry = leaderboardCache.get(leaderboardId); - - // Return cached data if it's still valid - if (cachedEntry && cachedEntry.isValid()) { - return cachedEntry.data; - } - - return hitEndpoint(leaderboardId); -}; - -const hitEndpoint = async (leaderboardId: string): Promise => { - try { - const response = await fetchData(leaderboardId); - const filteredData = filterLeaderboardData(response.data); - const newCacheEntry = new LeaderboardCacheEntry(filteredData); - leaderboardCache.set(leaderboardId, newCacheEntry); - console.log(`Leaderboard ${leaderboardId} data fetched successfully at ${new Date().toISOString()}`); - return filteredData; - } catch (error: unknown) { - if (error instanceof Error && error.message?.includes('REDIRECT_ERROR')) { - console.error('Session likely expired:', error.message); - } else if (error instanceof AxiosError && error.response) { - console.error("Response status:", error.response.status); - console.error("Response headers:", error.response.headers); - } else { - console.error(`Error fetching leaderboard ${leaderboardId} data:`, (error as Error).message); - } - throw error; - } -}; - -const fetchData = async (leaderboardId: string): Promise> => { - const url = `https://adventofcode.com/${config.year}/leaderboard/private/view/${leaderboardId}.json`; - - const response = await axios.get(url, { - headers: { - Cookie: `session=${config.sessionCookie}` - }, - }); - - if (response.data != null && response.data.toString().includes('')) { - throw new Error('REDIRECT_ERROR: Received HTML instead of JSON - likely redirected to login page'); - } - return response; -}; - -// Single leaderboard endpoint -app.get("/leaderboard/:id", cors(corsOptions), async (req: Request, res: Response) => { - try { - if (!config.leaderboardIds.includes(req.params.id)) { - res.status(404).json({ error: "Leaderboard not found" }); - return; - } - - const data = await fetchLeaderboardData(req.params.id); - res.json(data); - } catch (error: unknown) { - res.status(503).json( - { - error: "Failed to fetch leaderboard data", - message: (error as Error).message - }); - } -}); - -// All leaderboards endpoint -app.get("/leaderboards", cors(corsOptions), async (_: Request, res: Response) => { - try { - const results: LeaderboardResult[] = await Promise.all( - config.leaderboardIds.map(async (leaderboardId) => { - try { - const data = await fetchLeaderboardData(leaderboardId); - return { - id: leaderboardId, - data: data, - error: null - }; - } catch (error: unknown) { - return { - id: leaderboardId, - data: null, - error: (error as Error).message - }; - } - }) - ); - res.json(results); - } catch (error: unknown) { - res.status(503).json( - { - error: "Failed to fetch leaderboards data", - message: (error as Error).message - }); - } -}); +app.use('/leaderboard', createLeaderboardRouter(leaderboardService, config)); // Error handling middleware -// eslint-disable-next-line @typescript-eslint/no-unused-vars app.use((err: Error, req: Request, res: Response, _: NextFunction): void => { console.error('Error caught in middleware:', err); res.status(500).json( @@ -220,14 +33,6 @@ app.use((err: Error, req: Request, res: Response, _: NextFunction): void => { }); app.listen(PORT, async () => { - console.log(`Server running on http://localhost:${PORT}`); - - // Initial fetch of all leaderboards - for (const leaderboardId of config.leaderboardIds) { - try { - await hitEndpoint(leaderboardId); - } catch { - console.error(`Failed to fetch initial data for leaderboard ${leaderboardId}`); - } - } + console.log(`Server running on port ${PORT}`); + await leaderboardService.initializeCache(); }); diff --git a/aoc-backend/src/services/leaderboardCache.ts b/aoc-backend/src/services/leaderboardCache.ts new file mode 100644 index 0000000..9c2fbb2 --- /dev/null +++ b/aoc-backend/src/services/leaderboardCache.ts @@ -0,0 +1,24 @@ +import { LeaderboardData } from '../types/leaderboard'; + +export class LeaderboardCacheEntry { + constructor( + public data: LeaderboardData, + public lastFetchTime: number = Date.now() + ) {} + + isValid(cacheDuration: number): boolean { + return Date.now() - this.lastFetchTime < cacheDuration; + } +} + +export class LeaderboardCache { + private cache = new Map(); + + get(leaderboardId: string): LeaderboardCacheEntry | undefined { + return this.cache.get(leaderboardId); + } + + set(leaderboardId: string, data: LeaderboardData): void { + this.cache.set(leaderboardId, new LeaderboardCacheEntry(data)); + } +} diff --git a/aoc-backend/src/services/leaderboardService.ts b/aoc-backend/src/services/leaderboardService.ts new file mode 100644 index 0000000..c04055a --- /dev/null +++ b/aoc-backend/src/services/leaderboardService.ts @@ -0,0 +1,85 @@ +import axios, { AxiosError } from 'axios'; +import { Config, RawLeaderboardData, FilteredLeaderboardData, LeaderboardData } from '../types/leaderboard'; +import { LeaderboardCache } from './leaderboardCache'; + +export class LeaderboardService { + private cache: LeaderboardCache; + + constructor( + private config: Config, + private cacheDuration: number + ) { + this.cache = new LeaderboardCache(); + } + + private filterLeaderboardData(data: RawLeaderboardData): FilteredLeaderboardData { + const members = Object.values(data.members).map(member => ({ + id: member.id, + name: member.name, + local_score: member.local_score, + completion_day_level: member.completion_day_level + })); + + return { members }; + } + + private async fetchData(leaderboardId: string): Promise { + const url = `https://adventofcode.com/${this.config.year}/leaderboard/private/view/${leaderboardId}.json`; + + const response = await axios.get(url, { + headers: { + Cookie: `session=${this.config.sessionCookie}` + }, + }); + + if (response.data != null && response.data.toString().includes('')) { + throw new Error('REDIRECT_ERROR: Received HTML instead of JSON - likely redirected to login page'); + } + + return response.data; + } + + private async hitEndpoint(leaderboardId: string): Promise { + try { + const data = await this.fetchData(leaderboardId); + const filteredData = this.filterLeaderboardData(data); + this.cache.set(leaderboardId, filteredData); + console.log(`Leaderboard ${leaderboardId} data fetched successfully at ${new Date().toISOString()}`); + return filteredData; + } catch (error: unknown) { + this.handleError(error, leaderboardId); + throw error; + } + } + + private handleError(error: unknown, leaderboardId: string): void { + if (error instanceof Error && error.message?.includes('REDIRECT_ERROR')) { + console.error('Session likely expired:', error.message); + } else if (error instanceof AxiosError && error.response) { + console.error("Response status:", error.response.status); + console.error("Response headers:", error.response.headers); + } else { + console.error(`Error fetching leaderboard ${leaderboardId} data:`, (error as Error).message); + } + } + + async getLeaderboardData(leaderboardId: string): Promise { + const cachedEntry = this.cache.get(leaderboardId); + + if (cachedEntry && cachedEntry.isValid(this.cacheDuration)) { + return cachedEntry.data; + } + + return this.hitEndpoint(leaderboardId); + } + + async initializeCache(): Promise { + for (const leaderboardId of this.config.leaderboardIds) { + try { + await this.hitEndpoint(leaderboardId); + } catch { + console.error(`Failed to fetch initial data for leaderboard ${leaderboardId}`); + } + } + } +} diff --git a/aoc-backend/src/types/leaderboard.ts b/aoc-backend/src/types/leaderboard.ts new file mode 100644 index 0000000..06b983c --- /dev/null +++ b/aoc-backend/src/types/leaderboard.ts @@ -0,0 +1,47 @@ +import { AxiosResponse } from 'axios'; + +export interface Config { + sessionCookie: string; + leaderboardIds: string[]; + year: string; +} + +export interface Star { + star_index: number; + get_star_ts: number; +} + +export interface DayCompletion { + "1": Star; + "2": Star; +} + +export interface CompletionDayLevel { + [day: string]: DayCompletion; +} + +export type RawMember = Member +export interface Member { + id: string; + name: string; + local_score: number; + completion_day_level: CompletionDayLevel; +} + +export interface RawLeaderboardData { + members: { [key: string]: RawMember }; + // OTHER FIELDS IGNORED +} + +export type LeaderboardData = FilteredLeaderboardData; +export interface FilteredLeaderboardData { + members: Member[]; +} + +export interface LeaderboardResult { + id: string; + data: FilteredLeaderboardData | null; + error: string | null; +} + +export type LeaderboardResponse = AxiosResponse; From 122838f68b23b4cfd88900d4235d8eeb9d0a6be2 Mon Sep 17 00:00:00 2001 From: Supermarcel10 Date: Sun, 1 Dec 2024 22:47:47 +0000 Subject: [PATCH 6/6] - PR bump --- aoc-backend/src/config/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/aoc-backend/src/config/config.ts b/aoc-backend/src/config/config.ts index 3b22255..761df73 100644 --- a/aoc-backend/src/config/config.ts +++ b/aoc-backend/src/config/config.ts @@ -25,3 +25,4 @@ export function loadConfig(): Config { export const CACHE_DURATION = (process.env.CACHE_DURATION as unknown as number || 15) * 60 * 1000; export const PORT = process.env.PORT || 3000; +