/** * Shared export utilities for OData, CSV, and Excel. * Centralizes filter fields, order-by parsing, and row flattening. */ /** Allowed filter fields per object type for OData/CSV/Excel exports */ export const EXPORT_FILTER_BY_TYPE = { note: ['parent._id', 'noteType', 'user'], notification: ['user'], userNotifier: ['user', 'object', 'objectType'], printer: ['host'], job: ['printer', 'gcodeFile'], subJob: ['job'], filamentStock: ['filamentSku'], filament: ['material', 'material._id', 'name', 'diameter', 'cost'], filamentSku: ['filament', 'vendor', 'costTaxRate'], material: ['name', 'tags'], partStock: ['partSku'], partSku: ['part', 'vendor', 'priceTaxRate', 'costTaxRate'], productStock: ['productSku'], productSku: ['product', 'vendor', 'priceTaxRate', 'costTaxRate'], purchaseOrder: ['vendor'], orderItem: ['order._id', 'orderType', 'item._id', 'itemType', 'sku._id', 'shipment._id'], shipment: ['order._id', 'orderType', 'courierService._id'], stockEvent: ['parent._id', 'parentType', 'owner._id', 'ownerType'], stockLocation: ['name'], stockTransfer: ['state.type', 'postedAt'], stockAudit: ['filamentStock._id', 'partStock._id'], documentJob: ['documentTemplate', 'documentPrinter', 'object._id', 'objectType'], documentTemplate: ['parent._id', 'documentSize._id'], salesOrder: ['client'], invoice: ['to._id', 'from._id', 'order._id', 'orderType'], auditLog: ['parent._id', 'parentType', 'owner._id', 'ownerType'], appPassword: ['name', 'user', 'active'], }; /** * Get allowed filter fields for a given object type. * @param {string} objectType - Model type (e.g. 'filament', 'material') * @returns {string[]} Allowed filter field names */ export function getModelFilterFields(objectType) { const base = ['_id']; const extra = EXPORT_FILTER_BY_TYPE[objectType] || []; return [...base, ...extra]; } /** * Parse OData $orderby or orderby string into sort and order. * Supports "field asc", "field desc", or just "field" (defaults asc). * @param {string} [orderby] - Orderby string (e.g. "createdAt desc") * @returns {{ sort: string, order: 'ascend'|'descend' }} */ export function parseOrderBy(orderby) { if (!orderby || typeof orderby !== 'string') { return { sort: 'createdAt', order: 'ascend' }; } const trimmed = orderby.trim(); const parts = trimmed.split(/\s+/); const sort = parts[0] || 'createdAt'; const dir = (parts[1] || 'asc').toLowerCase(); const order = dir === 'desc' ? 'descend' : 'ascend'; return { sort, order }; } /** * Flatten nested objects for export display. * Objects become "key.subkey: value"; arrays become comma-separated strings. * @param {*} obj - Value to flatten * @param {string} [prefix=''] - Key prefix for nested values * @returns {Object} Flat key-value object */ export function flattenForExport(obj, prefix = '') { if (obj === null || obj === undefined) return {}; if (typeof obj !== 'object') return { [prefix]: obj }; if (obj instanceof Date) return { [prefix]: obj }; if (Array.isArray(obj)) { const str = obj .map((v) => (v && typeof v === 'object' && !(v instanceof Date) ? JSON.stringify(v) : v)) .join(', '); return { [prefix]: str }; } const result = {}; for (const [k, v] of Object.entries(obj)) { const key = prefix ? `${prefix}.${k}` : k; if (v !== null && typeof v === 'object' && !(v instanceof Date) && !Array.isArray(v)) { Object.assign(result, flattenForExport(v, key)); } else { result[key] = v; } } return result; } /** * Convert a row (e.g. OData value item) to flat key-value for CSV/Excel. * Nested objects are flattened; @odata.* keys are skipped. * @param {Object} row - Row object * @returns {Object} Flat key-value object */ export function rowToFlat(row) { const flat = {}; for (const [key, val] of Object.entries(row)) { if (key.startsWith('@')) continue; if (val !== null && typeof val === 'object' && !(val instanceof Date) && !Array.isArray(val)) { Object.assign(flat, flattenForExport(val, key)); } else { flat[key] = val; } } return flat; }