feat(sql): full sql start stop and query with crash prevention

This commit is contained in:
2025-12-22 14:54:43 -06:00
parent 878c3b3638
commit 6bb27e588d
15 changed files with 468 additions and 30 deletions

View File

@@ -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();

View File

@@ -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
},

View File

@@ -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];
`;

View File

@@ -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;

View File

@@ -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 () => {

View File

@@ -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: [],
});
}
};

View File

@@ -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);
};

View File

@@ -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",
},
},
},
},
},
},
},
},
},
};

View File

@@ -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.",
},
},
},
},
},
},
},
},
},
};

View File

@@ -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.",
},
},
},
},
},
},
},
},
},
};

View File

@@ -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",
},
},
},
},

View File

@@ -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
: [],
});
});

View File

@@ -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);
}

89
package-lock.json generated
View File

@@ -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",

View File

@@ -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",