L2.1 : résout entity_type via l'API Metadata (D21)
Context.{entity_type} attend le TableName du Metadata, pas le nom
d'entité de l'AD (Container -> Containers, mais Alias -> Alias) : un nom
faux partait en HTTP 500 de compilation LINQ. Nouveau service
entity-resolver.js : table Name|TableName (insensible à la casse) ->
TableName, agrégée sur les applications déployées (via
GET /configuration/applications — les applications sans contexte
requêtable n'y figurent pas et n'apportent 0 entité Metadata), cache TTL
partagé, invalidation par onSwitch (D8). Branché dans wms-query-service
(query/count/schema/search) et call_query_api.
Nom inconnu -> échec avant tout appel réseau de requête, suggestions
proches + renvoi vers get_entity_metadata. Metadata injoignable -> le
nom passe tel quel avec un warning dans la réponse.
Mesures (LIMAGRAIN, via le protocole) :
- query_wms_entities("Container", limit 1) -> succès, 1 ligne, résolu
Containers
- query_wms_entities("Alias") -> succès, invariant (pas de pluriel)
- query_wms_entities("Item") -> "Item" n'existe pas dans le modèle
Reading. Proches : RFMenuItems, Sites. 288 entités disponibles —
aucune ligne [API] POST dans stderr
- count_wms_entities("Product") -> 51160
- get_entity_schema("Container") et call_query_api("Container") : mêmes
résolutions
- 288 TableName distincts sur 5 applications, aucun conflit
Name -> TableName (mesuré le 24/08/2026)
Docs : D21 dans DECISIONS.md ; CLAUDE.md (piège retiré des points
ouverts, liste d'entités corrigée Aliases -> Alias, entity-resolver dans
la structure) ; exemple singulier/pluriel dans wms://query-examples ;
ROADMAP allégée (cause racine + L2.1 + L3.3 livrés).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@ src/
|
|||||||
│ └── logs.js logs://guide — patterns d'erreur et scénarios de debug
|
│ └── logs.js logs://guide — patterns d'erreur et scénarios de debug
|
||||||
├── services/ Logique métier
|
├── services/ Logique métier
|
||||||
│ ├── api-service.js OAuth + client HTTP + helpers de requête (singleton)
|
│ ├── api-service.js OAuth + client HTTP + helpers de requête (singleton)
|
||||||
|
│ ├── entity-resolver.js Résolution Name|TableName -> TableName (D21)
|
||||||
│ ├── workflow-service.js Workflows, lazy loading + cache
|
│ ├── workflow-service.js Workflows, lazy loading + cache
|
||||||
│ ├── ad-service.js Application Dictionary, 20 types, cache par type
|
│ ├── ad-service.js Application Dictionary, 20 types, cache par type
|
||||||
│ ├── wms-query-service.js Construction d'expressions LINQ
|
│ ├── wms-query-service.js Construction d'expressions LINQ
|
||||||
@@ -219,11 +220,14 @@ Autres règles :
|
|||||||
|
|
||||||
## Entités et éléments AD
|
## Entités et éléments AD
|
||||||
|
|
||||||
**Entités interrogeables** (Query API) : `Products`, `Containers`, `Accounts`,
|
**Entités interrogeables** (Query API) : `entity_type` accepte le nom d'entité
|
||||||
`Suppliers`, `Kits`, `Aliases`, `Tasks`, `Stocks`, `ProductLocations`,
|
AD (`Container`) ou le `TableName` (`Containers`), insensible à la casse — la
|
||||||
`InboundOrders`, `Receptions`, `OutboundOrders`. La liste faisant foi s'obtient
|
résolution passe par `entity-resolver.js` (D21). Courantes : `Products`,
|
||||||
par `get_entity_metadata` (API Metadata) — le catalogue de la resource
|
`Containers`, `Accounts`, `Suppliers`, `Kits`, `Alias` (invariant, pas de
|
||||||
`wms://entities` est un raccourci de confort, pas la référence.
|
pluriel), `Tasks`, `Stocks`, `ProductLocations`, `InboundOrders`, `Receptions`,
|
||||||
|
`OutboundOrders`. La liste faisant foi (288 entités, toutes applications
|
||||||
|
confondues) s'obtient par `get_entity_metadata` (API Metadata) — le catalogue
|
||||||
|
de la resource `wms://entities` est un raccourci de confort, pas la référence.
|
||||||
|
|
||||||
**Application Dictionary** : 20 types, ~38 800 éléments. `Resource` (29 374) est
|
**Application Dictionary** : 20 types, ~38 800 éléments. `Resource` (29 374) est
|
||||||
de loin le plus lourd ; 3 types sont valides mais vides (`Dashboard`,
|
de loin le plus lourd ; 3 types sont valides mais vides (`Dashboard`,
|
||||||
@@ -278,14 +282,5 @@ printf '%s\n%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"
|
|||||||
|
|
||||||
## Points ouverts
|
## Points ouverts
|
||||||
|
|
||||||
Voir [ROADMAP.md](ROADMAP.md) : lots de correction planifiés, cause racine
|
Voir [ROADMAP.md](ROADMAP.md) : lots de correction planifiés et propositions
|
||||||
commune (résolution `Name` -> `TableName` des entités), et propositions
|
|
||||||
explicitement écartées.
|
explicitement écartées.
|
||||||
|
|
||||||
⚠️ Piège connu et non encore corrigé, à garder en tête en attendant le lot 2 :
|
|
||||||
|
|
||||||
- `entity_type` est interpolé sans validation dans `Context.{entity_type}`. Le
|
|
||||||
nom attendu est le `TableName` de l'API Metadata, pas le nom d'entité de l'AD
|
|
||||||
(`Container` -> `Containers`, mais `Alias` -> `Alias`). Un mauvais nom donne un
|
|
||||||
HTTP 500 — dont le détail (erreur de compilation LINQ) remonte désormais dans
|
|
||||||
la réponse de l'outil (L1.1).
|
|
||||||
|
|||||||
@@ -353,6 +353,41 @@ publication du dépôt.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## D21 — `Context.{...}` attend le `TableName` du Metadata, résolu par service
|
||||||
|
|
||||||
|
**Piège.** Les expressions LINQ de `QueryExecute` référencent les entités par
|
||||||
|
le `TableName` de l'API Metadata, **pas** par le nom d'entité de l'Application
|
||||||
|
Dictionary. Ce n'est pas une pluralisation : `Container` -> `Containers`, mais
|
||||||
|
`Alias` -> `Alias` (invariant), et `Item` n'existe pas. Un nom faux part en
|
||||||
|
HTTP 500 (erreur de compilation `'ApplicationReadingContext' ne contient pas
|
||||||
|
de définition pour '...'`). Seul `TableName` fait foi — **ne réinventez pas de
|
||||||
|
règle grammaticale**.
|
||||||
|
|
||||||
|
**Décision.** `src/services/entity-resolver.js` construit une table
|
||||||
|
`Name | TableName (insensible à la casse) -> TableName` et tous les points
|
||||||
|
d'interpolation (`wms-query-service`, `call_query_api`) passent par elle.
|
||||||
|
Mesures du 24/08/2026 (`LIMAGRAI2512`) :
|
||||||
|
|
||||||
|
- Le contexte de lecture est **commun au tenant** : la table agrège le
|
||||||
|
Metadata de toutes les applications. La liste vient de
|
||||||
|
`GET /configuration/applications` (5 applications déployées avec version) —
|
||||||
|
les applications EasyBuilder sans contexte requêtable (`CustomApp`…) n'y
|
||||||
|
figurent pas et ne fournissent de toute façon **0 entité** Metadata.
|
||||||
|
- 288 `TableName` distincts, aucun conflit `Name -> TableName` entre
|
||||||
|
applications.
|
||||||
|
|
||||||
|
Comportements :
|
||||||
|
|
||||||
|
- **Nom inconnu** : échec avant tout appel réseau de requête, message avec
|
||||||
|
suggestions proches et renvoi vers `get_entity_metadata`.
|
||||||
|
- **Metadata injoignable** : le nom passe tel quel (comportement historique)
|
||||||
|
et la réponse porte un `warning` — on ne bloque pas tout le serveur pour un
|
||||||
|
cache irrécupérable.
|
||||||
|
- Cache : TTL partagé (`WORKFLOW_CACHE_TTL`), chargement paresseux,
|
||||||
|
invalidation par abonnement `onSwitch()` (D8, D10).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## D22 — Routage des outils par table explicite, plus par préfixe de nom
|
## D22 — Routage des outils par table explicite, plus par préfixe de nom
|
||||||
|
|
||||||
**Piège.** Le handler `tools/call` de `src/index.js` routait par préfixe de nom
|
**Piège.** Le handler `tools/call` de `src/index.js` routait par préfixe de nom
|
||||||
|
|||||||
+4
-54
@@ -12,51 +12,10 @@ contient que ce qui reste à faire.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cause racine commune
|
|
||||||
|
|
||||||
Le MCP interpole `entity_type` dans `Context.{entity_type}` **sans aucune
|
|
||||||
validation** (vérifié : aucune liste blanche dans le code). Or le nom attendu
|
|
||||||
par le contexte de lecture n'est pas le nom d'entité de l'Application
|
|
||||||
Dictionary.
|
|
||||||
|
|
||||||
L'API Metadata (`GET /Metadata/Entities`, **232 entités**) donne la
|
|
||||||
correspondance exacte :
|
|
||||||
|
|
||||||
| `Name` (renvoyé par `search_ad_elements`) | `TableName` (attendu par `Context.`) |
|
|
||||||
|---|---|
|
|
||||||
| `Container` | `Containers` |
|
|
||||||
| `Product` | `Products` |
|
|
||||||
| `ContainerType` | `ContainerTypes` |
|
|
||||||
| `Alias` | `Alias` — **invariant, pas de pluriel** |
|
|
||||||
| `Item` | *n'existe pas dans le modèle Reading* |
|
|
||||||
|
|
||||||
Ce n'est donc pas une règle de pluralisation : c'est un mapping, et seul
|
|
||||||
`TableName` fait foi. `TableName` est unique sur les 232 entités.
|
|
||||||
|
|
||||||
Conséquences déjà constatées :
|
|
||||||
- une session utilisant les noms de l'AD (singuliers) déclenche un **HTTP 500**
|
|
||||||
sur chaque requête ;
|
|
||||||
- la liste d'entités documentée était fausse (`Aliases` n'existe pas, c'est
|
|
||||||
`Alias`) ;
|
|
||||||
- le MCP n'expose que 12 entités figées là où l'API en connaît 232.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Lot 2 — Correctif de fond
|
## Lot 2 — Correctif de fond
|
||||||
|
|
||||||
### L2.1 — Résolution des entités via l'API Metadata
|
La cause racine commune du lot (résolution `Name` -> `TableName` via l'API
|
||||||
|
Metadata) est livrée — la règle et ses mesures vivent en **D21**.
|
||||||
Accepter `entity_type` au nom d'entité (`Container`) ou au nom de jeu
|
|
||||||
(`Containers`), insensible à la casse, et émettre `Context.{TableName}`. Cache
|
|
||||||
identique aux autres (TTL partagé, invalidation au changement de profil).
|
|
||||||
|
|
||||||
Sur nom inconnu, échouer **avant tout appel réseau**, avec un message
|
|
||||||
actionnable :
|
|
||||||
|
|
||||||
> « Item » n'existe pas dans le modèle Reading. Proches : ItemGroup, StockItem.
|
|
||||||
> 232 entités disponibles — utilisez `get_entity_metadata` pour la liste.
|
|
||||||
|
|
||||||
Supprime la cause des 500 et débloque 232 entités au lieu de 12.
|
|
||||||
|
|
||||||
### L2.2 — Rejeter les paramètres inconnus
|
### L2.2 — Rejeter les paramètres inconnus
|
||||||
|
|
||||||
@@ -114,14 +73,6 @@ diagnostic exact, invisible depuis les outils comme depuis le smoke test.
|
|||||||
Faire remonter `error.response.data` dans le message, comme L1.1 l'a fait pour
|
Faire remonter `error.response.data` dans le message, comme L1.1 l'a fait pour
|
||||||
les outils. Même contrainte : ne jamais logguer les credentials.
|
les outils. Même contrainte : ne jamais logguer les credentials.
|
||||||
|
|
||||||
### L3.3 — Documentation
|
|
||||||
|
|
||||||
- DECISIONS.md : **D21** la règle `TableName` (D22, le routage par table
|
|
||||||
explicite, a été livrée avec le lot 1).
|
|
||||||
- CLAUDE.md : corriger la liste d'entités (`Aliases` → `Alias`) et renvoyer vers
|
|
||||||
`get_entity_metadata` comme source de vérité.
|
|
||||||
- `wms://query-examples` : un exemple singulier/pluriel commenté.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Lot 4 — Modèle de données et applications
|
## Lot 4 — Modèle de données et applications
|
||||||
@@ -186,9 +137,8 @@ applications).
|
|||||||
`Application: "AGV"` qu'avec `Application: "EasyWMS"`. Le contexte de lecture est
|
`Application: "AGV"` qu'avec `Application: "EasyWMS"`. Le contexte de lecture est
|
||||||
**commun au tenant** : toutes les applications y déversent leurs entités.
|
**commun au tenant** : toutes les applications y déversent leurs entités.
|
||||||
|
|
||||||
Conséquence pour L2.1 : la table de résolution doit **agréger le Metadata de
|
Conséquence — traitée : la table de résolution (D21) **agrège le Metadata de
|
||||||
toutes les applications** (`GET /Metadata/Entities?applicationName=…` par
|
toutes les applications** déployées, et non le seul `EasyWMS`. Inutile en
|
||||||
application, 232 + 20 + 6 + …), et non se limiter à `EasyWMS`. Inutile en
|
|
||||||
revanche d'ajouter un paramètre `application` à `QueryExecute` : il ne changerait
|
revanche d'ajouter un paramètre `application` à `QueryExecute` : il ne changerait
|
||||||
rien.
|
rien.
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ function getQueryExamples() {
|
|||||||
> (QueryType=Reading). Status/enum fields are **strings** (enum names), never integers.
|
> (QueryType=Reading). Status/enum fields are **strings** (enum names), never integers.
|
||||||
> Always verify enum values via \`docs://entities/\` or \`get_entity_metadata\` before filtering.
|
> Always verify enum values via \`docs://entities/\` or \`get_entity_metadata\` before filtering.
|
||||||
|
|
||||||
|
## Entity names — singular AD name or TableName, both accepted
|
||||||
|
|
||||||
|
\`entity_type\` is resolved case-insensitively against the Metadata API: the AD
|
||||||
|
entity name (singular) and the TableName both work. The mapping is **not** a
|
||||||
|
pluralisation rule — only the Metadata \`TableName\` is authoritative:
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
query_wms_entities(entity_type="Container") # AD name -> resolved to Containers
|
||||||
|
query_wms_entities(entity_type="Containers") # TableName -> used as-is
|
||||||
|
query_wms_entities(entity_type="Alias") # invariant: TableName IS "Alias" (no plural)
|
||||||
|
query_wms_entities(entity_type="Item") # fails fast: not in the Reading model,
|
||||||
|
# error lists close matches + get_entity_metadata
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Diagnostic Recipes
|
## Diagnostic Recipes
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/**
|
||||||
|
* Entity Resolver Service
|
||||||
|
* Résout un nom d'entité (Name de l'AD ou TableName, insensible à la casse)
|
||||||
|
* vers le TableName attendu par Context.{...} dans les requêtes LINQ (D21).
|
||||||
|
*
|
||||||
|
* Le mapping n'est PAS une pluralisation (Container -> Containers, mais
|
||||||
|
* Alias -> Alias) : seul le TableName de l'API Metadata fait foi. Le contexte
|
||||||
|
* de lecture étant commun au tenant, la table agrège le Metadata de toutes
|
||||||
|
* les applications installées.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const apiService = require('./api-service').getInstance();
|
||||||
|
const profileManager = require('../config/profile-manager');
|
||||||
|
|
||||||
|
// Cache state — même TTL que les autres caches (D10)
|
||||||
|
let resolutionMap = null; // Map lower(Name | TableName) -> TableName
|
||||||
|
let tableNames = null; // TableName[] triés (suggestions + comptage)
|
||||||
|
let cacheTimestamp = null;
|
||||||
|
const CACHE_TTL = parseInt(process.env.WORKFLOW_CACHE_TTL) || 3600000;
|
||||||
|
|
||||||
|
// La table de résolution est par tenant — invalidée à chaque bascule (D8).
|
||||||
|
profileManager.onSwitch(() => invalidateCache());
|
||||||
|
|
||||||
|
function isCacheValid() {
|
||||||
|
if (!resolutionMap || !cacheTimestamp) return false;
|
||||||
|
return Date.now() - cacheTimestamp < CACHE_TTL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge la table de résolution depuis l'API Metadata, agrégée sur toutes
|
||||||
|
* les applications installées.
|
||||||
|
* GET /configuration/applications ne liste que les applications déployées
|
||||||
|
* avec une version — les applications EasyBuilder sans contexte requêtable
|
||||||
|
* (CustomApp...) n'y figurent pas et ne fournissent de toute façon aucune
|
||||||
|
* entité Metadata.
|
||||||
|
*/
|
||||||
|
async function loadResolutionMap() {
|
||||||
|
if (isCacheValid()) return;
|
||||||
|
|
||||||
|
console.error('[EntityResolver] Cache expired or empty, fetching Metadata...');
|
||||||
|
|
||||||
|
const apps = await apiService.get('/configuration/applications');
|
||||||
|
const appNames = (Array.isArray(apps) ? apps : [])
|
||||||
|
.map(a => a.Name || a.name)
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
if (appNames.length === 0) {
|
||||||
|
throw new Error('GET /configuration/applications returned no application');
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
const names = new Set();
|
||||||
|
|
||||||
|
for (const app of appNames) {
|
||||||
|
const entities = await apiService.getMetadataEntities(app);
|
||||||
|
for (const e of (Array.isArray(entities) ? entities : [])) {
|
||||||
|
const tableName = e.TableName || e.tableName;
|
||||||
|
const name = e.Name || e.name;
|
||||||
|
if (!tableName) continue;
|
||||||
|
names.add(tableName);
|
||||||
|
map.set(tableName.toLowerCase(), tableName);
|
||||||
|
if (name) map.set(name.toLowerCase(), tableName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (names.size === 0) {
|
||||||
|
throw new Error('Metadata API returned no entity for any application');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionMap = map;
|
||||||
|
tableNames = Array.from(names).sort();
|
||||||
|
cacheTimestamp = Date.now();
|
||||||
|
console.error(`[EntityResolver] Cached ${tableNames.size} entities from ${appNames.length} application(s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distance de Levenshtein — uniquement pour suggérer des noms proches.
|
||||||
|
*/
|
||||||
|
function levenshtein(a, b) {
|
||||||
|
const m = a.length;
|
||||||
|
const n = b.length;
|
||||||
|
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
||||||
|
for (let i = 1; i <= m; i++) {
|
||||||
|
const curr = [i];
|
||||||
|
for (let j = 1; j <= n; j++) {
|
||||||
|
curr[j] = Math.min(
|
||||||
|
prev[j] + 1,
|
||||||
|
curr[j - 1] + 1,
|
||||||
|
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prev = curr;
|
||||||
|
}
|
||||||
|
return prev[n];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suggère les TableName les plus proches d'un nom inconnu :
|
||||||
|
* correspondances par sous-chaîne d'abord, puis distance d'édition.
|
||||||
|
*/
|
||||||
|
function suggestClosest(input, limit = 5) {
|
||||||
|
const lower = input.toLowerCase();
|
||||||
|
const scored = tableNames.map(tn => {
|
||||||
|
const l = tn.toLowerCase();
|
||||||
|
const score = (l.includes(lower) || lower.includes(l))
|
||||||
|
? Math.abs(l.length - lower.length) // sous-chaîne : quasi-match
|
||||||
|
: 100 + levenshtein(lower, l); // sinon : distance d'édition
|
||||||
|
return { tn, score };
|
||||||
|
});
|
||||||
|
scored.sort((a, b) => a.score - b.score || a.tn.localeCompare(b.tn));
|
||||||
|
const maxEditDistance = Math.max(3, Math.floor(lower.length / 2));
|
||||||
|
return scored
|
||||||
|
.filter(s => s.score < 100 + maxEditDistance)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(s => s.tn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résout un nom d'entité vers son TableName.
|
||||||
|
*
|
||||||
|
* @param {string} entityType - Name AD ou TableName, insensible à la casse
|
||||||
|
* @returns {Promise<{tableName: string, warning?: string}>}
|
||||||
|
* - nom connu : { tableName } (le TableName exact)
|
||||||
|
* - Metadata injoignable : { tableName: entityType, warning } — on laisse
|
||||||
|
* passer le nom tel quel (comportement historique) plutôt que de tout
|
||||||
|
* bloquer, et on le dit dans la réponse
|
||||||
|
* @throws {Error} nom inconnu du modèle Reading — AVANT tout appel réseau de
|
||||||
|
* requête, avec suggestions proches et renvoi vers get_entity_metadata
|
||||||
|
*/
|
||||||
|
async function resolveEntityType(entityType) {
|
||||||
|
if (!entityType || typeof entityType !== 'string' || entityType.trim() === '') {
|
||||||
|
throw new Error('entity_type est requis. Utilisez get_entity_metadata pour la liste des entités interrogeables.');
|
||||||
|
}
|
||||||
|
const trimmed = entityType.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadResolutionMap();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[EntityResolver] Metadata unreachable, passing "${trimmed}" through as-is: ${err.message}`);
|
||||||
|
return {
|
||||||
|
tableName: trimmed,
|
||||||
|
warning: `Le nom d'entité "${trimmed}" n'a pas pu être validé (API Metadata injoignable : ${err.message}). Il est transmis tel quel au WMS.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableName = resolutionMap.get(trimmed.toLowerCase());
|
||||||
|
if (tableName) {
|
||||||
|
return { tableName };
|
||||||
|
}
|
||||||
|
|
||||||
|
const suggestions = suggestClosest(trimmed);
|
||||||
|
const closest = suggestions.length > 0 ? ` Proches : ${suggestions.join(', ')}.` : '';
|
||||||
|
throw new Error(
|
||||||
|
`"${trimmed}" n'existe pas dans le modèle Reading.${closest} ` +
|
||||||
|
`${tableNames.length} entités disponibles — utilisez get_entity_metadata pour la liste.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalide la table de résolution (bascule de profil).
|
||||||
|
*/
|
||||||
|
function invalidateCache() {
|
||||||
|
resolutionMap = null;
|
||||||
|
tableNames = null;
|
||||||
|
cacheTimestamp = null;
|
||||||
|
console.error('[EntityResolver] Cache cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* État du cache (exposé par get_application_summary si besoin).
|
||||||
|
*/
|
||||||
|
function getCacheStatus() {
|
||||||
|
return {
|
||||||
|
cached: resolutionMap !== null,
|
||||||
|
count: tableNames ? tableNames.length : 0,
|
||||||
|
timestamp: cacheTimestamp,
|
||||||
|
age: cacheTimestamp ? Math.floor((Date.now() - cacheTimestamp) / 1000) : null,
|
||||||
|
valid: isCacheValid(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
resolveEntityType,
|
||||||
|
invalidateCache,
|
||||||
|
getCacheStatus,
|
||||||
|
};
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const apiService = require('./api-service').getInstance();
|
const apiService = require('./api-service').getInstance();
|
||||||
|
const entityResolver = require('./entity-resolver');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a LINQ select expression
|
* Build a LINQ select expression
|
||||||
@@ -20,13 +21,17 @@ const apiService = require('./api-service').getInstance();
|
|||||||
* @param {number} limit - Result limit
|
* @param {number} limit - Result limit
|
||||||
*/
|
*/
|
||||||
async function queryEntities(entityType, selectExpression = 'z => z', filter = null, limit = 100) {
|
async function queryEntities(entityType, selectExpression = 'z => z', filter = null, limit = 100) {
|
||||||
|
// Résolution Name/TableName -> TableName (D21). Un nom inconnu échoue ici,
|
||||||
|
// avant tout appel réseau de requête — l'erreur porte les suggestions.
|
||||||
|
const { tableName, warning } = await entityResolver.resolveEntityType(entityType);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enforce max limit
|
// Enforce max limit
|
||||||
const maxLimit = parseInt(process.env.MAX_QUERY_ROWS) || 1000;
|
const maxLimit = parseInt(process.env.MAX_QUERY_ROWS) || 1000;
|
||||||
const actualLimit = Math.min(limit, maxLimit);
|
const actualLimit = Math.min(limit, maxLimit);
|
||||||
|
|
||||||
// Build expression: Context + optional Where + OrderBy (required by EF when Take is used)
|
// Build expression: Context + optional Where + OrderBy (required by EF when Take is used)
|
||||||
let expression = `Context.${entityType}`;
|
let expression = `Context.${tableName}`;
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
|
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
|
||||||
expression += `.Where(${whereExpr})`;
|
expression += `.Where(${whereExpr})`;
|
||||||
@@ -43,6 +48,8 @@ async function queryEntities(entityType, selectExpression = 'z => z', filter = n
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
entityType,
|
entityType,
|
||||||
|
resolvedTableName: tableName,
|
||||||
|
...(warning ? { warning } : {}),
|
||||||
expression,
|
expression,
|
||||||
limit: actualLimit,
|
limit: actualLimit,
|
||||||
count: Array.isArray(result) ? result.length : 0,
|
count: Array.isArray(result) ? result.length : 0,
|
||||||
@@ -144,9 +151,13 @@ async function getEntitySchema(entityType) {
|
|||||||
* @param {string|null} filter - Optional filter
|
* @param {string|null} filter - Optional filter
|
||||||
*/
|
*/
|
||||||
async function countEntities(entityType, filter = null) {
|
async function countEntities(entityType, filter = null) {
|
||||||
|
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
||||||
|
// sur nom inconnu.
|
||||||
|
const { tableName, warning } = await entityResolver.resolveEntityType(entityType);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build: Context.Entity.Where(...).Count()
|
// Build: Context.Entity.Where(...).Count()
|
||||||
const parts = [`Context.${entityType}`];
|
const parts = [`Context.${tableName}`];
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
|
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
|
||||||
parts.push(`Where(${whereExpr})`);
|
parts.push(`Where(${whereExpr})`);
|
||||||
@@ -160,6 +171,8 @@ async function countEntities(entityType, filter = null) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
entityType,
|
entityType,
|
||||||
|
resolvedTableName: tableName,
|
||||||
|
...(warning ? { warning } : {}),
|
||||||
filter,
|
filter,
|
||||||
count
|
count
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const apiService = require('../services/api-service').getInstance();
|
const apiService = require('../services/api-service').getInstance();
|
||||||
|
const entityResolver = require('../services/entity-resolver');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tools MCP pour interagir avec les APIs WMS
|
* Tools MCP pour interagir avec les APIs WMS
|
||||||
@@ -18,7 +19,7 @@ function listTools() {
|
|||||||
properties: {
|
properties: {
|
||||||
entity_type: {
|
entity_type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Type d\'entité (Containers, Stocks, ProductLocations, Tasks, Products, Accounts, Suppliers, Kits, Aliases, InboundOrders, Receptions, OutboundOrders)',
|
description: 'Type d\'entité — nom AD (Container) ou TableName (Containers), insensible à la casse, résolu via l\'API Metadata. Ex: Containers, Stocks, ProductLocations, Tasks, Products, Accounts, Suppliers, Kits, Alias, InboundOrders, Receptions, OutboundOrders. Liste complète via get_entity_metadata.',
|
||||||
},
|
},
|
||||||
expression: {
|
expression: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@@ -85,8 +86,12 @@ async function callQueryAPI(args) {
|
|||||||
const { entity_type, expression = 'z => z', filter, limit = 100 } = args;
|
const { entity_type, expression = 'z => z', filter, limit = 100 } = args;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
||||||
|
// sur nom inconnu.
|
||||||
|
const { tableName, warning } = await entityResolver.resolveEntityType(entity_type);
|
||||||
|
|
||||||
// Expression = Context.Entity + optional Where + OrderBy (required by EF when Take is used)
|
// Expression = Context.Entity + optional Where + OrderBy (required by EF when Take is used)
|
||||||
let linqExpression = `Context.${entity_type}`;
|
let linqExpression = `Context.${tableName}`;
|
||||||
if (filter) {
|
if (filter) {
|
||||||
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
|
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
|
||||||
linqExpression += `.Where(${whereExpr})`;
|
linqExpression += `.Where(${whereExpr})`;
|
||||||
@@ -106,6 +111,8 @@ async function callQueryAPI(args) {
|
|||||||
{
|
{
|
||||||
success: true,
|
success: true,
|
||||||
entityType: entity_type,
|
entityType: entity_type,
|
||||||
|
resolvedTableName: tableName,
|
||||||
|
...(warning ? { warning } : {}),
|
||||||
result,
|
result,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ function listTools() {
|
|||||||
name: 'query_wms_entities',
|
name: 'query_wms_entities',
|
||||||
description: `Query WMS entities using LINQ expressions. Returns rows (up to 1000).
|
description: `Query WMS entities using LINQ expressions. Returns rows (up to 1000).
|
||||||
Uses QueryExecute with QueryType=Reading — status fields are STRINGS (enum names, not integers).
|
Uses QueryExecute with QueryType=Reading — status fields are STRINGS (enum names, not integers).
|
||||||
Common entities: Products, Containers, Tasks, Stocks, ProductLocations, Location, InboundOrders, OutboundOrders, Receptions, Accounts, Suppliers, Kits, Aliases.
|
Common entities: Products, Containers, Tasks, Stocks, ProductLocations, Locations, InboundOrders, OutboundOrders, Receptions, Accounts, Suppliers, Kits, Alias. Full list via get_entity_metadata.
|
||||||
|
entity_type accepts the AD entity name (Container) or the TableName (Containers), case-insensitive — resolved via the Metadata API.
|
||||||
|
|
||||||
IMPORTANT — before building a filter with a status/enum field:
|
IMPORTANT — before building a filter with a status/enum field:
|
||||||
1. Check docs first: read resource docs://entities/ (e.g. easywms_reading_entites_outboundorder_OutboundOrderStatus for OutboundOrders)
|
1. Check docs first: read resource docs://entities/ (e.g. easywms_reading_entites_outboundorder_OutboundOrderStatus for OutboundOrders)
|
||||||
@@ -26,7 +27,7 @@ Never guess enum string values — they differ between Reading and Writing model
|
|||||||
properties: {
|
properties: {
|
||||||
entity_type: {
|
entity_type: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Entity type (Products, Containers, Tasks, Stocks, ProductLocations, InboundOrders, OutboundOrders, Accounts, Suppliers, Kits, Aliases, Receptions)',
|
description: 'Entity type — AD name (Container) or TableName (Containers), case-insensitive, resolved via the Metadata API. E.g. Products, Containers, Tasks, Stocks, ProductLocations, InboundOrders, OutboundOrders, Accounts, Suppliers, Kits, Alias, Receptions.',
|
||||||
},
|
},
|
||||||
select_expression: {
|
select_expression: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
|
|||||||
Reference in New Issue
Block a user