refactor & added parse status

This commit is contained in:
Sergey Yarkov 2026-03-04 15:26:53 +03:00
parent 38ca3af0a1
commit 821ee79873
19 changed files with 439 additions and 283 deletions

View File

@ -1,3 +0,0 @@
import { AsyncLocalStorage } from "node:async_hooks";
export const ctxStorage = new AsyncLocalStorage();

1
handlers/index.js Normal file
View File

@ -0,0 +1 @@
export * from "./reports/index.js";

View File

@ -0,0 +1,5 @@
import parseExcelHandler from "./parseExcel.handler.js";
import syncParsedHandler from "./syncParsed.handler.js";
import statusParseHandler from "./statusParse.handler.js";
export const reportsHandlers = { parseExcelHandler, statusParseHandler, syncParsedHandler };

View File

@ -0,0 +1,216 @@
import { BadRequestError, ServiceUnavailableError } from "#root/http-server/index.js";
import db from "#root/db.js";
import xlsx from "xlsx";
import pLimit from "p-limit";
import { clients } from "#root/index.js";
import { serializeError } from "serialize-error";
const sheetName = "Отчеты";
const limit = pLimit(10);
/** @type {xlsx.ParsingOptions} */
const xlsxParseOptions = {
sheets: sheetName,
cellHTML: false,
cellFormula: false,
cellDates: true,
dateNF: "yyyy-mm-dd"
};
/**
* URL: /api/reports_excel_file_parse
* Method: POST
* Description: Принимает на вход файл отчетов в формате Excel, парсит его, проверяет связи с БД,
* отдает идентификатор на статус парсинга.
*
* @type {import("#root/http-server/types/http-server.js").HttpRouteHandler}
*/
export default async (ctx) => {
const { res, params } = ctx;
const clientId = params.get("clientId");
if (!ctx.data) throw new BadRequestError("Cannot proceed uploaded file");
if (!clientId) throw new BadRequestError('"clientId" parameter is required');
const [, files] = ctx.data;
const excelFilePath = files?.report?.at(0)?.filepath;
const sseClientRes = clients.get(clientId);
if (!sseClientRes) throw new ServiceUnavailableError("Cannot proceed request due to unknown client");
let isSseClientClosed = false;
sseClientRes.res.on("close", () => (isSseClientClosed = true));
/** @type {ReportModel[]} */
const reports = [];
let parseResultCount = { equipments: 0, applicants: 0, executors: 0, reportsFullfilled: 0 };
// res.res.setHeaders(
// new Headers({
// Connection: "keep-alive",
// "Cache-Control": "no-cache",
// "Content-Type": "text/event-stream"
// })
// );
// ctx.res.res.on("close", () => (isReqClosed = true));
if (excelFilePath) {
const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions);
const sheet = workbook.Sheets[sheetName];
console.log();
if (!sheet) throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
res.sendJson({ message: "OK" });
const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true });
for (let i = 0; i < data.length; i++) {
const cols = Object.keys(data[i]);
if (i === 0) continue;
/** Несинхронизированные помеченные отчеты */
if ("__EMPTY_3" in data[i] && data[i]["__EMPTY_3"] === "r") {
reports.push({
date: data[i][cols[0]],
equipment: data[i][cols[1]],
reason_call: data[i][cols[3]].split("\r\r\n")[1] || "",
job_description: data[i][cols[4]].split("\r\r\n")[1] || "",
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] || "",
isMarked: true
});
continue;
}
/** Остальные отчеты */
reports.push({
date: data[i][cols[0]],
equipment: data[i][cols[1]],
reason_call: parseReasonCallAndJobDesc(data[i][cols[3]]).trim(),
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()
});
}
const requests = reports.map((report) =>
limit(() => {
return checkReportAssigments(
report,
(checkedReport) => {
const isEquipmentLinked = checkedReport.equipment instanceof Object;
const isApplicantLinked = checkedReport.applicant instanceof Object;
const isExecutorsLinked = checkedReport.executors && checkedReport.executors.length !== 0;
if (isEquipmentLinked) parseResultCount.equipments++;
if (isApplicantLinked) parseResultCount.applicants++;
if (isExecutorsLinked) parseResultCount.executors += checkedReport.executors?.length || 0;
if (isEquipmentLinked && isApplicantLinked && isExecutorsLinked) parseResultCount.reportsFullfilled++;
sseClientRes.sendSSEJson(checkedReport, "progress");
},
isSseClientClosed
);
})
);
await Promise.all(requests)
.then(() => {
console.log(parseResultCount);
sseClientRes.sendSSEJson(parseResultCount, "done");
sseClientRes.res.end();
})
.catch((error) => {
console.error(error);
sseClientRes.sendSSEJson({ error: serializeError(error) }, "error");
sseClientRes.res.end();
})
.finally(() => res.res.end());
} else {
throw new BadRequestError();
}
};
/**
* @param {ReportModel} report
* @param {(report: ReportModel) => void} callback
* @param {boolean} rejectCond
*/
async function checkReportAssigments(report, callback, rejectCond) {
if (rejectCond) return Promise.reject(`Report check is rejected`);
const executorSurnames = extractSurnamesFromExecutorField(report.executorNames);
const equipment = await db
.request()
.query(`select id, name from dbo.asu_system_api_subsystemlist where name like N'%${report.equipment}%';`);
const applicant = await db
.request()
.query(
`select id, name from dbo.gpp_report_api_applicant where name like N'${report.applicantName.split(" ")[0]}%';`
);
/** @type {ExecutorModel[]} */
const executors = [];
for (const eSurname of executorSurnames) {
if (eSurname) {
const executor = await db
.request()
.query(
`select id, surname + ' ' + name + ' ' + patronymic as fullname from user_user where surname like N'%${eSurname}%';`
);
if (executor.recordset.length !== 0) {
executors.push({ id: executor.recordset[0].id, fullname: executor.recordset[0].fullname });
}
}
}
const hasEquipmentLink = equipment.recordset.length !== 0;
const hasApplicantLink = applicant.recordset.length !== 0;
callback({
...report,
executors,
equipment: hasEquipmentLink ? { id: equipment.recordset[0].id, name: equipment.recordset[0].name } : null,
applicant: hasApplicantLink ? { id: applicant.recordset[0].id, name: applicant.recordset[0].name } : null
});
}
function parseReasonCallAndJobDesc(str = "") {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted.slice(1).join(" ");
return str.split("\r\n").slice(1).join(" ");
}
function parseNames(str = "") {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted[0];
return str.split("\r\n")[0];
}
/** @param {string} str */
function extractSurnamesFromExecutorField(str) {
return str
.replaceAll(",", "")
.split(/(?:.\..\.)|(?:.\..\s)|(?:.\.\s.\.)|(?:.\..)/g)
.map((s) => s.trim());
}
/**
* @param {string} str
* @param {number} min
*/
function splitLongSpacedSentence(str, min = 5) {
return str.split(new RegExp(`\\s{${min},}`));
}

View File

@ -0,0 +1,26 @@
import { clients } from "#root/index.js";
import crypto from "node:crypto";
/** @type {import("#root/http-server/types/http-server.js").HttpRouteHandler} */
export default (ctx) => {
const { res } = ctx;
const clientId = crypto.randomUUID();
clients.set(clientId, ctx.res);
res.res.setHeaders(
new Headers({
Connection: "keep-alive",
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream; charset=utf-8"
})
);
res.sendSSEJson({ message: "Connected!", clientId }, "data");
res.res.on("close", () => {
clients.delete(clientId);
console.log(clients);
res.res.end();
});
};

View File

@ -1,4 +1,4 @@
import { BadRequestError } from "#root/errors/index.js"; import { BadRequestError } from "#root/http-server/index.js";
import yup from "yup"; import yup from "yup";
var reportItemSchema = yup.object({ var reportItemSchema = yup.object({
@ -11,7 +11,7 @@ var reportItemSchema = yup.object({
executor_ids: yup.array(yup.number()).required() executor_ids: yup.array(yup.number()).required()
}); });
/** @type {HTTPRouteHandler} */ /** @type {import("#root/http-server/types/http-server.js").HttpRouteHandler} */
export default async ({ res, data }) => { export default async ({ res, data }) => {
const inputSchema = yup.object({ reports: yup.array(reportItemSchema).required() }).required(); const inputSchema = yup.object({ reports: yup.array(reportItemSchema).required() }).required();

View File

@ -16,7 +16,7 @@ export class BadRequestError extends HttpError {
*/ */
constructor(message) { constructor(message) {
super({ super({
name: "E_BAD_REQUEST", name: "ERR_BAD_REQUEST",
message: message || "Bad Request", message: message || "Bad Request",
statusCode: 400 statusCode: 400
}); });
@ -29,9 +29,22 @@ export class NotFoundError extends HttpError {
*/ */
constructor(message) { constructor(message) {
super({ super({
name: "E_NOT_FOUND", name: "ERR_NOT_FOUND",
message: message || "Not found", message: message || "Not found",
statusCode: 404 statusCode: 404
}); });
} }
} }
export class ServiceUnavailableError extends HttpError {
/**
* @param {string} [message]
*/
constructor(message) {
super({
name: "ERR_SERVICE_UNAVAILABLE",
message: message || "Service unavailable",
statusCode: 503
});
}
}

View File

@ -0,0 +1,63 @@
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";
/**
* @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
*/
/**
* @param {HttpServerOptions} options
* @returns {HttpServer}
*/
export function createServer(options) {
const { routeMap, enableCors } = options;
const ctxStorage = new AsyncLocalStorage();
let serverPort = 3000;
const server = http.createServer((req, res) => {
try {
const url = new URL(`http://${req.headers.host || `localhost:${serverPort}`}${req.url}`);
/** @type {HttpContext} */
const ctx = {
data: null,
req: createRequest(req),
res: createResponse(res),
params: url.searchParams,
local: {}
};
if (enableCors) {
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"
})
);
}
ctxStorage.run(ctx, () =>
routeHandler(url, ctxStorage, routeMap).then(() => {
console.log(`${new Date().toISOString()} Request: ${url.pathname}`);
})
);
} catch (error) {
console.error(error);
}
});
return {
listen(port = 3000, cb) {
serverPort = port;
server.listen(port, cb);
return server;
}
};
}

2
http-server/index.js Normal file
View File

@ -0,0 +1,2 @@
export * from "./http-server.js";
export * from "./errors.js";

View File

@ -1,6 +1,6 @@
/** /**
* @param {import('node:http').IncomingMessage} req * @param {import('node:http').IncomingMessage} req
* @returns {HTTPRequest} * @returns {import('./types/http-server.js').HttpRequest}
*/ */
export function createRequest(req) { export function createRequest(req) {
return { return {

View File

@ -1,6 +1,6 @@
/** /**
* @param {import('node:http').ServerResponse} res * @param {import('node:http').ServerResponse} res
* @returns {HTTPResponse} * @returns {import('./types/http-server.js').HttpResponse}
*/ */
export function createResponse(res) { export function createResponse(res) {
return { return {

View File

@ -1,43 +1,22 @@
import path from "node:path";
import formidable from "formidable";
import { ctxStorage } from "#root/context.js";
import { handlers as reportsHandlers } from "./reports/index.js";
import { BadRequestError, HttpError, NotFoundError } from "#root/errors/index.js";
import { serializeError } from "serialize-error"; import { serializeError } from "serialize-error";
import formidable from "formidable";
/** import { BadRequestError, HttpError, NotFoundError } from "./errors.js";
* @type {HTTPRouteMap}
*/
export const ROUTE_MAP = {
"/api/reports_excel_file_parse": {
method: "POST",
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": {
method: "POST",
handle: reportsHandlers.syncParsedHandler
}
};
/** /**
* @param {URL} url * @param {URL} url
* @param {import('node:async_hooks').AsyncLocalStorage<import('./http-server.js').HttpContext>} ctxStorage
* @param {import("./http-server.js").HttpRouteMap} routeMap
*/ */
export async function routeHandler(url) { export async function routeHandler(url, ctxStorage, routeMap) {
/** @type {HTTPContext} */
const ctx = ctxStorage.getStore(); const ctx = ctxStorage.getStore();
if (!ctx) throw new Error("HTTP context storage is undefined!");
const { req, res } = ctx; const { req, res } = ctx;
try { try {
if (url.pathname in ROUTE_MAP) { if (url.pathname in routeMap) {
const route = ROUTE_MAP[url.pathname]; const route = routeMap[url.pathname];
if (req.req.headers["content-type"] === "application/json") { if (req.req.headers["content-type"] === "application/json") {
ctx.data = await ctx.req.parseJson(); ctx.data = await ctx.req.parseJson();
@ -48,7 +27,7 @@ export async function routeHandler(url) {
throw new BadRequestError("Request cannot be processed without defined incoming form."); throw new BadRequestError("Request cannot be processed without defined incoming form.");
} }
ctx.data = await route.incomingForm.parse(req.req); ctx.data = await formidable(route.incomingForm).parse(req.req);
} }
if (route.method === req.req.method) { if (route.method === req.req.method) {
@ -88,5 +67,3 @@ export async function routeHandler(url) {
); );
} }
} }
export * from "./reports/parseExcel.handler.js";

42
http-server/types/http-server.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
import type http from "node:http";
import type IncomingForm from "formidable/Formidable.js";
import formidable from "formidable";
export interface HttpServer {
listen: (port: number, cb: () => void) => void;
}
export interface HttpServerOptions {
routeMap: HttpRouteMap;
enableCors?: boolean;
}
export interface HttpRoute {
method: "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
handle: HttpRouteHandler;
incomingForm?: formidable.Options;
}
export interface HttpRequest {
parseJson: () => Promise<any>;
req: http.IncomingMessage;
}
export interface HttpContext {
data: any;
res: HttpResponse;
req: HttpRequest;
params: URLSearchParams;
local: Record<string, unknown>;
}
export interface HttpResponse {
sendJson: (data: Record<string, unknown>, statusCode?: number) => http.ServerResponse;
sendSSEJson: (data: Record<string, unknown>, eventName: string, statusCode?: number) => void;
sendText: (text: string, statusCode?: number) => http.ServerResponse;
res: http.ServerResponse;
}
export type HttpRouteHandler = (ctx: HttpContext) => any;
export type HttpRouteMap = Record<string, HttpRoute>;
export type HttpCreateServer = (options: { routeMap: HttpRouteMap }) => HttpServer;

32
index.d.ts vendored
View File

@ -1,32 +0,0 @@
import type http from "node:http";
import type IncomingForm from "formidable/Formidable.js";
declare global {
type ReportModel = {
date: string;
equipment: string;
reason_call: string;
job_description: string;
root_cause: string;
applicantName: string;
executorNames: string;
isMarked?: boolean;
};
type HTTPRequest = {
parseJson: () => Promise<any>;
req: http.IncomingMessage;
};
type HTTPResponse = {
sendJson: (data: Record<string, unknown>, statusCode?: number = 200) => http.ServerResponse;
sendSSEJson: (data: Record<string, unknown>, eventName: string, statusCode?: number = 200) => void;
sendText: (text: string, statusCode?: number = 200) => http.ServerResponse;
res: http.ServerResponse;
};
type HTTPRoute = { method: "POST" | "GET"; handle: HTTPRouteHandler; incomingForm?: IncomingForm };
type HTTPRouteHandler = (ctx: HTTPContext) => Promise<void>;
type HTTPRouteMap = Record<string, HTTPRoute>;
type HTTPContext = { data: any; res: HTTPResponse; req: HTTPRequest };
}

View File

@ -1,54 +1,45 @@
import http from "node:http"; import path from "node:path";
import { routeHandler } from "./router/index.js"; import { createServer } from "./http-server/index.js";
import { reportsHandlers } from "./handlers/index.js";
import packageJson from "./package.json" with { type: "json" }; import packageJson from "./package.json" with { type: "json" };
import { ctxStorage } from "./context.js";
import db from "./db.js"; import db from "./db.js";
import { createResponse } from "./response.js";
import { createRequest } from "./request.js";
const SERVER_PORT = 3000; const SERVER_PORT = 3000;
function application() { /** @type {Map<string, import("./http-server/types/http-server.js").HttpResponse>} */
const server = http.createServer((req, res) => { export const clients = new Map();
try {
const url = new URL(`http://localhost:${SERVER_PORT}${req.url}`);
res.setHeaders(
new Headers({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST",
"Access-Control-Allow-Headers": "Content-Type"
})
);
/** @type {HTTPContext} */
const ctx = {
data: null,
req: createRequest(req),
res: createResponse(res)
};
ctxStorage.run(ctx, () =>
routeHandler(url).then(() => {
console.log(`${new Date().toISOString()} Request: ${url.pathname}`);
})
);
} catch (error) {
console.error(error);
}
});
return {
/** /**
* @param {number} port * @type {import("#root/http-server/http-server.js").HttpRouteMap}
*/ */
async run(port = 3000) { export const routeMap = {
await db.connect().then(() => console.log("Database connected!")); "/": {
server.listen(port, () => { method: "GET",
console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`); 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
} }
}; };
}
application().run(SERVER_PORT); createServer({ routeMap }).listen(SERVER_PORT, async () => {
console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`);
await db.connect().then(() => console.log("Database connected!"));
});

View File

@ -2,6 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"checkJs": true, "checkJs": true,
"allowJs": true,
"lib": ["ESNext"], "lib": ["ESNext"],
"strict": true, "strict": true,
"module": "nodenext", "module": "nodenext",
@ -9,5 +10,6 @@
"paths": { "paths": {
"#root/*": ["./*"] "#root/*": ["./*"]
} }
} },
"exclude": ["./http-server/types/*.d.ts"]
} }

14
models.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
type ReportModel = {
date: string;
equipment?: string | { id: string; name: string } | null;
applicant?: string | { id: string; name: string } | null;
reason_call: string;
job_description: string;
root_cause: string;
executors?: ExecutorModel[];
applicantName: string;
executorNames: string;
isMarked?: boolean;
};
type ExecutorModel = { id: string; fullname: string };

View File

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

View File

@ -1,157 +0,0 @@
import { BadRequestError } from "#root/errors/index.js";
import db from "#root/db.js";
import xlsx from "xlsx";
import pLimit from "p-limit";
import { serializeError } from "serialize-error";
const sheetName = "Отчеты";
const limit = pLimit(10);
/** @type {xlsx.ParsingOptions} */
const xlsxParseOptions = {
sheets: sheetName,
cellHTML: false,
cellFormula: false,
cellDates: true,
dateNF: "yyyy-mm-dd"
};
/**
* URL: /api/reports_excel_file_parse
* Method: POST
* Description: Принимает на вход файл отчетов в формате Excel, парсит его, проверяет связи с БД,
* отдает отчет в формате JSON частями.
*
* @type {HTTPRouteHandler}
*/
export default async (ctx) => {
const { res } = ctx;
if (!ctx.data) throw new BadRequestError("Cannot proceed uploaded file");
const [, files] = ctx.data;
const excelFilePath = files?.report?.at(0)?.filepath;
/** @type {ReportModel[]} */
const reports = [];
let parseResultCount = { equipments: 0, applicants: 0, executors: 0 };
let isReqClosed = false;
res.res.setHeaders(
new Headers({
Connection: "keep-alive",
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream"
})
);
res.res.on("close", () => (isReqClosed = true));
if (excelFilePath) {
const workbook = xlsx.readFile(excelFilePath, xlsxParseOptions);
const sheet = workbook.Sheets[sheetName];
if (!sheet) throw new Error("Invalid reports workbook.", { cause: { statusCode: 400 } });
const data = xlsx.utils.sheet_to_json(sheet, { raw: false, blankrows: true });
for (let i = 0; i < data.length; i++) {
const cols = Object.keys(data[i]);
if (i === 0) continue;
/** Несинхронизированные помеченные отчеты */
if ("__EMPTY_3" in data[i] && data[i]["__EMPTY_3"] === "r") {
reports.push({
date: data[i][cols[0]],
equipment: data[i][cols[1]],
reason_call: data[i][cols[3]].split("\r\r\n")[1] || "",
job_description: data[i][cols[4]].split("\r\r\n")[1] || "",
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] || "",
isMarked: true
});
continue;
}
/** Остальные отчеты */
reports.push({
date: data[i][cols[0]],
equipment: data[i][cols[1]],
reason_call: parseReasonCallAndJobDesc(data[i][cols[3]]).trim(),
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()
});
}
/** @param {ReportModel} report */
const checkReportAssigments = async (report) => {
if (isReqClosed) {
return Promise.reject("Client closed the connection.");
}
const equipment = await db
.request()
.query(`select id from dbo.asu_system_api_subsystemlist where name like N'%${report.equipment}%';`);
const applicant = await db
.request()
.query(
`select id from dbo.gpp_report_api_applicant where name like N'${report.applicantName.split(" ")[0]}%';`
);
if (equipment.recordset.length !== 0) parseResultCount.equipments++;
if (applicant.recordset.length !== 0) parseResultCount.applicants++;
res.sendSSEJson(
{
report: {
...report,
equipment: equipment.recordset.at(0)?.id || null,
applicant: applicant.recordset.at(0)?.id || null
}
},
"progress"
);
return equipment;
};
const requests = reports.map((r) => limit(() => checkReportAssigments(r)));
await Promise.all(requests)
.then(() => res.sendSSEJson(parseResultCount, "done"))
.catch((error) => {
console.error(error);
res.sendSSEJson({ error: serializeError(error) }, "error");
})
.finally(() => res.res.end());
} else {
throw new BadRequestError();
}
};
function parseReasonCallAndJobDesc(str = "") {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted.slice(1).join(" ");
return str.split("\r\n").slice(1).join(" ");
}
function parseNames(str = "") {
const splitted = splitLongSpacedSentence(str);
if (splitted.length > 1) return splitted[0];
return str.split("\r\n")[0];
}
/**
* @param {string} str
* @param {number} min
*/
function splitLongSpacedSentence(str, min = 5) {
return str.split(new RegExp(`\\s{${min},}`));
}