From 19ff6f8827cce61c3febf9fd6c70eec000aba13e Mon Sep 17 00:00:00 2001 From: sergeyyarkov Date: Mon, 9 Mar 2026 15:18:46 +0300 Subject: [PATCH] server refactor --- handlers/reports/parseExcel.handler.js | 10 +- http-server/http-server.js | 36 +++----- http-server/request.js | 4 + http-server/route.js | 122 +++++++++++++------------ http-server/types/http-server.d.ts | 39 ++++++-- index.js | 49 ++++------ 6 files changed, 138 insertions(+), 122 deletions(-) diff --git a/handlers/reports/parseExcel.handler.js b/handlers/reports/parseExcel.handler.js index 260d7cd..593ab5d 100644 --- a/handlers/reports/parseExcel.handler.js +++ b/handlers/reports/parseExcel.handler.js @@ -26,10 +26,10 @@ const xlsxParseOptions = { * @type {import("#root/http-server/types/http-server.js").HttpRouteHandler} */ export default async (ctx) => { - const { res, params } = ctx; + const { res, searchParams } = ctx; const [, files] = ctx.data; const limit = pLimit(10); - const clientId = params.get("clientId"); + const clientId = searchParams.get("clientId"); const excelFilePath = files?.report?.at(0)?.filepath; const reports = []; let isSseClientClosed = false; @@ -70,7 +70,7 @@ export default async (ctx) => { if (!sheet) 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, header: "A" }); res.sendJson({ message: "OK" }); sseClientCtx.local.isParsing = true; @@ -90,6 +90,7 @@ export default async (ctx) => { root_cause: data[i]["__EMPTY_2"] || "", applicantName: data[i][cols[3]].split("\r\r\n")[0] || "", executorNames: data[i][cols[4]].split("\r\r\n")[0] || "", + rowNum: data[i].__rowNum__ + 1, isMarked: true }); @@ -104,7 +105,8 @@ export default async (ctx) => { job_description: parseReasonCallAndJobDesc(data[i][cols[4]]).trim(), root_cause: data[i]["__EMPTY_2"] || "", applicantName: parseNames(data[i][cols[3]]).trim(), - executorNames: parseNames(data[i][cols[4]]).trim() + executorNames: parseNames(data[i][cols[4]]).trim(), + rowNum: data[i].__rowNum__ + 1 }); } } catch (error) { diff --git a/http-server/http-server.js b/http-server/http-server.js index 0500a46..3f161fd 100644 --- a/http-server/http-server.js +++ b/http-server/http-server.js @@ -1,14 +1,12 @@ import http from "node:http"; -import { AsyncLocalStorage } from "node:async_hooks"; import { createRequest } from "./request.js"; import { createResponse } from "./response.js"; -import { routeHandler } from "./route.js"; +import { createRouter } from "./route.js"; /** * @typedef {import('./types/http-server.js').HttpServer} HttpServer * @typedef {import('./types/http-server.js').HttpServerOptions} HttpServerOptions * @typedef {import('./types/http-server.js').HttpContext} HttpContext - * @typedef {import('./types/http-server.js').HttpRouteMap} HttpRouteMap */ /** @@ -16,38 +14,35 @@ import { routeHandler } from "./route.js"; * @returns {HttpServer} */ export function createServer(options) { - const { routeMap, enableCors } = options; - const ctxStorage = new AsyncLocalStorage(); - let serverPort = 3000; + const router = createRouter(); const server = http.createServer((req, res) => { try { - const url = new URL(`http://${req.headers.host || `localhost:${serverPort}`}${req.url}`); + const request = createRequest(req); + const response = createResponse(res); /** @type {HttpContext} */ const ctx = { data: null, - req: createRequest(req), - res: createResponse(res), - params: url.searchParams, + req: request, + res: response, + searchParams: request.url.searchParams, + params: {}, local: {} }; - if (enableCors) { + if (options.cors) { res.setHeaders( new Headers({ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET,POST,DELETE,UPDATE,PUT,PATCH", - "Access-Control-Allow-Headers": "Content-Type,Accept" + "Access-Control-Allow-Origin": options.cors.allow, + "Access-Control-Allow-Methods": options.cors.methods, + "Access-Control-Allow-Headers": options.cors.headers }) ); } - ctxStorage.run(ctx, () => - routeHandler(url, ctxStorage, routeMap).then(() => { - console.log(`${new Date().toISOString()} Request: ${url.pathname}`); - }) - ); + console.log(`${new Date().toISOString()} Request: ${ctx.req.url}`); + router.handle(ctx); } catch (error) { console.error(error); } @@ -55,9 +50,8 @@ export function createServer(options) { return { listen(port = 3000, cb) { - serverPort = port; server.listen(port, cb); - return server; + return { router }; } }; } diff --git a/http-server/request.js b/http-server/request.js index d9c3f65..d1feae4 100644 --- a/http-server/request.js +++ b/http-server/request.js @@ -8,6 +8,10 @@ export function createRequest(req) { return req; }, + get url() { + return new URL(`http://${req.headers.host || `localhost`}${req.url}`); + }, + async parseJson() { return new Promise((resolve, reject) => { /** @type {Buffer[]} */ diff --git a/http-server/route.js b/http-server/route.js index e334bf8..d13c43b 100644 --- a/http-server/route.js +++ b/http-server/route.js @@ -3,71 +3,75 @@ import formidable from "formidable"; import { BadRequestError, HttpError, MethodNotAllowedError, NotFoundError } from "./errors.js"; /** - * @param {URL} url - * @param {import('node:async_hooks').AsyncLocalStorage} ctxStorage - * @param {import("./http-server.js").HttpRouteMap} routeMap + * @returns {import("./types/http-server.js").HttpRouter} */ -export async function routeHandler(url, ctxStorage, routeMap) { - const ctx = ctxStorage.getStore(); +export function createRouter() { + return { + routes: [], + async handle(ctx) { + const { req, res } = ctx; - if (!ctx) throw new Error("HTTP context storage is undefined!"); + try { + for (const route of this.routes) { + const patternResult = route.pattern.exec(req.url.pathname); - const { req, res } = ctx; + if (!patternResult) continue; - try { - // TODO: create router based on URLPattern - if (url.pathname in routeMap) { - const route = routeMap[url.pathname]; - - if (route.method !== req.req.method) { - throw new MethodNotAllowedError(); - } - - if (req.req.headers["content-type"]?.includes("application/json")) { - ctx.data = await ctx.req.parseJson(); - } - - if (req.req.headers["content-type"]?.includes("multipart/form-data")) { - if (!route.incomingForm) { - throw new BadRequestError("Request cannot be processed without defined incoming form."); - } - - ctx.data = await formidable(route.incomingForm).parse(req.req); - } - - await route.handle(ctx); - - return; - } - - throw new NotFoundError("Route not found"); - } catch (error) { - console.error(error); - - if (error instanceof HttpError) { - res.sendJson( - { - error: { - name: error.name, - message: error.message || "Internal Server Error", - stack: error.stack || null + if (route.method !== req.req.method) { + throw new MethodNotAllowedError(); } - }, - error.statusCode || 500 - ); - return; - } - const serializedError = serializeError(error); + ctx.params = patternResult.pathname.groups; - res.sendJson( - { - error: { - ...serializedError, - stack: process.env.NODE_ENV !== "production" ? serializedError.stack : undefined + if (req.req.headers["content-type"]?.includes("application/json")) { + ctx.data = await ctx.req.parseJson(); + } + + if (req.req.headers["content-type"]?.includes("multipart/form-data")) { + if (!route.options.incomingForm) { + throw new BadRequestError("Request cannot be processed without defined incoming form."); + } + + ctx.data = await formidable(route.options.incomingForm).parse(req.req); + } + + await route.handler(ctx); + return; } - }, - 500 - ); - } + throw new NotFoundError("Route not found"); + } catch (error) { + console.error(error); + + if (error instanceof HttpError) { + res.sendJson( + { + error: { + name: error.name, + message: error.message || "Internal Server Error", + stack: error.stack || null + } + }, + error.statusCode || 500 + ); + return; + } + + const serializedError = serializeError(error); + + res.sendJson( + { + error: { + ...serializedError, + stack: process.env.NODE_ENV !== "production" ? serializedError.stack : undefined + } + }, + 500 + ); + } + }, + define(method, pathname, handler, options = {}) { + /** @type {import("./types/http-server.js").HttpRoute} */ + this.routes.push({ method, pattern: new URLPattern({ pathname }), handler, options }); + } + }; } diff --git a/http-server/types/http-server.d.ts b/http-server/types/http-server.d.ts index dc2fb76..0b01ae3 100644 --- a/http-server/types/http-server.d.ts +++ b/http-server/types/http-server.d.ts @@ -3,30 +3,52 @@ import type IncomingForm from "formidable/Formidable.js"; import formidable from "formidable"; export interface HttpServer { - listen: (port: number, cb: () => void) => void; + listen: (port: number, cb: () => void) => { router: HttpRouter }; } export interface HttpServerOptions { - routeMap: HttpRouteMap; - enableCors?: boolean; + cors?: { + allow?: string; + methods?: string; + headers?: string; + }; +} + +export type HttpMethod = "POST" | "GET" | "PUT" | "DELETE" | "PATCH"; + +export interface HttpRouteOptions { + incomingForm?: formidable.Options; } export interface HttpRoute { - method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH"; - handle: HttpRouteHandler; - incomingForm?: formidable.Options; + method: HttpMethod; + pattern: URLPattern; + handler: HttpRouteHandler; + options: HttpRouteOptions; +} +export interface HttpRouter { + routes: Array; + handle: (ctx: HttpContext) => Promise; + define: ( + method: HttpMethod, + pathname: string, + handler: (ctx: HttpContext) => void, + options?: HttpRouteOptions + ) => void; } export interface HttpRequest { parseJson: () => Promise; req: http.IncomingMessage; + url: URL; } export interface HttpContext { data: any; res: HttpResponse; req: HttpRequest; - params: URLSearchParams; + searchParams: URLSearchParams; + params: Record; local: Record; } @@ -38,5 +60,4 @@ export interface HttpResponse { } export type HttpRouteHandler = (ctx: HttpContext) => any; -export type HttpRouteMap = Record; -export type HttpCreateServer = (options: { routeMap: HttpRouteMap }) => HttpServer; +export type HttpCreateServer = (options: HttpServerOptions) => HttpServer; diff --git a/index.js b/index.js index 83448a1..4900c0e 100644 --- a/index.js +++ b/index.js @@ -10,38 +10,29 @@ const SERVER_PORT = Number.parseInt(`${process.env.PORT || "3000"}`, 10); /** @type {Map} */ export const clients = new Map(); -/** - * @type {import("#root/http-server/http-server.js").HttpRouteMap} - */ -export const routeMap = { - "/": { - method: "GET", - handle: (ctx) => ctx.res.sendText("Сервис инструментов таблицы АСУ.") - }, - "/api/reports_status_parse": { - method: "GET", - handle: reportsHandlers.statusParseHandler - }, - "/api/reports_excel_file_parse": { - method: "POST", - handle: reportsHandlers.parseExcelHandler, - incomingForm: { - 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": { - method: "POST", - handle: reportsHandlers.syncParsedHandler - } +/** @type {import("./http-server/http-server.js").HttpServerOptions['cors']} */ +const cors = { + allow: "*", + methods: "GET,POST,DELETE,UPDATE,PUT,PATCH", + headers: "Content-Type,Accept" }; -createServer({ routeMap, enableCors: true }).listen(SERVER_PORT, async () => { +const server = createServer({ cors }).listen(SERVER_PORT, async () => { console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`); await fsp.mkdir(path.join(process.cwd(), "/uploads"), { recursive: true }); await db.connect().then(() => console.log("Database connected!")); }); + +server.router.define("GET", "/", ({ res }) => res.sendText("Сервис инструментов таблицы АСУ.")); +server.router.define("GET", "/api/reports_status_parse", reportsHandlers.statusParseHandler); +server.router.define("POST", "/api/reports_excel_file_parse", reportsHandlers.parseExcelHandler, { + incomingForm: { + 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") + } +}); +server.router.define("POST", "/api/reports_sync", reportsHandlers.syncParsedHandler);