import { customAlphabet } from 'nanoid'; import log4js from 'log4js'; import ExcelJS from 'exceljs'; import config from '../config.js'; import { redisServer } from './redis.js'; const logger = log4js.getLogger('Excel'); const EXCEL_TEMP_KEY_PREFIX = 'excel:temp:'; const EXCEL_TEMP_TTL_SECONDS = 15; // 15 seconds const excelNanoid = customAlphabet( 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12 ); /** * Create a temp token and store export params in Redis. * @param {Object} params - { objectType, filter, sort, order } * @returns {Promise<{ token: string, url: string }>} */ export async function createExcelTempToken(params) { const baseUrl = config.app?.urlApi?.replace(/\/$/, '') || ''; if (!baseUrl) { throw new Error('config.app.urlApi is not set; required for Excel temp URLs'); } const objectType = params.objectType || 'Export'; const now = new Date(); const datetime = now.getFullYear() + String(now.getMonth() + 1).padStart(2, '0') + String(now.getDate()).padStart(2, '0') + String(now.getHours()).padStart(2, '0') + String(now.getMinutes()).padStart(2, '0'); const token = `${objectType}s-${datetime}-${excelNanoid()}`; const key = EXCEL_TEMP_KEY_PREFIX + token; const stored = { ...params, requestCount: 0 }; await redisServer.setKey(key, stored, EXCEL_TEMP_TTL_SECONDS); logger.debug('Stored excel temp token in Redis:', key); const url = `${baseUrl}/excel/temp/${token}.xlsx`; return { token, url }; } /** * Get export params for a temp token (supports up to 2 requests; requestCount stored in Redis). * @param {string} token * @returns {Promise} { objectType, filter, sort, order } or null */ export async function getExcelTempParams(token) { if (!token) return null; const key = EXCEL_TEMP_KEY_PREFIX + token; const params = await redisServer.getKey(key); if (!params) { logger.debug('Excel temp token not found in Redis:', key); return null; } return params; } /** * Convert a value to an Excel cell-friendly format. * Primitives pass through; objects/arrays are stringified; dates are preserved. */ function toExcelValue(val) { if (val === null || val === undefined) return null; if (val instanceof Date) return val; if (typeof val === 'number' || typeof val === 'boolean') return val; if (typeof val === 'string') return val; if (typeof val === 'object') { if (Array.isArray(val)) return val.map(toExcelValue).join(', '); return JSON.stringify(val); } return String(val); } /** * Generate an Excel workbook from tabular data. * @param {Array} data - Array of row objects (keys = column headers) * @param {Object} options - Options * @param {string} [options.sheetName='Export'] - Worksheet name * @param {string[]} [options.columnOrder] - Optional column order (uses Object.keys of first row if not provided) * @returns {Promise} Excel file as buffer */ export async function generateExcelTable(data, options = {}) { const { sheetName = 'Export', columnOrder } = options; const workbook = new ExcelJS.Workbook(); const worksheet = workbook.addWorksheet(sheetName, { views: [{ state: 'frozen', ySplit: 1 }], }); if (!data || data.length === 0) { const buffer = await workbook.xlsx.writeBuffer(); return Buffer.from(buffer); } const keys = columnOrder || Object.keys(data[0]).filter((k) => !k.startsWith('@')); const colCount = keys.length; const rowCount = data.length + 1; const toColLetter = (n) => { let s = ''; while (n >= 0) { s = String.fromCharCode((n % 26) + 65) + s; n = Math.floor(n / 26) - 1; } return s; }; const endCol = toColLetter(colCount - 1); const tableRows = data.map((row) => keys.map((key) => toExcelValue(row[key]))); worksheet.addTable({ name: 'DataTable', ref: `A1:${endCol}${rowCount}`, headerRow: true, style: { theme: 'TableStyleLight1', showRowStripes: true, }, columns: keys.map((key) => ({ name: key, filterButton: true })), rows: tableRows, }); // Auto-fit columns (approximate) worksheet.columns.forEach((col, i) => { let maxLen = keys[i]?.length || 10; worksheet.eachRow({ includeEmpty: false }, (row) => { const cell = row.getCell(i + 1); const val = cell.value; const len = val != null ? String(val).length : 0; maxLen = Math.min(Math.max(maxLen, len), 50); }); col.width = maxLen + 2; }); const buffer = await workbook.xlsx.writeBuffer(); return Buffer.from(buffer); } /** * Increment request count for a temp token. Returns new count or null if token not found. * @param {string} token * @returns {Promise} New requestCount or null */ export async function incrementExcelTempRequestCount(token) { if (!token) return null; const key = EXCEL_TEMP_KEY_PREFIX + token; const data = await redisServer.getKey(key); if (!data) return null; const requestCount = (data.requestCount ?? 0) + 1; const updated = { ...data, requestCount }; await redisServer.setKey(key, updated, EXCEL_TEMP_TTL_SECONDS); logger.debug('Incremented excel temp request count:', key, requestCount); return requestCount; } export async function deleteExcelTempToken(token) { const key = EXCEL_TEMP_KEY_PREFIX + token; await redisServer.deleteKey(key); logger.debug('Deleted excel temp token from Redis:', key); }