diff --git a/templates/basic/package.json b/templates/basic/package.json index 84fa255c..5c046f9a 100644 --- a/templates/basic/package.json +++ b/templates/basic/package.json @@ -12,12 +12,12 @@ "author": "", "license": "ISC", "dependencies": { - "express": "^4.17.1", - "dotenv": "^16.4.7" + "dotenv": "^16.4.7", + "express": "^4.17.1" }, "devDependencies": { - "nodemon": "^3.1.9", "jest": "^29.7.0", + "nodemon": "^3.1.9", "supertest": "^7.0.0" } } diff --git a/templates/express_mysql/app.js b/templates/express_mysql/app.js new file mode 100644 index 00000000..f8ea6e11 --- /dev/null +++ b/templates/express_mysql/app.js @@ -0,0 +1,23 @@ +import express, { json } from "express"; +import helmet from "helmet"; +import cors from "cors"; + +import sampleRouter from "./router/sampleRouter.js"; +import { appConfig } from "./config/appConfig.js"; + +export default function (database) { + const app = express(); + + app.use(helmet()); + + app.use(cors()); + app.use(json()); + + // Disable the X-Powered-By header to make it harder + // for attackers to find the tech stack. + app.disable("x-powered-by"); + + app.use(appConfig.router.SAMPLE_PREFIX, sampleRouter(database)); + + return app; +} diff --git a/templates/express_mysql/app.test.js b/templates/express_mysql/app.test.js new file mode 100644 index 00000000..9662eeb4 --- /dev/null +++ b/templates/express_mysql/app.test.js @@ -0,0 +1,50 @@ +import request from "supertest"; +import { expect, jest } from "@jest/globals"; +import makeApp from "./app.js"; + +const expectedSamples = [ + { id: 1, name: "sample1" }, + { id: 2, name: "sample2" }, +]; + +const getSamples = jest.fn().mockImplementation(async (req, res) => { + return res.status(200).send({ + MESSAGE: "Data fetched successfully.", + DATA: expectedSamples, + }); +}); + +const app = makeApp({ getSamples }); + +describe("API Endpoints", () => { + it("should return success message on GET /api/sample/test", async () => { + const res = await request(app).get("/api/sample/test"); + expect(res.statusCode).toBe(200); + expect(res.body).toEqual({ + MESSAGE: "It's Working. 👍🏻", + }); + }); + + it("should return all samples on GET /api/sample/all", async () => { + const res = await request(app).get("/api/sample/all"); + expect(getSamples.mock.calls.length).toBe(1); + expect(getSamples).toHaveBeenCalledWith( + expect.any(Object), + expect.any(Object), + ); + expect(res.statusCode).toBe(200); + expect(res.body).toHaveProperty( + "MESSAGE", + "Data fetched successfully.", + ); + expect(res.body).toHaveProperty("DATA"); + expect(Array.isArray(res.body.DATA)).toBe(true); + res.body.DATA.forEach((item) => { + expect(item).toHaveProperty("id"); + expect(item).toHaveProperty("name"); + expect(typeof item.id).toBe("number"); + expect(typeof item.name).toBe("string"); + }); + expect(res.body.DATA).toEqual(expect.arrayContaining(expectedSamples)); + }); +}); diff --git a/templates/express_mysql/connection/normalConnection.js b/templates/express_mysql/connection/normalConnection.js index e1a9ee55..f8359f71 100644 --- a/templates/express_mysql/connection/normalConnection.js +++ b/templates/express_mysql/connection/normalConnection.js @@ -6,6 +6,7 @@ const connectToDb = () => { let db = null; try { db = createConnection(appConfig.db); + db.connect(); return db; } catch (err) { const timeStamp = new Date().toLocaleString(); diff --git a/templates/express_mysql/controller/sampleController.js b/templates/express_mysql/controller/sampleController.js index 69f59cc0..074bf5f0 100644 --- a/templates/express_mysql/controller/sampleController.js +++ b/templates/express_mysql/controller/sampleController.js @@ -1,35 +1,11 @@ -import { appendFileSync } from "fs"; -import db from "../connection/poolConnection.js"; - -export async function test(_, res) { - return res.status(200).send({ - MESSAGE: "It's Working. 👍🏻", - }); -} - -export async function getAllSamples(_, res) { - const db_conn = await db.promise().getConnection(); - try { - await db_conn.query("LOCK TABLES sample_table READ"); - const [data] = await db_conn.query("SELECT * FROM sample_table"); +export default { + async test(_, res) { return res.status(200).send({ - MESSAGE: "Data fetched successfully.", - DATA: data, + MESSAGE: "It's Working. 👍🏻", }); - } catch { - const timeStamp = new Date().toLocaleString(); - const errMessage = `[ERROR]: ${timeStamp} - ${err.message}`; - console.error(errMessage); - appendFileSync( - "./logs/controller/sampleController.log", - `${errMessage}\n`, - ); + }, - return res.status(500).send({ - MESSAGE: "Something went wrong. Please try again later.", - }); - } finally { - await db_conn.query("UNLOCK TABLES"); - db_conn.release(); - } -} + async getAllSamples(_, res, database) { + await database.getSamples(_, res); + }, +}; diff --git a/templates/express_mysql/db/database.js b/templates/express_mysql/db/database.js new file mode 100644 index 00000000..f4e5dcd6 --- /dev/null +++ b/templates/express_mysql/db/database.js @@ -0,0 +1,32 @@ +import { appendFileSync } from "fs"; +import db from "../connection/poolConnection.js"; + +export default { + async getSamples(req, res) { + const db_conn = await db.promise().getConnection(); + + try { + await db_conn.query("LOCK TABLES sample_table READ"); + const [data] = await db_conn.query("SELECT * FROM sample_table"); + return res.status(200).send({ + MESSAGE: "Data fetched successfully.", + DATA: data, + }); + } catch (err) { + const timeStamp = new Date().toLocaleString(); + const errMessage = `[ERROR]: ${timeStamp} - ${err.message}`; + console.error(errMessage); + appendFileSync( + "./logs/controller/sampleController.log", + `${errMessage}\n`, + ); + + return res.status(500).send({ + MESSAGE: "Something went wrong. Please try again later.", + }); + } finally { + await db_conn.query("UNLOCK TABLES"); + db_conn.release(); + } + }, +}; diff --git a/templates/express_mysql/db/reInitDb.js b/templates/express_mysql/db/reInitDb.js index 2b9c2976..b607a615 100644 --- a/templates/express_mysql/db/reInitDb.js +++ b/templates/express_mysql/db/reInitDb.js @@ -1,36 +1,27 @@ -import { readFile, appendFileSync } from "fs"; +import { readFile } from "fs/promises"; import { appConfig } from "../config/appConfig.js"; -const reInitDb = (db) => { +const reInitDb = async (db) => { try { - readFile("./db/reInitDb.sql", "utf8", (err, data) => { - if (err) { - const timeStamp = new Date().toLocaleString(); - const errMessage = `[ERROR]: ${timeStamp} - reInitDb - ${err.message}`; - console.error(errMessage); - } else { - db.query(data, (err, _) => { - if (err) { - const timeStamp = new Date().toLocaleString(); - const errMessage = `[ERROR]: ${timeStamp} - reInitDb - ${err.message}`; - console.error(errMessage); - - if (err.message.includes("Unknown database")) { - console.warn( - `[HINT]: Database '${appConfig.db.database}' does not exist. Please create it by running the following command in your MySQL shell: 'CREATE DATABASE ${appConfig.db.database}'.`, - ); - } - } else { - console.info("[INFO]: Database re-initialized."); - } - }); - } - }); + const data = await readFile("./db/reInitDb.sql", "utf8"); + await db.promise().query(data); + console.info("[INFO]: Database re-initialized."); } catch (err) { - const timeStamp = new Date().toLocaleString(); - const errMessage = `[ERROR]: ${timeStamp} - ${err.message}`; - console.error(errMessage); - appendFileSync("./logs/db/reInitDb.log", `${errMessage}\n`); + console.error( + `[ERROR]: ${new Date().toLocaleString()} - reInitDb - ${err.message}`, + ); + if (err.message.includes("Unknown database")) { + console.warn( + `[HINT]: Database '${appConfig.db.database}' does not exist. Please create it by running the following command in your MySQL shell: 'CREATE DATABASE ${appConfig.db.database}'.`, + ); + } + } finally { + return new Promise((resolve, reject) => { + db.end((err) => { + if (err) reject(err); + else resolve(); + }); + }); } }; diff --git a/templates/express_mysql/index.js b/templates/express_mysql/index.js index 8ddca6bb..9456e9e6 100644 --- a/templates/express_mysql/index.js +++ b/templates/express_mysql/index.js @@ -1,33 +1,18 @@ -import "dotenv/config.js"; - -import express, { json } from "express"; -import helmet from "helmet"; import { appendFileSync } from "fs"; -import cors from "cors"; +import "dotenv/config.js"; +import makeApp from "./app.js"; +import database from "./db/database.js"; import { initLog } from "./logs/initLog.js"; -import sampleRouter from "./router/sampleRouter.js"; -import { appConfig } from "./config/appConfig.js"; import connectToDb from "./connection/normalConnection.js"; import reInitDb from "./db/reInitDb.js"; +import { appConfig } from "./config/appConfig.js"; -const app = express(); - -// Helmet sets HTTP headers for security. -app.use(helmet()); - -app.use(cors()); -app.use(json()); - -// Disable the X-Powered-By header to make it harder -// for attackers to find the tech stack. -app.disable("x-powered-by"); - -app.use(appConfig.router.SAMPLE_PREFIX, sampleRouter); +const app = makeApp(database); initLog(); const db = connectToDb(); -reInitDb(db); +await reInitDb(db); app.listen(appConfig.PORT, (err) => { if (err) { diff --git a/templates/express_mysql/package.json b/templates/express_mysql/package.json index 7202f7ff..e6e2a589 100644 --- a/templates/express_mysql/package.json +++ b/templates/express_mysql/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "start": "node index.js", - "dev": "nodemon index.js" + "dev": "nodemon index.js", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "author": "quick_start_express", "license": "ISC", @@ -18,7 +19,9 @@ "mysql2": "^3.11.3" }, "devDependencies": { - "nodemon": "^3.1.9" + "jest": "^29.7.0", + "nodemon": "^3.1.9", + "supertest": "^7.0.0" }, "type": "module" } diff --git a/templates/express_mysql/router/sampleRouter.js b/templates/express_mysql/router/sampleRouter.js index fca4d735..f77e6896 100644 --- a/templates/express_mysql/router/sampleRouter.js +++ b/templates/express_mysql/router/sampleRouter.js @@ -1,9 +1,14 @@ -import { test, getAllSamples } from "../controller/sampleController.js"; import { Router } from "express"; -const sampleRouter = Router(); +import sampleController from "../controller/sampleController.js"; -sampleRouter.get("/test", test); -sampleRouter.get("/all", getAllSamples); +export default function (database) { + const router = Router(); -export default sampleRouter; + router.get("/test", (req, res) => sampleController.test(req, res)); + router.get("/all", (req, res) => + sampleController.getAllSamples(req, res, database), + ); + + return router; +}