879 lines
51 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Сервис интеграции ПП Парус 8 с WEB API
Дополнительный модуль: Sheet Parser (SHP)
*/
//------------------------------
// Подключение внешних библиотек
//------------------------------
const xml2js = require("xml2js"); //Конвертация XML в JSON и JSON в XML
const XLSX = require("./shp_utils/node_modules/xlsx"); //Парсер табличных данных
//---------------------
// Глобальные константы
//---------------------
//Количество строк, загружаемых за одно обращение к серверу
const NIMPORT_BUFF_LEN = 500;
//Форматы файлов данных */
const NSHPFL_FORMAT_XLS = 0; //MS Excel (xls, xlsx)
const NSHPFL_FORMAT_ODS = 1; //OpenDocument Spreadsheet (ods)
const NSHPFL_FORMAT_CSV = 2; //Comma-Separated Values (csv)
const NSHPFL_FORMAT_DBF = 3; //Database File (dbf)
//Кодировки файлов данных */
const NSHPFL_ENC_DEFAULT = 0; //По умолчанию
const NSHPFL_ENC_WIN1251 = 1; //Windows-1251
const NSHPFL_ENC_UTF8 = 2; //UTF-8
//------------------------
// Вспомогательные функции
//------------------------
//Разбор XML (обёртка для async/await)
const parseXML = ({ sXML, options }) => {
//Возвращаем промис для использования async/await
return new Promise((resolve, reject) => {
//Разбираем XML
xml2js.parseString(sXML, options, (err, result) => {
//Если при разборе возникла ошибка
if (err) reject(err);
//Если всё успешно - возвращаем результат
else resolve(result);
});
});
};
//Конвертация в XML
const toXML = ({ rootName, data, headless = false }) => {
//Формируем XML
const builder = new xml2js.Builder({ rootName, headless });
return builder.buildObject(data);
};
//Получение текстового представления ячейки
const getCellText = cl => {
//Если ячейки нет - возвращаем пустую строку
if (!cl) return "";
//Если есть значение v - берём его
if (cl.v !== null && cl.v !== undefined) return String(cl.v);
//Если есть форматированное значение w - берём его
if (cl.w !== null && cl.w !== undefined) return String(cl.w);
//Если значений нет - возвращаем пустую строку
return "";
};
//Построение индекса колонок по заголовкам (имя заголовка -> номер колонки)
const buildHeadersIndex = ({ sheet, sheetData, nHeaderLineNo }) => {
//Если листа нет - индекса заголовков нет
if (!sheet) return null;
//Если данных листа нет - индекса заголовков нет
if (!Array.isArray(sheetData) || sheetData.length === 0) return null;
//Номер строки заголовков должен быть валидным (1..)
const nHdrLine = Number.isFinite(nHeaderLineNo) && nHeaderLineNo > 0 ? nHeaderLineNo : null;
//Если номер строки заголовков невалиден - индекса заголовков нет
if (!nHdrLine) return null;
//Строка заголовков
const headerRow = sheetData[nHdrLine - 1] || [];
//Если строки заголовков нет - индекса заголовков нет
if (!Array.isArray(headerRow) || headerRow.length === 0) return null;
//Создаём резолвер объединённых ячеек
const { resolveCell } = createMergedCellResolver(sheet);
//Результат индекса
const idx = new Map();
//Обходим все колонки строки заголовков
for (let c = 0; c < headerRow.length; c++) {
//Номер колонки
const colNo = c + 1;
//Резолвим ячейку заголовка
const cl = resolveCell(nHdrLine - 1, c, (headerRow || [])[c]);
//Текст заголовка
const sName = getCellText(cl).trim();
//Пустые заголовки не индексируем
if (!sName) continue;
//Ключ заголовка
const key = sName.toUpperCase();
//Если такой заголовок уже есть - не перезаписываем
if (idx.has(key)) continue;
//Сохраняем номер колонки
idx.set(key, colNo);
}
//Если индекс пустой - возвращаем null
if (idx.size === 0) return null;
//Возвращаем индекс
return idx;
};
//Формирование XML заголовков таблицы
const buildTableHeadersXML = ({ workbook, sSheetName, aColumns, nColumnsCount, nHeaderLineNo }) => {
//Если книги нет - заголовки построить нельзя
if (!workbook) return null;
//Если имя листа не задано - заголовки построить нельзя
if (!sSheetName) return null;
//Получаем лист
const sheet = workbook.Sheets?.[sSheetName] || null;
//Если листа нет - заголовки построить нельзя
if (!sheet) return null;
//Dense-данные листа (строки/ячейки)
const sheetData = sheet?.["!data"] || [];
//Если данных нет - заголовки построить нельзя
if (!Array.isArray(sheetData) || sheetData.length === 0) return null;
//Номер строки заголовков должен быть валидным (1..)
const nHdrLine = Number.isFinite(nHeaderLineNo) && nHeaderLineNo > 0 ? nHeaderLineNo : 1;
//Получаем строку заголовков
const headerRow = sheetData[nHdrLine - 1] || [];
//Создаём резолвер объединённых ячеек
const { hasMerges, resolveCell } = createMergedCellResolver(sheet);
//Нормализуем количество колонок таблицы
const nCols = Number.isFinite(nColumnsCount) && nColumnsCount > 0 ? nColumnsCount : 0;
//Если колонок нет - формируем пустой RHEADERS
if (!nCols) return toXML({ rootName: "RHEADERS", data: {}, headless: true });
//Список заголовков (RHEADER)
const rHeaders = [];
//Если задан список колонок
if (Array.isArray(aColumns) && aColumns.length > 0) {
//Обходим все выбранные колонки в заданном порядке
for (let i = 0; i < aColumns.length; i++) {
//Номер исходной колонки
const colNo = aColumns[i];
//Если номер колонки невалиден - пропускаем
if (!Number.isFinite(colNo) || colNo < 1) continue;
//Резолвим ячейку заголовка
const cl = resolveCell(nHdrLine - 1, colNo - 1, (headerRow || [])[colNo - 1]);
//Текст заголовка
const sName = getCellText(cl).trim();
//Добавляем RHEADER
rHeaders.push({ NCOLUMN: colNo, SCOLUMN: sName });
}
}
//Иначе - заголовки строим по фактическому набору колонок (1..nCols)
else {
//Определяем количество колонок в строке заголовков
const nHeaderRowCols = hasMerges ? nCols : (headerRow || []).length;
const nIter = Math.max(nCols, nHeaderRowCols, 0);
//Обходим колонки 1..nIter
for (let c = 0; c < nIter; c++) {
//Номер колонки
const colNo = c + 1;
//Резолвим ячейку заголовка
const cl = resolveCell(nHdrLine - 1, c, (headerRow || [])[c]);
//Текст заголовка
const sName = getCellText(cl).trim();
//Добавляем RHEADER
rHeaders.push({ NCOLUMN: colNo, SCOLUMN: sName });
}
}
//Собираем XML заголовков
return toXML({
rootName: "RHEADERS",
data: {
...(rHeaders.length > 0 ? { RHEADER: rHeaders } : {})
},
headless: true
});
};
//Преобразование "одиночное значение | массив | undefined" -> массив
const toArray = value => {
//Если значения нет - возвращаем пустой массив
if (!value) return [];
//Если уже массив - возвращаем как есть, иначе оборачиваем
return Array.isArray(value) ? value : [value];
};
//Разбор параметров выполнения SHP (clOptions: XML -> JSON)
const parseShpOptionsXML = async sOptionsXML => {
//Если параметр отсутствует или не строка - параметров выполнения нет
if (!sOptionsXML || !(sOptionsXML instanceof String || typeof sOptionsXML === "string")) return null;
//Если строка пустая - параметров выполнения нет
if (!String(sOptionsXML).trim()) return null;
//Результат парсинга XML
let parseRes = null;
try {
//Парсим XML
parseRes = await parseXML({
sXML: sOptionsXML,
options: {
explicitArray: false,
mergeAttrs: true,
valueProcessors: [xml2js.processors.parseNumbers, xml2js.processors.parseBooleans]
}
});
} catch (e) {
//Ошибка разбора параметров выполнения
throw new Error(`Ошибка разбора XML параметров выполнения: ${e.message || e}`);
}
//Корневой элемент параметров выполнения
const root = parseRes?.OPTIONS || null;
//Если корня нет - параметров выполнения нет
if (!root) return null;
//OPTION может быть одиночным объектом или массивом
const options = toArray(root.OPTION).filter(Boolean);
//Возвращаем нормализованный объект
return { options };
};
//Преобразование номера/имени колонки в число (A->1, B->2, AA->27, ...)
const normalizeColumn = value => {
//Если значение отсутствует - возвращаем null
if (value === null || value === undefined) return null;
//Приводим к строке и убираем пробелы
const sVal = String(value).trim();
//Пустое значение - нечего нормализовать
if (!sVal) return null;
//Если это число в строковом представлении
if (/^\d+$/.test(sVal)) {
//Преобразуем в число
const n = parseInt(sVal, 10);
//Колонки начинаются с 1
return Number.isFinite(n) && n > 0 ? n : null;
}
//Приводим буквы к верхнему регистру
const s = sVal.toUpperCase();
//Ожидаем только латинские буквы (A..Z)
if (!/^[A-Z]+$/.test(s)) return null;
//Результат в десятичном виде
let n = 0;
//Переводим base-26 представление в десятичное
for (let i = 0; i < s.length; i++) {
//A=1 ... Z=26
n = n * 26 + (s.charCodeAt(i) - 64);
}
//Возвращаем результат, если он валиден
return n > 0 ? n : null;
};
//Преобразование имени/номера колонки в число с учётом заголовков
const normalizeColumnWithHeaders = ({ value, headersIndex }) => {
//Если значение отсутствует - возвращаем null
if (value === null || value === undefined) return null;
//Приводим к строке и убираем пробелы
const sVal = String(value).trim();
//Пустое значение - нечего нормализовать
if (!sVal) return null;
//Если задан индекс заголовков - сначала ищем по нему
if (headersIndex && headersIndex instanceof Map) {
//Ключ поиска
const key = sVal.toUpperCase();
//Пробуем найти колонку по заголовку
const nByHdr = headersIndex.get(key);
//Если нашли валидный номер - возвращаем
if (Number.isFinite(nByHdr) && nByHdr > 0) return nByHdr;
}
//Если по заголовку не нашли - преобразуем имя колонки в число
return normalizeColumn(sVal);
};
//Формирование списка индексов (1..max) по описанию (RANGE/ENUM/COUNT)
const buildIndexList = ({ selector, nMax, normalizeItem, getDefaultFrom }) => {
//Нормализуем максимум
const nMaxSafe = Number.isFinite(nMax) && nMax > 0 ? nMax : 0;
//Если селектора нет или максимум не задан - считаем что фильтра нет
if (!selector || !nMaxSafe) return null;
//Тип выборки (RANGE/ENUM/COUNT)
const sType = String(selector.SADDRESS_TYPE || "")
.trim()
.toUpperCase();
//Если тип не задан - считаем что фильтра нет
if (!sType) return null;
//Добавление уникальных значений в множество
const pushUnique = (set, value) => {
//Должно быть число
if (!Number.isFinite(value)) return;
//Должно попадать в диапазон 1..nMaxSafe
if (value < 1 || value > nMaxSafe) return;
//Добавляем уникально
set.add(value);
};
//Множество для уникальных значений
const res = new Set();
//Адрес-диапазон
if (sType === "RANGE") {
//Узел диапазона
const range = selector.RADDRESS_RANGE || null;
//Начало диапазона обязательно
const nFrom = normalizeItem?.(range?.SVALUE_FROM);
//Если начала нет - фильтр считаем некорректным
if (!Number.isFinite(nFrom) || nFrom < 1) return null;
//Конец диапазона опционален
let nTo = normalizeItem?.(range?.SVALUE_TO);
//Если конец не указан - берём до конца
if (!Number.isFinite(nTo) || nTo < nFrom) nTo = nMaxSafe;
//Заполняем диапазон
for (let i = nFrom; i <= Math.min(nTo, nMaxSafe); i++) pushUnique(res, i);
//Возвращаем отсортированный список
return Array.from(res).sort((a, b) => a - b);
}
//Адрес-перечисление
if (sType === "ENUM") {
//Узел перечисления
const en = selector.RADDRESS_ENUM || null;
//Значение может быть одиночным значением или массивом
for (const v of toArray(en?.SVALUE)) pushUnique(res, normalizeItem?.(v));
//Возвращаем отсортированный список
return Array.from(res).sort((a, b) => a - b);
}
//Адрес-количество
if (sType === "COUNT") {
//Узел количества
const cnt = selector.RADDRESS_COUNT || null;
//Количество берём из SVALUE
const nCount = parseInt(String(cnt?.SVALUE ?? "").trim(), 10);
//Количество должно быть > 0
if (!Number.isFinite(nCount) || nCount <= 0) return null;
//Признак, что старт задан явно
const bHasFrom = cnt?.SVALUE_FROM !== null && cnt?.SVALUE_FROM !== undefined && String(cnt?.SVALUE_FROM).trim() !== "";
//Старт опционален (если не задан - берём вычисляемое значение, иначе 1)
const nFromRaw = bHasFrom ? normalizeItem?.(cnt?.SVALUE_FROM) : (getDefaultFrom?.() ?? 1);
//Если старт некорректен - начинаем с 1
const nFromSafe = Number.isFinite(nFromRaw) && nFromRaw > 0 ? nFromRaw : 1;
//Добавляем нужное количество индексов
for (let i = nFromSafe; i < nFromSafe + nCount; i++) pushUnique(res, i);
//Возвращаем отсортированный список
return Array.from(res).sort((a, b) => a - b);
}
//Неизвестный тип
return null;
};
//Формирование списка всех индексов (1..nMax)
const buildAllIndexList = ({ nMax }) => {
//Нормализуем максимум
const nMaxSafe = Number.isFinite(nMax) && nMax > 0 ? nMax : 0;
//Если максимум некорректен - возвращаем пустой список
if (!nMaxSafe) return [];
//Создаём массив нужной длины
const res = new Array(nMaxSafe);
//Заполняем индексами
for (let i = 0; i < nMaxSafe; i++) res[i] = i + 1;
//Возвращаем результат
return res;
};
//Проверка наличия данных в листе
const isSheetNotEmpty = ({ workbook, sSheetName }) =>
//Лист не пустой, если есть dense-данные и они содержат хотя бы одну строку
Array.isArray(workbook?.Sheets[sSheetName]?.["!data"]) && workbook?.Sheets[sSheetName]?.["!data"].length > 0;
//Подсчёт предельного количества столбцов в листе
const getSheetColumnsCount = ({ workbook, sSheetName }) => {
//Получаем dense-данные листа
const rows = workbook?.Sheets?.[sSheetName]?.["!data"];
//Если данных нет - возвращаем 0
if (!Array.isArray(rows) || rows.length === 0) return 0;
//Возвращаем максимальную длину строки (количество колонок)
return rows.reduce((cnt, curRow) => Math.max(cnt, (curRow || []).length), 0);
};
//Проверка, что ячейка содержит данные (v/w)
const isCellHasData = cl => {
//Если ячейки нет - данных нет
if (!cl) return false;
//Если есть значение v - проверяем его на пустоту
if (cl.v !== null && cl.v !== undefined && String(cl.v) !== "") return true;
//Если есть текстовое значение w - проверяем его на пустоту
if (cl.w !== null && cl.w !== undefined && String(cl.w) !== "") return true;
//Данных нет
return false;
};
//Поиск первой строки, где есть данные
const getFirstDataLineNo = ({ sheetData }) => {
//Если данных листа нет - возвращаем 1
if (!Array.isArray(sheetData) || sheetData.length === 0) return 1;
//Обходим строки сверху вниз
for (let r = 0; r < sheetData.length; r++) {
//Строка листа (массив ячеек)
const row = sheetData[r] || [];
//Обходим ячейки строки
for (let c = 0; c < row.length; c++) {
//Если нашли данные - возвращаем номер строки
if (isCellHasData(row[c])) return r + 1;
}
}
//Если данных не нашли - возвращаем 1
return 1;
};
//Поиск первой колонки, где есть данные
const getFirstDataColumnNo = ({ sheetData }) => {
//Если данных листа нет - возвращаем 1
if (!Array.isArray(sheetData) || sheetData.length === 0) return 1;
//Минимальный индекс колонки с данными
let minCol = null;
//Обходим строки
for (let r = 0; r < sheetData.length; r++) {
//Строка листа (массив ячеек)
const row = sheetData[r] || [];
//Обходим ячейки строки
for (let c = 0; c < row.length; c++) {
//Если данных нет - пропускаем
if (!isCellHasData(row[c])) continue;
//Фиксируем минимальную колонку с данными
minCol = minCol === null ? c : Math.min(minCol, c);
}
}
//Если данных не нашли - возвращаем 1
if (minCol === null) return 1;
//Возвращаем номер колонки
return minCol + 1;
};
//Построение таблиц для импорта по параметрам выполнения
const buildDataTablesByOptions = ({ workbook, nShpFileRn, options, sDataSheetName, bIgnoreSheetName, bHasHeaders }) => {
//Результирующий список таблиц
const res = [];
//Обходим все OPTION
for (const opt of toArray(options)) {
//Имя листа, к которому относится параметр
const sSheetName = String(opt?.SSHEET || "").trim();
//Если имя листа не задано - пропускаем
if (!sSheetName) continue;
//Если лист пустой - пропускаем
if (!isSheetNotEmpty({ workbook, sSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName })) continue;
//Список таблиц для данного параметра
const rTables = toArray(opt?.RTABLES?.RTABLE);
//Если таблицы не указаны - пропускаем
if (rTables.length === 0) continue;
//Фактическое количество колонок листа
const nColumnsCountRaw = getSheetColumnsCount({ workbook, sSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName });
//Фактическое количество строк листа
const nLinesCountRaw = (workbook.Sheets[bIgnoreSheetName ? sDataSheetName : sSheetName]["!data"] || []).length;
//Обходим описанные таблицы
for (const rt of rTables) {
//Имя таблицы
const sTableName = rt?.SNAME ? String(rt.SNAME).trim() : null;
//Лист, откуда берём данные
const sheet = workbook?.Sheets?.[bIgnoreSheetName ? sDataSheetName : sSheetName] || null;
//Dense-данные листа
const sheetData = sheet?.["!data"] || [];
//Номер строки заголовков
const nHeaderLineNo = bHasHeaders ? getFirstDataLineNo({ sheetData }) : null;
//Индекс заголовков
const headersIndex = bHasHeaders ? buildHeadersIndex({ sheet, sheetData, nHeaderLineNo }) : null;
//Формируем список колонок по описателю
const aCols = buildIndexList({
selector: rt?.RCOLUMNS || null,
nMax: nColumnsCountRaw,
normalizeItem: v => normalizeColumnWithHeaders({ value: v, headersIndex }),
getDefaultFrom: () => getFirstDataColumnNo({ sheetData })
});
//Формируем список строк по описателю
const aLines = buildIndexList({
selector: rt?.RLINES || null,
nMax: nLinesCountRaw,
normalizeItem: v => {
//Если значения нет - не нормализуем
if (v === null || v === undefined) return null;
//Парсим строку как число
const n = parseInt(String(v).trim(), 10);
//Индекс строки должен быть положительным
return Number.isFinite(n) && n > 0 ? n : null;
},
getDefaultFrom: () => getFirstDataLineNo({ sheetData })
});
//Добавляем описание таблицы в результат
res.push({
sSheetName,
sDataSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName,
sTableName,
nShpFileRn,
aColumns: aCols,
aLines
});
}
}
//Возвращаем список таблиц
return res;
};
//Преобразование значения ячейки в формат импорта
const makeImportCell = ({ cl, n }) => {
//Нормализуем номер колонки (1..)
const nCol = Number.isFinite(n) && n > 0 ? n : null;
//Если ячейки нет - пишем пустую ячейку
if (!cl) return { $: { t: "z", n: nCol }, v: null };
//Тип ячейки
const t = cl.t || "z";
//Значение ячейки (даты приводим к ISO-строке)
const v = (t === "d" ? cl.v?.toISOString?.() : cl.v) || cl.w || null;
//Возвращаем формат ячейки для импорта
return { $: { t, n: nCol }, v };
};
//Резолвер значений для объединённых ячеек
const createMergedCellResolver = sheet => {
//Считываем описания объединений ячеек (если они есть)
const merges = Array.isArray(sheet?.["!merges"]) ? sheet["!merges"] : [];
//Если объединений нет - возвращаем быстрый резолвер без дополнительной логики
if (merges.length === 0) {
return {
hasMerges: false,
resolveCell: (_r, _c, cell) => cell || null
};
}
//Создаём индекс объединений по строкам
const byRow = new Map();
//Обходим все описания объединений
for (const m of merges) {
//Начальная координата диапазона объединения
const s = m?.s;
//Конечная координата диапазона объединения
const e = m?.e;
//Если координат нет - пропускаем элемент
if (!s || !e) continue;
//Нормализуем начало по строкам
const rFrom = Math.min(s.r ?? 0, e.r ?? 0);
//Нормализуем конец по строкам
const rTo = Math.max(s.r ?? 0, e.r ?? 0);
//Нормализуем начало по колонкам
const cFrom = Math.min(s.c ?? 0, e.c ?? 0);
//Нормализуем конец по колонкам
const cTo = Math.max(s.c ?? 0, e.c ?? 0);
//Проверяем валидность нормализованных координат
const bOk = Number.isFinite(rFrom) && Number.isFinite(rTo) && Number.isFinite(cFrom) && Number.isFinite(cTo);
//Если координаты невалидны - пропускаем объединение
if (!bOk) continue;
//Индексируем объединение по всем строкам диапазона
for (let r = rFrom; r <= rTo; r++) {
//Текущий список объединений по строке (если уже есть)
const arr = byRow.get(r);
//Сохраняем диапазон и “ведущую” ячейку
const item = { rFrom, rTo, cFrom, cTo, rLead: rFrom, cLead: cFrom };
//Если массив по строке уже существует - добавляем
if (arr) arr.push(item);
//Иначе создаём новый список по строке
else byRow.set(r, [item]);
}
}
//Получаем dense-данные листа (для доступа к “ведущей” ячейке)
const data = sheet?.["!data"] || [];
//Возвращаем резолвер с поддержкой объединений
return {
hasMerges: true,
resolveCell: (r, c, cell) => {
//Если ячейка уже есть - возвращаем её без изменений
if (cell) return cell;
//Получаем список объединений для текущей строки
const rowMerges = byRow.get(r);
//Если объединений в этой строке нет - возвращаем null
if (!rowMerges || rowMerges.length === 0) return null;
//Проверяем попадание колонки в один из диапазонов объединений
for (const m of rowMerges) {
//Если колонка вне диапазона - проверяем следующее объединение
if (c < m.cFrom || c > m.cTo) continue;
//Возвращаем “ведущую” ячейку диапазона
return (data[m.rLead] || [])[m.cLead] || null;
}
//Если не попали ни в одно объединение - возвращаем null
return null;
}
};
};
//------------
// Тело модуля
//------------
//Обработчик "До"
const importBefore = async ({ queue, dbConn }) => {
//Если задано тело сообщения
if (queue.blMsg) {
//В теле должен быть рег. номер файла данных (EXSEXTSHPFL)
const nShpFileRn = parseInt(queue.blMsg.toString(), 10);
//Если там рег. номер (число)
if (!isNaN(nShpFileRn)) {
//Читаем по нему запись файла данных
const shpFileData = await dbConn.executeStored({
connection: dbConn.connection,
sName: "PKG_EXS_EXT_SHP.FILE_GET",
inPrms: { NEXSEXTSHPFL: nShpFileRn },
outPrms: { RFILE: dbConn.connector.DT_CURSOR }
});
//Если запись найдена
if (shpFileData.RFILE.length === 1) {
//Разбираем её
try {
//Признак наличия заголовков
const bHasHeaders = String(shpFileData.RFILE[0]?.nHasHeaders ?? "0").trim() === "1";
//Формат файла данных
const nFileFormat = shpFileData.RFILE[0]?.nFormat;
//Заголовки формируем только для CSV/DBF и только если они есть
const bNeedSendHeaders = bHasHeaders && [NSHPFL_FORMAT_CSV, NSHPFL_FORMAT_DBF].includes(nFileFormat);
//Объект рабочей книги
let workbook = null;
try {
//Кодировка файла данных
const nEnc = shpFileData.RFILE[0].nEnc;
//Кодовая страница (параметры чтения XLSX)
const cp = {};
//Байты файла данных
let blData = shpFileData.RFILE[0].blFileData;
//Если это CSV
if (nFileFormat === NSHPFL_FORMAT_CSV) {
//Если указана Win-1251 - перекодируем CSV в UTF-8
if (nEnc === NSHPFL_ENC_WIN1251) {
//Декодируем исходные байты как windows-1251
const s = new TextDecoder("windows-1251").decode(blData);
//Преобразуем строку обратно в байты UTF-8
blData = Buffer.from(s);
}
//Задаём codepage UTF-8
cp.codepage = 65001;
}
//Если это DBF
else if (nFileFormat === NSHPFL_FORMAT_DBF) {
//Если указана Win-1251
if (nEnc === NSHPFL_ENC_WIN1251)
//Задаём codepage 1251
cp.codepage = 1251;
else
//Задаём codepage UTF-8
cp.codepage = 65001;
}
//Выполняем парсинг
workbook = XLSX.read(blData, { dense: true, cellDates: true, ...cp });
} catch (e) {
//Ошибка чтения/парсинга файла данных
throw new Error(`Ошибка разбора файла данных: ${e.message}`);
}
//Если есть сведения о листахх
if (workbook && workbook?.SheetNames?.length > 0) {
//Признак формата, где листов фактически нет (csv/dbf) или их не нужно учитывать
const bIgnoreSheetName = ![NSHPFL_FORMAT_XLS, NSHPFL_FORMAT_ODS].includes(nFileFormat);
//Имя листа с данными (для csv/dbf используем первый лист книги)
const sDataSheetName = workbook.SheetNames[0];
//Разбор параметров выполнения (если есть)
const shpOptions = await parseShpOptionsXML(shpFileData.RFILE[0]?.clOptions);
//Определим буфер для хранения таблиц файла данных
const dataTables = [];
//Формирование списка таблиц для импорта
const tablesSpec = shpOptions
? buildDataTablesByOptions({
workbook,
nShpFileRn,
options: shpOptions.options,
sDataSheetName,
bIgnoreSheetName,
bHasHeaders
})
: (bIgnoreSheetName
? [sDataSheetName]
: workbook.SheetNames.filter(sSheetName => isSheetNotEmpty({ workbook, sSheetName }))
).map(sSheetName => ({
sSheetName,
sDataSheetName: bIgnoreSheetName ? sDataSheetName : sSheetName,
sTableName: null,
nShpFileRn,
aColumns: null,
aLines: null
}));
//Регистрируем таблицы на сервере
for (const tbl of tablesSpec) {
//Считаем фактическое количество колонок листа
const nColumnsCountRaw = getSheetColumnsCount({ workbook, sSheetName: tbl.sDataSheetName || tbl.sSheetName });
//Считаем фактическое количество строк листа
const nLinesCountRaw = (workbook.Sheets[tbl.sDataSheetName || tbl.sSheetName]["!data"] || []).length;
//Список колонок (или null - значит все)
const aColumns = Array.isArray(tbl.aColumns) ? tbl.aColumns : null;
//Список строк (или null - значит все)
const aLines = Array.isArray(tbl.aLines) ? tbl.aLines : null;
//Количество колонок для описания таблицы
const nColumnsCount = aColumns ? aColumns.length : nColumnsCountRaw;
//Определяем лист, с которого читаем данные
const sDataSheetName = tbl.sDataSheetName || tbl.sSheetName;
//Dense-данные листа
const sheetData = workbook?.Sheets?.[sDataSheetName]?.["!data"] || [];
//Номер строки заголовков (если заголовки нужны - берём первую строку с данными, иначе null)
const nHeaderLineNo = bNeedSendHeaders ? getFirstDataLineNo({ sheetData }) : null;
//Количество строк для описания таблицы
const nLinesCount = (() => {
//Если заголовков нет - количество строк
const nRaw = aLines ? aLines.length : nLinesCountRaw;
//Если заголовки не требуются - возвращаем как есть
if (!bNeedSendHeaders) return nRaw;
//Если строк нет - нечего исключать
if (!Number.isFinite(nRaw) || nRaw <= 0) return nRaw;
//Если номер заголовка невалиден
if (!Number.isFinite(nHeaderLineNo) || nHeaderLineNo <= 0) return nRaw;
//Если выбран конкретный список строк - исключаем заголовок только если он туда попал
if (aLines) return aLines.includes(nHeaderLineNo) ? Math.max(nRaw - 1, 0) : nRaw;
//Если строки не фильтруются - заголовок считаем частью данных листа и исключаем его
return nHeaderLineNo <= nLinesCountRaw ? Math.max(nRaw - 1, 0) : nRaw;
})();
//Формируем XML заголовков (только если требуется отправка заголовков)
const sHeadersXML = bNeedSendHeaders
? buildTableHeadersXML({
workbook,
sSheetName: sDataSheetName,
aColumns,
nColumnsCount,
nHeaderLineNo
})
: null;
try {
//Создаём таблицу в БД
const shpFileDataTable = await dbConn.executeStored({
connection: dbConn.connection,
sName: "PKG_EXS_EXT_SHP.FILE_TABLE_ADD",
inPrms: {
NEXSEXTSHPFL: nShpFileRn,
SSHEET: tbl.sSheetName,
STBL: tbl.sTableName,
NCOLS: nColumnsCount,
...(sHeadersXML ? { CHEADERS: sHeadersXML } : {}),
NLINES: nLinesCount
},
outPrms: { NEXSEXTSHPFLTB: dbConn.connector.DT_NUMBER }
});
//Сохраняем данные таблицы для дальнейшей загрузки строк
dataTables.push({
sSheetName: tbl.sSheetName,
sDataSheetName: tbl.sDataSheetName || tbl.sSheetName,
sTableName: tbl.sTableName,
nTableRn: shpFileDataTable.NEXSEXTSHPFLTB,
nColumnsCount,
nLinesCount,
aColumns,
aLines,
nHeaderLineNo
});
} catch (e) {
//Ошибка регистрации таблицы
throw new Error(
`Ошибка сохранения описания таблицы "${tbl.sTableName || "<НЕ УКАЗАНА>"}" листа "${tbl.sSheetName || "<НЕ УКАЗАН>"}": ${e.message}`
);
}
}
//Теперь загружаем таблицы - обходим их последовательно
for (const dataTable of dataTables) {
//Буфер строк для порционной загрузки
let impBuff = [];
//Сброс буфера
const flushBuff = async () => {
//Если буфер пуст - нечего сохранять
if (impBuff.length === 0) return;
//Преобразуем буфер в XML
let sXML = null;
try {
//Сборка XML для порции строк
sXML = toXML({ rootName: "lines", data: impBuff });
} catch (e) {
//Ошибка сборки XML
throw new Error(`Ошибка преобразования буфера в XML: ${e.message}.`);
}
//Сохраняем на сервере
const nLineFrom = impBuff[0].line.$.n;
const nLineTo = impBuff[impBuff.length - 1].line.$.n;
try {
//Сохраняем порцию строк в БД
await dbConn.executeStored({
connection: dbConn.connection,
sName: "PKG_EXS_EXT_SHP.FILE_TABLE_LINE_ADD",
inPrms: {
NEXSEXTSHPFLTB: dataTable.nTableRn,
NLN_FROM: nLineFrom,
NLN_TO: nLineTo,
BDT: Buffer.from(sXML)
}
});
} catch (e) {
//Ошибка сохранения порции строк
throw new Error(
`Ошибка сохранения строк (c ${nLineFrom} - по ${nLineTo}) таблицы "${dataTable.sTableName || "<НЕ УКАЗАНА>"}" листа "${dataTable.sSheetName || "<НЕ УКАЗАН>"}": ${e.message}`
);
}
//Очищаем буфер после успешной отправки
impBuff = [];
};
//Получаем объект листа
const sheet = workbook.Sheets[dataTable.sDataSheetName || dataTable.sSheetName];
//Получаем dense-данные листа (строки/ячейки)
const sheetData = sheet?.["!data"] || [];
//Создаём резолвер объединённых ячеек
const { hasMerges, resolveCell } = createMergedCellResolver(sheet);
//Формируем список строк (или берём все)
const aLines = dataTable.aLines ? dataTable.aLines : buildAllIndexList({ nMax: sheetData.length });
//Список колонок (или null - значит все)
const aColumns = dataTable.aColumns;
//Предыдущая строка для контроля непрерывности (NLN_FROM/NLN_TO)
let prevLineNo = null;
for (const lineNo of aLines) {
//Защита от невалидных индексов
if (!Number.isFinite(lineNo) || lineNo < 1 || lineNo > sheetData.length) continue;
//Если это строка заголовков - не отправляем её как данные
if (Number.isFinite(dataTable.nHeaderLineNo) && lineNo === dataTable.nHeaderLineNo) continue;
//Если есть разрыв диапазона строк - сбрасываем буфер (важно для NLN_FROM/NLN_TO)
if (prevLineNo !== null && lineNo !== prevLineNo + 1) await flushBuff();
//Получаем строку листа
const sheetLine = sheetData[lineNo - 1] || [];
//Формируем ячейки строки с учётом фильтра колонок и объединений
const cells = aColumns
? aColumns.map(colNo =>
//Для выбранной колонки получаем ячейку (с учётом объединения) и приводим к формату импорта
makeImportCell({
//Резолвим ячейку по исходной колонке
cl: resolveCell(lineNo - 1, colNo - 1, (sheetLine || [])[colNo - 1]),
//Пишем номер исходной колонки
n: colNo
})
)
: (() => {
//Определяем количество колонок для строки (в режиме объединений берём из описания таблицы)
const nCols = hasMerges ? dataTable.nColumnsCount : (sheetLine || []).length;
//Готовим массив ячеек нужной длины
const res = new Array(nCols);
//Обходим все колонки строки
for (let c = 0; c < nCols; c++) {
//Резолвим ячейку (с учётом объединения) и приводим к формату импорта
res[c] = makeImportCell({
//Резолвим ячейку по исходной колонке
cl: resolveCell(lineNo - 1, c, (sheetLine || [])[c]),
//Пишем номер исходной колонки
n: c + 1
});
}
//Возвращаем массив ячеек строки
return res;
})();
//Добавляем строку в буфер импорта
impBuff.push({
line: {
$: { n: lineNo },
cell: cells
}
});
//Запоминаем предыдущую строку
prevLineNo = lineNo;
//Если набрали порцию - отправляем
if (impBuff.length === NIMPORT_BUFF_LEN) await flushBuff();
}
//Сохраняем остаток буфера
await flushBuff();
}
}
} catch (e) {
//Любая ошибка импорта
throw new Error(e.message || "Неожиданная ошибка.");
}
}
//Не нашли запись файла данных
else throw new Error("Файл данных не определен.");
}
//Не смогли считать рег. номер из тела сообщения
else throw new Error("Некорректный идентификатор файла данных для разбора.");
}
//Тело сообщения не задано
else throw new Error("Нет данных для разбора.");
//Обработку закончили - продолжать не надо, мы всё сделали в обработчике
return { bStopPropagation: true };
};
//-----------------
// Интерфейс модуля
//-----------------
exports.importBefore = importBefore;