diff --git a/README.md b/README.md index 19a0670e1..4b5375c8f 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,103 @@ BlueWave uptime monitoring application 1. Change directory to the `Server` directory 2. Install all depencies by running `npm install` +--- + +#### Configuration + +Configure the server with the following environmental variables + +| ENV Varialbe Name | Required/Optional | Type | Description | +| -------------------- | ----------------- | ------ | --------------------------------- | +| DB_CONNECTION_STRING | Required | string | Specfies URL for MongoDB Database | + +--- + #### Starting the Development Server 1. run `npm run dev` to start the development server +--- + +#### Endpoints + +All endpoints return a response in this format: + +| Name | Type | Notes | +| ------- | --------- | ----------------------------- | +| success | `boolean` | Success or failure of request | +| msg | `string` | Message describing response | +| data | `Object` | Arbitrary Payload | + +Example: + +``` +{success: true, msg: "Successful Request", data: {test: testData}} +``` + +##### Data Types + +###### Monitor + +| Name | Type | Notes | +| ----------- | --------- | ---------------------------------------- | +| userId | `string` | Unique ID identifying monitor creator | +| name | `string` | Name of the monitor | +| description | `string` | Description of the monitor | +| url | `string` | Url the monitor will ping | +| isActive | `boolean` | Whether or not the monitor is active | +| interval | `integer` | Interval with which to ping monitor (ms) | +| updatedAt | `Date` | Last time the monitor was updated | +| CreatedAt | `Date` | When the monitor was updated | + +--- + +##### GET /api/v1/monitors + +###### Response + +| Status Code | Type | Description | +| ----------- | --------------------- | -------------------------------- | +| 200 | Response with Payload | Response with a list of monitors | + +###### Payload + +| Type | Notes | +| ---------------- | ----------------- | +| `Array[Monitor]` | Array of monitors | + +--- + +##### GET /api/v1/monitor/:id + +###### Response + +| Status Code | Type | Description | +| ----------- | --------------------- | ------------------------------ | +| 200 | Response with Payload | Response with a single monitor | + +###### Payload + +| Type | Notes | +| --------- | --------------------------------------------------- | +| `Monitor` | Single monitor with the id in the request parameter | + +--- + +##### GET /api/v1/monitors/user/:userId + +| Status Code | Type | Description | +| ----------- | --------------------- | -------------------------------- | +| 200 | Response with Payload | Response with a list of monitors | + +###### Payload + +| Type | Notes | +| ---------------- | ------------------------------------------------------------------ | +| `Array[Monitor]` | Array of monitors created by user with userId specified in request | + ## Contributors + diff --git a/Server/configs/db.js b/Server/configs/db.js index 174978ca3..ba3c8b660 100644 --- a/Server/configs/db.js +++ b/Server/configs/db.js @@ -1,18 +1,15 @@ -const mongoose = require('mongoose') +const PORT = process.env.PORT || 5000; -const PORT = process.env.PORT || 5000 +const connectDbAndRunServer = async (app, db) => { + try { + await db.connect(); + app.listen(PORT, () => { + console.log(`server started on port:${PORT}`); + }); + } catch (error) { + console.log("Failed to connect to DB"); + console.error(error); + } +}; -const connectDbAndRunServer = async (app) => { - await mongoose.connect(process.env.DB_CONNECTION_STRING).then(() => { - console.log("DB connected") - // run server - app.listen(PORT, () => { - console.log(`server started on port:${PORT}`) - }) - }).catch((err) => { - console.log(err) - }) -} - - -module.exports = { connectDbAndRunServer } \ No newline at end of file +module.exports = { connectDbAndRunServer }; diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js new file mode 100644 index 000000000..dda18d8ff --- /dev/null +++ b/Server/controllers/monitorController.js @@ -0,0 +1,56 @@ +const { + getMonitorsByIdValidation, + getMonitorsByUserIdValidation, +} = require("../validation/joi"); + +// Gets all monitors +const getAllMonitors = async (req, res) => { + try { + const monitors = await req.db.getAllMonitors(); + return res.json({ success: true, msg: "Monitors found", data: monitors }); + } catch (error) { + return res.status(500).json({ success: false, msg: error.message }); + } +}; + +// Get a monitor by ID +const getMonitorById = async (req, res) => { + const { error } = getMonitorsByIdValidation.validate(req.params); + if (error) { + return res + .status(400) + .json({ success: false, msg: error.details[0].message }); + } + + try { + const monitorId = req.params.monitorId; + const monitor = await req.db.getMonitorById(monitorId); + return res.json({ success: true, msg: "Monitor found", data: monitor }); + } catch (error) { + return res.status(500).json({ success: false, msg: error.message }); + } +}; + +// Gets a monitor by user ID +const getMonitorsByUserId = async (req, res) => { + const { error } = getMonitorsByUserIdValidation.validate(req.params); + if (error) { + return res + .status(400) + .json({ success: false, msg: error.details[0].message }); + } + + try { + const userId = req.params.userId; + const monitors = await req.db.getMonitorsByUserId(userId); + return res.json({ + success: true, + msg: `Monitors for user ${userId} found`, + data: monitors, + }); + } catch (error) { + return res.status(500).json({ success: false, msg: error.message }); + } +}; + +module.exports = { getAllMonitors, getMonitorById, getMonitorsByUserId }; diff --git a/Server/db/FakeDb.js b/Server/db/FakeDb.js new file mode 100644 index 000000000..6dfa89ac9 --- /dev/null +++ b/Server/db/FakeDb.js @@ -0,0 +1,81 @@ +// ************************** +// The idea here is to provide a layer of abstraction between the database and whoever is using it. +// Instead of directly calling mongoose methods, we can call the methods on the DB object. +// If this were Typescript or Java or Golang an interface would be implemented to ensure the methods are available. +// But we do the best we can with Javascript. +// +// If the methods are consistent all we have to do to swap out one DB for another is simply change the import. +// +// Example: +// We start with the fake DB: +// +// const db = require("../db/FakeDb"); +// const monitors = await db.getAllMonitors(); +// +// And when we want to swtich to a real DB, all we have to do is swap the import +// +// const db = require("../db/MongoDb"); +// const monitors = await db.getAllMonitors(); +// +// The rest of the code is the same, as all the `db` methods are standardized. +// ************************** + +const Monitor = require("../models/Monitor"); + +const FAKE_MONITOR_DATA = []; + +for (let i = 0; i < 10; i++) { + FAKE_MONITOR_DATA.push( + new Monitor({ + userId: i % 2 === 0 ? 1 : 2, + name: `Monitor ${i}`, + description: `Description for Monitor ${i}`, + url: `https://monitor${i}.com`, + isActive: true, + interval: 60000, + updated_at: new Date(), + created_at: new Date(), + }) + ); +} + +const connect = async () => { + try { + await console.log("Connected to FakeDB"); + } catch (error) { + console.error(error); + } +}; + +const getAllMonitors = async () => { + return FAKE_MONITOR_DATA; +}; + +const getMonitorById = async (monitorId) => { + const idx = FAKE_MONITOR_DATA.findIndex((monitor) => { + return monitor.id === monitorId; + }); + if (idx === -1) { + throw new Error(`Monitor with id ${monitorId} not found`); + } + return FAKE_MONITOR_DATA[idx]; +}; + +const getMonitorsByUserId = async (userId) => { + const userMonitors = FAKE_MONITOR_DATA.filter((monitor) => { + return monitor.userId === userId; + }); + + if (userMonitors.length === 0) { + throw new Error(`Monitors for user ${userId} not found`); + } + + return userMonitors; +}; + +module.exports = { + connect, + getAllMonitors, + getMonitorById, + getMonitorsByUserId, +}; diff --git a/Server/db/MongoDB.js b/Server/db/MongoDB.js new file mode 100644 index 000000000..6ad783877 --- /dev/null +++ b/Server/db/MongoDB.js @@ -0,0 +1,49 @@ +const Monitor = require("../models/Monitor"); +const mongoose = require("mongoose"); + +const connect = async () => { + try { + await mongoose.connect(process.env.DB_CONNECTION_STRING); + console.log("Connected to MongoDB"); + } catch (error) { + console.error("Failed to connect to MongoDB"); + throw error; + } +}; + +// Gets all monitors +const getAllMonitors = async (req, res) => { + try { + const monitors = await Monitor.find(); + return monitors; + } catch (error) { + throw error; + } +}; + +// Get a monitor by ID +const getMonitorById = async (req, res) => { + try { + const monitor = await Monitor.findById(req.params.monitorId); + return monitor; + } catch (error) { + throw error; + } +}; + +// Gets a monitor by user ID +const getMonitorsByUserId = async (req, res) => { + try { + const monitors = await Monitor.find({ userId: req.params.userId }); + return monitors; + } catch (error) { + throw error; + } +}; + +module.exports = { + connect, + getAllMonitors, + getMonitorById, + getMonitorsByUserId, +}; diff --git a/Server/index.js b/Server/index.js index 173c2c78d..ef8cdc982 100644 --- a/Server/index.js +++ b/Server/index.js @@ -1,28 +1,55 @@ -const express = require('express') -const helmet = require('helmet') -const cors = require('cors') -const authRouter = require('./routes/authRoute') -const { connectDbAndRunServer } = require('./configs/db') -require('dotenv').config() +const express = require("express"); +const helmet = require("helmet"); +const cors = require("cors"); +const authRouter = require("./routes/authRoute"); +const monitorRouter = require("./routes/monitorRoute"); +const { connectDbAndRunServer } = require("./configs/db"); +require("dotenv").config(); // const { sendEmail } = require('./utils/sendEmail') +// ************************** +// Here is where we can swap out DBs easily. Spin up a mongoDB instance and try it out. +// Simply comment out the FakeDB and uncomment the MongoDB or vice versa. +// We can easily swap between any type of data source as long as the methods are implemented +// +// FakeDB +// const db = require("./db/FakeDb"); +// +// MongoDB +const db = require("./db/MongoDB"); +// +// ************************** + /** * NOTES - * Email Service will be added + * Email Service will be added * Logger Service will be added (Winston or similar) */ -const app = express() +const app = express(); // middlewares -app.use(cors( - //We will add configuration later -)) -app.use(express.json()) -app.use(helmet()) +app.use( + cors() + //We will add configuration later +); +app.use(express.json()); +app.use(helmet()); + +// ************************** +// Make DB accessible anywhere we have a Request object +// By adding the DB to the request object, we can access it in any route +// Thus we do not need to import it in every route file, and we can easily swap out DBs as there is only one place to change it +// ************************** +app.use((req, res, next) => { + req.db = db; + next(); +}); //routes -app.use('/api/v1/auth', authRouter); +app.use("/api/v1/auth", authRouter); + +app.use("/api/v1/monitors", monitorRouter); // Testing email service // app.use('/sendEmail', async (req, res) => { @@ -31,12 +58,12 @@ app.use('/api/v1/auth', authRouter); // }) //health check -app.use('/api/v1/healthy', (req, res) => { - try { - return res.status(200).json({message:"Healthy"}) - } catch (error) { - return res.status(500).json({message:error.message}) - } -}) +app.use("/api/v1/healthy", (req, res) => { + try { + return res.status(200).json({ message: "Healthy" }); + } catch (error) { + return res.status(500).json({ message: error.message }); + } +}); -connectDbAndRunServer(app); +connectDbAndRunServer(app, db); diff --git a/Server/models/Monitor.js b/Server/models/Monitor.js new file mode 100644 index 000000000..513d4e2be --- /dev/null +++ b/Server/models/Monitor.js @@ -0,0 +1,37 @@ +const mongoose = require("mongoose"); + +const MonitorSchema = mongoose.Schema({ + userId: { + type: String, + }, + name: { + type: String, + required: true, + }, + description: { + type: String, + }, + url: { + type: String, + required: true, + }, + isActive: { + type: Boolean, + default: true, + }, + interval: { + // in milliseconds + type: Number, + default: 60000, + }, + updated_at: { + type: Date, + default: Date.now, + }, + created_at: { + type: Date, + default: Date.now, + }, +}); + +module.exports = mongoose.model("Monitor", MonitorSchema); diff --git a/Server/routes/monitorRoute.js b/Server/routes/monitorRoute.js new file mode 100644 index 000000000..e12982905 --- /dev/null +++ b/Server/routes/monitorRoute.js @@ -0,0 +1,7 @@ +const router = require("express").Router(); +const monitorController = require("../controllers/monitorController"); +router.get("/", monitorController.getAllMonitors); +router.get("/:monitorId", monitorController.getMonitorById); +router.get("/user/:userId", monitorController.getMonitorsByUserId); + +module.exports = router; diff --git a/Server/validation/joi.js b/Server/validation/joi.js index d28f9b7da..2fb59a377 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -1,10 +1,21 @@ -const joi = require('joi') - +const joi = require("joi"); +const user = require("../models/user"); const authValidation = joi.object({ - email: joi.string().email().required(), - password: joi.string().min(8).required(), + email: joi.string().email().required(), + password: joi.string().min(8).required(), }); +const getMonitorsByIdValidation = joi.object({ + monitorId: joi.string().required(), +}); -module.exports = {authValidation} \ No newline at end of file +const getMonitorsByUserIdValidation = joi.object({ + userId: joi.string().required(), +}); + +module.exports = { + authValidation, + getMonitorsByIdValidation, + getMonitorsByUserIdValidation, +};