Files
mcp-wms-api/src/services/wms-query-service.js
T
Arthur Ria cb625a7918 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>
2026-08-24 17:28:27 +02:00

192 lines
6.0 KiB
JavaScript

/**
* WMS Query Service
* Helper functions for building and executing LINQ queries via WMS API
*/
const apiService = require('./api-service').getInstance();
const entityResolver = require('./entity-resolver');
/**
* Build a LINQ select expression
* @param {string} entityType - Entity type (Products, Containers, etc.)
* @param {string} selectFields - Fields to select (e.g., "z => new { z.Id, z.Name }")
* @param {string|null} filter - Optional filter (e.g., "z.Status == 'Active'")
* @param {number|null} limit - Optional limit
*/
/**
* Query WMS entities with LINQ expression
* @param {string} entityType - Entity type
* @param {string} selectExpression - LINQ select expression
* @param {string|null} filter - Optional filter
* @param {number} limit - Result limit
*/
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 {
// Enforce max limit
const maxLimit = parseInt(process.env.MAX_QUERY_ROWS) || 1000;
const actualLimit = Math.min(limit, maxLimit);
// Build expression: Context + optional Where + OrderBy (required by EF when Take is used)
let expression = `Context.${tableName}`;
if (filter) {
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
expression += `.Where(${whereExpr})`;
}
// 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}`);
const result = await apiService.executeQuery(expression, {
take: actualLimit,
select: selectExpression !== 'z => z' ? selectExpression : undefined,
});
return {
entityType,
resolvedTableName: tableName,
...(warning ? { warning } : {}),
expression,
limit: actualLimit,
count: Array.isArray(result) ? result.length : 0,
data: result
};
} catch (error) {
console.error(`[WMSQuery] Query failed:`, error.message);
throw new Error(`Query failed for ${entityType}: ${error.message}`);
}
}
/**
* Query entity by ID
* @param {string} entityType - Entity type
* @param {string|number} id - Entity ID
*/
async function queryById(entityType, id) {
const filter = `z => z.Id == ${id}`;
const result = await queryEntities(entityType, 'z => z', filter, 1);
if (!result.data || result.data.length === 0) {
throw new Error(`${entityType} with ID ${id} not found`);
}
return result.data[0];
}
/**
* Search entities by keyword in common fields
* @param {string} entityType - Entity type
* @param {string} keyword - Search keyword
* @param {number} limit - Result limit
*/
async function searchEntities(entityType, keyword, limit = 50) {
// Common searchable fields by entity type
const searchFields = {
'Products': ['Code', 'Description', 'Reference'],
'Containers': ['Code', 'Reference'],
'Tasks': ['Code', 'Reference'],
'InboundOrders': ['Code', 'Reference'],
'OutboundOrders': ['Code', 'Reference'],
'Accounts': ['Code', 'Name'],
'Suppliers': ['Code', 'Name']
};
const fields = searchFields[entityType] || ['Code'];
// Build filter for multiple fields (OR condition)
const conditions = fields.map(field => `z.${field}.Contains("${keyword}")`).join(' || ');
const filter = `z => ${conditions}`;
try {
return await queryEntities(entityType, 'z => z', filter, limit);
} catch (error) {
// If Contains doesn't work, try simpler approach
console.error(`[WMSQuery] Search failed, trying alternative method:`, error.message);
return await queryEntities(entityType, 'z => z', null, limit);
}
}
/**
* Get entity schema information (example entity to show available fields)
* @param {string} entityType - Entity type
*/
async function getEntitySchema(entityType) {
try {
// Query one entity to see its structure
const result = await queryEntities(entityType, 'z => z', null, 1);
if (!result.data || result.data.length === 0) {
return {
entityType,
fields: [],
message: `No ${entityType} found to inspect schema`
};
}
const sample = result.data[0];
const fields = Object.keys(sample).map(key => ({
name: key,
type: typeof sample[key],
sample: sample[key]
}));
return {
entityType,
fields,
totalFields: fields.length
};
} catch (error) {
console.error(`[WMSQuery] Failed to get schema for ${entityType}:`, error.message);
throw error;
}
}
/**
* Count entities with optional filter
* @param {string} entityType - Entity type
* @param {string|null} filter - Optional filter
*/
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 {
// Build: Context.Entity.Where(...).Count()
const parts = [`Context.${tableName}`];
if (filter) {
const whereExpr = /^\s*\w+\s*=>/.test(filter) ? filter : `z => ${filter}`;
parts.push(`Where(${whereExpr})`);
}
parts.push('Count()');
const fullExpression = parts.join('.');
console.error(`[WMSQuery] Counting ${entityType}: ${fullExpression}`);
const count = await apiService.executeScalarQuery(fullExpression);
return {
entityType,
resolvedTableName: tableName,
...(warning ? { warning } : {}),
filter,
count
};
} catch (error) {
console.error(`[WMSQuery] Count failed:`, error.message);
throw new Error(`Count failed for ${entityType}: ${error.message}`);
}
}
module.exports = {
queryEntities,
queryById,
searchEntities,
getEntitySchema,
countEntities,
};