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 { getModelFilterFields, parseOrderBy } from './export.js'; const logger = log4js.getLogger('RSS'); logger.level = config.server.logLevel; function escapeXml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function getItemTitle(objectType, row) { const preferredTitle = row.name || row.title || row.label || row.code; if (preferredTitle) return String(preferredTitle); return `${objectType} ${row._id || 'entry'}`; } function getItemDescription(row) { const ignoredKeys = new Set(['_id', 'createdAt', 'updatedAt']); const summary = Object.entries(row || {}) .filter( ([key, val]) => !ignoredKeys.has(key) && val !== null && val !== undefined && val !== '' ) .slice(0, 10) .map(([key, val]) => `${key}: ${typeof val === 'object' ? JSON.stringify(val) : String(val)}`) .join(' | '); if (summary) return summary; return JSON.stringify(row || {}); } function buildRssXml({ objectType, feedUrl, rows }) { const now = new Date().toUTCString(); const itemsXml = rows .map((row) => { const rowId = row?._id ? String(row._id) : `${objectType}-${Date.now()}`; const pubDateSource = row?.updatedAt || row?.createdAt; const pubDate = pubDateSource ? new Date(pubDateSource).toUTCString() : now; const itemLink = `${feedUrl}#${encodeURIComponent(rowId)}`; return [ '', ` ${escapeXml(getItemTitle(objectType, row || {}))}`, ` ${escapeXml(itemLink)}`, ` ${escapeXml(`${objectType}:${rowId}`)}`, ` ${escapeXml(getItemDescription(row || {}))}`, ` ${escapeXml(pubDate)}`, '', ].join('\n'); }) .join('\n'); return ` ${escapeXml(`FarmControl ${objectType} RSS Feed`)} ${escapeXml(feedUrl)} ${escapeXml(`Recent ${objectType} updates from FarmControl`)} ${escapeXml(now)} ${itemsXml} `; } /** * Route handler for GET /rss/:objectType * Supports OData-style query options: $top, $skip, $orderby and allowed filters. */ export const listRssRouteHandler = async (req, res) => { const objectType = req.params.objectType; const entry = getModelByName(objectType); if (!entry?.model) { logger.warn(`RSS: unknown object type "${objectType}"`); return res.status(404).send({ error: `Unknown object type: ${objectType}` }); } const { $top, $skip, $orderby } = req.query; const queryWithoutAuth = { ...req.query }; delete queryWithoutAuth.u; delete queryWithoutAuth.p; const limit = $top ? Math.min(parseInt($top, 10) || 25, 1000) : 25; const skip = $skip ? Math.max(0, parseInt($skip, 10) || 0) : 0; const page = Math.floor(skip / limit) + 1; const { sort, order } = parseOrderBy($orderby); const allowedFilters = getModelFilterFields(objectType); const filter = getFilter(queryWithoutAuth, allowedFilters); const result = await listObjectsOData({ model: entry.model, populate: [], page, limit, filter, sort, order, pagination: true, project: '-__v', count: false, }); if (result?.error) { logger.error('RSS list error:', result.error); return res.status(result.code || 500).send(result); } const baseUrl = config.app?.urlApi || ''; const feedUrl = `${baseUrl}/rss/${objectType}`; const rows = result?.value || []; const xml = buildRssXml({ objectType, feedUrl, rows }); res.set('Content-Type', 'application/rss+xml; charset=utf-8'); res.send(xml); };