eslint + refactor

This commit is contained in:
Sergey Yarkov 2026-03-01 00:37:06 +03:00
parent 4d87c19481
commit db6de0b2f5
15 changed files with 1093 additions and 79 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
node_modules node_modules
uploads uploads/*
.env .env

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"trailingComma": "none",
"printWidth": 120,
"tabWidth": 2,
"singleQuote": false,
"semi": true
}

6
db.js
View File

@ -10,11 +10,11 @@ const connectionConfig = {
pool: { pool: {
max: 10, max: 10,
min: 0, min: 0,
idleTimeoutMillis: 5000, idleTimeoutMillis: 5000
}, },
options: { options: {
trustServerCertificate: true, trustServerCertificate: true
}, }
}; };
const pool = new sql.ConnectionPool(connectionConfig); const pool = new sql.ConnectionPool(connectionConfig);

14
eslint.config.js Normal file
View File

@ -0,0 +1,14 @@
import js from "@eslint/js";
import globals from "globals";
import { defineConfig } from "eslint/config";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
export default defineConfig([
{
files: ["**/*.{js,mjs,cjs}"],
plugins: { js },
extends: ["js/recommended"],
languageOptions: { globals: globals.node }
},
eslintPluginPrettierRecommended
]);

7
index.d.ts vendored
View File

@ -1,4 +1,5 @@
import type http from "node:http"; import type http from "node:http";
import type IncomingForm from "formidable/Formidable.js";
declare global { declare global {
type ReportModel = { type ReportModel = {
@ -13,16 +14,16 @@ declare global {
type HTTPRequest = { type HTTPRequest = {
parseJson: () => Promise<any>; parseJson: () => Promise<any>;
req: http.IncomingMessage | null; req: http.IncomingMessage;
}; };
type HTTPResponse = { type HTTPResponse = {
sendJson: (data: Record<string, unknown>, statusCode?: number = 200) => http.ServerResponse; sendJson: (data: Record<string, unknown>, statusCode?: number = 200) => http.ServerResponse;
sendText: (text: string, statusCode?: number = 200) => http.ServerResponse; sendText: (text: string, statusCode?: number = 200) => http.ServerResponse;
res: http.ServerResponse | null; res: http.ServerResponse;
}; };
type HTTPRoute = { method: "POST" | "GET"; handle: HTTPRouteHandler }; type HTTPRoute = { method: "POST" | "GET"; handle: HTTPRouteHandler; incomingForm?: IncomingForm };
type HTTPRouteHandler = (ctx: HTTPContext) => Promise<void>; type HTTPRouteHandler = (ctx: HTTPContext) => Promise<void>;
type HTTPRouteMap = Record<string, HTTPRoute>; type HTTPRouteMap = Record<string, HTTPRoute>;
type HTTPContext = { data: any; res: HTTPResponse; req: HTTPRequest }; type HTTPContext = { data: any; res: HTTPResponse; req: HTTPRequest };

View File

@ -2,13 +2,13 @@ import http from "node:http";
import { routeHandler } from "./router/index.js"; import { routeHandler } from "./router/index.js";
import packageJson from "./package.json" with { type: "json" }; import packageJson from "./package.json" with { type: "json" };
import { ctxAsyncLocalStorage } from "./context.js"; import { ctxAsyncLocalStorage } from "./context.js";
import db from "./db.js"; // import db from "./db.js";
import response from "./response.js"; import response from "./response.js";
import request from "./request.js"; import request from "./request.js";
const SERVER_PORT = 3001; const SERVER_PORT = 3000;
await db.connect().then(() => console.log("Database connected!")); // await db.connect().then(() => console.log("Database connected!"));
http http
.createServer((req, res) => { .createServer((req, res) => {
@ -16,11 +16,19 @@ http
const url = new URL(`http://localhost:${SERVER_PORT}${req.url}`); const url = new URL(`http://localhost:${SERVER_PORT}${req.url}`);
/** @type {HTTPContext} */ /** @type {HTTPContext} */
const ctx = { data: null, req: Object.create(request), res: Object.create(response) }; const ctx = {
data: null,
req: Object.create(request),
res: Object.create(response)
};
ctx.req.req = req; ctx.req.req = req;
ctx.res.res = res; ctx.res.res = res;
ctxAsyncLocalStorage.run(ctx, () => routeHandler(url)); ctxAsyncLocalStorage.run(ctx, () =>
routeHandler(url).then(() => {
console.log(`${new Date().toISOString()} Request: ${url.pathname}`);
})
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -2,11 +2,12 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"checkJs": true, "checkJs": true,
"lib": ["ESNext"], "lib": ["ESNext"],
"strict": true,
"module": "nodenext", "module": "nodenext",
"resolveJsonModule": true, "resolveJsonModule": true,
"paths": { "paths": {
"#root/*": ["./*"] "#root/*": ["./*"]
} }
} }
} }

978
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "node --env-file=.env index.js", "start": "node --env-file=.env index.js",
"dev": "node --env-file=.env --watch index.js" "dev": "node --env-file=.env --watch index.js",
"lint": "eslint ."
}, },
"keywords": [], "keywords": [],
"author": "Sergey Yarkov", "author": "Sergey Yarkov",
@ -19,5 +20,13 @@
}, },
"imports": { "imports": {
"#root/*": "./*" "#root/*": "./*"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"eslint": "^10.0.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^17.3.0",
"prettier": "3.8.1"
} }
} }

View File

@ -15,5 +15,5 @@ export default {
}); });
this.req.on("error", (err) => reject(err)); this.req.on("error", (err) => reject(err));
}); });
}, }
}; };

View File

@ -6,7 +6,9 @@ export default {
return this.res.end(JSON.stringify(data, null, 2)); return this.res.end(JSON.stringify(data, null, 2));
}, },
sendText(text, statusCode = 200) { sendText(text, statusCode = 200) {
this.res.writeHead(statusCode, { "content-type": "text/plain; charset=utf-8" }); this.res.writeHead(statusCode, {
"content-type": "text/plain; charset=utf-8"
});
return this.res.end(text); return this.res.end(text);
}, }
}; };

View File

@ -1,6 +1,7 @@
import path from "node:path";
import formidable from "formidable";
import { ctxAsyncLocalStorage } from "#root/context.js"; import { ctxAsyncLocalStorage } from "#root/context.js";
import handleExcelReportsFileParse from "./reports/parseExcel.handler.js"; import { handlers as reportsHandlers } from "./reports/index.js";
import handleReportsSyncParsed from "./reports/syncParsed.handler.js";
/** /**
* @type {HTTPRouteMap} * @type {HTTPRouteMap}
@ -8,12 +9,20 @@ import handleReportsSyncParsed from "./reports/syncParsed.handler.js";
export const ROUTE_MAP = { export const ROUTE_MAP = {
"/api/reports_excel_file_parse": { "/api/reports_excel_file_parse": {
method: "POST", method: "POST",
handle: handleExcelReportsFileParse, handle: reportsHandlers.parseExcelHandler,
incomingForm: formidable({
maxFields: 1,
maxFiles: 1,
filename: (_, ext) => `gpp-reports${ext}`,
keepExtensions: true,
uploadDir: path.join(process.cwd(), "./uploads"),
filter: ({ mimetype }) => mimetype && mimetype.includes("application/vnd.ms-excel")
})
}, },
"/api/reports_sync": { "/api/reports_sync": {
method: "POST", method: "POST",
handle: handleReportsSyncParsed, handle: reportsHandlers.syncParsedHandler
}, }
}; };
/** /**
@ -32,6 +41,11 @@ export async function routeHandler(url) {
ctx.data = await ctx.req.parseJson(); ctx.data = await ctx.req.parseJson();
} }
if (req.req.headers["content-type"]?.includes("multipart/form-data")) {
if (!route.incomingForm) throw new Error("Request cannot be processed without defined incoming form.");
ctx.data = await route.incomingForm.parse(req.req);
}
if (route.method === req.req.method) { if (route.method === req.req.method) {
await route.handle(ctx); await route.handle(ctx);
} }
@ -46,10 +60,10 @@ export async function routeHandler(url) {
{ {
error: { error: {
message: error?.message || "Internal Server Error", message: error?.message || "Internal Server Error",
stack: error?.stack || null, stack: error?.stack || null
}, }
}, },
error?.cause?.statusCode || 500, error?.cause?.statusCode || 500
); );
} }
} }

4
router/reports/index.js Normal file
View File

@ -0,0 +1,4 @@
import parseExcelHandler from "./parseExcel.handler.js";
import syncParsedHandler from "./syncParsed.handler.js";
export const handlers = { parseExcelHandler, syncParsedHandler };

View File

@ -1,6 +1,15 @@
import path from "node:path";
import xlsx from "xlsx"; import xlsx from "xlsx";
import formidable from "formidable";
const sheetName = "Отчеты";
/** @type {xlsx.ParsingOptions} */
const xlsxParseOptions = {
sheets: sheetName,
cellHTML: false,
cellFormula: false,
cellDates: true,
dateNF: "yyyy-mm-dd"
};
/** /**
* Читает Excel файл отчетов и возвращает его в формате JSON * Читает Excel файл отчетов и возвращает его в формате JSON
@ -8,40 +17,18 @@ import formidable from "formidable";
* @type {HTTPRouteHandler} * @type {HTTPRouteHandler}
*/ */
export default async (ctx) => { export default async (ctx) => {
const { req, res } = ctx; const { res } = ctx;
const sheetName = "Отчеты"; const [, files] = ctx.data;
const excelFilePath = files?.report?.at(0)?.filepath;
/** @type {{ markedReports: ReportModel[], reports: ReportModel[] }} */ /** @type {{ markedReports: ReportModel[], reports: ReportModel[] }} */
const ret = { reports: [], markedReports: [] }; const ret = { reports: [], markedReports: [] };
/** @type {xlsx.ParsingOptions} */
const xlsxParseOptions = {
sheets: sheetName,
cellHTML: false,
cellFormula: false,
cellDates: true,
dateNF: "yyyy-mm-dd",
};
const form = formidable({
maxFields: 1,
maxFiles: 1,
filename: (_, ext) => `gpp-reports${ext}`,
keepExtensions: true,
uploadDir: path.join(process.cwd(), "./uploads"),
filter: ({ mimetype }) => mimetype && mimetype.includes("application/vnd.ms-excel"),
});
const [, files] = await form.parse(req.req);
const excelFilePath = files?.report?.at(0)?.filepath;
if (excelFilePath) { if (excelFilePath) {
const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions); const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions);
const sheet = workbook.Sheets[sheetName]; const sheet = workbook.Sheets[sheetName];
if (!sheet) { if (!sheet) throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
}
const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true }); const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true });
@ -59,24 +46,12 @@ export default async (ctx) => {
job_description: data[i][cols[4]].split("\r\r\n")[1] || "", job_description: data[i][cols[4]].split("\r\r\n")[1] || "",
root_cause: data[i]["__EMPTY_2"] || "", root_cause: data[i]["__EMPTY_2"] || "",
applicantName: data[i][cols[3]].split("\r\r\n")[0] || "", applicantName: data[i][cols[3]].split("\r\r\n")[0] || "",
executorNames: data[i][cols[4]].split("\r\r\n")[0] || "", executorNames: data[i][cols[4]].split("\r\r\n")[0] || ""
}); });
continue; continue;
} }
const parseReasonCallAndJobDesc = (str = "") => {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted.slice(1).join(" ");
return str.split("\r\n").slice(1).join(" ");
};
const parseNames = (str = "") => {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted[0];
return str.split("\r\n")[0];
};
/** Остальные отчеты */ /** Остальные отчеты */
ret.reports.push({ ret.reports.push({
date: data[i][cols[0]], date: data[i][cols[0]],
@ -85,7 +60,7 @@ export default async (ctx) => {
job_description: parseReasonCallAndJobDesc(data[i][cols[4]]), job_description: parseReasonCallAndJobDesc(data[i][cols[4]]),
root_cause: data[i]["__EMPTY_2"] || "", root_cause: data[i]["__EMPTY_2"] || "",
applicantName: parseNames(data[i][cols[3]]), applicantName: parseNames(data[i][cols[3]]),
executorNames: parseNames(data[i][cols[4]]), executorNames: parseNames(data[i][cols[4]])
}); });
} }
} else { } else {
@ -95,16 +70,17 @@ export default async (ctx) => {
res.sendJson({ data: ret }); res.sendJson({ data: ret });
}; };
// /** function parseReasonCallAndJobDesc(str = "") {
// * @param {string} filepath const splitted = splitLongSpacedSentence(str);
// */ if (splitted.length > 1) return splitted.slice(1).join(" ");
// function reportsExcelFileToJson(filepath) { return str.split("\r\n").slice(1).join(" ");
// try { }
// } catch (error) { function parseNames(str = "") {
const splitted = splitLongSpacedSentence(str);
// } if (splitted.length > 1) return splitted[0];
// } return str.split("\r\n")[0];
}
/** /**
* @param {string} str * @param {string} str

View File

@ -7,7 +7,7 @@ var reportItemSchema = yup.object({
job_description: yup.string().required(), job_description: yup.string().required(),
root_cause: yup.string().required(), root_cause: yup.string().required(),
applicant: yup.number().required(), applicant: yup.number().required(),
executor_ids: yup.array(yup.number()).required(), executor_ids: yup.array(yup.number()).required()
}); });
/** @type {HTTPRouteHandler} */ /** @type {HTTPRouteHandler} */