Tom Butcher f852e607f9
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
Added excel support.
2026-03-03 01:01:52 +00:00

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);
}