// server/helpers.js const nocodb = require('./nocodb'); const { DEFAULTS } = require('./config'); // Name lookup cache const _nameCache = {}; // Clear cache periodically setInterval(() => { Object.keys(_nameCache).forEach(k => delete _nameCache[k]); }, DEFAULTS.cacheTTLMs); // Get a single record's display name async function getRecordName(table, id) { if (!id) return null; const key = `${table}:${id}`; if (_nameCache[key] !== undefined) return _nameCache[key]; try { const r = await nocodb.get(table, id); const name = r?.name || r?.title || r?.Name || null; _nameCache[key] = name; return name; } catch { _nameCache[key] = null; return null; } } // Batch resolve names for multiple IDs across tables // Usage: await batchResolveNames({ brand: { table: 'Brands', ids: [1,2,3] }, user: { table: 'Users', ids: [4,5] } }) // Returns: { 'brand:1': 'BrandA', 'user:4': 'Alice', ... } async function batchResolveNames(groups) { // groups is an object like: { brand: { table: 'Brands', ids: [1,2,3] }, user: { table: 'Users', ids: [4,5] } } const names = {}; const fetches = []; for (const [prefix, { table, ids }] of Object.entries(groups)) { const uniqueIds = [...new Set(ids.filter(Boolean))]; for (const id of uniqueIds) { const key = `${prefix}:${id}`; if (_nameCache[`${table}:${id}`] !== undefined) { names[key] = _nameCache[`${table}:${id}`]; } else { fetches.push( getRecordName(table, id).then(name => { names[key] = name; }) ); } } } await Promise.all(fetches); return names; } // Parse comma-separated approver IDs function parseApproverIds(str) { if (!str) return []; return str.split(',').map(s => s.trim()).filter(Boolean).map(Number); } // Safely parse JSON with fallback function safeJsonParse(str, fallback = null) { if (!str || typeof str !== 'string') return fallback; try { return JSON.parse(str); } catch { return fallback; } } // Pick allowed fields from request body function pickBodyFields(body, fields) { const data = {}; for (const f of fields) { if (body[f] !== undefined) data[f] = body[f]; } return data; } // Sanitize a value for use in NocoDB WHERE clauses // Prevents injection by removing NocoDB query operators function sanitizeWhereValue(val) { if (val === null || val === undefined) return ''; const str = String(val); // Remove characters that could manipulate NocoDB query syntax return str.replace(/[~(),$]/g, ''); } // Build user modules list from user record function getUserModules(user, allModules) { if (user.role === 'superadmin') return allModules; if (user.modules) return safeJsonParse(user.modules, allModules); return allModules; } // Strip sensitive fields from user data before sending to client const SENSITIVE_USER_FIELDS = ['password_hash', 'reset_token', 'reset_token_expires']; function stripSensitiveFields(data) { if (Array.isArray(data)) return data.map(stripSensitiveFields); if (data && typeof data === 'object') { const out = { ...data }; for (const f of SENSITIVE_USER_FIELDS) { delete out[f]; delete out[f.replace(/_([a-z])/g, (_, c) => c.toUpperCase())]; } return out; } return data; } // Get all team IDs for a user async function getUserTeamIds(userId) { const entries = await nocodb.list('TeamMembers', { where: `(user_id,eq,${userId})`, limit: 200 }); return new Set(entries.map(e => e.team_id)); } // Get full visibility context for a user (team IDs + team project/campaign IDs) async function getUserVisibilityContext(userId) { const myTeamIds = await getUserTeamIds(userId); if (myTeamIds.size === 0) return { myTeamIds, teamProjectIds: new Set(), teamCampaignIds: new Set() }; // Fetch projects and campaigns that belong to the user's teams const allProjects = await nocodb.list('Projects', { limit: 2000 }); const allCampaigns = await nocodb.list('Campaigns', { limit: 2000 }); const teamProjectIds = new Set( allProjects.filter(p => p.team_id && myTeamIds.has(p.team_id)).map(p => p.Id) ); const teamCampaignIds = new Set( allCampaigns.filter(c => c.team_id && myTeamIds.has(c.team_id)).map(c => c.Id) ); return { myTeamIds, teamProjectIds, teamCampaignIds }; } module.exports = { getRecordName, batchResolveNames, parseApproverIds, safeJsonParse, pickBodyFields, sanitizeWhereValue, getUserModules, stripSensitiveFields, getUserTeamIds, getUserVisibilityContext, _nameCache, };