mirror of
https://github.com/sergeyyarkov/asu_tools_service.git
synced 2026-10-11 10:29:22 +03:00
server refactor
This commit is contained in:
parent
c9914c8f19
commit
19ff6f8827
@ -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) {
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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[]} */
|
||||
|
||||
@ -3,43 +3,41 @@ import formidable from "formidable";
|
||||
import { BadRequestError, HttpError, MethodNotAllowedError, NotFoundError } from "./errors.js";
|
||||
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @param {import('node:async_hooks').AsyncLocalStorage<import('./http-server.js').HttpContext>} 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();
|
||||
|
||||
if (!ctx) throw new Error("HTTP context storage is undefined!");
|
||||
|
||||
export function createRouter() {
|
||||
return {
|
||||
routes: [],
|
||||
async handle(ctx) {
|
||||
const { req, res } = ctx;
|
||||
|
||||
try {
|
||||
// TODO: create router based on URLPattern
|
||||
if (url.pathname in routeMap) {
|
||||
const route = routeMap[url.pathname];
|
||||
for (const route of this.routes) {
|
||||
const patternResult = route.pattern.exec(req.url.pathname);
|
||||
|
||||
if (!patternResult) continue;
|
||||
|
||||
if (route.method !== req.req.method) {
|
||||
throw new MethodNotAllowedError();
|
||||
}
|
||||
|
||||
ctx.params = patternResult.pathname.groups;
|
||||
|
||||
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) {
|
||||
if (!route.options.incomingForm) {
|
||||
throw new BadRequestError("Request cannot be processed without defined incoming form.");
|
||||
}
|
||||
|
||||
ctx.data = await formidable(route.incomingForm).parse(req.req);
|
||||
ctx.data = await formidable(route.options.incomingForm).parse(req.req);
|
||||
}
|
||||
|
||||
await route.handle(ctx);
|
||||
|
||||
await route.handler(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotFoundError("Route not found");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -70,4 +68,10 @@ export async function routeHandler(url, ctxStorage, routeMap) {
|
||||
500
|
||||
);
|
||||
}
|
||||
},
|
||||
define(method, pathname, handler, options = {}) {
|
||||
/** @type {import("./types/http-server.js").HttpRoute} */
|
||||
this.routes.push({ method, pattern: new URLPattern({ pathname }), handler, options });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
39
http-server/types/http-server.d.ts
vendored
39
http-server/types/http-server.d.ts
vendored
@ -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<HttpRoute>;
|
||||
handle: (ctx: HttpContext) => Promise<void>;
|
||||
define: (
|
||||
method: HttpMethod,
|
||||
pathname: string,
|
||||
handler: (ctx: HttpContext) => void,
|
||||
options?: HttpRouteOptions
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface HttpRequest {
|
||||
parseJson: () => Promise<any>;
|
||||
req: http.IncomingMessage;
|
||||
url: URL;
|
||||
}
|
||||
|
||||
export interface HttpContext {
|
||||
data: any;
|
||||
res: HttpResponse;
|
||||
req: HttpRequest;
|
||||
params: URLSearchParams;
|
||||
searchParams: URLSearchParams;
|
||||
params: Record<string, unknown>;
|
||||
local: Record<string, unknown>;
|
||||
}
|
||||
|
||||
@ -38,5 +60,4 @@ export interface HttpResponse {
|
||||
}
|
||||
|
||||
export type HttpRouteHandler = (ctx: HttpContext) => any;
|
||||
export type HttpRouteMap = Record<string, HttpRoute>;
|
||||
export type HttpCreateServer = (options: { routeMap: HttpRouteMap }) => HttpServer;
|
||||
export type HttpCreateServer = (options: HttpServerOptions) => HttpServer;
|
||||
|
||||
43
index.js
43
index.js
@ -10,21 +10,22 @@ const SERVER_PORT = Number.parseInt(`${process.env.PORT || "3000"}`, 10);
|
||||
/** @type {Map<string, import("./http-server/types/http-server.js").HttpContext>} */
|
||||
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,
|
||||
/** @type {import("./http-server/http-server.js").HttpServerOptions['cors']} */
|
||||
const cors = {
|
||||
allow: "*",
|
||||
methods: "GET,POST,DELETE,UPDATE,PUT,PATCH",
|
||||
headers: "Content-Type,Accept"
|
||||
};
|
||||
|
||||
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,
|
||||
@ -33,15 +34,5 @@ export const routeMap = {
|
||||
uploadDir: path.join(process.cwd(), "./uploads"),
|
||||
filter: ({ mimetype }) => !!mimetype && mimetype.includes("application/vnd.ms-excel")
|
||||
}
|
||||
},
|
||||
"/api/reports_sync": {
|
||||
method: "POST",
|
||||
handle: reportsHandlers.syncParsedHandler
|
||||
}
|
||||
};
|
||||
|
||||
createServer({ routeMap, enableCors: true }).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("POST", "/api/reports_sync", reportsHandlers.syncParsedHandler);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user