L4.2 : paramètre application sur les outils AD et workflow (D26)
L'application venait de WMS_APPLICATION (partagée par tous les profils) : le MCP n'interrogeait que EasyWMS, alors que CustomApp porte le spécifique client (153 workflows CST_ sur ce tenant) et que 9 applications sont déclarées par Application/GetAll. Paramètre application (défaut : l'application du profil, comportement inchangé sans lui) sur get_ad_elements, search_ad_elements, get_ad_element_details, search_workflows, get_workflow_details, list_workflow_categories. Clés de cache : ad-service passe par (application, type), workflow-service par application — sans quoi un appel CustomApp polluerait le cache EasyWMS. L'invalidation reste l'abonnement onSwitch (D8), le chargement reste paresseux (aucun préchargement des 9 applications, D10). list_workflow_categories s'adosse à Application/GetAll (liste allégée en cache : le blob data de chaque application pèse ~100 Ko) ; get_application_summary regroupe par application et ne détaille que les entrées en cache (D24), workflows compris. Acté en D26 ; CLAUDE.md mis à jour (Caches, AD), L4.2 retiré de la ROADMAP. Vérifications rejouées via le protocole (LIMAGRAIN / LIMAGRAI2512), requêtes séquentielles : - search_workflows(CST_, application:CustomApp) -> 5 objets peuplés dont CST_SendRejectContainersToPK ([Workflow] Successfully cached 153 workflows for "CustomApp"). - get_ad_elements(Workflow, application:CustomApp) -> count 153, éléments CST_* ([AD] Successfully cached 153 CustomApp::Workflow). - Séquence EasyWMS -> CustomApp -> EasyWMS sur search_workflows : 4012 vs 153, retour en cache hit ([Workflow] Using cached data for "EasyWMS"), aucune pollution ; get_application_summary montre les deux caches (workflowCachesByApplication EasyWMS 4012 / CustomApp 153). - Sans paramètre application -> comportement inchangé (W1 = W3). - switch_wms_profile EUROTRAFIC puis retour -> [Workflow] Cache cleared, [AD] All caches invalidated, [EntityResolver] Cache cleared ; summary vide. - list_workflow_categories -> 9 applications, comptes réels des applications chargées. Baseline : tools/list 23 (les 23 noms répondent), resources 6, rejet D23 d'un paramètre inconnu OK (search_workflows/applikation), npm test 4/4 exit 0. Anomalie hors périmètre consignée dans ROADMAP.md : get_workflow_details peut dépasser le seuil de rejet client (~101 800 caractères mesurés sur CST_SendRejectContainersToPK), comportement antérieur au lot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,14 +2,19 @@
|
||||
* Workflow Service
|
||||
* Handles workflow fetching with lazy loading and caching
|
||||
* Workflows are only loaded when first requested (not at startup)
|
||||
*
|
||||
* Un cache par application (D26) : le paramètre `application` des outils
|
||||
* sélectionne l'application AD interrogée (défaut : celle du profil actif).
|
||||
*/
|
||||
|
||||
const apiService = require('./api-service').getInstance();
|
||||
const profileManager = require('../config/profile-manager');
|
||||
|
||||
// Cache state
|
||||
let workflowCache = null;
|
||||
let cacheTimestamp = null;
|
||||
// Cache state — un cache de workflows par application (D26)
|
||||
let workflowCaches = {}; // application -> workflows[]
|
||||
let cacheTimestamps = {}; // application -> timestamp
|
||||
let applicationsCache = null; // liste allégée de POST /Application/GetAll
|
||||
let applicationsTimestamp = 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
|
||||
@@ -17,43 +22,52 @@ const CACHE_TTL = parseInt(process.env.WORKFLOW_CACHE_TTL) || 3600000; // 1 hour
|
||||
profileManager.onSwitch(() => clearCache());
|
||||
|
||||
/**
|
||||
* Check if cache is still valid
|
||||
* Application effective : celle demandée, sinon celle du profil actif.
|
||||
*/
|
||||
function isCacheValid() {
|
||||
if (!workflowCache || !cacheTimestamp) {
|
||||
function resolveApplication(application) {
|
||||
return (application && application.trim()) || profileManager.getCurrent().application;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache is still valid for an application
|
||||
*/
|
||||
function isCacheValid(application) {
|
||||
if (!workflowCaches[application] || !cacheTimestamps[application]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const age = now - cacheTimestamp;
|
||||
const age = Date.now() - cacheTimestamps[application];
|
||||
return age < CACHE_TTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all workflows from API with pagination
|
||||
* Uses high page size (5000) to minimize API calls
|
||||
* Fetch all workflows of an application from API with pagination.
|
||||
* Uses high page size (5000) to minimize API calls.
|
||||
* Lazy : seule l'application effectivement demandée est chargée (D26) — ne
|
||||
* jamais précharger les 9 applications.
|
||||
* @param {string} [application] - Application AD (défaut : profil actif)
|
||||
*/
|
||||
async function fetchAllWorkflows() {
|
||||
async function fetchAllWorkflows(application) {
|
||||
const app = resolveApplication(application);
|
||||
|
||||
// Check cache validity
|
||||
if (isCacheValid()) {
|
||||
console.error('[Workflow] Using cached data');
|
||||
return workflowCache;
|
||||
if (isCacheValid(app)) {
|
||||
console.error(`[Workflow] Using cached data for "${app}"`);
|
||||
return workflowCaches[app];
|
||||
}
|
||||
|
||||
console.error('[Workflow] Cache expired or empty, fetching from API...');
|
||||
console.error(`[Workflow] Cache expired or empty for "${app}", 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;
|
||||
const tenant = profileManager.getCurrent().tenant;
|
||||
|
||||
while (true) {
|
||||
const body = [application, tenant, pageSize, offset];
|
||||
const body = [app, tenant, pageSize, offset];
|
||||
|
||||
console.error(`[Workflow] Fetching page: offset=${offset}, pageSize=${pageSize}`);
|
||||
console.error(`[Workflow] Fetching page: application=${app}, offset=${offset}, pageSize=${pageSize}`);
|
||||
|
||||
// Use AD API (useAdApi=true)
|
||||
const response = await apiService.post('/Workflow/GetByApplication', body, true);
|
||||
@@ -80,17 +94,45 @@ async function fetchAllWorkflows() {
|
||||
}
|
||||
|
||||
// Update cache
|
||||
workflowCache = allWorkflows;
|
||||
cacheTimestamp = Date.now();
|
||||
workflowCaches[app] = allWorkflows;
|
||||
cacheTimestamps[app] = Date.now();
|
||||
|
||||
console.error(`[Workflow] Successfully cached ${allWorkflows.length} workflows`);
|
||||
console.error(`[Workflow] Successfully cached ${allWorkflows.length} workflows for "${app}"`);
|
||||
return allWorkflows;
|
||||
} catch (error) {
|
||||
console.error('[Workflow] Error fetching workflows:', error.message);
|
||||
throw new Error(`Failed to fetch workflows: ${error.message}`);
|
||||
console.error(`[Workflow] Error fetching workflows for "${app}":`, error.message);
|
||||
throw new Error(`Failed to fetch workflows for application "${app}": ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste les applications déclarées (POST /Application/GetAll, payload null).
|
||||
* La réponse est une enveloppe { entities: [...] } (D4) dont chaque élément
|
||||
* porte un blob `data` volumineux — on ne conserve que les champs légers.
|
||||
* Cache TTL commun, vidé au switch de profil.
|
||||
* @returns {Promise<Array<{name: string, id: string, version: number}>>}
|
||||
*/
|
||||
async function fetchApplications() {
|
||||
if (applicationsCache && applicationsTimestamp &&
|
||||
Date.now() - applicationsTimestamp < CACHE_TTL) {
|
||||
return applicationsCache;
|
||||
}
|
||||
|
||||
console.error('[Workflow] Fetching application list (Application/GetAll)...');
|
||||
const response = await apiService.post('/Application/GetAll', null, true);
|
||||
const entities = response?.entities || [];
|
||||
|
||||
applicationsCache = entities.map(a => ({
|
||||
name: a.name || a.Name,
|
||||
id: a.id || a.Id,
|
||||
version: a.version ?? a.Version,
|
||||
})).filter(a => a.name);
|
||||
applicationsTimestamp = Date.now();
|
||||
|
||||
console.error(`[Workflow] Cached ${applicationsCache.length} application(s)`);
|
||||
return applicationsCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search workflows by query string.
|
||||
* Real AD keys (lowercase, cf. D5): id, name, version, applicationName,
|
||||
@@ -99,9 +141,10 @@ async function fetchAllWorkflows() {
|
||||
* @param {string|null} category - Optional applicationName filter (the only
|
||||
* grouping the AD API provides)
|
||||
* @param {number} limit - Maximum results to return
|
||||
* @param {string} [application] - Application AD interrogée (défaut : profil)
|
||||
*/
|
||||
async function searchWorkflows(query, category = null, limit = 50) {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
async function searchWorkflows(query, category = null, limit = 50, application) {
|
||||
const workflows = await fetchAllWorkflows(application);
|
||||
|
||||
let results = workflows;
|
||||
|
||||
@@ -130,15 +173,17 @@ async function searchWorkflows(query, category = null, limit = 50) {
|
||||
/**
|
||||
* Get workflow details by ID
|
||||
* @param {string|number} workflowId - Workflow ID
|
||||
* @param {string} [application] - Application AD interrogée (défaut : profil)
|
||||
*/
|
||||
async function getWorkflowDetails(workflowId) {
|
||||
async function getWorkflowDetails(workflowId, application) {
|
||||
// Garde d'entrée : sans elle, un workflow_id absent matchait le premier
|
||||
// workflow du cache (undefined === undefined sur les clés mortes ci-dessous).
|
||||
if (workflowId == null || workflowId === '') {
|
||||
throw new Error('workflow_id est requis (id ou nom exact du workflow). Utilisez search_workflows pour le trouver.');
|
||||
}
|
||||
|
||||
const workflows = await fetchAllWorkflows();
|
||||
const app = resolveApplication(application);
|
||||
const workflows = await fetchAllWorkflows(app);
|
||||
|
||||
// Clés réelles de l'API AD (minuscules, D5) : id, name. Les variantes
|
||||
// Id/Code/Name n'existent pas sur ces objets — les comparer faisait matcher
|
||||
@@ -149,82 +194,65 @@ async function getWorkflowDetails(workflowId) {
|
||||
);
|
||||
|
||||
if (!workflow) {
|
||||
throw new Error(`Workflow not found: ${workflowId}. Utilisez search_workflows pour trouver l'id ou le nom exact.`);
|
||||
throw new Error(
|
||||
`Workflow not found: ${workflowId} (application "${app}"). ` +
|
||||
`Utilisez search_workflows pour trouver l'id ou le nom exact — ` +
|
||||
`pensez au paramètre application (ex: "CustomApp" pour le spécifique client).`
|
||||
);
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* List distinct applicationName values.
|
||||
* Workflows have no category field — applicationName is the only grouping the
|
||||
* AD API provides, and every workflow of the active application carries the
|
||||
* same value (e.g. "EasyWMS").
|
||||
* Get workflow statistics for one application
|
||||
* @param {string} [application] - Application AD interrogée (défaut : profil)
|
||||
*/
|
||||
async function listWorkflowCategories() {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
|
||||
const categories = new Set();
|
||||
workflows.forEach(w => {
|
||||
const applicationName = w.applicationName || w.ApplicationName;
|
||||
if (applicationName) {
|
||||
categories.add(applicationName);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort alphabetically
|
||||
return Array.from(categories).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow statistics
|
||||
*/
|
||||
async function getWorkflowStats() {
|
||||
const workflows = await fetchAllWorkflows();
|
||||
const categories = await listWorkflowCategories();
|
||||
|
||||
// Count workflows per applicationName (the only grouping in the data)
|
||||
const categoryCounts = {};
|
||||
workflows.forEach(w => {
|
||||
const cat = w.applicationName || w.ApplicationName || '(unknown)';
|
||||
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
||||
});
|
||||
async function getWorkflowStats(application) {
|
||||
const app = resolveApplication(application);
|
||||
const workflows = await fetchAllWorkflows(app);
|
||||
|
||||
return {
|
||||
application: app,
|
||||
total: workflows.length,
|
||||
categories: categories.length,
|
||||
categoryCounts,
|
||||
cacheAge: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null
|
||||
cacheAge: cacheTimestamps[app] ? Math.floor((Date.now() - cacheTimestamps[app]) / 1000) : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear workflow cache (force refresh on next request)
|
||||
* Clear workflow caches (force refresh on next request) — toutes applications.
|
||||
*/
|
||||
function clearCache() {
|
||||
workflowCache = null;
|
||||
cacheTimestamp = null;
|
||||
workflowCaches = {};
|
||||
cacheTimestamps = {};
|
||||
applicationsCache = null;
|
||||
applicationsTimestamp = null;
|
||||
console.error('[Workflow] Cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache status
|
||||
* Get cache status, per application (D26)
|
||||
* @returns {Object} application -> { cached, count, timestamp, age, valid }
|
||||
*/
|
||||
function getCacheStatus() {
|
||||
return {
|
||||
cached: workflowCache !== null,
|
||||
count: workflowCache ? workflowCache.length : 0,
|
||||
timestamp: cacheTimestamp,
|
||||
age: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null,
|
||||
valid: isCacheValid()
|
||||
};
|
||||
const status = {};
|
||||
Object.keys(workflowCaches).forEach(app => {
|
||||
status[app] = {
|
||||
cached: true,
|
||||
count: workflowCaches[app].length,
|
||||
timestamp: cacheTimestamps[app],
|
||||
age: cacheTimestamps[app] ? Math.floor((Date.now() - cacheTimestamps[app]) / 1000) : null,
|
||||
valid: isCacheValid(app)
|
||||
};
|
||||
});
|
||||
return status;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchAllWorkflows,
|
||||
fetchApplications,
|
||||
searchWorkflows,
|
||||
getWorkflowDetails,
|
||||
listWorkflowCategories,
|
||||
getWorkflowStats,
|
||||
clearCache,
|
||||
getCacheStatus
|
||||
|
||||
Reference in New Issue
Block a user