UNPKG

@opengis/fastify-table

Version:

core-plugins

290 lines (239 loc) 10.4 kB
/* eslint-disable no-param-reassign */ /* eslint-disable no-plusplus */ /* eslint-disable no-await-in-loop */ /* eslint-disable no-nested-ternary */ import path from 'node:path'; import { createHash } from 'node:crypto'; import { existsSync } from 'node:fs'; import { mkdir, readFile, rm, writeFile, } from 'node:fs/promises'; import config from '../../../../config.js'; import logger from '../../../plugins/logger/getLogger.js'; import getTemplate from '../../../plugins/table/funcs/getTemplate.js'; import getMeta from '../../../plugins/pg/funcs/getMeta.js'; import pgClients from '../../../plugins/pg/pgClients.js'; import eventStream from '../../../plugins/util/funcs/eventStream.js'; import getData from '../../../plugins/table/funcs/getData.js'; import getFolder from '../../../plugins/crud/funcs/utils/getFolder.js'; import metaFormat from '../../../plugins/table/funcs/metaFormat/index.js'; import jsonToXls from './utils/jsonToXls.js'; import jsonToCsv from './utils/jsonToCsv.js'; import formatResult from './utils/formatResult.js'; const startStreamWithTotal = 10000; const rootDir = getFolder(config, 'local'); /** * Експорт даних з таблиці * * @method GET * @alias exportTable * @type api * @tag export * @summary Експорт даних у таблицю(xlsx, csv, json, geojson) * @priority 1 * @example * /api/export?table=com_property.subjects.table&format=csv&cols=economy_type,name_ua * @param {String} format Формат документу на виході * @param {Boolean} nocache Чи використовувати кеш * @param {String} table Таблиця в БД * @param {String|Number} filter Параметр фільтру для застосування до експортованих даних * @errors 400, 500 * @returns {Number} status Номер помилки * @returns {String} error Опис помилки * @returns {String|Object} message Повертає SQL запит або opt або рядки SQL запиту * @returns {String} file Шлях до файла для скачування або відображення */ export default async function exportTable({ pg = pgClients.client, user, unittest, columns: columns1, cls, query = {}, host = '127.0.0.1', tableSql, sourceName, }, reply) { const { id, cols, search, format = 'json', table, filter = 'empty', nocache, formatAnswer = 'file', sql, stream, } = query; if (!table && !tableSql) { return reply.status(400).send('not enough params: table'); } if (!['csv', 'xlsx', 'json', 'geojson'].includes(format)) { return reply.status(400).send('param format is invalid'); } const date = new Date(); const sufixName = `${filter}-${cols || 'all'}-${search}-${query.limit || 'unlimited'}`; const sufixDate = [date.getFullYear(), date.getMonth(), date.getDate(), date.getHours()].join('-'); const objInfo = createHash('md5').update([sufixName, sufixDate].join('-')).digest('hex'); const fileName = (table || (tableSql ? createHash('md5').update(tableSql).digest('hex') : '')).concat('_').concat(objInfo).concat('.').concat(format); const filePath = path.join(rootDir, '/files/temp', fileName); const ext = path.extname(filePath); const cacheFile = existsSync(filePath); const filePathJSON = ['csv', 'xlsx', 'geojson'].includes(format) ? filePath.replace(ext, '.json') : filePath; const cacheFileJSON = existsSync(filePathJSON); // return from cache if (cacheFile && !sql && !nocache && !config.disableCache) { return formatResult({ filePath, formatAnswer, folder: rootDir, reply, }); } // delete old file, prevent append if (nocache || config.disableCache) { if (cacheFile) await rm(filePath); if (cacheFileJSON && format !== 'json') await rm(filePathJSON); } const loadTable = await getTemplate('table', table); const meta = await getMeta({ pg, table: loadTable?.table || table }); if (!meta?.pk && !meta?.view && !tableSql) { return reply.status(404).send('table not found'); } if (format === 'geojson' && !meta?.geom) { return reply.status(400).send('Ця форма не містить полів геометрії. Виберіть тип, який не потребує геометрії для вивантаження'); } const options = { id, table, pg, filter, search, user, sql, sufix: false, }; // check total count, debug sql etc. const result = tableSql ? await pg.query(`select count(*) as total, json_agg(row_to_json(q)) as rows from (${tableSql})q`).then(el => el.rows?.[0] || {}) : await getData(options, reply, true); if (sql) return result; if (!result?.rows?.length) { return reply.status(200).send('Немає даних, які можна експортувати'); } const { total, filtered = result.total || 0 } = result; const limit = startStreamWithTotal > filtered ? filtered : (Math.min(query.limit || 1000, startStreamWithTotal) || startStreamWithTotal); Object.assign(options, { limit }); const colmodel = (columns1 || loadTable?.columns || meta?.columns || [])?.map((el) => ({ name: el.name, data: el.data || el.option, title: el.title || el.ua, type: el.type || el.format || 'text', html: el.html, })); // skip html to avoid errors // get present columns const columns = cols === 'all' || !cols ? colmodel : colmodel ?.filter((el) => (el.type || /\./.test(el.name)) && !el?.hidden && (cols?.split(',')?.length ? cols.split(',').includes(el.name) : true)) ?.filter(el => (Object.hasOwn(el, 'export') ? !el.export : true)); const htmls = columns.filter(el => el.html).reduce((acc, curr) => ({ ...acc, [curr.name]: curr.html }), {}); const columnList = columns?.map((el) => (/\./.test(el.name) ? `${el.name.split('.')[0]}->>'${el.name.split('.')[1]}' as ${el.name.split('.').pop()}` // check for json data : el.name)); const send = (+filtered > startStreamWithTotal || stream) && !unittest ? eventStream(reply) : (unittest ? console.log : () => { }); // export xlsx / csv / json const source = loadTable?.title || loadTable?.ua || table || sourceName; const interval = setInterval(async () => { send('process query...'); }, 5000); // start stream only if total exceed limit, but use while anyway const res = {}; let offset = 0; let page = 1; let seq = 0; send(`Всього в реєстрі: ${result.total} (${filtered} з урахуванням фільтрів)`); if (!cacheFileJSON || nocache || config.disableCache) { while ((+filtered - offset > 0) && !res?.error) { try { send(`Оброблено: ${offset}/${filtered}`); const { rows = [] } = tableSql ? await pg.query(`select * from (${tableSql})q limit ${options.limit} offset ${offset}`) : await getData({ ...options, page }, reply, true); send(`seq: ${++seq}`); send(`Обробка ${rows.length} об'єктів...`); if (!rows.length) { send('Обробка даних успішно завершена'); break; } await metaFormat({ rows, cls, htmls, sufix: false, }, pg); // skip non present after metaFormat if (!tableSql) { rows.forEach((row) => { Object.keys(row).filter((el) => !columnList.includes(el)).forEach((key) => delete row[key]); }); } const jsonFileExists = existsSync(filePathJSON); // convert from json to format if (!jsonFileExists) { // if json not exists await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePathJSON, JSON.stringify(rows)); } else { // if json exists const jsonData = JSON.parse(await readFile(filePathJSON) || '{}'); const moreData = jsonData.concat(rows); // rewrite to appendFile? await writeFile(filePathJSON, JSON.stringify(moreData)); } offset += rows.length; page++; } catch (err) { send(`error: ${err.toString()}`); logger.error('export/table', { filePath: filePathJSON, total, filtered, offset, result: res, error: err.toString(), stack: err.stack, }); Object.assign(res, { error: err.toString() }); } } } clearInterval(interval); if (res.error) { send(res.error, 1); return reply.status(500).send(res.error); } logger.file('export/table', { table, format, total, filtered, time: Date.now() - date.getTime(), }); if (format !== 'json') { const txt = nocache || config.disableCache || !cacheFileJSON ? `Сформовано файл формату json. Початок конвертації в ${format}...` : `Знайдено файл формату json. Початок конвертації в ${format}...`; send(txt); } if (format === 'geojson') { const rows = JSON.parse(await readFile(filePathJSON) || '[]'); const geojson = { type: 'FeatureCollection', features: rows.map((row) => ({ type: 'Feature', name: 'export', geometry: row.geom, properties: Object.fromEntries(Object.entries(row).filter(([key]) => key !== 'geom')), })), }; await mkdir(path.dirname(filePath), { recursive: true }); await writeFile(filePath, JSON.stringify(geojson)); } const resp = {}; if (format === 'csv') { await jsonToCsv({ filePath: filePathJSON, send, colmodel, domain: host, source, columnList, }); } if (format === 'xlsx') { await jsonToXls({ filePath: filePathJSON, send, colmodel, domain: host, source, resp, }); } if (resp.error) { return reply.status(resp.status || 500).send(resp.error); } send('Файл успішно сформовано. Натистіть кнопку ще раз для завантаження даних', 1); return formatResult({ filePath, formatAnswer, folder: rootDir, reply, }); }