All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
161 lines
5.3 KiB
JavaScript
161 lines
5.3 KiB
JavaScript
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<Object|null>} { 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<Object>} 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<Buffer>} 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<number|null>} 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);
|
|
}
|