Move middleware sources into core/, refresh config paths, and update design/user docs to reflect the raw payload pipeline.
196 lines
5.8 KiB
JavaScript
196 lines
5.8 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const mapCache = new Map();
|
|
|
|
function buildCanonical(entry, parsedPayload, connector) {
|
|
const translator = entry && typeof entry.translator === 'object' ? entry.translator : {};
|
|
const canonical = { ...parsedPayload };
|
|
if (translator.forceInstrumentId !== false) {
|
|
canonical.instrument_id = entry.instrument_id;
|
|
}
|
|
canonical.meta = {
|
|
...(parsedPayload.meta || {}),
|
|
...(translator.meta && typeof translator.meta === 'object' ? translator.meta : {}),
|
|
connector,
|
|
instrument_config: entry.config
|
|
};
|
|
return canonical;
|
|
}
|
|
|
|
function resolveTranslatorFilePath(filePath, configFilePath) {
|
|
if (!filePath || typeof filePath !== 'string') return '';
|
|
if (path.isAbsolute(filePath)) return filePath;
|
|
|
|
const candidates = [
|
|
path.resolve(process.cwd(), filePath)
|
|
];
|
|
|
|
if (configFilePath) {
|
|
candidates.push(path.resolve(path.dirname(configFilePath), filePath));
|
|
}
|
|
|
|
const matched = candidates.find((candidate) => fs.existsSync(candidate));
|
|
return matched || candidates[0];
|
|
}
|
|
|
|
function parseMapFile(fileContent, filePath) {
|
|
const lines = fileContent.split(/\r?\n/);
|
|
const rows = new Map();
|
|
|
|
lines.forEach((line, index) => {
|
|
const trimmed = line.trim();
|
|
if (!trimmed || trimmed.startsWith('#')) return;
|
|
|
|
const separator = line.indexOf('=');
|
|
if (separator < 0) {
|
|
throw new Error(`${filePath}:${index + 1} invalid mapping line (expected KEY = value)`);
|
|
}
|
|
|
|
const key = line.slice(0, separator).trim();
|
|
const value = line.slice(separator + 1).trim();
|
|
|
|
if (!key) {
|
|
throw new Error(`${filePath}:${index + 1} mapping key is required`);
|
|
}
|
|
|
|
rows.set(key, value);
|
|
});
|
|
|
|
return rows;
|
|
}
|
|
|
|
function loadMapFile(filePath) {
|
|
const stat = fs.statSync(filePath);
|
|
const cached = mapCache.get(filePath);
|
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
return cached.rows;
|
|
}
|
|
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const rows = parseMapFile(content, filePath);
|
|
mapCache.set(filePath, { mtimeMs: stat.mtimeMs, rows });
|
|
return rows;
|
|
}
|
|
|
|
function getPlaceholderValue(name, context) {
|
|
if (Object.hasOwn(context.flat, name)) {
|
|
return context.flat[name];
|
|
}
|
|
|
|
if (!name.includes('.')) {
|
|
return '';
|
|
}
|
|
|
|
const parts = name.split('.').filter(Boolean);
|
|
let current = context.root;
|
|
|
|
for (let i = 0; i < parts.length; i += 1) {
|
|
if (!current || typeof current !== 'object') return '';
|
|
current = current[parts[i]];
|
|
}
|
|
|
|
return current === undefined || current === null ? '' : current;
|
|
}
|
|
|
|
function renderTemplate(template, context) {
|
|
return String(template).replace(/\{([^{}]+)\}/g, (_, rawName) => {
|
|
const name = String(rawName || '').trim();
|
|
if (!name) return '';
|
|
const value = getPlaceholderValue(name, context);
|
|
return value === undefined || value === null ? '' : String(value);
|
|
});
|
|
}
|
|
|
|
function buildTemplateContext(entry, parsedPayload, connector) {
|
|
const root = {
|
|
...parsedPayload,
|
|
instrument_id: parsedPayload.instrument_id || entry.instrument_id,
|
|
connector,
|
|
config: entry.config || {},
|
|
meta: parsedPayload.meta || {}
|
|
};
|
|
|
|
const flat = {
|
|
...root,
|
|
...(root.meta && typeof root.meta === 'object' ? root.meta : {}),
|
|
...(root.config && typeof root.config === 'object' ? root.config : {})
|
|
};
|
|
|
|
if (Array.isArray(parsedPayload.results)) {
|
|
flat.order_tests = parsedPayload.results
|
|
.map((item) => item && item.test_code)
|
|
.filter(Boolean)
|
|
.map((testCode) => `^^^${testCode}`)
|
|
.join('\\');
|
|
}
|
|
|
|
return { root, flat };
|
|
}
|
|
|
|
function translateOverrides(entry, parsedPayload, connector) {
|
|
const translator = entry && typeof entry.translator === 'object' ? entry.translator : {};
|
|
const overrides = translator.overrides && typeof translator.overrides === 'object'
|
|
? translator.overrides
|
|
: {};
|
|
const canonical = buildCanonical(entry, { ...parsedPayload, ...overrides }, connector);
|
|
return canonical;
|
|
}
|
|
|
|
function translateTemplate(entry, parsedPayload, connector) {
|
|
const translator = entry && typeof entry.translator === 'object' ? entry.translator : {};
|
|
if (!translator.file || typeof translator.file !== 'string') {
|
|
throw new Error('translator.file is required for template engine');
|
|
}
|
|
|
|
const resolvedFilePath = resolveTranslatorFilePath(translator.file, entry?.files?.config);
|
|
if (!fs.existsSync(resolvedFilePath)) {
|
|
throw new Error(`translator file not found: ${translator.file}`);
|
|
}
|
|
|
|
const mapRows = loadMapFile(resolvedFilePath);
|
|
const messageKeys = Array.isArray(translator.messages) && translator.messages.length
|
|
? translator.messages.map((value) => String(value))
|
|
: Array.from(mapRows.keys());
|
|
const context = buildTemplateContext(entry, parsedPayload, connector);
|
|
const renderedMessages = messageKeys.map((messageKey) => {
|
|
if (!mapRows.has(messageKey)) {
|
|
throw new Error(`translator message key not found in map file: ${messageKey}`);
|
|
}
|
|
return {
|
|
key: messageKey,
|
|
body: renderTemplate(mapRows.get(messageKey), context)
|
|
};
|
|
});
|
|
|
|
const canonical = buildCanonical(entry, parsedPayload, connector);
|
|
canonical.meta.rendered_messages = renderedMessages;
|
|
canonical.meta.translator_file = resolvedFilePath;
|
|
return canonical;
|
|
}
|
|
|
|
const registry = new Map([
|
|
['overrides', { translate: translateOverrides }],
|
|
['template', { translate: translateTemplate }]
|
|
]);
|
|
|
|
function resolve(name) {
|
|
if (!name) return registry.get('overrides');
|
|
const key = String(name).trim().toLowerCase();
|
|
return registry.get(key) || null;
|
|
}
|
|
|
|
function translate(entry, parsedPayload, connector, engineName) {
|
|
const engine = resolve(engineName);
|
|
if (!engine) {
|
|
const options = engineName ? ` (requested: ${engineName})` : '';
|
|
throw new Error(`translator engine not found${options}`);
|
|
}
|
|
return engine.translate(entry, parsedPayload, connector);
|
|
}
|
|
|
|
module.exports = {
|
|
resolve,
|
|
translate
|
|
};
|