L4.1 : expose query_type sur les outils de requête (D25)
QueryType était figé à 0 en dur dans executeQuery/executeScalarQuery : le modèle Writing, opérationnel et mesuré, était inatteignable. Paramètre query_type (défaut 0) sur call_query_api, query_wms_entities, count_wms_entities — get_entity_schema et search_wms_data restent des raccourcis Reading. D3 reste la règle par défaut : la bascule est un opt-in, avertie dans les descriptions d'outils (statuts en énumérations en Writing). Garde de valeur assertValidQueryType dans le code (le wrapper D23 ne valide pas les valeurs), avant tout réseau. En query_type != 0, un nom inconnu du Metadata Reading passe tel quel avec warning (allowUnknown du resolver) — conservé aussi dans la réponse d'erreur si le WMS échoue ensuite. Acté en D25 ; CLAUDE.md nuancé, L4.1 retiré de la ROADMAP (reliquat : exploration Metrics, rapport à part). Vérifications rejouées via le protocole (LIMAGRAIN / LIMAGRAI2512) : - call_query_api(Products, query_type:1, limit:1) -> success, 1 ligne, queryType:1 dans la réponse. - call_query_api(Products, query_type:3) -> erreur structurée contenant 'ApplicationMetricDataContext' ne contient pas de définition pour 'Products'. - call_query_api(Products, query_type:7) -> erreur locale nommant les 4 contextes, aucun [API] POST/GET dans stderr. - query_wms_entities(Container, limit:1) sans query_type -> comportement inchangé (resolvedTableName Containers, Reading, pas de champ queryType). - call_query_api(FooBar123, query_type:1) -> transmis tel quel (payload QueryType:1 Expression Context.FooBar123...), erreur WMS ApplicationWritingRepository + warning de résolution dans la réponse. - count_wms_entities(Product, query_type:1) -> count 51145 (~51160). Baseline : tools/list 23, resources 6, rejet D23 d'un paramètre inconnu OK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -205,8 +205,10 @@ la plus fréquente :
|
|||||||
|
|
||||||
Autres règles :
|
Autres règles :
|
||||||
|
|
||||||
- **`QueryType: 0` (Reading)**, jamais 1 : les statuts sont alors des chaînes
|
- **`QueryType: 0` (Reading) par défaut** : les statuts sont alors des chaînes
|
||||||
(D3).
|
(D3). La bascule vers Writing/Metrics passe par le paramètre `query_type`
|
||||||
|
des outils de requête — un opt-in documenté (D25), jamais un défaut : ne
|
||||||
|
recopiez aucun exemple en `QueryType: 1`.
|
||||||
- **Pas de date relative.** `DateTime.Now`, `DateTime.Today`, `AddDays()` ne
|
- **Pas de date relative.** `DateTime.Now`, `DateTime.Today`, `AddDays()` ne
|
||||||
sont pas traduisibles : écrire `new DateTime(2026, 8, 1)` (D12).
|
sont pas traduisibles : écrire `new DateTime(2026, 8, 1)` (D12).
|
||||||
- **`select_expression` est instable** : les projections via le paramètre
|
- **`select_expression` est instable** : les projections via le paramètre
|
||||||
|
|||||||
@@ -480,3 +480,42 @@ Deux garde-fous de cadrage :
|
|||||||
Au passage, `totalParameters` a changé de sens : c'était le nombre brut
|
Au passage, `totalParameters` a changé de sens : c'était le nombre brut
|
||||||
d'entités `Parameter` chargées, c'est désormais le total correspondant aux
|
d'entités `Parameter` chargées, c'est désormais le total correspondant aux
|
||||||
filtres avant pagination (identique sans filtre).
|
filtres avant pagination (identique sans filtre).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## D25 — `query_type` : opt-in explicite, D3 reste la règle par défaut
|
||||||
|
|
||||||
|
**Contexte (mesures des 24-25/08/2026, `LIMAGRAI2512`).** `QueryType` était
|
||||||
|
figé à `0` en dur dans `executeQuery()` et `executeScalarQuery()`, rendant le
|
||||||
|
modèle Writing — opérationnel et mesuré (`Context.Products` en `QueryType: 1`
|
||||||
|
répond) — inatteignable.
|
||||||
|
|
||||||
|
**Décision.** Un paramètre `query_type` (entier, défaut `0`) est exposé sur
|
||||||
|
**trois outils** : `call_query_api`, `query_wms_entities`,
|
||||||
|
`count_wms_entities`. `get_entity_schema` et `search_wms_data` restent des
|
||||||
|
raccourcis Reading, sans paramètre.
|
||||||
|
|
||||||
|
**Rapport à D3.** D3 n'est pas révisée : le Reading reste la règle par défaut,
|
||||||
|
car en Writing les statuts sont des **énumérations** — les comparaisons de
|
||||||
|
chaînes (`== "Release"`), cas le plus courant en debug, y échouent. La bascule
|
||||||
|
est un opt-in explicite et les descriptions d'outils portent l'avertissement.
|
||||||
|
|
||||||
|
Modalités :
|
||||||
|
|
||||||
|
- **Garde de valeur dans le code de l'outil**, pas dans le wrapper : D23 valide
|
||||||
|
les noms de paramètres, pas les valeurs. Hors `0..3` (ou non entier) →
|
||||||
|
erreur locale via `assertValidQueryType()` (`wms-query-service.js`), **avant
|
||||||
|
tout appel réseau**, nommant les quatre contextes.
|
||||||
|
- **`2` et `3` sont transmis tels quels** : le WMS répond et son diagnostic
|
||||||
|
remonte entier (L1.1). Sur `LIMAGRAI2512` : `2` = DataWarehouse non configuré
|
||||||
|
(`Could not resolve serviceType 'IDataWarehouse…'`), `3` = Metrics, contexte
|
||||||
|
présent mais modèle distinct (`'ApplicationMetricDataContext' ne contient pas
|
||||||
|
de définition pour 'Products'`).
|
||||||
|
- **Interaction avec le resolver (D21)** : la table de résolution est
|
||||||
|
construite sur le Metadata **Reading**. Quand `query_type != 0`, un nom qui
|
||||||
|
se résout se résout normalement (`Products` marche en Writing, mesuré) ; un
|
||||||
|
nom **inconnu** du Reading n'est **pas** bloqué — il passe tel quel avec un
|
||||||
|
`warning` dans la réponse (`allowUnknown` du resolver, même mécanique que le
|
||||||
|
repli « Metadata injoignable »), car le modèle Writing/Metrics peut contenir
|
||||||
|
des entités hors Reading. Le `warning` est conservé aussi dans la réponse
|
||||||
|
d'erreur si le WMS échoue ensuite.
|
||||||
|
|||||||
+6
-18
@@ -17,25 +17,13 @@ contient que ce qui reste à faire.
|
|||||||
Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les
|
Deux angles morts constatés le 24/08/2026, plus larges que les lots 2 et 3. Les
|
||||||
chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`.
|
chiffres ci-dessous sont mesurés sur le tenant `LIMAGRAI2512`.
|
||||||
|
|
||||||
### L4.1 — Le modèle Writing est inatteignable
|
### L4.1 (reliquat) — Explorer le contexte Metrics
|
||||||
|
|
||||||
`QueryType` est figé à `0` (Reading) en dur dans `api-service.js`
|
L'exposition de `query_type` est livrée (D25). Reste l'investigation : le
|
||||||
(`executeQuery` et `executeScalarQuery`). Or `QueryContextType` a **quatre**
|
contexte `Metrics` (`QueryType: 3`, `ApplicationMetricDataContext`) mérite une
|
||||||
valeurs. Testées une à une :
|
exploration à part — c'est probablement là que vivent les données agrégées
|
||||||
|
produites par les jobs `MetricGatherer`. Livrable : un rapport, pas du code
|
||||||
| Valeur | Contexte | Résultat sur `LIMAGRAI2512` |
|
(même phase d'investigation que L4.4).
|
||||||
|---|---|---|
|
|
||||||
| `0` | Reading | opérationnel (seul utilisé aujourd'hui) |
|
|
||||||
| `1` | Writing | **opérationnel** — `Context.Products` répond |
|
|
||||||
| `2` | DataWarehouse | **non configuré** : `Could not resolve serviceType 'IDataWarehouse…'` |
|
|
||||||
| `3` | Metrics | contexte présent (`ApplicationMetricDataContext`), modèle non exploré |
|
|
||||||
|
|
||||||
Exposer `query_type` sur les outils de requête, défaut `0`. Attention : D3 reste
|
|
||||||
vrai — en Writing les champs de statut sont des **énumérations**, donc
|
|
||||||
`== "Release"` échoue. La bascule doit être un choix explicite et documenté.
|
|
||||||
|
|
||||||
Le contexte `Metrics` mérite une exploration à part : c'est probablement là que
|
|
||||||
vivent les données agrégées produites par les jobs `MetricGatherer`.
|
|
||||||
|
|
||||||
### L4.2 — Une seule application sur neuf est visible
|
### L4.2 — Une seule application sur neuf est visible
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Rules (see the query tools for details):
|
|||||||
- **\`QueryType: 0\` (Reading) is the default** — status fields are strings
|
- **\`QueryType: 0\` (Reading) is the default** — status fields are strings
|
||||||
(\`"Release"\`). \`QueryType: 1\` (Writing) exists but status fields become
|
(\`"Release"\`). \`QueryType: 1\` (Writing) exists but status fields become
|
||||||
enums there: string comparisons fail. Old examples using \`1\` must not be
|
enums there: string comparisons fail. Old examples using \`1\` must not be
|
||||||
copied.
|
copied. The query tools expose this as the opt-in \`query_type\` parameter.
|
||||||
- **\`Where\` and \`OrderBy\` go in the Expression; \`Take\`/\`Skip\` are API
|
- **\`Where\` and \`OrderBy\` go in the Expression; \`Take\`/\`Skip\` are API
|
||||||
parameters.** \`OrderBy\` is mandatory as soon as \`Take\` is used.
|
parameters.** \`OrderBy\` is mandatory as soon as \`Take\` is used.
|
||||||
- **No \`Select\` projections** — the \`Select\` parameter causes server-side
|
- **No \`Select\` projections** — the \`Select\` parameter causes server-side
|
||||||
|
|||||||
@@ -323,14 +323,17 @@ class APIService {
|
|||||||
* e.g. "Context.OutboundOrders.Where(z => z.OutboundOrderStatus == \"Release\").OrderBy(z => z.Id)"
|
* e.g. "Context.OutboundOrders.Where(z => z.OutboundOrderStatus == \"Release\").OrderBy(z => z.Id)"
|
||||||
*
|
*
|
||||||
* @param {string} expression - LINQ expression (Context.Entity or Context.Entity.Where(...))
|
* @param {string} expression - LINQ expression (Context.Entity or Context.Entity.Where(...))
|
||||||
* @param {object} options - { take, skip, select, orderBy, inlineCount }
|
* @param {object} options - { take, skip, select, orderBy, inlineCount, queryType }
|
||||||
*/
|
*/
|
||||||
async executeQuery(expression, options = {}) {
|
async executeQuery(expression, options = {}) {
|
||||||
const { take, skip, select, orderBy, inlineCount } = options;
|
const { take, skip, select, orderBy, inlineCount, queryType } = options;
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
Application: profileManager.getCurrent().application,
|
Application: profileManager.getCurrent().application,
|
||||||
QueryType: 0, // Reading = 0 (status fields are strings), Writing = 1 (enums)
|
// Reading = 0 par défaut (statuts en chaînes) ; Writing/Metrics en
|
||||||
|
// opt-in explicite via query_type (D25) — la garde de valeur vit dans
|
||||||
|
// wms-query-service.assertValidQueryType, pas ici.
|
||||||
|
QueryType: queryType ?? 0,
|
||||||
Expression: expression,
|
Expression: expression,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -363,11 +366,13 @@ class APIService {
|
|||||||
* Execute a scalar LINQ query (Count, Sum, etc.) via QueryScalarExecute.
|
* Execute a scalar LINQ query (Count, Sum, etc.) via QueryScalarExecute.
|
||||||
* Returns the scalar value directly.
|
* Returns the scalar value directly.
|
||||||
* @param {string} fullExpression - e.g. "Context.OutboundOrders.Where(...).Count()"
|
* @param {string} fullExpression - e.g. "Context.OutboundOrders.Where(...).Count()"
|
||||||
|
* @param {object} options - { queryType }
|
||||||
*/
|
*/
|
||||||
async executeScalarQuery(fullExpression) {
|
async executeScalarQuery(fullExpression, options = {}) {
|
||||||
const body = {
|
const body = {
|
||||||
Application: profileManager.getCurrent().application,
|
Application: profileManager.getCurrent().application,
|
||||||
QueryType: 0, // Reading = 0 — string enum names in filters (Writing=1 fails with enum comparisons)
|
// Reading = 0 par défaut — voir executeQuery / D25.
|
||||||
|
QueryType: options.queryType ?? 0,
|
||||||
Expression: fullExpression,
|
Expression: fullExpression,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -119,15 +119,22 @@ function suggestClosest(input, limit = 5) {
|
|||||||
* Résout un nom d'entité vers son TableName.
|
* Résout un nom d'entité vers son TableName.
|
||||||
*
|
*
|
||||||
* @param {string} entityType - Name AD ou TableName, insensible à la casse
|
* @param {string} entityType - Name AD ou TableName, insensible à la casse
|
||||||
|
* @param {object} [options]
|
||||||
|
* @param {boolean} [options.allowUnknown=false] - Un nom inconnu du Reading
|
||||||
|
* passe tel quel avec un warning au lieu d'échouer. Utilisé quand
|
||||||
|
* query_type != 0 (D25) : la table est construite sur le Metadata Reading,
|
||||||
|
* or le modèle Writing/Metrics peut contenir des entités hors Reading.
|
||||||
* @returns {Promise<{tableName: string, warning?: string}>}
|
* @returns {Promise<{tableName: string, warning?: string}>}
|
||||||
* - nom connu : { tableName } (le TableName exact)
|
* - nom connu : { tableName } (le TableName exact)
|
||||||
* - Metadata injoignable : { tableName: entityType, warning } — on laisse
|
* - Metadata injoignable : { tableName: entityType, warning } — on laisse
|
||||||
* passer le nom tel quel (comportement historique) plutôt que de tout
|
* passer le nom tel quel (comportement historique) plutôt que de tout
|
||||||
* bloquer, et on le dit dans la réponse
|
* bloquer, et on le dit dans la réponse
|
||||||
* @throws {Error} nom inconnu du modèle Reading — AVANT tout appel réseau de
|
* @throws {Error} nom inconnu du modèle Reading (sauf allowUnknown) — AVANT
|
||||||
* requête, avec suggestions proches et renvoi vers get_entity_metadata
|
* tout appel réseau de requête, avec suggestions proches et renvoi vers
|
||||||
|
* get_entity_metadata
|
||||||
*/
|
*/
|
||||||
async function resolveEntityType(entityType) {
|
async function resolveEntityType(entityType, options = {}) {
|
||||||
|
const { allowUnknown = false } = options;
|
||||||
if (!entityType || typeof entityType !== 'string' || entityType.trim() === '') {
|
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.');
|
throw new Error('entity_type est requis. Utilisez get_entity_metadata pour la liste des entités interrogeables.');
|
||||||
}
|
}
|
||||||
@@ -150,6 +157,15 @@ async function resolveEntityType(entityType) {
|
|||||||
|
|
||||||
const suggestions = suggestClosest(trimmed);
|
const suggestions = suggestClosest(trimmed);
|
||||||
const closest = suggestions.length > 0 ? ` Proches : ${suggestions.join(', ')}.` : '';
|
const closest = suggestions.length > 0 ? ` Proches : ${suggestions.join(', ')}.` : '';
|
||||||
|
|
||||||
|
if (allowUnknown) {
|
||||||
|
console.error(`[EntityResolver] "${trimmed}" unknown to Reading metadata, passing through (allowUnknown)`);
|
||||||
|
return {
|
||||||
|
tableName: trimmed,
|
||||||
|
warning: `"${trimmed}" est inconnu du modèle Reading (Metadata) ; il est transmis tel quel car query_type != 0 — le contexte demandé peut contenir des entités hors Reading.${closest}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`"${trimmed}" n'existe pas dans le modèle Reading.${closest} ` +
|
`"${trimmed}" n'existe pas dans le modèle Reading.${closest} ` +
|
||||||
`${tableNames.length} entités disponibles — utilisez get_entity_metadata pour la liste.`
|
`${tableNames.length} entités disponibles — utilisez get_entity_metadata pour la liste.`
|
||||||
|
|||||||
@@ -6,6 +6,27 @@
|
|||||||
const apiService = require('./api-service').getInstance();
|
const apiService = require('./api-service').getInstance();
|
||||||
const entityResolver = require('./entity-resolver');
|
const entityResolver = require('./entity-resolver');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Garde de valeur de query_type (D25). Le wrapper D23 valide les noms de
|
||||||
|
* paramètres, pas les valeurs — cette garde s'exécute AVANT tout appel réseau
|
||||||
|
* (y compris la résolution d'entité) et nomme les quatre contextes.
|
||||||
|
* @param {*} queryType - valeur reçue de l'outil (défaut 0 si absent)
|
||||||
|
* @returns {number} la valeur validée
|
||||||
|
*/
|
||||||
|
function assertValidQueryType(queryType) {
|
||||||
|
if (queryType == null) return 0;
|
||||||
|
if (!Number.isInteger(queryType) || queryType < 0 || queryType > 3) {
|
||||||
|
throw new Error(
|
||||||
|
`query_type invalide : ${JSON.stringify(queryType)}. Valeurs acceptées : ` +
|
||||||
|
`0 = Reading (défaut — statuts en chaînes, ex. "Release"), ` +
|
||||||
|
`1 = Writing (statuts en énumérations : les comparaisons de chaînes échouent), ` +
|
||||||
|
`2 = DataWarehouse (souvent non configuré), ` +
|
||||||
|
`3 = Metrics (modèle de données distinct).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return queryType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build a LINQ select expression
|
* Build a LINQ select expression
|
||||||
* @param {string} entityType - Entity type (Products, Containers, etc.)
|
* @param {string} entityType - Entity type (Products, Containers, etc.)
|
||||||
@@ -19,11 +40,19 @@ const entityResolver = require('./entity-resolver');
|
|||||||
* @param {string} selectExpression - LINQ select expression
|
* @param {string} selectExpression - LINQ select expression
|
||||||
* @param {string|null} filter - Optional filter
|
* @param {string|null} filter - Optional filter
|
||||||
* @param {number} limit - Result limit
|
* @param {number} limit - Result limit
|
||||||
|
* @param {number} queryType - QueryContextType (0 = Reading par défaut, D25)
|
||||||
*/
|
*/
|
||||||
async function queryEntities(entityType, selectExpression = 'z => z', filter = null, limit = 100) {
|
async function queryEntities(entityType, selectExpression = 'z => z', filter = null, limit = 100, queryType = 0) {
|
||||||
|
// Garde de valeur avant tout réseau (D25).
|
||||||
|
queryType = assertValidQueryType(queryType);
|
||||||
|
|
||||||
// Résolution Name/TableName -> TableName (D21). Un nom inconnu échoue ici,
|
// Résolution Name/TableName -> TableName (D21). Un nom inconnu échoue ici,
|
||||||
// avant tout appel réseau de requête — l'erreur porte les suggestions.
|
// avant tout appel réseau de requête — l'erreur porte les suggestions.
|
||||||
const { tableName, warning } = await entityResolver.resolveEntityType(entityType);
|
// En query_type != 0, un nom hors Reading passe tel quel avec warning : le
|
||||||
|
// modèle Writing/Metrics peut contenir des entités hors Reading (D25).
|
||||||
|
const { tableName, warning } = await entityResolver.resolveEntityType(entityType, {
|
||||||
|
allowUnknown: queryType !== 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enforce max limit
|
// Enforce max limit
|
||||||
@@ -39,17 +68,19 @@ async function queryEntities(entityType, selectExpression = 'z => z', filter = n
|
|||||||
// OrderBy must be embedded in the expression (not as a separate API param)
|
// OrderBy must be embedded in the expression (not as a separate API param)
|
||||||
expression += `.OrderBy(z => z.Id)`;
|
expression += `.OrderBy(z => z.Id)`;
|
||||||
|
|
||||||
console.error(`[WMSQuery] Querying ${entityType}: ${expression} | take=${actualLimit} select=${selectExpression}`);
|
console.error(`[WMSQuery] Querying ${entityType}: ${expression} | take=${actualLimit} select=${selectExpression} queryType=${queryType}`);
|
||||||
|
|
||||||
const result = await apiService.executeQuery(expression, {
|
const result = await apiService.executeQuery(expression, {
|
||||||
take: actualLimit,
|
take: actualLimit,
|
||||||
select: selectExpression !== 'z => z' ? selectExpression : undefined,
|
select: selectExpression !== 'z => z' ? selectExpression : undefined,
|
||||||
|
queryType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entityType,
|
entityType,
|
||||||
resolvedTableName: tableName,
|
resolvedTableName: tableName,
|
||||||
...(warning ? { warning } : {}),
|
...(warning ? { warning } : {}),
|
||||||
|
...(queryType !== 0 ? { queryType } : {}),
|
||||||
expression,
|
expression,
|
||||||
limit: actualLimit,
|
limit: actualLimit,
|
||||||
count: Array.isArray(result) ? result.length : 0,
|
count: Array.isArray(result) ? result.length : 0,
|
||||||
@@ -57,7 +88,9 @@ async function queryEntities(entityType, selectExpression = 'z => z', filter = n
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[WMSQuery] Query failed:`, error.message);
|
console.error(`[WMSQuery] Query failed:`, error.message);
|
||||||
throw new Error(`Query failed for ${entityType}: ${error.message}`);
|
// Le warning de résolution (nom hors Reading en query_type != 0) reste
|
||||||
|
// visible même quand le WMS échoue ensuite.
|
||||||
|
throw new Error(`Query failed for ${entityType}: ${error.message}${warning ? `\nWarning: ${warning}` : ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,11 +182,17 @@ async function getEntitySchema(entityType) {
|
|||||||
* Count entities with optional filter
|
* Count entities with optional filter
|
||||||
* @param {string} entityType - Entity type
|
* @param {string} entityType - Entity type
|
||||||
* @param {string|null} filter - Optional filter
|
* @param {string|null} filter - Optional filter
|
||||||
|
* @param {number} queryType - QueryContextType (0 = Reading par défaut, D25)
|
||||||
*/
|
*/
|
||||||
async function countEntities(entityType, filter = null) {
|
async function countEntities(entityType, filter = null, queryType = 0) {
|
||||||
|
// Garde de valeur avant tout réseau (D25).
|
||||||
|
queryType = assertValidQueryType(queryType);
|
||||||
|
|
||||||
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
||||||
// sur nom inconnu.
|
// sur nom inconnu, sauf en query_type != 0 (passage tel quel + warning, D25).
|
||||||
const { tableName, warning } = await entityResolver.resolveEntityType(entityType);
|
const { tableName, warning } = await entityResolver.resolveEntityType(entityType, {
|
||||||
|
allowUnknown: queryType !== 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build: Context.Entity.Where(...).Count()
|
// Build: Context.Entity.Where(...).Count()
|
||||||
@@ -165,20 +204,21 @@ async function countEntities(entityType, filter = null) {
|
|||||||
parts.push('Count()');
|
parts.push('Count()');
|
||||||
const fullExpression = parts.join('.');
|
const fullExpression = parts.join('.');
|
||||||
|
|
||||||
console.error(`[WMSQuery] Counting ${entityType}: ${fullExpression}`);
|
console.error(`[WMSQuery] Counting ${entityType}: ${fullExpression} | queryType=${queryType}`);
|
||||||
|
|
||||||
const count = await apiService.executeScalarQuery(fullExpression);
|
const count = await apiService.executeScalarQuery(fullExpression, { queryType });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entityType,
|
entityType,
|
||||||
resolvedTableName: tableName,
|
resolvedTableName: tableName,
|
||||||
...(warning ? { warning } : {}),
|
...(warning ? { warning } : {}),
|
||||||
|
...(queryType !== 0 ? { queryType } : {}),
|
||||||
filter,
|
filter,
|
||||||
count
|
count
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[WMSQuery] Count failed:`, error.message);
|
console.error(`[WMSQuery] Count failed:`, error.message);
|
||||||
throw new Error(`Count failed for ${entityType}: ${error.message}`);
|
throw new Error(`Count failed for ${entityType}: ${error.message}${warning ? `\nWarning: ${warning}` : ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,4 +228,5 @@ module.exports = {
|
|||||||
searchEntities,
|
searchEntities,
|
||||||
getEntitySchema,
|
getEntitySchema,
|
||||||
countEntities,
|
countEntities,
|
||||||
|
assertValidQueryType,
|
||||||
};
|
};
|
||||||
|
|||||||
+23
-3
@@ -1,5 +1,6 @@
|
|||||||
const apiService = require('../services/api-service').getInstance();
|
const apiService = require('../services/api-service').getInstance();
|
||||||
const entityResolver = require('../services/entity-resolver');
|
const entityResolver = require('../services/entity-resolver');
|
||||||
|
const { assertValidQueryType } = require('../services/wms-query-service');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tools MCP pour interagir avec les APIs WMS
|
* Tools MCP pour interagir avec les APIs WMS
|
||||||
@@ -36,6 +37,11 @@ function listTools() {
|
|||||||
description: 'Limite de résultats (défaut: 100)',
|
description: 'Limite de résultats (défaut: 100)',
|
||||||
default: 100,
|
default: 100,
|
||||||
},
|
},
|
||||||
|
query_type: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'QueryContextType (défaut: 0 = Reading — statuts en chaînes, à garder sauf raison explicite). Opt-in : 1 = Writing (statuts en ÉNUMÉRATIONS — les comparaisons de chaînes comme == "Release" ÉCHOUENT), 2 = DataWarehouse (souvent non configuré), 3 = Metrics (modèle de données distinct). En query_type != 0, un nom d\'entité inconnu du Metadata Reading est transmis tel quel avec un warning.',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['entity_type'],
|
required: ['entity_type'],
|
||||||
},
|
},
|
||||||
@@ -85,12 +91,23 @@ async function executeTool(name, args) {
|
|||||||
* Tool: call_query_api
|
* Tool: call_query_api
|
||||||
*/
|
*/
|
||||||
async function callQueryAPI(args) {
|
async function callQueryAPI(args) {
|
||||||
const { entity_type, expression = 'z => z', filter, limit = 100 } = args;
|
const { entity_type, expression = 'z => z', filter, limit = 100, query_type = 0 } = args;
|
||||||
|
|
||||||
|
// Conservé hors du try : si la requête échoue ensuite côté WMS, le warning
|
||||||
|
// de résolution (nom hors Reading) reste dans la réponse d'erreur.
|
||||||
|
let resolution = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Garde de valeur avant tout réseau (D25) — le wrapper D23 ne valide pas
|
||||||
|
// les valeurs.
|
||||||
|
const queryType = assertValidQueryType(query_type);
|
||||||
|
|
||||||
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
// Résolution Name/TableName -> TableName (D21) — échec avant appel réseau
|
||||||
// sur nom inconnu.
|
// sur nom inconnu, sauf en query_type != 0 (passage tel quel + warning, D25).
|
||||||
const { tableName, warning } = await entityResolver.resolveEntityType(entity_type);
|
resolution = await entityResolver.resolveEntityType(entity_type, {
|
||||||
|
allowUnknown: queryType !== 0,
|
||||||
|
});
|
||||||
|
const { tableName, warning } = resolution;
|
||||||
|
|
||||||
// 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.${tableName}`;
|
let linqExpression = `Context.${tableName}`;
|
||||||
@@ -103,6 +120,7 @@ async function callQueryAPI(args) {
|
|||||||
const result = await apiService.executeQuery(linqExpression, {
|
const result = await apiService.executeQuery(linqExpression, {
|
||||||
take: limit || undefined,
|
take: limit || undefined,
|
||||||
select: expression !== 'z => z' ? expression : undefined,
|
select: expression !== 'z => z' ? expression : undefined,
|
||||||
|
queryType,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -115,6 +133,7 @@ async function callQueryAPI(args) {
|
|||||||
entityType: entity_type,
|
entityType: entity_type,
|
||||||
resolvedTableName: tableName,
|
resolvedTableName: tableName,
|
||||||
...(warning ? { warning } : {}),
|
...(warning ? { warning } : {}),
|
||||||
|
...(queryType !== 0 ? { queryType } : {}),
|
||||||
result,
|
result,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
@@ -132,6 +151,7 @@ async function callQueryAPI(args) {
|
|||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: err.message,
|
error: err.message,
|
||||||
|
...(resolution?.warning ? { warning: resolution.warning } : {}),
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2
|
2
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ 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 by default — status fields are STRINGS (enum names, not integers). Other contexts via query_type (opt-in, see the parameter warning).
|
||||||
Common entities: Products, Containers, Tasks, Stocks, ProductLocations, Locations, InboundOrders, OutboundOrders, Receptions, Accounts, Suppliers, Kits, Alias. Full list via get_entity_metadata.
|
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.
|
entity_type accepts the AD entity name (Container) or the TableName (Containers), case-insensitive — resolved via the Metadata API.
|
||||||
|
|
||||||
@@ -44,6 +44,11 @@ Never guess enum string values — they differ between Reading and Writing model
|
|||||||
description: 'Maximum results to return (default: 100, max: 1000)',
|
description: 'Maximum results to return (default: 100, max: 1000)',
|
||||||
default: 100,
|
default: 100,
|
||||||
},
|
},
|
||||||
|
query_type: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'QueryContextType (default: 0 = Reading — status fields are strings, keep it unless you know why). Opt-in: 1 = Writing (status fields become ENUMS — string comparisons like == "Release" FAIL), 2 = DataWarehouse (often not configured), 3 = Metrics (different data model). Entity names unknown to the Reading metadata are passed through as-is with a warning when query_type != 0.',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['entity_type'],
|
required: ['entity_type'],
|
||||||
},
|
},
|
||||||
@@ -96,6 +101,11 @@ Verified values (curl-tested):
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'Optional LINQ filter condition. Status fields are strings (enum names from Reading model). Always verify enum values via docs://entities/ before use.',
|
description: 'Optional LINQ filter condition. Status fields are strings (enum names from Reading model). Always verify enum values via docs://entities/ before use.',
|
||||||
},
|
},
|
||||||
|
query_type: {
|
||||||
|
type: 'number',
|
||||||
|
description: 'QueryContextType (default: 0 = Reading — status fields are strings, keep it unless you know why). Opt-in: 1 = Writing (status fields become ENUMS — string comparisons like == "Release" FAIL), 2 = DataWarehouse (often not configured), 3 = Metrics (different data model). Entity names unknown to the Reading metadata are passed through as-is with a warning when query_type != 0.',
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: ['entity_type'],
|
required: ['entity_type'],
|
||||||
},
|
},
|
||||||
@@ -169,15 +179,16 @@ async function executeTool(name, args) {
|
|||||||
* Tool: query_wms_entities
|
* Tool: query_wms_entities
|
||||||
*/
|
*/
|
||||||
async function queryWmsEntities(args) {
|
async function queryWmsEntities(args) {
|
||||||
const { entity_type, select_expression = 'z => z', filter, limit = 100 } = args;
|
const { entity_type, select_expression = 'z => z', filter, limit = 100, query_type = 0 } = args;
|
||||||
|
|
||||||
console.error(`[WMSQueryTools] Querying ${entity_type}: limit=${limit}`);
|
console.error(`[WMSQueryTools] Querying ${entity_type}: limit=${limit} query_type=${query_type}`);
|
||||||
|
|
||||||
const result = await wmsQueryService.queryEntities(
|
const result = await wmsQueryService.queryEntities(
|
||||||
entity_type,
|
entity_type,
|
||||||
select_expression,
|
select_expression,
|
||||||
filter,
|
filter,
|
||||||
limit
|
limit,
|
||||||
|
query_type
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -195,11 +206,11 @@ async function queryWmsEntities(args) {
|
|||||||
* Tool: count_wms_entities
|
* Tool: count_wms_entities
|
||||||
*/
|
*/
|
||||||
async function countWmsEntities(args) {
|
async function countWmsEntities(args) {
|
||||||
const { entity_type, filter } = args;
|
const { entity_type, filter, query_type = 0 } = args;
|
||||||
|
|
||||||
console.error(`[WMSQueryTools] Counting ${entity_type}${filter ? ` where ${filter}` : ''}`);
|
console.error(`[WMSQueryTools] Counting ${entity_type}${filter ? ` where ${filter}` : ''} query_type=${query_type}`);
|
||||||
|
|
||||||
const result = await wmsQueryService.countEntities(entity_type, filter || null);
|
const result = await wmsQueryService.countEntities(entity_type, filter || null, query_type);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [{
|
content: [{
|
||||||
|
|||||||
Reference in New Issue
Block a user