All checks were successful
Deploy / deploy (push) Successful in 12s
- Add Roles table with CRUD routes and Settings page management - Unify user management: remove Users page, enhance Team page with permission level + role dropdowns - Add team-based visibility scoping to projects, campaigns, posts, tasks, issues, artefacts, and dashboard - Add team_id to projects and campaigns (create + edit forms) - Add getUserTeamIds/getUserVisibilityContext helpers - Fix Budgets modal horizontal scroll (separate linked-to row) - Add collapsible filter bar to PostProduction page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
4.4 KiB
JavaScript
143 lines
4.4 KiB
JavaScript
// 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,
|
|
};
|