diff --git a/package-lock.json b/package-lock.json index 648c072..e29e802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jetsetradio-api", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 8fcbb8a..bc5517e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jetsetradio-api", - "version": "1.1.2", + "version": "1.1.3", "description": "A Data Provider relating to the JSR/JSRF universe", "type": "module", "main": "src/app.js", diff --git a/src/app.js b/src/app.js index 11e3e25..c4e8865 100644 --- a/src/app.js +++ b/src/app.js @@ -4,6 +4,7 @@ import dotenv from "dotenv"; dotenv.config(); import LOGGER from "./utils/logger.js"; +import {connectToDb} from "./config/db.js"; import MiddlewareManager from "./managers/MiddlewareManager.js"; const middlewareManager = new MiddlewareManager(); @@ -14,12 +15,15 @@ const baseUrl = process.env.BASE_URL; middlewareManager.setMiddleware(app); -app.listen(PORT || 8080, () => { - LOGGER.info(`JSR-API Listening on port ${PORT}`); +(async () => { + await connectToDb(); + app.listen(PORT || 8080, () => { + LOGGER.info(`JSR-API Listening on port ${PORT}`); - // Ping App every 10 minutes - setInterval(async () => { - const res = await axios.get(`${baseUrl}/health`); - console.log(`App Ping - ${baseUrl}. Status: ${res.data.message}`); - }, 600000); -}); + // Ping App every 10 minutes + setInterval(async () => { + const res = await axios.get(`${baseUrl}/health`); + console.log(`App Ping - ${baseUrl}. Status: ${res.data.message}`); + }, 600000); + }); +})(); diff --git a/src/config/db.js b/src/config/db.js index 0771db6..6e19df9 100644 --- a/src/config/db.js +++ b/src/config/db.js @@ -3,51 +3,43 @@ import {ObjectId} from "mongodb"; import dotenv from "dotenv"; dotenv.config(); -import LOGGER from "../utils/logger.js"; import Constants from "../constants/dbConstants.js"; +import LOGGER from "../utils/logger.js"; const {CORE_DB} = Constants; -const buildMongoUri = () => { - const user = process.env.MONGO_USER; - const password = process.env.MONGO_PASS; - const clusterName = process.env.MONGO_CLUSTER; - const domainName = process.env.MONGO_DOMAIN; - if (!user) { - return LOGGER.error(`Invalid admin user found while building mongo uri`); - } - if (!password) { - return LOGGER.error( - `Invalid admin password found while building mongo uri` - ); - } - if (!clusterName) { - return LOGGER.error(`Invalid cluster name found while building mongo uri`); +/* Database Connections */ +const client = new MongoClient(process.env.MONGO_URI); +let isConnected = false; + +export const connectToDb = async () => { + if (isConnected) { + return client; // reuse existing connection } - if (!domainName) { - return LOGGER.error(`Invalid domain name found while building mongo uri`); + try { + await client.connect(); + isConnected = true; + LOGGER.info("✅ Connected to MongoDB"); + return client; + } catch (err) { + LOGGER.error("❌ Failed to connect to MongoDB", err); + throw err; } - return `mongodb+srv://${user}:${password}@${clusterName}.${domainName}?retryWrites=true&w=majority`; }; -const client = new MongoClient(buildMongoUri()); - -/* Database Connections */ export const performAdminAction = async (action, username) => { try { - await client.connect(); + await connectToDb(); return await action(client, CORE_DB, "Admin", username); } catch (err) { console.error(err); return err; - } finally { - await client.close(); } }; export const performDBAction = async (action, dbName, collection, id, qps) => { try { - await client.connect(); + await connectToDb(); const queryActions = [getSortQuery(qps), getLimitSize(qps)]; return await action( client, @@ -60,20 +52,16 @@ export const performDBAction = async (action, dbName, collection, id, qps) => { } catch (err) { console.error(err); return err; - } finally { - await client.close(); } }; export const listCollections = async (dbName) => { try { - await client.connect(); + await connectToDb(); return await client.db(dbName).listCollections().toArray(); } catch (err) { console.error(err); return err; - } finally { - await client.close(); } }; diff --git a/src/config/dbActions.js b/src/config/dbActions.js index 9a92442..39ff93c 100644 --- a/src/config/dbActions.js +++ b/src/config/dbActions.js @@ -6,5 +6,5 @@ export const Actions = { fetchWithQuery: async (client, dbName, collectionName, id, qps, queryActions) => { return await client.db(dbName).collection(collectionName).find(qps).sort(queryActions[0]).limit(queryActions[1]).toArray() }, fetchById: async (client, dbName, collectionName, id) => { return await client.db(dbName).collection(collectionName).findOne({ _id: new ObjectId(id) }) }, fetchAdmin: async (client, dbName, collectionName, username) => { return await client.db(dbName).collection(collectionName).findOne({ username: username }) }, - fetchRandom: async (client, dbName, collectionName) => { return await client.db(dbName).collection(collectionName).aggregate([{ $sample: { size: 1 } }]).toArray(); } + fetchRandom: async (client, dbName, collectionName, count) => { return await client.db(dbName).collection(collectionName).aggregate([{ $sample: { size: count } }]).toArray(); } } diff --git a/src/controllers/characterController.js b/src/controllers/characterController.js index ae0b18f..80136ab 100644 --- a/src/controllers/characterController.js +++ b/src/controllers/characterController.js @@ -2,10 +2,11 @@ import Constants from "../constants/dbConstants.js"; import {Actions} from "../config/dbActions.js"; import {performDBAction} from "../config/db.js"; import {sortObjects} from "../utils/utility.js"; +import {fetchRandom} from "./utilController.js"; import LOGGER from "../utils/logger.js"; const Character = "Character"; -const {JSR_DB, JSRF_DB, BRC_DB, gameMap} = Constants; +const {JSR_DB, JSRF_DB, BRC_DB} = Constants; export const getAllCharacters = async (req, res) => { try { @@ -24,13 +25,7 @@ export const getAllCharacters = async (req, res) => { export const getRandomCharacter = async (req, res) => { try { - const games = [JSR_DB, JSRF_DB, BRC_DB]; - const userSelectedGame = req?.query?.game; - let game = - gameMap[userSelectedGame] || - games[Math.floor(Math.random() * games.length)]; - const randomCharacter = await fetchRandomCharacter(req, game); - res.json(randomCharacter[0]); + res.send(await fetchRandom(req, Character)); } catch (err) { LOGGER.error(`Could not fetch random character`, err); res.status(500).json({error: "Failed to fetch random character"}); @@ -130,13 +125,3 @@ export const fetchCharacters = async (req, dbName) => { req?.query ); }; - -export const fetchRandomCharacter = async (req, dbName) => { - return await performDBAction( - Actions.fetchRandom, - dbName, - Character, - null, - req?.query - ); -}; diff --git a/src/controllers/collectibleController.js b/src/controllers/collectibleController.js index d63653f..3aea8d3 100644 --- a/src/controllers/collectibleController.js +++ b/src/controllers/collectibleController.js @@ -3,6 +3,7 @@ import {Actions} from "../config/dbActions.js"; import {performDBAction} from "../config/db.js"; import {sortObjects} from "../utils/utility.js"; import LOGGER from "../utils/logger.js"; +import {fetchRandom} from "./utilController.js"; const Collectible = "Collectible"; const {BRC_DB} = Constants; @@ -27,8 +28,7 @@ export const getCollectibles = async (req, res) => { export const getRandomCollectible = async (req, res) => { try { - const randomCollectible = await fetchRandomCollectible(req, BRC_DB); - res.json(randomCollectible[0]); + res.send(await fetchRandom(req, Collectible, BRC_DB)); } catch (err) { LOGGER.error(`Could not fetch random collectible`, err); res.status(500).json({error: "Failed to fetch random collectible"}); @@ -60,12 +60,6 @@ export const fetchCollectibles = async (req) => { return await performDBAction(Actions.fetchAll, BRC_DB, Collectible, null); }; -export const fetchRandomCollectible = async (req, dbName) => { - return await performDBAction( - Actions.fetchRandom, - dbName, - Collectible, - null, - req?.query - ); +export const fetchRandomCollectible = async (req, dbName, count) => { + return await performDBAction(Actions.fetchRandom, dbName, Collectible, count); }; diff --git a/src/controllers/locationController.js b/src/controllers/locationController.js index 7330728..d8dca7a 100644 --- a/src/controllers/locationController.js +++ b/src/controllers/locationController.js @@ -2,11 +2,12 @@ import Constants from "../constants/dbConstants.js"; import {Actions} from "../config/dbActions.js"; import {performDBAction} from "../config/db.js"; import {sortObjects} from "../utils/utility.js"; +import {fetchRandom} from "./utilController.js"; import LOGGER from "../utils/logger.js"; const Location = "Location"; const Level = "Level"; -const {JSR_DB, JSRF_DB, BRC_DB, gameMap} = Constants; +const {JSR_DB, JSRF_DB, BRC_DB} = Constants; export const getLocations = async (req, res) => { try { @@ -25,13 +26,7 @@ export const getLocations = async (req, res) => { export const getRandomLocation = async (req, res) => { try { - const games = [JSR_DB, JSRF_DB, BRC_DB]; - const userSelectedGame = req?.query?.game; - let game = - gameMap[userSelectedGame] || - games[Math.floor(Math.random() * games.length)]; - const randomLocation = await fetchRandomLocation(req, game); - res.json(randomLocation[0]); + res.send(await fetchRandom(req, Location)); } catch (err) { LOGGER.error(`Could not fetch random location`, err); res.status(500).json({error: "Failed to fetch random location"}); @@ -152,13 +147,3 @@ export const fetchLocations = async (req, dbName) => { req?.query ); }; - -export const fetchRandomLocation = async (req, dbName) => { - return await performDBAction( - Actions.fetchRandom, - dbName, - Location, - null, - req?.query - ); -}; diff --git a/src/controllers/songController.js b/src/controllers/songController.js index a2a2cbf..0da7244 100644 --- a/src/controllers/songController.js +++ b/src/controllers/songController.js @@ -2,10 +2,11 @@ import Constants from "../constants/dbConstants.js"; import {Actions} from "../config/dbActions.js"; import {performDBAction} from "../config/db.js"; import {sortObjects} from "../utils/utility.js"; +import {fetchRandom} from "./utilController.js"; import LOGGER from "../utils/logger.js"; const Song = "Song"; -const {JSR_DB, JSRF_DB, BRC_DB, gameMap} = Constants; +const {JSR_DB, JSRF_DB, BRC_DB} = Constants; export const getSongs = async (req, res) => { try { @@ -24,13 +25,7 @@ export const getSongs = async (req, res) => { export const getRandomSong = async (req, res) => { try { - const games = [JSR_DB, JSRF_DB, BRC_DB]; - const userSelectedGame = req?.query?.game; - let game = - gameMap[userSelectedGame] || - games[Math.floor(Math.random() * games.length)]; - const randomSong = await fetchRandomSong(req, game); - res.json(randomSong[0]); + res.send(await fetchRandom(req, Song)); } catch (err) { LOGGER.error(`Could not fetch random song`, err); res.status(500).json({error: "Failed to fetch random song"}); @@ -152,13 +147,3 @@ export const fetchSongs = async (req, dbName) => { req?.query ); }; - -export const fetchRandomSong = async (req, dbName) => { - return await performDBAction( - Actions.fetchRandom, - dbName, - Song, - null, - req?.query - ); -}; diff --git a/src/controllers/utilController.js b/src/controllers/utilController.js new file mode 100644 index 0000000..011e7b2 --- /dev/null +++ b/src/controllers/utilController.js @@ -0,0 +1,50 @@ +import {performDBAction} from "../config/db.js"; +import {Actions} from "../config/dbActions.js"; +import Constants from "../constants/dbConstants.js"; +import LOGGER from "../utils/logger.js"; + +const {JSR_DB, JSRF_DB, BRC_DB, gameMap} = Constants; + +/* Helper Functions to support all other Controllers */ +export const fetchRandom = async (req, resource, game) => { + try { + const games = [JSR_DB, JSRF_DB, BRC_DB]; + const selectedGame = req?.query?.game; + const count = Number(req?.query?.count); + const safeCount = Number.isFinite(count) && count > 0 ? count : 1; + + /* if a game is provided */ + if (game || selectedGame) { + const dbName = game || gameMap[selectedGame]; + return await performDBAction( + Actions.fetchRandom, + dbName, + resource, + safeCount + ); + } + + /* If no game is provided, select a random characters from a random games */ + const shuffled = [...games].sort(() => Math.random() - 0.5); + let remaining = safeCount; + const promises = []; + + for (const dbName of shuffled) { + if (remaining <= 0) break; + const take = Math.min( + remaining, + Math.floor(Math.random() * remaining) + 1 + ); + remaining -= take; + promises.push( + performDBAction(Actions.fetchRandom, dbName, resource, take) + ); + } + + const results = await Promise.all(promises); + return results.flat(); + } catch (err) { + LOGGER.error(`Error fetching random ${resource} from game ${game}`, err); + return []; + } +}; diff --git a/src/public/docs.html b/src/public/docs.html index 97998a0..8347bc0 100644 --- a/src/public/docs.html +++ b/src/public/docs.html @@ -23,49 +23,14 @@

Jet Set Radio API
Documentation

Introduction

Welcome to the jsrapi, the Jet Set Radio API! This documentation should help get you familiar with which resources are available and how to consume them with HTTP requests.

-
- -
-

Base URL

-

The Base URL is the root URL for all of the API. If you ever get an error performing a request check the BaseURL first.

-

The Base URL is:

- https://jetsetradio-api.onrender.com/v1/api/ -
- -
-

Authentication

-

Jsrapi is a completely open API. No authentication is required. This means you are only able to do GET requests. If you find a mistake in the data, then send an email to jetsetradio.api@gmail.com or post on issue on the github page.

-
- -
-

Rate Limiting

-

A request limit has been set to avoid malicious abuse(if anyone were to do that!). Limiting is set per IP address. You can make a total of up to 1000 requests per hour.

-
- -
-

Sorting

-

All API routes support the sortBy and orderBy parameters. If orderBy is omitted, it will assume an ascending direction.

-

Example:

- https://jetsetradio-api.onrender.com/v1/api/characters/jsr?sortBy=name&orderBy=desc -
- -
-

Limiting

-

All API routes support the limit parameter. You can use this to return a limited number of results instead of the entire response.

-

Example:

- https://jetsetradio-api.onrender.com/v1/api/locations/jsrf?limit=5 -
- -
-

Caching

-

All API routes utilize cache in memory to be able to serve content faster! The cache is automatically set to expire after 1 hour. The expiration is subject to future change.

+

More info on rate limiting, caching, and other info can be found near the bottom of the page.

Swagger Docs

-

You can use the swagger docs to view the available endpoints, see their parameters, and test the routes out!

-
- +

You can also use the swagger docs to view the available endpoints, see their parameters, and test the routes out!

+ + ___________________________________________________________

Resources

@@ -118,6 +83,7 @@

Characters

  • /characters/random?game=jsr ==> Returns a Random JSR Character
  • /characters/random?game=jsrf ==> Returns a Random JSRF Character
  • /characters/random?game=brc ==> Returns a Random BRC Character
  • +
  • /characters/random?count=10 ==> Returns 10 random Characters
  • /characters/jsr ==> Returns all Jet Set Radio Characters
  • /characters/jsr/:id ==> Returns a single JSR Character by ID
  • /characters/jsrf ==> Returns all Jet Set Radio Future Characters
  • @@ -141,6 +107,7 @@

    Locations

  • /locations/random?game=jsr ==> Returns a Random JSR Location
  • /locations/random?game=jsrf ==> Returns a Random JSRF Location
  • /locations/random?game=brc ==> Returns a Random BRC Location
  • +
  • /locations/random?count=10 ==> Returns 10 random Locations
  • /locations/jsr ==> Returns all Jet Set Radio Locations
  • /locations/jsr/:id ==> Returns a single JSR Location by ID
  • /locations/jsrf ==> Returns all Jet Set Radio Future Locations
  • @@ -160,7 +127,7 @@

    Levels

    Endpoints:

    Example Request:

    https://jetsetradio-api.onrender.com/v1/api/levels?sortBy=chapter @@ -197,6 +164,7 @@

    Songs

  • /songs/random?game=jsr ==> Returns a Random JSR Song
  • /songs/random?game=jsrf ==> Returns a Random JSRF Song
  • /songs/random?game=brc ==> Returns a Random BRC Song
  • +
  • /songs/random?count=10&game=jsrf ==> Returns 10 random JSRF Songs
  • /songs/jsr ==> Returns all Jet Set Radio Songs
  • /songs/jsr/:id ==> Returns a single JSR Song by ID
  • /songs/jsrf ==> Returns all Jet Set Radio Future Songs
  • @@ -232,6 +200,7 @@

    Collectibles

    + +
    +

    Base URL

    +

    The Base URL is the root URL for all of the API. If you ever get an error performing a request check the BaseURL first.

    +

    The Base URL is:

    + https://jetsetradio-api.onrender.com/v1/api/ +
    + +
    +

    Authentication

    +

    Jsrapi is a completely open API. No authentication is required. This means you are only able to do GET requests. If you find a mistake in the data, then send an email to jetsetradio.api@gmail.com or post on issue on the github page.

    +
    + +
    +

    Rate Limiting

    +

    A request limit has been set to avoid malicious abuse(if anyone were to do that!). Limiting is set per IP address. You can make a total of up to 1000 requests per hour.

    +
    + +
    +

    Sorting

    +

    All API routes support the sortBy and orderBy parameters. If orderBy is omitted, it will assume an ascending direction.

    +

    Example:

    + https://jetsetradio-api.onrender.com/v1/api/characters/jsr?sortBy=name&orderBy=desc +
    + +
    +

    Limiting

    +

    All API routes support the limit parameter. You can use this to return a limited number of results instead of the entire response.

    +

    Example:

    + https://jetsetradio-api.onrender.com/v1/api/locations/jsrf?limit=5 +
    + +
    +

    Caching

    +

    All API routes utilize cache in memory to be able to serve content faster! The cache is automatically set to expire after 1 hour. The expiration is subject to future change.

    +
    diff --git a/src/public/index.html b/src/public/index.html index b523e27..a2981b0 100644 --- a/src/public/index.html +++ b/src/public/index.html @@ -22,6 +22,7 @@

    Jet Set Radio API

    Documentation + Swagger Docs

    JSRAPI

    diff --git a/src/public/style/index.css b/src/public/style/index.css index aef688b..56bef00 100644 --- a/src/public/style/index.css +++ b/src/public/style/index.css @@ -50,7 +50,7 @@ a { font-size: 5.5vw; } -#documentation { +#documentation, #swagger { position: absolute; right: 3%; top: 3%; @@ -61,6 +61,10 @@ a { font-size: calc(8px + 1vw); } +#swagger { + top: 14%; +} + #main { font-family: jetSet; display: flex; diff --git a/src/utils/swagger-docs.json b/src/utils/swagger-docs.json index 03cf9ff..d85c14c 100644 --- a/src/utils/swagger-docs.json +++ b/src/utils/swagger-docs.json @@ -2,7 +2,7 @@ "swagger": "2.0", "info": { "title": "JetSetRadio-API", - "version": "1.1.1", + "version": "1.1.3", "description": "Providing data for all things JSR and JSRF!" }, "host": "localhost:9005",