diff --git a/backend/app.ts b/backend/app.ts index 1809bb7..5bb829e 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -1,13 +1,21 @@ import express from "express"; +import morgan from "morgan"; import { createLogger } from "./src/logger/logger.controller.js"; import { connectProdSql } from "./src/prodSql/sqlConnection.controller.js"; import { setupRoutes } from "./src/routeHandler.route.js"; const port = Number(process.env.PORT); -export const baseUrl = ""; + const startApp = async () => { const log = createLogger({ module: "system", subModule: "main start" }); const app = express(); + let baseUrl = "/"; + + // global env that run only in dev + if (process.env.NODE_ENV?.trim() !== "production") { + app.use(morgan("tiny")); + baseUrl = "/lst"; + } // start the connection to the prod sql server connectProdSql(); diff --git a/backend/src/configs/scaler.config.ts b/backend/src/configs/scaler.config.ts index b7ef1ba..5f8bf38 100644 --- a/backend/src/configs/scaler.config.ts +++ b/backend/src/configs/scaler.config.ts @@ -8,7 +8,9 @@ import type { Express } from "express"; import { apiReference } from "@scalar/express-api-reference"; // const port = 3000; import type { OpenAPIV3_1 } from "openapi-types"; - +import { prodRestartSpec } from "../scaler/prodSqlRestart.spec.js"; +import { prodStartSpec } from "../scaler/prodSqlStart.spec.js"; +import { prodStopSpec } from "../scaler/prodSqlStop.spec.js"; // all the specs import { statusSpec } from "../scaler/stats.spec.js"; @@ -21,7 +23,7 @@ export const openApiBase: OpenAPIV3_1.Document = { }, servers: [ { - url: "http://localhost:3000", + url: `http://localhost:3000${process.env.NODE_ENV?.trim() !== "production" ? "/lst" : "/"}`, description: "Development server", }, ], @@ -33,15 +35,15 @@ export const openApiBase: OpenAPIV3_1.Document = { bearerFormat: "JWT", }, }, - schemas: { - Error: { - type: "object", - properties: { - error: { type: "string" }, - message: { type: "string" }, - }, - }, - }, + // schemas: { + // Error: { + // type: "object", + // properties: { + // error: { type: "string" }, + // message: { type: "string" }, + // }, + // }, + // }, }, tags: [ // { name: "Health", description: "Health check endpoints" }, @@ -57,6 +59,9 @@ export const setupApiDocsRoutes = (baseUrl: string, app: Express) => { ...openApiBase, paths: { ...statusSpec, + ...prodStartSpec, + ...prodStopSpec, + ...prodRestartSpec, // Add more specs here as you build features }, diff --git a/backend/src/prodSql/querys/prodSqlStats.ts b/backend/src/prodSql/querys/prodSqlStats.ts new file mode 100644 index 0000000..2c7808f --- /dev/null +++ b/backend/src/prodSql/querys/prodSqlStats.ts @@ -0,0 +1,16 @@ +export const prodSqlServerStats = ` +DECLARE @UptimeSeconds INT; +DECLARE @StartTime DATETIME; + +SELECT @StartTime = sqlserver_start_time FROM sys.dm_os_sys_info; +SET @UptimeSeconds = DATEDIFF(SECOND, @StartTime, GETDATE()); + +SELECT + @StartTime AS [Server Start Time], + GETDATE() AS [Current Time], + @UptimeSeconds AS [UptimeSeconds], + @UptimeSeconds / 86400 AS [Days], + (@UptimeSeconds % 86400) / 3600 AS [Hours], + (@UptimeSeconds % 3600) / 60 AS [Minutes], + (@UptimeSeconds % 60) AS [Seconds]; +`; diff --git a/backend/src/prodSql/sql.route.ts b/backend/src/prodSql/sql.route.ts index f584ed7..6577221 100644 --- a/backend/src/prodSql/sql.route.ts +++ b/backend/src/prodSql/sql.route.ts @@ -1,5 +1,47 @@ import { Router } from "express"; +import { apiReturn } from "../utils/returnHelper.utils.js"; +import { closePool, connectProdSql } from "./sqlConnection.controller.js"; const r = Router(); +r.post("/start", async (_, res) => { + const connect = await connectProdSql(); + apiReturn(res, { + success: connect.success, + level: connect.success ? "info" : "error", + module: "routes", + subModule: "prodSql", + message: connect.message, + data: connect.data, + status: connect.success ? 200 : 400, + }); +}); +r.post("/stop", async (_, res) => { + const connect = await closePool(); + apiReturn(res, { + success: connect.success, + level: connect.success ? "info" : "error", + module: "routes", + subModule: "prodSql", + message: connect.message, + data: connect.data, + status: connect.success ? 200 : 400, + }); +}); +r.post("/restart", async (_, res) => { + await closePool(); + + await new Promise((r) => setTimeout(r, 2000)); + + const connect = await connectProdSql(); + apiReturn(res, { + success: connect.success, + level: connect.success ? "info" : "error", + module: "routes", + subModule: "prodSql", + message: "Sql Server has been restarted", + data: connect.data, + status: connect.success ? 200 : 400, + }); +}); export default r; diff --git a/backend/src/prodSql/sqlConnection.controller.ts b/backend/src/prodSql/sqlConnection.controller.ts index 3c933f3..a35bcac 100644 --- a/backend/src/prodSql/sqlConnection.controller.ts +++ b/backend/src/prodSql/sqlConnection.controller.ts @@ -10,10 +10,6 @@ export let reconnecting = false; export const connectProdSql = async () => { const serverUp = await checkHostnamePort(`${process.env.PROD_SERVER}:1433`); - const log = createLogger({ - module: "system", - subModule: "db", - }); if (!serverUp) { // we will try to reconnect connected = false; @@ -41,9 +37,15 @@ export const connectProdSql = async () => { try { pool = await sql.connect(prodSqlConfig); connected = true; - log.info( - `${prodSqlConfig.server} is connected to ${prodSqlConfig.database}`, - ); + return returnFunc({ + success: true, + level: "info", + module: "system", + subModule: "db", + message: `${prodSqlConfig.server} is connected to ${prodSqlConfig.database}`, + data: [], + notify: false, + }); } catch (error) { return returnFunc({ success: false, @@ -58,10 +60,6 @@ export const connectProdSql = async () => { }; export const closePool = async () => { - const log = createLogger({ - module: "system", - subModule: "db", - }); if (!connected) { return returnFunc({ success: false, @@ -74,15 +72,25 @@ export const closePool = async () => { try { await pool.close(); - log.info("Connection pool closed"); connected = false; - return { + return returnFunc({ success: true, - message: "The sql server connection has been closed", - }; + level: "info", + module: "system", + subModule: "db", + message: "The sql connection has been closed.", + }); } catch (error) { connected = false; - console.log("There was an error closing the sql connection", error); + + return returnFunc({ + success: false, + level: "error", + module: "system", + subModule: "db", + message: "There was an error closing the sql connection", + data: [error], + }); } }; export const reconnectToSql = async () => { diff --git a/backend/src/prodSql/sqlQuery.controller.ts b/backend/src/prodSql/sqlQuery.controller.ts index e69de29..f04677c 100644 --- a/backend/src/prodSql/sqlQuery.controller.ts +++ b/backend/src/prodSql/sqlQuery.controller.ts @@ -0,0 +1,100 @@ +import { returnFunc } from "../utils/returnHelper.utils.js"; +import { + closePool, + connected, + pool, + reconnecting, + reconnectToSql, +} from "./sqlConnection.controller.js"; + +interface SqlError extends Error { + code?: string; + originalError?: { + info?: { message?: string }; + }; +} + +/** + * Run a prod query + * just pass over the query as a string and the name of the query. + * Query should be like below. + * * select * from AlplaPROD_test1.dbo.table + * You must use test1 always as it will be changed via query + */ +export const prodQuery = async (queryToRun: string, name: string) => { + if (!connected) { + reconnectToSql(); + + if (reconnecting) { + return returnFunc({ + success: false, + level: "error", + module: "system", + subModule: "prodSql", + message: `The sql ${process.env.PROD_PLANT_TOKEN} is trying to reconnect already`, + data: [], + notify: false, + }); + } else { + return returnFunc({ + success: false, + level: "error", + module: "system", + subModule: "prodSql", + message: `${process.env.PROD_PLANT_TOKEN} is not connected, and failed to connect.`, + data: [], + notify: true, + }); + } + } + + //change to the correct server + const query = queryToRun.replaceAll( + "test1", + `${process.env.PROD_PLANT_TOKEN}`, + ); + + try { + const result = await pool.request().query(query); + return { + success: true, + message: `Query results for: ${name}`, + data: result.recordset, + }; + } catch (error: unknown) { + const err = error as SqlError; + if (err.code === "ETIMEOUT") { + closePool(); + return returnFunc({ + success: false, + module: "system", + subModule: "prodSql", + level: "error", + message: `${name} did not run due to a timeout.`, + notify: false, + data: [], + }); + } + + if (err.code === "EREQUEST") { + closePool(); + return returnFunc({ + success: false, + module: "system", + subModule: "prodSql", + level: "error", + message: `${name} encountered an error ${err.originalError?.info?.message || "undefined error"}`, + data: [], + }); + } + + return returnFunc({ + success: false, + module: "system", + subModule: "prodSql", + level: "error", + message: `${name} encountered an unknown error.`, + data: [], + }); + } +}; diff --git a/backend/src/routeHandler.route.ts b/backend/src/routeHandler.route.ts index 51e9192..d19c297 100644 --- a/backend/src/routeHandler.route.ts +++ b/backend/src/routeHandler.route.ts @@ -2,10 +2,12 @@ import type { Express } from "express"; // import the routes and route setups import { setupApiDocsRoutes } from "./configs/scaler.config.js"; +import prodSql from "./prodSql/sql.route.js"; import stats from "./system/stats.route.js"; export const setupRoutes = (baseUrl: string, app: Express) => { //setup all the routes setupApiDocsRoutes(baseUrl, app); app.use(`${baseUrl}/api/stats`, stats); + app.use(`${baseUrl}/api/system/prodSql`, prodSql); }; diff --git a/backend/src/scaler/prodSqlRestart.spec.ts b/backend/src/scaler/prodSqlRestart.spec.ts new file mode 100644 index 0000000..7ec22bc --- /dev/null +++ b/backend/src/scaler/prodSqlRestart.spec.ts @@ -0,0 +1,34 @@ +import type { OpenAPIV3_1 } from "openapi-types"; + +export const prodRestartSpec: OpenAPIV3_1.PathsObject = { + "/api/system/prodSql/restart": { + post: { + summary: "Prod restart sql connection", + description: "Attempts to restart the sql connection.", + tags: ["System"], + responses: { + "200": { + description: "Success from server restarting", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + format: "true", + example: true, + }, + message: { + type: "string", + format: "Sql Server has been restarted", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/backend/src/scaler/prodSqlStart.spec.ts b/backend/src/scaler/prodSqlStart.spec.ts new file mode 100644 index 0000000..daa657e --- /dev/null +++ b/backend/src/scaler/prodSqlStart.spec.ts @@ -0,0 +1,55 @@ +import type { OpenAPIV3_1 } from "openapi-types"; + +export const prodStartSpec: OpenAPIV3_1.PathsObject = { + "/api/system/prodSql/start": { + post: { + summary: "Prod start sql connection", + description: "Connects to the prod sql server.", + tags: ["System"], + responses: { + "200": { + description: "Data that is returned from the connection", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + format: "true", + example: true, + }, + message: { + type: "string", + format: "usmcd1vms036 is connected to AlplaPROD_test3_cus", + }, + }, + }, + }, + }, + }, + "400": { + description: "Data that is returned from the connection", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + format: "false", + example: false, + }, + message: { + type: "string", + format: "The Sql server is already connected.", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/backend/src/scaler/prodSqlStop.spec.ts b/backend/src/scaler/prodSqlStop.spec.ts new file mode 100644 index 0000000..a16eb4e --- /dev/null +++ b/backend/src/scaler/prodSqlStop.spec.ts @@ -0,0 +1,56 @@ +import type { OpenAPIV3_1 } from "openapi-types"; + +export const prodStopSpec: OpenAPIV3_1.PathsObject = { + "/api/system/prodSql/stop": { + post: { + summary: "Prod stop sql connection", + description: "Closes the connection to the prod server.", + tags: ["System"], + responses: { + "200": { + description: "Success from server starting", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + format: "true", + example: true, + }, + message: { + type: "string", + format: "The sql connection has been closed.", + }, + }, + }, + }, + }, + }, + "400": { + description: "Errors on why the server could not be stopped", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { + type: "boolean", + format: "false", + example: false, + }, + message: { + type: "string", + format: + "There is no connection to the prod server currently.", + }, + }, + }, + }, + }, + }, + }, + }, + }, +}; diff --git a/backend/src/scaler/stats.spec.ts b/backend/src/scaler/stats.spec.ts index c10bc90..e13c434 100644 --- a/backend/src/scaler/stats.spec.ts +++ b/backend/src/scaler/stats.spec.ts @@ -28,6 +28,10 @@ export const statusSpec: OpenAPIV3_1.PathsObject = { type: "string", format: "Heap: 11.62 MB / RSS: 86.31 MB", }, + sqlServerStats: { + type: "number", + format: "442127", + }, }, }, }, diff --git a/backend/src/system/stats.route.ts b/backend/src/system/stats.route.ts index 381392d..93707da 100644 --- a/backend/src/system/stats.route.ts +++ b/backend/src/system/stats.route.ts @@ -1,15 +1,22 @@ import { Router } from "express"; +import { prodSqlServerStats } from "../prodSql/querys/prodSqlStats.js"; +import { prodQuery } from "../prodSql/sqlQuery.controller.js"; const router = Router(); router.get("/", async (_, res) => { const used = process.memoryUsage(); + + const sqlServerStats = await prodQuery(prodSqlServerStats, "Sql Stats"); res.status(200).json({ status: "ok", uptime: process.uptime(), memoryUsage: `Heap: ${(used.heapUsed / 1024 / 1024).toFixed(2)} MB / RSS: ${( used.rss / 1024 / 1024 ).toFixed(2)} MB`, + sqlServerStats: sqlServerStats?.success + ? sqlServerStats?.data[0].UptimeSeconds + : [], }); }); diff --git a/backend/src/utils/returnHelper.utils.ts b/backend/src/utils/returnHelper.utils.ts index bc5b484..22b4973 100644 --- a/backend/src/utils/returnHelper.utils.ts +++ b/backend/src/utils/returnHelper.utils.ts @@ -1,9 +1,10 @@ +import type { Response } from "express"; import { createLogger } from "../logger/logger.controller.js"; interface Data { success: boolean; - module: "system" | "ocp"; - subModule: "db" | "labeling" | "printer"; + module: "system" | "ocp" | "routes"; + subModule: "db" | "labeling" | "printer" | "prodSql"; level: "info" | "error" | "debug" | "fatal"; message: string; data?: unknown[]; @@ -49,3 +50,12 @@ export const returnFunc = (data: Data) => { data: data.data || [], }; }; + +export function apiReturn( + res: Response, + opts: Data & { status?: number }, +): Response { + const result = returnFunc(opts); + const code = opts.status ?? (opts.success ? 200 : 500); + return res.status(code ?? (opts.success ? 200 : 500)).json(result); +} diff --git a/package-lock.json b/package-lock.json index b3ccbf8..e4484e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@commitlint/config-conventional": "^18.4.0", "@scalar/express-api-reference": "^0.8.28", "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", "@types/mssql": "^9.1.8", "@types/node": "^24.10.1", "@types/swagger-jsdoc": "^6.0.4", @@ -29,6 +30,7 @@ "cz-conventional-changelog": "^3.3.0", "express": "^5.2.1", "husky": "^8.0.3", + "morgan": "^1.10.1", "mssql": "^12.2.0", "npm-check-updates": "^19.1.2", "openapi-types": "^12.1.3", @@ -1992,6 +1994,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mssql": { "version": "9.1.8", "resolved": "https://registry.npmjs.org/@types/mssql/-/mssql-9.1.8.tgz", @@ -2390,6 +2402,26 @@ ], "license": "MIT" }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, "node_modules/better-auth": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.4.6.tgz", @@ -5669,6 +5701,53 @@ "node": ">=10" } }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -5863,6 +5942,16 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", diff --git a/package.json b/package.json index 2a2e397..04397b6 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@commitlint/config-conventional": "^18.4.0", "@scalar/express-api-reference": "^0.8.28", "@types/express": "^5.0.6", + "@types/morgan": "^1.9.10", "@types/mssql": "^9.1.8", "@types/node": "^24.10.1", "@types/swagger-jsdoc": "^6.0.4", @@ -43,6 +44,7 @@ "cz-conventional-changelog": "^3.3.0", "express": "^5.2.1", "husky": "^8.0.3", + "morgan": "^1.10.1", "mssql": "^12.2.0", "npm-check-updates": "^19.1.2", "openapi-types": "^12.1.3",