From c0e1e2f65506a4a81b3fd270fe5a523eeb45fe15 Mon Sep 17 00:00:00 2001 From: sergeyyarkov Date: Thu, 12 Mar 2026 16:28:17 +0300 Subject: [PATCH] upd --- handlers/reports/parseExcel.handler.js | 2 +- handlers/reports/syncParsed.handler.js | 4 +- jsconfig.json | 6 ++- models.d.ts | 2 +- models/index.js | 1 + models/report.model.js | 50 +++++++++++++++++++++ package.json | 6 ++- repositories/index.js | 1 + repositories/report.repository.js | 61 ++++++++++++++++++++++++++ schemas/report.schema.js | 2 +- services/reports.service.js | 59 +++++++++++-------------- tables/base.table.js | 35 +++++++++++++++ tables/index.js | 1 + tables/report.table.js | 23 ++++++++++ utils/date.js | 7 +++ utils/index.js | 1 + 16 files changed, 221 insertions(+), 40 deletions(-) create mode 100644 models/index.js create mode 100644 models/report.model.js create mode 100644 repositories/index.js create mode 100644 repositories/report.repository.js create mode 100644 tables/base.table.js create mode 100644 tables/index.js create mode 100644 tables/report.table.js create mode 100644 utils/date.js create mode 100644 utils/index.js diff --git a/handlers/reports/parseExcel.handler.js b/handlers/reports/parseExcel.handler.js index 70f956a..c3748ea 100644 --- a/handlers/reports/parseExcel.handler.js +++ b/handlers/reports/parseExcel.handler.js @@ -45,7 +45,7 @@ export default async (ctx) => { throw new ServiceUnavailableError("Your request on parsing is processing"); } - /** @param {ReportModel} report */ + /** @param {ParsedReport} report */ const onParseProgress = async (report) => sseClientCtx.res.sendSSEJson(report, "progress"); const cancelAnalyzeCond = () => Boolean(sseClientCtx.local?.isClosed); diff --git a/handlers/reports/syncParsed.handler.js b/handlers/reports/syncParsed.handler.js index 1db4960..e72d589 100644 --- a/handlers/reports/syncParsed.handler.js +++ b/handlers/reports/syncParsed.handler.js @@ -14,6 +14,6 @@ export default async ({ res, data }) => { throw new BadRequestError(err.message); }); - await reportsService.syncReportsWithDatabase(reports); - res.sendJson({ data: { message: "OK" } }); + const rows = await reportsService.syncReportsWithDatabase(reports); + res.sendJson({ data: { rows } }); }; diff --git a/jsconfig.json b/jsconfig.json index 40793a2..d5499e2 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -11,7 +11,11 @@ "#root/*": ["./*"], "#handlers/*": ["./handlers/*"], "#services/*": ["./services/*"], - "#schemas/*": ["./schemas/*"] + "#schemas/*": ["./schemas/*"], + "#repositories/*": ["./repositories/*"], + "#tables/*": ["./tables/*"], + "#models/*": ["./models/*"], + "#utils/*": ["./utils/*"] } }, "exclude": ["./http-server/types/*.d.ts"] diff --git a/models.d.ts b/models.d.ts index 5804862..964bf7a 100644 --- a/models.d.ts +++ b/models.d.ts @@ -1,4 +1,4 @@ -type ReportModel = { +type ParsedReport = { date: string; equipment?: | string diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..a900aec --- /dev/null +++ b/models/index.js @@ -0,0 +1 @@ +export * from "./report.model.js"; diff --git a/models/report.model.js b/models/report.model.js new file mode 100644 index 0000000..e1bedda --- /dev/null +++ b/models/report.model.js @@ -0,0 +1,50 @@ +/** @import { ReportColumns } from "#tables/report.table.js" */ + +export class ReportModel { + /** + * @type {ReportColumns['id'] | undefined} + */ + id; + + /** + * @type {ReportColumns['date']} + */ + date; + + /** + * @type {ReportColumns['reason_call']} + */ + reason_call; + + /** + * @type {ReportColumns['job_description']} + */ + job_description; + + /** + * @type {ReportColumns['root_cause']} + */ + root_cause; + + /** + * @type {ReportColumns['applicant_id']} + */ + applicant_id; + + /** + * @type {ReportColumns['equipment_id']} + */ + equipment_id; + + /** + * @param {Omit} columns + */ + constructor(columns) { + this.date = columns.date; + this.reason_call = columns.reason_call; + this.job_description = columns.job_description; + this.root_cause = columns.root_cause; + this.applicant_id = columns.applicant_id; + this.equipment_id = columns.equipment_id; + } +} diff --git a/package.json b/package.json index d5348ff..949bdbb 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,11 @@ "#root/*": "./*", "#handlers/*": "./handlers/*", "#services/*": "./services/*", - "#schemas/*": "./schemas/*" + "#schemas/*": "./schemas/*", + "#repositories/*": "./repositories/*", + "#tables/*": "./tables/*", + "#models/*": "./models/*", + "#utils/*": "./utils/*" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/repositories/index.js b/repositories/index.js new file mode 100644 index 0000000..aa4c505 --- /dev/null +++ b/repositories/index.js @@ -0,0 +1 @@ +export * from "./report.repository.js"; diff --git a/repositories/report.repository.js b/repositories/report.repository.js new file mode 100644 index 0000000..0401937 --- /dev/null +++ b/repositories/report.repository.js @@ -0,0 +1,61 @@ +import sql from "mssql"; +import db from "#root/db.js"; +import { ReportModel } from "#models/index.js"; +import { ReportTable } from "#tables/index.js"; + +/** @import { ReportColumns } from "#tables/report.table.js" */ + +export const reportRepository = { + async getAll() { + const request = new sql.Request(db); + console.log(ReportTable.tableName); + + const reports = await request.query(`select * from dbo.${ReportTable.tableName};`); + console.log(reports); + + return reports.recordset; + }, + + /** + * @param {Omit} entry + */ + async create(entry) { + const report = new ReportModel(entry); + const request = new sql.Request(db); + + request.input("date", ReportTable.getColumn("date"), report.date); + request.input("reasonCall", ReportTable.getColumn("reason_call"), report.reason_call); + request.input("jobDesc", ReportTable.getColumn("job_description"), report.job_description); + request.input("rootCause", ReportTable.getColumn("root_cause"), report.root_cause); + request.input("applicantId", ReportTable.getColumn("applicant_id"), report.applicant_id); + request.input("eqpId", ReportTable.getColumn("equipment_id"), report.equipment_id); + + const createdReport = await request.query( + `insert into ${ReportTable.table.name} + (date, reason_call, job_description, root_cause, applicant_id, equipment_id) + output inserted.id + values (@date, @reasonCall, @jobDesc, @rootCause, @applicantId, @eqpId); + ` + ); + + report.id = Number.parseInt(createdReport.recordset[0].id, 10); + + return report; + }, + + /** + * @param {Omit[]} entries + * @param {sql.Transaction} [trx] + */ + async bulk(entries, trx) { + const request = trx ? new sql.Request(trx) : new sql.Request(db); + const table = ReportTable.createInstance(); + + for (const r of entries) { + table.rows.add(r.date, r.reason_call, r.job_description, r.root_cause, r.applicant_id, r.equipment_id); + } + + await request.bulk(table); + return table.rows.length; + } +}; diff --git a/schemas/report.schema.js b/schemas/report.schema.js index 9d58ca0..bf89015 100644 --- a/schemas/report.schema.js +++ b/schemas/report.schema.js @@ -23,4 +23,4 @@ export const reportItemSchema = y.object({ executors: y.array(y.object({ id: y.number(), fullname: y.string() })).required() }); -export const reportsSchema = y.object({ reports: y.array(reportItemSchema).required() }).required(); +export const reportsSchema = y.object({ reports: y.array(reportItemSchema).min(1).required() }).required(); diff --git a/services/reports.service.js b/services/reports.service.js index 7df9e7f..efdbd79 100644 --- a/services/reports.service.js +++ b/services/reports.service.js @@ -2,6 +2,8 @@ import xlsx from "xlsx"; import pLimit from "p-limit"; import db from "#root/db.js"; import mssql from "mssql"; +import { reportRepository } from "#repositories/index.js"; +// import * as utilsDate from "#utils/date.js"; /** @import { reportsSchema } from "#schemas/report.schema.js" */ /** @import { InferType } from "yup" */ @@ -22,13 +24,13 @@ export const reportsService = { * Читает файл Excel и возвращает данные о репорте в массиве * * @param {string} filepath - * @returns {ReportModel[]} + * @returns {ParsedReport[]} */ parseExcelFile(filepath) { const workbook = xlsx.readFile(filepath, xlsxParseOptions); const sheet = workbook.Sheets[sheetName]; - /** @type {ReportModel[]} */ + /** @type {ParsedReport[]} */ const reports = []; if (!sheet) throw new Error("Invalid reports workbook."); @@ -77,9 +79,9 @@ export const reportsService = { * Проверяет отчет на наличие связей с БД АСУ и возвращает * этот отчет с устанновленными связями или без них * - * @param {ReportModel} report + * @param {ParsedReport} report * @param {() => boolean} cancelCondFn - * @returns {Promise} + * @returns {Promise} */ async checkReportLinks(report, cancelCondFn) { const executorSurnames = extractSurnamesFromExecutorField(report.executorNames); @@ -141,9 +143,9 @@ export const reportsService = { /** * Проверяет связи отчетов с БД и возвращает результат * - * @param {ReportModel[]} reports + * @param {ParsedReport[]} reports * @param {() => boolean} cancelCondFn - * @param {(report: ReportModel) => Promise | void} [progressCb] + * @param {(report: ParsedReport) => Promise | void} [progressCb] */ analyzeParseResult(reports, cancelCondFn, progressCb) { const limit = pLimit(10); @@ -174,38 +176,29 @@ export const reportsService = { */ async syncReportsWithDatabase(reports) { const trx = new mssql.Transaction(db); - const table = new mssql.Table("dbo.gpp_report_api_report"); + const dbReports = await reportRepository.getAll(); - table.create = false; - - table.columns.add("date", mssql.Date, { nullable: false }); - table.columns.add("reason_call", mssql.NVarChar(mssql.MAX), { nullable: false }); - table.columns.add("job_description", mssql.NVarChar(mssql.MAX), { nullable: false }); - table.columns.add("root_cause", mssql.NVarChar(mssql.MAX), { nullable: true }); - table.columns.add("applicant_id", mssql.BigInt, { nullable: true }); - table.columns.add("equipment_id", mssql.BigInt, { nullable: true }); + /** + * 1. проверить на существование дубликатов и отфильтровать + * 2. начать транзакцию на множественную вставку отчетов + * 3. установить связь добавленных отчетов с исполнителями и завершить транзакцию + */ try { - const addReportsRequest = new mssql.Request(trx); - - for (const r of reports) { - let date = new Date(r.date || "2026"); - - if (!(date instanceof Date && !Number.isNaN(date.valueOf()))) date = new Date("2026"); - - table.rows.add( - date, - r.reason_call || " ", - r.job_description || " ", - r.root_cause || " ", - r.applicant?.id || null, - r.equipment?.id || null - ); - } - await trx.begin(); - await addReportsRequest.bulk(table); + const countRows = await reportRepository.bulk( + reports.map((r) => ({ + applicant_id: r.applicant?.id || null, + equipment_id: r.equipment?.id || null, + date: new Date("2026"), + job_description: r.job_description || "", + reason_call: r.reason_call || "", + root_cause: r.root_cause || "" + })), + trx + ); await trx.commit(); + return countRows; } catch (error) { console.error(error); await trx.rollback(); diff --git a/tables/base.table.js b/tables/base.table.js new file mode 100644 index 0000000..8b5e456 --- /dev/null +++ b/tables/base.table.js @@ -0,0 +1,35 @@ +import sql from "mssql"; + +export class BaseTable { + static tableName = ""; + static table = new sql.Table(this.tableName); + + /** @type {Record} */ + static columns = {}; + + /** + * @template {typeof BaseTable} T + * @this {T} + * @param {keyof T['columns']} name + */ + static getColumn(name) { + const col = this.table.columns.find((col) => col.name === name); + if (!col) throw new Error(`Unknown column with name "${String(name)}"`); + return col; + } + + /** + * @param {sql.Table} [table] + */ + static init(table) { + table = table || this.table; + // TODO: fix types + Object.entries(this.columns).forEach(([name, { type, ...opts }]) => table.columns.add(name, type, opts)); + } + + static createInstance() { + const table = new sql.Table(this.tableName); + this.init(table); + return table; + } +} diff --git a/tables/index.js b/tables/index.js new file mode 100644 index 0000000..26f1f3a --- /dev/null +++ b/tables/index.js @@ -0,0 +1 @@ +export * from "./report.table.js"; diff --git a/tables/report.table.js b/tables/report.table.js new file mode 100644 index 0000000..497ad3d --- /dev/null +++ b/tables/report.table.js @@ -0,0 +1,23 @@ +import sql from "mssql"; +import { BaseTable } from "./base.table.js"; + +/** + * @typedef {{ id: number; date: Date; reason_call: string; job_description: string; root_cause: string | null; applicant_id: number | null; equipment_id: number | null; }} ReportColumns + */ + +export class ReportTable extends BaseTable { + static tableName = "gpp_report_api_report"; + static table = new sql.Table(this.tableName); + + static columns = { + date: { type: sql.Date, nullable: false }, + reason_call: { type: sql.NVarChar(sql.MAX), nullable: false }, + job_description: { type: sql.NVarChar(sql.MAX), nullable: false }, + root_cause: { type: sql.NVarChar(sql.MAX), nullable: true }, + applicant_id: { type: sql.BigInt, nullable: true }, + equipment_id: { type: sql.BigInt, nullable: true } + }; +} + +ReportTable.table.create = false; +ReportTable.init(); diff --git a/utils/date.js b/utils/date.js new file mode 100644 index 0000000..7d3fb84 --- /dev/null +++ b/utils/date.js @@ -0,0 +1,7 @@ +/** + * @param {unknown} date + * @returns {boolean is Date} + */ +export function isDateValid(date) { + return date instanceof Date && !Number.isNaN(date.valueOf()); +} diff --git a/utils/index.js b/utils/index.js new file mode 100644 index 0000000..82e6eff --- /dev/null +++ b/utils/index.js @@ -0,0 +1 @@ +export * from "./date.js";