mirror of
https://github.com/sergeyyarkov/asu_tools_service.git
synced 2026-10-11 18:39:15 +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}
|
* @type {import("#root/http-server/types/http-server.js").HttpRouteHandler}
|
||||||
*/
|
*/
|
||||||
export default async (ctx) => {
|
export default async (ctx) => {
|
||||||
const { res, params } = ctx;
|
const { res, searchParams } = ctx;
|
||||||
const [, files] = ctx.data;
|
const [, files] = ctx.data;
|
||||||
const limit = pLimit(10);
|
const limit = pLimit(10);
|
||||||
const clientId = params.get("clientId");
|
const clientId = searchParams.get("clientId");
|
||||||
const excelFilePath = files?.report?.at(0)?.filepath;
|
const excelFilePath = files?.report?.at(0)?.filepath;
|
||||||
const reports = [];
|
const reports = [];
|
||||||
let isSseClientClosed = false;
|
let isSseClientClosed = false;
|
||||||
@ -70,7 +70,7 @@ export default async (ctx) => {
|
|||||||
|
|
||||||
if (!sheet) throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
|
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" });
|
res.sendJson({ message: "OK" });
|
||||||
sseClientCtx.local.isParsing = true;
|
sseClientCtx.local.isParsing = true;
|
||||||
@ -90,6 +90,7 @@ export default async (ctx) => {
|
|||||||
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] || "",
|
||||||
|
rowNum: data[i].__rowNum__ + 1,
|
||||||
isMarked: true
|
isMarked: true
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -104,7 +105,8 @@ export default async (ctx) => {
|
|||||||
job_description: parseReasonCallAndJobDesc(data[i][cols[4]]).trim(),
|
job_description: parseReasonCallAndJobDesc(data[i][cols[4]]).trim(),
|
||||||
root_cause: data[i]["__EMPTY_2"] || "",
|
root_cause: data[i]["__EMPTY_2"] || "",
|
||||||
applicantName: parseNames(data[i][cols[3]]).trim(),
|
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) {
|
} catch (error) {
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import { AsyncLocalStorage } from "node:async_hooks";
|
|
||||||
import { createRequest } from "./request.js";
|
import { createRequest } from "./request.js";
|
||||||
import { createResponse } from "./response.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').HttpServer} HttpServer
|
||||||
* @typedef {import('./types/http-server.js').HttpServerOptions} HttpServerOptions
|
* @typedef {import('./types/http-server.js').HttpServerOptions} HttpServerOptions
|
||||||
* @typedef {import('./types/http-server.js').HttpContext} HttpContext
|
* @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}
|
* @returns {HttpServer}
|
||||||
*/
|
*/
|
||||||
export function createServer(options) {
|
export function createServer(options) {
|
||||||
const { routeMap, enableCors } = options;
|
const router = createRouter();
|
||||||
const ctxStorage = new AsyncLocalStorage();
|
|
||||||
let serverPort = 3000;
|
|
||||||
|
|
||||||
const server = http.createServer((req, res) => {
|
const server = http.createServer((req, res) => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(`http://${req.headers.host || `localhost:${serverPort}`}${req.url}`);
|
const request = createRequest(req);
|
||||||
|
const response = createResponse(res);
|
||||||
|
|
||||||
/** @type {HttpContext} */
|
/** @type {HttpContext} */
|
||||||
const ctx = {
|
const ctx = {
|
||||||
data: null,
|
data: null,
|
||||||
req: createRequest(req),
|
req: request,
|
||||||
res: createResponse(res),
|
res: response,
|
||||||
params: url.searchParams,
|
searchParams: request.url.searchParams,
|
||||||
|
params: {},
|
||||||
local: {}
|
local: {}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (enableCors) {
|
if (options.cors) {
|
||||||
res.setHeaders(
|
res.setHeaders(
|
||||||
new Headers({
|
new Headers({
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": options.cors.allow,
|
||||||
"Access-Control-Allow-Methods": "GET,POST,DELETE,UPDATE,PUT,PATCH",
|
"Access-Control-Allow-Methods": options.cors.methods,
|
||||||
"Access-Control-Allow-Headers": "Content-Type,Accept"
|
"Access-Control-Allow-Headers": options.cors.headers
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctxStorage.run(ctx, () =>
|
console.log(`${new Date().toISOString()} Request: ${ctx.req.url}`);
|
||||||
routeHandler(url, ctxStorage, routeMap).then(() => {
|
router.handle(ctx);
|
||||||
console.log(`${new Date().toISOString()} Request: ${url.pathname}`);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
@ -55,9 +50,8 @@ export function createServer(options) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
listen(port = 3000, cb) {
|
listen(port = 3000, cb) {
|
||||||
serverPort = port;
|
|
||||||
server.listen(port, cb);
|
server.listen(port, cb);
|
||||||
return server;
|
return { router };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,10 @@ export function createRequest(req) {
|
|||||||
return req;
|
return req;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
return new URL(`http://${req.headers.host || `localhost`}${req.url}`);
|
||||||
|
},
|
||||||
|
|
||||||
async parseJson() {
|
async parseJson() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
/** @type {Buffer[]} */
|
/** @type {Buffer[]} */
|
||||||
|
|||||||
@ -3,71 +3,75 @@ import formidable from "formidable";
|
|||||||
import { BadRequestError, HttpError, MethodNotAllowedError, NotFoundError } from "./errors.js";
|
import { BadRequestError, HttpError, MethodNotAllowedError, NotFoundError } from "./errors.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {URL} url
|
* @returns {import("./types/http-server.js").HttpRouter}
|
||||||
* @param {import('node:async_hooks').AsyncLocalStorage<import('./http-server.js').HttpContext>} ctxStorage
|
|
||||||
* @param {import("./http-server.js").HttpRouteMap} routeMap
|
|
||||||
*/
|
*/
|
||||||
export async function routeHandler(url, ctxStorage, routeMap) {
|
export function createRouter() {
|
||||||
const ctx = ctxStorage.getStore();
|
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 {
|
if (route.method !== req.req.method) {
|
||||||
// TODO: create router based on URLPattern
|
throw new MethodNotAllowedError();
|
||||||
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
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
error.statusCode || 500
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const serializedError = serializeError(error);
|
ctx.params = patternResult.pathname.groups;
|
||||||
|
|
||||||
res.sendJson(
|
if (req.req.headers["content-type"]?.includes("application/json")) {
|
||||||
{
|
ctx.data = await ctx.req.parseJson();
|
||||||
error: {
|
}
|
||||||
...serializedError,
|
|
||||||
stack: process.env.NODE_ENV !== "production" ? serializedError.stack : undefined
|
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;
|
||||||
}
|
}
|
||||||
},
|
throw new NotFoundError("Route not found");
|
||||||
500
|
} 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
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";
|
import formidable from "formidable";
|
||||||
|
|
||||||
export interface HttpServer {
|
export interface HttpServer {
|
||||||
listen: (port: number, cb: () => void) => void;
|
listen: (port: number, cb: () => void) => { router: HttpRouter };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpServerOptions {
|
export interface HttpServerOptions {
|
||||||
routeMap: HttpRouteMap;
|
cors?: {
|
||||||
enableCors?: boolean;
|
allow?: string;
|
||||||
|
methods?: string;
|
||||||
|
headers?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HttpMethod = "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
|
||||||
|
|
||||||
|
export interface HttpRouteOptions {
|
||||||
|
incomingForm?: formidable.Options;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpRoute {
|
export interface HttpRoute {
|
||||||
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
|
method: HttpMethod;
|
||||||
handle: HttpRouteHandler;
|
pattern: URLPattern;
|
||||||
incomingForm?: formidable.Options;
|
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 {
|
export interface HttpRequest {
|
||||||
parseJson: () => Promise<any>;
|
parseJson: () => Promise<any>;
|
||||||
req: http.IncomingMessage;
|
req: http.IncomingMessage;
|
||||||
|
url: URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HttpContext {
|
export interface HttpContext {
|
||||||
data: any;
|
data: any;
|
||||||
res: HttpResponse;
|
res: HttpResponse;
|
||||||
req: HttpRequest;
|
req: HttpRequest;
|
||||||
params: URLSearchParams;
|
searchParams: URLSearchParams;
|
||||||
|
params: Record<string, unknown>;
|
||||||
local: Record<string, unknown>;
|
local: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,5 +60,4 @@ export interface HttpResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type HttpRouteHandler = (ctx: HttpContext) => any;
|
export type HttpRouteHandler = (ctx: HttpContext) => any;
|
||||||
export type HttpRouteMap = Record<string, HttpRoute>;
|
export type HttpCreateServer = (options: HttpServerOptions) => HttpServer;
|
||||||
export type HttpCreateServer = (options: { routeMap: HttpRouteMap }) => HttpServer;
|
|
||||||
|
|||||||
49
index.js
49
index.js
@ -10,38 +10,29 @@ const SERVER_PORT = Number.parseInt(`${process.env.PORT || "3000"}`, 10);
|
|||||||
/** @type {Map<string, import("./http-server/types/http-server.js").HttpContext>} */
|
/** @type {Map<string, import("./http-server/types/http-server.js").HttpContext>} */
|
||||||
export const clients = new Map();
|
export const clients = new Map();
|
||||||
|
|
||||||
/**
|
/** @type {import("./http-server/http-server.js").HttpServerOptions['cors']} */
|
||||||
* @type {import("#root/http-server/http-server.js").HttpRouteMap}
|
const cors = {
|
||||||
*/
|
allow: "*",
|
||||||
export const routeMap = {
|
methods: "GET,POST,DELETE,UPDATE,PUT,PATCH",
|
||||||
"/": {
|
headers: "Content-Type,Accept"
|
||||||
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
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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}.`);
|
console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`);
|
||||||
await fsp.mkdir(path.join(process.cwd(), "/uploads"), { recursive: true });
|
await fsp.mkdir(path.join(process.cwd(), "/uploads"), { recursive: true });
|
||||||
await db.connect().then(() => console.log("Database connected!"));
|
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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user