mahdahar dc6cca71cf feat: move instrument onboarding to YAML config
Replace DB-backed instrument upserts with app.yaml-driven config loading, matching, and translator application in the ingestion workflow. Also add serial-port connector support, startup validation tooling, and migration tracking updates to keep runtime behavior and docs aligned.
2026-04-06 16:50:17 +07:00

153 lines
4.6 KiB
JavaScript

const fs = require('fs');
const config = require('../../config/app');
function normalizeConnectorType(type) {
const value = String(type || '').trim().toLowerCase();
if (value === 'serial' || value === 'astm-serial') return 'astm-serial';
if (value === 'tcp-server' || value === 'hl7-tcp') return 'hl7-tcp';
if (value === 'http-json' || value === 'http') return 'http-json';
if (value === 'tcp-client') return 'tcp-client';
return '';
}
function defaultParserForConnector(connector) {
if (connector === 'astm-serial') return 'astm';
if (connector === 'hl7-tcp') return 'hl7';
if (connector === 'http-json') return 'http-json';
return '';
}
function toEntityRows(entities = []) {
return entities.map((entity) => {
const connector = entity.connector && typeof entity.connector === 'object' ? entity.connector : {};
return {
instrument_id: entity.instrument_id,
enabled: entity.enabled,
connector: connector.type,
connectorConfig: connector,
config: entity.config,
translator: entity.translator
};
});
}
class InstrumentConfigValidationError extends Error {
constructor(errors) {
super(`instrument config validation failed with ${errors.length} issue(s)`);
this.name = 'InstrumentConfigValidationError';
this.errors = errors;
}
}
function validateAndLoadInstrumentConfigs({
instruments = config.instrumentEntities?.length ? toEntityRows(config.instrumentEntities) : config.instruments,
configFilePath = config.configPath
} = {}) {
const errors = [];
const entries = [];
const instrumentIds = new Set();
if (configFilePath && !fs.existsSync(configFilePath)) {
errors.push(`config file not found: ${configFilePath}`);
throw new InstrumentConfigValidationError(errors);
}
if (!Array.isArray(instruments)) {
errors.push('config.instruments: expected an array');
throw new InstrumentConfigValidationError(errors);
}
if (!instruments.length) {
errors.push('config.instruments: array cannot be empty');
}
for (let index = 0; index < instruments.length; index += 1) {
const item = instruments[index];
const label = `instrument[${index}]`;
if (!item || typeof item !== 'object' || Array.isArray(item)) {
errors.push(`${label}: must be an object`);
continue;
}
const instrumentId = item.instrument_id;
const connector = normalizeConnectorType(item.connector);
const translator = item.translator;
if (!instrumentId || typeof instrumentId !== 'string') {
errors.push(`${label}: instrument_id is required`);
continue;
}
if (instrumentIds.has(instrumentId)) {
errors.push(`${label}: duplicate instrument_id "${instrumentId}"`);
continue;
}
instrumentIds.add(instrumentId);
if (!connector) {
errors.push(`${label}: connector.type is required`);
continue;
}
if (connector === 'tcp-client') {
errors.push(`${label}: connector.type tcp-client is not supported yet`);
continue;
}
if (!translator || typeof translator !== 'object' || Array.isArray(translator)) {
item.translator = { parser: defaultParserForConnector(connector) };
}
const resolvedTranslator = item.translator;
if (!resolvedTranslator.parser || typeof resolvedTranslator.parser !== 'string') {
errors.push(`${label}: translator.parser is required`);
continue;
}
const connectorConfig = item.connectorConfig && typeof item.connectorConfig === 'object'
? item.connectorConfig
: {};
const match = item.match && typeof item.match === 'object' ? { ...item.match } : {};
if (connector === 'astm-serial') {
const comPort = connectorConfig.port || connectorConfig.comPort;
if (!comPort || typeof comPort !== 'string') {
errors.push(`${label}: connector.port is required for serial`);
continue;
}
match.comPort = comPort;
}
if (connector === 'hl7-tcp' || connector === 'http-json') {
const localPort = connectorConfig.port || connectorConfig.localPort;
if (localPort !== undefined && localPort !== null && localPort !== '') {
match.localPort = Number(localPort);
}
}
entries.push({
instrument_id: instrumentId,
connector,
enabled: item.enabled !== false,
match,
config: item.config || {},
translator: resolvedTranslator,
connectorConfig,
files: {
config: configFilePath
}
});
}
if (errors.length) {
throw new InstrumentConfigValidationError(errors);
}
return entries;
}
module.exports = {
InstrumentConfigValidationError,
validateAndLoadInstrumentConfigs
};