diff --git a/context.js b/context.js deleted file mode 100644 index e50229a..0000000 --- a/context.js +++ /dev/null @@ -1,3 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; - -export const ctxStorage = new AsyncLocalStorage(); diff --git a/handlers/index.js b/handlers/index.js new file mode 100644 index 0000000..0a07c4d --- /dev/null +++ b/handlers/index.js @@ -0,0 +1 @@ +export * from "./reports/index.js"; diff --git a/handlers/reports/index.js b/handlers/reports/index.js new file mode 100644 index 0000000..0cadf94 --- /dev/null +++ b/handlers/reports/index.js @@ -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 }; diff --git a/handlers/reports/parseExcel.handler.js b/handlers/reports/parseExcel.handler.js new file mode 100644 index 0000000..90f69ac --- /dev/null +++ b/handlers/reports/parseExcel.handler.js @@ -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},}`)); +} diff --git a/handlers/reports/statusParse.handler.js b/handlers/reports/statusParse.handler.js new file mode 100644 index 0000000..82b63fc --- /dev/null +++ b/handlers/reports/statusParse.handler.js @@ -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(); + }); +}; diff --git a/router/reports/syncParsed.handler.js b/handlers/reports/syncParsed.handler.js similarity index 81% rename from router/reports/syncParsed.handler.js rename to handlers/reports/syncParsed.handler.js index a83bb49..d86ba64 100644 --- a/router/reports/syncParsed.handler.js +++ b/handlers/reports/syncParsed.handler.js @@ -1,4 +1,4 @@ -import { BadRequestError } from "#root/errors/index.js"; +import { BadRequestError } from "#root/http-server/index.js"; import yup from "yup"; var reportItemSchema = yup.object({ @@ -11,7 +11,7 @@ var reportItemSchema = yup.object({ executor_ids: yup.array(yup.number()).required() }); -/** @type {HTTPRouteHandler} */ +/** @type {import("#root/http-server/types/http-server.js").HttpRouteHandler} */ export default async ({ res, data }) => { const inputSchema = yup.object({ reports: yup.array(reportItemSchema).required() }).required(); diff --git a/errors/index.js b/http-server/errors.js similarity index 69% rename from errors/index.js rename to http-server/errors.js index 0325fd1..b88e4bb 100644 --- a/errors/index.js +++ b/http-server/errors.js @@ -16,7 +16,7 @@ export class BadRequestError extends HttpError { */ constructor(message) { super({ - name: "E_BAD_REQUEST", + name: "ERR_BAD_REQUEST", message: message || "Bad Request", statusCode: 400 }); @@ -29,9 +29,22 @@ export class NotFoundError extends HttpError { */ constructor(message) { super({ - name: "E_NOT_FOUND", + name: "ERR_NOT_FOUND", message: message || "Not found", statusCode: 404 }); } } + +export class ServiceUnavailableError extends HttpError { + /** + * @param {string} [message] + */ + constructor(message) { + super({ + name: "ERR_SERVICE_UNAVAILABLE", + message: message || "Service unavailable", + statusCode: 503 + }); + } +} diff --git a/http-server/http-server.js b/http-server/http-server.js new file mode 100644 index 0000000..0500a46 --- /dev/null +++ b/http-server/http-server.js @@ -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; + } + }; +} diff --git a/http-server/index.js b/http-server/index.js new file mode 100644 index 0000000..cb28c5b --- /dev/null +++ b/http-server/index.js @@ -0,0 +1,2 @@ +export * from "./http-server.js"; +export * from "./errors.js"; diff --git a/request.js b/http-server/request.js similarity index 91% rename from request.js rename to http-server/request.js index 3f90a9b..7c6f3ab 100644 --- a/request.js +++ b/http-server/request.js @@ -1,6 +1,6 @@ /** * @param {import('node:http').IncomingMessage} req - * @returns {HTTPRequest} + * @returns {import('./types/http-server.js').HttpRequest} */ export function createRequest(req) { return { diff --git a/response.js b/http-server/response.js similarity index 92% rename from response.js rename to http-server/response.js index 8aec98b..2872d44 100644 --- a/response.js +++ b/http-server/response.js @@ -1,6 +1,6 @@ /** * @param {import('node:http').ServerResponse} res - * @returns {HTTPResponse} + * @returns {import('./types/http-server.js').HttpResponse} */ export function createResponse(res) { return { diff --git a/router/index.js b/http-server/route.js similarity index 55% rename from router/index.js rename to http-server/route.js index fc1e6a5..1682829 100644 --- a/router/index.js +++ b/http-server/route.js @@ -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"; - -/** - * @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 - } -}; +import formidable from "formidable"; +import { BadRequestError, HttpError, NotFoundError } from "./errors.js"; /** * @param {URL} url + * @param {import('node:async_hooks').AsyncLocalStorage} ctxStorage + * @param {import("./http-server.js").HttpRouteMap} routeMap */ -export async function routeHandler(url) { - /** @type {HTTPContext} */ +export async function routeHandler(url, ctxStorage, routeMap) { const ctx = ctxStorage.getStore(); + + if (!ctx) throw new Error("HTTP context storage is undefined!"); + const { req, res } = ctx; try { - if (url.pathname in ROUTE_MAP) { - const route = ROUTE_MAP[url.pathname]; + if (url.pathname in routeMap) { + const route = routeMap[url.pathname]; if (req.req.headers["content-type"] === "application/json") { 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."); } - ctx.data = await route.incomingForm.parse(req.req); + ctx.data = await formidable(route.incomingForm).parse(req.req); } if (route.method === req.req.method) { @@ -88,5 +67,3 @@ export async function routeHandler(url) { ); } } - -export * from "./reports/parseExcel.handler.js"; diff --git a/http-server/types/http-server.d.ts b/http-server/types/http-server.d.ts new file mode 100644 index 0000000..dc2fb76 --- /dev/null +++ b/http-server/types/http-server.d.ts @@ -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; + req: http.IncomingMessage; +} + +export interface HttpContext { + data: any; + res: HttpResponse; + req: HttpRequest; + params: URLSearchParams; + local: Record; +} + +export interface HttpResponse { + sendJson: (data: Record, statusCode?: number) => http.ServerResponse; + sendSSEJson: (data: Record, 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; +export type HttpCreateServer = (options: { routeMap: HttpRouteMap }) => HttpServer; diff --git a/index.d.ts b/index.d.ts deleted file mode 100644 index 093d793..0000000 --- a/index.d.ts +++ /dev/null @@ -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; - req: http.IncomingMessage; - }; - - type HTTPResponse = { - sendJson: (data: Record, statusCode?: number = 200) => http.ServerResponse; - sendSSEJson: (data: Record, 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; - type HTTPRouteMap = Record; - type HTTPContext = { data: any; res: HTTPResponse; req: HTTPRequest }; -} diff --git a/index.js b/index.js index 218da70..730fd46 100644 --- a/index.js +++ b/index.js @@ -1,54 +1,45 @@ -import http from "node:http"; -import { routeHandler } from "./router/index.js"; +import path from "node:path"; +import { createServer } from "./http-server/index.js"; +import { reportsHandlers } from "./handlers/index.js"; import packageJson from "./package.json" with { type: "json" }; -import { ctxStorage } from "./context.js"; import db from "./db.js"; -import { createResponse } from "./response.js"; -import { createRequest } from "./request.js"; const SERVER_PORT = 3000; -function application() { - const server = http.createServer((req, res) => { - try { - const url = new URL(`http://localhost:${SERVER_PORT}${req.url}`); +/** @type {Map} */ +export const clients = new Map(); - 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); +/** + * @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 + } +}; - return { - /** - * @param {number} port - */ - async run(port = 3000) { - await db.connect().then(() => console.log("Database connected!")); - server.listen(port, () => { - console.log(`Service ${packageJson.name} is running at port ${SERVER_PORT}.`); - }); - } - }; -} - -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!")); +}); diff --git a/jsconfig.json b/jsconfig.json index b47042f..d5bd13c 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "baseUrl": ".", "checkJs": true, + "allowJs": true, "lib": ["ESNext"], "strict": true, "module": "nodenext", @@ -9,5 +10,6 @@ "paths": { "#root/*": ["./*"] } - } + }, + "exclude": ["./http-server/types/*.d.ts"] } diff --git a/models.d.ts b/models.d.ts new file mode 100644 index 0000000..6e039f5 --- /dev/null +++ b/models.d.ts @@ -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 }; diff --git a/router/reports/index.js b/router/reports/index.js deleted file mode 100644 index 4eba478..0000000 --- a/router/reports/index.js +++ /dev/null @@ -1,4 +0,0 @@ -import parseExcelHandler from "./parseExcel.handler.js"; -import syncParsedHandler from "./syncParsed.handler.js"; - -export const handlers = { parseExcelHandler, syncParsedHandler }; diff --git a/router/reports/parseExcel.handler.js b/router/reports/parseExcel.handler.js deleted file mode 100644 index e65fcaf..0000000 --- a/router/reports/parseExcel.handler.js +++ /dev/null @@ -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},}`)); -}