/** * Workflow Service * Handles workflow fetching with lazy loading and caching * Workflows are only loaded when first requested (not at startup) */ const apiService = require('./api-service').getInstance(); const profileManager = require('../config/profile-manager'); // Cache state let workflowCache = null; let cacheTimestamp = null; const CACHE_TTL = parseInt(process.env.WORKFLOW_CACHE_TTL) || 3600000; // 1 hour in milliseconds // Clear cache when profile changes — workflows are per-tenant, so the previous // profile's cache is meaningless after a switch. profileManager.onSwitch(() => clearCache()); /** * Check if cache is still valid */ function isCacheValid() { if (!workflowCache || !cacheTimestamp) { return false; } const now = Date.now(); const age = now - cacheTimestamp; return age < CACHE_TTL; } /** * Fetch all workflows from API with pagination * Uses high page size (5000) to minimize API calls */ async function fetchAllWorkflows() { // Check cache validity if (isCacheValid()) { console.error('[Workflow] Using cached data'); return workflowCache; } console.error('[Workflow] Cache expired or empty, fetching from API...'); try { let allWorkflows = []; let offset = 0; const pageSize = parseInt(process.env.WORKFLOW_PAGE_SIZE) || 5000; const profile = profileManager.getCurrent(); const application = profile.application; const tenant = profile.tenant; while (true) { const body = [application, tenant, pageSize, offset]; console.error(`[Workflow] Fetching page: offset=${offset}, pageSize=${pageSize}`); // Use AD API (useAdApi=true) const response = await apiService.post('/Workflow/GetByApplication', body, true); // Extract entities array from response const workflows = response?.entities || []; // Check if response is valid if (!workflows || workflows.length === 0) { console.error('[Workflow] No more workflows to fetch'); break; } allWorkflows = allWorkflows.concat(workflows); console.error(`[Workflow] Fetched ${workflows.length} workflows (total: ${allWorkflows.length})`); // If we got less than page size, we've reached the last page if (workflows.length < pageSize) { console.error('[Workflow] Last page reached'); break; } offset += pageSize; } // Update cache workflowCache = allWorkflows; cacheTimestamp = Date.now(); console.error(`[Workflow] Successfully cached ${allWorkflows.length} workflows`); return allWorkflows; } catch (error) { console.error('[Workflow] Error fetching workflows:', error.message); throw new Error(`Failed to fetch workflows: ${error.message}`); } } /** * Search workflows by query string * @param {string} query - Search query (matches name, description, etc.) * @param {string|null} category - Optional category filter * @param {number} limit - Maximum results to return */ async function searchWorkflows(query, category = null, limit = 50) { const workflows = await fetchAllWorkflows(); let results = workflows; // Filter by query if provided if (query) { const lowerQuery = query.toLowerCase(); results = results.filter(w => { const name = (w.name || w.Name || '').toLowerCase(); const description = (w.description || w.Description || '').toLowerCase(); const code = (w.code || w.Code || '').toLowerCase(); return name.includes(lowerQuery) || description.includes(lowerQuery) || code.includes(lowerQuery); }); } // Filter by category if provided if (category) { const lowerCategory = category.toLowerCase(); results = results.filter(w => { const wfCategory = (w.category || w.Category || '').toLowerCase(); return wfCategory.includes(lowerCategory); }); } // Limit results return results.slice(0, limit); } /** * Get workflow details by ID * @param {string|number} workflowId - Workflow ID */ async function getWorkflowDetails(workflowId) { const workflows = await fetchAllWorkflows(); // Try to find by Id, id, Code, code, Name, or name const workflow = workflows.find(w => w.id === workflowId || w.Id === workflowId || w.id === parseInt(workflowId) || w.Id === parseInt(workflowId) || w.Code === workflowId || w.code === workflowId || w.Name === workflowId || w.name === workflowId ); if (!workflow) { throw new Error(`Workflow not found: ${workflowId}`); } return workflow; } /** * List all workflow categories */ async function listWorkflowCategories() { const workflows = await fetchAllWorkflows(); // Extract unique categories (try both lowercase and uppercase) const categories = new Set(); workflows.forEach(w => { const category = w.category || w.Category; if (category) { categories.add(category); } }); // Sort alphabetically return Array.from(categories).sort(); } /** * Get workflow statistics */ async function getWorkflowStats() { const workflows = await fetchAllWorkflows(); const categories = await listWorkflowCategories(); // Count workflows per category const categoryCounts = {}; workflows.forEach(w => { const cat = w.category || w.Category || 'Uncategorized'; categoryCounts[cat] = (categoryCounts[cat] || 0) + 1; }); return { total: workflows.length, categories: categories.length, categoryCounts, cacheAge: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null }; } /** * Clear workflow cache (force refresh on next request) */ function clearCache() { workflowCache = null; cacheTimestamp = null; console.error('[Workflow] Cache cleared'); } /** * Get cache status */ function getCacheStatus() { return { cached: workflowCache !== null, count: workflowCache ? workflowCache.length : 0, timestamp: cacheTimestamp, age: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null, valid: isCacheValid() }; } module.exports = { fetchAllWorkflows, searchWorkflows, getWorkflowDetails, listWorkflowCategories, getWorkflowStats, clearCache, getCacheStatus };