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:
Arthur Ria
2026-08-25 11:17:10 +02:00
parent 88dab289cc
commit 706e628715
9 changed files with 171 additions and 49 deletions
+10 -5
View File
@@ -323,14 +323,17 @@ class APIService {
* 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 {object} options - { take, skip, select, orderBy, inlineCount }
* @param {object} options - { take, skip, select, orderBy, inlineCount, queryType }
*/
async executeQuery(expression, options = {}) {
const { take, skip, select, orderBy, inlineCount } = options;
const { take, skip, select, orderBy, inlineCount, queryType } = options;
const body = {
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,
};
@@ -363,11 +366,13 @@ class APIService {
* Execute a scalar LINQ query (Count, Sum, etc.) via QueryScalarExecute.
* Returns the scalar value directly.
* @param {string} fullExpression - e.g. "Context.OutboundOrders.Where(...).Count()"
* @param {object} options - { queryType }
*/
async executeScalarQuery(fullExpression) {
async executeScalarQuery(fullExpression, options = {}) {
const body = {
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,
};
+19 -3
View File
@@ -119,15 +119,22 @@ function suggestClosest(input, limit = 5) {
* Résout un nom d'entité vers son TableName.
*
* @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}>}
* - 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
* @throws {Error} nom inconnu du modèle Reading (sauf allowUnknown) — AVANT
* 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() === '') {
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 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(
`"${trimmed}" n'existe pas dans le modèle Reading.${closest} ` +
`${tableNames.length} entités disponibles — utilisez get_entity_metadata pour la liste.`
+51 -10
View File
@@ -6,6 +6,27 @@
const apiService = require('./api-service').getInstance();
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
* @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|null} filter - Optional filter
* @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,
// 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 {
// 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)
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, {
take: actualLimit,
select: selectExpression !== 'z => z' ? selectExpression : undefined,
queryType,
});
return {
entityType,
resolvedTableName: tableName,
...(warning ? { warning } : {}),
...(queryType !== 0 ? { queryType } : {}),
expression,
limit: actualLimit,
count: Array.isArray(result) ? result.length : 0,
@@ -57,7 +88,9 @@ async function queryEntities(entityType, selectExpression = 'z => z', filter = n
};
} catch (error) {
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
* @param {string} entityType - Entity type
* @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
// sur nom inconnu.
const { tableName, warning } = await entityResolver.resolveEntityType(entityType);
// sur nom inconnu, sauf en query_type != 0 (passage tel quel + warning, D25).
const { tableName, warning } = await entityResolver.resolveEntityType(entityType, {
allowUnknown: queryType !== 0,
});
try {
// Build: Context.Entity.Where(...).Count()
@@ -165,20 +204,21 @@ async function countEntities(entityType, filter = null) {
parts.push('Count()');
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 {
entityType,
resolvedTableName: tableName,
...(warning ? { warning } : {}),
...(queryType !== 0 ? { queryType } : {}),
filter,
count
};
} catch (error) {
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,
getEntitySchema,
countEntities,
assertValidQueryType,
};