Tom Butcher 2a30b7f3f6 Enhance filtering and parsing logic in utils.js and update route handlers for async support
This update introduces several utility functions in `utils.js` to improve the handling of ObjectId paths, schema types, and filtering logic. Key additions include `isObjectIdPath`, `getBaseProperty`, and `getFilterFieldKind`, which enhance the flexibility of property handling. Additionally, all route handlers in the finance and inventory modules have been refactored to support asynchronous operations, ensuring better performance and responsiveness when processing requests.
2026-08-09 22:27:45 +01:00

207 lines
7.4 KiB
JavaScript

import config from '../../config.js';
import log4js from 'log4js';
import { getModelByName } from './model.js';
import { listObjectsOData } from '../../database/odata.js';
import { getFilter } from '../../utils.js';
import {
generateExcelTable,
createExcelTempToken,
getExcelTempParams,
incrementExcelTempRequestCount,
deleteExcelTempToken,
} from '../../database/excel.js';
import { getModelFilterFields, parseOrderBy, rowToFlat } from './export.js';
const logger = log4js.getLogger('Excel');
logger.level = config.server.logLevel;
/**
* Generate Excel file for the given object type.
* @param {Object} options
* @param {string} options.objectType - Model type (e.g. 'appPassword', 'user')
* @param {Object} [options.filter] - Filter object
* @param {string} [options.sort] - Sort field
* @param {string} [options.order] - 'ascend' | 'descend'
* @param {number} [options.limit=10000] - Max rows to export
* @returns {Promise<{ buffer: Buffer, error?: Object }>}
*/
export async function exportToExcel({ objectType, filter = {}, sort, order, limit = 10000 }) {
logger.info('[Excel Export] Starting', { objectType, filter, sort, order });
const entry = getModelByName(objectType);
if (!entry?.model) {
logger.warn('[Excel Export] Unknown object type:', objectType);
return { error: { message: `Unknown object type: ${objectType}` }, code: 404 };
}
const orderbyStr = sort ? `${sort} ${order === 'descend' ? 'desc' : 'asc'}` : undefined;
const { sort: sortField, order: orderDir } = parseOrderBy(orderbyStr);
const result = await listObjectsOData({
model: entry.model,
populate: [],
page: 1,
limit,
filter,
sort: sortField,
order: orderDir,
pagination: true,
project: undefined,
count: false,
});
if (result?.error) {
logger.error('[Excel Export] listObjectsOData error:', result.error);
return { error: result.error, code: result.code || 500 };
}
const rows = result?.value || [];
logger.info('[Excel Export] Rows fetched', { rowCount: rows.length });
const flatRows = rows.map(rowToFlat);
const allKeys = new Set();
flatRows.forEach((r) => Object.keys(r).forEach((k) => allKeys.add(k)));
const columnOrder = Array.from(allKeys).sort();
let buffer;
try {
buffer = await generateExcelTable(flatRows, {
sheetName: entry.model.modelName || objectType,
columnOrder,
});
logger.info('[Excel Export] Buffer generated', { bufferLength: buffer?.length });
} catch (err) {
logger.error('[Excel Export] generateExcelTable threw:', err?.message, err?.stack);
return { error: { message: err.message || 'Failed to generate Excel' }, code: 500 };
}
return { buffer };
}
/**
* Route handler for GET /odata/:objectType/excel
*/
export const excelExportRouteHandler = async (req, res) => {
const objectType = req.params.objectType;
const allowedFilters = getModelFilterFields(objectType);
const filter = await getFilter(req.query, allowedFilters);
const { sort, order } = parseOrderBy(req.query.$orderby);
const result = await exportToExcel({
objectType,
filter,
sort,
order,
});
if (result.error) {
return res.status(result.code || 500).json(result.error);
}
const filename = `${objectType}-export-${new Date().toISOString().slice(0, 10)}.xlsx`;
res.set('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.set('Content-Disposition', `attachment; filename="${filename}"`);
res.send(result.buffer);
};
/**
* Route handler for POST /excel/:objectType/open - creates a temporary URL for opening in Excel.
* Stores export params in Redis; spreadsheet is generated when Excel fetches the temp URL.
* Returns { url } - an https URL Excel can fetch (no auth required, one-time use, 5 min expiry).
*/
export const excelOpenRouteHandler = async (req, res) => {
try {
const objectType = req.params.objectType;
logger.info('[Excel Open] POST received', { objectType, query: req.query });
const allowedFilters = getModelFilterFields(objectType);
const filter = await getFilter(req.query, allowedFilters);
const { sort, order } = parseOrderBy(req.query.$orderby);
const entry = getModelByName(objectType);
if (!entry?.model) {
logger.warn('[Excel Open] Unknown object type:', objectType);
return res.status(404).json({ message: `Unknown object type: ${objectType}` });
}
const params = { objectType, filter, sort, order };
logger.info('[Excel Open] Creating temp token with params:', JSON.stringify(params));
const { token, url } = await createExcelTempToken(params);
if (!url) {
logger.error('[Excel Open] createExcelTempToken returned no url');
return res.status(500).json({ message: 'Failed to create temporary URL' });
}
logger.info('[Excel Open] Temp URL created', { objectType, token, url });
res.json({ url });
} catch (err) {
logger.error('[Excel Open] Route error:', err?.message, err?.stack);
res.status(500).json({ message: err.message || 'Failed to create Excel URL' });
}
};
/**
* Route handler for GET /excel/temp/:token - generates and serves temporary Excel file (no auth).
* Retrieves export params from Redis, generates spreadsheet on demand.
* Token allows 2 requests before deletion; expires after 5 minutes.
*/
export const excelTempRouteHandler = async (req, res) => {
const tokenParam = req.params.token || '';
const token = tokenParam.endsWith('.xlsx') ? tokenParam.slice(0, -5) : tokenParam;
logger.info('[Excel Temp] GET received', {
tokenParam,
token,
userAgent: req.get('User-Agent'),
referer: req.get('Referer'),
});
const params = await getExcelTempParams(token);
if (!params) {
logger.error('[Excel Temp] Token not found or expired', { token, tokenParam });
return res.status(404).json({ message: 'Link expired or invalid' });
}
logger.info('[Excel Temp] Params retrieved from Redis', { objectType: params.objectType });
const result = await exportToExcel({
objectType: params.objectType,
filter: params.filter || {},
sort: params.sort,
order: params.order,
});
if (result.error) {
logger.error('[Excel Temp] Export failed', { error: result.error, code: result.code });
return res.status(result.code || 500).json(result.error);
}
const buffer = Buffer.isBuffer(result.buffer) ? result.buffer : Buffer.from(result.buffer);
const filename = `${token}.xlsx`;
logger.info('[Excel Temp] Sending buffer', { bufferLength: buffer.length });
// Strong ETag based on buffer length + timestamp
const strongETag = `"${buffer.length.toString(16)}-${Date.now()}"`;
res.setHeader('Content-Length', buffer.length);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition');
res.setHeader('Cache-Control', 'private, max-age=300');
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('ETag', strongETag);
// Remove headers that may break Excel
res.removeHeader('Vary');
res.removeHeader('Access-Control-Allow-Credentials');
res.removeHeader('X-Powered-By');
res.end(buffer);
if (req.method === 'GET') {
const requestCount = await incrementExcelTempRequestCount(token);
if (requestCount !== null && requestCount >= 2) {
await deleteExcelTempToken(token);
}
}
};