UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

122 lines (121 loc) 6.44 kB
import { REGEX_BETWEEN_PARENS } from '@directus/constants'; import { adjustDate } from '@directus/utils'; export const DYNAMIC_VARIABLE_MAP = { $CURRENT_USER: 'directus_users', $CURRENT_ROLE: 'directus_roles', $CURRENT_ROLES: 'directus_roles', $CURRENT_POLICIES: 'directus_policies', }; /** * Calculate logical expiry (TTL) and dependencies for permissions caching. */ export function calculateCacheMetadata(collection, itemData, rawPermissions, schema, accountability) { let ttlMs; const dependencies = new Set(); if (itemData) { const now = Date.now(); let closestExpiry = Infinity; // Scan permission filters for dynamic variables and relational dependencies to determine cache invalidation rules const scan = (val, fieldKey, currentCollection = collection) => { if (!val || typeof val !== 'object') return; for (const [key, value] of Object.entries(val)) { // Parse dynamic variables if (typeof value === 'string' && value.startsWith('$')) { // $NOW requires calculating a logical expiry (TTL) based on the field value if (value.startsWith('$NOW')) { const field = fieldKey || key; const dateValue = itemData[field]; if (dateValue) { let ruleDate = new Date(); if (value.includes('(')) { const adjustment = value.match(REGEX_BETWEEN_PARENS)?.[1]; if (adjustment) ruleDate = adjustDate(ruleDate, adjustment) || ruleDate; } const adjustmentMs = ruleDate.getTime() - now; const expiry = new Date(dateValue).getTime() - adjustmentMs; if (expiry > now && expiry < closestExpiry) { closestExpiry = expiry; } } } else { // Other dynamic variables ($CURRENT_USER, etc) create collection-based dependencies const parts = value.split('.'); const dynamicVariable = parts[0]; const rootCollection = DYNAMIC_VARIABLE_MAP[dynamicVariable]; if (rootCollection) { // Only $CURRENT_USER needs granular tagging // Other dynamic variables trigger full cache wipe so collection-level is sufficient if (dynamicVariable === '$CURRENT_USER' && accountability.user) { dependencies.add(`${rootCollection}:${accountability.user}`); } else { dependencies.add(rootCollection); } // Track all intermediate collections in the path if (parts.length > 1) { let currentCollection = rootCollection; for (const segment of parts.slice(1, -1)) { if (!currentCollection) break; const relation = schema.relations.find((r) => (r.collection === currentCollection && r.field === segment) || (r.related_collection === currentCollection && r.meta?.one_field === segment)); if (relation) { currentCollection = relation.collection === currentCollection ? relation.related_collection : relation.collection; if (currentCollection) dependencies.add(currentCollection); } else { currentCollection = null; } } } } } } // Parse relational filter dependencies to track which collections affect this permission let field = key; if (key.includes('(') && key.includes(')')) { const columnName = key.match(REGEX_BETWEEN_PARENS)?.[1]; if (columnName) field = columnName; } if (!field.startsWith('_')) { const relation = schema.relations.find((r) => (r.collection === currentCollection && r.field === field) || (r.related_collection === currentCollection && r.meta?.one_field === field)); let targetCol = null; if (relation) { targetCol = relation.collection === currentCollection ? relation.related_collection : relation.collection; } if (targetCol) { dependencies.add(targetCol); scan(value, undefined, targetCol); } else { // Not a relation, but might be a nested filter object scan(value, field.startsWith('_') ? fieldKey : field, currentCollection); } } else { // Keep scanning filter operators scan(value, fieldKey, currentCollection); } } }; // Scan all raw permissions to collect dependencies and calculate TTL for (const permission of rawPermissions) { scan(permission.permissions); } if (closestExpiry !== Infinity) { ttlMs = closestExpiry - now; // Limit TTL to between 1s and 1 hour ttlMs = Math.max(1000, Math.min(ttlMs, 3600000)); } } return { ttlMs, dependencies: Array.from(dependencies) }; }