cb625a7918
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>
192 lines
6.0 KiB
JavaScript
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,
|
|
};
|